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)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user