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:
2026-07-02 07:47:28 +02:00
parent db7a85cc4c
commit f43e92c49f
@@ -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)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------