624 lines
20 KiB
Go
624 lines
20 KiB
Go
package test
|
|
|
|
import (
|
|
"RenewCMS/internal/domain/audit"
|
|
persistence "RenewCMS/internal/infrastructure/persistence/repositories"
|
|
"RenewCMS/internal/infrastructure/useCases"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"reflect"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const sizeTestGB = audit.BytesPerGB
|
|
|
|
type sizedEvent struct {
|
|
id uint64
|
|
at time.Time
|
|
bytes int64
|
|
}
|
|
|
|
type sizedEventsFake struct {
|
|
events []sizedEvent
|
|
ageCalls int
|
|
oldestCalls int
|
|
sizeCalls int
|
|
sizeErr error
|
|
oldestErr error
|
|
}
|
|
|
|
var _ audit.Repository = &sizedEventsFake{}
|
|
|
|
func (f *sizedEventsFake) add(at time.Time, bytes int64) {
|
|
f.events = append(f.events, sizedEvent{id: uint64(len(f.events) + 1), at: at, bytes: bytes})
|
|
}
|
|
|
|
func (f *sizedEventsFake) Append(event audit.Event) (audit.Event, error) { return event, nil }
|
|
|
|
func (f *sizedEventsFake) List(audit.Filter) (audit.Page, error) { return audit.Page{}, nil }
|
|
|
|
func (f *sizedEventsFake) Summary(audit.SummaryQuery) (audit.Summary, error) {
|
|
return audit.Summary{}, nil
|
|
}
|
|
|
|
func (f *sizedEventsFake) DeleteOlderThan(cutoff time.Time) (int, error) {
|
|
f.ageCalls++
|
|
kept := f.events[:0:0]
|
|
removed := 0
|
|
for _, event := range f.events {
|
|
if event.at.Before(cutoff) {
|
|
removed++
|
|
continue
|
|
}
|
|
kept = append(kept, event)
|
|
}
|
|
f.events = kept
|
|
return removed, nil
|
|
}
|
|
|
|
func (f *sizedEventsFake) DeleteOldest(limit int, before time.Time) (int, error) {
|
|
f.oldestCalls++
|
|
if f.oldestErr != nil {
|
|
return 0, f.oldestErr
|
|
}
|
|
sort.SliceStable(f.events, func(i, j int) bool {
|
|
if f.events[i].at.Equal(f.events[j].at) {
|
|
return f.events[i].id < f.events[j].id
|
|
}
|
|
return f.events[i].at.Before(f.events[j].at)
|
|
})
|
|
removed := 0
|
|
for removed < limit && removed < len(f.events) && f.events[removed].at.Before(before) {
|
|
removed++
|
|
}
|
|
f.events = f.events[removed:]
|
|
return removed, nil
|
|
}
|
|
|
|
func (f *sizedEventsFake) SizeBytes() (int64, error) {
|
|
f.sizeCalls++
|
|
if f.sizeErr != nil {
|
|
return 0, f.sizeErr
|
|
}
|
|
var total int64
|
|
for _, event := range f.events {
|
|
total += event.bytes
|
|
}
|
|
return total, nil
|
|
}
|
|
|
|
type recordingRecorder struct {
|
|
events []audit.Event
|
|
}
|
|
|
|
func (r *recordingRecorder) Record(_ context.Context, event audit.Event) {
|
|
r.events = append(r.events, event)
|
|
}
|
|
|
|
type sizeCleanupEnv struct {
|
|
events *sizedEventsFake
|
|
settings *activitySettingsFake
|
|
recorder *recordingRecorder
|
|
cleanup *useCases.CleanupActivityUseCase
|
|
now time.Time
|
|
}
|
|
|
|
func newSizeCleanupEnv(days int, maxGB int) *sizeCleanupEnv {
|
|
e := &sizeCleanupEnv{
|
|
events: &sizedEventsFake{},
|
|
settings: newActivitySettingsFake(),
|
|
recorder: &recordingRecorder{},
|
|
now: cleanupAt(6, 3, 0),
|
|
}
|
|
e.settings.settings.RetentionDays = days
|
|
e.settings.settings.RetentionMaxGB = maxGB
|
|
e.cleanup = useCases.NewCleanupActivityUseCase(e.events, e.settings, e.recorder).WithBatchSize(1)
|
|
return e
|
|
}
|
|
|
|
func (e *sizeCleanupEnv) daysAgo(days int) time.Time {
|
|
return e.now.Add(-time.Duration(days) * 24 * time.Hour)
|
|
}
|
|
|
|
func TestActivityRetentionSizeDomain(t *testing.T) {
|
|
t.Run("The size options are 0, 1, 5, 10, 25 and 50", func(t *testing.T) {
|
|
if !reflect.DeepEqual(audit.SizeOptions(), []int{0, 1, 5, 10, 25, 50}) {
|
|
t.Errorf("unexpected options %v", audit.SizeOptions())
|
|
}
|
|
for _, gb := range []int{0, 1, 5, 10, 25, 50} {
|
|
if err := audit.ValidateRetentionMaxGB(gb); err != nil {
|
|
t.Errorf("%d should be valid: %v", gb, err)
|
|
}
|
|
}
|
|
for _, gb := range []int{-1, 2, 3, 100, 51} {
|
|
if err := audit.ValidateRetentionMaxGB(gb); !errors.Is(err, audit.ErrInvalidRetention) {
|
|
t.Errorf("%d should be invalid, got %v", gb, err)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("A fresh install has no size limit", func(t *testing.T) {
|
|
if audit.DefaultSettings().RetentionMaxGB != 0 || audit.BytesPerGB != 1024*1024*1024 {
|
|
t.Errorf("unexpected defaults %+v", audit.DefaultSettings())
|
|
}
|
|
})
|
|
|
|
t.Run("The next cleanup is empty only when both limits are 0", func(t *testing.T) {
|
|
now := time.Date(2026, 10, 6, 10, 0, 0, 0, settingsTestZone)
|
|
cases := []struct {
|
|
days, gb int
|
|
scheduled bool
|
|
}{{90, 0, true}, {90, 5, true}, {0, 5, true}, {0, 0, false}}
|
|
for _, c := range cases {
|
|
next := audit.Settings{RetentionDays: c.days, RetentionMaxGB: c.gb}.WithNextCleanup(now).NextCleanupAt
|
|
if (next != nil) != c.scheduled {
|
|
t.Errorf("days %d gb %d: unexpected %v", c.days, c.gb, next)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("The limit in bytes is gigabytes times 1024 cubed", func(t *testing.T) {
|
|
if got := (audit.Settings{RetentionMaxGB: 5}).MaxLogSizeBytes(); got != 5368709120 {
|
|
t.Errorf("unexpected %d", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestActivityRetentionSizeCleanup(t *testing.T) {
|
|
t.Run("The size limit removes the oldest events", func(t *testing.T) {
|
|
for _, days := range []int{90, 0} {
|
|
e := newSizeCleanupEnv(days, 5)
|
|
for age := 20; age > 13; age-- {
|
|
e.events.add(e.daysAgo(age), sizeTestGB)
|
|
}
|
|
|
|
result, ran, err := e.cleanup.Tick(e.now)
|
|
|
|
size, _ := e.events.SizeBytes()
|
|
if err != nil || !ran || result.Removed != 2 || size != 5*sizeTestGB {
|
|
t.Fatalf("days %d: unexpected %+v ran=%v size=%d err=%v", days, result, ran, size, err)
|
|
}
|
|
if e.events.events[0].id != 3 {
|
|
t.Errorf("days %d: the oldest events must go first, first left is %d", days, e.events.events[0].id)
|
|
}
|
|
last := e.settings.settings
|
|
if last.LastCleanupAt == nil || !last.LastCleanupAt.Equal(e.now) || last.LastCleanupRemoved != 2 {
|
|
t.Errorf("days %d: unexpected last cleanup %+v", days, last)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("Age first then size, and one event with the total", func(t *testing.T) {
|
|
e := newSizeCleanupEnv(30, 5)
|
|
for i := 0; i < 10; i++ {
|
|
e.events.add(e.daysAgo(40), 10)
|
|
}
|
|
for age := 20; age > 13; age-- {
|
|
e.events.add(e.daysAgo(age), sizeTestGB)
|
|
}
|
|
|
|
result, _, err := e.cleanup.Tick(e.now)
|
|
|
|
if err != nil || result.Removed != 12 || e.settings.settings.LastCleanupRemoved != 12 {
|
|
t.Fatalf("expected 12 removed, got %+v, %v", result, err)
|
|
}
|
|
if len(e.recorder.events) != 1 || e.recorder.events[0].Action != "system.activity_cleanup" || e.recorder.events[0].Params[audit.ParamRemoved] != 12 {
|
|
t.Errorf("unexpected events %+v", e.recorder.events)
|
|
}
|
|
})
|
|
|
|
t.Run("The age limit alone is enough", func(t *testing.T) {
|
|
e := newSizeCleanupEnv(30, 5)
|
|
e.events.add(e.daysAgo(40), 10*sizeTestGB)
|
|
e.events.add(e.daysAgo(2), sizeTestGB)
|
|
|
|
result, _, err := e.cleanup.Tick(e.now)
|
|
|
|
if err != nil || result.Removed != 1 || e.events.oldestCalls != 0 {
|
|
t.Errorf("unexpected %+v, %d size removals, %v", result, e.events.oldestCalls, err)
|
|
}
|
|
})
|
|
|
|
t.Run("The size is already met", func(t *testing.T) {
|
|
e := newSizeCleanupEnv(0, 5)
|
|
e.events.add(e.daysAgo(10), sizeTestGB)
|
|
|
|
result, ran, err := e.cleanup.Tick(e.now)
|
|
|
|
if err != nil || !ran || result.Removed != 0 || e.events.oldestCalls != 0 || len(e.recorder.events) != 0 {
|
|
t.Errorf("unexpected %+v ran=%v %v", result, ran, err)
|
|
}
|
|
})
|
|
|
|
t.Run("Forever with no size limit never runs", func(t *testing.T) {
|
|
e := newSizeCleanupEnv(0, 0)
|
|
e.events.add(e.daysAgo(10), 100*sizeTestGB)
|
|
|
|
_, ran, err := e.cleanup.Tick(e.now)
|
|
|
|
if err != nil || ran || e.events.ageCalls != 0 || e.events.oldestCalls != 0 || e.events.sizeCalls != 0 {
|
|
t.Errorf("expected nothing, got ran=%v %v", ran, err)
|
|
}
|
|
})
|
|
|
|
t.Run("Forever with a size limit runs and does no age removal", func(t *testing.T) {
|
|
e := newSizeCleanupEnv(0, 1)
|
|
e.events.add(e.daysAgo(400), 2*sizeTestGB)
|
|
|
|
_, ran, err := e.cleanup.Tick(e.now)
|
|
|
|
if err != nil || !ran || e.events.ageCalls != 0 || len(e.events.events) != 0 {
|
|
t.Errorf("unexpected ran=%v %v", ran, err)
|
|
}
|
|
})
|
|
|
|
t.Run("The last 24 hours are protected even when the size cannot be met", func(t *testing.T) {
|
|
e := newSizeCleanupEnv(0, 1)
|
|
for i := 0; i < 4; i++ {
|
|
e.events.add(e.now.Add(-time.Duration(i+1)*time.Hour), sizeTestGB)
|
|
}
|
|
|
|
result, ran, err := e.cleanup.Tick(e.now)
|
|
|
|
if err != nil || !ran || result.Removed != 0 || len(e.events.events) != 4 {
|
|
t.Fatalf("unexpected %+v ran=%v %v", result, ran, err)
|
|
}
|
|
if e.settings.settings.LastCleanupAt == nil || e.settings.settings.LastCleanupRemoved != 0 || len(e.recorder.events) != 0 {
|
|
t.Errorf("the run must be recorded with 0 removed, got %+v", e.settings.settings)
|
|
}
|
|
})
|
|
|
|
t.Run("A failing size removal is retried", func(t *testing.T) {
|
|
e := newSizeCleanupEnv(0, 1)
|
|
e.events.add(e.daysAgo(10), 4*sizeTestGB)
|
|
e.events.oldestErr = errRepositoryFailure
|
|
|
|
_, ran, err := e.cleanup.Tick(e.now)
|
|
|
|
if !errors.Is(err, errRepositoryFailure) || ran || e.settings.settings.LastCleanupAt != nil {
|
|
t.Fatalf("unexpected ran=%v %v", ran, err)
|
|
}
|
|
e.events.oldestErr = nil
|
|
if _, ran, err = e.cleanup.Tick(e.now.Add(time.Hour)); err != nil || !ran {
|
|
t.Errorf("expected the next tick to run, got ran=%v %v", ran, err)
|
|
}
|
|
})
|
|
|
|
t.Run("A failing size measurement is returned", func(t *testing.T) {
|
|
e := newSizeCleanupEnv(0, 1)
|
|
e.events.sizeErr = errRepositoryFailure
|
|
|
|
_, ran, err := e.cleanup.Tick(e.now)
|
|
|
|
if !errors.Is(err, errRepositoryFailure) || ran {
|
|
t.Errorf("unexpected ran=%v %v", ran, err)
|
|
}
|
|
})
|
|
|
|
t.Run("The batch loop ends when a batch removes nothing", func(t *testing.T) {
|
|
e := newSizeCleanupEnv(0, 1)
|
|
e.events.add(e.daysAgo(10), 3*sizeTestGB)
|
|
e.events.add(e.now.Add(-time.Hour), 3*sizeTestGB)
|
|
|
|
_, _, err := e.cleanup.Tick(e.now)
|
|
|
|
if err != nil || e.events.oldestCalls != 2 || len(e.events.events) != 1 {
|
|
t.Errorf("unexpected %d calls, %d left, %v", e.events.oldestCalls, len(e.events.events), err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestActivityRetentionSizeUseCases(t *testing.T) {
|
|
newEnv := func() (*activitySettingsFake, *sizedEventsFake, *recordingRecorder, *useCases.UpdateActivityRetentionUseCase) {
|
|
settings := newActivitySettingsFake()
|
|
events := &sizedEventsFake{}
|
|
recorder := &recordingRecorder{}
|
|
return settings, events, recorder, useCases.NewUpdateActivityRetentionUseCase(settings, events, recorder, settingsTestClock)
|
|
}
|
|
|
|
t.Run("Get carries the measured size", func(t *testing.T) {
|
|
settings := newActivitySettingsFake()
|
|
settings.settings.RetentionMaxGB = 5
|
|
events := &sizedEventsFake{}
|
|
events.add(settingsTestClock(), 3435973837)
|
|
|
|
current, err := useCases.NewGetActivityRetentionUseCase(settings, events, settingsTestClock).GetActivityRetention()
|
|
|
|
if err != nil || current.LogSizeBytes != 3435973837 || current.RetentionMaxGB != 5 {
|
|
t.Errorf("unexpected %+v, %v", current, err)
|
|
}
|
|
})
|
|
|
|
t.Run("A size failure propagates from Get and Update", func(t *testing.T) {
|
|
settings, events, _, update := newEnv()
|
|
events.sizeErr = errRepositoryFailure
|
|
|
|
_, getErr := useCases.NewGetActivityRetentionUseCase(settings, events, settingsTestClock).GetActivityRetention()
|
|
_, updateErr := update.UpdateActivityRetention(testCtx, retentionMaxGB(5))
|
|
|
|
if !errors.Is(getErr, errRepositoryFailure) || !errors.Is(updateErr, errRepositoryFailure) || settings.retentionOK != 0 {
|
|
t.Errorf("unexpected %v, %v", getErr, updateErr)
|
|
}
|
|
})
|
|
|
|
t.Run("An empty command or an invalid size is refused and saves nothing", func(t *testing.T) {
|
|
settings, _, recorder, update := newEnv()
|
|
two := 2
|
|
for _, command := range []useCases.UpdateActivityRetentionCommand{{}, {MaxGB: &two}, {Days: activityPtr(30), MaxGB: &two}} {
|
|
if _, err := update.UpdateActivityRetention(testCtx, command); !errors.Is(err, audit.ErrInvalidRetention) {
|
|
t.Errorf("expected a refusal, got %v", err)
|
|
}
|
|
}
|
|
if settings.retentionOK != 0 || len(recorder.events) != 0 {
|
|
t.Error("something was saved")
|
|
}
|
|
})
|
|
|
|
t.Run("Each key leaves the other unchanged", func(t *testing.T) {
|
|
settings, _, _, update := newEnv()
|
|
|
|
sized, err := update.UpdateActivityRetention(testCtx, retentionMaxGB(5))
|
|
if err != nil || sized.RetentionDays != 90 || sized.RetentionMaxGB != 5 {
|
|
t.Fatalf("unexpected %+v, %v", sized, err)
|
|
}
|
|
aged, err := update.UpdateActivityRetention(testCtx, retentionDays(30))
|
|
if err != nil || aged.RetentionDays != 30 || aged.RetentionMaxGB != 5 || settings.settings.RetentionMaxGB != 5 {
|
|
t.Fatalf("unexpected %+v, %v", aged, err)
|
|
}
|
|
})
|
|
|
|
t.Run("The audit params follow what changed", func(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
command useCases.UpdateActivityRetentionCommand
|
|
params map[string]any
|
|
}{
|
|
{"age", retentionDays(30), map[string]any{"from": 90, "to": 30}},
|
|
{"size", retentionMaxGB(5), map[string]any{"max_gb_from": 0, "max_gb_to": 5}},
|
|
{"both", useCases.UpdateActivityRetentionCommand{Days: activityPtr(30), MaxGB: activityPtr(5)}, map[string]any{"from": 90, "to": 30, "max_gb_from": 0, "max_gb_to": 5}},
|
|
}
|
|
for _, c := range cases {
|
|
_, _, recorder, update := newEnv()
|
|
if _, err := update.UpdateActivityRetention(testCtx, c.command); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(recorder.events) != 1 || recorder.events[0].Action != "system.activity_retention_changed" || !reflect.DeepEqual(recorder.events[0].Params, c.params) {
|
|
t.Errorf("%s: unexpected events %+v", c.name, recorder.events)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("Saving the same pair is a no-op", func(t *testing.T) {
|
|
settings, _, recorder, update := newEnv()
|
|
settings.settings.RetentionMaxGB = 5
|
|
|
|
current, err := update.UpdateActivityRetention(testCtx, useCases.UpdateActivityRetentionCommand{Days: activityPtr(90), MaxGB: activityPtr(5)})
|
|
|
|
if err != nil || current.RetentionMaxGB != 5 || settings.retentionOK != 0 || len(recorder.events) != 0 {
|
|
t.Errorf("unexpected %+v, %v", current, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestActivityRetentionSizePersistence(t *testing.T) {
|
|
text := func(event audit.Event) int64 {
|
|
var total int64
|
|
for _, field := range []string{event.Action, event.Actor.Name, event.Target.Type, event.Target.ID, event.Target.Name} {
|
|
total += int64(len([]byte(field)))
|
|
}
|
|
if event.FailureReason != nil {
|
|
total += int64(len(*event.FailureReason))
|
|
}
|
|
if event.IP != nil {
|
|
total += int64(len(*event.IP))
|
|
}
|
|
if event.UserAgent != nil {
|
|
total += int64(len(*event.UserAgent))
|
|
}
|
|
return total
|
|
}
|
|
|
|
t.Run("SizeBytes is 0 for an empty table", func(t *testing.T) {
|
|
repo := persistence.NewAuditEventRepository(openActivityDatabase(t))
|
|
|
|
size, err := repo.SizeBytes()
|
|
|
|
if err != nil || size != 0 {
|
|
t.Errorf("unexpected %d, %v", size, err)
|
|
}
|
|
})
|
|
|
|
t.Run("SizeBytes sums the byte lengths of the text columns plus the row overhead", func(t *testing.T) {
|
|
repo := persistence.NewAuditEventRepository(openActivityDatabase(t))
|
|
plain := activityUserEvent(1, "marta", activityDay(1, 10))
|
|
plain.Params = map[string]any{"a": 1}
|
|
accented := activityUserEvent(2, "Zoë Çelik 日本", activityDay(2, 10))
|
|
accented.IP = activityPtr("203.0.113.12")
|
|
accented.UserAgent = activityPtr("Mozilla/5.0 é")
|
|
accented.FailureReason = activityPtr("Wrong username or password.")
|
|
accented.Changes = []audit.Change{{Field: "title", Kind: audit.ChangeText, Before: activityPtr("é"), After: activityPtr("e")}}
|
|
var wanted int64
|
|
for _, event := range []audit.Event{plain, accented} {
|
|
stored := activityMustAppend(t, repo, event)
|
|
wanted += text(stored) + int64(len(mustMarshalParams(t, stored))) + int64(len(mustMarshalChanges(t, stored))) + audit.RowOverheadBytes
|
|
}
|
|
|
|
size, err := repo.SizeBytes()
|
|
|
|
if err != nil || size != wanted {
|
|
t.Errorf("expected %d, got %d, %v", wanted, size, err)
|
|
}
|
|
})
|
|
|
|
t.Run("Deleting events lowers the size", func(t *testing.T) {
|
|
repo := persistence.NewAuditEventRepository(openActivityDatabase(t))
|
|
activityMustAppend(t, repo, activityUserEvent(1, "a", activityDay(1, 10)))
|
|
activityMustAppend(t, repo, activityUserEvent(1, "a", activityDay(20, 10)))
|
|
before, _ := repo.SizeBytes()
|
|
|
|
if _, err := repo.DeleteOlderThan(activityDay(10, 10)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
after, err := repo.SizeBytes()
|
|
if err != nil || after >= before || after <= 0 {
|
|
t.Errorf("expected a lower size than %d, got %d, %v", before, after, err)
|
|
}
|
|
})
|
|
|
|
t.Run("DeleteOldest removes the oldest events up to a limit and a guard", func(t *testing.T) {
|
|
repo := persistence.NewAuditEventRepository(openActivityDatabase(t))
|
|
now := activityDay(20, 12)
|
|
var ids []uint64
|
|
for i := 0; i < 7; i++ {
|
|
ids = append(ids, activityMustAppend(t, repo, activityUserEvent(1, "a", now.Add(-time.Duration(10-i)*24*time.Hour))).ID)
|
|
}
|
|
for i := 0; i < 3; i++ {
|
|
ids = append(ids, activityMustAppend(t, repo, activityUserEvent(1, "a", now.Add(-time.Duration(i+1)*time.Hour))).ID)
|
|
}
|
|
guard := audit.GuardCutoff(now)
|
|
|
|
removed, err := repo.DeleteOldest(5, guard)
|
|
if err != nil || removed != 5 {
|
|
t.Fatalf("expected 5 removed, got %d, %v", removed, err)
|
|
}
|
|
page, _ := repo.List(audit.Filter{Page: 1, Size: 100})
|
|
left := activityIDs(page.Items)
|
|
sort.Slice(left, func(i, j int) bool { return left[i] < left[j] })
|
|
if !reflect.DeepEqual(left, ids[5:]) {
|
|
t.Errorf("expected %v left, got %v", ids[5:], left)
|
|
}
|
|
|
|
removed, err = repo.DeleteOldest(20, guard)
|
|
page, _ = repo.List(audit.Filter{Page: 1, Size: 100})
|
|
if err != nil || removed != 2 || page.Total != 3 {
|
|
t.Errorf("expected only the 2 older events removed, got %d with %d left, %v", removed, page.Total, err)
|
|
}
|
|
if removed, err = repo.DeleteOldest(0, guard); err != nil || removed != 0 {
|
|
t.Errorf("a zero limit removes nothing, got %d, %v", removed, err)
|
|
}
|
|
if removed, err = repo.DeleteOldest(-1, guard); err != nil || removed != 0 {
|
|
t.Errorf("a negative limit removes nothing, got %d, %v", removed, err)
|
|
}
|
|
})
|
|
|
|
t.Run("DeleteOldest honours a limit of 1", func(t *testing.T) {
|
|
repo := persistence.NewAuditEventRepository(openActivityDatabase(t))
|
|
now := activityDay(20, 12)
|
|
for i := 0; i < 3; i++ {
|
|
activityMustAppend(t, repo, activityUserEvent(1, "a", now.Add(-time.Duration(10-i)*24*time.Hour)))
|
|
}
|
|
|
|
removed, err := repo.DeleteOldest(1, audit.GuardCutoff(now))
|
|
|
|
page, _ := repo.List(audit.Filter{Page: 1, Size: 100})
|
|
if err != nil || removed != 1 || page.Total != 2 {
|
|
t.Errorf("expected 1 removed and 2 left, got %d and %d, %v", removed, page.Total, err)
|
|
}
|
|
})
|
|
|
|
t.Run("DeleteOldest removes more events than one delete chunk", func(t *testing.T) {
|
|
repo := persistence.NewAuditEventRepository(openActivityDatabase(t))
|
|
now := activityDay(20, 12)
|
|
for i := 0; i < 1001; i++ {
|
|
activityMustAppend(t, repo, activityUserEvent(1, "a", now.Add(-time.Duration(2000-i)*time.Hour)))
|
|
}
|
|
|
|
removed, err := repo.DeleteOldest(1001, audit.GuardCutoff(now))
|
|
|
|
page, _ := repo.List(audit.Filter{Page: 1, Size: 10})
|
|
if err != nil || removed != 1001 || page.Total != 0 {
|
|
t.Errorf("expected 1001 removed, got %d with %d left, %v", removed, page.Total, err)
|
|
}
|
|
})
|
|
|
|
t.Run("The size limit column defaults to 0 and is stored", func(t *testing.T) {
|
|
repo := persistence.NewAuditSettingsRepository(openActivityDatabase(t))
|
|
settings, err := repo.Get()
|
|
if err != nil || settings.RetentionMaxGB != 0 {
|
|
t.Fatalf("unexpected %+v, %v", settings, err)
|
|
}
|
|
|
|
if err := repo.SaveRetention(0, 5); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings, _ = repo.Get()
|
|
if settings.RetentionDays != 0 || settings.RetentionMaxGB != 5 {
|
|
t.Errorf("unexpected %+v", settings)
|
|
}
|
|
if err := repo.SaveLastCleanup(activityDay(6, 3), 1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings, _ = repo.Get()
|
|
if settings.RetentionMaxGB != 5 {
|
|
t.Errorf("the size limit must survive a cleanup record, got %+v", settings)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestActivityRetentionSizePersistenceErrors(t *testing.T) {
|
|
closedDatabase := func(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
db := openActivityDatabase(t)
|
|
sqlDb, err := db.DB()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := sqlDb.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return db
|
|
}
|
|
|
|
t.Run("SizeBytes returns the database error", func(t *testing.T) {
|
|
size, err := persistence.NewAuditEventRepository(closedDatabase(t)).SizeBytes()
|
|
|
|
if err == nil || size != 0 {
|
|
t.Errorf("expected an error and 0, got %d, %v", size, err)
|
|
}
|
|
})
|
|
|
|
t.Run("DeleteOldest returns the database error", func(t *testing.T) {
|
|
removed, err := persistence.NewAuditEventRepository(closedDatabase(t)).DeleteOldest(5, activityDay(20, 12))
|
|
|
|
if err == nil || removed != 0 {
|
|
t.Errorf("expected an error and 0, got %d, %v", removed, err)
|
|
}
|
|
})
|
|
|
|
t.Run("SaveRetention returns the database error", func(t *testing.T) {
|
|
if err := persistence.NewAuditSettingsRepository(closedDatabase(t)).SaveRetention(30, 5); err == nil {
|
|
t.Error("expected an error")
|
|
}
|
|
})
|
|
|
|
t.Run("Get returns the database error", func(t *testing.T) {
|
|
if _, err := persistence.NewAuditSettingsRepository(closedDatabase(t)).Get(); err == nil {
|
|
t.Error("expected an error")
|
|
}
|
|
})
|
|
}
|
|
|
|
func mustMarshalParams(t *testing.T, event audit.Event) []byte {
|
|
t.Helper()
|
|
encoded, err := json.Marshal(event.Params)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return encoded
|
|
}
|
|
|
|
func mustMarshalChanges(t *testing.T, event audit.Event) []byte {
|
|
t.Helper()
|
|
encoded, err := json.Marshal(event.Changes)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return encoded
|
|
}
|