Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6cc0d02e97 | |||
| de6986340d | |||
| 27909e4502 | |||
| b4aaed4b7c | |||
| 8716fa5234 |
+5
-4
@@ -4,8 +4,8 @@ milestone: v1.2
|
||||
current_phase: 18
|
||||
current_phase_name: desktop-client-fertigstellen
|
||||
status: verified
|
||||
stopped_at: "Sechs Quick-Vorgaenge am 2026-09-21 abgeschlossen (9ie, a1d, bi2, fi3, gof, i8x). Offen als naechste Fehlerklasse: Listenschluessel per Positionsnummer (19 im Quellcode) und Ausrufezeichen-Zusicherungen (11 im Quellcode). Danach kommen zwei neue Widgets. i8x ist noch nicht gepusht."
|
||||
last_updated: "2026-09-21T12:00:00.000Z"
|
||||
stopped_at: "Sieben Quick-Vorgaenge am 2026-09-21 abgeschlossen und verifiziert (9ie, a1d, bi2, fi3, gof, i8x, iwr). Der fehlerverdaechtige Lint-Rueckstand ist damit vollstaendig geprueft: zwei echte Fehler gefunden und behoben (tote Passwortwechsel-Sperre an der API, stille 403-Antworten in der Benutzerverwaltung), der Rest war harmlos oder Absicht, je Stelle in den Akten begruendet. Offen und NICHT fehlerverdaechtig: 288 any im Quellcode, 30 a11y-Befunde mit Gestaltungsbedarf. Naechster Auftrag des Nutzers: zwei neue Dashboard-Widgets."
|
||||
last_updated: "2026-09-21T12:45:00.000Z"
|
||||
last_activity: 2026-09-21
|
||||
last_activity_desc: Quick 260921-9ie, a1d, bi2, fi3 und gof — Lint-Tor scharf, Benutzerverwaltung meldet abgewiesene Aktionen, Lint-Rueckstand 2923 → 446, erzwungener Passwortwechsel an der API durchgesetzt (war eine tote Sperre), 21 Effekt-Abhaengigkeiten einzeln beurteilt; alle fuenf verifiziert, die letzten drei am laufenden System
|
||||
state_head: 4d485432c003a6caf68f6d85aff7de0bd27794e2
|
||||
@@ -31,7 +31,7 @@ See: .planning/PROJECT.md (updated 2026-07-17)
|
||||
Phase: 18 (desktop-client-fertigstellen) — COMPLETE (2026-09-17, Verifikation passed, Windows-Bedienprobe bestanden)
|
||||
Plan: 6 of 6
|
||||
Status: Alle 18 Phasen abgeschlossen; Version 1.2.0 freigegeben. Kein laufender Meilenstein. Nach 1.2.0 auf main (Beta): Bildmarke in Akzentfarbe, CI-Desktop-Skip, Favoriten-Symbol/-Sortierung, Desktop-Server-Adresse, Update in der App (signiert), Versionszeile auf der Setup-Seite — alles verifiziert und auf VM/CI nachgewiesen
|
||||
Last activity: 2026-09-21 - Quick 260921-i8x: fuenf fehlerverdaechtige Lint-Klassen geprueft, kein echter Fehler darunter; Weiterleitungsschutz safe-next.ts von rohen Steuerbytes auf Escapes gehaertet, Gleichwertigkeit ueber alle 65536 Codepunkte nachgerechnet (54 abgelehnte Zeichen, null Abweichung)
|
||||
Last activity: 2026-09-21 - Quick 260921-iwr: 30 Listenschluessel- und Zusicherungs-Stellen geprueft, kein echter Fehler; beide Verdachtsmomente (LDAP-Seite, Cert-Manager) durch Messung widerlegt. Der fehlerverdaechtige Rueckstand ist damit abgearbeitet
|
||||
|
||||
Progress: [██████████] 99%
|
||||
|
||||
@@ -450,6 +450,7 @@ Gerettet aus `.continue-here.md`. Relevant fuer die noch offenen Live-Tests.
|
||||
| 260921-fi3 | **Erzwungener Passwortwechsel wurde an der API nie durchgesetzt — Sicherheitsfix.** Aus dem Lint-Durchlauf 260921-bi2 kamen fuenf gemeldete "Symptome". Alle am laufenden System nachgestellt: eines widerlegt (Passwortwechsel-Seite leitet sehr wohl weiter, siehe bi2-VERIFICATION), drei bestaetigt, eines (ZIP-Dateiname) bewusst nicht angefasst. **Der schwere Befund:** `auth.service.ts:176` legt `mustChangePassword` in den JWT, `jwt.strategy.ts` liess das Feld beim Auspacken fallen, also war `request.user.mustChangePassword` immer `undefined` und der global registrierte `ForcePasswordChangeInterceptor` eine Attrappe — er hat seit seiner Einfuehrung nie etwas blockiert. Durchgesetzt wurde der Zwangswechsel allein von der Web-Middleware; jeder Weg daran vorbei (Desktop-App, Skript, curl) umging ihn. Der Kommentar des Interceptors behauptete woertlich "T-02-14: Prevents bypass via direct API access" — das war falsch. Kein Rechteausbau: die eigene Rolle bleibt, aber der Zwang entfaellt. **Gemessen vorher:** Sitzung mit `mustChangePassword=true` bekam auf `GET /users` **200 samt vollstaendiger Benutzerliste**. **Behoben:** Strategie reicht das Feld durch (strikt `=== true`, fehlender Anspruch in alten Sitzungen wird `false`), Erlaubnisliste von Teilzeichenketten-Vergleich auf exaktes Verfahren+Pfad umgestellt. **Sauberer Nachweis** (gleicher Nutzer, gleiche Rolle, gleiche Route, nur die Kennzeichnung unterscheidet sich — auf `/users` haette der Rollen-Riegel das Ergebnis verdeckt): `GET /modules/active` → 403 `{"message":"FORCE_PASSWORD_CHANGE"}` mit Zwang, 200 ohne. `/auth/me`, `/auth/change-password` und `/auth/logout` kommen weiterhin durch. **Rot-dann-Gruen belegt:** neue Spezifikationen gegen den alten Stand 6 von 12 rot, danach 12/12 gruen — vom Verifier unabhaengig nachgestellt (alte Dateien aus `116041b` rekonstruiert). Gezielt nach Schlupfloechern gesucht (Schraegstrich am Ende, Abfragezeichen, Gross/Klein, `../`): keins. **Kein Aussperren:** kompletter Browser-Ablauf durchgespielt — Anmeldung leitet auf `/change-password`, Seite bedienbar, Wechsel gelingt, landet auf `/`, Kennzeichnung geloescht, freie Navigation. Die Seitenleiste zeigt waehrenddessen "Keine Module" (neuer 403 auf `/modules/active`, wortlos geschluckt) — sachlich richtig. **Dazu Fahrzeugtabelle (dkv-fleet):** Loeschknopf war doppelt ausloesbar (`isDeleting` wurde geschrieben, nie gelesen; Dialog blieb waehrend der Anfrage offen) — beide Dialogknoepfe jetzt gesperrt. Nebenbefund des Verifiers: die Wirkung kommt vom `disabled`-Attribut, React unterdrueckt Klicks darauf selbst; der Zustandscheck ist redundant, nicht falsch. Ausserdem sieben fest verdrahtete deutsche Texte und sechs Vorlese-Beschriftungen auf next-intl umgestellt (de und en, 87 Schluessel deckungsgleich). **Nicht angefasst, begruendet:** `SplitTab.tsx` `'certificates.zip'` — ein Downloadname ist ein Dateisystem-Artefakt, kein Bedienelement; uebersetzt braechte er Umlaute in Windows-Dateifreigaben. **Balken:** api 71 Dateien/1136 Tests, web 66/462, type-check 4/4, `pnpm lint` 5/5 ohne Fehlerstufe, Warnungen 466. Verifikation passed (9/9). | 2026-09-21 | f7c02b7,e56cce4 | [260921-fi3-erzwungener-passwortwechsel-wird-von-der](./quick/260921-fi3-erzwungener-passwortwechsel-wird-von-der/) |
|
||||
| 260921-gof | **21 React-Effekt-Abhaengigkeiten einzeln beurteilt — 15 davon waren Fallen, nicht Fehler.** Die Klasse war aus 260921-bi2 zurueckgestellt worden, weil jeder Befund einzeln zu beurteilen ist. Ergebnis: nur **2 echte Defekte** (A), **15 Fallen** (B, das naive Eintragen haette eine Abruf-Schleife erzeugt), **3 Absicht** (C, mit begruendetem `biome-ignore` — erste Verwendung im Projekt), **1 Ballast** (D). **Die gefaehrlichste Stelle:** `calendar-widget.tsx:82` — `showToday` setzt bei jedem Klick ein frisches `Date`; `monthDate` naiv in die Liste einzutragen haette **jeden** Druck auf den Monatstitel einen Termin-Abruf ausloesen lassen, ueber die API bis zum Exchange-Server. Reihenfolge war Pflicht: erst Identitaet stabilisieren, dann die Liste umstellen. **Die haeufigste Falle:** `t` aus `useTranslations` ist in diesem Projekt bei jedem Durchlauf eine frische Funktion (die Test-Attrappen sind nachweislich so gebaut) — 8 Befunde. Griff ohne Ausnahme-Kommentar: den uebersetzten Text vor dem Hook in eine Konstante ziehen und diese eintragen; React vergleicht Zeichenketten per Wert. **Nebenbefund:** 11 `eslint-disable`-Zeilen fuer genau diese Regel waren wirkungslos, seit Biome ESLint abgeloest hat — alle entfernt. **Laufzeitnachweis vom Orchestrator im Browser** (Netzwerkprotokoll, nie `fetch` aus der Seite; gegen neu gebaute Abbilder): Dashboard 62 s Ruhe → Protokoll byte-identisch, genau 1 `calendar/events`; Monatstitel 3x gedrueckt → nur der erste Druck (Bereich aendert sich wirklich) loest einen Abruf aus, Druck 2 und 3 **null**; "Weiter" 3x → 3 Abrufe, korrekt; Stoppuhr 6 s real → Anzeige 00:06, 4 Runden ueber 4,8 s → 16/17/19/20 monoton, kein Ruecksprung. Dazu acht weitere Ansichten je 20-25 s ruhen gelassen (Marktplatz, Modulverwaltung, Gruppenverwaltung, DKV dreimal, Ausschreibungsradar zweimal) — jeder Endpunkt genau einmal. `InvoiceHistoryTable` hatte als einzige Datei keinen Test und ist damit gemessen statt nur gelesen; `ResultsList` ist die Stelle, an der die `t`-Falle in bi2 tatsaechlich zuschnappte. **Zahlen:** Warnungen 467 → 446 (exakt 21, nichts anderswo gewachsen), `useExhaustiveDependencies` 0, web-Tests 66/462 → 67/477, api 71/1136 unveraendert, type-check 4/4, `pnpm lint` 5/5 ohne Fehlerrang. Verifikation passed. **Benannt, nicht behoben:** zwei Verschwendungen im Kalender-Abruffenster (gleicher Zeitbereich zweimal geholt; `calendar/sources` bei jedem Monatswechsel) — vorbestehend; und `t` in vier vorbestehenden Abhaengigkeitslisten ausserhalb des Auftrags, die Biome nie gemeldet hat. | 2026-09-21 | b3f0e3c,e2c508c,e780b2c | [260921-gof-effekt-abhaengigkeiten-in-react-21-befun](./quick/260921-gof-effekt-abhaengigkeiten-in-react-21-befun/) |
|
||||
| 260921-i8x | **Fuenf fehlerverdaechtige Lint-Klassen geprueft — kein einziger echter Fehler darunter.** Zwoelf Stellen einzeln beurteilt, Ergebnis: 7x gleichwertig oder Absicht, 2x Haertung, 3x idiomatisch korrekt. Das ist das Ergebnis, keine Ausrede — die Klassen klangen gefaehrlicher als sie waren. **Die eine Stelle mit echtem Wert:** `apps/web/src/lib/safe-next.ts`, der Schutz gegen Weiterleitung auf fremde Seiten nach der Anmeldung. Der Kommentar dort behauptete, die Steuerzeichen stuenden als Unicode-Escapes im Muster; die rohen Bytes zeigten das Gegenteil (NUL, 0x1F, 0x7F direkt eingebettet). Funktionierte, war aber zerbrechlich: verschluckt ein Werkzeug das NUL-Byte, wird aus dem Bereich stillschweigend ein anderer und der Schutz loechrig — der Rueckgabewert landet in `login/page.tsx` direkt in `window.location.href`. Jetzt echte Escapes, Datei ohne ein einziges Steuerbyte. **Beweis der Gleichwertigkeit, nicht Behauptung:** ueber alle 65536 Codepunkte dieselbe Menge abgelehnter Zeichen — 54 Stueck (32 Steuerzeichen 0x00-0x1F, dazu 0x7F, Backslash und die 20 Leerraum-Zeichen von `\s`), null Abweichung; vom Orchestrator unabhaengig gegen ein selbst gebautes Referenzmuster nachgerechnet. **Bewusst nicht angefasst:** die NUL-Maskierung in `ldap.service.ts` (RFC 4515) — genau dieses Zeichen zu treffen ist ihr Zweck, wer sie "repariert", oeffnet LDAP-Filter-Injection. Ebenso die drei `while ((m = re.exec(s)))`-Schleifen (idiomatisch, kein verrutschtes Gleichheitszeichen) und `noUselessSwitchCase` aus bi2. **Zwei Korrekturen an frueheren Annahmen:** `sanitizeNextPath` laeuft NICHT in der Edge-Middleware (die importiert nur `buildNextParam`), und das blosse Umschreiben auf Escapes senkt die Warnzahl nicht — Biome beanstandet die Escape-Schreibweise genauso, es braucht zusaetzlich einen einzeiligen Unterdrueckungskommentar. **Werkzeugfalle, dreimal zugeschnappt:** das Schreibwerkzeug wandelt `\uXXXX` still in das echte Zeichen um — der Planer erzeugte so zehn rohe Steuerbytes in seiner ersten Planfassung, der Executor zweimal in Commit-Text und Akte (git verweigerte den Commit wegen eines NUL-Bytes), und der Orchestrator beim Nachrechnen. Umgehung ueber `python3`/`chr(92)` ist im Plan hinterlegt. **Zahlen:** 446 → 434, `noControlCharactersInRegex`/`useIterableCallbackReturn`/`noGlobalIsNan` je 0, `suppressions/unused` 0, web-Tests 67/477 → 68/481, api 71/1136 → 72/1137, type-check 4/4, lint 5/5. | 2026-09-21 | f85c91b,076ca4b,b92dd5d | [260921-i8x-fehlerverdaechtige-lint-klassen-steuerze](./quick/260921-i8x-fehlerverdaechtige-lint-klassen-steuerze/) |
|
||||
| 260921-iwr | **Listenschluessel und Ausrufezeichen-Zusicherungen: 30 Stellen geprueft, wieder kein echter Fehler.** Damit ist der fehlerverdaechtige Rueckstand abgearbeitet. **Zwei Vorannahmen des Orchestrators widerlegt, beide durch Messung statt Argument:** (1) Die LDAP-Seite galt als heisser Kandidat, weil dort Zuordnungsregeln hinzugefuegt und geloescht werden — die Liste, die tatsaechlich waechst und schrumpft (`config.fieldMappings`), benutzt jedoch laengst `key={mapping.id}`; die sechs Meldungen betreffen zustandslose Textlisten. (2) Im Cert-Manager galt eine Zusicherung auf hochgeladenen Dateiinhalt als moeglicher Absturz — der Planer hat eine 83-Byte-Schrottdatei gebaut, die node-forge `bag.cert = null` setzen laesst, und gegen den echten Dienst laufen lassen: **alle vier Pfade enden mit 400, nie 500**, und `certificateToPem(null)` wirft nachweislich, statt still ein falsches Zertifikat zu bauen. Also weder Verfuegbarkeits- noch Integritaetsluecke, sondern eine irrefuehrende Fehlermeldung. **Ein Fund dreht die Richtung um:** bei `admin/modules/grants/page.tsx:246` waere die Korrektur schaedlich — die Gruppierung fasst nur aufeinanderfolgende Kategorien zusammen, die Positionsnummer ist dort fuer die Eindeutigkeit noetig, ohne sie entstuenden doppelte Schluessel. **Geaendert: 5 Stellen** (drei Waechter im Cert-Manager, die den Meldungstext praezisieren — Status bleibt 400, rot-dann-gruen belegt; zwei ueberfluessige Zusicherungen in `imap.provider.ts`, die imapflow ohnehin als Pflichtfeld typisiert). **25 Stellen bleiben bewusst stehen und bleiben in der Zaehlung sichtbar** — mit Begruendung je Stelle in der Akte, damit der naechste Durchgang sie nicht erneut aufrollt; kein Unterdrueckungskommentar, um die Zahl zu schoenen. **Das Tor hat sich selbst bewaehrt:** der erste Entwurf eines Waechters erzeugte einen neuen Lint-Fund (430 statt 429) und wurde von der Verifikation des Plans gefangen; die Reparatur brach `tsc`, weil `@types/node-forge` `Bag.cert` als `Certificate | undefined` deklariert, waehrend die Bibliothek zur Laufzeit `null` zuweist — Endfassung prueft beides. **Zahlen:** 434 → 429, `noArrayIndexKey` unveraendert 19 (alle geprueft, alle harmlos), `noNonNullAssertion` 11 → 6, web-Tests 68/481 → 69/484, api 72/1137 → 72/1143, type-check 4/4, lint 5/5. | 2026-09-21 | 8716fa5,b4aaed4,27909e4,de69863 | [260921-iwr-listenschluessel-per-positionsnummer-und](./quick/260921-iwr-listenschluessel-per-positionsnummer-und/) |
|
||||
|
||||
## Deferred Items
|
||||
|
||||
@@ -495,4 +496,4 @@ Last session: 2026-09-21T04:50:00Z
|
||||
Resumed: 2026-09-21 — Sitzung ueber /gsd-resume-work fortgesetzt. Stand geprueft: Arbeitsbaum sauber, main == origin/main auf 55aa287, CI-Lauf 387 fuer 55aa287 erfolgreich (Beta-Images gebaut). Push und CI aus dem letzten Stopp-Punkt sind damit erledigt.
|
||||
Stopped at: Warte auf Nutzerentscheidung, womit weitergearbeitet wird. Offen fuer den User: alpha pullen (web+api) und danach am Windows-VM-Client die echte Fehlermeldung schicken (Betreff `[Desktop/Windows]` + `Herkunft:`-Zeile pruefen); eigenen Arbeitsplatz-Client einmal per Browser-Installer erneuern; Freigabe 1.3.0 auf Zuruf. Technisch offen im Ledger: WINDOWS #35 (Biome laeuft nicht — biome.json:3 `organizeImports` ist in Biome 2.5.0 unbekannt, `biome check` bricht mit Konfigurationsfehler ab, reproduziert 2026-09-21) und WINDOWS #36 (403-Antworten bleiben in handleSubmit/handleDelete ohne sichtbare Reaktion).
|
||||
Resume file: None
|
||||
Last activity: 2026-09-21 - Quick 260921-i8x: fuenf fehlerverdaechtige Lint-Klassen geprueft, kein echter Fehler darunter; Weiterleitungsschutz safe-next.ts von rohen Steuerbytes auf Escapes gehaertet, Gleichwertigkeit ueber alle 65536 Codepunkte nachgerechnet (54 abgelehnte Zeichen, null Abweichung)
|
||||
Last activity: 2026-09-21 - Quick 260921-iwr: 30 Listenschluessel- und Zusicherungs-Stellen geprueft, kein echter Fehler; beide Verdachtsmomente (LDAP-Seite, Cert-Manager) durch Messung widerlegt. Der fehlerverdaechtige Rueckstand ist damit abgearbeitet
|
||||
|
||||
+413
@@ -0,0 +1,413 @@
|
||||
---
|
||||
phase: quick-260921-iwr
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
autonomous: true
|
||||
requirements: [D-01, D-02, D-03, D-04, D-05, D-06]
|
||||
files_modified:
|
||||
- apps/api/src/cert-manager/cert-manager.service.ts
|
||||
- apps/api/src/cert-manager/cert-manager.service.spec.ts
|
||||
- apps/api/src/inbox/imap.provider.ts
|
||||
- apps/web/src/components/dashboard/widgets/stopwatch-widget.tsx
|
||||
- apps/web/src/components/dashboard/widgets/stopwatch-widget.test.tsx
|
||||
- apps/web/src/app/(portal)/modules/cert-manager/components/MergeTab.test.tsx
|
||||
|
||||
estimate:
|
||||
tokens: 78000
|
||||
raw_tokens: 39000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Jede der 30 Fundstellen traegt ein Urteil mit einer Begruendung, die die Folge benennt, nicht die Regel (D-01)."
|
||||
- "Biome meldet danach ARRAYKEY=19, NONNULL=6, ERRORS=0, TOTAL=429 — genau die 5 tatsaechlich geaenderten Stellen weniger (D-06)."
|
||||
- "Eine hochgeladene PFX-Datei mit unlesbarem Zertifikats-Bag wird mit einer praezisen 400-Meldung abgewiesen statt mit einer irrefuehrenden."
|
||||
- "Kein Zertifikat wird still falsch ausgegeben und keine Eingabepruefung wurde abgeschwaecht (D-04)."
|
||||
- "pnpm lint bleibt 5/5, pnpm type-check 4/4, beide Testsuiten mindestens auf Ausgangsstand."
|
||||
artifacts:
|
||||
- apps/api/src/cert-manager/cert-manager.service.ts
|
||||
- apps/api/src/cert-manager/cert-manager.service.spec.ts
|
||||
- apps/api/src/inbox/imap.provider.ts
|
||||
- apps/web/src/app/(portal)/modules/cert-manager/components/MergeTab.test.tsx
|
||||
- .planning/quick/260921-iwr-listenschluessel-per-positionsnummer-und/260921-iwr-SUMMARY.md
|
||||
key_links:
|
||||
- "Die drei bag.cert-Waechter haengen an node-forge lib/pkcs12.js Zeile 708 (bag.cert = null bei unlesbarem X.509) — das ist der einzige Grund, warum die Zusicherung sachlich falsch ist."
|
||||
- "Die 400-Zusage haengt an den catch-Bloecken in Zeile 232, 486, 533 und 625: BadRequestException wird unveraendert durchgereicht, alles andere wird zu 400 umgewandelt."
|
||||
- "Die Unbedenklichkeit aller 19 Positionsschluessel haengt an einer einzigen Tatsache: keine der betroffenen Zeilen haelt Zustand, den React pro Schluessel fuehrt."
|
||||
---
|
||||
|
||||
<objective>
|
||||
Die 30 gemeldeten Fundstellen zweier Lint-Klassen einzeln beurteilen und nur dort eingreifen,
|
||||
wo die Beurteilung einen echten Grund liefert.
|
||||
|
||||
Purpose: Letzte Fehlerklasse vor den beiden neuen Widgets. Es geht um ein belastbares Urteil je
|
||||
Stelle, nicht um eine niedrigere Zahl.
|
||||
Output: 5 begruendete Aenderungen, 25 begruendet stehengelassene Fundstellen, neue Tests fuer die
|
||||
beiden Stellen, an denen die Beurteilung nicht offensichtlich war.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/gsd-core/workflows/execute-plan.md
|
||||
@~/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/STATE.md
|
||||
@CLAUDE.md
|
||||
|
||||
@apps/api/src/cert-manager/cert-manager.service.ts
|
||||
@apps/api/src/inbox/imap.provider.ts
|
||||
@apps/web/src/components/dashboard/widgets/stopwatch-widget.tsx
|
||||
</context>
|
||||
|
||||
<planning_observations>
|
||||
Beim Planen am 2026-09-21 nachgemessen und nachgewiesen — der Ausfuehrende muss das nicht
|
||||
wiederholen, aber er darf es nicht ungeprueft umstossen:
|
||||
|
||||
**Ausgangsstand (identisch zur Vorgabe des Orchestrators):**
|
||||
TOTAL=434, ERRORS=0, ARRAYKEY=19, NONNULL=11; web 68 Dateien / 481 Tests,
|
||||
api 72 / 1137, pnpm type-check 4/4, pnpm lint 5/5.
|
||||
|
||||
**Die Vermutung zu admin/ldap ist widerlegt.** Die Liste, die dort wirklich waechst und
|
||||
schrumpft, ist `config.fieldMappings` in Zeile 712 — sie benutzt bereits `key={mapping.id}`.
|
||||
Die 6 gemeldeten Stellen sind etwas anderes: reine Meldungslisten
|
||||
(`nameCollisions`, `errors`, `emailConflicts`, `skippedNoLogin`, `entryFailures`, `errors`),
|
||||
die als zustandslose Absaetze gerendert und beim naechsten Lauf komplett ersetzt werden
|
||||
(setSyncResult(null) in Zeile 284, dann setSyncResult(data) in Zeile 293).
|
||||
|
||||
**node-forge kann bag.cert auf null setzen** — lib/pkcs12.js Zeile 703-709: wenn
|
||||
`certificateFromAsn1` wirft, faengt node-forge das ab und setzt `bag.cert = null`. Die drei
|
||||
Zusicherungen 207/469/602 behaupten also etwas, das nicht gilt.
|
||||
|
||||
**Die Folge davon ist aber bereits behandelt.** Mit einer eigens gebauten 83-Byte-PFX-Datei
|
||||
(Zertifikats-Bag mit wohlgeformtem, aber unlesbarem Inhalt) gegen den echten Service gemessen:
|
||||
|
||||
| Pfad | heutiges Ergebnis |
|
||||
|------|-------------------|
|
||||
| parseCert | 400 "Failed to extract certificate details" |
|
||||
| mergeCerts pem | 400 "Failed to create merged certificate output" |
|
||||
| mergeCerts pfx | 400 "Failed to create merged certificate output" |
|
||||
| convertCert | 400 "Failed to convert certificate to pem: serialization error" |
|
||||
|
||||
Kein 500, kein Absturz. Zusaetzlich direkt an node-forge geprueft: `certificateToPem(null)` und
|
||||
`toPkcs12Asn1(null, [cert, null], pw)` werfen beide eine TypeError — die Bibliothek laesst ein
|
||||
fehlendes Zertifikat **nie** still durchrutschen. Das in D-04 befuerchtete "still falsches
|
||||
Zertifikat" ist damit ausgeschlossen, nicht nur unwahrscheinlich.
|
||||
|
||||
Daraus folgt die Einstufung: die drei Stellen sind **keine Verfuegbarkeits- und keine
|
||||
Integritaetsluecke**. Was bleibt, ist eine sachlich falsche Zusicherung und eine irrefuehrende
|
||||
Fehlermeldung. Der Eingriff ist deshalb als Diagnose-/Lesbarkeitsarbeit zu fuehren, nicht als
|
||||
Fehlerbehebung.
|
||||
|
||||
**imapflow typisiert `uid` als Pflichtfeld** (lib/imap-flow.d.ts Zeile 469 in
|
||||
`FetchMessageObject`, Kommentar "Always included in the response"). Das `!` ist dort schlicht
|
||||
ueberfluessig. Entfernen wurde beim Planen probeweise durchgefuehrt: `tsc --noEmit` in apps/api
|
||||
endet mit 0. Die Aenderung wurde danach zurueckgenommen, der Baum ist sauber.
|
||||
|
||||
**ESLint existiert in diesem Repo nicht** (kein Paket, keine Konfigurationsdatei; Treffer nur in
|
||||
mitgelieferten Fremdpaketen unter .next/standalone). Der Unterdrueckungskommentar in
|
||||
stopwatch-widget.tsx Zeile 277 wirkt daher nicht.
|
||||
|
||||
**Zaehlbefehl** (in jedem verify benutzt, Kategorien kommen aus dem JSON-Feld `category`,
|
||||
nie aus dem Quelltext):
|
||||
|
||||
`npx biome lint --reporter=json . 2>/dev/null | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const ds=JSON.parse(s).diagnostics||[];const isTest=p=>p.includes(".spec.")||p.includes(".test.")||p.includes("__tests__")||p.includes("__fixtures__")||p.includes("__mocks__");const real=ds.filter(x=>!isTest(x.location.path));const c=r=>real.filter(x=>x.category===r).length;console.log("TOTAL="+ds.length+" ERRORS="+ds.filter(x=>x.severity==="error").length+" ARRAYKEY="+c("lint/suspicious/noArrayIndexKey")+" NONNULL="+c("lint/style/noNonNullAssertion"));});'`
|
||||
|
||||
Er meldet im Ausgangsstand `TOTAL=434 ERRORS=0 ARRAYKEY=19 NONNULL=11`.
|
||||
</planning_observations>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: Die 19 Positionsschluessel beurteilen und die zwei nicht offensichtlichen Faelle nachmessen</name>
|
||||
<files>
|
||||
apps/web/src/components/dashboard/widgets/stopwatch-widget.tsx,
|
||||
apps/web/src/components/dashboard/widgets/stopwatch-widget.test.tsx,
|
||||
apps/web/src/app/(portal)/modules/cert-manager/components/MergeTab.test.tsx
|
||||
</files>
|
||||
<read_first>
|
||||
apps/web/src/app/(portal)/modules/cert-manager/components/MergeTab.tsx,
|
||||
apps/web/src/components/dashboard/widgets/stopwatch-widget.tsx,
|
||||
apps/web/src/components/dashboard/widgets/stopwatch-widget.test.tsx
|
||||
</read_first>
|
||||
<behavior>
|
||||
- MergeTab: drei Dateien auswaehlen, die mittlere ueber ihren Entfernen-Knopf loeschen; danach
|
||||
stehen genau die erste und die dritte Datei in der Liste, in dieser Reihenfolge, mit ihren
|
||||
eigenen Namen. Das ist der Nachweis, dass der Positionsschluessel beim Schrumpfen der Liste
|
||||
nichts verwechselt.
|
||||
- Stopwatch: zwei Runden nacheinander aufzeichnen; die Liste zeigt die neuere Runde oben, die
|
||||
Rundennummern lauten von oben nach unten 2 und 1, und die beiden Zeiten stehen bei der
|
||||
jeweils richtigen Nummer. Das ist der Nachweis fuer die Liste, die von vorn waechst.
|
||||
</behavior>
|
||||
<action>
|
||||
Beurteile alle 19 Fundstellen einzeln und halte je Stelle ein Urteil mit Folgenbegruendung fest
|
||||
(D-01). Die Beurteilung aus den planning_observations ist nachgewiesen und darf uebernommen
|
||||
werden; wenn du beim Lesen etwas anderes siehst, hat deine Beobachtung Vorrang und du sagst es.
|
||||
|
||||
Erwartete Einstufung, nach Muster gruppiert:
|
||||
(a) Zwoelf Stellen sind Ladeplatzhalter aus Array.from mit fester Laenge ohne Inhalt —
|
||||
InvoiceHistoryTable 129 und 131, VehicleTable 342 und 344, ResultsList 240 und 242,
|
||||
InboxConfigForm 251, EmailAlertConfigForm 225, RssFeedListForm 151, SourceConfigForm 129.
|
||||
Dort gibt es ausser der Position ueberhaupt keine Identitaet, die Laenge ist konstant, die
|
||||
Reihenfolge aendert sich nie. Harmlos.
|
||||
(b) Sechs Stellen in admin/ldap sind Meldungslisten, die beim naechsten Lauf komplett ersetzt
|
||||
werden und deren Zeilen nur Text enthalten. Harmlos. Halte dabei ausdruecklich fest, dass die
|
||||
Liste, die dort tatsaechlich waechst und schrumpft, bereits mapping.id benutzt.
|
||||
(c) grants/page.tsx 246: die Gruppierung in der useMemo ab Zeile 168 fasst nur unmittelbar
|
||||
aufeinanderfolgende Module gleicher Kategorie zusammen, dieselbe Kategorie kann also mehrfach
|
||||
vorkommen. Der Positionsanteil im Schluessel ist deshalb noetig; ihn zu entfernen wuerde
|
||||
doppelte Schluessel erzeugen. Die Haken haengen ohnehin nicht am Schluessel, sondern an der
|
||||
aeusseren Menge grants ueber cellKey(mod.id, g.id). Harmlos, und der Index bleibt bewusst.
|
||||
(d) MergeTab 87 und stopwatch-widget 276: die einzigen beiden Listen, die sich waehrend der
|
||||
Anzeige wirklich veraendern. Belege deren Unbedenklichkeit mit den beiden Tests aus dem
|
||||
behavior-Block, statt sie nur zu behaupten.
|
||||
|
||||
Aendere keinen einzigen Schluessel im Produktivcode. Fuer die zwoelf Platzhalter und die sechs
|
||||
Meldungslisten gibt es keine stabile Identitaet, die ungenutzt herumlaege; bei grants waere die
|
||||
Aenderung sogar schaedlich; bei den Runden der Stoppuhr saesse eine echte Identitaet nur in der
|
||||
gespeicherten Form laps als Zahlenliste, deren Umbau bereits abgelegte Widget-Konfigurationen
|
||||
und bestehende Tests brechen wuerde — das waere eine Verhaltensaenderung ohne Gegenwert und
|
||||
unterbleibt (D-02).
|
||||
|
||||
Entferne in stopwatch-widget.tsx die wirkungslose Unterdrueckungszeile ueber dem Schluessel
|
||||
(sie nennt eine Regel eines Linters, den dieses Projekt nicht einsetzt) und setze an ihre
|
||||
Stelle einen kurzen Sachhinweis, warum die Position hier als Schluessel vertretbar ist: die
|
||||
Zeilen halten keinen Zustand, und die angezeigte Rundennummer wird ohnehin aus Laenge und
|
||||
Position berechnet. Fuege keine neue Unterdrueckung hinzu (D-06) — die Fundstelle bleibt
|
||||
ausdruecklich in der Zaehlung stehen.
|
||||
|
||||
Lege MergeTab.test.tsx neu an. Orientiere dich an den Mustern in stopwatch-widget.test.tsx
|
||||
fuer render, fireEvent und die Uebersetzungsattrappe. Die Dateiauswahl laeuft ueber das
|
||||
verborgene Dateifeld; setze die Dateien per fireEvent.change mit echten File-Objekten. Den
|
||||
Entfernen-Knopf findest du ueber sein aria-label, das den Dateinamen enthaelt. Ergaenze den
|
||||
Rundentest in stopwatch-widget.test.tsx als zusaetzlichen Fall, ohne bestehende Faelle zu
|
||||
veraendern.
|
||||
|
||||
ARRAYKEY bleibt danach bei 19. Das ist das erwartete Ergebnis, kein Versaeumnis.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd apps/web && npx vitest run src/app/\(portal\)/modules/cert-manager/components/MergeTab.test.tsx src/components/dashboard/widgets/stopwatch-widget.test.tsx</automated>
|
||||
<automated>cd apps/web && npx vitest run 2>&1 | tail -4 # erwartet: mindestens 69 Dateien, mindestens 484 Tests, alle gruen</automated>
|
||||
<automated>npx biome lint --reporter=json . 2>/dev/null | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const ds=JSON.parse(s).diagnostics||[];const isTest=p=>p.includes(".spec.")||p.includes(".test.")||p.includes("__tests__")||p.includes("__fixtures__")||p.includes("__mocks__");const real=ds.filter(x=>!isTest(x.location.path));const c=r=>real.filter(x=>x.category===r).length;console.log("ARRAYKEY="+c("lint/suspicious/noArrayIndexKey")+" ERRORS="+ds.filter(x=>x.severity==="error").length);});' # erwartet: ARRAYKEY=19 ERRORS=0</automated>
|
||||
</verify>
|
||||
<done>
|
||||
Alle 19 Fundstellen haben ein Urteil mit Folgenbegruendung. Kein Schluessel im Produktivcode
|
||||
geaendert. Die wirkungslose Unterdrueckungszeile ist weg, keine neue hinzugekommen. Die beiden
|
||||
Tests belegen, dass Entfernen in der Mitte und Wachsen von vorn keine Zeile verwechseln. Die
|
||||
Web-Suite ist gewachsen und gruen, ARRAYKEY steht unveraendert bei 19.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Die sechs Zusicherungen in cert-manager.service.ts beurteilen, drei davon zu echten Waechtern machen</name>
|
||||
<files>
|
||||
apps/api/src/cert-manager/cert-manager.service.ts,
|
||||
apps/api/src/cert-manager/cert-manager.service.spec.ts
|
||||
</files>
|
||||
<read_first>
|
||||
apps/api/src/cert-manager/cert-manager.service.ts,
|
||||
apps/api/src/cert-manager/cert-manager.service.spec.ts
|
||||
</read_first>
|
||||
<precondition>node_modules ist installiert und node-forge 1.4.0 aufloesbar (die Tests bauen die Pruefdatei mit forge.asn1 selbst).</precondition>
|
||||
<behavior>
|
||||
Pruefgegenstand ist eine PFX-Datei, deren Zertifikats-Bag wohlgeformt, aber kein lesbares
|
||||
X.509 ist. node-forge setzt bag.cert dann auf null.
|
||||
|
||||
- parseCert mit dieser Datei wirft BadRequestException (Status 400) und die Meldung benennt,
|
||||
dass der Zertifikats-Bag kein lesbares X.509-Zertifikat enthaelt. Heute lautet sie
|
||||
"Failed to extract certificate details" — der Test ist vor dem Waechter rot.
|
||||
- mergeCerts mit dieser Datei wirft 400 mit derselben praezisen Aussage, sowohl bei
|
||||
outputFormat pem als auch bei pfx. Heute lautet sie beide Male
|
||||
"Failed to create merged certificate output" — vor dem Waechter rot.
|
||||
- convertCert mit dieser Datei wirft 400 mit der praezisen Aussage. Heute lautet sie
|
||||
"Failed to convert certificate to pem: serialization error" — vor dem Waechter rot.
|
||||
- Eine gueltige PFX-Datei verhaelt sich unveraendert: parseCert, mergeCerts und convertCert
|
||||
liefern weiterhin ihr bisheriges Ergebnis. Die bestehenden Faelle in der Spezifikation
|
||||
bleiben gruen.
|
||||
</behavior>
|
||||
<action>
|
||||
Beurteile alle sechs Fundstellen einzeln (D-01) und benenne bei jeder sicheren die Zeile, die
|
||||
sie garantiert.
|
||||
|
||||
Sicher und unveraendert zu lassen:
|
||||
Zeile 133 — die Zerlegung des Fingerabdrucks in Zweiergruppen. Ein SHA-1- oder
|
||||
SHA-256-Hexwert ist immer 40 beziehungsweise 64 Zeichen lang, die Suche findet also immer
|
||||
etwas. Garantiert durch die Laenge des Hashes, nicht durch Eingabedaten.
|
||||
Zeile 516 — das Kennwort beim Erzeugen einer PFX-Ausgabe im Zusammenfuehren. Garantiert durch
|
||||
die Pruefung in Zeile 438 bis 440, die bei fehlendem oder leerem Kennwort vorher mit 400
|
||||
abbricht.
|
||||
Zeile 661 — dasselbe im Umwandeln. Garantiert durch die Pruefung in Zeile 564 bis 566.
|
||||
|
||||
Zu aendern sind die drei Stellen, an denen die Zusicherung sachlich falsch ist, weil node-forge
|
||||
bag.cert auf null setzen kann (lib/pkcs12.js Zeile 703 bis 709): Zeile 207 in parseCert,
|
||||
Zeile 469 in mergeCerts, Zeile 602 in convertCert.
|
||||
|
||||
Schreibe zuerst die Tests aus dem behavior-Block und weise nach, dass sie mit den heutigen
|
||||
Meldungen fehlschlagen. Baue die Pruefdatei im Test selbst mit forge.asn1 auf. Ihr Aufbau, von
|
||||
aussen nach innen: eine SEQUENCE aus der INTEGER-Version 3 und einem ContentInfo; das
|
||||
ContentInfo besteht aus der OID data und einem kontextspezifischen Element 0, das eine
|
||||
OCTETSTRING mit dem DER des AuthenticatedSafe traegt; das AuthenticatedSafe ist eine SEQUENCE
|
||||
aus einem weiteren ContentInfo gleicher Bauart, dessen OCTETSTRING das DER der SafeContents
|
||||
traegt; die SafeContents sind eine SEQUENCE aus einem SafeBag; der SafeBag ist eine SEQUENCE
|
||||
aus der OID certBag und einem kontextspezifischen Element 0, das eine SEQUENCE aus der OID
|
||||
x509Certificate und einem kontextspezifischen Element 0 mit einer OCTETSTRING enthaelt; in
|
||||
dieser OCTETSTRING steht das DER einer SEQUENCE mit einem einzigen INTEGER 1 — wohlgeformt,
|
||||
aber kein Zertifikat. Als Kennwort dient die leere Zeichenkette. Die Datei ist rund 83 Byte
|
||||
gross. Pruefe im Test vorab, dass genau ein Bag entsteht und dessen cert null ist; damit ist
|
||||
belegt, dass der Waechter den gemeinten Fall trifft und nicht einen anderen.
|
||||
|
||||
Ersetze dann an den drei Stellen die Zusicherung durch eine ausdrueckliche Pruefung, die eine
|
||||
BadRequestException mit einer praezisen Meldung wirft. In Zeile 207 und 602 lies bag.cert in
|
||||
eine lokale Variable, pruefe sie und wirf bei fehlendem Wert. In Zeile 469 darf kein Eintrag
|
||||
verschluckt werden: bilde die Bag-Liste auf ihre Zertifikate ab, pruefe, ob darunter ein
|
||||
fehlendes ist, und wirf in dem Fall unter Nennung des Dateinamens — erst danach gib die Liste
|
||||
zurueck, eingeengt ueber ein Typpraedikat statt ueber eine Zusicherung. Kein Filtern, kein
|
||||
Ueberspringen, kein Ersatzwert (D-04).
|
||||
|
||||
Die BadRequestException wird von den catch-Bloecken in Zeile 232, 486 und 625 unveraendert
|
||||
durchgereicht, die praezise Meldung kommt also beim Aufrufer an.
|
||||
|
||||
Ausdrueckliche Verhaltensaenderung nach D-02, vorab benannt und beabsichtigt: fuer genau diese
|
||||
Eingabeklasse aendert sich der Text der Fehlermeldung. Der Statuscode bleibt in allen vier
|
||||
Pfaden 400, der Vertrag nach aussen bleibt damit unveraendert. Gewollt ist, dass die Meldung
|
||||
den tatsaechlichen Grund nennt statt eines irrefuehrenden Sammelbegriffs.
|
||||
|
||||
Halte in der Zusammenfassung fest, dass dies Diagnose- und Lesbarkeitsarbeit ist: die drei
|
||||
Zusicherungen waren sachlich falsch, ihre Folge war aber bereits behandelt — nachgewiesen mit
|
||||
400 statt 500 in allen vier Pfaden und damit, dass node-forge bei einem fehlenden Zertifikat
|
||||
ausnahmslos wirft und nie still eine unvollstaendige Ausgabe erzeugt. Es war keine
|
||||
Verfuegbarkeits- und keine Integritaetsluecke.
|
||||
|
||||
Das Kennwort darf weiterhin nirgends in eine Meldung oder ins Protokoll geraten (T-09-02).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd apps/api && npx vitest run src/cert-manager/cert-manager.service.spec.ts</automated>
|
||||
<automated>cd apps/api && npx vitest run 2>&1 | tail -4 # erwartet: mindestens 72 Dateien, mindestens 1141 Tests, alle gruen</automated>
|
||||
<automated>npx biome lint --reporter=json . 2>/dev/null | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const ds=JSON.parse(s).diagnostics||[];const isTest=p=>p.includes(".spec.")||p.includes(".test.")||p.includes("__tests__")||p.includes("__fixtures__")||p.includes("__mocks__");const real=ds.filter(x=>!isTest(x.location.path));const n=real.filter(x=>x.category==="lint/style/noNonNullAssertion");console.log("NONNULL="+n.length+" CERTMGR="+n.filter(x=>x.location.path.includes("cert-manager.service.ts")).length);});' # erwartet: NONNULL=8 CERTMGR=3</automated>
|
||||
</verify>
|
||||
<done>
|
||||
Alle sechs Fundstellen beurteilt, bei den drei sicheren ist die garantierende Zeile genannt.
|
||||
Die drei falschen Zusicherungen sind ausdrueckliche Waechter mit praeziser 400-Meldung; die
|
||||
Tests waren vorher rot und sind jetzt gruen. Keine Eingabepruefung abgeschwaecht, kein
|
||||
Eintrag verschluckt, kein Ersatzwert eingefuehrt. Die api-Suite ist gewachsen und gruen.
|
||||
NONNULL steht bei 8, davon 3 in cert-manager.service.ts.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Die restlichen fuenf Zusicherungen beurteilen, zwei ueberfluessige entfernen</name>
|
||||
<files>apps/api/src/inbox/imap.provider.ts</files>
|
||||
<read_first>
|
||||
apps/api/src/inbox/imap.provider.ts,
|
||||
apps/api/src/tenders/tenders.controller.ts,
|
||||
apps/api/src/tenders/tenders.module.ts,
|
||||
apps/web/src/components/dashboard/widgets/favorites-widget.tsx,
|
||||
apps/web/src/components/layout/sidebar.tsx
|
||||
</read_first>
|
||||
<action>
|
||||
Beurteile die fuenf verbliebenen Fundstellen einzeln (D-01) und benenne bei jeder sicheren die
|
||||
Zeile oder den Umstand, der sie garantiert.
|
||||
|
||||
Zu aendern — imap.provider.ts Zeile 214 und 310, beide `uid: msg.uid!`: imapflow typisiert uid
|
||||
in FetchMessageObject als Pflichtfeld (lib/imap-flow.d.ts Zeile 469, Kommentar "Always
|
||||
included in the response"). Die Zusicherung sichert damit einen Wert ab, der ohnehin nicht
|
||||
fehlen kann; sie ist ueberfluessig. Entferne an beiden Stellen nur das Ausrufezeichen und
|
||||
sonst nichts. Das ist eine reine Lesbarkeitsaenderung ohne Verhaltensaenderung; tsc belegt sie.
|
||||
|
||||
Sicher und unveraendert zu lassen:
|
||||
tenders.controller.ts Zeile 244 — der Fragezeichen-Parameter im Konstruktor ist nur dort, um
|
||||
die Reihenfolge der Konstruktorargumente nicht zu brechen. Es gibt kein Optional-Merkmal, und
|
||||
TenderIngestionService steht in tenders.module.ts unter providers, wird von Nest also immer
|
||||
eingesetzt. Garantiert durch den Provider-Eintrag.
|
||||
favorites-widget.tsx Zeile 149 — die Reihenfolge stammt aus sortedFavorites, und das ist laut
|
||||
Zeile 90 bis 97 nur eine sortierte Kopie von favorites. Die Zuordnungstabelle wird aus
|
||||
denselben Eintraegen gebaut, jede gesuchte Kennung ist also enthalten. Garantiert durch die
|
||||
Herleitung in Zeile 90 bis 97.
|
||||
sidebar.tsx Zeile 80 — unmittelbar darueber, in Zeile 79, wird der Eintrag angelegt, falls er
|
||||
fehlt. Garantiert durch die Zeile direkt davor.
|
||||
|
||||
Diese drei bleiben in der Zaehlung stehen, und das wird in der Zusammenfassung gesagt (D-06).
|
||||
Ersetze sie nicht durch gleichwertige Abfragen, nur um die Zahl zu druecken — sie sind sicher,
|
||||
und ein Umbau waere Geraeusch ohne Gewinn.
|
||||
|
||||
Fuege nirgends eine Unterdrueckung hinzu und aendere keine Abhaengigkeiten (D-05).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd apps/api && npx tsc --noEmit; echo "tsc=$?" # erwartet: tsc=0</automated>
|
||||
<automated>cd apps/api && npx vitest run 2>&1 | tail -4 # erwartet: mindestens 72 Dateien, mindestens 1141 Tests, alle gruen</automated>
|
||||
<automated>npx biome lint --reporter=json . 2>/dev/null | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const ds=JSON.parse(s).diagnostics||[];const isTest=p=>p.includes(".spec.")||p.includes(".test.")||p.includes("__tests__")||p.includes("__fixtures__")||p.includes("__mocks__");const real=ds.filter(x=>!isTest(x.location.path));const n=real.filter(x=>x.category==="lint/style/noNonNullAssertion");console.log("NONNULL="+n.length+" IMAP="+n.filter(x=>x.location.path.includes("imap.provider.ts")).length);});' # erwartet: NONNULL=6 IMAP=0</automated>
|
||||
</verify>
|
||||
<done>
|
||||
Alle fuenf beurteilt. Die beiden ueberfluessigen Ausrufezeichen in imap.provider.ts sind weg,
|
||||
tsc ist gruen. Die drei sicheren stehen unveraendert und ihre Begruendung nennt jeweils die
|
||||
garantierende Zeile. NONNULL steht bei 6, keines davon mehr in imap.provider.ts.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
ASVS Level 1, block_on high.
|
||||
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Browser zu API, Datei-Upload | Zertifikatsdateien kommen ungeprueft vom Benutzer und werden von node-forge zerlegt |
|
||||
| Browser zu Admin-Oberflaeche | admin/ldap konfiguriert Verzeichnisbindungen; eine falsch zugeordnete Zeile wirkt hier unmittelbar auf Zugaenge |
|
||||
| IMAP-Server zu API | Nachrichtenkopfdaten stammen aus einer fremden Quelle |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-iwr-01 | Tampering | admin/ldap/page.tsx Listen | medium | accept | Beim Planen widerlegt: die veraenderliche Liste nutzt bereits mapping.id, die 6 gemeldeten Listen sind zustandslose Meldungen, die komplett ersetzt werden. Kein Bedienpfad, auf dem ein Eintrag an der falschen Zeile haengen bleibt. |
|
||||
| T-iwr-02 | Denial of Service | cert-manager.service.ts, bag.cert bei Upload | high | mitigate | Zusicherung kann durch eine praeparierte PFX-Datei verletzt werden. Gemessen: alle vier Pfade enden bereits mit 400, nie 500. Task 2 setzt ausdrueckliche Waechter mit praeziser Meldung; Test mit echter Pruefdatei sichert das ab. |
|
||||
| T-iwr-03 | Tampering | mergeCerts, Zertifikatsliste aus PFX-Bags | high | mitigate | Ein fehlendes Zertifikat koennte stillschweigend ein unvollstaendiges Buendel erzeugen. An node-forge nachgemessen: sowohl certificateToPem als auch toPkcs12Asn1 werfen bei einem fehlenden Eintrag. Task 2 lehnt zusaetzlich vor der Serialisierung ausdruecklich ab, ohne zu filtern (D-04). |
|
||||
| T-iwr-04 | Information Disclosure | Fehlermeldungen mit Dateinamen | low | accept | Der Dateiname ist benutzergesteuert, erscheint aber bereits heute so in Zeile 467. Kennwoerter bleiben aus Meldung und Protokoll ausgeschlossen (T-09-02). |
|
||||
| T-iwr-05 | Spoofing | imap.provider.ts, uid aus Serverantwort | low | accept | uid ist in imapflow ein Pflichtfeld; das Entfernen des Ausrufezeichens aendert nichts am Wert, nur an der ueberfluessigen Zusicherung. |
|
||||
| T-iwr-SC | Tampering | Paketinstallationen | n/a | accept | Dieser Vorgang installiert nichts und aendert keine Abhaengigkeit (D-05). Keine Paketpruefung noetig. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
Nach allen drei Aufgaben, aus dem Wurzelverzeichnis:
|
||||
|
||||
1. `npx biome lint --reporter=json .` durch den Zaehlbefehl aus den planning_observations —
|
||||
erwartet `TOTAL=429 ERRORS=0 ARRAYKEY=19 NONNULL=6`. TOTAL faellt um genau 5, also um die
|
||||
Zahl der tatsaechlich geaenderten Stellen, und steigt nirgends anders an (D-06).
|
||||
2. `pnpm lint` — erwartet 5 successful, 5 total, 0 error-severity.
|
||||
3. `pnpm type-check` — erwartet 4 successful, 4 total.
|
||||
4. `cd apps/web && npx vitest run` — mindestens 69 Dateien, mindestens 484 Tests, alle gruen.
|
||||
5. `cd apps/api && npx vitest run` — mindestens 72 Dateien, mindestens 1141 Tests, alle gruen.
|
||||
6. `git status --porcelain` — nur die in files_modified genannten Pfade plus die Zusammenfassung.
|
||||
|
||||
Kein laufender Stapel noetig: beide beurteilten Klassen sind im Test vollstaendig nachweisbar,
|
||||
fuer die Listenschluessel ist der Komponententest ohnehin das schaerfere Werkzeug als der Browser.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Alle 30 Fundstellen haben ein Urteil mit einer Begruendung, die die Folge benennt (D-01).
|
||||
- Die 5 geaenderten Stellen sind verschwunden, die 25 stehengelassenen stehen weiter in der
|
||||
Zaehlung und sind als bewusst stehengelassen benannt (D-06).
|
||||
- Die einzige Verhaltensaenderung — praezisere Fehlermeldung bei unlesbarem Zertifikats-Bag, Status
|
||||
weiterhin 400 — ist vorab benannt und ist die beabsichtigte (D-02).
|
||||
- Keine Eingabepruefung abgeschwaecht, kein Eintrag verschluckt, kein Ersatzwert eingefuehrt (D-04).
|
||||
- Keine neue Unterdrueckung, keine neue Abhaengigkeit, keine Versionsanhebung, keine
|
||||
Neuformatierung (D-05, D-06).
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Schreibe `.planning/quick/260921-iwr-listenschluessel-per-positionsnummer-und/260921-iwr-SUMMARY.md`.
|
||||
Sie muss eine Tabelle mit allen 30 Fundstellen enthalten: Datei, Zeile, Regel, Urteil
|
||||
(echter Fehler / harmlos / Lesbarkeit) und die einzeilige Begruendung. Ausserdem die
|
||||
Vorher-Nachher-Zahlen aus dem Zaehlbefehl und die ausdrueckliche Aussage, welche Fundstellen
|
||||
bewusst stehen bleiben und warum.
|
||||
|
||||
Hinweis zur Werkzeugfalle: Das Write-Werkzeug wandelt Folgen der Form Backslash-u-vier-Ziffern in
|
||||
das jeweilige Zeichen um. Schreibe in Zusammenfassung und Commit-Nachricht keine solchen Folgen.
|
||||
Falls doch eine gebraucht wird, erzeuge sie ueber python3 mit chr(92) fuer den Backslash und
|
||||
pruefe anschliessend die Rohbytes der Datei mit `git diff --check` und `LC_ALL=C grep -n '[^[:print:][:space:]]'`.
|
||||
</output>
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
---
|
||||
phase: quick-260921-iwr
|
||||
plan: 01
|
||||
subsystem: testing
|
||||
tags: [biome, lint, react, node-forge, pkcs12, imapflow, cert-manager]
|
||||
|
||||
# Dependency graph
|
||||
requires: []
|
||||
provides:
|
||||
- 30 einzeln beurteilte Fundstellen (19 noArrayIndexKey, 11 noNonNullAssertion) mit Urteil und Folgenbegruendung
|
||||
- Drei echte Waechter in cert-manager.service.ts statt sachlich falscher Zusicherungen (bag.cert = null)
|
||||
- Zwei entfernte ueberfluessige Zusicherungen in imap.provider.ts (uid ist Pflichtfeld)
|
||||
- Neue Tests: MergeTab.test.tsx (Entfernen mittig/vorne), Rundenliste in stopwatch-widget.test.tsx, sechs neue Faelle fuer die malformte PFX in cert-manager.service.spec.ts
|
||||
affects: []
|
||||
|
||||
# Actuals (#2632)
|
||||
actuals:
|
||||
tokens: 5300
|
||||
tasks: 3
|
||||
commits: 4
|
||||
plan_head_before: cfba3c953202057064adbe691b735977654f127c
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Malformte PFX-Testdatei per forge.asn1 handgebaut statt Fixture-Datei — exakte Kontrolle ueber den Fehlerfall (bag.cert = null)"
|
||||
- "Zusicherung durch Typpraedikat-Filter ersetzt statt Filtern/Ueberspringen bei fehlendem Zertifikat (D-04)"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- apps/web/src/app/(portal)/modules/cert-manager/components/MergeTab.test.tsx
|
||||
modified:
|
||||
- apps/api/src/cert-manager/cert-manager.service.ts
|
||||
- apps/api/src/cert-manager/cert-manager.service.spec.ts
|
||||
- apps/api/src/inbox/imap.provider.ts
|
||||
- apps/web/src/components/dashboard/widgets/stopwatch-widget.tsx
|
||||
- apps/web/src/components/dashboard/widgets/stopwatch-widget.test.tsx
|
||||
|
||||
key-decisions:
|
||||
- "Alle 19 noArrayIndexKey-Fundstellen bleiben im Produktivcode unveraendert — zwoelf sind Ladeplatzhalter/Meldungslisten ohne Identitaet (tatsaechlich zehn, siehe Korrektur unten), eine (grants) braucht den Index sachlich, zwei (MergeTab, Stoppuhr) sind jetzt per Test belegt statt nur behauptet."
|
||||
- "Drei bag.cert-Zusicherungen in cert-manager.service.ts sind sachlich falsch (node-forge kann bag.cert auf null setzen), aber keine Verfuegbarkeits- oder Integritaetsluecke: alle vier betroffenen Pfade antworteten schon vorher mit 400, nie 500. Der Eingriff ist Diagnose-/Lesbarkeitsarbeit — die Meldung wird praezise, der Statuscode bleibt 400 (D-02)."
|
||||
- "Zwei uid!-Zusicherungen in imap.provider.ts entfernt: imapflow typisiert uid als Pflichtfeld, das Ausrufezeichen war wirkungslos."
|
||||
- "Drei weitere Zusicherungen (cert-manager 133/516/661, tenders.controller.ts:244, favorites-widget.tsx:149, sidebar.tsx:80) bleiben unveraendert — jede durch eine konkrete vorgelagerte Zeile oder einen strukturellen Fakt garantiert."
|
||||
|
||||
patterns-established:
|
||||
- "Handgebaute ASN.1-Testfixtures fuer node-forge-Grenzfaelle (forge.asn1.create) statt auf Zufallsdaten oder Mocks zu setzen"
|
||||
|
||||
requirements-completed: [D-01, D-02, D-03, D-04, D-05, D-06]
|
||||
|
||||
coverage:
|
||||
- id: D1
|
||||
description: "Alle 19 noArrayIndexKey-Fundstellen einzeln beurteilt, MergeTab-Entfernen (mittig + vorne) und Stoppuhr-Rundenreihenfolge per Test belegt"
|
||||
requirement: "D-01"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/web/src/app/(portal)/modules/cert-manager/components/MergeTab.test.tsx#quick-260921-iwr: Entfernen der mittleren Datei laesst genau die erste und dritte Datei uebrig..."
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "apps/web/src/components/dashboard/widgets/stopwatch-widget.test.tsx#quick-260921-iwr: zwei Runden nacheinander..."
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D2
|
||||
description: "Drei bag.cert-Zusicherungen in cert-manager.service.ts zu echten Waechtern mit praeziser 400-Meldung gemacht (parseCert, mergeCerts, convertCert)"
|
||||
requirement: "D-04"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/api/src/cert-manager/cert-manager.service.spec.ts#PFX mit unlesbarem Zertifikats-Bag (bag.cert = null)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D3
|
||||
description: "Zwei ueberfluessige Ausrufezeichen in imap.provider.ts entfernt (uid ist imapflow-Pflichtfeld)"
|
||||
requirement: "D-05"
|
||||
verification:
|
||||
- kind: other
|
||||
ref: "cd apps/api && npx tsc --noEmit (exit 0)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D4
|
||||
description: "Biome-Zaehlbefehl bestaetigt TOTAL 434 -> 429, ARRAYKEY unveraendert 19, NONNULL 11 -> 6, ERRORS 0"
|
||||
requirement: "D-06"
|
||||
verification:
|
||||
- kind: other
|
||||
ref: "npx biome lint --reporter=json . | Zaehlbefehl aus planning_observations"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
|
||||
duration: 20min
|
||||
completed: 2026-09-21
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Quick 260921-iwr: Listenschluessel per Positionsnummer und Ausrufezeichen-Zusicherungen Summary
|
||||
|
||||
**30 Fundstellen zweier Lint-Klassen einzeln beurteilt — 5 geaenderte Stellen (3 echte Waechter in cert-manager.service.ts, 2 ueberfluessige Zusicherungen in imap.provider.ts entfernt), 25 bewusst stehengelassen, zwei bislang nur behauptete Faelle jetzt per Test belegt.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~20 min
|
||||
- **Tasks:** 3
|
||||
- **Files modified:** 6 (5 geaendert, 1 neu angelegt)
|
||||
- **Commits:** 4 (3 Task-Commits + 1 Nachbesserung, siehe Deviations)
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Alle 19 `noArrayIndexKey`-Fundstellen beurteilt: zehn Ladeplatzhalter mit fester Laenge, sechs zustandslose Meldungslisten in admin/ldap, eine Fundstelle (grants), bei der der Index sachlich notwendig ist, und zwei echte Faelle (MergeTab, Stoppuhr-Runden), die jetzt per Test statt nur per Behauptung abgesichert sind.
|
||||
- Alle 11 `noNonNullAssertion`-Fundstellen beurteilt: drei sind durch node-forges eigenes Verhalten sachlich falsch (bag.cert kann null sein) und wurden zu echten Waechtern mit praeziser 400-Meldung; zwei sind schlicht ueberfluessig (imapflow typisiert `uid` als Pflichtfeld) und wurden entfernt; sechs sind durch eine konkrete Zeile oder einen strukturellen Fakt garantiert und bleiben unveraendert.
|
||||
- Neue `MergeTab.test.tsx` belegt: Entfernen der mittleren sowie der ersten Datei aus einer Dreier- bzw. Zweierliste laesst die verbleibenden Dateien mit ihrem eigenen Namen und eigenen Entfernen-Knopf zurueck — kein Verwechseln durch den Positionsschluessel.
|
||||
- Neuer Testfall in `stopwatch-widget.test.tsx` belegt: zwei Runden nacheinander zeigen die neuere Runde oben mit Rundennummer 2, die aeltere unten mit Rundennummer 1, jede Zeit bei ihrer eigenen Nummer.
|
||||
- Sechs neue Testfaelle in `cert-manager.service.spec.ts`, RED zuerst geschrieben (heutige Meldungen bestaetigt), dann GREEN nach Einbau der drei Waechter — inklusive einer selbst mit `forge.asn1` gebauten 83-Byte-PFX-Datei mit unlesbarem Zertifikats-Bag und einer Vorbedingungspruefung, dass diese Datei tatsaechlich `bag.cert = null` erzeugt.
|
||||
|
||||
## Task Commits
|
||||
|
||||
Jede Aufgabe wurde atomar committet:
|
||||
|
||||
1. **Task 1: Die 19 Positionsschluessel beurteilen und die zwei nicht offensichtlichen Faelle nachmessen** - `8716fa5` (feat)
|
||||
2. **Task 2: Die sechs Zusicherungen in cert-manager.service.ts beurteilen, drei davon zu echten Waechtern machen** - `b4aaed4` (fix, TDD: RED zuerst, dann GREEN)
|
||||
3. **Task 3: Die restlichen fuenf Zusicherungen beurteilen, zwei ueberfluessige entfernen** - `27909e4` (refactor)
|
||||
|
||||
**Nachbesserung (Deviation, siehe unten):** `de69863` (fix) — behebt einen durch Task 2 selbst eingefuehrten neuen Lint-Fund, der die Endverifikation (TOTAL=429) verfehlt haette.
|
||||
|
||||
**Plan-Metadaten:** wird vom Orchestrator committet (SUMMARY.md, STATE.md, ROADMAP.md).
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `apps/api/src/cert-manager/cert-manager.service.ts` - drei bag.cert-Zusicherungen durch echte Waechter mit praeziser 400-Meldung ersetzt
|
||||
- `apps/api/src/cert-manager/cert-manager.service.spec.ts` - neue Testgruppe fuer die malformte PFX (Vorbedingung + vier Pfade + Regressionstest fuer eine gueltige PFX)
|
||||
- `apps/api/src/inbox/imap.provider.ts` - zwei ueberfluessige `msg.uid!` zu `msg.uid` vereinfacht
|
||||
- `apps/web/src/components/dashboard/widgets/stopwatch-widget.tsx` - wirkungslose eslint-disable-Zeile durch Sachhinweis ersetzt, kein Schluessel geaendert
|
||||
- `apps/web/src/components/dashboard/widgets/stopwatch-widget.test.tsx` - neuer Testfall fuer zwei aufeinanderfolgende Runden
|
||||
- `apps/web/src/app/(portal)/modules/cert-manager/components/MergeTab.test.tsx` - neu angelegt, zwei Testfaelle fuer das Entfernen aus der Dateiliste
|
||||
|
||||
## Die 30 Fundstellen — vollstaendiges Urteil
|
||||
|
||||
Urteil-Spalte: **Lesbarkeit** = geaendert (Meldung praezisiert bzw. ueberfluessige Zusicherung entfernt, Verhalten sonst unveraendert). **harmlos** = bewusst unveraendert stehengelassen, mit Begruendung.
|
||||
|
||||
### noArrayIndexKey (19 — alle unveraendert im Produktivcode, ARRAYKEY bleibt 19)
|
||||
|
||||
| # | Datei | Zeile | Regel | Urteil | Begruendung |
|
||||
|---|-------|-------|-------|--------|--------------|
|
||||
| 1 | InvoiceHistoryTable.tsx | 129 | noArrayIndexKey | harmlos | Ladeplatzhalter, `Array.from({length:5})`, feste Laenge, keine Identitaet, Reihenfolge aendert sich nie |
|
||||
| 2 | InvoiceHistoryTable.tsx | 131 | noArrayIndexKey | harmlos | Verschachtelter Platzhalter, `Array.from({length:6})`, gleiche Begruendung |
|
||||
| 3 | VehicleTable.tsx | 342 | noArrayIndexKey | harmlos | Ladeplatzhalter, `Array.from({length:3})`, feste Laenge |
|
||||
| 4 | VehicleTable.tsx | 344 | noArrayIndexKey | harmlos | Verschachtelter Platzhalter, `Array.from({length:5})` |
|
||||
| 5 | ResultsList.tsx | 240 | noArrayIndexKey | harmlos | Ladeplatzhalter, `Array.from({length:5})` |
|
||||
| 6 | ResultsList.tsx | 242 | noArrayIndexKey | harmlos | Verschachtelter Platzhalter, `Array.from({length:6})` |
|
||||
| 7 | InboxConfigForm.tsx | 251 | noArrayIndexKey | harmlos | Ladeplatzhalter, `Array.from({length:6})`, Formularfelder ohne Inhalt |
|
||||
| 8 | EmailAlertConfigForm.tsx | 225 | noArrayIndexKey | harmlos | Ladeplatzhalter, `Array.from({length:5})` |
|
||||
| 9 | RssFeedListForm.tsx | 151 | noArrayIndexKey | harmlos | Ladeplatzhalter, `Array.from({length:2})` |
|
||||
| 10 | SourceConfigForm.tsx | 129 | noArrayIndexKey | harmlos | Ladeplatzhalter, `Array.from({length:2})` |
|
||||
| 11 | admin/ldap/page.tsx | 1040 | noArrayIndexKey | harmlos | `nameCollisions`-Meldungsliste; `setGroupImportResult(data)` ersetzt das ganze Ergebnis auf einen Schlag, keine Zeile bleibt haengen |
|
||||
| 12 | admin/ldap/page.tsx | 1049 | noArrayIndexKey | harmlos | `errors`-Meldungsliste im Gruppenimport, gleicher Ersetz-Mechanismus |
|
||||
| 13 | admin/ldap/page.tsx | 1369 | noArrayIndexKey | harmlos | `emailConflicts`-Meldungsliste; `setSyncResult(null)` dann `setSyncResult(data)` — komplettes Ersetzen |
|
||||
| 14 | admin/ldap/page.tsx | 1384 | noArrayIndexKey | harmlos | `skippedNoLogin`-Meldungsliste, gleicher Mechanismus |
|
||||
| 15 | admin/ldap/page.tsx | 1396 | noArrayIndexKey | harmlos | `entryFailures`-Meldungsliste, gleicher Mechanismus |
|
||||
| 16 | admin/ldap/page.tsx | 1405 | noArrayIndexKey | harmlos | `errors`-Meldungsliste im Sync-Ergebnis, gleicher Mechanismus |
|
||||
| 17 | admin/modules/grants/page.tsx | 246 | noArrayIndexKey | harmlos | `Fragment key={cat-${category}-${groupIndex}}` — die Gruppierung fasst nur unmittelbar aufeinanderfolgende Module gleicher Kategorie zusammen, dieselbe Kategorie kann also mehrfach vorkommen; der Positionsanteil ist notwendig, Entfernen wuerde doppelte Schluessel erzeugen. Die Haken selbst haengen an `cellKey(mod.id, g.id)`, nicht an diesem Schluessel |
|
||||
| 18 | MergeTab.tsx | 87 | noArrayIndexKey | harmlos | `key={\`${f.name}-${i}\`}` — jetzt per Test belegt statt nur behauptet: Entfernen der mittleren/ersten Datei verwechselt keine Zeile (neue MergeTab.test.tsx) |
|
||||
| 19 | stopwatch-widget.tsx | 276 | noArrayIndexKey | harmlos | `key={idx}` — Zeilen halten keinen eigenen Zustand, Rundennummer wird aus Laenge und Position berechnet; jetzt per neuem Testfall belegt (zwei Runden, richtige Zeit bei richtiger Nummer). Wirkungslose eslint-disable-Zeile ersetzt durch Sachhinweis, keine neue Unterdrueckung |
|
||||
|
||||
**Korrektur zur Planvorgabe:** Die planning_observations sprechen von "zwoelf" Ladeplatzhaltern in Gruppe (a); die dort selbst aufgelistete Datei/Zeile-Aufstellung nennt aber nur zehn Stellen (InvoiceHistoryTable x2, VehicleTable x2, ResultsList x2, InboxConfigForm x1, EmailAlertConfigForm x1, RssFeedListForm x1, SourceConfigForm x1 = 10). Zusammen mit den sechs admin/ldap-Meldungslisten, der einen grants-Stelle und den zwei echten Faellen (MergeTab, Stoppuhr) ergeben sich 10+6+1+2 = 19 — die Gesamtzahl stimmt, nur die Zwischensumme der Gruppe (a) war im Plantext falsch benannt. Kein Code-Fund betroffen, reine Dokumentationskorrektur.
|
||||
|
||||
### noNonNullAssertion (11 — 5 geaendert, 6 unveraendert, NONNULL 11 -> 6)
|
||||
|
||||
| # | Datei | Zeile | Regel | Urteil | Begruendung |
|
||||
|---|-------|-------|-------|--------|--------------|
|
||||
| 20 | cert-manager.service.ts | 133 | noNonNullAssertion | harmlos | Zerlegung des Fingerabdruck-Hex in Zweiergruppen — SHA-1/SHA-256-Hex ist immer 40/64 Zeichen lang, `match(/.{2}/g)` findet garantiert etwas. Durch die Hash-Laenge garantiert, nicht durch Eingabedaten |
|
||||
| 21 | cert-manager.service.ts | 207 | noNonNullAssertion | **Lesbarkeit** | `bags[0].cert!` in parseCert — node-forge kann bag.cert auf null setzen (lib/pkcs12.js Zeile 703-709), die Zusicherung war sachlich falsch. Folge war aber bereits behandelt (400, nie 500). Jetzt echter Waechter: praezise BadRequestException statt der irrefuehrenden "Failed to extract certificate details" |
|
||||
| 22 | cert-manager.service.ts | 469 | noNonNullAssertion | **Lesbarkeit** | `bags.map((bag) => bag.cert!)` in mergeCerts — gleicher Grund. Jetzt: alle Bag-Zertifikate werden auf Vollstaendigkeit geprueft (kein Filtern, kein Ueberspringen, D-04), bei fehlendem Zertifikat wirft der Guard unter Nennung des Dateinamens, sonst wird die Liste ueber ein Typpraedikat zurueckgegeben |
|
||||
| 23 | cert-manager.service.ts | 516 | noNonNullAssertion | harmlos | Kennwort beim PFX-Erzeugen in mergeCerts — durch die vorgelagerte Pruefung in Zeile 438-440 garantiert (bricht bei fehlendem/leerem Kennwort vorher mit 400 ab) |
|
||||
| 24 | cert-manager.service.ts | 602 | noNonNullAssertion | **Lesbarkeit** | `bags[0].cert!` in convertCert — gleicher Grund wie #21. Jetzt echter Waechter mit praeziser Meldung statt der irrefuehrenden "Failed to convert certificate to pem: serialization error" |
|
||||
| 25 | cert-manager.service.ts | 661 | noNonNullAssertion | harmlos | Kennwort beim PFX-Erzeugen in convertCert — durch die vorgelagerte Pruefung in Zeile 564-566 garantiert |
|
||||
| 26 | imap.provider.ts | 214 | noNonNullAssertion | **Lesbarkeit** | `msg.uid!` — imapflow typisiert `uid` in `FetchMessageObject` als Pflichtfeld (lib/imap-flow.d.ts Zeile 469, "Always included in the response"). Zusicherung war ueberfluessig, entfernt (nur `!` weg, sonst nichts). tsc bleibt gruen |
|
||||
| 27 | imap.provider.ts | 310 | noNonNullAssertion | **Lesbarkeit** | `msg.uid!` — gleicher Grund wie #26 |
|
||||
| 28 | tenders.controller.ts | 244 | noNonNullAssertion | harmlos | `this.tenderIngestionService!.pollDueSources()` — das Fragezeichen im Konstruktor dient nur der Argumentreihenfolge; `TenderIngestionService` steht in tenders.module.ts unter providers, wird von Nest also immer eingesetzt. Durch den Provider-Eintrag garantiert |
|
||||
| 29 | favorites-widget.tsx | 149 | noNonNullAssertion | harmlos | `byId.get(fid)!` — die Reihenfolge stammt aus `sortedFavorites` (Zeile 90-97, sortierte Kopie von `favorites`), die Zuordnungstabelle wird aus denselben Eintraegen gebaut. Durch die gemeinsame Herleitung garantiert |
|
||||
| 30 | sidebar.tsx | 80 | noNonNullAssertion | harmlos | `categories.get(cat)!.push(mod)` — Zeile 79 legt den Eintrag an, falls er fehlt. Durch die Zeile unmittelbar davor garantiert |
|
||||
|
||||
## Vorher-Nachher-Zahlen (Zaehlbefehl aus den planning_observations, Feld `category`, nie Quelltext)
|
||||
|
||||
| Zaehlwert | Vorher | Nachher | Erwartung laut Plan | Ergebnis |
|
||||
|-----------|--------|---------|----------------------|----------|
|
||||
| TOTAL | 434 | 429 | 429 | ✓ (faellt um genau 5, die Zahl der geaenderten Stellen) |
|
||||
| ERRORS | 0 | 0 | 0 | ✓ |
|
||||
| ARRAYKEY | 19 | 19 | 19 | ✓ unveraendert |
|
||||
| NONNULL | 11 | 6 | 6 | ✓ (5 geaendert: 3 cert-manager + 2 imap) |
|
||||
|
||||
## Verhaltensaenderung (vorab benannt, D-02)
|
||||
|
||||
Die einzige Verhaltensaenderung betrifft eine Eingabeklasse: eine PFX-Datei, deren Zertifikats-Bag wohlgeformt, aber nicht als X.509 lesbar ist. Vorher und nachher antworten alle vier betroffenen Pfade mit **Status 400** — das aendert sich nicht. Was sich aendert, ist ausschliesslich der **Text** der Fehlermeldung:
|
||||
|
||||
| Pfad | Meldung vorher | Meldung nachher |
|
||||
|------|-----------------|-------------------|
|
||||
| parseCert | "Failed to extract certificate details" | "Certificate bag in PFX/PKCS12 does not contain a readable X.509 certificate" |
|
||||
| mergeCerts (pem) | "Failed to create merged certificate output" | "Certificate bag in \"<Dateiname>\" does not contain a readable X.509 certificate" |
|
||||
| mergeCerts (pfx) | "Failed to create merged certificate output" | dieselbe praezise Meldung wie oben |
|
||||
| convertCert | "Failed to convert certificate to pem: serialization error" | "Certificate bag in PFX/PKCS12 does not contain a readable X.509 certificate" |
|
||||
|
||||
Das ist **Diagnose-/Lesbarkeitsarbeit, keine Fehlerbehebung**: node-forge wirft bei einem fehlenden Zertifikat in `certificateToPem` und `toPkcs12Asn1` ausnahmslos, ein still falsches Zertifikat war schon vorher ausgeschlossen. Die drei Zusicherungen waren sachlich falsch, aber ihre Folge war bereits sicher behandelt.
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- Die drei bag.cert-Zusicherungen wurden als "keine Verfuegbarkeits- und keine Integritaetsluecke" eingestuft, nachdem die gemessene Tabelle (400 statt 500 in allen vier Pfaden) und die node-forge-Pruefung (certificateToPem(null)/toPkcs12Asn1(null,...) werfen beide) das belegten. Der Eingriff blieb deshalb auf die Meldung beschraenkt, keine Statuscode- oder Fehlerbehandlungs-Aenderung.
|
||||
- In mergeCerts wurde bewusst kein Filtern/Ueberspringen fehlender Zertifikate implementiert (D-04): stattdessen prueft der Guard alle Bags auf Vollstaendigkeit und wirft bei der ersten Luecke unter Nennung des Dateinamens, erst danach wird die Liste ueber ein Typpraedikat auf den non-null-Typ eingeengt.
|
||||
- Fuer Task 1 wurde keine der 19 Fundstellen im Produktivcode geaendert — stattdessen wurden die zwei einzigen tatsaechlich dynamischen Listen (MergeTab, Stoppuhr) per neuem Test statt nur per Behauptung abgesichert, wie von der Aufgabe gefordert.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] mergeCerts-Waechter loeste einen zusaetzlichen Biome-Lint-Fund aus**
|
||||
- **Found during:** Endverifikation nach Task 3 (Schritt "1. `npx biome lint`" aus dem `<verification>`-Block des Plans)
|
||||
- **Issue:** Die erste Fassung des Guards in mergeCerts (`bagCerts.findIndex((c) => c === null)`) erzeugte einen neuen `lint/complexity/useIndexOf`-Fund (info-Severity), der TOTAL auf 430 statt der erwarteten 429 anhob — ein Verstoss gegen D-06 ("TOTAL steigt nirgends anders an"). Ein direkter Fix mit `indexOf(null)` scheiterte an `tsc`, weil `@types/node-forge` `Bag.cert` als `Certificate | undefined` deklariert (nicht `| null`), obwohl die node-forge-Laufzeit selbst `null` setzt.
|
||||
- **Fix:** Die Pruefung testet jetzt ausdruecklich auf `undefined` UND `null` (`c === undefined || c === null`) — das ist fuer Biome kein Single-Value-Vergleich mehr (kein useIndexOf-Vorschlag) und fuer TypeScript typsicher.
|
||||
- **Files modified:** apps/api/src/cert-manager/cert-manager.service.ts
|
||||
- **Verification:** `npx biome lint --reporter=json .` liefert danach TOTAL=429 ERRORS=0 ARRAYKEY=19 NONNULL=6; `tsc --noEmit` exit 0; volle api-Suite weiterhin 72/1143 gruen.
|
||||
- **Committed in:** de69863 (eigener Fix-Commit, da Task 2 bereits committet war, als die Endverifikation dies aufdeckte)
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 1 auto-fixed (Rule 1 - Bug, durch die eigene Aenderung eingefuehrt und in der Endverifikation entdeckt)
|
||||
**Impact on plan:** Kein Scope-Creep; die Korrektur war notwendig, um D-06 exakt zu erfuellen (TOTAL=429, nicht 430).
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
Keine ungeloesten Probleme. Die einzige Ueberraschung war die oben dokumentierte Deviation — sie wurde durch den letzten Verifikationsschritt selbst gefangen, bevor sie zur Endabgabe kam.
|
||||
|
||||
## Auth Gates
|
||||
|
||||
Keine — dieser Vorgang hatte keine Authentifizierungs-Interaktion.
|
||||
|
||||
## Known Stubs
|
||||
|
||||
Keine.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
Keine neue sicherheitsrelevante Oberflaeche eingefuehrt. Die drei Waechter in cert-manager.service.ts verschaerfen die Fehlerbehandlung an einer bestehenden Vertrauensgrenze (Datei-Upload, T-iwr-02/T-iwr-03 aus dem Plan-Threat-Model), ohne neue Endpunkte, Auth-Pfade oder Schema-Aenderungen einzufuehren.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - keine externe Dienstkonfiguration erforderlich.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
Die letzte Fehlerklasse vor den beiden neuen Dashboard-Widgets ist abgeschlossen: Lint-Rueckstand bei TOTAL=429, ERRORS=0. Keine Blocker fuer die naechsten geplanten Schritte (zwei neue Widgets, laut STATE.md).
|
||||
|
||||
## Self-Check
|
||||
|
||||
- FOUND: apps/api/src/cert-manager/cert-manager.service.ts (geaendert)
|
||||
- FOUND: apps/api/src/cert-manager/cert-manager.service.spec.ts (geaendert)
|
||||
- FOUND: apps/api/src/inbox/imap.provider.ts (geaendert)
|
||||
- FOUND: apps/web/src/components/dashboard/widgets/stopwatch-widget.tsx (geaendert)
|
||||
- FOUND: apps/web/src/components/dashboard/widgets/stopwatch-widget.test.tsx (geaendert)
|
||||
- FOUND: apps/web/src/app/(portal)/modules/cert-manager/components/MergeTab.test.tsx (neu)
|
||||
- FOUND: Commit 8716fa5 (git log --oneline --all)
|
||||
- FOUND: Commit b4aaed4 (git log --oneline --all)
|
||||
- FOUND: Commit 27909e4 (git log --oneline --all)
|
||||
- FOUND: Commit de69863 (git log --oneline --all)
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
---
|
||||
*Phase: quick-260921-iwr*
|
||||
*Completed: 2026-09-21*
|
||||
@@ -526,3 +526,253 @@ describe('convertCert', () => {
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PFX mit unlesbarem Zertifikats-Bag — bag.cert = null (quick-260921-iwr, D-01/D-04)
|
||||
//
|
||||
// node-forge setzt bag.cert bei einem wohlgeformten, aber nicht als X.509
|
||||
// lesbaren Zertifikats-Bag auf null (lib/pkcs12.js Zeile 703-709: der Fehler
|
||||
// aus certificateFromAsn1 wird abgefangen und durch bag.cert = null ersetzt).
|
||||
// Die drei betroffenen Zusicherungen (parseCert Zeile 207, mergeCerts Zeile
|
||||
// 469, convertCert Zeile 602) behaupten also etwas, das node-forge selbst
|
||||
// widerlegt. Alle vier Pfade antworteten schon vorher mit 400 statt 500 — die
|
||||
// hier gepruefte Aenderung ist die Meldung, nicht der Statuscode (D-02).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('PFX mit unlesbarem Zertifikats-Bag (bag.cert = null)', () => {
|
||||
let malformedPfxBuffer: Buffer;
|
||||
|
||||
beforeAll(() => {
|
||||
// Handgebaute ~83-Byte-PFX-Datei mit forge.asn1: wohlgeformte DER-Struktur
|
||||
// von aussen nach innen (PFX -> ContentInfo -> AuthenticatedSafe ->
|
||||
// ContentInfo -> SafeContents -> SafeBag -> CertBag), deren
|
||||
// Zertifikats-Bag-Inhalt aber nur "SEQUENCE { INTEGER 1 }" ist — kein
|
||||
// X.509-Zertifikat. Passwort ist die leere Zeichenkette.
|
||||
const fakeCertAsn1 = forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.SEQUENCE,
|
||||
true,
|
||||
[forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.INTEGER, false, String.fromCharCode(1))],
|
||||
);
|
||||
const fakeCertDer = forge.asn1.toDer(fakeCertAsn1).getBytes();
|
||||
|
||||
const certBagAsn1 = forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.SEQUENCE,
|
||||
true,
|
||||
[
|
||||
forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.OID,
|
||||
false,
|
||||
forge.asn1.oidToDer(forge.pki.oids.x509Certificate).getBytes(),
|
||||
),
|
||||
forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [
|
||||
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OCTETSTRING, false, fakeCertDer),
|
||||
]),
|
||||
],
|
||||
);
|
||||
|
||||
const safeBagAsn1 = forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.SEQUENCE,
|
||||
true,
|
||||
[
|
||||
forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.OID,
|
||||
false,
|
||||
forge.asn1.oidToDer(forge.pki.oids.certBag).getBytes(),
|
||||
),
|
||||
forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [certBagAsn1]),
|
||||
],
|
||||
);
|
||||
|
||||
const safeContentsAsn1 = forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.SEQUENCE,
|
||||
true,
|
||||
[safeBagAsn1],
|
||||
);
|
||||
const safeContentsDer = forge.asn1.toDer(safeContentsAsn1).getBytes();
|
||||
|
||||
const innerContentInfoAsn1 = forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.SEQUENCE,
|
||||
true,
|
||||
[
|
||||
forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.OID,
|
||||
false,
|
||||
forge.asn1.oidToDer(forge.pki.oids.data).getBytes(),
|
||||
),
|
||||
forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [
|
||||
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OCTETSTRING, false, safeContentsDer),
|
||||
]),
|
||||
],
|
||||
);
|
||||
|
||||
const authSafeAsn1 = forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.SEQUENCE,
|
||||
true,
|
||||
[innerContentInfoAsn1],
|
||||
);
|
||||
const authSafeDer = forge.asn1.toDer(authSafeAsn1).getBytes();
|
||||
|
||||
const outerContentInfoAsn1 = forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.SEQUENCE,
|
||||
true,
|
||||
[
|
||||
forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.OID,
|
||||
false,
|
||||
forge.asn1.oidToDer(forge.pki.oids.data).getBytes(),
|
||||
),
|
||||
forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [
|
||||
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OCTETSTRING, false, authSafeDer),
|
||||
]),
|
||||
],
|
||||
);
|
||||
|
||||
const pfxAsn1 = forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.SEQUENCE,
|
||||
true,
|
||||
[
|
||||
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.INTEGER, false, String.fromCharCode(3)),
|
||||
outerContentInfoAsn1,
|
||||
],
|
||||
);
|
||||
|
||||
const pfxDer = forge.asn1.toDer(pfxAsn1).getBytes();
|
||||
malformedPfxBuffer = Buffer.from(pfxDer, 'binary');
|
||||
});
|
||||
|
||||
it('Vorbedingung: forge liefert genau einen Bag mit cert=null fuer diese Datei (belegt, dass der Waechter den gemeinten Fall trifft)', () => {
|
||||
expect(malformedPfxBuffer.length).toBe(83);
|
||||
|
||||
const p12Asn1 = forge.asn1.fromDer(
|
||||
forge.util.createBuffer(malformedPfxBuffer.toString('binary')),
|
||||
);
|
||||
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, '');
|
||||
const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });
|
||||
const bags = certBags[forge.pki.oids.certBag] ?? [];
|
||||
|
||||
expect(bags).toHaveLength(1);
|
||||
expect(bags[0].cert).toBeNull();
|
||||
});
|
||||
|
||||
it('parseCert: wirft 400 mit einer Meldung, die den Zertifikats-Bag benennt, statt der irrefuehrenden "Failed to extract certificate details"', async () => {
|
||||
const service = new CertManagerService();
|
||||
|
||||
await expect(
|
||||
service.parseCert({
|
||||
file: { originalname: 'bad.pfx', buffer: malformedPfxBuffer },
|
||||
password: '',
|
||||
}),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
|
||||
await expect(
|
||||
service.parseCert({
|
||||
file: { originalname: 'bad.pfx', buffer: malformedPfxBuffer },
|
||||
password: '',
|
||||
}),
|
||||
).rejects.toThrow(/certificate bag/i);
|
||||
});
|
||||
|
||||
it('mergeCerts (outputFormat pem): wirft 400 mit derselben praezisen Aussage statt "Failed to create merged certificate output"', async () => {
|
||||
const service = new CertManagerService();
|
||||
|
||||
await expect(
|
||||
service.mergeCerts({
|
||||
files: [{ originalname: 'bad.pfx', buffer: malformedPfxBuffer }],
|
||||
outputFormat: 'pem',
|
||||
password: '',
|
||||
}),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
|
||||
await expect(
|
||||
service.mergeCerts({
|
||||
files: [{ originalname: 'bad.pfx', buffer: malformedPfxBuffer }],
|
||||
outputFormat: 'pem',
|
||||
password: '',
|
||||
}),
|
||||
).rejects.toThrow(/certificate bag/i);
|
||||
});
|
||||
|
||||
it('mergeCerts (outputFormat pfx): wirft 400 mit derselben praezisen Aussage statt "Failed to create merged certificate output"', async () => {
|
||||
const service = new CertManagerService();
|
||||
|
||||
await expect(
|
||||
service.mergeCerts({
|
||||
files: [{ originalname: 'bad.pfx', buffer: malformedPfxBuffer }],
|
||||
outputFormat: 'pfx',
|
||||
password: 'secret',
|
||||
}),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
|
||||
await expect(
|
||||
service.mergeCerts({
|
||||
files: [{ originalname: 'bad.pfx', buffer: malformedPfxBuffer }],
|
||||
outputFormat: 'pfx',
|
||||
password: 'secret',
|
||||
}),
|
||||
).rejects.toThrow(/certificate bag/i);
|
||||
});
|
||||
|
||||
it('convertCert: wirft 400 mit einer Meldung, die den Zertifikats-Bag benennt, statt der irrefuehrenden "Failed to convert certificate to pem: serialization error"', async () => {
|
||||
const service = new CertManagerService();
|
||||
|
||||
await expect(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
service.convertCert({
|
||||
file: { originalname: 'bad.pfx', buffer: malformedPfxBuffer },
|
||||
password: '',
|
||||
targetFormat: 'pem',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
|
||||
await expect(
|
||||
service.convertCert({
|
||||
file: { originalname: 'bad.pfx', buffer: malformedPfxBuffer },
|
||||
password: '',
|
||||
targetFormat: 'pem',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any),
|
||||
).rejects.toThrow(/certificate bag/i);
|
||||
});
|
||||
|
||||
it('eine gueltige PFX-Datei verhaelt sich unveraendert: parseCert liefert weiterhin die CertDetails', async () => {
|
||||
const service = new CertManagerService();
|
||||
|
||||
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: 'valid-pfx.example.com' }];
|
||||
cert.setSubject(attrs);
|
||||
cert.setIssuer(attrs);
|
||||
cert.sign(keys.privateKey, forge.md.sha256.create());
|
||||
|
||||
const p12Asn1 = forge.pkcs12.toPkcs12Asn1(keys.privateKey, [cert], 'secret', {
|
||||
algorithm: '3des',
|
||||
});
|
||||
const validPfxBuffer = Buffer.from(forge.asn1.toDer(p12Asn1).getBytes(), 'binary');
|
||||
|
||||
const result = await (service.parseCert({
|
||||
file: { originalname: 'valid.pfx', buffer: validPfxBuffer },
|
||||
password: 'secret',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
}) as Promise<any>);
|
||||
|
||||
expect(result.subject.cn).toBe('valid-pfx.example.com');
|
||||
}, 15000);
|
||||
});
|
||||
|
||||
@@ -204,7 +204,17 @@ export class CertManagerService {
|
||||
if (bags.length === 0) {
|
||||
throw new Error('No certificate bag found in PFX/PKCS12');
|
||||
}
|
||||
cert = bags[0].cert!;
|
||||
// node-forge sets bag.cert to null when the bag's content parses as
|
||||
// valid DER but is not a readable X.509 certificate (lib/pkcs12.js
|
||||
// certBag decoder). An explicit guard here — not an assertion — so
|
||||
// the 400 names the real cause (quick-260921-iwr, D-01/D-04).
|
||||
const parsedCert = bags[0].cert;
|
||||
if (!parsedCert) {
|
||||
throw new BadRequestException(
|
||||
'Certificate bag in PFX/PKCS12 does not contain a readable X.509 certificate',
|
||||
);
|
||||
}
|
||||
cert = parsedCert;
|
||||
} else {
|
||||
// ── P7B/PKCS7 file — PEM-wrapped or binary DER (Pitfall 4) ────────
|
||||
const isPemP7b = (file.buffer as Buffer)
|
||||
@@ -466,7 +476,22 @@ export class CertManagerService {
|
||||
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password ?? '');
|
||||
const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });
|
||||
const bags = certBags[forge.pki.oids.certBag] ?? [];
|
||||
return bags.map((bag) => bag.cert!);
|
||||
// node-forge sets bag.cert to null when a bag's content parses as
|
||||
// valid DER but is not a readable X.509 certificate (lib/pkcs12.js
|
||||
// certBag decoder). No entry may be silently dropped (D-04) — check
|
||||
// every bag and reject the whole file, naming it, before returning.
|
||||
const bagCerts = bags.map((bag) => bag.cert);
|
||||
// @types/node-forge declares Bag.cert as `Certificate | undefined`,
|
||||
// but node-forge's own runtime sets it to `null` for an unreadable
|
||||
// bag (lib/pkcs12.js certBag decoder) — check both, not just `===
|
||||
// undefined`, so the guard actually catches what the library does.
|
||||
const missingCertIndex = bagCerts.findIndex((c) => c === undefined || c === null);
|
||||
if (missingCertIndex !== -1) {
|
||||
throw new BadRequestException(
|
||||
`Certificate bag in "${file.originalname as string}" does not contain a readable X.509 certificate`,
|
||||
);
|
||||
}
|
||||
return bagCerts.filter((c): c is forge.pki.Certificate => c !== undefined && c !== null);
|
||||
} else {
|
||||
// P7B/PKCS7 — PEM-wrapped or binary DER (Pitfall 4)
|
||||
const isPemP7b = (file.buffer as Buffer)
|
||||
@@ -599,7 +624,17 @@ export class CertManagerService {
|
||||
if (bags.length === 0) {
|
||||
throw new Error('No certificate bag found in PFX/PKCS12');
|
||||
}
|
||||
cert = bags[0].cert!;
|
||||
// node-forge sets bag.cert to null when the bag's content parses as
|
||||
// valid DER but is not a readable X.509 certificate (lib/pkcs12.js
|
||||
// certBag decoder). An explicit guard here — not an assertion — so
|
||||
// the 400 names the real cause (quick-260921-iwr, D-01/D-04).
|
||||
const parsedCert = bags[0].cert;
|
||||
if (!parsedCert) {
|
||||
throw new BadRequestException(
|
||||
'Certificate bag in PFX/PKCS12 does not contain a readable X.509 certificate',
|
||||
);
|
||||
}
|
||||
cert = parsedCert;
|
||||
} else {
|
||||
// P7B — extract first cert
|
||||
const isPemP7b = (file.buffer as Buffer)
|
||||
|
||||
@@ -211,7 +211,7 @@ export class ImapProvider implements InboxProvider {
|
||||
|
||||
if (attachments.length > 0) {
|
||||
results.push({
|
||||
uid: msg.uid!,
|
||||
uid: msg.uid,
|
||||
messageId: msg.envelope?.messageId ?? '',
|
||||
subject: msg.envelope?.subject ?? '',
|
||||
from: msg.envelope?.from?.[0]?.address ?? '',
|
||||
@@ -307,7 +307,7 @@ export class ImapProvider implements InboxProvider {
|
||||
}
|
||||
|
||||
results.push({
|
||||
uid: msg.uid!,
|
||||
uid: msg.uid,
|
||||
messageId: msg.envelope?.messageId ?? '',
|
||||
subject: msg.envelope?.subject ?? '',
|
||||
from: msg.envelope?.from?.[0]?.address ?? '',
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// Mock next-intl — passthrough t(key) => key (same pattern as stopwatch-widget.test.tsx)
|
||||
vi.mock('next-intl', () => ({
|
||||
useTranslations: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
// Mock ../actions — MergeTab only needs mergeCertsAction and downloadBase64 to exist;
|
||||
// this test never triggers the merge action itself, only file-list management.
|
||||
vi.mock('../actions', () => ({
|
||||
mergeCertsAction: vi.fn(),
|
||||
downloadBase64: vi.fn(),
|
||||
}));
|
||||
|
||||
import { MergeTab } from './MergeTab';
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('MergeTab — Dateiliste', () => {
|
||||
it('quick-260921-iwr: Entfernen der mittleren Datei laesst genau die erste und dritte Datei uebrig, in dieser Reihenfolge, mit ihren eigenen Namen (belegt den Positionsschluessel bei einer schrumpfenden Liste)', async () => {
|
||||
render(<MergeTab password="" />);
|
||||
|
||||
const fileInput = screen.getByTestId('merge-file-input');
|
||||
const file1 = new File(['cert1'], 'erste.pem', { type: 'application/x-pem-file' });
|
||||
const file2 = new File(['cert2'], 'mittlere.pem', { type: 'application/x-pem-file' });
|
||||
const file3 = new File(['cert3'], 'dritte.pem', { type: 'application/x-pem-file' });
|
||||
|
||||
fireEvent.change(fileInput, { target: { files: [file1, file2, file3] } });
|
||||
|
||||
// Alle drei Dateien sind zunaechst gelistet
|
||||
expect(screen.getByText('erste.pem')).toBeInTheDocument();
|
||||
expect(screen.getByText('mittlere.pem')).toBeInTheDocument();
|
||||
expect(screen.getByText('dritte.pem')).toBeInTheDocument();
|
||||
|
||||
// Die mittlere Datei ueber ihren Entfernen-Knopf loeschen (aria-label enthaelt den Dateinamen)
|
||||
const removeMiddleBtn = screen.getByLabelText('Remove mittlere.pem');
|
||||
fireEvent.click(removeMiddleBtn);
|
||||
|
||||
// "mittlere.pem" ist verschwunden
|
||||
expect(screen.queryByText('mittlere.pem')).not.toBeInTheDocument();
|
||||
|
||||
// Die erste und dritte Datei stehen weiterhin mit ihren eigenen Namen in der Liste,
|
||||
// in dieser Reihenfolge — kein Verwechseln durch den Positionsschluessel.
|
||||
const remainingNames = screen
|
||||
.getAllByText(/\.pem$/)
|
||||
.map((el) => el.textContent);
|
||||
expect(remainingNames).toEqual(['erste.pem', 'dritte.pem']);
|
||||
|
||||
// Die dritte Datei behaelt ihren eigenen Entfernen-Knopf (eigener Name im aria-label,
|
||||
// nicht der der geloeschten mittleren Datei).
|
||||
expect(screen.getByLabelText('Remove erste.pem')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Remove dritte.pem')).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('Remove mittlere.pem')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('quick-260921-iwr: Entfernen der ersten Datei laesst die zweite an ihrer eigenen Stelle mit eigenem Namen zurueck', async () => {
|
||||
render(<MergeTab password="" />);
|
||||
|
||||
const fileInput = screen.getByTestId('merge-file-input');
|
||||
const file1 = new File(['cert1'], 'a.pem', { type: 'application/x-pem-file' });
|
||||
const file2 = new File(['cert2'], 'b.pem', { type: 'application/x-pem-file' });
|
||||
|
||||
fireEvent.change(fileInput, { target: { files: [file1, file2] } });
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Remove a.pem'));
|
||||
|
||||
expect(screen.queryByText('a.pem')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('b.pem')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Remove b.pem')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -158,6 +158,43 @@ describe('StopwatchWidget', () => {
|
||||
expect(lapItems.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('quick-260921-iwr: zwei Runden nacheinander — neuere Runde steht oben, Rundennummern 2 und 1 stimmen zu ihrer eigenen Zeit (belegt den Positionsschluessel bei einer von vorn wachsenden Liste)', async () => {
|
||||
render(<StopwatchWidget instanceId="sw-laps" config={{}} isEditMode={false} />);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /stopwatch\.start/i }));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /stopwatch\.lap/i }));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /stopwatch\.lap/i }));
|
||||
});
|
||||
|
||||
const lapItems = screen.getAllByRole('listitem');
|
||||
expect(lapItems).toHaveLength(2);
|
||||
|
||||
// Oben steht die neuere Runde (Nummer 2), unten die aeltere (Nummer 1) —
|
||||
// die Liste waechst von vorn, newest-first.
|
||||
expect(lapItems[0]?.textContent).toContain('Runde 2');
|
||||
expect(lapItems[1]?.textContent).toContain('Runde 1');
|
||||
|
||||
// Runde 2 wurde spaeter aufgezeichnet und zeigt daher die laengere Zeit,
|
||||
// Runde 1 die kuerzere — jede Zeit steht bei ihrer eigenen Nummer.
|
||||
expect(lapItems[0]?.textContent).toContain('00:02');
|
||||
expect(lapItems[1]?.textContent).toContain('00:01');
|
||||
});
|
||||
|
||||
it('reload reconstruction: renders ~7000ms elapsed from config with startedAt 5s ago', async () => {
|
||||
// Fix system time so Date.now() is deterministic
|
||||
const fixedNow = new Date('2026-07-01T12:00:00.000Z');
|
||||
|
||||
@@ -271,8 +271,10 @@ export function StopwatchWidget({ instanceId, config, isEditMode: _isEditMode }:
|
||||
<div className="mt-1 max-h-32 overflow-y-auto border-t border-border pt-2">
|
||||
<ul className="space-y-0.5">
|
||||
{sw.laps.map((lapMs, idx) => (
|
||||
// Positionsschluessel ist hier vertretbar: die Zeilen halten keinen
|
||||
// eigenen Zustand, und die angezeigte Rundennummer wird ohnehin aus
|
||||
// Laenge und Position berechnet (quick-260921-iwr, D-01/D-06).
|
||||
<li
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={idx}
|
||||
className="flex justify-between px-1 py-0.5 text-xs text-muted-foreground"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user