bc4eda926b
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017CDMFpFx84pfviCTVuGGhf
97 lines
4.0 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|