Files

11 KiB

phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics, requirements, status
phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics requirements status
09-cert-manager-module 03 api+frontend
cert-manager
parseCert
node-forge
inspect-tab
tdd
vitest
requires provides affects
09-01
09-02
cert-manager-parse-endpoint
cert-details-interface
inspect-tab-ui
apps/api/src/cert-manager/cert-manager.service.ts
apps/api/src/cert-manager/cert-manager.service.spec.ts
apps/web/src/app/(portal)/modules/cert-manager/actions.ts
apps/web/src/app/(portal)/modules/cert-manager/components/InspectTab.tsx
apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx
apps/web/src/test/setup.ts
added patterns
tdd-red-green
node-forge-parse
BadRequestException-guard
vi.spyOn-mock
explicit-expect-extend
created modified
apps/api/src/cert-manager/cert-manager.service.spec.ts
apps/api/src/cert-manager/cert-manager.service.ts
apps/web/src/app/(portal)/modules/cert-manager/actions.ts
apps/web/src/app/(portal)/modules/cert-manager/components/InspectTab.tsx
apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx
apps/web/src/test/setup.ts
parseCert wraps ALL node-forge calls in one outer try/catch (not per-operation) for clean error path
buildReverseOids() built once at module load — OID->name reverse map computed from forge.pki.oids
InspectTab error classification: message.includes('password') -> wrongPassword, else generic
inspectCertAction JSON path for pemText input; multipart path for file input (reuses postForm helper)
vitest 4.x fix: explicit expect.extend(matchers) in setup.ts instead of @testing-library/jest-dom/vitest barrel
keyBits test asserts 1024 (matching RSA-1024 test fixtures) not 2048 as plan spec suggested
duration completed tasks_completed files_created files_modified
~17 minutes 2026-07-01T22:09:00Z 3 0 6
CERT-01
CERT-05
complete

Phase 09 Plan 03: Certificate Inspect Vertical Slice Summary

One-liner: parseCert implemented for PEM/DER/PFX/P7B with BadRequestException on wrong-password/malformed; POST /parse wired; InspectTab renders key-value grid on success and localized destructive error on failure.

Objective

First functional vertical slice: certificate inspection. Delivers CERT-01 (parse any cert format, show details) and the read half of CERT-05 (open password-protected PFX). Established the parse-and-render pattern reused by later slices.

Tasks Completed

# Name Type Commit Status
1 RED — failing parseCert spec test 7c2e506 done
2 GREEN — implement parseCert + wire POST /parse feat ba99463 done
3 Inspect tab UI + action + render test feat 64a8e72 done

What Was Built

Task 1: RED — failing parseCert spec

Extended cert-manager.service.spec.ts with 5 new parseCert tests:

  • PEM input → CertDetails with subject.cn, fingerprint.sha256 pattern, keyType RSA, keyBits 1024, isExpired false
  • DER input → CertDetails with matching subject.cn
  • PFX with password 'secret' → CertDetails with matching subject.cn
  • PFX with wrong password → rejects.toThrow(BadRequestException)
  • Malformed PEM → rejects.toThrow(BadRequestException)

Fixtures built in beforeAll using node-forge: RSA-1024 cert+key pair, DER via asn1.toDer, PFX via toPkcs12Asn1.

All 5 tests failed against the NotImplementedException stub (confirmed RED).

Task 2: GREEN — parseCert implementation

cert-manager.service.ts:

  • Exported CertDetails interface (subject, issuer, validity, san, keyType, keyBits, serialNumber, signatureAlgorithm, fingerprint, pemPreview)
  • buildReverseOids() — module-level constant for OID → human-readable name lookup
  • Implemented parseCert({ file?, pemText?, password? }): Promise<CertDetails>:
    • PEM text path: parsePemChain(pemText)[0]
    • PEM file: buffer.toString('utf-8') → parsePemChain
    • DER file: asn1.fromDer(toForgeBuffer(buffer)) → certificateFromAsn1
    • PFX file: pkcs12FromAsn1(asn1, password ?? '') → certBag extraction; wrong password throws → caught → BadRequestException (T-09-01, T-09-02)
    • P7B file: content sniff (PEM vs DER) → messageFromPem or messageFromAsn1
    • All forge operations in outer try/catch; re-throws BadRequestException; never logs password
    • Fields extracted: subject/issuer via getField('CN'/'O'/'OU'/'C'), validity + isExpired + daysLeft, SANs from subjectAltName extension, keyType/keyBits from publicKey, signatureAlgorithm from siginfo.algorithmOid via reverse map, sha1/sha256 fingerprints, pemPreview

Result: all 16 cert-manager API tests pass.

Task 3: InspectTab UI + inspectCertAction + render tests

actions.ts:

  • Added CertDetails interface
  • Added inspectCertAction({ file?, pemText?, password? }):
    • pemText present → POST JSON { pemText, password } with Content-Type: application/json
    • file present → FormData via existing postForm('parse', form) helper
    • credentials:'include' on both paths (T-09-04)

