diff --git a/backend/package-lock.json b/backend/package-lock.json
index 10d87ec..172d694 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -21,6 +21,7 @@
"zod": "^4.3.6"
},
"devDependencies": {
+ "@faker-js/faker": "^8.4.1",
"jest": "^29.7.0",
"nodemon": "^3.0.2",
"supertest": "^6.3.4"
@@ -119,6 +120,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",
@@ -654,6 +656,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",
@@ -1689,6 +1708,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
@@ -2559,6 +2579,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 70bd234..3f36b08 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -30,6 +30,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 7312768..92a1160 100644
--- a/backend/src/app.js
+++ b/backend/src/app.js
@@ -18,6 +18,7 @@ const { getRpcServer } = require('./stellar/soroban');
const app = express();
+
app.use(cors());
app.use(express.json({ limit: config.BODY_LIMIT }));
@@ -42,6 +43,8 @@ app.use('/vaccination', vaccinationRoutes);
app.use('/verify', verifyRoutes);
app.use('/admin', adminRoutes);
app.use('/patient', patientRoutes);
+app.use('/events', eventsRoutes);
+
/**
* Health check endpoint.
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/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.