package test import ( "RenewCMS/internal/domain/purge" "RenewCMS/internal/infrastructure/useCases" "bytes" "crypto/sha256" "encoding/hex" "errors" "log" "reflect" "sync" "testing" "time" ) func TestUpdatePurgeSettingsRejectsOutOfRangeIntervals(t *testing.T) { for _, days := range []int{0, 366} { repo := newMockPurgeSettingsRepo() update := useCases.NewUpdatePurgeSettingsUseCase(repo, time.Now) _, err := update.Update(useCases.UpdatePurgeSettingsCommand{AutoPurgeIntervalDays: &days}) if !errors.Is(err, purge.ErrInvalidPurgeInterval) { t.Errorf("interval %d: expected ErrInvalidPurgeInterval, got %v", days, err) } if repo.saveCalls != 0 { t.Errorf("interval %d: expected Save not to be called", days) } } } func TestRunPurgeDistinguishesBusyFromConflict(t *testing.T) { newRun := func(env *mediaEnv) *useCases.RunPurgeUseCase { merge := useCases.NewMergeMediaUseCase(env.images, env.articles, env.storage, newMockVariantFileRepo(), discardRecorder) return useCases.NewRunPurgeUseCase(env.images, env.articles, merge, env.purgeSettings, func() time.Time { return env.clock }, log.New(&bytes.Buffer{}, "", 0), discardRecorder) } t.Run("a non-matching fingerprint is a conflict and writes nothing", func(t *testing.T) { env := newPurgeEnv(t) env.seedGroup(hashA, 1, 2) env.useImages(10, 1) wrong := "wrong" _, err := newRun(env).Run(testCtx, &wrong) if !errors.Is(err, purge.ErrPurgeConflict) || errors.Is(err, purge.ErrPurgeBusy) { t.Errorf("expected only ErrPurgeConflict, got %v", err) } if env.purgeSettings.saveCalls != 0 || len(env.images.deleteCalls) != 0 { t.Errorf("expected no write") } }) t.Run("a run while another holds the guard is busy and writes nothing", func(t *testing.T) { env := newPurgeEnv(t) env.seedGroup(hashA, 1, 2) env.useImages(10, 1) entered := make(chan struct{}) release := make(chan struct{}) var once sync.Once env.images.onDelete = func(uint32) { once.Do(func() { close(entered) <-release }) } run := newRun(env) done := make(chan error, 1) go func() { _, err := run.Run(testCtx, nil) done <- err }() <-entered savesBefore := env.purgeSettings.saveCalls _, err := run.Run(testCtx, nil) if !errors.Is(err, purge.ErrPurgeBusy) || errors.Is(err, purge.ErrPurgeConflict) { t.Errorf("expected only ErrPurgeBusy, got %v", err) } if env.purgeSettings.saveCalls != savesBefore { t.Errorf("expected no write from the busy call") } close(release) if err := <-done; err != nil { t.Errorf("expected the first run to finish, got %v", err) } }) t.Run("a scheduler run without a fingerprint gets the result and the persistence", func(t *testing.T) { env := newPurgeEnv(t) env.seedGroup(hashA, 1, 2) env.useImages(10, 1) result, err := newRun(env).Run(testCtx, nil) if err != nil || result != (purge.RunResult{GroupsTotal: 1, GroupsPurged: 1, Merged: 1}) { t.Errorf("unexpected result %+v, %v", result, err) } if env.purgeSettings.settings.LastPurgeAt == nil || env.purgeSettings.settings.LastPurgeMerged != 1 { t.Errorf("expected last_purge_* persisted") } }) } func TestPurgePlanOrderingAndFingerprint(t *testing.T) { t.Run("groups are ordered by the kept image id whatever the order they were found in", func(t *testing.T) { env := newPurgeEnv(t) env.seedGroup(hashA, 1, 6) env.seedGroup(hashB, 2, 5) env.seedGroup(hashC, 3, 4) env.useImages(10, 6) env.useImages(11, 5) env.useImages(12, 3) preview, err := useCases.NewPreviewPurgeUseCase(env.images, env.articles).Preview() var kept []uint32 for _, group := range preview.List { kept = append(kept, group.KeepID) } if err != nil || !reflect.DeepEqual(kept, []uint32{3, 5, 6}) { t.Errorf("expected kept ids [3 5 6], got %v, %v", kept, err) } }) t.Run("the fingerprint hashes the kept id and the comma separated merged ids of each group", func(t *testing.T) { env := newPurgeEnv(t) env.seedGroup(hashA, 1, 2, 3) env.seedGroup(hashB, 4, 5) env.useImages(10, 1, 2, 3) env.useImages(11, 5) preview, err := useCases.NewPreviewPurgeUseCase(env.images, env.articles).Preview() sum := sha256.Sum256([]byte("1:2,3;5:4;")) if err != nil || preview.Fingerprint != hex.EncodeToString(sum[:]) { t.Errorf("unexpected fingerprint %q, %v", preview.Fingerprint, err) } }) } func TestMergeMediaExposesTheRewrittenArticleIds(t *testing.T) { t.Run("Existing merges report the same counts and now also the ids", func(t *testing.T) { env := newPurgeEnv(t) env.seedGroup(hashA, 1, 2) env.useImages(1, 2) env.useImages(2, 1, 2) env.useImages(3, 1) merge := useCases.NewMergeMediaUseCase(env.images, env.articles, env.storage, newMockVariantFileRepo(), discardRecorder) result, err := merge.Merge(testCtx, 1, []uint32{2}) if err != nil || result.Merged != 1 || result.ArticlesUpdated != 2 || !reflect.DeepEqual(result.UpdatedArticleIDs, []uint32{1, 2}) { t.Errorf("unexpected result %+v, %v", result, err) } }) t.Run("A rolled-back merge returns no ids", func(t *testing.T) { env := newPurgeEnv(t) env.seedGroup(hashA, 1, 2) env.useImages(1, 2) env.useImages(2, 2) env.articles.failUpdateBodyFor = map[uint32]error{2: errRepositoryFailure} merge := useCases.NewMergeMediaUseCase(env.images, env.articles, env.storage, newMockVariantFileRepo(), discardRecorder) result, err := merge.Merge(testCtx, 1, []uint32{2}) if err == nil || result.Merged != 0 || result.ArticlesUpdated != 0 || len(result.UpdatedArticleIDs) != 0 { t.Errorf("unexpected result %+v, %v", result, err) } }) } func TestPurgeSettingsNextPurgeAt(t *testing.T) { t.Run("is null when off or when never enabled", func(t *testing.T) { if purge.DefaultSettings().NextPurgeAt() != nil { t.Errorf("expected nil") } if (purge.Settings{AutoPurgeEnabled: true, AutoPurgeIntervalDays: 3}).NextPurgeAt() != nil { t.Errorf("expected nil without enabled_at") } }) }