Files
nexarch/archive/internal/moduleadapter/moduleadapter_test.go
T
sysops 0db32007ba 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.
2026-08-30 01:43:33 +02:00

155 lines
5.1 KiB
Go

package moduleadapter
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
func jsonDecode(r *http.Request, v interface{}) error {
defer func() { _ = r.Body.Close() }()
return json.NewDecoder(r.Body).Decode(v)
}
func requireTestPool(t *testing.T) *pgxpool.Pool {
t.Helper()
dsn := os.Getenv("TEST_TENANT_DSN")
if dsn == "" {
t.Skip("TEST_TENANT_DSN nicht gesetzt, Integrationstest uebersprungen")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatalf("pool: %v", err)
}
t.Cleanup(func() { pool.Close() })
if _, err := pool.Exec(ctx, `
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, retention_class TEXT NOT NULL, callback_url TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (module_name, object_type)
);
`); err != nil {
t.Fatalf("schema: %v", err)
}
t.Cleanup(func() { _, _ = pool.Exec(context.Background(), `TRUNCATE module_registrations`) })
return pool
}
// TestRegister_TwoModulesNoCollision ist Pruefung 1: zwei fiktive Module
// (DMS, Mail) parallel registriert ohne Kollision.
func TestRegister_TwoModulesNoCollision(t *testing.T) {
pool := requireTestPool(t)
ctx := context.Background()
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", "klasse-korrespondenz", "https://mail.example.test/callback")
if err != nil {
t.Fatalf("mail registrieren: %v", err)
}
if dmsReg.ID == mailReg.ID {
t.Fatal("dms und mail erhielten dieselbe id - kollision")
}
all, err := ListRegistrations(ctx, pool)
if err != nil {
t.Fatalf("listregistrations: %v", err)
}
if len(all) != 2 {
t.Fatalf("erwartet 2 registrierungen, habe %d", len(all))
}
}
// TestRegister_IsIdempotent_UnchangedExistingState ist Pruefung 3:
// erneute Registrierung desselben Objekttyps aendert NICHTS am
// bestehenden Zustand - auch nicht bei abweichender callback_url im
// zweiten Aufruf.
func TestRegister_IsIdempotent_UnchangedExistingState(t *testing.T) {
pool := requireTestPool(t)
ctx := context.Background()
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", "klasse-ANDERE", "https://dms.example.test/ANDERE-url")
if err != nil {
t.Fatalf("zweite registrierung: %v", err)
}
if second.ID != first.ID {
t.Fatalf("erneute registrierung erzeugte neue id: %q, want %q", second.ID, first.ID)
}
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 {
t.Fatalf("listregistrations: %v", err)
}
if len(all) != 1 {
t.Fatalf("erwartet weiterhin genau 1 registrierung, habe %d", len(all))
}
}
// TestNotifyDestruction_CallsRealTestEndpoint ist Pruefung 2: Rueckruf
// bei Vernichtung erfolgreich gegen einen Testendpunkt ausgefuehrt -
// echter HTTP-Server, echter Request, echte Payload-Pruefung.
func TestNotifyDestruction_CallsRealTestEndpoint(t *testing.T) {
var receivedNotice DestructionNotice
called := false
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
if r.Method != http.MethodPost {
t.Errorf("erwartet POST, habe %s", r.Method)
}
if err := jsonDecode(r, &receivedNotice); err != nil {
t.Errorf("payload dekodieren: %v", err)
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
notice := DestructionNotice{ObjectType: "document", ObjectReference: "doc-789", DestroyedAt: time.Now().UTC()}
if err := NotifyDestruction(context.Background(), server.Client(), server.URL, notice); err != nil {
t.Fatalf("notifydestruction: %v", err)
}
if !called {
t.Fatal("testendpunkt wurde nie aufgerufen")
}
if receivedNotice.ObjectReference != "doc-789" {
t.Fatalf("empfangene objekt-referenz = %q, want doc-789", receivedNotice.ObjectReference)
}
}
// TestNotifyDestruction_ReturnsErrorOnNonSuccessStatus ist Nachweis des
// Fehlerverhaltens: ein fehlschlagender Rueckruf wird als Fehler
// gemeldet, nicht verschluckt.
func TestNotifyDestruction_ReturnsErrorOnNonSuccessStatus(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()
err := NotifyDestruction(context.Background(), server.Client(), server.URL, DestructionNotice{})
if err == nil {
t.Fatal("erwartet fehler bei 500-antwort des rueckruf-endpunkts")
}
}