Files
felhom-controller/controller/internal/backup/r477_r474_test.go
T
admin bdcbd50b42
gates / gates (push) Successful in 13s
controller v0.240.0: seven defects from the any-tier proof and the first nightly rotation
R-486 (P1): removing an app with its backups KEPT keeps its Tier-2 record,
so the second-drive restore is no longer refused over an intact mirror.
R-484: postgis/pgvector/timescaledb images are Postgres (logical dumps).
R-485: the backup card sizes the recovery unit and the mirror(s).
R-480: a held update's sentence leaves the card once the hold is lifted.
R-477: the update's off-site lookup is one snapshots call, no stats.
R-478: a copy older than this install's deploy does not count.
R-474: "delete backups" deletes the unit, the mirror(s) and the prefs.

Tests and red-proofs per row; evidence in felhom.eu
documentation/audits/v0240-2026-09-13/ and nightly-2026-09-13-adventurelog/.
2026-09-13 19:26:50 +02:00

111 lines
4.0 KiB
Go

package backup
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
)
// R-477 — the update's Tier-3 lookup is ONE snapshots call. The control proves the fake runner does
// see stats calls when something makes them (the inventory page), so a zero is a measurement.
//
// COMPANION RED-PROOF (REPORT.md): make OffsiteSnapshotTimes read OffsiteInventoryList — this fails.
func TestR477_TheUpdateOffsiteLookupRunsNoStats(t *testing.T) {
m, _ := newOffboxManager(t)
var calls []string
m.SetOffboxRunner(func(_ context.Context, _ []string, args ...string) ([]byte, error) {
calls = append(calls, strings.Join(args, " "))
if contains(args, "snapshots") {
return []byte(`[{"short_id":"a1","time":"2026-09-13T01:00:00Z","tags":["gokapi"]},{"short_id":"b2","time":"2026-09-13T02:00:00Z","tags":["kimai"]}]`), nil
}
return []byte(`{"total_size":1024,"total_file_count":1}`), nil
})
m.updateTier2PointFn, m.updateTier1PointsFn = noTier2, noTier1
count := func(word string) int {
n := 0
for _, c := range calls {
if strings.Contains(" "+c+" ", " "+word+" ") {
n++
}
}
return n
}
p, ok, _ := m.UpdateRestorePoints(context.Background(), "gokapi", nil)
if !ok || p.Tier != UpdateTierOffsite || !p.At.Equal(time.Date(2026, 9, 13, 1, 0, 0, 0, time.UTC)) {
t.Fatalf("want gokapi's off-site snapshot; got %+v ok=%v", p, ok)
}
if count("snapshots") != 1 || count("stats") != 0 {
t.Errorf("the update lookup must be one snapshots call and no stats; calls=%v", calls)
}
calls = nil
if _, err := m.OffsiteInventoryList(context.Background()); err != nil {
t.Fatal(err)
}
if count("stats") == 0 {
t.Fatalf("control: the inventory DOES run stats, so the fake must see them; calls=%v", calls)
}
}
// R-474 — only the app's own Tier-2 mirror is found and removed.
func TestR474_OnlyTheAppsOwnMirrorIsRemoved(t *testing.T) {
m, sett := newOffboxManager(t)
drive := t.TempDir()
if err := sett.AddStoragePath(settings.StoragePath{Path: drive, Label: "HDD", Schedulable: true}); err != nil {
t.Fatal(err)
}
root := NamespaceRootFor(drive, m.systemDataPath)
sec := filepath.Join(root, "backups", "secondary")
mine, other, shares := filepath.Join(sec, "gokapi"), filepath.Join(sec, "kimai"), filepath.Join(sec, SharesPseudoStack)
for _, d := range []string{mine, other, shares} {
if err := os.MkdirAll(filepath.Join(d, "recovery-unit"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(d, "recovery-unit", "manifest.json"), []byte("{}"), 0o644); err != nil {
t.Fatal(err)
}
}
dirs := m.Tier2MirrorDirsForApp("gokapi")
if len(dirs) != 1 || dirs[0] != mine {
t.Fatalf("mirror dirs for gokapi = %v, want [%s]", dirs, mine)
}
removed := m.RemoveTier2Mirrors("gokapi", append(dirs, other, shares, filepath.Join(sec, "gokapi", "..", "kimai")))
if len(removed) != 1 {
t.Errorf("exactly the app's own mirror must be removed, got %v", removed)
}
if _, err := os.Stat(mine); !os.IsNotExist(err) {
t.Error("gokapi's mirror must be gone")
}
for _, keep := range []string{other, shares} {
if _, err := os.Stat(keep); err != nil {
t.Errorf("%s must survive: %v", keep, err)
}
}
if m.Tier2MirrorDirsForApp("../kimai") != nil || m.RemoveTier2Mirrors(SharesPseudoStack, []string{shares}) != nil {
t.Error("an unsafe or shares name must find and remove nothing")
}
}
func TestR474_DeleteAppBackupPrefsForgetsOnlyThatApp(t *testing.T) {
_, sett := newOffboxManager(t)
_ = sett.SetAppOffbox("gokapi", true)
_ = sett.SetCrossDriveConfig("gokapi", &settings.CrossDriveBackup{DestinationPath: "/mnt/x"})
_ = sett.SetAppOffbox("kimai", true)
if err := sett.DeleteAppBackupPrefs("gokapi"); err != nil {
t.Fatal(err)
}
if sett.IsAppOffbox("gokapi") || sett.GetCrossDriveConfig("gokapi") != nil {
t.Error("gokapi's prefs must be forgotten")
}
if !sett.IsAppOffbox("kimai") {
t.Error("another app's prefs must stay")
}
if err := sett.DeleteAppBackupPrefs("never-there"); err != nil {
t.Errorf("deleting absent prefs is a no-op, got %v", err)
}
}