From 60b22b503682941566e2b9acdc6864884d278b99 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Mon, 30 Mar 2026 15:25:30 +0200 Subject: [PATCH 1/8] feat(home): add SaaS readiness checklist + admin route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/admin/readiness (admin-only) returns startup checklist as JSON - validateSaaSReadiness() prints status table at boot (skipped in test) - 7 categories: Config, Security, Auth, Mail, Billing, Analytics, Monitoring - Smart dependencies: Resend configured → skip Nodemailer warning - Non-blocking: warnings only, never prevents startup --- lib/app.js | 20 ++++- lib/middlewares/policy.js | 1 + modules/home/controllers/home.controller.js | 15 ++++ modules/home/policies/home.policy.js | 3 + modules/home/routes/home.route.js | 2 + modules/home/services/home.service.js | 78 ++++++++++++++++++++ modules/home/tests/home.integration.tests.js | 46 ++++++++++++ 7 files changed, 163 insertions(+), 2 deletions(-) diff --git a/lib/app.js b/lib/app.js index 5a94a950e..9eb346727 100644 --- a/lib/app.js +++ b/lib/app.js @@ -89,7 +89,7 @@ const bootstrap = async () => { /** * log server configuration */ -const logConfiguration = () => { +const logConfiguration = async () => { // Create server URL const server = `${(config.secure && config.secure.credentials ? 'https://' : 'http://') + config.api.host}:${config.api.port}`; // Logging initialization @@ -99,6 +99,22 @@ const logConfiguration = () => { console.log(chalk.green(`Server: ${server}`)); console.log(chalk.green(`Database: ${config.db.uri}`)); if (config.cors.origin.length > 0) console.log(chalk.green(`Cors: ${config.cors.origin}`)); + + // SaaS readiness summary (skip in test to keep output clean) + if (process.env.NODE_ENV !== 'test') { + try { + const { default: HomeService } = await import('../modules/home/services/home.service.js'); + const checks = HomeService.getReadinessStatus(); + console.log(); + console.log(chalk.green('SaaS Readiness:')); + checks.forEach((c) => { + const icon = c.status === 'ok' ? chalk.green('OK') : chalk.yellow('WARN'); + console.log(` ${icon} ${c.category.padEnd(12)} ${c.message}`); + }); + } catch (_err) { + // Non-blocking — readiness check failure should not prevent boot + } + } }; // Boot up the server @@ -118,7 +134,7 @@ const start = async () => { if (config.secure && config.secure.credentials) http = await nodeHttps.createServer(config.secure.credentials, app).setTimeout(config.api.timeout).listen(config.api.port, config.api.host); else http = await nodeHttp.createServer(app).setTimeout(config.api.timeout).listen(config.api.port, config.api.host); - logConfiguration(); + await logConfiguration(); return { db, orm, diff --git a/lib/middlewares/policy.js b/lib/middlewares/policy.js index a585fc96a..f20ae8e66 100644 --- a/lib/middlewares/policy.js +++ b/lib/middlewares/policy.js @@ -201,6 +201,7 @@ const deriveSubjectType = (routePath) => { if (routePath.startsWith('/api/tasks')) return 'Task'; if (routePath.startsWith('/api/uploads')) return 'Upload'; if (routePath.startsWith('/api/home')) return 'Home'; + if (routePath === '/api/admin/readiness') return 'Readiness'; if (routePath.startsWith('/api/admin/organizations')) return 'Organization'; if (routePath.includes('/requests')) return 'Membership'; if (routePath.includes('/members')) return 'Membership'; diff --git a/modules/home/controllers/home.controller.js b/modules/home/controllers/home.controller.js index dae22cf64..afc5bc900 100644 --- a/modules/home/controllers/home.controller.js +++ b/modules/home/controllers/home.controller.js @@ -96,6 +96,20 @@ const health = (req, res) => { responses.success(res, 'health check')(payload); }; +/** + * @desc Endpoint to return SaaS readiness checks (admin only) + * @param {Object} req - Express request object + * @param {Object} res - Express response object + */ +const readiness = (req, res) => { + try { + const data = HomeService.getReadinessStatus(); + responses.success(res, 'readiness check')(data); + } catch (err) { + responses.error(res, 422, 'Unprocessable Entity', errors.getMessage(err))(err); + } +}; + export default { releases, changelogs, @@ -103,4 +117,5 @@ export default { page, pageByName, health, + readiness, }; diff --git a/modules/home/policies/home.policy.js b/modules/home/policies/home.policy.js index 0d4d6684a..bfa063fb8 100644 --- a/modules/home/policies/home.policy.js +++ b/modules/home/policies/home.policy.js @@ -12,6 +12,9 @@ */ export function homeAbilities(user, membership, { can }) { can('read', 'Home'); + if (Array.isArray(user?.roles) && user.roles.includes('admin')) { + can('read', 'Readiness'); + } } /** diff --git a/modules/home/routes/home.route.js b/modules/home/routes/home.route.js index 8bf8dc0ea..cae39b492 100644 --- a/modules/home/routes/home.route.js +++ b/modules/home/routes/home.route.js @@ -35,6 +35,8 @@ export default (app) => { app.route('/api/home/team').all(policy.isAllowed).get(home.team); // markdown files app.route('/api/home/pages/:name').all(policy.isAllowed).get(home.page); + // readiness check — admin only (JWT + CASL) + app.route('/api/admin/readiness').all(passport.authenticate('jwt', { session: false }), policy.isAllowed).get(home.readiness); // Finish by binding the task middleware app.param('name', home.pageByName); diff --git a/modules/home/services/home.service.js b/modules/home/services/home.service.js index 44228d98a..0b620a50b 100644 --- a/modules/home/services/home.service.js +++ b/modules/home/services/home.service.js @@ -10,8 +10,16 @@ import mongoose from 'mongoose'; import AuthService from '../../auth/services/auth.service.js'; import config from '../../../config/index.js'; +import mailer from '../../../lib/helpers/mailer/index.js'; import HomeRepository from '../repositories/home.repository.js'; +/** + * @desc Check whether a config value is meaningfully set (truthy, non-empty, not a DEVKIT placeholder). + * @param {*} value - Config value to check + * @returns {boolean} true if set and not a placeholder + */ +const isSet = (value) => !!(value && typeof value === 'string' && value.trim() !== '' && !value.startsWith('DEVKIT_NODE_')); + /** * @desc Function to get all admin users in db * @return {Promise} All users @@ -102,10 +110,80 @@ const getHealthStatus = () => { }; }; +/** + * @desc Run SaaS readiness checks against current configuration. + * Each check returns { category, status, message }. + * @returns {Array<{category: string, status: string, message: string}>} + */ +const getReadinessStatus = () => { + const checks = []; + + // config — domain + checks.push({ + category: 'config', + status: isSet(config.domain) ? 'ok' : 'warning', + message: isSet(config.domain) ? 'Domain configured' : 'Domain not configured', + }); + + // security — JWT secret + const jwtDefault = config.jwt?.secret === 'WaosSecretKeyExampleToChnageAbsolutely'; + checks.push({ + category: 'security', + status: jwtDefault ? 'warning' : 'ok', + message: jwtDefault ? 'JWT secret is default — change it before production' : 'JWT secret is custom', + }); + + // auth — OAuth providers + const oAuthProviders = []; + if (isSet(config.oAuth?.google?.clientID)) oAuthProviders.push('Google'); + if (isSet(config.oAuth?.apple?.clientID)) oAuthProviders.push('Apple'); + checks.push({ + category: 'auth', + status: oAuthProviders.length > 0 ? 'ok' : 'warning', + message: oAuthProviders.length > 0 ? `OAuth configured (${oAuthProviders.join(', ')})` : 'No OAuth provider configured', + }); + + // mail — mailer from + const mailConfigured = mailer.isConfigured(); + const mailProvider = config.mailer?.provider || 'nodemailer'; + checks.push({ + category: 'mail', + status: mailConfigured ? 'ok' : 'warning', + message: mailConfigured ? `Mail configured (${mailProvider})` : 'No mail provider configured', + }); + + // billing — Stripe + const stripeConfigured = isSet(config.stripe?.secretKey); + checks.push({ + category: 'billing', + status: stripeConfigured ? 'ok' : 'warning', + message: stripeConfigured ? 'Stripe configured' : 'Stripe not configured', + }); + + // analytics — PostHog + const posthogConfigured = isSet(config.posthog?.apiKey); + checks.push({ + category: 'analytics', + status: posthogConfigured ? 'ok' : 'warning', + message: posthogConfigured ? 'PostHog configured' : 'PostHog not configured', + }); + + // monitoring — Sentry + const sentryConfigured = isSet(config.sentry?.dsn); + checks.push({ + category: 'monitoring', + status: sentryConfigured ? 'ok' : 'warning', + message: sentryConfigured ? 'Sentry configured' : 'Sentry not configured', + }); + + return checks; +}; + export default { page, releases, changelogs, team, getHealthStatus, + getReadinessStatus, }; diff --git a/modules/home/tests/home.integration.tests.js b/modules/home/tests/home.integration.tests.js index a99a1fb32..e319a9085 100644 --- a/modules/home/tests/home.integration.tests.js +++ b/modules/home/tests/home.integration.tests.js @@ -19,6 +19,7 @@ describe('Home integration tests:', () => { let HomeService; let adminToken; let adminUser; + let userToken; let originalOrganizationsEnabled; // init @@ -52,6 +53,17 @@ describe('Home integration tests:', () => { roles: ['admin'], }); adminToken = jwt.sign({ userId: adminUser.id }, config.jwt.secret, { expiresIn: config.jwt.expiresIn }); + + // Create regular user and sign JWT for readiness auth tests + const regularUser = await User.create({ + firstName: 'Regular', + lastName: 'User', + email: 'regular-readiness@test.com', + password: 'W@os.jsI$Aw3$0m3', + provider: 'local', + roles: ['user'], + }); + userToken = jwt.sign({ userId: regularUser.id }, config.jwt.secret, { expiresIn: config.jwt.expiresIn }); } catch (err) { console.log(err); expect(err).toBeFalsy(); @@ -188,6 +200,40 @@ describe('Home integration tests:', () => { }); }); + describe('Readiness', () => { + test('should return 401 for unauthenticated user', async () => { + const result = await agent.get('/api/admin/readiness').expect(401); + expect(result.body).toBeDefined(); + }); + + test('should return 403 for regular user', async () => { + const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${userToken}`).expect(403); + expect(result.body.type).toBe('error'); + }); + + test('should return 200 with readiness checks for admin', async () => { + const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); + expect(result.body.type).toBe('success'); + expect(result.body.message).toBe('readiness check'); + expect(result.body.data).toBeInstanceOf(Array); + expect(result.body.data.length).toBeGreaterThan(0); + }); + + test('should return correct shape for each readiness check', async () => { + const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); + const expectedCategories = ['config', 'security', 'auth', 'mail', 'billing', 'analytics', 'monitoring']; + const categories = result.body.data.map((c) => c.category); + expect(categories).toEqual(expectedCategories); + result.body.data.forEach((item) => { + expect(item).toHaveProperty('category'); + expect(item).toHaveProperty('status'); + expect(item).toHaveProperty('message'); + expect(['ok', 'warning']).toContain(item.status); + expect(typeof item.message).toBe('string'); + }); + }); + }); + describe('Errors', () => { test('should return 422 when team service fails', async () => { jest.spyOn(HomeService, 'team').mockRejectedValueOnce(new Error('DB error')); From 05c5037131b04379bfe09873447b1b8c3feb1c48 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Mon, 30 Mar 2026 16:02:27 +0200 Subject: [PATCH 2/8] =?UTF-8?q?fix(home):=20address=20readiness=20review?= =?UTF-8?q?=20=E2=80=94=20JSDoc,=20JWT=20check,=20mail=20sanitization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add proper JSDoc with @returns on logConfiguration - Fix isSet JSDoc to match implementation (non-empty string check) - JWT check now catches missing/empty secret, not just default value - Remove mail provider name from response to avoid leaking placeholders --- lib/app.js | 3 ++- modules/home/services/home.service.js | 16 ++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/app.js b/lib/app.js index 9eb346727..9db1de8a5 100644 --- a/lib/app.js +++ b/lib/app.js @@ -87,7 +87,8 @@ const bootstrap = async () => { }; /** - * log server configuration + * @desc Log server configuration and SaaS readiness summary to console. + * @returns {Promise} */ const logConfiguration = async () => { // Create server URL diff --git a/modules/home/services/home.service.js b/modules/home/services/home.service.js index 0b620a50b..e2544d5c9 100644 --- a/modules/home/services/home.service.js +++ b/modules/home/services/home.service.js @@ -14,9 +14,9 @@ import mailer from '../../../lib/helpers/mailer/index.js'; import HomeRepository from '../repositories/home.repository.js'; /** - * @desc Check whether a config value is meaningfully set (truthy, non-empty, not a DEVKIT placeholder). + * @desc Check whether a config value is meaningfully set (non-empty string, not a DEVKIT placeholder). * @param {*} value - Config value to check - * @returns {boolean} true if set and not a placeholder + * @returns {boolean} true if value is a non-empty string and not a DEVKIT_NODE_ placeholder */ const isSet = (value) => !!(value && typeof value === 'string' && value.trim() !== '' && !value.startsWith('DEVKIT_NODE_')); @@ -126,11 +126,12 @@ const getReadinessStatus = () => { }); // security — JWT secret - const jwtDefault = config.jwt?.secret === 'WaosSecretKeyExampleToChnageAbsolutely'; + const jwtSecret = config.jwt?.secret; + const jwtInsecure = !jwtSecret || jwtSecret.trim() === '' || jwtSecret === 'WaosSecretKeyExampleToChnageAbsolutely'; checks.push({ category: 'security', - status: jwtDefault ? 'warning' : 'ok', - message: jwtDefault ? 'JWT secret is default — change it before production' : 'JWT secret is custom', + status: jwtInsecure ? 'warning' : 'ok', + message: jwtInsecure ? 'JWT secret is missing or default — change it before production' : 'JWT secret is custom', }); // auth — OAuth providers @@ -143,13 +144,12 @@ const getReadinessStatus = () => { message: oAuthProviders.length > 0 ? `OAuth configured (${oAuthProviders.join(', ')})` : 'No OAuth provider configured', }); - // mail — mailer from + // mail — mailer const mailConfigured = mailer.isConfigured(); - const mailProvider = config.mailer?.provider || 'nodemailer'; checks.push({ category: 'mail', status: mailConfigured ? 'ok' : 'warning', - message: mailConfigured ? `Mail configured (${mailProvider})` : 'No mail provider configured', + message: mailConfigured ? 'Mail provider configured' : 'No mail provider configured', }); // billing — Stripe From 43d3ad9f472b162ea09eb35461c8274c8ef24474 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Mon, 30 Mar 2026 16:11:38 +0200 Subject: [PATCH 3/8] =?UTF-8?q?fix(home):=20address=20remaining=20review?= =?UTF-8?q?=20=E2=80=94=20JSDoc,=20CASL=20manage,=20log=20errors,=20cache?= =?UTF-8?q?=20isSet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add @returns {void} to readiness controller JSDoc - Use can('manage', 'Readiness') matching standard CASL pattern - Log readiness check failures at boot instead of swallowing - Cache isSet(config.domain) to avoid duplicate calls --- lib/app.js | 4 ++-- modules/home/controllers/home.controller.js | 3 ++- modules/home/policies/home.policy.js | 2 +- modules/home/services/home.service.js | 5 +++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/app.js b/lib/app.js index 9db1de8a5..041298ebb 100644 --- a/lib/app.js +++ b/lib/app.js @@ -112,8 +112,8 @@ const logConfiguration = async () => { const icon = c.status === 'ok' ? chalk.green('OK') : chalk.yellow('WARN'); console.log(` ${icon} ${c.category.padEnd(12)} ${c.message}`); }); - } catch (_err) { - // Non-blocking — readiness check failure should not prevent boot + } catch (err) { + console.log(chalk.yellow(` SaaS readiness check failed: ${err.message}`)); } } }; diff --git a/modules/home/controllers/home.controller.js b/modules/home/controllers/home.controller.js index afc5bc900..2c9d9c983 100644 --- a/modules/home/controllers/home.controller.js +++ b/modules/home/controllers/home.controller.js @@ -97,9 +97,10 @@ const health = (req, res) => { }; /** - * @desc Endpoint to return SaaS readiness checks (admin only) + * @desc Endpoint to return SaaS readiness checks (admin only). * @param {Object} req - Express request object * @param {Object} res - Express response object + * @returns {void} */ const readiness = (req, res) => { try { diff --git a/modules/home/policies/home.policy.js b/modules/home/policies/home.policy.js index bfa063fb8..5f1ace8ab 100644 --- a/modules/home/policies/home.policy.js +++ b/modules/home/policies/home.policy.js @@ -13,7 +13,7 @@ export function homeAbilities(user, membership, { can }) { can('read', 'Home'); if (Array.isArray(user?.roles) && user.roles.includes('admin')) { - can('read', 'Readiness'); + can('manage', 'Readiness'); } } diff --git a/modules/home/services/home.service.js b/modules/home/services/home.service.js index e2544d5c9..7aa657598 100644 --- a/modules/home/services/home.service.js +++ b/modules/home/services/home.service.js @@ -119,10 +119,11 @@ const getReadinessStatus = () => { const checks = []; // config — domain + const domainSet = isSet(config.domain); checks.push({ category: 'config', - status: isSet(config.domain) ? 'ok' : 'warning', - message: isSet(config.domain) ? 'Domain configured' : 'Domain not configured', + status: domainSet ? 'ok' : 'warning', + message: domainSet ? 'Domain configured' : 'Domain not configured', }); // security — JWT secret From 33dbccb27f40cc150e6a22ea0e51d4047c36e018 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Thu, 2 Apr 2026 08:24:24 +0200 Subject: [PATCH 4/8] test(home): add readiness branch coverage and controller error tests Cover all getReadinessStatus branches (ok/warning paths for each check category) and the readiness controller error handler to improve patch and project coverage. --- modules/home/tests/home.integration.tests.js | 74 ++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/modules/home/tests/home.integration.tests.js b/modules/home/tests/home.integration.tests.js index e319a9085..a922e4bdd 100644 --- a/modules/home/tests/home.integration.tests.js +++ b/modules/home/tests/home.integration.tests.js @@ -232,6 +232,80 @@ describe('Home integration tests:', () => { expect(typeof item.message).toBe('string'); }); }); + + test('should report ok status when config values are properly set', async () => { + const mailer = (await import('../../../lib/helpers/mailer/index.js')).default; + const origDomain = config.domain; + const origJwt = config.jwt.secret; + const origOAuth = config.oAuth; + const origStripe = config.stripe; + const origPosthog = config.posthog; + const origSentry = config.sentry; + const mailerSpy = jest.spyOn(mailer, 'isConfigured').mockReturnValue(true); + + config.domain = 'example.com'; + config.jwt.secret = 'a-real-custom-secret-key'; + config.oAuth = { google: { clientID: 'google-id' }, apple: { clientID: 'apple-id' } }; + config.stripe = { secretKey: 'sk_test_123' }; + config.posthog = { apiKey: 'phk_123' }; + config.sentry = { dsn: 'https://sentry.io/123' }; + + const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); + result.body.data.forEach((item) => { + expect(item.status).toBe('ok'); + }); + // Verify OAuth message includes both providers + const authCheck = result.body.data.find((c) => c.category === 'auth'); + expect(authCheck.message).toContain('Google'); + expect(authCheck.message).toContain('Apple'); + + config.domain = origDomain; + config.jwt.secret = origJwt; + config.oAuth = origOAuth; + config.stripe = origStripe; + config.posthog = origPosthog; + config.sentry = origSentry; + mailerSpy.mockRestore(); + }); + + test('should report warning when JWT secret is the default value', async () => { + const origJwt = config.jwt.secret; + config.jwt.secret = 'WaosSecretKeyExampleToChnageAbsolutely'; + const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); + const secCheck = result.body.data.find((c) => c.category === 'security'); + expect(secCheck.status).toBe('warning'); + expect(secCheck.message).toContain('default'); + config.jwt.secret = origJwt; + }); + + test('should report warning when domain is a DEVKIT placeholder', async () => { + const origDomain = config.domain; + config.domain = 'DEVKIT_NODE_DOMAIN'; + const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); + const cfgCheck = result.body.data.find((c) => c.category === 'config'); + expect(cfgCheck.status).toBe('warning'); + config.domain = origDomain; + }); + + test('should handle only Google OAuth configured', async () => { + const origOAuth = config.oAuth; + config.oAuth = { google: { clientID: 'google-id' }, apple: {} }; + const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); + const authCheck = result.body.data.find((c) => c.category === 'auth'); + expect(authCheck.status).toBe('ok'); + expect(authCheck.message).toContain('Google'); + expect(authCheck.message).not.toContain('Apple'); + config.oAuth = origOAuth; + }); + + test('should return 422 when readiness service throws', async () => { + jest.spyOn(HomeService, 'getReadinessStatus').mockImplementationOnce(() => { + throw new Error('config error'); + }); + const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(422); + expect(result.body.type).toBe('error'); + expect(result.body.description).toBe('config error.'); + }); }); describe('Errors', () => { From a9276d920b639bf80895e977bc87f9745cae36f8 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Thu, 2 Apr 2026 08:28:48 +0200 Subject: [PATCH 5/8] test(home): cover isSet edge cases (empty, whitespace, missing JWT) --- modules/home/tests/home.integration.tests.js | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/modules/home/tests/home.integration.tests.js b/modules/home/tests/home.integration.tests.js index a922e4bdd..4f6071bdd 100644 --- a/modules/home/tests/home.integration.tests.js +++ b/modules/home/tests/home.integration.tests.js @@ -298,6 +298,29 @@ describe('Home integration tests:', () => { config.oAuth = origOAuth; }); + test('should report warning when config values are empty strings or whitespace', async () => { + const origDomain = config.domain; + const origStripe = config.stripe; + config.domain = ' '; + config.stripe = { secretKey: '' }; + const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); + const cfgCheck = result.body.data.find((c) => c.category === 'config'); + const billingCheck = result.body.data.find((c) => c.category === 'billing'); + expect(cfgCheck.status).toBe('warning'); + expect(billingCheck.status).toBe('warning'); + config.domain = origDomain; + config.stripe = origStripe; + }); + + test('should report warning when JWT secret is empty', async () => { + const origJwt = config.jwt.secret; + config.jwt.secret = ''; + const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); + const secCheck = result.body.data.find((c) => c.category === 'security'); + expect(secCheck.status).toBe('warning'); + config.jwt.secret = origJwt; + }); + test('should return 422 when readiness service throws', async () => { jest.spyOn(HomeService, 'getReadinessStatus').mockImplementationOnce(() => { throw new Error('config error'); From 76628f92cc7300d3ec227e4901e05b8a5191e160 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Thu, 2 Apr 2026 08:35:32 +0200 Subject: [PATCH 6/8] docs(home): clarify isSet JSDoc wording --- modules/home/services/home.service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/home/services/home.service.js b/modules/home/services/home.service.js index 7aa657598..61f638079 100644 --- a/modules/home/services/home.service.js +++ b/modules/home/services/home.service.js @@ -14,9 +14,9 @@ import mailer from '../../../lib/helpers/mailer/index.js'; import HomeRepository from '../repositories/home.repository.js'; /** - * @desc Check whether a config value is meaningfully set (non-empty string, not a DEVKIT placeholder). + * @desc Check whether a config value is meaningfully set (non-empty, not a DEVKIT placeholder). * @param {*} value - Config value to check - * @returns {boolean} true if value is a non-empty string and not a DEVKIT_NODE_ placeholder + * @returns {boolean} true when value is a non-empty string and not a DEVKIT_NODE_ placeholder */ const isSet = (value) => !!(value && typeof value === 'string' && value.trim() !== '' && !value.startsWith('DEVKIT_NODE_')); From 6eec090ac28481177b126ea4ec4ec1df8112baea Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Thu, 2 Apr 2026 08:47:03 +0200 Subject: [PATCH 7/8] =?UTF-8?q?fix(home):=20address=20CodeRabbit=20review?= =?UTF-8?q?=20=E2=80=94=20try/finally=20guards,=20401=20assertion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Wrap config mutation tests with try/finally to prevent state leakage - Strengthen 401 assertion with status check - Clarify isSet JSDoc wording --- modules/home/tests/home.integration.tests.js | 129 +++++++++++-------- 1 file changed, 73 insertions(+), 56 deletions(-) diff --git a/modules/home/tests/home.integration.tests.js b/modules/home/tests/home.integration.tests.js index 4f6071bdd..fc9e9eca3 100644 --- a/modules/home/tests/home.integration.tests.js +++ b/modules/home/tests/home.integration.tests.js @@ -204,6 +204,7 @@ describe('Home integration tests:', () => { test('should return 401 for unauthenticated user', async () => { const result = await agent.get('/api/admin/readiness').expect(401); expect(result.body).toBeDefined(); + expect(result.status).toBe(401); }); test('should return 403 for regular user', async () => { @@ -242,83 +243,99 @@ describe('Home integration tests:', () => { const origPosthog = config.posthog; const origSentry = config.sentry; const mailerSpy = jest.spyOn(mailer, 'isConfigured').mockReturnValue(true); - - config.domain = 'example.com'; - config.jwt.secret = 'a-real-custom-secret-key'; - config.oAuth = { google: { clientID: 'google-id' }, apple: { clientID: 'apple-id' } }; - config.stripe = { secretKey: 'sk_test_123' }; - config.posthog = { apiKey: 'phk_123' }; - config.sentry = { dsn: 'https://sentry.io/123' }; - - const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); - result.body.data.forEach((item) => { - expect(item.status).toBe('ok'); - }); - // Verify OAuth message includes both providers - const authCheck = result.body.data.find((c) => c.category === 'auth'); - expect(authCheck.message).toContain('Google'); - expect(authCheck.message).toContain('Apple'); - - config.domain = origDomain; - config.jwt.secret = origJwt; - config.oAuth = origOAuth; - config.stripe = origStripe; - config.posthog = origPosthog; - config.sentry = origSentry; - mailerSpy.mockRestore(); + try { + config.domain = 'example.com'; + config.jwt.secret = 'a-real-custom-secret-key'; + config.oAuth = { google: { clientID: 'google-id' }, apple: { clientID: 'apple-id' } }; + config.stripe = { secretKey: 'sk_test_123' }; + config.posthog = { apiKey: 'phk_123' }; + config.sentry = { dsn: 'https://sentry.io/123' }; + + const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); + result.body.data.forEach((item) => { + expect(item.status).toBe('ok'); + }); + // Verify OAuth message includes both providers + const authCheck = result.body.data.find((c) => c.category === 'auth'); + expect(authCheck.message).toContain('Google'); + expect(authCheck.message).toContain('Apple'); + } finally { + config.domain = origDomain; + config.jwt.secret = origJwt; + config.oAuth = origOAuth; + config.stripe = origStripe; + config.posthog = origPosthog; + config.sentry = origSentry; + mailerSpy.mockRestore(); + } }); test('should report warning when JWT secret is the default value', async () => { const origJwt = config.jwt.secret; - config.jwt.secret = 'WaosSecretKeyExampleToChnageAbsolutely'; - const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); - const secCheck = result.body.data.find((c) => c.category === 'security'); - expect(secCheck.status).toBe('warning'); - expect(secCheck.message).toContain('default'); - config.jwt.secret = origJwt; + try { + config.jwt.secret = 'WaosSecretKeyExampleToChnageAbsolutely'; + const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); + const secCheck = result.body.data.find((c) => c.category === 'security'); + expect(secCheck.status).toBe('warning'); + expect(secCheck.message).toContain('default'); + } finally { + config.jwt.secret = origJwt; + } }); test('should report warning when domain is a DEVKIT placeholder', async () => { const origDomain = config.domain; - config.domain = 'DEVKIT_NODE_DOMAIN'; - const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); - const cfgCheck = result.body.data.find((c) => c.category === 'config'); - expect(cfgCheck.status).toBe('warning'); - config.domain = origDomain; + try { + config.domain = 'DEVKIT_NODE_DOMAIN'; + const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); + const cfgCheck = result.body.data.find((c) => c.category === 'config'); + expect(cfgCheck.status).toBe('warning'); + } finally { + config.domain = origDomain; + } }); test('should handle only Google OAuth configured', async () => { const origOAuth = config.oAuth; - config.oAuth = { google: { clientID: 'google-id' }, apple: {} }; - const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); - const authCheck = result.body.data.find((c) => c.category === 'auth'); - expect(authCheck.status).toBe('ok'); - expect(authCheck.message).toContain('Google'); - expect(authCheck.message).not.toContain('Apple'); - config.oAuth = origOAuth; + try { + config.oAuth = { google: { clientID: 'google-id' }, apple: {} }; + const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); + const authCheck = result.body.data.find((c) => c.category === 'auth'); + expect(authCheck.status).toBe('ok'); + expect(authCheck.message).toContain('Google'); + expect(authCheck.message).not.toContain('Apple'); + } finally { + config.oAuth = origOAuth; + } }); test('should report warning when config values are empty strings or whitespace', async () => { const origDomain = config.domain; const origStripe = config.stripe; - config.domain = ' '; - config.stripe = { secretKey: '' }; - const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); - const cfgCheck = result.body.data.find((c) => c.category === 'config'); - const billingCheck = result.body.data.find((c) => c.category === 'billing'); - expect(cfgCheck.status).toBe('warning'); - expect(billingCheck.status).toBe('warning'); - config.domain = origDomain; - config.stripe = origStripe; + try { + config.domain = ' '; + config.stripe = { secretKey: '' }; + const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); + const cfgCheck = result.body.data.find((c) => c.category === 'config'); + const billingCheck = result.body.data.find((c) => c.category === 'billing'); + expect(cfgCheck.status).toBe('warning'); + expect(billingCheck.status).toBe('warning'); + } finally { + config.domain = origDomain; + config.stripe = origStripe; + } }); test('should report warning when JWT secret is empty', async () => { const origJwt = config.jwt.secret; - config.jwt.secret = ''; - const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); - const secCheck = result.body.data.find((c) => c.category === 'security'); - expect(secCheck.status).toBe('warning'); - config.jwt.secret = origJwt; + try { + config.jwt.secret = ''; + const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); + const secCheck = result.body.data.find((c) => c.category === 'security'); + expect(secCheck.status).toBe('warning'); + } finally { + config.jwt.secret = origJwt; + } }); test('should return 422 when readiness service throws', async () => { From 0e0badeb6d03c7ce2bc753cf422a130a702a5468 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Thu, 2 Apr 2026 09:14:31 +0200 Subject: [PATCH 8/8] test(home): fix regularUser fixture leak in readiness integration tests Pre-clean regular-readiness@test.com before creating it to prevent unique index failures on re-runs, hoist to suite scope, and delete in afterAll. --- modules/home/tests/home.integration.tests.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/home/tests/home.integration.tests.js b/modules/home/tests/home.integration.tests.js index fc9e9eca3..8112dfa08 100644 --- a/modules/home/tests/home.integration.tests.js +++ b/modules/home/tests/home.integration.tests.js @@ -20,6 +20,7 @@ describe('Home integration tests:', () => { let adminToken; let adminUser; let userToken; + let regularUser; let originalOrganizationsEnabled; // init @@ -55,7 +56,8 @@ describe('Home integration tests:', () => { adminToken = jwt.sign({ userId: adminUser.id }, config.jwt.secret, { expiresIn: config.jwt.expiresIn }); // Create regular user and sign JWT for readiness auth tests - const regularUser = await User.create({ + await User.deleteOne({ email: 'regular-readiness@test.com' }); + regularUser = await User.create({ firstName: 'Regular', lastName: 'User', email: 'regular-readiness@test.com', @@ -371,9 +373,10 @@ describe('Home integration tests:', () => { jest.restoreAllMocks(); config.organizations.enabled = originalOrganizationsEnabled; try { - if (adminUser) { + if (adminUser || regularUser) { const User = mongoose.model('User'); - await User.deleteOne({ _id: adminUser._id }); + if (adminUser) await User.deleteOne({ _id: adminUser._id }); + if (regularUser) await User.deleteOne({ _id: regularUser._id }); } } catch (_) { /* cleanup – ignore errors */ } try {