package test import ( "RenewCMS/api/pkg/apperr" domainPurge "RenewCMS/internal/domain/purge" "RenewCMS/internal/domain/user" "RenewCMS/internal/infrastructure/persistence/models" persistence "RenewCMS/internal/infrastructure/persistence/repositories" "net/http" "os/exec" "reflect" "strings" "testing" "time" ) func TestPurgeRoleGuardIsAnAllowlist(t *testing.T) { for _, role := range []string{"OWNER", "Admin", "superuser"} { for _, route := range []struct{ method, path string }{ {http.MethodGet, "/v1/media/purge-settings"}, {http.MethodPatch, "/v1/media/purge-settings"}, {http.MethodGet, "/v1/media/purge-preview"}, {http.MethodPost, "/v1/media/purge"}, } { t.Run(role+" "+route.method+" "+route.path, func(t *testing.T) { env := newPurgeEnv(t) env.users.users[20] = user.User{ID: 20, Username: "other", IsVerified: true, Role: user.Role(role)} rec := env.send(t, route.method, route.path, "", 20) expectRaw(t, rec, 403, apperr.OwnerOrAdminRequiredErrorMessage+"\n") if env.purgeSettings.getCalls != 0 || env.purgeSettings.saveCalls != 0 { t.Errorf("expected no purge setting access") } }) } } } func TestPurgeLayering(t *testing.T) { list := func(t *testing.T, format string, pkg string) []string { t.Helper() output, err := exec.Command("go", "list", "-tags", "nofrontend", "-f", format, pkg).CombinedOutput() if err != nil { t.Fatalf("go list failed: %v: %s", err, output) } return strings.Fields(string(output)) } t.Run("internal/domain/purge imports only the stdlib and other domain packages", func(t *testing.T) { for _, imported := range list(t, `{{join .Imports "\n"}}`, "RenewCMS/internal/domain/purge") { if strings.Contains(imported, ".") && !strings.HasPrefix(imported, "RenewCMS/internal/domain/") { t.Errorf("unexpected import %s", imported) } } }) t.Run("the use cases import no http, chi, gorm, persistence, mailer, models or api package", func(t *testing.T) { for _, imported := range list(t, `{{join .Imports "\n"}}`, "RenewCMS/internal/infrastructure/useCases") { for _, forbidden := range []string{"net/http", "go-chi", "gorm", "persistence", "mailer", "RenewCMS/api"} { if strings.Contains(imported, forbidden) { t.Errorf("useCases must not import %s", imported) } } } }) t.Run("the scheduler imports only the stdlib and domain packages", func(t *testing.T) { imports := list(t, `{{join .Imports "\n"}}`, "RenewCMS/internal/infrastructure/scheduler") if len(imports) == 0 { t.Fatal("expected imports") } for _, imported := range imports { if strings.Contains(imported, ".") && !strings.HasPrefix(imported, "RenewCMS/internal/domain/") { t.Errorf("unexpected import %s", imported) } if strings.HasPrefix(imported, "RenewCMS/") && !strings.HasPrefix(imported, "RenewCMS/internal/domain/") { t.Errorf("scheduler must not import %s", imported) } } }) } func TestPurgeSettingsRepositoryRoundTrip(t *testing.T) { newRepo := func(t *testing.T) *persistence.PurgeSettingsRepository { t.Helper() db := openInMemoryDb(t) if err := db.AutoMigrate(&models.PurgeSettings{}); err != nil { t.Fatalf("failed to migrate: %v", err) } return persistence.NewPurgeSettingsRepository(db) } t.Run("Nothing saved yields the defaults", func(t *testing.T) { got, err := newRepo(t).Get() if err != nil || !reflect.DeepEqual(got, domainPurge.DefaultSettings()) { t.Errorf("expected defaults, got %+v, %v", got, err) } }) t.Run("Null and set timestamps survive a save", func(t *testing.T) { repo := newRepo(t) withNulls := domainPurge.Settings{AutoPurgeEnabled: true, AutoPurgeIntervalDays: 7} if err := repo.Save(withNulls); err != nil { t.Fatal(err) } got, err := repo.Get() if err != nil || !reflect.DeepEqual(got, withNulls) { t.Errorf("expected %+v, got %+v, %v", withNulls, got, err) } last := time.Date(2026, 9, 1, 9, 0, 0, 0, time.UTC) enabled := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) full := domainPurge.Settings{AutoPurgeEnabled: true, AutoPurgeIntervalDays: 365, LastPurgeAt: &last, LastPurgeMerged: 3, LastPurgeArticlesUpdated: 1, EnabledAt: &enabled} if err := repo.Save(full); err != nil { t.Fatal(err) } got, err = repo.Get() if err != nil || got.LastPurgeAt == nil || !got.LastPurgeAt.Equal(last) || got.EnabledAt == nil || !got.EnabledAt.Equal(enabled) || got.LastPurgeMerged != 3 || got.LastPurgeArticlesUpdated != 1 || got.AutoPurgeIntervalDays != 365 { t.Errorf("unexpected round trip %+v, %v", got, err) } }) t.Run("SaveLastRun writes only the last_purge_* columns", func(t *testing.T) { repo := newRepo(t) enabled := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) if err := repo.Save(domainPurge.Settings{AutoPurgeEnabled: true, AutoPurgeIntervalDays: 9, EnabledAt: &enabled}); err != nil { t.Fatal(err) } at := time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC) if err := repo.SaveLastRun(at, 4, 2); err != nil { t.Fatal(err) } got, err := repo.Get() if err != nil || !got.AutoPurgeEnabled || got.AutoPurgeIntervalDays != 9 || got.EnabledAt == nil || !got.EnabledAt.Equal(enabled) || got.LastPurgeAt == nil || !got.LastPurgeAt.Equal(at) || got.LastPurgeMerged != 4 || got.LastPurgeArticlesUpdated != 2 { t.Errorf("unexpected settings %+v, %v", got, err) } }) t.Run("SaveLastRun with no saved row keeps the defaults", func(t *testing.T) { repo := newRepo(t) at := time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC) if err := repo.SaveLastRun(at, 0, 0); err != nil { t.Fatal(err) } got, err := repo.Get() defaults := domainPurge.DefaultSettings() if err != nil || got.AutoPurgeEnabled != defaults.AutoPurgeEnabled || got.AutoPurgeIntervalDays != defaults.AutoPurgeIntervalDays || got.LastPurgeAt == nil || !got.LastPurgeAt.Equal(at) { t.Errorf("unexpected settings %+v, %v", got, err) } }) t.Run("Repeated saves keep a single row", func(t *testing.T) { db := openInMemoryDb(t) if err := db.AutoMigrate(&models.PurgeSettings{}); err != nil { t.Fatal(err) } repo := persistence.NewPurgeSettingsRepository(db) for days := 1; days <= 3; days++ { if err := repo.Save(domainPurge.Settings{AutoPurgeIntervalDays: days}); err != nil { t.Fatal(err) } } var count int64 db.Model(&models.PurgeSettings{}).Count(&count) if count != 1 { t.Errorf("expected a single row, got %d", count) } }) }