fix(federation): use real PEM certs in enrollment + ca service tests (#507)
This commit was merged in pull request #507.
This commit is contained in:
@@ -24,10 +24,11 @@
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest';
|
||||
import { GoneException, NotFoundException } from '@nestjs/common';
|
||||
import type { Db } from '@mosaicstack/db';
|
||||
import { EnrollmentService } from '../enrollment.service.js';
|
||||
import { makeSelfSignedCert } from './helpers/test-cert.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test constants
|
||||
@@ -38,10 +39,18 @@ const PEER_ID = 'p2222222-2222-2222-2222-222222222222';
|
||||
const USER_ID = 'u3333333-3333-3333-3333-333333333333';
|
||||
const TOKEN = 'a'.repeat(64); // 64-char hex
|
||||
|
||||
const MOCK_CERT_PEM = '-----BEGIN CERTIFICATE-----\nMOCK\n-----END CERTIFICATE-----\n';
|
||||
const MOCK_CHAIN_PEM = MOCK_CERT_PEM + MOCK_CERT_PEM;
|
||||
// Real self-signed EC P-256 cert — populated once in beforeAll.
|
||||
// Required because EnrollmentService.extractCertNotAfter calls new X509Certificate(certPem)
|
||||
// with strict parsing (PR #501 HIGH-2: no silent fallback).
|
||||
let REAL_CERT_PEM: string;
|
||||
|
||||
const MOCK_CHAIN_PEM = () => REAL_CERT_PEM + REAL_CERT_PEM;
|
||||
const MOCK_SERIAL = 'ABCD1234';
|
||||
|
||||
beforeAll(async () => {
|
||||
REAL_CERT_PEM = await makeSelfSignedCert();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Factory helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -103,11 +112,27 @@ function makeDb({
|
||||
const claimUpdateMock = vi.fn().mockReturnValue({ set: setClaimMock });
|
||||
|
||||
// transaction(cb) — cb receives txMock; txMock has update + insert
|
||||
const txInsertValues = vi.fn().mockResolvedValue(undefined);
|
||||
const txInsertMock = vi.fn().mockReturnValue({ values: txInsertValues });
|
||||
const txWhereUpdate = vi.fn().mockResolvedValue(undefined);
|
||||
//
|
||||
// The tx mock must support two tx.update() call patterns (CRIT-2, PR #501):
|
||||
// 1. Grant activation: .update().set().where().returning() → resolves to [{ id }]
|
||||
// 2. Peer update: .update().set().where() → resolves to undefined
|
||||
//
|
||||
// We achieve this by making txWhereUpdate return an object with BOTH a thenable
|
||||
// interface (so `await tx.update().set().where()` works) AND a .returning() method.
|
||||
const txGrantActivatedRow = { id: GRANT_ID };
|
||||
const txReturningMock = vi.fn().mockResolvedValue([txGrantActivatedRow]);
|
||||
const txWhereUpdate = vi.fn().mockReturnValue({
|
||||
// .returning() for grant activation (first tx.update call)
|
||||
returning: txReturningMock,
|
||||
// thenables so `await tx.update().set().where()` also works for peer update
|
||||
then: (resolve: (v: undefined) => void) => resolve(undefined),
|
||||
catch: () => undefined,
|
||||
finally: () => undefined,
|
||||
});
|
||||
const txSetMock = vi.fn().mockReturnValue({ where: txWhereUpdate });
|
||||
const txUpdateMock = vi.fn().mockReturnValue({ set: txSetMock });
|
||||
const txInsertValues = vi.fn().mockResolvedValue(undefined);
|
||||
const txInsertMock = vi.fn().mockReturnValue({ values: txInsertValues });
|
||||
const txMock = { update: txUpdateMock, insert: txInsertMock };
|
||||
const transactionMock = vi
|
||||
.fn()
|
||||
@@ -132,6 +157,7 @@ function makeDb({
|
||||
txInsertValues,
|
||||
txInsertMock,
|
||||
txWhereUpdate,
|
||||
txReturningMock,
|
||||
txSetMock,
|
||||
txUpdateMock,
|
||||
txMock,
|
||||
@@ -146,11 +172,13 @@ function makeDb({
|
||||
|
||||
function makeCaService() {
|
||||
return {
|
||||
issueCert: vi.fn().mockResolvedValue({
|
||||
certPem: MOCK_CERT_PEM,
|
||||
certChainPem: MOCK_CHAIN_PEM,
|
||||
// REAL_CERT_PEM is populated by beforeAll — safe to reference via closure here
|
||||
// because makeCaService() is only called after the suite's beforeAll runs.
|
||||
issueCert: vi.fn().mockImplementation(async () => ({
|
||||
certPem: REAL_CERT_PEM,
|
||||
certChainPem: MOCK_CHAIN_PEM(),
|
||||
serialNumber: MOCK_SERIAL,
|
||||
}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -301,29 +329,29 @@ describe('EnrollmentService.redeem — success path', () => {
|
||||
});
|
||||
caService.issueCert.mockImplementation(async () => {
|
||||
callOrder.push('issueCert');
|
||||
return { certPem: MOCK_CERT_PEM, certChainPem: MOCK_CHAIN_PEM, serialNumber: MOCK_SERIAL };
|
||||
return { certPem: REAL_CERT_PEM, certChainPem: MOCK_CHAIN_PEM(), serialNumber: MOCK_SERIAL };
|
||||
});
|
||||
|
||||
await service.redeem(TOKEN, MOCK_CERT_PEM);
|
||||
await service.redeem(TOKEN, '---CSR---');
|
||||
|
||||
expect(callOrder).toEqual(['claim', 'issueCert']);
|
||||
});
|
||||
|
||||
it('calls CaService.issueCert with grantId, subjectUserId, csrPem, ttlSeconds=300', async () => {
|
||||
await service.redeem(TOKEN, MOCK_CERT_PEM);
|
||||
await service.redeem(TOKEN, '---CSR---');
|
||||
|
||||
expect(caService.issueCert).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
grantId: GRANT_ID,
|
||||
subjectUserId: USER_ID,
|
||||
csrPem: MOCK_CERT_PEM,
|
||||
csrPem: '---CSR---',
|
||||
ttlSeconds: 300,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('runs activate grant + peer update + audit inside a transaction', async () => {
|
||||
await service.redeem(TOKEN, MOCK_CERT_PEM);
|
||||
await service.redeem(TOKEN, '---CSR---');
|
||||
|
||||
expect(db._mocks.transactionMock).toHaveBeenCalledOnce();
|
||||
// tx.update called twice: activate grant + update peer
|
||||
@@ -333,17 +361,17 @@ describe('EnrollmentService.redeem — success path', () => {
|
||||
});
|
||||
|
||||
it('activates grant (sets status=active) inside the transaction', async () => {
|
||||
await service.redeem(TOKEN, MOCK_CERT_PEM);
|
||||
await service.redeem(TOKEN, '---CSR---');
|
||||
|
||||
expect(db._mocks.txSetMock).toHaveBeenCalledWith(expect.objectContaining({ status: 'active' }));
|
||||
});
|
||||
|
||||
it('updates the federationPeers row with certPem, certSerial, state=active inside the transaction', async () => {
|
||||
await service.redeem(TOKEN, MOCK_CERT_PEM);
|
||||
await service.redeem(TOKEN, '---CSR---');
|
||||
|
||||
expect(db._mocks.txSetMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
certPem: MOCK_CERT_PEM,
|
||||
certPem: REAL_CERT_PEM,
|
||||
certSerial: MOCK_SERIAL,
|
||||
state: 'active',
|
||||
}),
|
||||
@@ -351,7 +379,7 @@ describe('EnrollmentService.redeem — success path', () => {
|
||||
});
|
||||
|
||||
it('inserts an audit log row inside the transaction', async () => {
|
||||
await service.redeem(TOKEN, MOCK_CERT_PEM);
|
||||
await service.redeem(TOKEN, '---CSR---');
|
||||
|
||||
expect(db._mocks.txInsertValues).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -363,11 +391,11 @@ describe('EnrollmentService.redeem — success path', () => {
|
||||
});
|
||||
|
||||
it('returns { certPem, certChainPem } from CaService', async () => {
|
||||
const result = await service.redeem(TOKEN, MOCK_CERT_PEM);
|
||||
const result = await service.redeem(TOKEN, '---CSR---');
|
||||
|
||||
expect(result).toEqual({
|
||||
certPem: MOCK_CERT_PEM,
|
||||
certChainPem: MOCK_CHAIN_PEM,
|
||||
certPem: REAL_CERT_PEM,
|
||||
certChainPem: MOCK_CHAIN_PEM(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user