Files
2026-07-12 07:49:58 +02:00

253 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}
_, 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)
}
}