test(09-06): RED — failing mergeCerts spec (PEM chain + password-PFX round-trip)
- 4 new mergeCerts tests: PEM chain 2 blocks, PFX round-trip with password, missing PFX password → BadRequestException, garbage input → BadRequestException - Controller enforces 2-file minimum; service tests use 1-2 files directly - All 4 fail against NotImplementedException stub (RED confirmed) - Prior 23 tests remain green
This commit is contained in:
@@ -323,6 +323,122 @@ describe('splitCerts', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// mergeCerts — RED tests (CERT-03, CERT-05 write half)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('mergeCerts', () => {
|
||||||
|
let service: CertManagerService;
|
||||||
|
let certAPem: string;
|
||||||
|
let certBPem: string;
|
||||||
|
const CNA = 'merge-a.example.com';
|
||||||
|
const CNB = 'merge-b.example.com';
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
service = new CertManagerService();
|
||||||
|
|
||||||
|
// Cert A (RSA-1024 for speed)
|
||||||
|
const keysA = forge.pki.rsa.generateKeyPair(1024);
|
||||||
|
const certA = forge.pki.createCertificate();
|
||||||
|
certA.publicKey = keysA.publicKey;
|
||||||
|
certA.serialNumber = '01';
|
||||||
|
certA.validity.notBefore = new Date();
|
||||||
|
certA.validity.notAfter = new Date();
|
||||||
|
certA.validity.notAfter.setFullYear(certA.validity.notBefore.getFullYear() + 1);
|
||||||
|
const attrsA = [{ name: 'commonName', value: CNA }];
|
||||||
|
certA.setSubject(attrsA);
|
||||||
|
certA.setIssuer(attrsA);
|
||||||
|
certA.sign(keysA.privateKey, forge.md.sha256.create());
|
||||||
|
certAPem = forge.pki.certificateToPem(certA);
|
||||||
|
|
||||||
|
// Cert B
|
||||||
|
const keysB = forge.pki.rsa.generateKeyPair(1024);
|
||||||
|
const certB = forge.pki.createCertificate();
|
||||||
|
certB.publicKey = keysB.publicKey;
|
||||||
|
certB.serialNumber = '02';
|
||||||
|
certB.validity.notBefore = new Date();
|
||||||
|
certB.validity.notAfter = new Date();
|
||||||
|
certB.validity.notAfter.setFullYear(certB.validity.notBefore.getFullYear() + 1);
|
||||||
|
const attrsB = [{ name: 'commonName', value: CNB }];
|
||||||
|
certB.setSubject(attrsB);
|
||||||
|
certB.setIssuer(attrsB);
|
||||||
|
certB.sign(keysB.privateKey, forge.md.sha256.create());
|
||||||
|
certBPem = forge.pki.certificateToPem(certB);
|
||||||
|
}, 30000); // 30s — two RSA-1024 keygens
|
||||||
|
|
||||||
|
it('returns PEM chain with 2 BEGIN CERTIFICATE blocks when merging 2 PEM files', async () => {
|
||||||
|
// RED: mergeCerts throws NotImplementedException — FAIL
|
||||||
|
const result = await (service.mergeCerts({
|
||||||
|
files: [
|
||||||
|
{ originalname: 'certA.pem', buffer: Buffer.from(certAPem, 'utf-8') },
|
||||||
|
{ originalname: 'certB.pem', buffer: Buffer.from(certBPem, 'utf-8') },
|
||||||
|
],
|
||||||
|
outputFormat: 'pem',
|
||||||
|
}) as Promise<any>);
|
||||||
|
|
||||||
|
expect(result.mimeType).toBe('application/x-pem-file');
|
||||||
|
expect(result.filename).toContain('chain');
|
||||||
|
|
||||||
|
// Decoded content must have exactly 2 cert blocks
|
||||||
|
const decoded = Buffer.from(result.content as string, 'base64').toString('utf-8');
|
||||||
|
const blocks = decoded.match(/-----BEGIN CERTIFICATE-----/g);
|
||||||
|
expect(blocks).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns password-protected PFX that round-trips with the correct password', async () => {
|
||||||
|
// RED: mergeCerts throws NotImplementedException — FAIL
|
||||||
|
// Note: 2-file minimum is enforced at the controller level; service accepts 1 file for PFX
|
||||||
|
const result = await (service.mergeCerts({
|
||||||
|
files: [
|
||||||
|
{ originalname: 'certA.pem', buffer: Buffer.from(certAPem, 'utf-8') },
|
||||||
|
],
|
||||||
|
outputFormat: 'pfx',
|
||||||
|
password: 'secret',
|
||||||
|
}) as Promise<any>);
|
||||||
|
|
||||||
|
expect(result.mimeType).toBe('application/x-pkcs12');
|
||||||
|
expect(result.filename).toBe('bundle.pfx');
|
||||||
|
|
||||||
|
// Round-trip: decode base64 PFX → re-parse with password 'secret' → verify cert bag present
|
||||||
|
const pfxBuf = Buffer.from(result.content as string, 'base64');
|
||||||
|
const p12Asn1 = forge.asn1.fromDer(
|
||||||
|
forge.util.createBuffer(pfxBuf.toString('binary')),
|
||||||
|
);
|
||||||
|
// Should NOT throw with the correct password (Open Question 1 resolution)
|
||||||
|
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, 'secret');
|
||||||
|
const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });
|
||||||
|
const bags = certBags[forge.pki.oids.certBag] ?? [];
|
||||||
|
expect(bags.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws BadRequestException when PFX output is requested without a password', async () => {
|
||||||
|
// RED: mergeCerts throws NotImplementedException (not BadRequestException) — FAIL
|
||||||
|
await expect(
|
||||||
|
service.mergeCerts({
|
||||||
|
files: [
|
||||||
|
{ originalname: 'certA.pem', buffer: Buffer.from(certAPem, 'utf-8') },
|
||||||
|
{ originalname: 'certB.pem', buffer: Buffer.from(certBPem, 'utf-8') },
|
||||||
|
],
|
||||||
|
outputFormat: 'pfx',
|
||||||
|
// no password — should be rejected
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws BadRequestException for malformed (garbage) input files', async () => {
|
||||||
|
// RED: mergeCerts throws NotImplementedException (not BadRequestException) — FAIL
|
||||||
|
await expect(
|
||||||
|
service.mergeCerts({
|
||||||
|
files: [
|
||||||
|
{ originalname: 'bad.pem', buffer: Buffer.from('this is garbage') },
|
||||||
|
{ originalname: 'bad2.pem', buffer: Buffer.from('also garbage') },
|
||||||
|
],
|
||||||
|
outputFormat: 'pem',
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// convertCert — RED tests (CERT-04)
|
// convertCert — RED tests (CERT-04)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user