docs(09-03): complete inspect-slice plan

This commit is contained in:
2026-07-01 23:57:54 +02:00
parent 64a8e725e7
commit da676705d9
4 changed files with 205 additions and 8 deletions
@@ -0,0 +1,188 @@
---
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<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: 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 |