TEN-08: tenant-loeschung-unter-retention-vorbehalt-gobd (RetentionChecker-Schnittstelle gegen Archive RET-03/CMP-06, ProcessDueDeletions haelt gesperrte Tenants zurueck)
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user