Files
Felis/internal/pgint/scheduled_test.go
T

146 lines
5.8 KiB
Go

//go:build pgint
package pgint
import (
"context"
"strings"
"testing"
"time"
"felis.lolicon.best/internal/reaper"
)
// TestScheduledBackupCandidates pins the SQL behind felis-api's scheduled
// backups: an owned world joined since its owner's newest intact scheduled
// backup is due once that backup is older than the period, worlds without one
// first. A previous owner's, a corrupt, a deleted, a pre-claim or a manual
// backup is no restore point of the current owner's world. A world being given
// up is never due: the reaper archives it anyway.
func TestScheduledBackupCandidates(t *testing.T) {
ctx := context.Background()
u := newUser(t, "user", "sched")
prev := newUser(t, "user", "sched-prev")
now := time.Now().UTC()
sfx := suffix(t)
server := func(tag, owner string, claimed, active time.Time, deleted bool) string {
t.Helper()
name := "sch-" + tag + "-" + sfx
var deletedAt any
if deleted {
deletedAt = now
}
if _, err := db.ExecContext(ctx,
`INSERT INTO servers (name, owner_id, claimed_at, last_active_at, deleted_at, cached_cpu_milli, cached_memory_mb, cached_storage_mb)
VALUES ($1, NULLIF($2, ''), $3, $4, $5, 100, 128, 1)`,
name, owner, claimed, active, deletedAt); err != nil {
t.Fatalf("seed server %s: %v", name, err)
}
return name
}
n := 0
backup := func(server, owner, reason, status string, created time.Time, corrupt bool) {
t.Helper()
n++
id := "bk-sch-" + sfx + "-" + string(rune('a'+n))
var corruptAt any
if corrupt {
corruptAt = created
}
if _, err := db.ExecContext(ctx,
`INSERT INTO world_backups (id, server_name, former_owner, backup_ref, size_bytes, reason, status, created_at, expires_at, corrupt_at)
VALUES ($1, $2, NULLIF($3, ''), $4, 1, $5, $6, $7, $8, $9)`,
id, server, owner, "/archives/"+id+".tar.gz", reason, status, created, created.Add(90*reaper.Day), corruptAt); err != nil {
t.Fatalf("seed backup of %s: %v", server, err)
}
}
h := time.Hour
claimed := now.Add(-10 * reaper.Day)
fresh := server("fresh", u.ID, claimed, now.Add(-h), false)
stale := server("stale", u.ID, claimed, now.Add(-h), false)
backup(stale, u.ID, "scheduled", "present", now.Add(-30*h), false)
recent := server("recent", u.ID, claimed, now.Add(-h), false)
backup(recent, u.ID, "scheduled", "present", now.Add(-2*h), false)
idle := server("idle", u.ID, claimed, now.Add(-40*h), false)
backup(idle, u.ID, "scheduled", "present", now.Add(-30*h), false)
prevOwner := server("prevowner", u.ID, claimed, now.Add(-h), false)
backup(prevOwner, prev.ID, "scheduled", "present", now.Add(-2*h), false)
preClaim := server("preclaim", u.ID, now.Add(-2*h), now.Add(-2*h), false)
backup(preClaim, u.ID, "scheduled", "present", now.Add(-3*h), false)
corrupt := server("corrupt", u.ID, claimed, now.Add(-h), false)
backup(corrupt, u.ID, "scheduled", "present", now.Add(-30*time.Minute), true)
deletedBk := server("deletedbk", u.ID, claimed, now.Add(-h), false)
backup(deletedBk, u.ID, "scheduled", "deleted", now.Add(-30*time.Minute), false)
manual := server("manual", u.ID, claimed, now.Add(-h), false)
backup(manual, u.ID, "manual", "present", now.Add(-30*time.Minute), false)
server("unowned", "", claimed, now.Add(-h), false)
server("gone", u.ID, claimed, now.Add(-h), true)
retiring := server("retiring", u.ID, claimed, now.Add(-h), false)
if _, err := repo.RequestRetire(ctx, retiring, false); err != nil {
t.Fatalf("RequestRetire: %v", err)
}
got, err := repo.ScheduledBackupCandidates(ctx, now.Add(-24*h))
if err != nil {
t.Fatalf("ScheduledBackupCandidates: %v", err)
}
var mine []string
for _, c := range got {
if strings.HasSuffix(c.Name, sfx) {
if c.OwnerID != u.ID {
t.Fatalf("candidate %+v; want owner %s", c, u.ID)
}
mine = append(mine, c.Name)
}
}
want := []string{corrupt, deletedBk, fresh, manual, preClaim, prevOwner, stale}
if strings.Join(mine, " ") != strings.Join(want, " ") {
t.Fatalf("due = %v\nwant %v (worlds without a point by name, then the longest waiting)", mine, want)
}
}
// TestExcessBackupsPerOwner pins that the backup Job's keep-N prune counts only
// the new backup's owner's backups: the ones a previous owner took of the same
// world stay theirs to restore until their own retention runs out.
func TestExcessBackupsPerOwner(t *testing.T) {
ctx := context.Background()
st := reaper.NewPGStore(db)
u := newUser(t, "user", "keep")
prev := newUser(t, "user", "keep-prev")
sfx := suffix(t)
name := "keep-" + sfx
if _, err := db.ExecContext(ctx,
`INSERT INTO servers (name, owner_id, cached_cpu_milli, cached_memory_mb, cached_storage_mb) VALUES ($1, $2, 100, 128, 1)`,
name, u.ID); err != nil {
t.Fatalf("seed server: %v", err)
}
now := time.Now().UTC()
var ids []string
for i, owner := range []string{prev.ID, prev.ID, prev.ID, u.ID, u.ID} {
id := "bk-keep-" + sfx + "-" + string(rune('0'+i))
ids = append(ids, id)
created := now.Add(time.Duration(i-5) * time.Hour)
if _, err := db.ExecContext(ctx,
`INSERT INTO world_backups (id, server_name, former_owner, backup_ref, size_bytes, reason, status, created_at, expires_at)
VALUES ($1, $2, $3, $4, 1, 'scheduled', 'present', $5, $6)`,
id, name, owner, "/archives/"+id+".tar.gz", created, created.Add(90*reaper.Day)); err != nil {
t.Fatalf("seed %s: %v", id, err)
}
}
excess, err := st.ExcessBackups(ctx, name, u.ID, "scheduled", 1, "")
if err != nil {
t.Fatalf("ExcessBackups: %v", err)
}
if len(excess) != 1 || excess[0].ID != ids[3] {
t.Fatalf("excess for the owner = %+v; want only %s", excess, ids[3])
}
excess, err = st.ExcessBackups(ctx, name, prev.ID, "scheduled", 1, "")
if err != nil {
t.Fatalf("ExcessBackups(previous owner): %v", err)
}
if len(excess) != 2 || excess[0].ID != ids[0] || excess[1].ID != ids[1] {
t.Fatalf("excess for the previous owner = %+v; want %s, %s", excess, ids[0], ids[1])
}
}