diff --git a/archive/docs/RET-05-PRUEFPROTOKOLL.md b/archive/docs/RET-05-PRUEFPROTOKOLL.md index 46198bc..bca4251 100644 --- a/archive/docs/RET-05-PRUEFPROTOKOLL.md +++ b/archive/docs/RET-05-PRUEFPROTOKOLL.md @@ -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 diff --git a/archive/internal/moduleadapter/handler.go b/archive/internal/moduleadapter/handler.go index 27d087d..63c587b 100644 --- a/archive/internal/moduleadapter/handler.go +++ b/archive/internal/moduleadapter/handler.go @@ -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 diff --git a/archive/internal/moduleadapter/handler_test.go b/archive/internal/moduleadapter/handler_test.go index 99e8194..d4b0479 100644 --- a/archive/internal/moduleadapter/handler_test.go +++ b/archive/internal/moduleadapter/handler_test.go @@ -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) } diff --git a/archive/internal/moduleadapter/moduleadapter.go b/archive/internal/moduleadapter/moduleadapter.go index f5a3788..46eb760 100644 --- a/archive/internal/moduleadapter/moduleadapter.go +++ b/archive/internal/moduleadapter/moduleadapter.go @@ -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(®.ID, ®.ModuleName, ®.ObjectType, ®.CallbackURL) + RETURNING id, module_name, object_type, retention_class, callback_url + `, moduleName, objectType, retentionClass, callbackURL).Scan(®.ID, ®.ModuleName, ®.ObjectType, ®.RetentionClass, ®.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(®.ID, ®.ModuleName, ®.ObjectType, ®.CallbackURL) + `, moduleName, objectType).Scan(®.ID, ®.ModuleName, ®.ObjectType, ®.RetentionClass, ®.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) diff --git a/archive/internal/moduleadapter/moduleadapter_test.go b/archive/internal/moduleadapter/moduleadapter_test.go index 5253dac..a98a70d 100644 --- a/archive/internal/moduleadapter/moduleadapter_test.go +++ b/archive/internal/moduleadapter/moduleadapter_test.go @@ -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 { diff --git a/archive/migrations/0003_module_registrations.up.sql b/archive/migrations/0003_module_registrations.up.sql index dcb4f09..9557068 100644 --- a/archive/migrations/0003_module_registrations.up.sql +++ b/archive/migrations/0003_module_registrations.up.sql @@ -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) );