package main import ( "context" "encoding/json" "errors" "os" "path/filepath" "strings" "sync" "testing" "time" "git.novox.be/novox/mesh-sdk/go/health" ) // The secret every test that has one uses; it must never appear in anything the module says. const testPassword = "s3cr3t-Pa55" // fakeSystem is a machine under a temporary root with a recorded service manager. type fakeSystem struct { // mu guards calls, units and reachable, for a test that runs RunApply beside itself. mu sync.Mutex t *testing.T root string calls []string // units are the service manager's answers: ActiveState and UnitFileState by unit. units map[string]map[string]string // reachable are the host:port pairs that answer. reachable map[string]bool } func newSystem(t *testing.T) *fakeSystem { f := &fakeSystem{t: t, root: t.TempDir(), units: map[string]map[string]string{}, reachable: map[string]bool{}} for _, d := range []string{UnitDir, RunDir, BusDir, "/etc/mesh-mounts", "/proc/self", "/var/lib/mesh/mounts"} { if err := os.MkdirAll(filepath.Join(f.root, d), 0o755); err != nil { t.Fatal(err) } } f.write("/proc/self/mountinfo", "") // The mesh keeps no data on this machine, as the controller said through the bundle. f.write(DataFile, `{"machine":"workstation","paths":[]}`) return f } func (f *fakeSystem) write(p, content string) { f.t.Helper() full := filepath.Join(f.root, p) if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { f.t.Fatal(err) } if err := os.WriteFile(full, []byte(content), 0o644); err != nil { f.t.Fatal(err) } } func (f *fakeSystem) read(p string) string { b, _ := os.ReadFile(filepath.Join(f.root, p)) return string(b) } func (f *fakeSystem) run(_ context.Context, name string, args ...string) (string, error) { f.mu.Lock() defer f.mu.Unlock() f.calls = append(f.calls, name+" "+strings.Join(args, " ")) if name != "systemctl" || len(args) == 0 { return "", nil } switch args[0] { case "show": if len(args) >= 5 && args[3] == "--value" { return f.units[args[4]]["ActiveState"] + "\n", nil } var blocks []string for _, u := range args { if !strings.HasSuffix(u, ".mount") && !strings.HasSuffix(u, ".automount") { continue } s := f.units[u] blocks = append(blocks, "Id="+u+"\nActiveState="+orDefault(s["ActiveState"], "inactive")+"\nSubState=dead\nResult="+ orDefault(s["Result"], "success")+"\nUnitFileState="+orDefault(s["UnitFileState"], "disabled")) } return strings.Join(blocks, "\n\n") + "\n", nil case "enable": u := args[len(args)-1] f.units[u] = map[string]string{"ActiveState": "active", "UnitFileState": "enabled"} case "start": u := args[len(args)-1] if f.units[u] == nil { f.units[u] = map[string]string{} } f.units[u]["ActiveState"], f.units[u]["Result"] = "active", "success" case "reset-failed": u := args[len(args)-1] if f.units[u]["ActiveState"] == "failed" { f.units[u]["ActiveState"], f.units[u]["Result"] = "inactive", "success" } } return "", nil } func (f *fakeSystem) machine() *Machine { return &Machine{Root: f.root, Run: f.run, Escalate: f.run, LookPath: func(string) bool { return true }} } func (f *fakeSystem) applier(now time.Time) *Applier { return &Applier{M: f.machine(), Act: true, Judge: true, Dial: func(addr string, _ time.Duration) error { f.mu.Lock() defer f.mu.Unlock() if f.reachable[addr] { return nil } return errors.New("dial tcp " + addr + ": connect: no route to host") }, Now: func() time.Time { return now }, Lookup: func(string) (int, int, error) { return 1000, 1000, nil }, } } func (f *fakeSystem) called(prefix string) bool { f.mu.Lock() defer f.mu.Unlock() for _, c := range f.calls { if strings.HasPrefix(c, prefix) { return true } } return false } // everything is every file under the root, for a search that a secret appears nowhere it should not. func (f *fakeSystem) everything(except ...string) string { var b strings.Builder filepath.Walk(f.root, func(p string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return nil } for _, e := range except { if strings.HasSuffix(p, e) { return nil } } raw, _ := os.ReadFile(p) b.Write(raw) return nil }) return b.String() } const config = "shares=library=/srv/library downloads=/srv/downloads:ro\n" + "sources=games=smb://root@192.0.2.76/Share@/mnt/games usb=disk:1234-ABCD@/mnt/usb\n" + "server=homeserver\nat=192.0.2.10\nnode=workstation\naccount=operator\n" + "passwords=/var/lib/mesh/mounts\nstate=/var/lib/mesh/mounts\n" func exportsAnswer(t *testing.T, shares ...ServerShare) string { raw, err := json.Marshal(ExportsAnswer{Server: "homeserver", At: "192.0.2.10", Shares: shares}) if err != nil { t.Fatal(err) } return string(raw) } var ( library = ServerShare{Name: "library", Path: "/tank/media", Access: "rw", UID: 1001, GID: 2000, Live: true, To: []ShareClient{{Node: "workstation", Address: "10.0.0.3", Access: "rw"}}} downloads = ServerShare{Name: "downloads", Path: "/tank/downloads", Access: "rw", UID: 1001, GID: 2000, Live: true, To: []ShareClient{{Node: "workstation", Address: "10.0.0.3", Access: "rw"}}} ) func TestTheSettingsAreReadAndWhatCannotBeIsSaidPlainly(t *testing.T) { shares, problems := ParseShares("library=/srv/library downloads=/srv/downloads:ro,bad Upper=/x other=relative library=/srv/again root=/") if len(shares) != 2 || shares[0] != (Wanted{"library", "/srv/library", false}) || shares[1] != (Wanted{"downloads", "/srv/downloads", true}) { t.Fatalf("shares %+v", shares) } want := []string{`"bad" is not name=mountpoint`, `"Upper" is not lower-case`, `"relative" is not an absolute path`, "named twice", `"/" is the system's own directory`} for i, w := range want { if i >= len(problems) || !strings.Contains(problems[i], w) { t.Errorf("problem %d: want %q in %q", i, w, problems) } } if s, p := ParseShares("none"); len(s) != 0 || len(p) != 0 { t.Errorf("none is nothing: %v %v", s, p) } sources, problems := ParseSources("games=smb://192.0.2.76/Share@/mnt/games:ro usb=disk:1234-ABCD@/mnt/usb ftp=ftp://x/y@/mnt/x nomount=smb://h/s") if len(sources) != 2 || sources[0].Kind != "smb" || sources[0].Host != "192.0.2.76" || sources[0].Share != "Share" || !sources[0].ReadOnly || sources[1].Kind != "disk" || sources[1].UUID != "1234-ABCD" { t.Fatalf("sources %+v", sources) } if len(problems) != 2 || !strings.Contains(problems[0], "neither smb://") || !strings.Contains(problems[1], "names no mount point") { t.Errorf("problems %q", problems) } if err := CheckMountpoint("/etc/x"); err == nil { t.Error("a mount point under /etc is the system's") } } func TestAMountPointIsNamedAsSystemdEscapeNamesIt(t *testing.T) { for in, want := range map[string]string{ "/srv/library": "srv-library", "/storage/media": "storage-media", "/mnt/my-disk": `mnt-my\x2ddisk`, "/mnt/.hidden": `mnt-.hidden`, "/.x": `\x2ex`, "/srv/a b": `srv-a\x20b`, "/dev/disk/by-uuid/1234-ABCD": `dev-disk-by\x2duuid-1234\x2dABCD`, } { if got := EscapePath(in); got != want { t.Errorf("EscapePath(%q) = %q, want %q", in, got, want) } } } func TestANodesShareIsMountedHardWhenWritableAndSoftWhenNot(t *testing.T) { rw := NFSPair(Wanted{Name: "library", Mountpoint: "/srv/library"}, "192.0.2.10", library) for _, want := range []string{"What=192.0.2.10:/tank/media", "Where=/srv/library", "Type=nfs4", "Options=vers=4.2,hard,timeo=150,retrans=3,_netdev", "TimeoutSec=30", "After=network-online.target"} { if !strings.Contains(rw.Mount, want) { t.Errorf("the read-write mount lacks %q:\n%s", want, rw.Mount) } } if !strings.HasPrefix(rw.Mount, Marker) || !strings.HasPrefix(rw.Automount, Marker) { t.Error("both units open with the module's marker") } if strings.Contains(rw.Mount, "[Install]") { t.Error("the mount is never enabled: only the automount is") } for _, want := range []string{"[Automount]\nWhere=/srv/library\nTimeoutIdleSec=600", "WantedBy=remote-fs.target"} { if !strings.Contains(rw.Automount, want) { t.Errorf("the automount lacks %q:\n%s", want, rw.Automount) } } if rw.MountUnit != "srv-library.mount" || rw.AutoUnit != "srv-library.automount" || rw.Probe != "192.0.2.10:2049" { t.Errorf("names %+v", rw) } ro := NFSPair(Wanted{Name: "downloads", Mountpoint: "/srv/downloads", ReadOnly: true}, "192.0.2.10", downloads) if !strings.Contains(ro.Mount, "Options=ro,vers=4.2,soft,") { t.Errorf("a read-only share is mounted ro and soft:\n%s", ro.Mount) } served := library served.Access = "ro" if p := NFSPair(Wanted{Name: "library", Mountpoint: "/srv/library"}, "192.0.2.10", served); !strings.Contains(p.Mount, "Options=ro,") { t.Error("a share the server exports read-only is mounted read-only") } } func TestAnOccasionalSourceWaitsTenSecondsAndNeverFailsABoot(t *testing.T) { smb := SMBPair(Source{Name: "games", Kind: "smb", Host: "192.0.2.76", Share: "Share", Mountpoint: "/mnt/games"}, "/var/lib/mesh/mounts/smb-games.credentials", 1000, 1000) for _, want := range []string{"What=//192.0.2.76/Share", "Type=cifs", "credentials=/var/lib/mesh/mounts/smb-games.credentials", "nofail", "uid=1000,gid=1000", "TimeoutSec=10"} { if !strings.Contains(smb.Mount, want) { t.Errorf("the SMB mount lacks %q:\n%s", want, smb.Mount) } } if !smb.Occasional || smb.Probe != "192.0.2.76:445" || strings.Contains(smb.Mount, "password") { t.Errorf("smb %+v", smb) } disk := DiskPair(Source{Name: "usb", Kind: "disk", UUID: "1234-ABCD", Mountpoint: "/mnt/usb"}) dev := `dev-disk-by\x2duuid-1234\x2dABCD.device` if !strings.Contains(disk.Mount, "BindsTo="+dev) || !strings.Contains(disk.Mount, "What=/dev/disk/by-uuid/1234-ABCD") { t.Errorf("the disk's mount goes with the disk:\n%s", disk.Mount) } if strings.Contains(disk.Automount, "BindsTo") || !strings.Contains(disk.Automount, "WantedBy=local-fs.target") { t.Errorf("the disk's automount stays armed when the disk is away:\n%s", disk.Automount) } } func TestALookWritesArmsAndRemovesOnlyTheModulesOwnUnits(t *testing.T) { f := newSystem(t) f.write(ConfigFile, config) f.write(ExportsFile, exportsAnswer(t, library, downloads)) f.write("/var/lib/mesh/mounts/smb-password-games.secret", testPassword+"\n") // The operator's own unit for the downloads' mount point: never written over. f.write(filepath.Join(UnitDir, "srv-downloads.mount"), "[Mount]\nWhat=elsewhere\n") // A pair of the module's whose mount point nothing names any more. f.write(filepath.Join(UnitDir, "srv-old.mount"), Marker+"\n[Mount]\n") f.write(filepath.Join(UnitDir, "srv-old.automount"), Marker+"\n[Automount]\n") f.reachable["192.0.2.10:2049"] = true now := time.Date(2026, 10, 8, 19, 21, 0, 0, time.UTC) st := f.applier(now).Look(context.Background()) if got := f.read(filepath.Join(UnitDir, "srv-library.mount")); !strings.Contains(got, "What=192.0.2.10:/tank/media") { t.Errorf("the library's mount: %q", got) } if got := f.read(filepath.Join(UnitDir, "srv-downloads.mount")); got != "[Mount]\nWhat=elsewhere\n" { t.Errorf("a unit file that is not the module's is left byte for byte: %q", got) } if _, err := os.Stat(filepath.Join(f.root, UnitDir, "srv-old.mount")); !os.IsNotExist(err) { t.Error("the module's pair nothing names any more is removed") } for _, c := range []string{"systemctl disable --now srv-old.automount", "systemctl stop srv-old.mount", "systemctl daemon-reload", "systemctl enable --now srv-library.automount", "systemctl enable --now mnt-games.automount", "systemctl enable --now mnt-usb.automount"} { if !f.called(c) { t.Errorf("not called: %s (calls %q)", c, f.calls) } } if f.called("systemctl enable --now srv-downloads.automount") { t.Error("the refused share is not armed") } if info, err := os.Stat(filepath.Join(f.root, "/srv/library")); err != nil || !info.IsDir() { t.Error("a missing mount point is made") } creds := filepath.Join(f.root, "/var/lib/mesh/mounts/smb-games.credentials") if info, err := os.Stat(creds); err != nil || info.Mode().Perm() != 0o600 { t.Fatalf("the source's login is a file only root reads: %v", err) } if b, _ := os.ReadFile(creds); string(b) != "username=root\npassword="+testPassword+"\n" { t.Errorf("credentials file %q", b) } states := map[string]ShareStatus{} for _, s := range st.Shares { states[s.Name] = s } if states["library"].State != StateArmed || states["games"].State != StateAbsent || states["usb"].State != StateAbsent { t.Errorf("states %+v", states) } if d := states["downloads"]; d.State != StateFailed || !strings.Contains(d.Why, "not the module's") { t.Errorf("downloads %+v", d) } if !strings.Contains(states["games"].Why, "not present") || states["games"].Since != now.Format(time.RFC3339) { t.Errorf("games %+v", states["games"]) } // The password is in the secret and in the credentials file, and nowhere else. if strings.Contains(f.everything("smb-password-games.secret", "smb-games.credentials"), testPassword) { t.Error("the password leaked into a file the module wrote") } raw, _ := json.Marshal(f.machine().Health(now)) if strings.Contains(string(raw), testPassword) { t.Error("the password leaked into the health") } // A second look a minute later changes nothing and keeps since. f.calls = nil later := now.Add(time.Minute) st = f.applier(later).Look(context.Background()) if f.called("systemctl daemon-reload") || f.called("systemctl enable") { t.Errorf("a look with nothing to change changes nothing: %q", f.calls) } for _, s := range st.Shares { if s.Since != now.Format(time.RFC3339) { t.Errorf("%s since %s, want the first look's", s.Name, s.Since) } } } func TestASilentServerRemovesNoMount(t *testing.T) { f := newSystem(t) f.write(ConfigFile, "shares=library=/srv/library\nserver=homeserver\nat=192.0.2.10\n") f.write(filepath.Join(UnitDir, "srv-library.mount"), Marker+"\nkept\n") f.write(filepath.Join(UnitDir, "srv-library.automount"), Marker+"\nkept\n") st := f.applier(time.Now()).Look(context.Background()) if f.read(filepath.Join(UnitDir, "srv-library.mount")) != Marker+"\nkept\n" { t.Error("with no answer from the server the pair stays as it is") } if len(st.Shares) != 1 || st.Shares[0].State != StateUnreachable || !strings.Contains(st.Shares[0].Why, "has not been read from the bus yet") { t.Errorf("shares %+v", st.Shares) } // And a missing config file writes and removes nothing. g := newSystem(t) g.write(filepath.Join(UnitDir, "srv-x.mount"), Marker+"\n") st = g.applier(time.Now()).Look(context.Background()) if _, err := os.Stat(filepath.Join(g.root, UnitDir, "srv-x.mount")); err != nil || len(st.Problems) == 0 { t.Errorf("without its config the process removes nothing and says why: %v %v", err, st.Problems) } } func TestASourceWithoutItsLoginWaitsForTheOperatorAndIsNotMounted(t *testing.T) { for name, c := range map[string]struct { user, password string writePassword bool waitsFor []string }{ "no username": {"", testPassword, true, []string{"setting sources"}}, "no password": {"root@", "", false, []string{"secret smb-password-games"}}, "an empty password file": {"root@", "", true, []string{"secret smb-password-games"}}, "a line break only": {"root@", "\n", true, []string{"secret smb-password-games"}}, "neither": {"", "", false, []string{"setting sources", "secret smb-password-games"}}, } { f := newSystem(t) f.write(ConfigFile, "sources=games=smb://"+c.user+"192.0.2.76/Share@/mnt/games\npasswords=/var/lib/mesh/mounts\nstate=/var/lib/mesh/mounts\n") if c.writePassword { f.write("/var/lib/mesh/mounts/smb-password-games.secret", c.password) } now := time.Now() st := f.applier(now).Look(context.Background()) if len(st.Shares) != 1 || st.Shares[0].State != StateWaiting || len(st.Shares[0].Waits) != len(c.waitsFor) { t.Fatalf("%s: shares %+v", name, st.Shares) } for i, w := range st.Shares[0].Waits { if got := strings.TrimPrefix(w.Names(), "the "); got != c.waitsFor[i] || w.Part != "the source games" || w.Check() != nil { t.Errorf("%s: wait %d is %+v, want %s", name, i, w, c.waitsFor[i]) } } if _, err := os.Stat(filepath.Join(f.root, UnitDir, "mnt-games.mount")); !os.IsNotExist(err) { t.Errorf("%s: a source without its login gets no units", name) } if _, err := os.Stat(filepath.Join(f.root, "/var/lib/mesh/mounts/smb-games.credentials")); !os.IsNotExist(err) { t.Errorf("%s: a source without its login gets no credentials file", name) } h := f.machine().Health(now) raw, _ := json.Marshal(h) if h.State != health.StateWaits || h.Healthy || len(h.Waits) != len(c.waitsFor) || len(h.Found) != 0 { t.Errorf("%s: health %s", name, raw) } // What the node-engine reads is a wait it can check: the SDK's reader keeps it a wait. if read, err := health.Read(raw); err != nil || read.State != health.StateWaits { t.Errorf("%s: the engine reads %+v %v", name, read, err) } if strings.Contains(string(raw), testPassword) || strings.Contains(f.everything("smb-password-games.secret"), testPassword) { t.Errorf("%s: the password leaked", name) } } } func TestAPasswordIsOneLineKeptAsTyped(t *testing.T) { pw := " a b=c " + testPassword + "==" for text, want := range map[string]string{pw: pw, pw + "\n": pw, pw + "\r\n": pw} { got, given, err := ReadPassword(text) if err != nil || !given || got != want { t.Errorf("%q read as %q %v %v", strings.ReplaceAll(text, testPassword, ""), strings.ReplaceAll(got, testPassword, ""), given, err) } } if _, given, err := ReadPassword(""); given || err != nil { t.Error("an empty file is not given") } if _, given, err := ReadPassword(testPassword + "\nsecond\n"); !given || err == nil || strings.Contains(err.Error(), testPassword) { t.Errorf("two lines are refused, saying nothing typed: %v", err) } f := newSystem(t) f.write(ConfigFile, "sources=games=smb://root@192.0.2.76/Share@/mnt/games\npasswords=/var/lib/mesh/mounts\nstate=/var/lib/mesh/mounts\n") f.write("/var/lib/mesh/mounts/smb-password-games.secret", pw+"\n") f.applier(time.Now()).Look(context.Background()) creds := filepath.Join(f.root, "/var/lib/mesh/mounts/smb-games.credentials") if info, err := os.Stat(creds); err != nil || info.Mode().Perm() != 0o600 { t.Fatalf("the source's login is a file only root reads: %v", err) } if b, _ := os.ReadFile(creds); string(b) != "username=root\npassword="+pw+"\n" { t.Errorf("credentials file %q", strings.ReplaceAll(string(b), testPassword, "")) } // Two lines: refused as a fault the operator mends by giving it again, the password never said. f.write("/var/lib/mesh/mounts/smb-password-games.secret", testPassword+"\nsecond\n") now := time.Now() st := f.applier(now).Look(context.Background()) if len(st.Shares) != 1 || st.Shares[0].State != StateFailed || !strings.Contains(st.Shares[0].Why, "more than one line") { t.Fatalf("shares %+v", st.Shares) } raw, _ := json.Marshal(f.machine().Health(now)) if strings.Contains(string(raw), testPassword) || strings.Contains(f.everything("smb-password-games.secret", "smb-games.credentials"), testPassword) { t.Error("the password leaked") } } func TestAnSMBSourcesUserIsInItsEntryAndAPasswordThereIsRefusedUnsaid(t *testing.T) { sources, problems := ParseSources("a=smb://root@192.0.2.76/Share@/mnt/a b=smb://192.0.2.76/Share@/mnt/b " + "c=smb://WORKGROUP%5Cme@nas/Media@/mnt/c d=smb://me%40example@nas/Media@/mnt/d:ro") users := map[string]string{} for _, s := range sources { users[s.Name] = s.User if s.Host == "" || s.Share == "" { t.Errorf("%s: %+v", s.Name, s) } } if len(problems) != 0 || users["a"] != "root" || users["b"] != "" || users["c"] != `WORKGROUP\me` || users["d"] != "me@example" { t.Errorf("users %v problems %q", users, problems) } for what, entry := range map[string]string{ "a password in the URL": "e=smb://root:" + testPassword + "@nas/Media@/mnt/e", "a password with a slash": "e=smb://root:a/" + testPassword + "@nas/Media@/mnt/e", "a user with a space, encoded": "e=smb://a%20" + testPassword + "@nas/Media@/mnt/e", "a bad percent escape": "e=smb://%zz" + testPassword + "@nas/Media@/mnt/e", "no mount point, with a password": "smb://root:" + testPassword + "@nas/Media", "another scheme, upper case": "e=SMB://root:" + testPassword + "@nas/Media@/mnt/e", "a cifs URL": "e=cifs://root:" + testPassword + "@nas/Media@/mnt/e", "two @ before the host": "e=smb://me@example@nas/Media@/mnt/e" + testPassword, "a control character in the user": "e=smb://me%07" + testPassword + "@nas/Media@/mnt/e", } { got, problems := ParseSources(entry) said := strings.Join(problems, " ") if len(got) != 0 || len(problems) != 1 || strings.Contains(said, testPassword) { t.Errorf("%s: %+v %q", what, got, said) } } if _, problems := ParseSources("e=smb://root:" + testPassword + "@nas/Media@/mnt/e"); !strings.Contains(problems[0], "smb-password-") { t.Errorf("a password in the URL is said with where it goes: %q", problems) } // A password with a space or a comma splits the entry: neither half, nor a name that is not one, is said. const secret = "SECRETPW" for _, v := range []string{ "e=smb://root:SEC RETPW@nas/Media@/mnt/e", "e=smb://root:SEC,RETPW@nas/Media@/mnt/e", "e=SMB://root:" + secret + "@nas/Media@/mnt/e", "e=cifs://root:" + secret + "@nas/Media@/mnt/e", "SECRET" + "PW=smb://nas/Media@/mnt/e", // A password holding '=' and a space, or '=' and a comma: the tail reads like name=… and is still not said. "e=smb://root:sec RETPW=x@nas/Media@/mnt/e", "e=smb://root:sec,RETPW=x@nas/Media@/mnt/e", "e=smb://root:SEC RETPW=smb://nas/Media@/mnt/e", } { _, problems := ParseSources(v) said := strings.Join(problems, " ") if len(problems) == 0 || strings.Contains(strings.ToUpper(said), "RETPW") || strings.Contains(said, "SEC") || strings.Contains(said, "sec") && !strings.Contains(said, "secret smb-password") { t.Errorf("%q says %q", strings.ReplaceAll(v, "RETPW", ""), said) } // And nothing of it reaches the status or the health answer. f := newSystem(t) f.write(ConfigFile, "sources="+v+"\npasswords=/var/lib/mesh/mounts\nstate=/var/lib/mesh/mounts\n") now := time.Now() f.applier(now).Look(context.Background()) raw, _ := json.Marshal(f.machine().Health(now)) if strings.Contains(strings.ToUpper(string(raw)), "RETPW") || strings.Contains(strings.ToUpper(f.everything("mounts.conf")), "RETPW") { t.Errorf("%q leaked into the status or the health: %s", strings.ReplaceAll(v, "RETPW", ""), raw) } } } func TestTheHealthGivesAShareFiveMinutesAndAnAbsentSourceNone(t *testing.T) { now := time.Date(2026, 10, 8, 19, 30, 0, 0, time.Local) at := func(d time.Duration) string { return now.Add(-d).Format(time.RFC3339) } st := Status{Looked: at(10 * time.Second), Server: "homeserver", Shares: []ShareStatus{ {Name: "library", Kind: "nfs", Mountpoint: "/srv/library", State: StateUnreachable, Since: at(4 * time.Minute), Why: "192.0.2.10:2049 does not answer"}, {Name: "games", Kind: "smb", Occasional: true, Mountpoint: "/mnt/games", State: StateAbsent, Since: at(3 * time.Hour)}, }} if h := Judge(st, nil, now); !h.Healthy || !strings.Contains(strings.Join(h.Said, " "), "games at /mnt/games: not present since 16:30") { t.Errorf("under five minutes, and an absent source, are healthy: %+v", h) } st.Shares[0].Since = at(9 * time.Minute) h := Judge(st, nil, now) if h.Healthy || !strings.Contains(h.Why, "The share library of homeserver is not reachable from this machine since 19:21") { t.Errorf("five minutes away raises: %+v", h) } st.Shares = []ShareStatus{{Name: "library", Kind: "nfs", Mountpoint: "/srv/library", State: StateFailed, Since: at(time.Minute), Why: "its automount srv-library.automount is not armed (inactive)"}} if h := Judge(st, nil, now); h.Healthy { t.Error("an automount not armed is raised at once") } st.Looked = at(3 * time.Minute) st.Shares = nil if h := Judge(st, nil, now); h.Healthy || !strings.Contains(h.Why, "last looked") { t.Errorf("a stale look is raised: %+v", h) } if h := Judge(Status{}, errors.New("not present"), now); h.Healthy { t.Error("no status is unhealthy") } if h := Judge(Status{Looked: at(time.Second)}, nil, now); !h.Healthy || h.Why != "This machine is asked to mount nothing." { t.Errorf("nothing asked: %+v", h) } } // The workstation's two lines as they were found (research 038), in documentation addresses. const operatorsFstab = "UUID=ad8b / ext4 rw,relatime 0 1\n" + "192.0.2.10:/tank/media /srv/library nfs defaults,_netdev 0 0\n" + "//192.0.2.76/Share /mnt/games cifs username=root,password=" + testPassword + ",iocharset=utf8,sec=ntlmssp 0 0\n" func TestAnFstabLineIsTakenOverOnlyOnceItsReplacementIsArmed(t *testing.T) { f := newSystem(t) f.write("/etc/fstab", operatorsFstab) m := f.machine() now := time.Date(2026, 10, 8, 20, 0, 0, 0, time.UTC) ctx := context.Background() a, err := m.Adopt(ctx, "/mnt/games", false, "", now) if err != nil || a.Done || !strings.HasPrefix(a.Said, "Would be refused now: the module has no unit pair") { t.Fatalf("no pair yet: %+v %v", a, err) } if strings.Contains(a.Was, testPassword) || !strings.Contains(a.Was, "password=(redacted)") { t.Errorf("the dry run says the line without its password: %q", a.Was) } if _, err := m.AdoptAsRoot(ctx, "/mnt/games", now); err == nil { t.Fatal("the root half refuses too") } if f.read("/etc/fstab") != operatorsFstab { t.Fatal("a refusal changes nothing") } f.write(filepath.Join(UnitDir, "mnt-games.mount"), Marker+"\n") f.write(filepath.Join(UnitDir, "mnt-games.automount"), Marker+"\n") f.units["mnt-games.automount"] = map[string]string{"ActiveState": "inactive"} if _, err := m.AdoptAsRoot(ctx, "/mnt/games", now); err == nil || !strings.Contains(err.Error(), "not armed") { t.Fatalf("an automount not armed refuses: %v", err) } f.units["mnt-games.automount"] = map[string]string{"ActiveState": "active"} if a, err := m.Adopt(ctx, "/mnt/games", false, "", now); err != nil || !strings.HasPrefix(a.Said, "A dry run") || a.Line != 3 { t.Fatalf("dry run: %+v %v", a, err) } if _, err := m.Adopt(ctx, "/mnt/games", true, "", now); err == nil { t.Error("confirm without why is refused") } a, err = m.AdoptAsRoot(ctx, "/mnt/games", now) if err != nil || !a.Done || a.Line != 3 || a.Copy != "/etc/fstab.mesh-adopted-20261008-200000" { t.Fatalf("adopt: %+v %v", a, err) } got := f.read("/etc/fstab") lines := strings.Split(got, "\n") if lines[0] != "UUID=ad8b / ext4 rw,relatime 0 1" || lines[1] != "192.0.2.10:/tank/media /srv/library nfs defaults,_netdev 0 0" { t.Errorf("every other line is kept byte for byte:\n%s", got) } if !strings.HasPrefix(lines[2], AdoptedPrefix+"2026-10-08: //192.0.2.76/Share") || strings.Contains(got, testPassword) { t.Errorf("the line is commented out with its password redacted:\n%s", got) } info, err := os.Stat(filepath.Join(f.root, a.Copy)) if err != nil || info.Mode().Perm() != 0o600 || f.read(a.Copy) != operatorsFstab { t.Errorf("the file as it was is kept, root only: %v", err) } if !f.called("systemctl daemon-reload") { t.Error("the service manager reloads") } raw, _ := json.Marshal(a) if strings.Contains(string(raw), testPassword) { t.Error("the answer holds the password") } if _, err := m.AdoptAsRoot(ctx, "/mnt/games", now); err == nil || !strings.Contains(err.Error(), "no line for /mnt/games") { t.Errorf("a line taken over is not taken twice: %v", err) } if _, _, err := PlanAdoption("a /x nfs d 0 0\nb /x nfs d 0 0\n", "/x", now); err == nil || !strings.Contains(err.Error(), "more than one line") { t.Errorf("two lines for one mount point are left to a person: %v", err) } } func TestAFailedAskKeepsTheLastAnswer(t *testing.T) { cfg := ParseConfig("shares=library=/srv/library\nserver=homeserver\nat=192.0.2.10\n") now := time.Now() var asked string ok := func(key string) (json.RawMessage, error) { asked = key return json.RawMessage(`{"shares":[{"name":"library","path":"/tank/media","access":"rw","uid":1001,"gid":2000,"live":true}]}`), nil } first := AskExports(cfg, ExportsAnswer{}, ok, now) if asked != "homeserver" || len(first.Shares) != 1 || first.Error != "" { t.Fatalf("first %+v asked %q", first, asked) } fail := func(string) (json.RawMessage, error) { return nil, errors.New("no responders") } second := AskExports(cfg, first, fail, now.Add(time.Minute)) empty := AskExports(cfg, first, func(string) (json.RawMessage, error) { return nil, nil }, now) if !strings.Contains(empty.Error, "keeps nothing") || len(empty.Shares) != 1 { t.Errorf("a machine with nothing kept yet: %+v", empty) } if second.Error != "no responders" || len(second.Shares) != 1 || second.Answered != first.Answered { t.Errorf("second %+v", second) } if got := ParseConfig("at=homeserver.mesh.example\n").ServerNode(); got != "homeserver" { t.Errorf("without from, the first label of at: %q", got) } } func TestTheBundlePutsWhatTheProcessFoundOnTheBus(t *testing.T) { f := newSystem(t) f.write(ConfigFile, "shares=library=/srv/library\nserver=homeserver\nat=192.0.2.10\n") f.write(StatusFile, `{"looked":"2026-10-08T19:21:00Z","shares":[{"name":"library","state":"armed"}]}`) var put []string last := "" ask := func(string) (json.RawMessage, error) { return json.RawMessage(`{"shares":[]}`), nil } said := f.machine().Share("workstation", ask, func(key string, _ any) error { put = append(put, key); return nil }, &last, time.Now()) if len(said) != 0 || len(put) != 1 || put[0] != "workstation" || !strings.Contains(f.read(ExportsFile), `"server": "homeserver"`) { t.Errorf("said %q put %q exports %q", said, put, f.read(ExportsFile)) } f.machine().Share("workstation", ask, func(key string, _ any) error { put = append(put, key); return nil }, &last, time.Now()) if len(put) != 1 { t.Error("an unchanged status is not put again") } } // In production the acting process and the judging one are two: apply writes and arms and says nothing // of how the shares are; watch writes and arms nothing and says how they are. func TestApplyActsAndWatchJudgesAndNeitherDoesTheOthersWork(t *testing.T) { f := newSystem(t) f.write(ConfigFile, config) f.write(ExportsFile, exportsAnswer(t, library, downloads)) f.write("/var/lib/mesh/mounts/smb-password-other.secret", testPassword+"\n") f.write(filepath.Join(UnitDir, "srv-downloads.mount"), "[Mount]\nWhat=elsewhere\n") f.reachable["192.0.2.10:2049"] = true now := time.Now() apply := f.applier(now) apply.Judge = false apply.Look(context.Background()) if _, err := os.Stat(filepath.Join(f.root, StatusFile)); !os.IsNotExist(err) { t.Error("apply leaves no status: that is watch's") } if !f.called("systemctl enable --now srv-library.automount") || f.read(filepath.Join(UnitDir, "srv-library.mount")) == "" { t.Fatal("apply writes and arms") } applied, err := readJSON[Applied](f.machine(), AppliedFile) if err != nil || len(applied.NotSet) != 2 { t.Fatalf("apply says what it could not set up: %+v %v", applied, err) } f.calls = nil watch := f.applier(now) watch.Act = false f.write(filepath.Join(UnitDir, "srv-gone.mount"), Marker+"\n") st := watch.Look(context.Background()) for _, c := range f.calls { if !strings.HasPrefix(c, "systemctl show") { t.Errorf("watch only reads the service manager: %s", c) } } if _, err := os.Stat(filepath.Join(f.root, UnitDir, "srv-gone.mount")); err != nil { t.Error("watch removes nothing") } states := map[string]string{} for _, s := range st.Shares { states[s.Name] = s.State } if states["library"] != StateArmed || states["downloads"] != StateFailed || states["games"] != StateWaiting || states["usb"] != StateAbsent { t.Errorf("watch says how each is: %v", states) } if strings.Contains(f.everything("smb-password-other.secret"), testPassword) { t.Error("the password leaked") } } // setUnit sets one unit's state as the service manager answers it, under the lock. func (f *fakeSystem) setUnit(unit string, state map[string]string) { f.mu.Lock() defer f.mu.Unlock() f.units[unit] = state } // A wait beside a fault is said and excuses nothing: the answer is unhealthy, naming both (ADR 0283). func TestAWaitBesideAFaultIsUnhealthy(t *testing.T) { now := time.Date(2026, 10, 10, 19, 30, 0, 0, time.UTC) at := now.Add(-time.Hour).Format(time.RFC3339) wait := health.Wait{Part: "the source games", Secret: "smb-password-games", What: "the password of the source games"} games := ShareStatus{Name: "games", Kind: "smb", Occasional: true, Mountpoint: "/mnt/games", State: StateWaiting, Since: at, Why: "it waits for the operator to give the password of the source games (the secret smb-password-games)", Waits: []health.Wait{wait}} library := ShareStatus{Name: "library", Kind: "nfs", Mountpoint: "/srv/library", State: StateUnreachable, Since: at, Why: "no answer"} h := Judge(Status{Looked: now.Format(time.RFC3339), Server: "homeserver", Shares: []ShareStatus{games, library}}, nil, now) if h.State != health.StateUnhealthy || h.Healthy || len(h.Waits) != 1 || !strings.Contains(h.Why, "library") || !strings.Contains(h.Why, "games") { t.Errorf("a wait beside a fault: %+v", h) } h = Judge(Status{Looked: now.Format(time.RFC3339), Shares: []ShareStatus{games}}, nil, now) if h.State != health.StateWaits || len(h.Waits) != 1 || len(h.Found) != 0 || !strings.Contains(h.Why, "waits for the operator") { t.Errorf("a wait alone: %+v", h) } } // A password that exists and cannot be read, and a configuration naming no directory for the passwords, are // refused as faults: said without the value and without the path, and nothing is read from a relative path. func TestAPasswordThatCannotBeReadIsAFaultSaidWithoutValueOrPath(t *testing.T) { // A directory where the file should be: read fails, and not as "not there" (as root a mode would not stop it). f := newSystem(t) f.write(ConfigFile, "sources=games=smb://root@192.0.2.76/Share@/mnt/games\npasswords=/var/lib/mesh/mounts\nstate=/var/lib/mesh/mounts\n") f.write("/var/lib/mesh/mounts/smb-password-games.secret/inside", testPassword) st := f.applier(time.Now()).Look(context.Background()) if len(st.Shares) != 1 || st.Shares[0].State != StateFailed || !strings.Contains(st.Shares[0].Why, "could not be read") || strings.Contains(st.Shares[0].Why, "/var/lib") || strings.Contains(st.Shares[0].Why, testPassword) { t.Fatalf("an unreadable password: %+v", st.Shares) } // No passwords= and no state=: the source is not set up, and no relative path is read. for _, cfg := range []string{ "sources=games=smb://root@192.0.2.76/Share@/mnt/games\n", "sources=games=smb://root@192.0.2.76/Share@/mnt/games\npasswords=relative/dir\n", } { g := newSystem(t) g.write(ConfigFile, cfg) g.write("smb-password-games.secret", testPassword) g.write("relative/dir/smb-password-games.secret", testPassword) st := g.applier(time.Now()).Look(context.Background()) if len(st.Shares) != 1 || st.Shares[0].State != StateFailed || !strings.Contains(st.Shares[0].Why, "names no directory for its passwords") || strings.Contains(st.Shares[0].Why, testPassword) || strings.Contains(st.Shares[0].Why, "relative") { t.Errorf("%q: %+v", cfg, st.Shares) } if _, err := os.Stat(filepath.Join(g.root, "/var/lib/mesh/mounts/smb-games.credentials")); !os.IsNotExist(err) { t.Errorf("%q: a credentials file was written", cfg) } } }