Files
felhom-controller/controller/internal/web/filebrowser_gate_test.go
T
admin 9cd566ded7 v0.82.0: gate FileBrowser recreate on actual change (F2); drop unused restic binary (F1)
syncFileBrowserMounts no longer force-recreates FileBrowser unconditionally:
captures config.yaml+compose before writes, re-reads final content after, and
recreates only when they actually changed (new pure helper fbNeedsRecreate).
Controller restarts / no-op storage syncs now issue a plain up -d and do NOT
bounce the customer's file UI. Restore-mode DB reset still forces a recreate.

Dockerfile: removed the unused restic apt package (disk-tier restic moved to the
host agent; no controller code execs the binary). ResticSchedule/migrateResticToRsync
config+settings paths untouched (still live in the dashboard).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpBYrZCt9sFDqLgbG5GRGD
2026-06-24 11:01:18 +02:00

52 lines
2.2 KiB
Go

package web
import "testing"
// T4: skipFileBrowserPath skips ONLY an external drive path (under StableParentDir) that is not a live
// mountpoint. A mounted external path and any system/local path are never skipped.
// Companion red-proof: dropping the gate (always-false) makes the absent-usb case fail.
func TestSkipFileBrowserPath(t *testing.T) {
// flash is mounted; usb is not.
isMount := func(p string) bool { return p == StableParentDir+"/flash" }
cases := []struct {
path string
want bool
}{
{StableParentDir + "/flash", false}, // external + mounted → keep
{StableParentDir + "/usb", true}, // external + NOT mounted → skip
{"/mnt/sys_drive/felhom-data", false}, // system path (not under StableParentDir) → never skip
}
for _, c := range cases {
if got := skipFileBrowserPath(c.path, isMount); got != c.want {
t.Errorf("skipFileBrowserPath(%q) = %v, want %v", c.path, got, c.want)
}
}
}
// F2: fbNeedsRecreate gates the FileBrowser --force-recreate so a no-op controller restart / storage
// sync no longer bounces the customer's file UI. Recreate only when config OR compose actually changed;
// the first-ever run (empty old files) differs from the generated content → recreate (creates it).
// Companion red-proof: hard-wiring the helper to always return true (the OLD unconditional behaviour)
// makes the "unchanged → no recreate" case fail — restoring the byte-equality gate turns it green.
func TestFbNeedsRecreate(t *testing.T) {
cfg := []byte("sources:\n - /srv/usb\n")
compose := []byte("services:\n filebrowser:\n image: x\n")
cases := []struct {
name string
oldCfg, newCfg, oldCmp, newCmp []byte
want bool
}{
{"unchanged → no recreate", cfg, cfg, compose, compose, false},
{"config differs → recreate", cfg, []byte("sources:\n - /srv/hdd\n"), compose, compose, true},
{"compose differs → recreate", cfg, cfg, compose, []byte("services:\n filebrowser:\n image: y\n"), true},
{"first run (no old files) → recreate", nil, cfg, nil, compose, true},
}
for _, c := range cases {
if got := fbNeedsRecreate(c.oldCfg, c.newCfg, c.oldCmp, c.newCmp); got != c.want {
t.Errorf("%s: fbNeedsRecreate = %v, want %v", c.name, got, c.want)
}
}
}