From f43e92c49f3cffad1885d2b237f255405a66a46e Mon Sep 17 00:00:00 2001 From: Schalli Date: Thu, 2 Jul 2026 07:47:28 +0200 Subject: [PATCH] =?UTF-8?q?test(09-06):=20RED=20=E2=80=94=20failing=20merg?= =?UTF-8?q?eCerts=20spec=20(PEM=20chain=20+=20password-PFX=20round-trip)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../cert-manager/cert-manager.service.spec.ts | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/apps/api/src/cert-manager/cert-manager.service.spec.ts b/apps/api/src/cert-manager/cert-manager.service.spec.ts index 9b3237d..16bfd2b 100644 --- a/apps/api/src/cert-manager/cert-manager.service.spec.ts +++ b/apps/api/src/cert-manager/cert-manager.service.spec.ts @@ -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); + + 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); + + 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) // ---------------------------------------------------------------------------