fix(archive): RET-05 retention_class fehlte, AC1 verlangt es explizit

Vor Board-Flip bemerkt: Akzeptanzkriterium 1 fordert Objekttyp MIT
Aufbewahrungsklasse UND Rueckruf-Adresse, retention_class fehlte im
ersten Entwurf komplett. Migration, Registration-Struct, Register,
ListRegistrations und RegisterHandler ergaenzt, Tests angepasst
(Idempotenz jetzt auch fuer retention_class geprueft, nicht nur
callback_url). Real auf 131 gedroppt und neu angewendet.
This commit is contained in:
sysops
2026-08-30 01:43:33 +02:00
parent 19dca43012
commit 0db32007ba
6 changed files with 52 additions and 31 deletions
+15
View File
@@ -24,6 +24,19 @@ fehlgeschlagenem Rückruf (Interface-Vertrag ist klar: Erfolg = HTTP
2xx, sonst Fehler — WIE mit einem Fehler umgegangen wird, ist 2xx, sonst Fehler — WIE mit einem Fehler umgegangen wird, ist
Aufgabe des aufrufenden Vernichtungs-Jobs, nicht dieses Pakets). Aufgabe des aufrufenden Vernichtungs-Jobs, nicht dieses Pakets).
## Korrektur vor Abschluss: `retention_class` fehlte im ersten Entwurf
Akzeptanzkriterium 1 verlangt "Objekttyp MIT Aufbewahrungsklasse UND
Rückruf-Adresse" — der erste Entwurf von `module_registrations` und
`Register` hatte nur `callback_url`, `retention_class` fehlte komplett.
Vor dem Board-Flip auf „Fertig" bemerkt und korrigiert: Migration,
`Registration`-Struct, `Register`, `ListRegistrations` und
`RegisterHandler` um `retention_class` ergänzt, alle Tests entsprechend
angepasst (inkl. Idempotenz-Nachweis auch für `retention_class`, nicht
nur `callback_url`). Reale, bereits angewendete Migration auf
`dms_tenant_test` musste dafür gedroppt und neu angewendet werden (kein
Produktivbestand betroffen, Testsystem).
## Umsetzung ## Umsetzung
- `migrations/0003_module_registrations.up.sql`/`.down.sql` - `migrations/0003_module_registrations.up.sql`/`.down.sql`
@@ -68,6 +81,8 @@ go build ./... -> clean
go vet ./... -> clean go vet ./... -> clean
golangci-lint run ./... -> 0 issues golangci-lint run ./... -> 0 issues
go test ./... -p 1 -count=1 -> 9/9 Pakete mit Tests ok, 0 Fehlschläge go test ./... -p 1 -count=1 -> 9/9 Pakete mit Tests ok, 0 Fehlschläge
(nach Korrektur; internal/retention und internal/moduleadapter brauchen
TEST_TENANT_DSN/TEST_TENANT_DSN_B bzw. TEST_TENANT_DSN)
``` ```
## Gesamtergebnis ## Gesamtergebnis
+4 -3
View File
@@ -10,6 +10,7 @@ import (
type registerRequest struct { type registerRequest struct {
ModuleName string `json:"module_name"` ModuleName string `json:"module_name"`
ObjectType string `json:"object_type"` ObjectType string `json:"object_type"`
RetentionClass string `json:"retention_class"`
CallbackURL string `json:"callback_url"` CallbackURL string `json:"callback_url"`
} }
@@ -27,12 +28,12 @@ func RegisterHandler(pool *pgxpool.Pool) http.HandlerFunc {
http.Error(w, "ungültiger request-body: "+err.Error(), http.StatusBadRequest) http.Error(w, "ungültiger request-body: "+err.Error(), http.StatusBadRequest)
return return
} }
if req.ModuleName == "" || req.ObjectType == "" || req.CallbackURL == "" { if req.ModuleName == "" || req.ObjectType == "" || req.RetentionClass == "" || req.CallbackURL == "" {
http.Error(w, "module_name, object_type und callback_url sind pflichtfelder", http.StatusBadRequest) http.Error(w, "module_name, object_type, retention_class und callback_url sind pflichtfelder", http.StatusBadRequest)
return return
} }
reg, err := Register(r.Context(), pool, req.ModuleName, req.ObjectType, req.CallbackURL) reg, err := Register(r.Context(), pool, req.ModuleName, req.ObjectType, req.RetentionClass, req.CallbackURL)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@@ -34,12 +34,12 @@ func TestRegisterHandler_RealHTTPRoundTrip(t *testing.T) {
return resp.StatusCode, reg return resp.StatusCode, reg
} }
status1, reg1 := post(registerRequest{ModuleName: "dms", ObjectType: "document", CallbackURL: "https://dms.example.test/original"}) status1, reg1 := post(registerRequest{ModuleName: "dms", ObjectType: "document", RetentionClass: "klasse-original", CallbackURL: "https://dms.example.test/original"})
if status1 != http.StatusOK { if status1 != http.StatusOK {
t.Fatalf("erster post: status = %d, want 200", status1) t.Fatalf("erster post: status = %d, want 200", status1)
} }
status2, reg2 := post(registerRequest{ModuleName: "dms", ObjectType: "document", CallbackURL: "https://dms.example.test/andere"}) status2, reg2 := post(registerRequest{ModuleName: "dms", ObjectType: "document", RetentionClass: "klasse-ANDERE", CallbackURL: "https://dms.example.test/andere"})
if status2 != http.StatusOK { if status2 != http.StatusOK {
t.Fatalf("zweiter post: status = %d, want 200", status2) t.Fatalf("zweiter post: status = %d, want 200", status2)
} }
@@ -24,6 +24,7 @@ type Registration struct {
ID string ID string
ModuleName string ModuleName string
ObjectType string ObjectType string
RetentionClass string
CallbackURL string CallbackURL string
} }
@@ -36,23 +37,23 @@ type Registration struct {
// UPSERT-artig), weil ein bereits registrierter Rückruf nicht // UPSERT-artig), weil ein bereits registrierter Rückruf nicht
// stillschweigend durch eine zweite, möglicherweise abweichende // stillschweigend durch eine zweite, möglicherweise abweichende
// Registrierung ersetzt werden darf. // Registrierung ersetzt werden darf.
func Register(ctx context.Context, pool *pgxpool.Pool, moduleName, objectType, callbackURL string) (Registration, error) { func Register(ctx context.Context, pool *pgxpool.Pool, moduleName, objectType, retentionClass, callbackURL string) (Registration, error) {
var reg Registration var reg Registration
err := pool.QueryRow(ctx, ` err := pool.QueryRow(ctx, `
INSERT INTO module_registrations (module_name, object_type, callback_url) INSERT INTO module_registrations (module_name, object_type, retention_class, callback_url)
VALUES ($1, $2, $3) VALUES ($1, $2, $3, $4)
ON CONFLICT (module_name, object_type) DO NOTHING ON CONFLICT (module_name, object_type) DO NOTHING
RETURNING id, module_name, object_type, callback_url RETURNING id, module_name, object_type, retention_class, callback_url
`, moduleName, objectType, callbackURL).Scan(&reg.ID, &reg.ModuleName, &reg.ObjectType, &reg.CallbackURL) `, moduleName, objectType, retentionClass, callbackURL).Scan(&reg.ID, &reg.ModuleName, &reg.ObjectType, &reg.RetentionClass, &reg.CallbackURL)
if err == nil { if err == nil {
return reg, nil return reg, nil
} }
// ON CONFLICT DO NOTHING liefert keine Zeile zurueck (pgx: ErrNoRows) - // ON CONFLICT DO NOTHING liefert keine Zeile zurueck (pgx: ErrNoRows) -
// bestehende Registrierung unveraendert nachlesen und zurueckgeben. // bestehende Registrierung unveraendert nachlesen und zurueckgeben.
err = pool.QueryRow(ctx, ` err = pool.QueryRow(ctx, `
SELECT id, module_name, object_type, callback_url FROM module_registrations SELECT id, module_name, object_type, retention_class, callback_url FROM module_registrations
WHERE module_name = $1 AND object_type = $2 WHERE module_name = $1 AND object_type = $2
`, moduleName, objectType).Scan(&reg.ID, &reg.ModuleName, &reg.ObjectType, &reg.CallbackURL) `, moduleName, objectType).Scan(&reg.ID, &reg.ModuleName, &reg.ObjectType, &reg.RetentionClass, &reg.CallbackURL)
if err != nil { if err != nil {
return Registration{}, fmt.Errorf("moduleadapter: registrierung lesen/anlegen: %w", err) return Registration{}, fmt.Errorf("moduleadapter: registrierung lesen/anlegen: %w", err)
} }
@@ -63,7 +64,7 @@ func Register(ctx context.Context, pool *pgxpool.Pool, moduleName, objectType, c
// für Statusübersichten und Tests (Pflichtprüfung 1: zwei Module // für Statusübersichten und Tests (Pflichtprüfung 1: zwei Module
// parallel registriert ohne Kollision). // parallel registriert ohne Kollision).
func ListRegistrations(ctx context.Context, pool *pgxpool.Pool) ([]Registration, error) { func ListRegistrations(ctx context.Context, pool *pgxpool.Pool) ([]Registration, error) {
rows, err := pool.Query(ctx, `SELECT id, module_name, object_type, callback_url FROM module_registrations ORDER BY module_name, object_type`) rows, err := pool.Query(ctx, `SELECT id, module_name, object_type, retention_class, callback_url FROM module_registrations ORDER BY module_name, object_type`)
if err != nil { if err != nil {
return nil, fmt.Errorf("moduleadapter: registrierungen auflisten: %w", err) return nil, fmt.Errorf("moduleadapter: registrierungen auflisten: %w", err)
} }
@@ -72,7 +73,7 @@ func ListRegistrations(ctx context.Context, pool *pgxpool.Pool) ([]Registration,
var regs []Registration var regs []Registration
for rows.Next() { for rows.Next() {
var r Registration var r Registration
if err := rows.Scan(&r.ID, &r.ModuleName, &r.ObjectType, &r.CallbackURL); err != nil { if err := rows.Scan(&r.ID, &r.ModuleName, &r.ObjectType, &r.RetentionClass, &r.CallbackURL); err != nil {
return nil, fmt.Errorf("moduleadapter: registrierungs-zeile lesen: %w", err) return nil, fmt.Errorf("moduleadapter: registrierungs-zeile lesen: %w", err)
} }
regs = append(regs, r) regs = append(regs, r)
@@ -34,7 +34,7 @@ func requireTestPool(t *testing.T) *pgxpool.Pool {
CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE IF NOT EXISTS module_registrations ( CREATE TABLE IF NOT EXISTS module_registrations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), module_name TEXT NOT NULL, id UUID PRIMARY KEY DEFAULT gen_random_uuid(), module_name TEXT NOT NULL,
object_type TEXT NOT NULL, callback_url TEXT NOT NULL, object_type TEXT NOT NULL, retention_class TEXT NOT NULL, callback_url TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (module_name, object_type) UNIQUE (module_name, object_type)
); );
@@ -51,11 +51,11 @@ func TestRegister_TwoModulesNoCollision(t *testing.T) {
pool := requireTestPool(t) pool := requireTestPool(t)
ctx := context.Background() ctx := context.Background()
dmsReg, err := Register(ctx, pool, "dms", "document", "https://dms.example.test/callback") dmsReg, err := Register(ctx, pool, "dms", "document", "klasse-B2B-standard", "https://dms.example.test/callback")
if err != nil { if err != nil {
t.Fatalf("dms registrieren: %v", err) t.Fatalf("dms registrieren: %v", err)
} }
mailReg, err := Register(ctx, pool, "mail", "message", "https://mail.example.test/callback") mailReg, err := Register(ctx, pool, "mail", "message", "klasse-korrespondenz", "https://mail.example.test/callback")
if err != nil { if err != nil {
t.Fatalf("mail registrieren: %v", err) t.Fatalf("mail registrieren: %v", err)
} }
@@ -80,11 +80,11 @@ func TestRegister_IsIdempotent_UnchangedExistingState(t *testing.T) {
pool := requireTestPool(t) pool := requireTestPool(t)
ctx := context.Background() ctx := context.Background()
first, err := Register(ctx, pool, "dms", "document", "https://dms.example.test/original") first, err := Register(ctx, pool, "dms", "document", "klasse-original", "https://dms.example.test/original")
if err != nil { if err != nil {
t.Fatalf("erste registrierung: %v", err) t.Fatalf("erste registrierung: %v", err)
} }
second, err := Register(ctx, pool, "dms", "document", "https://dms.example.test/ANDERE-url") second, err := Register(ctx, pool, "dms", "document", "klasse-ANDERE", "https://dms.example.test/ANDERE-url")
if err != nil { if err != nil {
t.Fatalf("zweite registrierung: %v", err) t.Fatalf("zweite registrierung: %v", err)
} }
@@ -95,6 +95,9 @@ func TestRegister_IsIdempotent_UnchangedExistingState(t *testing.T) {
if second.CallbackURL != "https://dms.example.test/original" { if second.CallbackURL != "https://dms.example.test/original" {
t.Fatalf("callback_url wurde ueberschrieben: %q, want unveraendert %q", second.CallbackURL, first.CallbackURL) t.Fatalf("callback_url wurde ueberschrieben: %q, want unveraendert %q", second.CallbackURL, first.CallbackURL)
} }
if second.RetentionClass != "klasse-original" {
t.Fatalf("retention_class wurde ueberschrieben: %q, want unveraendert %q", second.RetentionClass, "klasse-original")
}
all, err := ListRegistrations(ctx, pool) all, err := ListRegistrations(ctx, pool)
if err != nil { if err != nil {
@@ -8,6 +8,7 @@ CREATE TABLE IF NOT EXISTS module_registrations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
module_name TEXT NOT NULL, module_name TEXT NOT NULL,
object_type TEXT NOT NULL, object_type TEXT NOT NULL,
retention_class TEXT NOT NULL,
callback_url TEXT NOT NULL, callback_url TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (module_name, object_type) UNIQUE (module_name, object_type)