diff --git a/backend/package-lock.json b/backend/package-lock.json index e8e2aa7..fb63d01 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -22,8 +22,8 @@ "zod": "^4.3.6" }, "devDependencies": { - "c8": "^11.0.0", "@faker-js/faker": "^8.4.1", + "c8": "^11.0.0", "jest": "^29.7.0", "nodemon": "^3.0.2", "supertest": "^6.3.4" @@ -784,7 +784,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2958,7 +2957,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -3994,7 +3992,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", diff --git a/backend/src/config.js b/backend/src/config.js index 9b6def3..2f66b2d 100644 --- a/backend/src/config.js +++ b/backend/src/config.js @@ -12,7 +12,9 @@ const schema = z.object({ VACCINATIONS_CONTRACT_ID: z.string().min(1), // Backend - ADMIN_SECRET_KEY: z.string().min(1), + ADMIN_SECRET_KEY: z.string().min(1).refine(v => v.startsWith('S'), { + message: 'ADMIN_SECRET_KEY must start with S', + }), SEP10_SERVER_KEY: z.string().min(1), JWT_SECRET: z.string().min(1), PORT: z.coerce.number().int().positive().default(4000), @@ -36,12 +38,23 @@ const schema = z.object({ DATABASE_PATH: z.string().default('/data/vaccichain.db'), }); -const result = schema.safeParse(process.env); - -if (!result.success) { - const missing = result.error.issues.map(i => ` ${i.path[0]}: ${i.message}`).join('\n'); - console.error(`[config] Missing or invalid environment variables:\n${missing}`); - process.exit(1); +function validateEnv(env) { + const result = schema.safeParse(env); + if (!result.success) { + const missing = result.error.issues.map(i => ` ${i.path[0]}: ${i.message}`).join('\n'); + throw new Error(`[config] Missing or invalid environment variables:\n${missing}`); + } + return result.data; } -module.exports = result.data; +const config = (() => { + try { + return validateEnv(process.env); + } catch (err) { + console.error(err.message); + process.exit(1); + } +})(); + +module.exports = config; +module.exports.validateEnv = validateEnv; diff --git a/backend/tests/config.test.js b/backend/tests/config.test.js new file mode 100644 index 0000000..d49b512 --- /dev/null +++ b/backend/tests/config.test.js @@ -0,0 +1,39 @@ +const { validateEnv } = require('../src/config'); + +const valid = { + HORIZON_URL: 'https://horizon-testnet.stellar.org', + SOROBAN_RPC_URL: 'https://soroban-testnet.stellar.org', + STELLAR_NETWORK_PASSPHRASE: 'Test SDF Network ; September 2015', + VACCINATIONS_CONTRACT_ID: 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4', + ADMIN_SECRET_KEY: 'STEST000000000000000000000000000000000000000000000000000', + SEP10_SERVER_KEY: 'STEST000000000000000000000000000000000000000000000000001', + JWT_SECRET: 'test-jwt-secret', +}; + +describe('validateEnv', () => { + it('passes with all required variables present', () => { + expect(() => validateEnv(valid)).not.toThrow(); + }); + + it('throws with the variable name when a required variable is missing', () => { + const { HORIZON_URL: _, ...env } = valid; + expect(() => validateEnv(env)).toThrow('HORIZON_URL'); + }); + + it('throws a format error when ADMIN_SECRET_KEY does not start with S', () => { + expect(() => validateEnv({ ...valid, ADMIN_SECRET_KEY: 'XBADKEY' })).toThrow( + 'ADMIN_SECRET_KEY' + ); + }); + + it('throws when STELLAR_NETWORK is set to an invalid value', () => { + expect(() => validateEnv({ ...valid, STELLAR_NETWORK: 'devnet' })).toThrow('STELLAR_NETWORK'); + }); + + it('is callable without side effects (no process.exit)', () => { + const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {}); + expect(() => validateEnv({ ...valid, HORIZON_URL: 'not-a-url' })).toThrow(); + expect(exitSpy).not.toHaveBeenCalled(); + exitSpy.mockRestore(); + }); +});