package retention import ( "context" "os" "testing" "github.com/jackc/pgx/v5/pgxpool" ) const schemaSQL = ` CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE TABLE IF NOT EXISTS retention_objects ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), object_type TEXT NOT NULL, object_reference TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'expired', 'deleted')), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (object_type, object_reference) ); CREATE TABLE IF NOT EXISTS retention_class_assignments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), retention_object_id UUID NOT NULL REFERENCES retention_objects(id) ON DELETE CASCADE, retention_class TEXT NOT NULL, assigned_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ` func requireTestPool(t *testing.T, dsn string) *pgxpool.Pool { t.Helper() 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, schemaSQL); err != nil { t.Fatalf("schema: %v", err) } t.Cleanup(func() { _, _ = pool.Exec(context.Background(), `TRUNCATE retention_class_assignments, retention_objects CASCADE`) }) return pool } // TestRegisterObject_MapsDMSAndMailAdapterObjectsIdentically ist // Pruefung 2: Testobjekt aus fiktivem DMS- und Mail-Adapter beide // korrekt abgebildet - KEINE modulspezifischen Spalten noetig, nur // object_type/object_reference. func TestRegisterObject_MapsDMSAndMailAdapterObjectsIdentically(t *testing.T) { pool := requireTestPool(t, os.Getenv("TEST_TENANT_DSN")) ctx := context.Background() dmsID, err := RegisterObject(ctx, pool, "dms_document", "doc-123") if err != nil { t.Fatalf("dms-adapter registrieren: %v", err) } mailID, err := RegisterObject(ctx, pool, "mail_message", "msg-456") if err != nil { t.Fatalf("mail-adapter registrieren: %v", err) } if dmsID == "" || mailID == "" || dmsID == mailID { t.Fatalf("erwartet zwei unterschiedliche, gueltige ids, habe dms=%q mail=%q", dmsID, mailID) } // Idempotenz: erneute Registrierung desselben Objekts liefert dieselbe id. dmsIDAgain, err := RegisterObject(ctx, pool, "dms_document", "doc-123") if err != nil { t.Fatalf("erneute registrierung: %v", err) } if dmsIDAgain != dmsID { t.Fatalf("erneute registrierung lieferte andere id: %q, want %q", dmsIDAgain, dmsID) } } // TestAssignClass_IsHistoricized ist Pruefung fuer Akzeptanzkriterium 2: // Aufbewahrungsklasse ist eindeutig zugeordnet UND historisierbar. func TestAssignClass_IsHistoricized(t *testing.T) { pool := requireTestPool(t, os.Getenv("TEST_TENANT_DSN")) ctx := context.Background() objID, err := RegisterObject(ctx, pool, "dms_document", "doc-hist") if err != nil { t.Fatalf("registrieren: %v", err) } if err := AssignClass(ctx, pool, objID, "klasse-A"); err != nil { t.Fatalf("erste zuordnung: %v", err) } if err := AssignClass(ctx, pool, objID, "klasse-B"); err != nil { t.Fatalf("zweite zuordnung: %v", err) } current, err := CurrentClass(ctx, pool, objID) if err != nil { t.Fatalf("currentclass: %v", err) } if current.RetentionClass != "klasse-B" { t.Fatalf("aktuelle klasse = %q, want klasse-B", current.RetentionClass) } history, err := ClassHistory(ctx, pool, objID) if err != nil { t.Fatalf("classhistory: %v", err) } if len(history) != 2 || history[0].RetentionClass != "klasse-A" || history[1].RetentionClass != "klasse-B" { t.Fatalf("erwartet [klasse-A, klasse-B] chronologisch, habe %+v", history) } } // TestTenantIsolation_Negativtest ist Pruefung 3: ein in Tenant-DB A // registriertes Objekt ist ueber eine Verbindung zu Tenant-DB B technisch // nicht sichtbar - real gegen zwei unabhaengige Datenbanken (Modell C, // TEN-01), keine gemeinsame Tabelle mit tenant_id-Filter. func TestTenantIsolation_Negativtest(t *testing.T) { dsnA := os.Getenv("TEST_TENANT_DSN") dsnB := os.Getenv("TEST_TENANT_DSN_B") if dsnA == "" || dsnB == "" { t.Skip("TEST_TENANT_DSN und TEST_TENANT_DSN_B nicht beide gesetzt, Integrationstest uebersprungen") } poolA := requireTestPool(t, dsnA) poolB := requireTestPool(t, dsnB) ctx := context.Background() if _, err := RegisterObject(ctx, poolA, "dms_document", "nur-in-tenant-a"); err != nil { t.Fatalf("registrieren in tenant a: %v", err) } var count int if err := poolB.QueryRow(ctx, `SELECT count(*) FROM retention_objects WHERE object_reference = 'nur-in-tenant-a'`).Scan(&count); err != nil { t.Fatalf("tenant b abfragen: %v", err) } if count != 0 { t.Fatalf("objekt aus tenant a in tenant b sichtbar (count=%d) - mandantentrennung verletzt", count) } }