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") } }