--- phase: 09-cert-manager-module plan: "03" subsystem: api+frontend tags: [cert-manager, parseCert, node-forge, inspect-tab, tdd, vitest] dependency_graph: requires: [09-01, 09-02] provides: [cert-manager-parse-endpoint, cert-details-interface, inspect-tab-ui] affects: - 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 tech_stack: added: [] patterns: [tdd-red-green, node-forge-parse, BadRequestException-guard, vi.spyOn-mock, explicit-expect-extend] key_files: 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 decisions: - "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" metrics: duration: "~17 minutes" completed: "2026-07-01T22:09:00Z" tasks_completed: 3 files_created: 0 files_modified: 6 requirements: [CERT-01, CERT-05] status: 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`: - 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: 5*1024*1024 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 |