bc4eda926b
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017CDMFpFx84pfviCTVuGGhf
253 lines
12 KiB
Go
253 lines
12 KiB
Go
package storage
|
||
|
||
import (
|
||
"context"
|
||
"os"
|
||
"path/filepath"
|
||
"runtime"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// The §8 decision table, encoded exactly (CAMPAIGN-3 F11: fstype-driven, automount state IGNORED).
|
||
func TestNetReassertClassify_Table(t *testing.T) {
|
||
cases := []struct {
|
||
fstype string
|
||
want string
|
||
}{
|
||
{"nfs4", NetReassertSkipActive}, // real mount — inherited by fresh namespaces
|
||
{"nfs", NetReassertSkipActive}, // real mount
|
||
{"cifs", NetReassertSkipActive}, // real mount
|
||
{"autofs", NetReassertRearmed}, // idle trigger — NOT inherited, re-arm to propagate
|
||
{"", NetReassertRearmed}, // F10: a failed/disarmed automount leaves NO mount entry — re-arm
|
||
{"ext4", NetReassertSkipForeign}, // a foreign local fs at the path is not ours to churn
|
||
{"tmpfs", NetReassertSkipForeign}, //
|
||
}
|
||
for _, c := range cases {
|
||
if got := netReassertClassify(c.fstype); got != c.want {
|
||
t.Errorf("netReassertClassify(%q) = %q, want %q", c.fstype, got, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// installNetUnitFile writes a rendered network .mount unit straight into unitDir (bypassing the
|
||
// runner-mediated install — the reassert only READS unit files).
|
||
func installNetUnitFile(t *testing.T, unitDir string, spec NetworkMountSpec) (where, automountUnit string) {
|
||
t.Helper()
|
||
where = spec.Where()
|
||
mountUnit, err := UnitNameForMount(where)
|
||
if err != nil {
|
||
t.Fatalf("unit name: %v", err)
|
||
}
|
||
if err := os.WriteFile(filepath.Join(unitDir, mountUnit), []byte(renderNetworkMountUnit(spec)), 0o644); err != nil {
|
||
t.Fatalf("write unit: %v", err)
|
||
}
|
||
return where, strings.TrimSuffix(mountUnit, ".mount") + ".automount"
|
||
}
|
||
|
||
// netReassertOps builds ops with a hermetic unitFailed seam (default: nothing failed — no shelling out
|
||
// to a real systemctl is-failed). failedUnits, if set, marks specific unit names as failed.
|
||
func netReassertOps(t *testing.T, unitDir string, mounts []Mount, failedUnits ...string) (*SudoHostOps, *recordingRunner) {
|
||
t.Helper()
|
||
rr := &recordingRunner{}
|
||
ops := NewSudoHostOps(SudoHostOpsConfig{
|
||
Runner: rr, Bins: Binaries{}.withDefaults(), UnitDir: unitDir, StageDir: t.TempDir(),
|
||
Host: &fakeHostReader{mounts: mounts}, Logger: quietLogger(),
|
||
})
|
||
failed := map[string]bool{}
|
||
for _, u := range failedUnits {
|
||
failed[u] = true
|
||
}
|
||
ops.unitFailed = func(_ context.Context, unit string) bool { return failed[unit] }
|
||
return ops, rr
|
||
}
|
||
|
||
// An idle trigger (autofs at the mountpoint) must be re-armed with EXACTLY the granted verbs:
|
||
// `systemctl stop -- <unit>.automount` then `systemctl enable --now -- <unit>.automount`. No
|
||
// reset-failed when nothing is failed.
|
||
func TestReassertNetworkAutomounts_RearmsIdleTrigger(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}
|
||
where, autoUnit := installNetUnitFile(t, unitDir, spec)
|
||
ops, rr := netReassertOps(t, unitDir, []Mount{{MountPoint: where, FSType: "autofs"}})
|
||
|
||
results := ops.ReassertNetworkAutomounts(context.Background())
|
||
if len(results) != 1 || results[0].Action != NetReassertRearmed || results[0].Err != nil {
|
||
t.Fatalf("want one rearmed result, got %+v", results)
|
||
}
|
||
if len(rr.calls) != 2 {
|
||
t.Fatalf("want exactly stop + enable --now (no reset-failed when clean), got %d calls: %v", len(rr.calls), rr.calls)
|
||
}
|
||
stop, enable := strings.Join(rr.calls[0], " "), strings.Join(rr.calls[1], " ")
|
||
if !strings.Contains(stop, "systemctl stop -- "+autoUnit) {
|
||
t.Errorf("first call must be `systemctl stop -- %s`, got: %s", autoUnit, stop)
|
||
}
|
||
if !strings.Contains(enable, "systemctl enable --now -- "+autoUnit) {
|
||
t.Errorf("second call must be `systemctl enable --now -- %s`, got: %s", autoUnit, enable)
|
||
}
|
||
}
|
||
|
||
// F10: a failed/start-limited automount (the campaign's unexport→idle-timeout→access×5 sequence leaves
|
||
// NO mount entry, fstype "") must be reset-failed FIRST, then re-armed — verdict reset-failed+rearmed.
|
||
// Companion to the campaign's reboots #2–#4: the pre-0.85 code returned skip-none here and left it dead.
|
||
func TestReassertNetworkAutomounts_ResetsFailedThenRearms(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}
|
||
where, autoUnit := installNetUnitFile(t, unitDir, spec)
|
||
// Nothing mounted (fstype "" — a start-limited automount), and the automount unit is in failed state.
|
||
ops, rr := netReassertOps(t, unitDir, nil, autoUnit)
|
||
|
||
results := ops.ReassertNetworkAutomounts(context.Background())
|
||
if len(results) != 1 || results[0].Action != NetReassertResetRearmed || results[0].Err != nil {
|
||
t.Fatalf("want one reset-failed+rearmed result, got %+v", results)
|
||
}
|
||
// reset-failed <automount>, then stop + enable --now.
|
||
var sawReset, sawEnable bool
|
||
for _, c := range rr.calls {
|
||
j := strings.Join(c, " ")
|
||
if strings.Contains(j, "reset-failed -- "+autoUnit) {
|
||
sawReset = true
|
||
}
|
||
if strings.Contains(j, "enable --now -- "+autoUnit) {
|
||
sawEnable = true
|
||
}
|
||
}
|
||
if !sawReset {
|
||
t.Errorf("a failed unit must be reset-failed before re-arm (F10); calls: %v", rr.calls)
|
||
}
|
||
if !sawEnable {
|
||
t.Errorf("the trigger must still be re-armed after reset-failed; calls: %v", rr.calls)
|
||
}
|
||
}
|
||
|
||
// An ACTIVE real mount must not be touched — stopping the automount of a live mount would churn it.
|
||
// (Red-proof companion: a naive always-rearm implementation fails this with recorded calls.)
|
||
func TestReassertNetworkAutomounts_ActiveMountUntouched(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}
|
||
where, _ := installNetUnitFile(t, unitDir, spec)
|
||
ops, rr := netReassertOps(t, unitDir, []Mount{{MountPoint: where, FSType: "nfs4"}})
|
||
|
||
results := ops.ReassertNetworkAutomounts(context.Background())
|
||
if len(results) != 1 || results[0].Action != NetReassertSkipActive {
|
||
t.Fatalf("want one skip-active result, got %+v", results)
|
||
}
|
||
if len(rr.calls) != 0 {
|
||
t.Fatalf("an actively-mounted share must trigger ZERO systemctl calls, got: %v", rr.calls)
|
||
}
|
||
}
|
||
|
||
// The automount unit's own state is IGNORED (F11 red-proof): even though a mutant that consulted
|
||
// `systemctl is-active <automount>` would see an armed trigger as "active" and skip it, the fstype at
|
||
// the path is autofs (idle) so the correct code RE-ARMS. Encoded as: an idle trigger re-arms regardless
|
||
// of failed/armed unit state — the decision is fstype only.
|
||
func TestReassertNetworkAutomounts_IgnoresAutomountUnitState(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}
|
||
where, _ := installNetUnitFile(t, unitDir, spec)
|
||
// fstype autofs (idle-armed): the correct decision is re-arm, NOT skip — a state-of-the-automount
|
||
// check would mis-skip an armed trigger (which always reports "active").
|
||
ops, _ := netReassertOps(t, unitDir, []Mount{{MountPoint: where, FSType: "autofs"}})
|
||
results := ops.ReassertNetworkAutomounts(context.Background())
|
||
if len(results) != 1 || (results[0].Action != NetReassertRearmed && results[0].Action != NetReassertResetRearmed) {
|
||
t.Fatalf("an idle (autofs) trigger must re-arm regardless of automount unit state, got %+v", results)
|
||
}
|
||
}
|
||
|
||
// A foreign filesystem at the path (ext4/tmpfs) is skipped — re-arming over it would fail (busy).
|
||
func TestReassertNetworkAutomounts_ForeignFSSkipped(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}
|
||
where, _ := installNetUnitFile(t, unitDir, spec)
|
||
ops, rr := netReassertOps(t, unitDir, []Mount{{MountPoint: where, FSType: "ext4"}})
|
||
results := ops.ReassertNetworkAutomounts(context.Background())
|
||
if len(results) != 1 || results[0].Action != NetReassertSkipForeign {
|
||
t.Fatalf("want one skip-foreign result, got %+v", results)
|
||
}
|
||
if len(rr.calls) != 0 {
|
||
t.Fatalf("a foreign fs at the path must trigger zero systemctl calls, got: %v", rr.calls)
|
||
}
|
||
}
|
||
|
||
// F9: the pass returns exactly one verdict per installed unit — including a failed one. An
|
||
// empty-looking sweep over N shares is structurally impossible (the campaign's silent zero-line sweep).
|
||
func TestReassertNetworkAutomounts_VerdictPerUnit(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()
|
||
specs := []NetworkMountSpec{
|
||
{Name: "alpha", Protocol: ProtocolNFS, Server: "10.0.0.5", Export: "/srv/a", MappedUID: 1000, MappedGID: 1000},
|
||
{Name: "beta", Protocol: ProtocolNFS, Server: "10.0.0.6", Export: "/srv/b", MappedUID: 1000, MappedGID: 1000},
|
||
{Name: "gamma", Protocol: ProtocolNFS, Server: "10.0.0.7", Export: "/srv/c", MappedUID: 1000, MappedGID: 1000},
|
||
}
|
||
for _, s := range specs {
|
||
installNetUnitFile(t, unitDir, s)
|
||
}
|
||
// alpha active (skip-active), beta idle (rearm), gamma failed-and-unmounted (reset-failed+rearm).
|
||
gammaAuto := func() string {
|
||
u, _ := UnitNameForMount(specs[2].Where())
|
||
return strings.TrimSuffix(u, ".mount") + ".automount"
|
||
}()
|
||
ops, _ := netReassertOps(t, unitDir, []Mount{
|
||
{MountPoint: specs[0].Where(), FSType: "nfs4"},
|
||
{MountPoint: specs[1].Where(), FSType: "autofs"},
|
||
}, gammaAuto)
|
||
|
||
results := ops.ReassertNetworkAutomounts(context.Background())
|
||
if len(results) != len(specs) {
|
||
t.Fatalf("verdict count must equal unit count (%d), got %d: %+v", len(specs), len(results), results)
|
||
}
|
||
for _, r := range results {
|
||
if r.Action == "" {
|
||
t.Errorf("every share must carry a verdict (F9), got empty for %s", r.Name)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Idempotency: two consecutive passes over an idle trigger both re-arm with the same action, no error.
|
||
func TestReassertNetworkAutomounts_Idempotent(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}
|
||
where, _ := installNetUnitFile(t, unitDir, spec)
|
||
ops, rr := netReassertOps(t, unitDir, []Mount{{MountPoint: where, FSType: "autofs"}})
|
||
|
||
first := ops.ReassertNetworkAutomounts(context.Background())
|
||
second := ops.ReassertNetworkAutomounts(context.Background())
|
||
if first[0].Action != NetReassertRearmed || second[0].Action != NetReassertRearmed {
|
||
t.Fatalf("both passes must re-arm: first=%+v second=%+v", first, second)
|
||
}
|
||
if len(rr.calls) != 4 {
|
||
t.Fatalf("two passes = 2×(stop+enable), got %d: %v", len(rr.calls), rr.calls)
|
||
}
|
||
}
|
||
|
||
// Zero configured network units → empty pass, zero commands.
|
||
func TestReassertNetworkAutomounts_NoUnitsNoOp(t *testing.T) {
|
||
ops, rr := netReassertOps(t, t.TempDir(), nil)
|
||
if results := ops.ReassertNetworkAutomounts(context.Background()); len(results) != 0 {
|
||
t.Fatalf("no units must yield no results, got %+v", results)
|
||
}
|
||
if len(rr.calls) != 0 {
|
||
t.Fatalf("no units must construct zero commands, got: %v", rr.calls)
|
||
}
|
||
}
|