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.
155 lines
5.1 KiB
Go
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")
|
|
}
|
|
}
|