# RET-03 – Prüfprotokoll: Löschworkflow & Aufbewahrungssperre (Legal Hold) Voraussetzung RET-01, RET-02 – beide bereits Fertig. ## Umsetzung - `archive/migrations/0008_legal_hold_destruction.up/down.sql`: - `legal_holds` – historisiert (Sperre wird NIE gelöscht, nur `released_at`/`released_by` gesetzt), Partial-Unique-Index erlaubt höchstens eine AKTIVE Sperre je Objekt. - `destruction_log` – append-only, real per Postgres-TRIGGER gegen UPDATE/DELETE geschützt (`destruction_log_immutable()`, `RAISE EXCEPTION`) — DB-Ebene, nicht nur Anwendungscode. - `archive/internal/deletionworkflow`: - `SetLegalHold` — Begründung PFLICHT (`ErrReasonRequired`), `ErrAlreadyOnHold` bei bereits aktiver Sperre (DB-Unique-Index als zusätzliche Absicherung gegen Wettlaufsituationen). - `ReleaseLegalHold` — hebt auf, OHNE die Zeile zu löschen (das Aufheben selbst bleibt dauerhaft nachvollziehbar). - `ReleaseExpired` — Freigabeprozess: `active` → `expired`, NIEMALS direkt `deleted`. Objekte mit aktiver Sperre werden übersprungen, unabhängig vom Fristablauf. - `Destroy` — verlangt vorherigen Status `expired` (`ErrNotReleased` sonst), prüft die Sperre ZUSÄTZLICH direkt vor der Vernichtung erneut (Verteidigung in der Tiefe gegen eine zwischenzeitlich gesetzte Sperre), schreibt Statuswechsel und Protokolleintrag in EINER Transaktion. ## Prüfungen | # | Prüfung | Ergebnis | |---|---|---| | 1 | Objekt mit aktiver Sperre widersteht einem direkten Löschversuch | **bestanden** – `TestDestroy_ObjectWithActiveHoldResistsDeletion`: Objekt bereits im Status `expired` (simuliert vorherige Freigabe), Sperre danach gesetzt, `Destroy` liefert `ErrOnLegalHold`, Status bleibt unverändert | | 2 | Protokolleintrag nach Vernichtung ist nachträglich nicht änderbar (DB-Constraint oder Trigger) | **bestanden** – `TestDestructionLog_IsImmutable`: nach echter `Destroy`-Ausführung direkter `UPDATE`/`DELETE` per rohem SQL (umgeht die Go-API vollständig) — beide scheitern real am Postgres-Trigger, nicht nur weil das Paket keine Änderungsfunktion anbietet | | 3 | Aufheben einer Sperre ist selbst protokolliert | **bestanden** – `TestReleaseLegalHold_IsItselfLogged`: nach `ReleaseLegalHold` sind `released_at`/`released_by` real gesetzt, die ursprüngliche Sperr-Zeile bleibt bestehen (kein DELETE) | **Zusätzliche Tests (Akzeptanzkriterium 1, nicht in den drei Pflichtprüfungen, aber je AC mindestens ein Test):** - `TestReleaseExpired_NoImmediateDeletionAndHoldIsRespected` — fälliges Objekt wird `expired` (NICHT `deleted`), gesperrtes fälliges Objekt bleibt `active`, nicht fälliges Objekt bleibt `active`. - `TestSetLegalHold_RequiresReason`, `TestDestroy_RequiresPriorRelease`. ## Build/Test-Ergebnis (192.168.1.131) ``` go build ./... -> clean go vet ./... -> clean golangci-lint run ./... -> 0 issues go test ./... -p 1 -> alle Archive-Pakete bestanden (inkl. deletionworkflow, 6 Tests) ``` Migration `0008_legal_hold_destruction` real auf `dms_tenant_test` angewendet (Test-DB, in der auch RET-06-API läuft) — sauber, ohne Fehler. ## Gesamtergebnis **Bestanden.** Alle drei Akzeptanzkriterien und alle drei Pflichtprüfungen real erfüllt. Damit ist das RET-Epic (RET-01 bis RET-10) vollständig abgeschlossen: Objektmodell, Fristenengine, Modul-Adapter (Interface + Dienst), Konfigurationsoberfläche (Backend + Frontend + CORS), Benachrichtigungen, WORM-Speicher und jetzt Löschworkflow mit Legal Hold.