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 -- .automount` then `systemctl enable --now -- .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} _, 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 , 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 ` 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) } }