import { BadRequestException } from '@nestjs/common'; import * as forge from 'node-forge'; import { beforeAll, describe, expect, it, vi } from 'vitest'; import { seedCertManagerModule } from './cert-manager.seed'; import { CertManagerService } from './cert-manager.service'; // --------------------------------------------------------------------------- // Test helpers // --------------------------------------------------------------------------- /** Generate a self-signed X.509 cert via node-forge (RSA 1024 — fast for tests) */ function generateSelfSignedCert(): forge.pki.Certificate { const keys = forge.pki.rsa.generateKeyPair(1024); const cert = forge.pki.createCertificate(); cert.publicKey = keys.publicKey; cert.serialNumber = '01'; cert.validity.notBefore = new Date(); cert.validity.notAfter = new Date(); cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1); const attrs = [{ name: 'commonName', value: 'test.example.com' }]; cert.setSubject(attrs); cert.setIssuer(attrs); cert.sign(keys.privateKey, forge.md.sha256.create()); return cert; } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe('seedCertManagerModule', () => { it('calls moduleRegistryService.seedModule once with cert-manager slug', async () => { const mockSeedModule = vi.fn().mockResolvedValue(undefined); const mockModuleRegistryService = { seedModule: mockSeedModule } as any; await seedCertManagerModule(mockModuleRegistryService); expect(mockSeedModule).toHaveBeenCalledTimes(1); const arg = mockSeedModule.mock.calls[0][0]; expect(arg.slug).toBe('cert-manager'); expect(arg.category).toBe('security-tools'); expect(arg.isSystem).toBe(true); }); }); describe('CertManagerService helpers', () => { let service: CertManagerService; let testCert: forge.pki.Certificate; beforeAll(() => { service = new CertManagerService(); testCert = generateSelfSignedCert(); }); // ------------------------------------------------------------------------- // detectFormat // ------------------------------------------------------------------------- describe('detectFormat', () => { it('returns "pfx" for .pfx extension', () => { const buf = Buffer.from([0x30, 0x82]); // arbitrary binary expect(service.detectFormat('cert.pfx', buf)).toBe('pfx'); }); it('returns "p7b" for .p7b extension', () => { const buf = Buffer.from([0x30, 0x82]); expect(service.detectFormat('cert.p7b', buf)).toBe('p7b'); }); it('returns "der" for .der extension', () => { const buf = Buffer.from([0x30, 0x82]); expect(service.detectFormat('cert.der', buf)).toBe('der'); }); it('returns "pem" for .pem extension', () => { const buf = Buffer.from('-----BEGIN CERTIFICATE-----\nfake\n-----END CERTIFICATE-----'); expect(service.detectFormat('cert.pem', buf)).toBe('pem'); }); it('returns "pem" for .cer extension with PEM content', () => { const buf = Buffer.from('-----BEGIN CERTIFICATE-----\nfake\n-----END CERTIFICATE-----'); expect(service.detectFormat('cert.cer', buf)).toBe('pem'); }); it('returns "der" for .cer extension with binary (non-PEM) content', () => { const buf = Buffer.from([0x30, 0x82, 0x01, 0x00]); expect(service.detectFormat('cert.cer', buf)).toBe('der'); }); }); // ------------------------------------------------------------------------- // getFingerprint // ------------------------------------------------------------------------- describe('getFingerprint', () => { it('returns uppercase colon-separated hex for sha256', () => { const fp = service.getFingerprint(testCert, 'sha256'); // e.g. "AA:BB:CC:..." expect(fp).toMatch(/^[0-9A-F]{2}(:[0-9A-F]{2})+$/); }); it('returns uppercase colon-separated hex for sha1', () => { const fp = service.getFingerprint(testCert, 'sha1'); expect(fp).toMatch(/^[0-9A-F]{2}(:[0-9A-F]{2})+$/); }); }); // ------------------------------------------------------------------------- // parsePemChain // ------------------------------------------------------------------------- describe('parsePemChain', () => { it('returns array of length 2 for two concatenated cert PEMs', () => { const cert1 = generateSelfSignedCert(); const cert2 = generateSelfSignedCert(); const pem1 = forge.pki.certificateToPem(cert1); const pem2 = forge.pki.certificateToPem(cert2); const chain = pem1 + '\n' + pem2; const parsed = service.parsePemChain(chain); expect(parsed).toHaveLength(2); }); it('returns array of length 1 for a single PEM', () => { const pem = forge.pki.certificateToPem(testCert); const parsed = service.parsePemChain(pem); expect(parsed).toHaveLength(1); }); }); }); // --------------------------------------------------------------------------- // parseCert — RED tests (CERT-01, CERT-05 read half) // --------------------------------------------------------------------------- describe('parseCert', () => { let service: CertManagerService; let certPem: string; let certDerBuffer: Buffer; let pfxBuffer: Buffer; const CN = 'parsecert.example.com'; beforeAll(() => { service = new CertManagerService(); // Create a cert+key pair for all fixtures (RSA-1024 for speed) const keys = forge.pki.rsa.generateKeyPair(1024); const cert = forge.pki.createCertificate(); cert.publicKey = keys.publicKey; cert.serialNumber = '01'; cert.validity.notBefore = new Date(); cert.validity.notAfter = new Date(); cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1); const attrs = [{ name: 'commonName', value: CN }]; cert.setSubject(attrs); cert.setIssuer(attrs); cert.sign(keys.privateKey, forge.md.sha256.create()); // PEM fixture certPem = forge.pki.certificateToPem(cert); // DER fixture — binary encoding required (Pitfall 1) const derBytes = forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes(); certDerBuffer = Buffer.from(derBytes, 'binary'); // PFX fixture with password 'secret' const p12Asn1 = forge.pkcs12.toPkcs12Asn1( keys.privateKey, [cert], 'secret', { algorithm: '3des' }, ); const p12DerBytes = forge.asn1.toDer(p12Asn1).getBytes(); pfxBuffer = Buffer.from(p12DerBytes, 'binary'); }, 15000); // 15s timeout — RSA keygen can be slow in pure JS it('returns CertDetails with subject.cn, fingerprint.sha256, keyType RSA, keyBits, isExpired false for PEM input', async () => { // RED: parseCert currently throws NotImplementedException — this test will FAIL const result = await (service.parseCert({ pemText: certPem }) as Promise); expect(result.subject.cn).toBe(CN); expect(result.fingerprint.sha256).toMatch(/^[0-9A-F]{2}(:[0-9A-F]{2})+$/); expect(result.keyType).toBe('RSA'); expect(result.keyBits).toBe(1024); expect(result.validity.isExpired).toBe(false); }); it('returns CertDetails with matching subject.cn for DER input', async () => { // RED: parseCert throws NotImplementedException — this test will FAIL const result = await (service.parseCert({ file: { originalname: 'c.der', buffer: certDerBuffer }, }) as Promise); expect(result.subject.cn).toBe(CN); }); it('returns CertDetails for PFX with correct password', async () => { // RED: parseCert throws NotImplementedException — this test will FAIL const result = await (service.parseCert({ file: { originalname: 'c.pfx', buffer: pfxBuffer }, password: 'secret', }) as Promise); expect(result.subject.cn).toBe(CN); }); it('throws BadRequestException for PFX with wrong password', async () => { // RED: currently throws NotImplementedException (not BadRequestException) — FAIL await expect( service.parseCert({ file: { originalname: 'c.pfx', buffer: pfxBuffer }, password: 'wrong', }), ).rejects.toThrow(BadRequestException); }); it('throws BadRequestException for malformed PEM input', async () => { // RED: currently throws NotImplementedException (not BadRequestException) — FAIL await expect( service.parseCert({ pemText: 'not a cert' }), ).rejects.toThrow(BadRequestException); }); }); // --------------------------------------------------------------------------- // splitCerts — RED tests (CERT-02) // --------------------------------------------------------------------------- describe('splitCerts', () => { let service: CertManagerService; let cert1Pem: string; let cert2Pem: string; let fullchainBuffer: Buffer; let p7bBuffer: Buffer; const CN1 = 'split1.example.com'; const CN2 = 'split2.example.com'; beforeAll(() => { service = new CertManagerService(); // Build cert 1 (RSA-1024 for speed) const keys1 = forge.pki.rsa.generateKeyPair(1024); const c1 = forge.pki.createCertificate(); c1.publicKey = keys1.publicKey; c1.serialNumber = '01'; c1.validity.notBefore = new Date(); c1.validity.notAfter = new Date(); c1.validity.notAfter.setFullYear(c1.validity.notBefore.getFullYear() + 1); const attrs1 = [{ name: 'commonName', value: CN1 }]; c1.setSubject(attrs1); c1.setIssuer(attrs1); c1.sign(keys1.privateKey, forge.md.sha256.create()); cert1Pem = forge.pki.certificateToPem(c1); // Build cert 2 const keys2 = forge.pki.rsa.generateKeyPair(1024); const c2 = forge.pki.createCertificate(); c2.publicKey = keys2.publicKey; c2.serialNumber = '02'; c2.validity.notBefore = new Date(); c2.validity.notAfter = new Date(); c2.validity.notAfter.setFullYear(c2.validity.notBefore.getFullYear() + 1); const attrs2 = [{ name: 'commonName', value: CN2 }]; c2.setSubject(attrs2); c2.setIssuer(attrs2); c2.sign(keys2.privateKey, forge.md.sha256.create()); cert2Pem = forge.pki.certificateToPem(c2); // Fullchain fixture: two PEMs concatenated const fullchainPem = cert1Pem + '\n' + cert2Pem; fullchainBuffer = Buffer.from(fullchainPem, 'utf-8'); // P7B fixture: PEM-wrapped PKCS7 SignedData bundle with both certs const p7 = forge.pkcs7.createSignedData(); p7.addCertificate(c1); p7.addCertificate(c2); const p7DerBytes = forge.asn1.toDer(p7.toAsn1()).getBytes(); const p7PemStr = forge.pem.encode({ type: 'PKCS7', body: p7DerBytes }); p7bBuffer = Buffer.from(p7PemStr, 'utf-8'); }, 30000); // 30s — two RSA-1024 keygens it('returns count 2 and two certs each with a single PEM block and correct CN for fullchain PEM', async () => { // RED: splitCerts throws NotImplementedException — FAIL const result = await (service.splitCerts({ file: { originalname: 'fullchain.pem', buffer: fullchainBuffer }, }) as Promise); expect(result.count).toBe(2); expect(result.certs).toHaveLength(2); // Each cert content decodes to exactly one BEGIN CERTIFICATE block for (const entry of result.certs) { const decoded = Buffer.from(entry.content, 'base64').toString('utf-8'); const matches = decoded.match(/-----BEGIN CERTIFICATE-----/g); expect(matches).toHaveLength(1); } // CN values are present const cns = result.certs.map((c: any) => c.subject.cn); expect(cns).toContain(CN1); expect(cns).toContain(CN2); // validity.notAfter is present and looks like ISO 8601 for (const entry of result.certs) { expect(entry.validity.notAfter).toMatch(/^\d{4}-\d{2}-\d{2}T/); } }); it('returns enclosed certs from a P7B PEM bundle', async () => { // RED: splitCerts throws NotImplementedException — FAIL const result = await (service.splitCerts({ file: { originalname: 'bundle.p7b', buffer: p7bBuffer }, }) as Promise); expect(result.count).toBeGreaterThanOrEqual(1); expect(result.certs.length).toBeGreaterThanOrEqual(1); }); it('throws BadRequestException for malformed input', async () => { // RED: splitCerts throws NotImplementedException (not BadRequestException) — FAIL await expect( service.splitCerts({ file: { originalname: 'bad.pem', buffer: Buffer.from('this is garbage') }, }), ).rejects.toThrow(BadRequestException); }); }); // --------------------------------------------------------------------------- // 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) // --------------------------------------------------------------------------- describe('convertCert', () => { let service: CertManagerService; let certPem: string; let certDerBuffer: Buffer; const CN = 'convert.example.com'; beforeAll(() => { service = new CertManagerService(); // RSA-1024 self-signed cert (fast for tests) const keys = forge.pki.rsa.generateKeyPair(1024); const cert = forge.pki.createCertificate(); cert.publicKey = keys.publicKey; cert.serialNumber = '01'; cert.validity.notBefore = new Date(); cert.validity.notAfter = new Date(); cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1); const attrs = [{ name: 'commonName', value: CN }]; cert.setSubject(attrs); cert.setIssuer(attrs); cert.sign(keys.privateKey, forge.md.sha256.create()); // PEM fixture certPem = forge.pki.certificateToPem(cert); // DER fixture — binary encoding required (Pitfall 1) const derBytes = forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes(); certDerBuffer = Buffer.from(derBytes, 'binary'); }, 15000); it('PEM→DER: returns FileResponse with correct mimeType and round-trip identity', async () => { // RED: convertCert throws NotImplementedException — this test will FAIL // eslint-disable-next-line @typescript-eslint/no-explicit-any const result = await (service.convertCert({ pemText: certPem, targetFormat: 'der' } as any) as Promise); expect(result.mimeType).toBe('application/x-x509-ca-cert'); expect(result.filename).toContain('converted'); expect(result.content).toBeTruthy(); // Round-trip identity: decode base64 DER → re-parse → verify CN matches original const derBuf = Buffer.from(result.content as string, 'base64'); const asn1 = forge.asn1.fromDer(forge.util.createBuffer(derBuf.toString('binary'))); const reparsed = forge.pki.certificateFromAsn1(asn1); expect(reparsed.subject.getField('CN')?.value).toBe(CN); }); it('DER→PEM: returned PEM parses to cert with same CN (round-trip identity)', async () => { // RED: convertCert throws NotImplementedException — this test will FAIL const result = await (service.convertCert({ file: { originalname: 'c.der', buffer: certDerBuffer }, targetFormat: 'pem', // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any) as Promise); expect(result.mimeType).toBe('application/x-pem-file'); // Decode base64 PEM content → parse → verify CN matches original const pemStr = Buffer.from(result.content as string, 'base64').toString('utf-8'); const reparsed = forge.pki.certificateFromPem(pemStr); expect(reparsed.subject.getField('CN')?.value).toBe(CN); }); it('PEM→P7B: returned P7B contains the original certificate', async () => { // RED: convertCert throws NotImplementedException — this test will FAIL // eslint-disable-next-line @typescript-eslint/no-explicit-any const result = await (service.convertCert({ pemText: certPem, targetFormat: 'p7b' } as any) as Promise); expect(result.mimeType).toBe('application/x-pkcs7-certificates'); expect(result.filename).toContain('converted'); // Decode base64 P7B (PEM-wrapped PKCS7) and verify at least 1 cert enclosed const p7bContent = Buffer.from(result.content as string, 'base64').toString('utf-8'); // eslint-disable-next-line @typescript-eslint/no-explicit-any const p7 = forge.pkcs7.messageFromPem(p7bContent) as any; expect((p7.certificates as forge.pki.Certificate[]).length).toBeGreaterThanOrEqual(1); }); it('throws BadRequestException for malformed PEM input', async () => { // RED: convertCert throws NotImplementedException (not BadRequestException) — FAIL await expect( // eslint-disable-next-line @typescript-eslint/no-explicit-any service.convertCert({ pemText: 'garbage input not a cert', targetFormat: 'der' } as any), ).rejects.toThrow(BadRequestException); }); });