Archive-based world backup and restore, plus the reaper that enforces retention and reclaims idle servers.
85 lines
2.7 KiB
Go
85 lines
2.7 KiB
Go
package restore_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"felis.lolicon.best/internal/restore"
|
|
)
|
|
|
|
// fakeJobs is an in-memory Jobs that records the params it was handed and
|
|
// returns a programmable error, so the orchestration is tested without a
|
|
// cluster.
|
|
type fakeJobs struct {
|
|
calls []restore.JobParams
|
|
err error
|
|
}
|
|
|
|
func (f *fakeJobs) CreateRestoreJob(_ context.Context, p restore.JobParams) error {
|
|
f.calls = append(f.calls, p)
|
|
return f.err
|
|
}
|
|
|
|
func TestRestoreEnqueuesJobWithDerivedParams(t *testing.T) {
|
|
jobs := &fakeJobs{}
|
|
r := &restore.Restorer{
|
|
Jobs: jobs,
|
|
Config: restore.Config{
|
|
Image: "registry.internal/felis:test",
|
|
BackupPVC: "felis-backups",
|
|
},
|
|
}
|
|
|
|
if err := r.Restore(context.Background(), "survival", "/backups/survival/2026.tar.gz"); err != nil {
|
|
t.Fatalf("Restore: %v", err)
|
|
}
|
|
if len(jobs.calls) != 1 {
|
|
t.Fatalf("CreateRestoreJob called %d times, want 1", len(jobs.calls))
|
|
}
|
|
got := jobs.calls[0]
|
|
if got.Server != "survival" {
|
|
t.Errorf("Server = %q, want survival", got.Server)
|
|
}
|
|
// The world PVC must come from the shared naming convention, not be invented
|
|
// here: it is the same name the operator created and the reaper deletes.
|
|
if got.WorldPVC != "world-survival-0" {
|
|
t.Errorf("WorldPVC = %q, want world-survival-0", got.WorldPVC)
|
|
}
|
|
if got.BackupRef != "/backups/survival/2026.tar.gz" {
|
|
t.Errorf("BackupRef = %q, want the passed ref", got.BackupRef)
|
|
}
|
|
if got.BackupPVC != "felis-backups" {
|
|
t.Errorf("BackupPVC = %q, want felis-backups", got.BackupPVC)
|
|
}
|
|
if got.Image != "registry.internal/felis:test" {
|
|
t.Errorf("Image = %q, want the configured image", got.Image)
|
|
}
|
|
// withDefaults must have filled the unset fields.
|
|
if got.Namespace == "" || got.ServiceAccount == "" || got.WorldsRoot == "" || got.BackupRoot == "" {
|
|
t.Errorf("defaults not applied: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestRestoreIsIdempotentOnAlreadyExists(t *testing.T) {
|
|
jobs := &fakeJobs{err: restore.ErrAlreadyExists}
|
|
r := &restore.Restorer{Jobs: jobs, Config: restore.Config{Image: "img", BackupPVC: "pvc"}}
|
|
|
|
// A restore already in flight is success, not an error: the handler must be
|
|
// able to answer 202 for a coalesced duplicate request.
|
|
if err := r.Restore(context.Background(), "survival", "ref"); err != nil {
|
|
t.Fatalf("Restore on AlreadyExists = %v, want nil (idempotent)", err)
|
|
}
|
|
}
|
|
|
|
func TestRestorePropagatesGenericError(t *testing.T) {
|
|
sentinel := errors.New("apiserver exploded")
|
|
jobs := &fakeJobs{err: sentinel}
|
|
r := &restore.Restorer{Jobs: jobs, Config: restore.Config{Image: "img", BackupPVC: "pvc"}}
|
|
|
|
err := r.Restore(context.Background(), "survival", "ref")
|
|
if !errors.Is(err, sentinel) {
|
|
t.Fatalf("Restore error = %v, want the underlying error propagated", err)
|
|
}
|
|
}
|