Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
]
},
"devDependencies": {
"@faker-js/faker": "^8.4.1",
"jest": "^29.7.0",
"nodemon": "^3.0.2",
"supertest": "^6.3.4"
Expand Down
3 changes: 3 additions & 0 deletions backend/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const { getRpcServer } = require('./stellar/soroban');

const app = express();


app.use(cors());
app.use(express.json({ limit: config.BODY_LIMIT }));

Expand All @@ -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.
Expand Down
17 changes: 7 additions & 10 deletions backend/tests/auth-claims.test.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -17,22 +17,19 @@ 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',
role: 'patient',
wallet: 'GB...',
exp: Math.floor(Date.now() / 1000) + 3600,
};
setAuthHeader(createToken(payload));
setAuthHeader(jwtFactory(payload));

authMiddleware(req, res, next);

Expand All @@ -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);

Expand All @@ -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);

Expand All @@ -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);

Expand All @@ -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);

Expand Down
9 changes: 9 additions & 0 deletions backend/tests/factories/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const vaccinationRecordFactory = require('./vaccinationRecord');
const jwtFactory = require('./jwt');
const sep10ChallengeFactory = require('./sep10');

module.exports = {
vaccinationRecordFactory,
jwtFactory,
sep10ChallengeFactory,
};
25 changes: 25 additions & 0 deletions backend/tests/factories/jwt.js
Original file line number Diff line number Diff line change
@@ -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;
50 changes: 50 additions & 0 deletions backend/tests/factories/sep10.js
Original file line number Diff line number Diff line change
@@ -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;
20 changes: 20 additions & 0 deletions backend/tests/factories/vaccinationRecord.js
Original file line number Diff line number Diff line change
@@ -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;
9 changes: 7 additions & 2 deletions backend/tests/indexer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`);

Expand All @@ -16,14 +18,15 @@ afterAll(() => {
});

describe('db — upsertEvents / queryEvents', () => {
const record = vaccinationRecordFactory({ vaccine_name: 'COVID-19', issuer_address: 'GISSUER1' });
const sample = [
{
id: 'evt-001',
event_type: 'VaccinationMinted',
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',
Expand All @@ -35,6 +38,7 @@ describe('db — upsertEvents / queryEvents', () => {
},
];


it('stores events and returns them', () => {
upsertEvents(sample);
const results = queryEvents();
Expand All @@ -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', () => {
Expand Down
13 changes: 4 additions & 9 deletions backend/tests/patient-register.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand All @@ -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}`);
Expand All @@ -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}`);
Expand Down
15 changes: 6 additions & 9 deletions backend/tests/verifier-api-key.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`);

Expand All @@ -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', () => {
Expand All @@ -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}`)
Expand Down
Loading