Files
mesh-catalog/modules/mounts/cmd/mounts/mounts_test.go
T
jschoubben 99df6b999b
mesh/merge-gate pass: builds mounts → shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
mounts: pass unit names to systemctl show after --, so the root mount's -.mount is not read as an option
systemctl refused the whole call on -.mount, and list answered every mount
without its unit state (novox/hq issue 374). The test fake now refuses a
dash-led argument as systemctl does, so it no longer agrees with the mistake.
2026-10-11 02:02:18 +02:00

280 lines
12 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
)
// fake is a machine for a test: a tree standing in for /etc, /proc and /var/lib, and a runner
// answering from a table.
type fake struct {
t *testing.T
root string
answers map[string]string
zfs bool
}
func newFake(t *testing.T) *fake {
t.Helper()
return &fake{t: t, root: t.TempDir(), answers: map[string]string{}}
}
func (f *fake) file(path, text string) {
f.t.Helper()
p := filepath.Join(f.root, path)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
f.t.Fatal(err)
}
if err := os.WriteFile(p, []byte(text), 0o644); err != nil {
f.t.Fatal(err)
}
}
func (f *fake) machine() *Machine {
return &Machine{
Root: f.root,
Run: func(_ context.Context, name string, args ...string) (string, error) {
line := strings.TrimSpace(name + " " + strings.Join(args, " "))
if name == "systemctl" {
if err := systemctlRefuses(args); err != nil {
return "", err
}
}
if out, ok := f.answers[line]; ok {
return out, nil
}
return "", fmt.Errorf("%s: not found", line)
},
LookPath: func(n string) bool { return n == "zfs" && f.zfs },
}
}
// systemctlRefuses is what systemctl itself does with an argument that starts with a dash before
// "--": it reads it as an option, so the root mount's unit -.mount is refused. The message is the
// one systemctl gave on the desktop (novox/hq issue 374); a fake that took -.mount as a unit name
// agreed with the mistake.
func systemctlRefuses(args []string) error {
for i := 0; i < len(args); i++ {
a := args[i]
switch {
case a == "--":
return nil
case a == "-p":
i++ // its value is the property list
case strings.HasPrefix(a, "--"):
case strings.HasPrefix(a, "-") && len(a) > 1:
return fmt.Errorf("exit status 1: systemctl: invalid option -- '%c'\nHint: to specify units starting with a dash, use \"--\":\n systemctl [OPTIONS…] COMMAND -- %s …", a[1], a)
}
}
return nil
}
func TestRedactOptions(t *testing.T) {
cases := []struct{ in, want string }{
{"", ""},
{"defaults,_netdev", "defaults,_netdev"},
{"username=root,password=hunter2,iocharset=utf8", "username=root,password=(redacted),iocharset=utf8"},
{"user=a,pass=x,PASSWD=y", "user=a,pass=(redacted),PASSWD=(redacted)"},
{"credentials=/etc/samba/creds,sec=ntlmssp", "credentials=/etc/samba/creds,sec=ntlmssp"},
{"password=", "password=(redacted)"},
{"password2=z,passwordless", "password2=(redacted),passwordless"},
}
for _, c := range cases {
if got := RedactOptions(c.in); got != c.want {
t.Errorf("RedactOptions(%q) = %q, want %q", c.in, got, c.want)
}
}
}
const sampleFstab = `# /etc/fstab
UUID=aaaa / ext4 rw,relatime 0 1
UUID=bbbb none swap defaults 0 0
192.0.2.10:/storage/media /storage/media nfs defaults,_netdev 0 0
//192.0.2.20/Share /mnt/my\040share cifs username=root,password=s3cret,iocharset=utf8 0 0
192.0.2.10:/backup /mnt/backup nfs4 nofail,x-systemd.automount 0 0
`
func TestParseFstab(t *testing.T) {
got := ParseFstab(sampleFstab)
if len(got) != 5 {
t.Fatalf("got %d entries, want 5: %+v", len(got), got)
}
cases := []struct {
i int
point string
network bool
line int
}{
{0, "/", false, 2}, {2, "/storage/media", true, 4}, {3, "/mnt/my share", true, 5}, {4, "/mnt/backup", true, 6},
}
for _, c := range cases {
e := got[c.i]
if e.Mountpoint != c.point || e.Network != c.network || e.Line != c.line {
t.Errorf("entry %d = %+v, want point %q network %v line %d", c.i, e, c.point, c.network, c.line)
}
}
if strings.Contains(got[3].Options, "s3cret") {
t.Errorf("a password was answered: %q", got[3].Options)
}
}
func TestIsNetwork(t *testing.T) {
cases := []struct {
typ, src string
want bool
}{
{"nfs", "h:/x", true}, {"cifs", "//h/s", true}, {"fuse.sshfs", "u@h:/x", true}, {"ext4", "/dev/sda1", false},
{"fuseblk", "/dev/sda2", false}, {"fuse.rclone", "remote:", true}, {"fuse.portal", "portal", false},
}
for _, c := range cases {
if got := IsNetwork(c.typ, c.src); got != c.want {
t.Errorf("IsNetwork(%q, %q) = %v, want %v", c.typ, c.src, got, c.want)
}
}
}
const sampleMountinfo = `22 1 259:3 / / rw,relatime shared:1 - ext4 /dev/nvme0n1p3 rw
40 22 0:50 / /mnt/backup rw,relatime shared:30 - autofs systemd-1 rw,fd=50
61 40 0:60 / /mnt/backup rw,relatime shared:31 - nfs4 192.0.2.10:/backup rw,vers=4.2
70 22 0:70 / /mnt/my\040share rw - cifs //192.0.2.20/Share rw,username=root,password=leaked
`
func TestParseMountinfo(t *testing.T) {
got := ParseMountinfo(sampleMountinfo)
if got["/mnt/backup"].Type != "nfs4" {
t.Errorf("the last mount at a point must win: %+v", got["/mnt/backup"])
}
share, ok := got["/mnt/my share"]
if !ok {
t.Fatalf("an escaped mount point was not decoded: %+v", got)
}
if strings.Contains(share.Options, "leaked") {
t.Errorf("a password in mountinfo was answered: %q", share.Options)
}
}
const sampleListUnits = `-.mount loaded active mounted Root Mount
mnt-backup.automount loaded active waiting mnt-backup.automount
mnt-backup.mount loaded active mounted /mnt/backup
mnt-my\x20share.mount loaded failed failed /mnt/my share
storage-media.mount loaded failed failed /storage/media
dev-mqueue.mount loaded active mounted POSIX Message Queue File System
`
var sampleShow = strings.Join([]string{
"Id=-.mount\nWhere=/\nWhat=/dev/nvme0n1p3\nType=ext4\nOptions=rw\nResult=success\nActiveState=active\nSubState=mounted\nFragmentPath=/run/systemd/generator/-.mount\nSourcePath=/etc/fstab\nTimeoutUSec=1min 30s\nUnitFileState=generated",
"Id=mnt-backup.automount\nWhere=/mnt/backup\nResult=success\nActiveState=active\nSubState=waiting\nFragmentPath=/run/systemd/generator/mnt-backup.automount\nSourcePath=/etc/fstab\nUnitFileState=generated",
"Id=mnt-backup.mount\nWhere=/mnt/backup\nWhat=192.0.2.10:/backup\nType=nfs4\nOptions=rw,vers=4.2\nResult=success\nActiveState=active\nSubState=mounted\nFragmentPath=/run/systemd/generator/mnt-backup.mount\nSourcePath=/etc/fstab\nTimeoutUSec=1min 30s\nUnitFileState=generated",
"Id=mnt-my\\x20share.mount\nWhere=/mnt/my share\nWhat=//192.0.2.20/Share\nType=cifs\nOptions=username=root,password=s3cret\nResult=exit-code\nActiveState=failed\nSubState=failed\nFragmentPath=/run/systemd/generator/mnt-my\\x20share.mount\nSourcePath=/etc/fstab\nUnitFileState=generated",
"Id=storage-media.mount\nWhere=/storage/media\nWhat=192.0.2.10:/storage/media\nType=nfs\nOptions=defaults,_netdev\nResult=timeout\nActiveState=failed\nSubState=failed\nFragmentPath=/run/systemd/generator/storage-media.mount\nSourcePath=/etc/fstab\nTimeoutUSec=1min 30s\nUnitFileState=generated",
"Id=dev-mqueue.mount\nWhere=/dev/mqueue\nWhat=mqueue\nType=mqueue\nActiveState=active\nSubState=mounted\nFragmentPath=/usr/lib/systemd/system/dev-mqueue.mount\nUnitFileState=static",
}, "\n\n") + "\n"
func listFake(t *testing.T) *fake {
f := newFake(t)
f.file("/etc/fstab", sampleFstab)
f.file("/proc/self/mountinfo", sampleMountinfo)
f.answers["systemctl list-units --type=mount,automount --all --no-legend --plain --no-pager"] = sampleListUnits
names := strings.Join(ParseListUnits(sampleListUnits), " ")
f.answers["systemctl show -p "+ShowProperties+" --no-pager -- "+names] = sampleShow
return f
}
func TestList(t *testing.T) {
r := listFake(t).machine().List(false, "")
said := strings.Join(r.Said, "\n")
for _, want := range []string{
"/storage/media (nfs from 192.0.2.10:/storage/media): failed, timeout; from /etc/fstab line 4, no automount, no nofail",
"/mnt/my share (cifs from //192.0.2.20/Share): failed, exit-code; from /etc/fstab line 5, no automount, no nofail",
"/mnt/backup (nfs4 from 192.0.2.10:/backup): mounted; from /etc/fstab line 6",
"/ (ext4 from UUID=aaaa): mounted; from /etc/fstab line 2",
} {
if !strings.Contains(said, want) {
t.Errorf("said lacks %q:\n%s", want, said)
}
}
if strings.Contains(said, "mqueue") {
t.Errorf("a kernel filesystem was said:\n%s", said)
}
for _, mt := range r.Mounts {
if mt.Mountpoint == "/mnt/backup" && (mt.Automount == nil || mt.MountUnit == nil) {
t.Errorf("/mnt/backup must carry both units: %+v", mt)
}
if mt.Mountpoint == "/storage/media" && mt.MountUnit.From != "generated from /etc/fstab" {
t.Errorf("from = %q", mt.MountUnit.From)
}
}
all, _ := json.Marshal(r)
if strings.Contains(string(all), "s3cret") || strings.Contains(string(all), "leaked") {
t.Errorf("a credential was answered: %s", all)
}
}
// The desktop's own mount units, as its service manager listed them on 2026-10-11 (novox/hq issue
// 374): the root mount's unit is -.mount, and systemctl reads a name that starts with a dash as an
// option unless it comes after "--". Every mount then lost its unit state, not only the root's.
const desktopListUnits = `-.mount loaded active mounted Root Mount
boot.mount loaded active mounted /boot
home.mount loaded active mounted /home
mnt-hdd_2tb.mount loaded active mounted /mnt/hdd_2tb
mnt-recalbox.automount loaded active waiting recalbox, mounted by the mesh at first use
mnt-recalbox.mount loaded inactive dead recalbox (//192.0.2.176/Share), mounted by the mesh
storage-media.automount loaded active waiting media, mounted by the mesh at first use
storage-media.mount loaded inactive dead media (192.0.2.10:/storage/media), mounted by the mesh
`
func TestListReadsTheRootMountsUnit(t *testing.T) {
f := newFake(t)
f.file("/etc/fstab", "UUID=aaaa / ext4 rw,relatime 0 1\n")
f.file("/proc/self/mountinfo", "22 1 259:3 / / rw,relatime shared:1 - ext4 /dev/nvme0n1p3 rw\n")
f.answers["systemctl list-units --type=mount,automount --all --no-legend --plain --no-pager"] = desktopListUnits
names := strings.Join(ParseListUnits(desktopListUnits), " ")
if !strings.HasPrefix(names, "-.mount ") {
t.Fatalf("the root mount's unit was not listed: %q", names)
}
f.answers["systemctl show -p "+ShowProperties+" --no-pager -- "+names] =
"Id=-.mount\nWhere=/\nWhat=/dev/nvme0n1p3\nType=ext4\nOptions=rw\nResult=success\nActiveState=active\nSubState=mounted\n" +
"FragmentPath=/run/systemd/generator/-.mount\nSourcePath=/etc/fstab\nUnitFileState=generated\n\n" +
"Id=storage-media.automount\nWhere=/storage/media\nResult=success\nActiveState=active\nSubState=waiting\n" +
"FragmentPath=/etc/systemd/system/storage-media.automount\nUnitFileState=enabled\n"
r := f.machine().List(false, "")
if len(r.Notes) != 0 {
t.Fatalf("the units' properties must be read, the root mount's included: %q", r.Notes)
}
got := map[string]Mount{}
for _, mt := range r.Mounts {
got[mt.Mountpoint] = mt
}
if root := got["/"]; root.MountUnit == nil || root.MountUnit.Unit != "-.mount" {
t.Errorf("/ must carry its unit -.mount: %+v", root.MountUnit)
}
if media := got["/storage/media"]; media.Automount == nil {
t.Errorf("/storage/media must carry its automount: %+v", media)
}
}
func TestListNarrowed(t *testing.T) {
m := listFake(t).machine()
if r := m.List(true, ""); len(r.Mounts) != 3 {
t.Errorf("network only: got %d mounts, want 3: %v", len(r.Mounts), r.Said)
}
if r := m.List(false, "192.0.2.20"); len(r.Mounts) != 1 || r.Mounts[0].Mountpoint != "/mnt/my share" {
t.Errorf("match on source: %v", r.Said)
}
}
func TestListWithoutFstabSaysSo(t *testing.T) {
f := newFake(t)
f.file("/proc/self/mountinfo", sampleMountinfo)
r := f.machine().List(false, "")
notes := strings.Join(r.Notes, "\n")
if !strings.Contains(notes, "/etc/fstab: not present") || !strings.Contains(notes, "units were not read") {
t.Errorf("a file or command that could not be read must be said: %q", notes)
}
}