Files
felhom-agent/internal/storage/netresidue_test.go
T

97 lines
4.0 KiB
Go

package storage
import (
"context"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
// F2/F1 (CAMPAIGN-3): a remove (and, via the same path, a rolled-back add) must leave ZERO residue —
// no failed-state units, no leftover mountpoint dir. RemoveNetworkMount must reset-failed the stuck
// unit BEFORE removing the files (or systemd keeps it as not-found/failed) and rmdir the mountpoint.
func TestRemoveNetworkMount_ZeroResidue(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("systemd-escaped unit filename contains a backslash; exercised on the Linux build server")
}
unitDir := t.TempDir()
stageDir := t.TempDir()
spec := NetworkMountSpec{Name: "media", Protocol: ProtocolNFS, Server: "10.0.0.5", Export: "/srv/media", MappedUID: 1000, MappedGID: 1000}
where := spec.Where()
mountName, err := UnitNameForMount(where)
if err != nil {
t.Fatalf("unit name: %v", err)
}
autoName := strings.TrimSuffix(mountName, ".mount") + ".automount"
// Both unit files present on disk (rm is recorded, so they stay for the assertion).
if err := os.WriteFile(filepath.Join(unitDir, mountName), []byte(renderNetworkMountUnit(spec)), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(unitDir, autoName), []byte(renderNetworkAutomountUnit(spec)), 0o644); err != nil {
t.Fatal(err)
}
rr := &recordingRunner{}
ops := NewSudoHostOps(SudoHostOpsConfig{Runner: rr, Bins: Binaries{}.withDefaults(), UnitDir: unitDir, StageDir: stageDir, Host: &fakeHostReader{}, Logger: quietLogger()})
// The automount is in the failed state (start-limit residue) — must be reset-failed.
ops.unitFailed = func(_ context.Context, unit string) bool { return unit == autoName }
if err := ops.RemoveNetworkMount(context.Background(), "media"); err != nil {
t.Fatalf("remove: %v", err)
}
var sawReset, sawRmUnit, sawRmdir, sawReload bool
for _, c := range rr.calls {
j := strings.Join(c, " ")
switch {
case strings.Contains(j, "reset-failed -- "+autoName):
sawReset = true
case strings.Contains(j, "rm -f") && strings.Contains(j, autoName):
sawRmUnit = true
case strings.Contains(j, "rmdir") && strings.Contains(j, where):
sawRmdir = true
case strings.Contains(j, "daemon-reload"):
sawReload = true
}
}
if !sawReset {
t.Errorf("F2: a failed unit must be reset-failed on remove; calls: %v", rr.calls)
}
if !sawRmUnit {
t.Errorf("the unit files must be removed; calls: %v", rr.calls)
}
if !sawRmdir {
t.Errorf("F1: the empty mountpoint dir must be rmdir'd; calls: %v", rr.calls)
}
if !sawReload {
t.Errorf("daemon-reload must run after removal; calls: %v", rr.calls)
}
}
// rmdir is used (never rm -rf) — the fail-safe: a non-empty dir is left in place, not force-removed.
func TestRemoveNetworkMount_NeverForceRemovesMountpoint(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("systemd-escaped unit filename contains a backslash; exercised on the Linux build server")
}
unitDir := t.TempDir()
spec := NetworkMountSpec{Name: "media", Protocol: ProtocolNFS, Server: "10.0.0.5", Export: "/srv/media", MappedUID: 1000, MappedGID: 1000}
mountName, _ := UnitNameForMount(spec.Where())
autoName := strings.TrimSuffix(mountName, ".mount") + ".automount"
_ = os.WriteFile(filepath.Join(unitDir, mountName), []byte(renderNetworkMountUnit(spec)), 0o644)
_ = os.WriteFile(filepath.Join(unitDir, autoName), []byte(renderNetworkAutomountUnit(spec)), 0o644)
rr := &recordingRunner{}
ops := NewSudoHostOps(SudoHostOpsConfig{Runner: rr, Bins: Binaries{}.withDefaults(), UnitDir: unitDir, StageDir: t.TempDir(), Host: &fakeHostReader{}, Logger: quietLogger()})
if err := ops.RemoveNetworkMount(context.Background(), "media"); err != nil {
t.Fatalf("remove: %v", err)
}
for _, c := range rr.calls {
j := strings.Join(c, " ")
if strings.Contains(j, "rm -rf") || (strings.Contains(j, "rm ") && strings.Contains(j, "/mnt/felhom-drives/media") && !strings.Contains(j, "rmdir")) {
t.Errorf("mountpoint cleanup must be rmdir-only (never rm -rf); offending call: %s", j)
}
}
}