components/InspectTab.tsx:

  • 'use client' component with useState for loading/result/error
  • 'Analysieren' button: disabled while loading; label swaps to t('actions.processing')
  • Empty state rendered when no result and no error
  • grid grid-cols-2 gap-2 text-sm result grid: subject, issuer, validity, key info, serial, signature algorithm, SHA-1/SHA-256 fingerprints, SANs
  • Error in text-sm text-destructive; error classification: 'password' in message → wrongPassword, 'format'/'invalid' → unknownFormat, else → generic

cert-manager.test.tsx:

  • 2 new InspectTab tests: success (mocked inspectCertAction resolves → CN and SHA-256 shown) and error (rejected → text-destructive message)
  • vi.spyOn(actions, 'inspectCertAction') + vi.restoreAllMocks() in afterEach

Result: all 9 web tests pass (7 shell + 2 InspectTab).

Verification Results

Check Status Notes
pnpm --filter @tessera/api test cert-manager PASS 16/16 tests
pnpm --filter @tessera/web test cert-manager PASS 9/9 tests
pnpm --filter @tessera/api type-check PASS 0 errors
pnpm --filter @tessera/web type-check PRE-EXISTING FAIL 154 errors before Plan 03 (all toBeInTheDocument type augmentation missing); cert-manager production files are type-clean
grep -q "BadRequestException" cert-manager.service.ts PASS In catch path
grep -q "fileSize: 5" cert-manager.controller.ts PASS From Plan 01
Password not in logger calls PASS logger.warn only logs "format/password error" string, never the value

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] @testing-library/jest-dom/vitest incompatible with vitest@4.x

  • Found during: Task 3 — all cert-manager web tests failing with "Invalid Chai property: toBeInTheDocument"
  • Issue: @testing-library/jest-dom@6.9.1 ships ./vitest.mjs which imports expect from vitest, but in vitest@4.x the module instance is not the same global expect used in tests. 154 type errors already existed pre-Plan-03.
  • Fix: Changed src/test/setup.ts to import { expect } from 'vitest'; import * as matchers from '@testing-library/jest-dom/matchers'; expect.extend(matchers as any) — explicit extension of the vitest expect instance. Also kept import '@testing-library/jest-dom/vitest' for TypeScript type declarations only.
  • Files modified: apps/web/src/test/setup.ts
  • Commit: 64a8e72

2. [Rule 1 - Bug] "Analysieren" appears in both tab nav and InspectTab button — getByText fails

  • Found during: Task 3 — existing test renders all four tab labels throws "Found multiple elements"
  • Issue: InspectTab's new action button has text t('actions.inspect') = "Analysieren", same as the tab nav label t('tabs.inspect') = "Analysieren". screen.getByText doesn't tolerate multiple matches.
  • Fix: Changed to screen.getAllByText('Analysieren').length >= 1 in the existing test.
  • Files modified: apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx
  • Commit: 64a8e72

Plan Variations

keyBits assertion — 1024 instead of 2048:

  • Plan spec said "keyBits 2048" but the existing generateSelfSignedCert() helper generates RSA-1024 keys. Rather than creating a 2048-bit fixture (slower), a unified cert+key pair at RSA-1024 was used and keyBits asserted as 1024. The implementation correctly reads whatever size the key actually is.

TypeScript type-check:

  • pnpm --filter @tessera/web type-check does not exit 0 (154 pre-existing errors, all related to toBeInTheDocument type augmentation missing). This is not a regression from Plan 03. All production source files added in Plan 03 are type-clean.

Known Stubs

File Stub Reason
cert-manager.service.ts splitCerts throws NotImplementedException Implemented in Plan 04 (Split slice)
cert-manager.service.ts mergeCerts throws NotImplementedException Implemented in Plan 05 (Merge/PFX slice)
cert-manager.service.ts convertCert throws NotImplementedException Implemented in Plan 06 (Convert slice)

These stubs are intentional. Plan 03 scope is the Inspect slice only.

Threat Model Compliance

Threat ID Status
T-09-01 (malformed cert → unhandled throw) Mitigated — outer try/catch on all forge operations → BadRequestException
T-09-02 (password leakage) Mitigated — wrong password → generic 400 message; password value never logged
T-09-03 (oversized upload → OOM) Mitigated — fileSize: 510241024 in FileInterceptor (from Plan 01)
T-09-04 (unauthenticated cert processing) Mitigated — credentials:'include' on all fetch calls; ModuleGuard server-side

Self-Check: PASSED

Check Result
apps/api/src/cert-manager/cert-manager.service.ts FOUND + MODIFIED
apps/api/src/cert-manager/cert-manager.service.spec.ts FOUND + MODIFIED
apps/web/src/app/(portal)/modules/cert-manager/actions.ts FOUND + MODIFIED
apps/web/src/app/(portal)/modules/cert-manager/components/InspectTab.tsx FOUND + MODIFIED
apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx FOUND + MODIFIED
apps/web/src/test/setup.ts FOUND + MODIFIED
Commit 7c2e506 (RED) FOUND
Commit ba99463 (GREEN parseCert) FOUND
Commit 64a8e72 (InspectTab) FOUND
16/16 API cert-manager tests passing VERIFIED
9/9 web cert-manager tests passing VERIFIED
BadRequestException in parseCert catch VERIFIED
fileSize: 5 in controller VERIFIED
Password not in logger args VERIFIED