From be8b5ef70adf60c707d1bc9cafda006e0baaa2ce Mon Sep 17 00:00:00 2001 From: KernelDeimos <7225168+KernelDeimos@users.noreply.github.com> Date: Tue, 6 Jan 2026 12:43:19 -0500 Subject: [PATCH] fix(backend): singleton in helpers for testability The `helpers.js` file has two instances when testing with vitest. Usually this can be solved by creating an "import bridge" file - another CJS file that imports the desired CJS file and re-exports it so that the `require` mechanism for caching modules is used instead of the `import` mechanism for caching modules. Unfortunately in my most recent test cases this does not work. I do not know why this does not work. However, without doing a deep-dive into the internals of vitest I can at least see that the only state in helpers is for providing `services` as a pseudo-global, so a hack with `Symbol.for` solves everything. --- src/backend/src/helpers.js | 81 +++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/src/backend/src/helpers.js b/src/backend/src/helpers.js index 4f0b611e4..d5313c329 100644 --- a/src/backend/src/helpers.js +++ b/src/backend/src/helpers.js @@ -29,10 +29,16 @@ const { NodeUIDSelector } = require('./filesystem/node/selectors'); const { object_returned_by_get_app } = require('./annotatedobjects.js'); const { kv } = require('./util/kvSingleton'); -let services = null; +const identifying_uuid = require('uuid').v4(); + +// Use global singleton for services to handle ESM/CJS dual-loading in vitest +const SERVICES_KEY = Symbol.for('puter.helpers.services'); +globalThis[SERVICES_KEY] = globalThis[SERVICES_KEY] ?? { services: null }; +const _servicesHolder = globalThis[SERVICES_KEY]; + const tmp_provide_services = async ss => { - services = ss; - await services.ready; + _servicesHolder.services = ss; + await _servicesHolder.services.ready; }; // TTL for pending get_app queries (request coalescing) @@ -40,7 +46,7 @@ const PENDING_QUERY_TTL = 10; // seconds async function is_empty (dir_uuid) { /** @type BaseDatabaseAccessService */ - const db = services.get('database').get(DB_READ, 'filesystem'); + const db = _servicesHolder.services.get('database').get(DB_READ, 'filesystem'); let rows; @@ -98,7 +104,7 @@ async function is_shared_with_anyone (fsentry_id) { * @returns {boolean} */ async function is_temp_users_disabled () { - const svc_feature_flag = await services.get('feature-flag'); + const svc_feature_flag = await _servicesHolder.services.get('feature-flag'); return await svc_feature_flag.check('temp-users-disabled'); } @@ -107,7 +113,7 @@ async function is_temp_users_disabled () { * @returns {boolean} */ async function is_user_signup_disabled () { - const svc_feature_flag = await services.get('feature-flag'); + const svc_feature_flag = await _servicesHolder.services.get('feature-flag'); return await svc_feature_flag.check('user-signup-disabled'); } @@ -187,7 +193,7 @@ function validate_fsentry_name (name) { */ async function id2uuid (id) { /** @type BaseDatabaseAccessService */ - const db = services.get('database').get(DB_READ, 'filesystem'); + const db = _servicesHolder.services.get('database').get(DB_READ, 'filesystem'); let fsentry = await db.requireRead('SELECT `uuid`, immutable FROM `fsentries` WHERE `id` = ? LIMIT 1', [id]); @@ -209,7 +215,7 @@ async function id2uuid (id) { */ async function df (user_id) { /** @type BaseDatabaseAccessService */ - const db = services.get('database').get(DB_READ, 'filesystem'); + const db = _servicesHolder.services.get('database').get(DB_READ, 'filesystem'); const fsentry = await db.read('SELECT SUM(size) AS total FROM `fsentries` WHERE `user_id` = ? LIMIT 1', [user_id]); if ( !fsentry[0] || !fsentry[0].total ) @@ -232,7 +238,7 @@ async function df (user_id) { * @returns {Promise} */ async function get_user (options) { - return await services.get('get-user').get_user(options); + return await _servicesHolder.services.get('get-user').get_user(options); } /** @@ -269,9 +275,9 @@ async function refresh_apps_cache (options, override) { async function refresh_associations_cache () { /** @type BaseDatabaseAccessService */ - const db = services.get('database').get(DB_READ, 'apps'); + const db = _servicesHolder.services.get('database').get(DB_READ, 'apps'); - const log = services.get('log-service').create('helpers.js'); + const log = _servicesHolder.services.get('log-service').create('helpers.js'); log.tick('refresh file associations'); const associations = await db.read('SELECT * FROM app_filetype_association'); const lists = {}; @@ -306,11 +312,11 @@ async function get_app (options) { kv.set(`apps:id:${app.id}`, app, { EX: 30 }); }; - const log = services.get('log-service').create('get_app'); + const log = _servicesHolder.services.get('log-service').create('get_app'); // This condition should be updated if the code below is re-ordered. if ( options.follow_old_names && !options.uid && options.name ) { - const svc_oldAppName = services.get('old-app-name'); + const svc_oldAppName = _servicesHolder.services.get('old-app-name'); const old_name = await svc_oldAppName.check_app_name(options.name); if ( old_name ) { options.uid = old_name.app_uid; @@ -367,7 +373,7 @@ async function get_app (options) { try { /** @type BaseDatabaseAccessService */ - const db = services.get('database').get(DB_READ, 'apps'); + const db = _servicesHolder.services.get('database').get(DB_READ, 'apps'); if ( options.uid ) { app = (await db.read('SELECT * FROM `apps` WHERE `uid` = ? LIMIT 1', [options.uid]))[0]; @@ -403,7 +409,7 @@ async function get_app (options) { */ async function app_exists (options) { /** @type BaseDatabaseAccessService */ - const db = services.get('database').get(DB_READ, 'apps'); + const db = _servicesHolder.services.get('database').get(DB_READ, 'apps'); let app; if ( options.uid ) @@ -430,7 +436,7 @@ async function app_exists (options) { */ async function change_username (user_id, new_username) { /** @type BaseDatabaseAccessService */ - const db = services.get('database').get(DB_WRITE, 'auth'); + const db = _servicesHolder.services.get('database').get(DB_WRITE, 'auth'); const old_username = (await get_user({ id: user_id })).username; @@ -442,7 +448,7 @@ async function change_username (user_id, new_username) { [new_username, `/${ new_username}`, user_id]); console.log(`User ${old_username} changed username to ${new_username}`); - await services.get('filesystem').update_child_paths(`/${old_username}`, `/${new_username}`, user_id); + await _servicesHolder.services.get('filesystem').update_child_paths(`/${old_username}`, `/${new_username}`, user_id); invalidate_cached_user_by_id(user_id); } @@ -456,7 +462,7 @@ async function change_username (user_id, new_username) { */ async function uuid2fsentry (uuid, return_thumbnail) { /** @type BaseDatabaseAccessService */ - const db = services.get('database').get(DB_READ, 'filesystem'); + const db = _servicesHolder.services.get('database').get(DB_READ, 'filesystem'); // todo optim, check if uuid is not exactly 36 characters long, if not it's invalid // and we can avoid one unnecessary DB lookup @@ -504,7 +510,7 @@ async function uuid2fsentry (uuid, return_thumbnail) { */ async function id2fsentry (id, return_thumbnail) { /** @type BaseDatabaseAccessService */ - const db = services.get('database').get(DB_READ, 'filesystem'); + const db = _servicesHolder.services.get('database').get(DB_READ, 'filesystem'); // todo optim, check if uuid is not exactly 36 characters long, if not it's invalid // and we can avoid one unnecessary DB lookup @@ -580,7 +586,7 @@ async function convert_path_to_fsentry (path) { let result; /** @type BaseDatabaseAccessService */ - const db = services.get('database').get(DB_READ, 'filesystem'); + const db = _servicesHolder.services.get('database').get(DB_READ, 'filesystem'); // Try stored path first result = await db.read('SELECT * FROM fsentries WHERE path=? LIMIT 1', @@ -645,7 +651,7 @@ function byte_format (bytes) { * @returns */ async function getDescendantsHelper (path, user, depth, return_thumbnail = false) { - const log = services.get('log-service').create('get_descendants'); + const log = _servicesHolder.services.get('log-service').create('get_descendants'); log.called(); // decrement depth if it's set @@ -668,7 +674,7 @@ async function getDescendantsHelper (path, user, depth, return_thumbnail = false } /** @type BaseDatabaseAccessService */ - const db = services.get('database').get(DB_READ, 'filesystem'); + const db = _servicesHolder.services.get('database').get(DB_READ, 'filesystem'); // ------------------------------------- // parent is root ('/') @@ -823,7 +829,7 @@ async function getDescendantsHelper (path, user, depth, return_thumbnail = false }; async function get_descendants (...args) { - const tracer = services.get('traceService').tracer; + const tracer = _servicesHolder.services.get('traceService').tracer; let ret; await tracer.startActiveSpan('get_descendants', async span => { ret = await getDescendantsHelper(...args); @@ -855,12 +861,12 @@ async function id2path (entry_uid) { } /** @type BaseDatabaseAccessService */ - const db = services.get('database').get(DB_READ, 'filesystem'); + const db = _servicesHolder.services.get('database').get(DB_READ, 'filesystem'); - const traces = services.get('traceService'); - const log = services.get('log-service').create('helpers.id2path'); + const traces = _servicesHolder.services.get('traceService'); + const log = _servicesHolder.services.get('log-service').create('helpers.id2path'); log.traceOn(); - const errors = services.get('error-service').create(log); + const errors = _servicesHolder.services.get('error-service').create(log); log.called(); let result; @@ -1018,7 +1024,7 @@ async function is_ancestor_of (ancestor_uid, descendant_uid) { } /** @type BaseDatabaseAccessService */ - const db = services.get('database').get(DB_READ, 'filesystem'); + const db = _servicesHolder.services.get('database').get(DB_READ, 'filesystem'); // root is an ancestor to all FSEntries if ( ancestor_uid === null ) @@ -1111,7 +1117,7 @@ async function gen_public_token (file_uuid, ttl = 24 * 60 * 60) { const contentType = mime.contentType(fsentry.name); /** @type BaseDatabaseAccessService */ - const db = services.get('database').get(DB_WRITE, 'filesystem'); + const db = _servicesHolder.services.get('database').get(DB_WRITE, 'filesystem'); // insert into DB try { @@ -1140,8 +1146,8 @@ async function gen_public_token (file_uuid, ttl = 24 * 60 * 60) { async function deleteUser (user_id) { /** @type BaseDatabaseAccessService */ - const db = services.get('database').get(DB_READ, 'filesystem'); - const svc_fs = services.get('filesystem'); + const db = _servicesHolder.services.get('database').get(DB_READ, 'filesystem'); + const svc_fs = _servicesHolder.services.get('filesystem'); // get a list of up to 5000 files owned by this user for ( let offset = 0; true; offset += 5000 ) { @@ -1243,7 +1249,7 @@ async function jwt_auth (req) { */ async function ancestors (fsentry_id) { /** @type BaseDatabaseAccessService */ - const db = services.get('database').get(DB_READ, 'filesystem'); + const db = _servicesHolder.services.get('database').get(DB_READ, 'filesystem'); const ancestors = []; // first parent @@ -1278,7 +1284,7 @@ function hyphenize_confirm_code (email_confirm_code) { async function username_exists (username) { /** @type BaseDatabaseAccessService */ - const db = services.get('database').get(DB_READ, 'filesystem'); + const db = _servicesHolder.services.get('database').get(DB_READ, 'filesystem'); let rows = await db.read('SELECT EXISTS(SELECT 1 FROM user WHERE username=?) AS username_exists', [username]); if ( rows[0].username_exists ) @@ -1289,7 +1295,7 @@ async function username_exists (username) { async function app_name_exists (name) { /** @type BaseDatabaseAccessService */ - const db = services.get('database').get(DB_READ, 'filesystem'); + const db = _servicesHolder.services.get('database').get(DB_READ, 'filesystem'); let rows = await db.read('SELECT EXISTS(SELECT 1 FROM apps WHERE apps.name=?) AS app_name_exists', [name]); if ( rows[0].app_name_exists ) @@ -1297,7 +1303,7 @@ async function app_name_exists (name) { return true; } - const svc_oldAppName = services.get('old-app-name'); + const svc_oldAppName = _servicesHolder.services.get('old-app-name'); const name_info = await svc_oldAppName.check_app_name(name); if ( name_info ) return true; } @@ -1536,7 +1542,7 @@ async function suggest_app_for_fsentry (fsentry, options) { async function get_taskbar_items (user, { icon_size, no_icons } = {}) { /** @type BaseDatabaseAccessService */ - const db = services.get('database').get(DB_WRITE, 'filesystem'); + const db = _servicesHolder.services.get('database').get(DB_WRITE, 'filesystem'); let taskbar_items_from_db = []; // If taskbar items don't exist (specifically NULL) @@ -1601,7 +1607,7 @@ async function get_taskbar_items (user, { icon_size, no_icons } = {}) { if ( no_icons ) { delete item.icon; } else { - const svc_appIcon = services.get('app-icon'); + const svc_appIcon = _servicesHolder.services.get('app-icon'); const icon_result = await svc_appIcon.get_icon_stream({ app_icon: item.icon, app_uid: item.uid, @@ -1767,4 +1773,5 @@ module.exports = { validate_fsentry_name, validate_signature_auth, tmp_provide_services, + identifying_uuid, };