package tenant import ( "context" "testing" "time" ) // fakeRetentionChecker simuliert Archive RET-03/CMP-06 in Tests — echte // Anbindung existiert noch nicht (siehe retention.go), diese Kachel ruft nur auf. type fakeRetentionChecker struct { blocked map[string]string // tenantID -> Grund } func (f fakeRetentionChecker) CheckTenantRetention(_ context.Context, tenantID string) (RetentionResult, error) { if reason, ok := f.blocked[tenantID]; ok { return RetentionResult{Blocked: true, Reason: reason}, nil } return RetentionResult{Blocked: false}, nil } // Akzeptanzkriterium 1 + Pruefung 1: Loeschung eines Tenants mit aktiver // GoBD-Aufbewahrungspflicht wird abgewiesen, Grund wird protokolliert // (Akzeptanzkriterium 2). func TestLifecycle_ProcessDueDeletions_BlockedByRetention(t *testing.T) { registry, lifecycle, adminPool, cleanup := newLifecycleTestSetup(t) defer cleanup() provisionTestTenant(t, registry, adminPool, "lc_retention_blocked") ctx := context.Background() tenantBeforeSchedule, err := registry.GetBySlug(ctx, "lc_retention_blocked") if err != nil { t.Fatalf("get tenant: %v", err) } if _, err := registry.ScheduleDeletion(ctx, "lc_retention_blocked", -time.Minute); err != nil { t.Fatalf("schedule deletion: %v", err) } lifecycle.WithRetentionChecker(fakeRetentionChecker{ blocked: map[string]string{ tenantBeforeSchedule.ID: "GoBD-Aufbewahrungsfrist bis 2034-01-01 (Buchungsbeleg-Klasse)", }, }) processed, err := lifecycle.ProcessDueDeletions(ctx) if err != nil { t.Fatalf("process due deletions: %v", err) } if processed != 0 { t.Fatalf("erwartet 0 tatsaechlich verarbeitete loeschungen, habe %d", processed) } after, err := registry.GetBySlug(ctx, "lc_retention_blocked") if err != nil { t.Fatalf("get tenant nach sweep: %v", err) } if after.Status != StatusPendingDeletion { t.Fatalf("status = %q, want pending_deletion (gesperrt, nicht geloescht)", after.Status) } if after.RetentionBlockReason == nil || *after.RetentionBlockReason == "" { t.Fatal("erwartet gesetzten retention_block_reason (Akzeptanzkriterium 2)") } if after.RetentionCheckedAt == nil { t.Fatal("erwartet gesetzten retention_checked_at") } var exists bool if err := adminPool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname = $1)`, dbNameForSlug("lc_retention_blocked")).Scan(&exists); err != nil { t.Fatalf("pg_database pruefen: %v", err) } if !exists { t.Fatal("tenant-datenbank haette NICHT geloescht werden duerfen (retention-sperre)") } } // Akzeptanzkriterium 1 + Pruefung 2: Loeschung eines Tenants mit Legal Hold // wird ebenfalls abgewiesen — derselbe Mechanismus wie GoBD-Frist, nur anderer Grund. func TestLifecycle_ProcessDueDeletions_BlockedByLegalHold(t *testing.T) { registry, lifecycle, adminPool, cleanup := newLifecycleTestSetup(t) defer cleanup() provisionTestTenant(t, registry, adminPool, "lc_legal_hold") ctx := context.Background() tenant, err := registry.GetBySlug(ctx, "lc_legal_hold") if err != nil { t.Fatalf("get tenant: %v", err) } if _, err := registry.ScheduleDeletion(ctx, "lc_legal_hold", -time.Minute); err != nil { t.Fatalf("schedule deletion: %v", err) } lifecycle.WithRetentionChecker(fakeRetentionChecker{ blocked: map[string]string{ tenant.ID: "Legal Hold: laufendes Gerichtsverfahren, Aktenzeichen XY-2026-042", }, }) if _, err := lifecycle.ProcessDueDeletions(ctx); err != nil { t.Fatalf("process due deletions: %v", err) } after, err := registry.GetBySlug(ctx, "lc_legal_hold") if err != nil { t.Fatalf("get tenant nach sweep: %v", err) } if after.Status != StatusPendingDeletion { t.Fatalf("status = %q, want pending_deletion", after.Status) } if after.RetentionBlockReason == nil || *after.RetentionBlockReason == "" { t.Fatal("erwartet gesetzten retention_block_reason") } } // Akzeptanzkriterium 3 + Pruefung 3: nach Aufhebung aller Sperren wird die // Loeschung bei der naechsten Sweep-Runde automatisch ausgefuehrt — kein // manueller Re-Trigger noetig, derselbe Sweeper-Aufruf greift erneut. func TestLifecycle_ProcessDueDeletions_ExecutesAfterRetentionCleared(t *testing.T) { registry, lifecycle, adminPool, cleanup := newLifecycleTestSetup(t) defer cleanup() provisionTestTenant(t, registry, adminPool, "lc_retention_cleared") ctx := context.Background() tenant, err := registry.GetBySlug(ctx, "lc_retention_cleared") if err != nil { t.Fatalf("get tenant: %v", err) } if _, err := registry.ScheduleDeletion(ctx, "lc_retention_cleared", -time.Minute); err != nil { t.Fatalf("schedule deletion: %v", err) } blockingChecker := fakeRetentionChecker{blocked: map[string]string{tenant.ID: "Aufbewahrungsfrist laeuft noch"}} lifecycle.WithRetentionChecker(blockingChecker) if _, err := lifecycle.ProcessDueDeletions(ctx); err != nil { t.Fatalf("erster sweep (blockiert): %v", err) } blockedState, err := registry.GetBySlug(ctx, "lc_retention_cleared") if err != nil { t.Fatalf("get tenant nach erstem sweep: %v", err) } if blockedState.Status != StatusPendingDeletion { t.Fatalf("status nach erstem sweep = %q, want pending_deletion", blockedState.Status) } // Sperre aufgehoben: naechster Checker blockiert nicht mehr (fakeRetentionChecker.blocked leer). lifecycle.WithRetentionChecker(fakeRetentionChecker{}) processed, err := lifecycle.ProcessDueDeletions(ctx) if err != nil { t.Fatalf("zweiter sweep (unblockiert): %v", err) } if processed != 1 { t.Fatalf("erwartet genau 1 verarbeitete loeschung im zweiten sweep, habe %d", processed) } final, err := registry.GetBySlug(ctx, "lc_retention_cleared") if err != nil { t.Fatalf("get tenant nach zweitem sweep: %v", err) } if final.Status != StatusDeleted { t.Fatalf("status = %q, want deleted", final.Status) } }