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
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
- `migrations/0003_module_registrations.up.sql`/`.down.sql`
@@ -68,6 +81,8 @@ go build ./... -> clean
go vet ./... -> clean
golangci-lint run ./... -> 0 issues
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
+7 -6
View File
@@ -8,9 +8,10 @@ import (
)
type registerRequest struct {
ModuleName string `json:"module_name"`
ObjectType string `json:"object_type"`
CallbackURL string `json:"callback_url"`
ModuleName string `json:"module_name"`
ObjectType string `json:"object_type"`
RetentionClass string `json:"retention_class"`
CallbackURL string `json:"callback_url"`
}
// RegisterHandler ist die REST-Schnittstelle (Ticket-Technikvorgabe), über
@@ -27,12 +28,12 @@ func RegisterHandler(pool *pgxpool.Pool) http.HandlerFunc {
http.Error(w, "ungültiger request-body: "+err.Error(), http.StatusBadRequest)
return
}
if req.ModuleName == "" || req.ObjectType == "" || req.CallbackURL == "" {
http.Error(w, "module_name, object_type und callback_url sind pflichtfelder", http.StatusBadRequest)
if req.ModuleName == "" || req.ObjectType == "" || req.RetentionClass == "" || req.CallbackURL == "" {
http.Error(w, "module_name, object_type, retention_class und callback_url sind pflichtfelder", http.StatusBadRequest)
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 {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -34,12 +34,12 @@ func TestRegisterHandler_RealHTTPRoundTrip(t *testing.T) {
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 {
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 {
t.Fatalf("zweiter post: status = %d, want 200", status2)
}
+14 -13
View File
@@ -21,10 +21,11 @@ import (
// Registration ist EIN registrierter Objekttyp eines Moduls.
type Registration struct {
ID string
ModuleName string
ObjectType string
CallbackURL string
ID string
ModuleName string
ObjectType string
RetentionClass string
CallbackURL string
}
// Register registriert einen Objekttyp eines Moduls mit Rückruf-Adresse
@@ -36,23 +37,23 @@ type Registration struct {
// UPSERT-artig), weil ein bereits registrierter Rückruf nicht
// stillschweigend durch eine zweite, möglicherweise abweichende
// 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
err := pool.QueryRow(ctx, `
INSERT INTO module_registrations (module_name, object_type, callback_url)
VALUES ($1, $2, $3)
INSERT INTO module_registrations (module_name, object_type, retention_class, callback_url)
VALUES ($1, $2, $3, $4)
ON CONFLICT (module_name, object_type) DO NOTHING
RETURNING id, module_name, object_type, callback_url
`, moduleName, objectType, callbackURL).Scan(&reg.ID, &reg.ModuleName, &reg.ObjectType, &reg.CallbackURL)
RETURNING id, module_name, object_type, retention_class, callback_url
`, moduleName, objectType, retentionClass, callbackURL).Scan(&reg.ID, &reg.ModuleName, &reg.ObjectType, &reg.RetentionClass, &reg.CallbackURL)
if err == nil {
return reg, nil
}
// ON CONFLICT DO NOTHING liefert keine Zeile zurueck (pgx: ErrNoRows) -
// bestehende Registrierung unveraendert nachlesen und zurueckgeben.
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
`, 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 {
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
// parallel registriert ohne Kollision).
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 {
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
for rows.Next() {
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)
}
regs = append(regs, r)
@@ -34,7 +34,7 @@ func requireTestPool(t *testing.T) *pgxpool.Pool {
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE IF NOT EXISTS module_registrations (
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(),
UNIQUE (module_name, object_type)
);
@@ -51,11 +51,11 @@ func TestRegister_TwoModulesNoCollision(t *testing.T) {
pool := requireTestPool(t)
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 {
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 {
t.Fatalf("mail registrieren: %v", err)
}
@@ -80,11 +80,11 @@ func TestRegister_IsIdempotent_UnchangedExistingState(t *testing.T) {
pool := requireTestPool(t)
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 {
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 {
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" {
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)
if err != nil {
@@ -5,10 +5,11 @@
-- isolierte Tenant-DB, TEN-01) - kein tenant_id-Feld noetig, dieselbe
-- Begruendung wie RET-01s retention_objects.
CREATE TABLE IF NOT EXISTS module_registrations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
module_name TEXT NOT NULL,
object_type TEXT NOT NULL,
callback_url TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
module_name 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(),
UNIQUE (module_name, object_type)
);