165 lines
5.7 KiB
Go
165 lines
5.7 KiB
Go
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)
|
|
}
|
|
}
|