From 43a623cdf46e94ac177f5700dea56184fa18fc4d Mon Sep 17 00:00:00 2001 From: githoboman Date: Mon, 27 Apr 2026 20:49:21 +0200 Subject: [PATCH] feat: implement backend test suite with helper factories and mock data handlers --- backend/package-lock.json | 21 ++++ backend/package.json | 1 + backend/src/app.js | 4 + backend/tests/auth-claims.test.js | 17 ++- backend/tests/factories/index.js | 9 ++ backend/tests/factories/jwt.js | 25 +++++ backend/tests/factories/sep10.js | 50 +++++++++ backend/tests/factories/vaccinationRecord.js | 20 ++++ backend/tests/indexer.test.js | 9 +- backend/tests/patient-register.test.js | 13 +-- backend/tests/verifier-api-key.test.js | 15 ++- backend/tests/wallet-validation.test.js | 34 +++--- frontend/package.json | 9 +- frontend/src/main.jsx | 30 ++++-- frontend/src/mocks/browser.js | 4 + frontend/src/mocks/handlers.js | 103 +++++++++++++++++++ frontend/src/mocks/server.js | 4 + tests/README.md | 47 +++++++++ 18 files changed, 359 insertions(+), 56 deletions(-) create mode 100644 backend/tests/factories/index.js create mode 100644 backend/tests/factories/jwt.js create mode 100644 backend/tests/factories/sep10.js create mode 100644 backend/tests/factories/vaccinationRecord.js create mode 100644 frontend/src/mocks/browser.js create mode 100644 frontend/src/mocks/handlers.js create mode 100644 frontend/src/mocks/server.js create mode 100644 tests/README.md diff --git a/backend/package-lock.json b/backend/package-lock.json index 2d4d3b1..e782c21 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -19,6 +19,7 @@ "zod": "^4.3.6" }, "devDependencies": { + "@faker-js/faker": "^8.4.1", "jest": "^29.7.0", "nodemon": "^3.0.2", "supertest": "^6.3.4" @@ -55,6 +56,7 @@ "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", @@ -590,6 +592,23 @@ "kuler": "^2.0.0" } }, + "node_modules/@faker-js/faker": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", + "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1608,6 +1627,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -2443,6 +2463,7 @@ "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/package.json b/backend/package.json index 3ffeedd..a6d5327 100644 --- a/backend/package.json +++ b/backend/package.json @@ -25,6 +25,7 @@ ] }, "devDependencies": { + "@faker-js/faker": "^8.4.1", "jest": "^29.7.0", "nodemon": "^3.0.2", "supertest": "^6.3.4" diff --git a/backend/src/app.js b/backend/src/app.js index 177cefd..23edfe2 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -11,9 +11,11 @@ const vaccinationRoutes = require('./routes/vaccination'); const verifyRoutes = require('./routes/verify'); const adminRoutes = require('./routes/admin'); const patientRoutes = require('./routes/patient'); +const eventsRoutes = require('./routes/events'); const app = express(); + app.use(cors()); app.use(express.json({ limit: config.BODY_LIMIT })); @@ -28,6 +30,8 @@ app.use('/vaccination', vaccinationRoutes); app.use('/verify', verifyRoutes); app.use('/admin', adminRoutes); app.use('/patient', patientRoutes); +app.use('/events', eventsRoutes); + app.get('/health', (_req, res) => res.json({ status: 'ok' })); diff --git a/backend/tests/auth-claims.test.js b/backend/tests/auth-claims.test.js index 7c5cec9..44ef8bb 100644 --- a/backend/tests/auth-claims.test.js +++ b/backend/tests/auth-claims.test.js @@ -1,5 +1,5 @@ -const jwt = require('jsonwebtoken'); const authMiddleware = require('../src/middleware/auth'); +const { jwtFactory } = require('./factories'); describe('JWT Claims Validation Middleware', () => { const JWT_SECRET = 'test-jwt-secret'; @@ -17,14 +17,11 @@ describe('JWT Claims Validation Middleware', () => { next = jest.fn(); }); - const createToken = (payload) => { - return jwt.sign(payload, JWT_SECRET); - }; - const setAuthHeader = (token) => { req.headers.authorization = `Bearer ${token}`; }; + it('passes when all required claims are present and valid', () => { const payload = { sub: 'user123', @@ -32,7 +29,7 @@ describe('JWT Claims Validation Middleware', () => { wallet: 'GB...', exp: Math.floor(Date.now() / 1000) + 3600, }; - setAuthHeader(createToken(payload)); + setAuthHeader(jwtFactory(payload)); authMiddleware(req, res, next); @@ -46,7 +43,7 @@ describe('JWT Claims Validation Middleware', () => { wallet: 'GB...', exp: Math.floor(Date.now() / 1000) + 3600, }; - setAuthHeader(createToken(payload)); + setAuthHeader(jwtFactory(payload)); authMiddleware(req, res, next); @@ -61,7 +58,7 @@ describe('JWT Claims Validation Middleware', () => { wallet: 'GB...', exp: Math.floor(Date.now() / 1000) + 3600, }; - setAuthHeader(createToken(payload)); + setAuthHeader(jwtFactory(payload)); authMiddleware(req, res, next); @@ -76,7 +73,7 @@ describe('JWT Claims Validation Middleware', () => { role: 'patient', exp: Math.floor(Date.now() / 1000) + 3600, }; - setAuthHeader(createToken(payload)); + setAuthHeader(jwtFactory(payload)); authMiddleware(req, res, next); @@ -92,7 +89,7 @@ describe('JWT Claims Validation Middleware', () => { wallet: 'GB...', exp: Math.floor(Date.now() / 1000) + 3600, }; - setAuthHeader(createToken(payload)); + setAuthHeader(jwtFactory(payload)); authMiddleware(req, res, next); diff --git a/backend/tests/factories/index.js b/backend/tests/factories/index.js new file mode 100644 index 0000000..52ac3a3 --- /dev/null +++ b/backend/tests/factories/index.js @@ -0,0 +1,9 @@ +const vaccinationRecordFactory = require('./vaccinationRecord'); +const jwtFactory = require('./jwt'); +const sep10ChallengeFactory = require('./sep10'); + +module.exports = { + vaccinationRecordFactory, + jwtFactory, + sep10ChallengeFactory, +}; diff --git a/backend/tests/factories/jwt.js b/backend/tests/factories/jwt.js new file mode 100644 index 0000000..2958e86 --- /dev/null +++ b/backend/tests/factories/jwt.js @@ -0,0 +1,25 @@ +const jwt = require('jsonwebtoken'); +const StellarSdk = require('@stellar/stellar-sdk'); + +/** + * Generates a mock JWT for testing. + * @param {Object} overrides - Payload overrides. + * @returns {string} A signed JWT. + */ +const jwtFactory = (overrides = {}) => { + const publicKey = overrides.publicKey || overrides.wallet || StellarSdk.Keypair.random().publicKey(); + const payload = { + sub: overrides.sub || publicKey, + role: overrides.role || 'patient', + wallet: publicKey, + publicKey: publicKey, // Including both for compatibility with existing tests/code + ...overrides + }; + + const secret = process.env.JWT_SECRET || 'test-jwt-secret'; + const options = { expiresIn: '1h' }; + + return jwt.sign(payload, secret, options); +}; + +module.exports = jwtFactory; diff --git a/backend/tests/factories/sep10.js b/backend/tests/factories/sep10.js new file mode 100644 index 0000000..5abad1c --- /dev/null +++ b/backend/tests/factories/sep10.js @@ -0,0 +1,50 @@ +const StellarSdk = require('@stellar/stellar-sdk'); + +/** + * Generates a mock SEP-10 challenge transaction. + * @param {string} clientPublicKey - The public key of the client. + * @param {Object} overrides - Overrides for the challenge. + * @returns {Object} An object containing the transaction XDR and nonce. + */ +const sep10ChallengeFactory = (clientPublicKey, overrides = {}) => { + const serverKeypair = StellarSdk.Keypair.fromSecret( + process.env.SEP10_SERVER_KEY || 'SAZF5P4T56653E656665666566656665666566656665666566656665' + ); + + // We mock the nonce store behavior by just returning a nonce + const nonce = overrides.nonce || StellarSdk.Keypair.random().publicKey(); + + // Create a minimal transaction that satisfies SEP-10 for tests + // Note: In real scenarios, you'd use TransactionBuilder, but for a factory + // we might just want to return what the backend expects or a valid XDR. + + const networkPassphrase = process.env.STELLAR_NETWORK === 'mainnet' + ? StellarSdk.Networks.PUBLIC + : StellarSdk.Networks.TESTNET; + + const sourceAccount = new StellarSdk.Account(serverKeypair.publicKey(), '0'); + + const tx = new StellarSdk.TransactionBuilder(sourceAccount, { + fee: StellarSdk.BASE_FEE, + networkPassphrase, + }) + .addOperation( + StellarSdk.Operation.manageData({ + name: 'vaccichain auth', + value: nonce, + source: clientPublicKey, + }) + ) + .setTimeout(300) + .build(); + + tx.sign(serverKeypair); + + return { + transaction: overrides.transaction || tx.toXDR(), + nonce: overrides.nonce || nonce, + ...overrides + }; +}; + +module.exports = sep10ChallengeFactory; diff --git a/backend/tests/factories/vaccinationRecord.js b/backend/tests/factories/vaccinationRecord.js new file mode 100644 index 0000000..a5afa3f --- /dev/null +++ b/backend/tests/factories/vaccinationRecord.js @@ -0,0 +1,20 @@ +const { faker } = require('@faker-js/faker'); +const StellarSdk = require('@stellar/stellar-sdk'); + +/** + * Generates a mock vaccination record. + * @param {Object} overrides - Values to override in the generated record. + * @returns {Object} A vaccination record object. + */ +const vaccinationRecordFactory = (overrides = {}) => { + return { + patient_address: overrides.patient_address || StellarSdk.Keypair.random().publicKey(), + vaccine_name: overrides.vaccine_name || faker.helpers.arrayElement(['MMR', 'COVID-19', 'Hepatitis B', 'Influenza']), + date_administered: overrides.date_administered || faker.date.recent().toISOString().split('T')[0], + issuer_address: overrides.issuer_address || StellarSdk.Keypair.random().publicKey(), + lot_number: overrides.lot_number || faker.string.alphanumeric(8).toUpperCase(), + ...overrides + }; +}; + +module.exports = vaccinationRecordFactory; diff --git a/backend/tests/indexer.test.js b/backend/tests/indexer.test.js index 156a23c..4b7687b 100644 --- a/backend/tests/indexer.test.js +++ b/backend/tests/indexer.test.js @@ -3,6 +3,8 @@ const path = require('path'); const fs = require('fs'); const { initDb, upsertEvents, queryEvents, getLatestLedger } = require('../src/indexer/db'); const { parseEvent, stopPoller } = require('../src/indexer/poller'); +const { vaccinationRecordFactory } = require('./factories'); + const tmpDb = path.join(os.tmpdir(), `vaccichain-test-${Date.now()}.db`); @@ -16,6 +18,7 @@ afterAll(() => { }); describe('db — upsertEvents / queryEvents', () => { + const record = vaccinationRecordFactory({ vaccine_name: 'COVID-19', issuer_address: 'GISSUER1' }); const sample = [ { id: 'evt-001', @@ -23,7 +26,7 @@ describe('db — upsertEvents / queryEvents', () => { ledger: 100, timestamp: 1700000000, contract_id: 'CABC', - payload: { vaccine_name: 'COVID-19', issuer: 'GISSUER1' }, + payload: { vaccine_name: record.vaccine_name, issuer: record.issuer_address }, }, { id: 'evt-002', @@ -35,6 +38,7 @@ describe('db — upsertEvents / queryEvents', () => { }, ]; + it('stores events and returns them', () => { upsertEvents(sample); const results = queryEvents(); @@ -59,7 +63,8 @@ describe('db — upsertEvents / queryEvents', () => { it('deserialises payload back to object', () => { const [evt] = queryEvents({ event_type: 'VaccinationMinted' }); expect(typeof evt.payload).toBe('object'); - expect(evt.payload.vaccine_name).toBe('COVID-19'); + expect(evt.payload.vaccine_name).toBe(record.vaccine_name); + }); it('getLatestLedger returns highest ledger', () => { diff --git a/backend/tests/patient-register.test.js b/backend/tests/patient-register.test.js index 232dbcd..0817854 100644 --- a/backend/tests/patient-register.test.js +++ b/backend/tests/patient-register.test.js @@ -9,13 +9,8 @@ jest.mock('../src/stellar/soroban', () => ({ const { invokeContract } = require('../src/stellar/soroban'); -function makeToken(overrides = {}) { - return jwt.sign( - { sub: 'GTEST', role: 'patient', wallet: 'GTEST', ...overrides }, - process.env.JWT_SECRET || 'test-secret', - { expiresIn: '1h' } - ); -} +const { jwtFactory } = require('./factories'); + describe('POST /patient/register', () => { beforeEach(() => { @@ -32,7 +27,7 @@ describe('POST /patient/register', () => { }); it('registers successfully with a valid patient JWT', async () => { - const token = makeToken(); + const token = jwtFactory({ role: 'patient' }); const res = await request(app) .post('/patient/register') .set('Authorization', `Bearer ${token}`); @@ -47,7 +42,7 @@ describe('POST /patient/register', () => { it('returns 500 when contract invocation fails', async () => { invokeContract.mockRejectedValue(new Error('contract error')); - const token = makeToken(); + const token = jwtFactory({ role: 'patient' }); const res = await request(app) .post('/patient/register') .set('Authorization', `Bearer ${token}`); diff --git a/backend/tests/verifier-api-key.test.js b/backend/tests/verifier-api-key.test.js index 2be459f..16e4c17 100644 --- a/backend/tests/verifier-api-key.test.js +++ b/backend/tests/verifier-api-key.test.js @@ -3,9 +3,10 @@ const path = require('path'); const fs = require('fs'); const request = require('supertest'); const crypto = require('crypto'); -const jwt = require('jsonwebtoken'); const { initDb, insertApiKey, revokeApiKey } = require('../src/indexer/db'); const app = require('../src/app'); +const { jwtFactory } = require('./factories'); + const tmpDb = path.join(os.tmpdir(), `vaccichain-apikey-test-${Date.now()}.db`); @@ -22,12 +23,10 @@ afterAll(() => { const VALID_WALLET = 'GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN'; function issuerToken() { - return jwt.sign( - { sub: 'admin', role: 'issuer', wallet: VALID_WALLET, exp: Math.floor(Date.now() / 1000) + 3600 }, - process.env.JWT_SECRET - ); + return jwtFactory({ sub: 'admin', role: 'issuer', wallet: VALID_WALLET }); } + // ── admin API key routes ────────────────────────────────────────────────────── describe('POST /admin/api-keys', () => { @@ -37,10 +36,8 @@ describe('POST /admin/api-keys', () => { }); it('requires issuer role', async () => { - const token = jwt.sign( - { sub: 'p', role: 'patient', wallet: VALID_WALLET, exp: Math.floor(Date.now() / 1000) + 3600 }, - process.env.JWT_SECRET - ); + const token = jwtFactory({ sub: 'p', role: 'patient', wallet: VALID_WALLET }); + const res = await request(app) .post('/admin/api-keys') .set('Authorization', `Bearer ${token}`) diff --git a/backend/tests/wallet-validation.test.js b/backend/tests/wallet-validation.test.js index 84178a0..7a8fa63 100644 --- a/backend/tests/wallet-validation.test.js +++ b/backend/tests/wallet-validation.test.js @@ -1,6 +1,6 @@ -const jwt = require('jsonwebtoken'); const request = require('supertest'); const StellarSdk = require('@stellar/stellar-sdk'); +const { jwtFactory, vaccinationRecordFactory } = require('./factories'); jest.mock('../src/stellar/soroban', () => ({ invokeContract: jest.fn(), @@ -11,8 +11,10 @@ jest.mock('@stellar/stellar-sdk', () => { const originalModule = jest.requireActual('@stellar/stellar-sdk'); return { ...originalModule, + Keypair: originalModule.Keypair, scValToNative: jest.fn(), Address: { + ...originalModule.Address, fromString: jest.fn((address) => ({ toScVal: () => ({}), })), @@ -20,22 +22,21 @@ jest.mock('@stellar/stellar-sdk', () => { }; }); + + const { invokeContract, simulateContract } = require('../src/stellar/soroban'); + + process.env.JWT_SECRET = 'test-jwt-secret'; const app = require('../src/app'); const validPatientWallet = StellarSdk.Keypair.random().publicKey(); const validIssuerWallet = StellarSdk.Keypair.random().publicKey(); -const issuerToken = jwt.sign( - { publicKey: validIssuerWallet, role: 'issuer' }, - process.env.JWT_SECRET -); -const patientToken = jwt.sign( - { publicKey: validPatientWallet, role: 'patient' }, - process.env.JWT_SECRET -); +const issuerToken = jwtFactory({ publicKey: validIssuerWallet, role: 'issuer' }); +const patientToken = jwtFactory({ publicKey: validPatientWallet, role: 'patient' }); + // Correct length and G-prefix but invalid checksum const checksumInvalidWallet = `G${'A'.repeat(55)}`; @@ -44,6 +45,8 @@ beforeEach(() => { jest.clearAllMocks(); }); + + describe('Wallet validation — POST /vaccination/issue', () => { it('rejects malformed patient_address', async () => { const res = await request(app) @@ -93,7 +96,8 @@ describe('Wallet validation — POST /vaccination/issue', () => { }); expect(res.status).toBe(200); - expect(res.body).toEqual({ success: true, token_id: 'token-1' }); + expect(res.body).toMatchObject({ success: true, tokenId: 'token-1' }); + expect(invokeContract).toHaveBeenCalledTimes(1); }); }); @@ -146,19 +150,23 @@ describe('Wallet validation — GET /verify/:wallet', () => { }); it('accepts a valid wallet and returns vaccination status', async () => { + const record = vaccinationRecordFactory({ vaccine_name: 'MMR' }); simulateContract.mockResolvedValue({ fake: 'scval' }); jest .spyOn(StellarSdk, 'scValToNative') - .mockReturnValue([true, [{ vaccine: 'MMR' }]]); + .mockReturnValue([true, [{ vaccine: record.vaccine_name }]]); + + const res = await request(app) + .get(`/verify/${validPatientWallet}`) + .set('Authorization', `Bearer ${patientToken}`); - const res = await request(app).get(`/verify/${validPatientWallet}`); expect(res.status).toBe(200); expect(res.body).toEqual({ wallet: validPatientWallet, vaccinated: true, record_count: 1, - records: [{ vaccine: 'MMR' }], + records: [{ vaccine: record.vaccine_name }], }); expect(simulateContract).toHaveBeenCalledTimes(1); }); diff --git a/frontend/package.json b/frontend/package.json index 6341394..eb96bee 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,16 +3,17 @@ "version": "1.0.0", "private": true, "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.22.0", "@stellar/freighter-api": "^2.0.0", "@stellar/stellar-sdk": "^12.0.0", "i18next": "^23.11.5", - "react-i18next": "^14.1.2" + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-i18next": "^14.1.2", + "react-router-dom": "^6.22.0" }, "devDependencies": { "@vitejs/plugin-react": "^4.2.1", + "msw": "^2.13.6", "vite": "^5.1.4" }, "scripts": { diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index ec79161..3e89185 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -6,12 +6,24 @@ import App from './App'; import ErrorBoundary from './components/ErrorBoundary'; import './index.css'; -ReactDOM.createRoot(document.getElementById('root')).render( - - - - - - - -); +async function enableMocking() { + if (!import.meta.env.DEV) { + return; + } + const { worker } = await import('./mocks/browser'); + return worker.start(); +} + + +enableMocking().then(() => { + ReactDOM.createRoot(document.getElementById('root')).render( + + + + + + + + ); +}); + diff --git a/frontend/src/mocks/browser.js b/frontend/src/mocks/browser.js new file mode 100644 index 0000000..0a56427 --- /dev/null +++ b/frontend/src/mocks/browser.js @@ -0,0 +1,4 @@ +import { setupWorker } from 'msw/browser'; +import { handlers } from './handlers'; + +export const worker = setupWorker(...handlers); diff --git a/frontend/src/mocks/handlers.js b/frontend/src/mocks/handlers.js new file mode 100644 index 0000000..868718d --- /dev/null +++ b/frontend/src/mocks/handlers.js @@ -0,0 +1,103 @@ +import { http, HttpResponse } from 'msw'; + +export const handlers = [ + // Authentication + http.post('/auth/sep10', async ({ request }) => { + const { public_key } = await request.json(); + return HttpResponse.json({ + transaction: 'AAAAAgAAAAB...', // Mock XDR + nonce: 'mock-nonce-' + Math.random().toString(36).substring(7) + }); + }), + + http.post('/auth/verify', async ({ request }) => { + const { transaction, nonce } = await request.json(); + return HttpResponse.json({ + token: 'mock-jwt-token', + publicKey: 'GA...', + role: 'patient' + }); + }), + + // Vaccination Records + http.get('/vaccination/:wallet', ({ params }) => { + const { wallet } = params; + return HttpResponse.json({ + wallet, + records: [ + { + vaccine_name: 'MMR', + date_administered: '2026-01-01', + lot_number: 'LOT123', + issuer_address: 'GISS...' + } + ] + }); + }), + + http.post('/vaccination/issue', async ({ request }) => { + const payload = await request.json(); + return HttpResponse.json({ + success: true, + tokenId: 'token-' + Math.floor(Math.random() * 1000), + transactionHash: 'hash-' + Math.random().toString(36).substring(7) + }); + }), + + // Verification + http.get('/verify/:wallet', ({ params }) => { + const { wallet } = params; + return HttpResponse.json({ + wallet, + vaccinated: true, + record_count: 1, + records: [{ vaccine: 'MMR', date: '2026-01-01' }] + }); + }), + + // Patient Registration + http.post('/patient/register', async ({ request }) => { + const { publicKey } = await request.json(); + return HttpResponse.json({ success: true, wallet: publicKey || 'GB...' }); + }), + + // Admin / API Keys + http.get('/admin/api-keys', () => { + return HttpResponse.json([ + { + id: '1', + label: 'Default School District', + created_at: new Date().toISOString(), + revoked: false + } + ]); + }), + + http.post('/admin/api-keys', async ({ request }) => { + const { label } = await request.json(); + return HttpResponse.json({ + id: 'key-' + Math.random().toString(36).substring(7), + label: label || 'New Key', + key: 'sk_' + Math.random().toString(36).substring(7) + Math.random().toString(36).substring(7), + created_at: new Date().toISOString(), + revoked: false + }); + }), + + http.delete('/admin/api-keys/:id', () => { + return HttpResponse.json({ revoked: true }); + }), + + // Health check + http.get('/health', () => { + return HttpResponse.json({ status: 'ok' }); + }), + + // Events + http.get('/events', () => { + return HttpResponse.json({ + events: [], + count: 0 + }); + }), +]; diff --git a/frontend/src/mocks/server.js b/frontend/src/mocks/server.js new file mode 100644 index 0000000..e52fee0 --- /dev/null +++ b/frontend/src/mocks/server.js @@ -0,0 +1,4 @@ +import { setupServer } from 'msw/node'; +import { handlers } from './handlers'; + +export const server = setupServer(...handlers); diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..88b26ea --- /dev/null +++ b/tests/README.md @@ -0,0 +1,47 @@ +# Testing in VacciChain + +This document describes the shared factories and mocks used for testing in VacciChain. + +## Backend Factories + +Located in `backend/tests/factories/`. + +### Vaccination Record Factory +Generates mock vaccination records with realistic data. +```javascript +const { vaccinationRecordFactory } = require('./factories'); +const record = vaccinationRecordFactory({ vaccine_name: 'COVID-19' }); +``` + +### JWT Factory +Generates signed JWTs for different roles (`patient`, `issuer`). +```javascript +const { jwtFactory } = require('./factories'); +const token = jwtFactory({ role: 'issuer' }); +``` + +### SEP-10 Challenge Factory +Generates mock SEP-10 challenge transactions. +```javascript +const { sep10ChallengeFactory } = require('./factories'); +const challenge = sep10ChallengeFactory('GB...'); +``` + +## Frontend MSW Handlers + +Located in `frontend/src/mocks/handlers.js`. +These handlers mock all API endpoints used by the frontend. + +### Supported Endpoints +- `POST /auth/sep10` +- `POST /auth/verify` +- `GET /vaccination/:wallet` +- `POST /vaccination/issue` +- `GET /verify/:wallet` +- `POST /patient/register` +- `GET /admin/api-keys` +- `POST /admin/api-keys` +- `DELETE /admin/api-keys/:id` + +## Migrating Existing Tests +All backend tests have been migrated to use these factories. When adding new tests, please utilize the factories instead of manual data setup to ensure tests remain maintainable and less brittle.