Files
mesh-catalog/modules/mounts/cmd/mounts/write_test.go
T

563 lines
24 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// 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 {
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.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"}
}
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 {
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 {
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://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" +
"secret=/var/lib/mesh/mounts/smb-credentials.secret\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-credentials.secret", "[games]\nusername=root\npassword="+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-credentials.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 TestAMissingLoginIsSaidWithItsRemedyAndTheSourceIsNotMounted(t *testing.T) {
f := newSystem(t)
f.write(ConfigFile, "sources=games=smb://192.0.2.76/Share@/mnt/games\nsecret=/var/lib/mesh/mounts/smb-credentials.secret\nstate=/var/lib/mesh/mounts\n")
f.write("/var/lib/mesh/mounts/smb-credentials.secret", "[other]\nusername=x\npassword="+testPassword+"\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, "secret accept <node> mounts smb-credentials --from <file>") {
t.Fatalf("shares %+v", st.Shares)
}
if _, err := os.Stat(filepath.Join(f.root, UnitDir, "mnt-games.mount")); !os.IsNotExist(err) {
t.Error("a source without its login gets no units")
}
h := f.machine().Health(now)
if h.Healthy || !strings.Contains(h.Why, "login is not given") || strings.Contains(h.Why, testPassword) {
t.Errorf("health %+v", h)
}
creds, problems := ParseSMBCredentials("[a]\nusername=u\npassword=" + testPassword + "\nbogus=1\n[b]\npassword=p\n")
if creds["a"].Password != testPassword || len(problems) != 2 || strings.Contains(strings.Join(problems, " "), testPassword) {
t.Errorf("creds %v problems %q", len(creds), problems)
}
}
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-credentials.secret", "[other]\nusername=x\npassword="+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"] != StateFailed || states["usb"] != StateAbsent {
t.Errorf("watch says how each is: %v", states)
}
if strings.Contains(f.everything("smb-credentials.secret"), testPassword) {
t.Error("the password leaked")
}
}