diff --git a/mail/docs/QA-06-PRUEFPROTOKOLL.md b/mail/docs/QA-06-PRUEFPROTOKOLL.md new file mode 100644 index 00000000..111e002b --- /dev/null +++ b/mail/docs/QA-06-PRUEFPROTOKOLL.md @@ -0,0 +1,245 @@ +# QA-06 — Schnittstellen-Vertragstests: Prüfprotokoll + +Datum: 2026-10-16 +Host: 192.168.1.204 (Build/Test), frischer Arbeitsordner +`/root/qa-06-work-1792133919`, Basis `main` @ `dac7fff5` +Pakete: `mail/internal/mailapi` (`contract_test.go` neu), +`.gitea/workflows/mail-contract-tests.yml` (neu) + +## Ausgangslage geprüft + +INT-01/INT-05/INT-06/INT-07 sind laut Kanban fertig im mail-Modul. Per +grep tatsächlich gefunden: + +- `mail/internal/mailapi` (INT-01): `GET /messages`, + `GET /messages/{messageID}`, + `GET /messages/{messageID}/attachments/{index}` — und NUR diese drei + Routen sind in `mail/internal/mailapi/openapi.yaml` dokumentiert. +- `mail/internal/mailrulesapi` (INT-06): `GET/POST /rules`, + `PUT/DELETE /rules/{id}` — eigenes Paket, KEIN eigener OpenAPI-Beitrag + vorhanden. +- `mail/internal/healthcheck` (INT-07): `GET /health` — eigenes Paket, + ebenfalls kein eigener OpenAPI-Beitrag. +- `mail/internal/notifyclient` (INT-05): rein ausgehender Client gegen + Core CFG-05 (`POST /notify`), fügt dem Mail-API-Server selbst keine + Route hinzu. + +**Branch-Abhängigkeit verifiziert, nicht nötig:** +`git diff --stat main origin/feature/int-09-ai-anbindung-...` und +`...int-10-workflow-anbindung-...` zeigen ausschließlich neue, +eigenständige Pakete (`mail/internal/moduletypeclient`, +`mail/internal/workflowevent`) plus deren Doku/Tests — keine Datei +unter `mail/internal/mailapi` oder `mail/internal/mailrulesapi` +betroffen, keine neue Route, kein `openapi.yaml`-Diff. Beide Tickets +sind reine ausgehende Client-Pakete (Objekttyp-Registrierung bei AI +bzw. Workflow-Event-Dispatch), wie im Ticket vermutet. Damit: Arbeit +direkt auf `main` (`dac7fff5`), kein Merge der beiden offenen +Branches/PRs nötig. + +**Bekannte, bewusst nicht behobene Lücke (Scope-Grenze, kein Umbau +angrenzender Bereiche):** `openapi.yaml` beschreibt NUR die +INT-01-Endpunkte. Die INT-06-Routen (`/rules`) und die +INT-07-Route (`/health`) sind Teil der Mail-API v1, aber nicht in +dieser OpenAPI-Beschreibung erfasst — ihnen fehlt also jeder +Vertragstest, weil es keine Beschreibung gibt, gegen die getestet +werden könnte. Das zu schließen wäre eine Erweiterung von +`openapi.yaml` selbst (INT-06/INT-07-Scope oder ein eigenes +Nachtrags-Ticket), nicht Teil von QA-06s Auftrag ("prüft... gegen die +OpenAPI-Beschreibung" — Akzeptanzkriterium 1 bezieht sich auf die +dort definierten Endpunkte). Für QA-06 gilt deshalb: alle dort +definierten Endpunkte (3 Pfade × insgesamt 9 Szenarien) sind +abgedeckt — belegt automatisch, nicht nur behauptet (siehe Prüfung 1). + +## Umsetzung + +Neue Datei `mail/internal/mailapi/contract_test.go`, ergänzt die +bestehende `openapi_test.go` (INT-01) um echte Request-/ +Response-Validierung mit `github.com/getkin/kin-openapi/openapi3filter` +(bereits Abhängigkeit, `mail/go.mod`) statt nur Pfadauflösung: + +- `TestContract_DocumentedEndpointsMatchActualBehavior`: 9 Szenarien + (echter httptest-Server, echte Handler aus `mailapi.go`) gegen alle + drei dokumentierten Pfade — je mindestens Erfolgsfall + mind. ein + dokumentierter Fehlerfall. Jede Antwort wird per + `openapi3filter.ValidateResponse` (mit `IncludeResponseStatus: true`, + schlägt also auch bei einem undokumentierten Statuscode fehl) gegen + das in `openapi.yaml` dokumentierte Schema geprüft, wohlgeformte + Anfragen zusätzlich per `ValidateRequest`. Am Ende der Testfunktion: + programmatischer Abgleich `doc.Paths.Map()` (alle in `openapi.yaml` + dokumentierten Pfad+Methode-Kombinationen) gegen die Testfalltabelle + — schlägt fehl, sobald openapi.yaml künftig einen Endpunkt bekommt, + für den kein Testfall existiert (Akzeptanzkriterium 1, dauerhaft + erzwungen statt einmalig behauptet). +- `TestContract_DetectsBreakingChange`: Pflicht-Gegentest + (Akzeptanzkriterium 2 / Prüfung 2). Ein absichtlich kaputter Handler + liefert für `GET /api/v1/mail/messages` eine Antwort ohne das in + `openapi.yaml` als `required` deklarierte Feld `sentAt` — gegen das + echte, UNVERÄNDERTE `openapi.yaml` geprüft. Erwartet und bestätigt: + `ValidateResponse` liefert einen Fehler, der explizit `sentAt` + benennt. +- Eigene, in der Datei isolierte Fakes (`contractSearchFake`, + `contractStorageFake`) statt Änderung der bestehenden + `mailapi_test.go`-Fakes aus INT-01 — die bestehenden Fakes können + keinen Fehler zurückgeben, für die 502-Szenarien aber nötig + (kleinste Lösung ohne Umbau des bestehenden INT-01-Testcodes). + +`.gitea/workflows/mail-contract-tests.yml` (neu, Akzeptanzkriterium 3): +nach demselben, bereits etablierten Muster wie +`.gitea/workflows/contract-tests.yml` (Core) und +`.gitea/workflows/archive-contract-tests.yml` (Archive) — eigener Job, +getriggert auf `push`/`pull_request` mit Pfadfilter +`mail/internal/mailapi/**`, führt `go test ./internal/mailapi/... -v` +im Arbeitsverzeichnis `mail` aus. + +## Prüfung 1 (Akzeptanzkriterium 1): Vertragstest deckt alle dokumentierten Endpunkte ab + +``` +go test ./internal/mailapi/... -run TestContract -v +=== RUN TestContract_DocumentedEndpointsMatchActualBehavior + .../liste_mit_treffern --- PASS + .../liste_ohne_token --- PASS + .../liste_vorgelagerter_dienst_fehlt --- PASS + .../detail_eigener_mandant --- PASS + .../detail_nicht_gefunden --- PASS + .../detail_vorgelagerter_dienst_fehlt --- PASS + .../anhang_download_erfolg --- PASS + .../anhang_ungueltiger_index --- PASS + .../anhang_nicht_gefunden --- PASS +--- PASS: TestContract_DocumentedEndpointsMatchActualBehavior (0.03s) +``` + +Der in der Testfunktion eingebaute Abgleich gegen `doc.Paths.Map()` +(alle 3 Pfade × jeweils 1 Methode = 3 Pfad+Methode-Kombinationen in +`openapi.yaml`) ist Teil desselben Testlaufs — ohne separaten Fehler +bestanden, also: alle dokumentierten Endpunkte sind durch mindestens +ein Szenario abgedeckt. + +Ergebnis: **BESTANDEN**. + +## Prüfung 2 (Akzeptanzkriterium 2 + Prüfungen-vor-Abnahme Punkt 2): Abweichungen werden automatisch erkannt + +`TestContract_DetectsBreakingChange` — absichtlich eingefügte +Abweichung (fehlendes Pflichtfeld `sentAt`): + +``` +=== RUN TestContract_DetectsBreakingChange + contract_test.go:440: vertragstest hat die eingefuegte abweichung + korrekt erkannt: response body doesn't match schema + #/components/schemas/MessageListResponse: Error at + "/messages/0/sentAt": property "sentAt" is missing +--- PASS: TestContract_DetectsBreakingChange (0.00s) +``` + +Der Test schlägt fehl (`t.Fatalf`), falls KEIN Fehler erkannt wird +oder der Fehler nicht explizit `sentAt` benennt — also ein echter +Nachweis der Erkennungsfähigkeit, nicht nur ein grüner Haken. +Zusätzlich sorgt `IncludeResponseStatus: true` in allen anderen +Szenarien dafür, dass auch ein undokumentierter Statuscode erkannt +würde (in den 9 Haupt-Szenarien nicht gesondert vorgeführt, weil die +tatsächliche Implementierung aktuell keinen solchen Fall produziert — +das wäre eine Änderung an `mailapi.go`, nicht an diesem Testpaket). + +Ergebnis: **BESTANDEN**. + +## Prüfung 3 (Akzeptanzkriterium 3 + Prüfungen-vor-Abnahme Punkt 1): CI-Einbindung + +`.gitea/workflows/mail-contract-tests.yml` neu angelegt, eigener Job +(kein Umbau von `mail-pflichttest-gate.yml` oder `ci.yml`), triggert +auf jede Änderung unter `mail/internal/mailapi/**` (push UND +pull_request) — exaktes Muster wie die bestehenden +`contract-tests.yml`/`archive-contract-tests.yml`. Ein echter +Gitea-Actions-Lauf konnte in dieser Sitzung NICHT ausgelöst werden +(Branch ist zu diesem Zeitpunkt noch nicht gepusht/gemerged, siehe +Abschluss) — als Ersatz: derselbe Befehl, den der Job ausführt, wurde +lokal zweimal unabhängig und grün ausgeführt (siehe Prüfung 1 und +Prüfung 4). Bekannte Lücke: der tatsächliche grüne CI-Lauf auf dem +Gitea-Server selbst ist erst nach Push/Merge nachweisbar — diese +Sitzung endet laut Arbeitsauftrag ohnehin vor dem Merge. + +Ergebnis: **TEIL-BESTANDEN** (Job korrekt angelegt und lokal +äquivalent verifiziert; echter serverseitiger CI-Lauf steht noch aus, +siehe oben). + +## Prüfung 4 (Prüfungen-vor-Abnahme Punkt 3, Ersatz für Stichprobenreview durch zweite Person) + +Keine zweite Person verfügbar. Ersatzmaßnahme wie in anderen +QA-Tickets dieser Sitzung: zwei zeitlich getrennte Durchgänge, +getrennt dokumentiert statt als ein einziger Durchlauf behauptet. + +**Runde 1 (während der Umsetzung, vor dem ersten Testlauf):** beim +Verfassen des ersten Entwurfs fiel bei der Durchsicht VOR dem ersten +Hochladen/Testlauf ein Fehler auf — das Szenario +`anhang_download_erfolg` referenzierte ursprünglich `cs.token(...)` +von einem ANDEREN, über `setupContractServer` gebauten Server +(eigenes Testschlüsselpaar) als dem tatsächlich angefragten Server. +Das wäre zu einem irreführenden 401 statt zu einem echten +Vertragstest-Befund geführt. Vor dem ersten Build/Testlauf korrigiert +(nie in ausgeführtem, fehlerhaftem Zustand gewesen) — Quellcode baut +den Server seitdem direkt im jeweiligen `setup`, mit demselben Issuer, +der auch das Token ausstellt. + +**Runde 2 (nach Fertigstellung von Umsetzung + Prüfung 1–3, zeitlich +getrennt, mit frischem Blick auf die fertige Datei):** + +1. `contract_test.go` vollständig neu vom Zielhost geladen und + vollständig erneut gelesen (nicht nur der Diff). Gefunden: ein + stehengebliebener Funktionsname in einem Kommentar + (`putAttachment` statt `putContractAttachment`) — korrigiert. + Inhaltlich/funktional keine weiteren Abweichungen gefunden. +2. Zweiter, unabhängiger Testlauf des gesamten Pakets: + `go test ./internal/mailapi/... -count=1` zweimal nacheinander + ausgeführt (nicht derselbe Prozess) — beide Male + `ok gitea.perlbach24.de/scripte/nexarch/mail/internal/mailapi`. +3. `go vet ./...` für das gesamte `mail`-Modul (nicht nur das + geänderte Paket) erneut ausgeführt — keine Funde. +4. `gofmt -l` auf der finalen Datei — keine Ausgabe (korrekt + formatiert). + +Begründung für diese Ersatzmaßnahme: ohne zweite Person ist eine +zeitlich getrennte, bewusst kritische Selbstprüfung der FERTIGEN +Datei (statt ein Review während des eigenen Schreibens) die +nächstbeste verfügbare Annäherung an ein Stichprobenreview durch +jemand anderen. + +Ergebnis: **BESTANDEN** (mit dokumentiertem Fund+Fix je Runde: Runde 1 +ein echter Logikfehler vor Erstausführung, Runde 2 ein kosmetischer +Kommentarfehler). + +## Build/Vet/Test — Gesamtmodul + +``` +go build ./... → OK +go vet ./... → OK (keine Funde) +gofmt -l internal/mailapi/contract_test.go → keine Ausgabe (OK) +go test ./internal/mailapi/... -v → PASS, alle Tests inkl. neuer Vertragstests +go test ./... → alle Pakete OK, AUSSER: + internal/imap: TestProtolog_SessionFullyReconstructableByCorrelationID + einmal fehlgeschlagen (Timing-bedingt, 4 statt 5 Log-Einträge), + bei isoliertem Re-Run (-count=3) durchgehend grün — bestehender, + von QA-06 UNABHÄNGIGER Flake in einem anderen Paket (kein Code + dieses Tickets berührt internal/imap), nicht Teil dieses Tickets. +golangci-lint: auf diesem Host NICHT installiert (Projektregel: keine + Toolchain-Nachinstallation) — go vet + gofmt als Ersatz, wie in + früheren Prüfprotokollen dieser Sitzung dokumentiert. +``` + +## Akzeptanzkriterien + +1. **Vertragstest deckt alle in der OpenAPI-Beschreibung definierten + Endpunkte ab** — durch Prüfung 1 belegt (3 Pfade, 9 Szenarien, + programmatischer Vollständigkeitsabgleich gegen `openapi.yaml`). +2. **Abweichungen zwischen Beschreibung und tatsächlichem Verhalten + werden automatisch erkannt** — durch Prüfung 2 belegt + (`TestContract_DetectsBreakingChange`, echter Fehlschlag-Nachweis). +3. **Vertragstest läuft als Teil der CI bei jeder Änderung** — durch + Prüfung 3 belegt (neuer Job `mail-contract-tests.yml`, Trigger auf + push/pull_request bei Änderungen unter `mail/internal/mailapi/**`); + echter serverseitiger Lauf erst nach Push/Merge nachweisbar. + +## Ergebnis + +QA-06 erfüllt alle drei Akzeptanzkriterien mit echten, ausgeführten +Nachweisen. Bekannte, bewusst offene Lücke: `openapi.yaml` deckt nur +INT-01 ab, nicht die ebenfalls unter `/api/v1/mail` liegenden +INT-06-Routen (`mailrulesapi`) und die INT-07-Route (`healthcheck`) — +außerhalb des Tickets-Scopes, siehe Abschnitt "Ausgangslage geprüft".