feat: media duplicate purge, part 1 of 3: persisted purge settings and the purge API
This commit is contained in:
1 parent
161ddc0f7d
commit
e82d0ce254
25 files changed
+2459
-41
No files matched your search
@@ -0,0 +1,185 @@
|
||||
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)
|
||||
return useCases.NewRunPurgeUseCase(env.images, env.articles, merge, env.purgeSettings, func() time.Time { return env.clock }, log.New(&bytes.Buffer{}, "", 0))
|
||||
}
|
||||
|
||||
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(&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(nil)
|
||||
done <- err
|
||||
}()
|
||||
<-entered
|
||||
savesBefore := env.purgeSettings.saveCalls
|
||||
|
||||
_, err := run.Run(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(nil)
|
||||
|
||||
if err != nil || result != (useCases.RunPurgeResult{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)
|
||||
|
||||
result, err := merge.Merge(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)
|
||||
|
||||
result, err := merge.Merge(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")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user