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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user