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

330 lines
14 KiB
Go

package main
// The review of 2026-10-08 (mesh-catalog #136): each finding reproduced by a test before it was fixed.
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// dataAnswer is mesh-controller.data's answer for this machine, as the bundle leaves it for the process.
func (f *fakeSystem) dataPaths(paths ...string) {
f.t.Helper()
if paths == nil {
paths = []string{}
}
raw, _ := json.Marshal(DataPaths{Machine: "workstation", Asked: time.Now().Format(time.RFC3339), Paths: paths})
f.write(DataFile, string(raw))
}
func shareOf(st Status, name string) ShareStatus {
for _, s := range st.Shares {
if s.Name == name {
return s
}
}
return ShareStatus{}
}
const libraryOnly = "shares=library=/srv/library\nserver=homeserver\nat=192.0.2.10\nnode=workstation\n"
// Finding 8: a setting entry that cannot be read removed the module's pair for a mount point it had named
// before, because only what parsed counted as named.
func TestAnUnreadableEntryRemovesNoMount(t *testing.T) {
f := newSystem(t)
f.dataPaths()
f.write(ExportsFile, exportsAnswer(t, library))
f.write(filepath.Join(UnitDir, "srv-library.mount"), Marker+"\n[Mount]\nWhat=192.0.2.10:/tank/media\nWhere=/srv/library\n")
f.write(filepath.Join(UnitDir, "srv-library.automount"), Marker+"\n[Automount]\nWhere=/srv/library\n")
// The operator meant library=/srv/library and wrote it without its slash.
f.write(ConfigFile, "shares=library=srv/library\nserver=homeserver\nat=192.0.2.10\nnode=workstation\n")
st := f.applier(time.Now()).Look(context.Background())
if _, err := os.Stat(filepath.Join(f.root, UnitDir, "srv-library.mount")); err != nil {
t.Fatal("an entry that cannot be read removes no pair")
}
if f.called("systemctl stop srv-library.mount") || f.called("systemctl disable --now srv-library.automount") {
t.Errorf("nothing is stopped: %q", f.calls)
}
said := strings.Join(st.Problems, " ")
if !strings.Contains(said, "srv/library") || !strings.Contains(said, "nothing is removed") {
t.Errorf("the bad entry and what is kept are said: %q", st.Problems)
}
}
// Finding 4a: a mount point already mounted by something else, or a folder with files in it, was taken.
func TestAMountPointInUseOrNotEmptyIsRefused(t *testing.T) {
f := newSystem(t)
f.dataPaths()
f.write(ConfigFile, "shares=library=/srv/library downloads=/srv/downloads\nserver=homeserver\nat=192.0.2.10\nnode=workstation\n")
f.write(ExportsFile, exportsAnswer(t, library, downloads))
// Something else is mounted at the library's mount point; the downloads' folder holds a file.
f.write("/proc/self/mountinfo", "36 1 8:2 / /srv/library rw,relatime shared:1 - ext4 /dev/sdb1 rw\n")
f.write("/srv/downloads/notes.txt", "the operator's")
st := f.applier(time.Now()).Look(context.Background())
for _, name := range []string{"library", "downloads"} {
s := shareOf(st, name)
if s.State != StateFailed {
t.Errorf("%s: %+v", name, s)
}
if f.called("systemctl enable --now srv-" + name + ".automount") {
t.Errorf("%s is not armed", name)
}
if _, err := os.Stat(filepath.Join(f.root, UnitDir, "srv-"+name+".mount")); !os.IsNotExist(err) {
t.Errorf("%s: no unit is written", name)
}
}
if s := shareOf(st, "library"); !strings.Contains(s.Why, "/dev/sdb1") {
t.Errorf("the library says what is mounted there: %q", s.Why)
}
if s := shareOf(st, "downloads"); !strings.Contains(s.Why, "not empty") {
t.Errorf("the downloads say the folder is not empty: %q", s.Why)
}
if f.read("/srv/downloads/notes.txt") != "the operator's" {
t.Error("the operator's file is untouched")
}
}
// Its own share, mounted by its own unit, is not "in use by something else".
func TestTheModulesOwnMountIsNotRefused(t *testing.T) {
f := newSystem(t)
f.dataPaths()
f.write(ConfigFile, libraryOnly)
f.write(ExportsFile, exportsAnswer(t, library))
f.write("/proc/self/mountinfo", "40 1 0:50 / /srv/library rw - autofs systemd-1 rw\n"+
"41 40 0:51 / /srv/library rw - nfs4 192.0.2.10:/tank/media rw\n")
f.write(filepath.Join(UnitDir, "srv-library.mount"), Marker+"\n")
f.write(filepath.Join(UnitDir, "srv-library.automount"), Marker+"\n")
f.reachable["192.0.2.10:2049"] = true
st := f.applier(time.Now()).Look(context.Background())
if s := shareOf(st, "library"); s.State != StateMounted {
t.Errorf("library %+v", s)
}
}
// Finding 4b: a share the machine serves itself was mounted onto itself.
func TestAShareThisMachineServesIsRefused(t *testing.T) {
f := newSystem(t)
f.dataPaths()
f.write(ConfigFile, "shares=library=/srv/library\nserver=homeserver\nat=192.0.2.10\nnode=homeserver\n")
f.write(ExportsFile, exportsAnswer(t, library))
st := f.applier(time.Now()).Look(context.Background())
s := shareOf(st, "library")
if s.State != StateFailed || !strings.Contains(s.Why, "this machine shares it") {
t.Errorf("library %+v", s)
}
if _, err := os.Stat(filepath.Join(f.root, UnitDir, "srv-library.mount")); !os.IsNotExist(err) {
t.Error("no unit is written")
}
}
// Finding 4c: removing a pair stopped its .mount even when what was mounted there was not its source.
func TestAMountThatIsNotItsOwnSourceIsNeverStopped(t *testing.T) {
f := newSystem(t)
f.dataPaths()
f.write(ConfigFile, "shares=none\nnode=workstation\n")
f.write(filepath.Join(UnitDir, "srv-old.mount"), Marker+"\n[Mount]\nWhat=192.0.2.10:/tank/old\nWhere=/srv/old\nType=nfs4\n")
f.write(filepath.Join(UnitDir, "srv-old.automount"), Marker+"\n[Automount]\nWhere=/srv/old\n")
f.write("/proc/self/mountinfo", "36 1 8:2 / /srv/old rw - ext4 /dev/sdb1 rw\n")
st := f.applier(time.Now()).Look(context.Background())
if f.called("systemctl stop srv-old.mount") || f.called("systemctl disable --now srv-old.automount") {
t.Errorf("a mount that is not its own source is not stopped: %q", f.calls)
}
if _, err := os.Stat(filepath.Join(f.root, UnitDir, "srv-old.mount")); err != nil {
t.Error("and its pair is kept")
}
if !strings.Contains(strings.Join(st.Problems, " "), "/dev/sdb1") {
t.Errorf("said: %q", st.Problems)
}
// The verb unmount refuses the same.
f.write(StatusFile, `{"shares":[{"name":"old","kind":"nfs","mountpoint":"/srv/old","what":"192.0.2.10:/tank/old","state":"mounted","mount_unit":"srv-old.mount","automount_unit":"srv-old.automount"}]}`)
f.calls = nil
_, err := callVerb(t, f.machine(), "unmount", map[string]any{"name": "old"})
if err == nil || !strings.Contains(err.Error(), "/dev/sdb1") || f.called("systemctl stop") {
t.Errorf("unmount refuses: %v %q", err, f.calls)
}
}
// Finding 4d: a mount point at, under or above data the mesh keeps on this machine was taken.
func TestAMountPointNearTheMeshsDataIsRefused(t *testing.T) {
cases := map[string]string{
"/mnt/plex": "above",
"/mnt/plex/data": "at",
"/mnt/plex/data/x": "under",
}
for point, how := range cases {
f := newSystem(t)
f.dataPaths("/mnt/plex/data", "/mnt/plex/config")
f.write(ConfigFile, "shares=library="+point+"\nserver=homeserver\nat=192.0.2.10\nnode=workstation\n")
f.write(ExportsFile, exportsAnswer(t, library))
st := f.applier(time.Now()).Look(context.Background())
s := shareOf(st, "library")
if s.State != StateFailed || !strings.Contains(s.Why, "/mnt/plex/data") || !strings.Contains(s.Why, how) {
t.Errorf("%s: %+v", point, s)
}
if f.called("systemctl enable") {
t.Errorf("%s is not armed", point)
}
}
}
// When what the mesh keeps here cannot be read, nothing new is set up; what exists stays.
func TestWithoutTheMeshsDataNothingNewIsSetUp(t *testing.T) {
f := newSystem(t)
os.Remove(filepath.Join(f.root, DataFile))
f.write(ConfigFile, libraryOnly)
f.write(ExportsFile, exportsAnswer(t, library))
st := f.applier(time.Now()).Look(context.Background())
if _, err := os.Stat(filepath.Join(f.root, UnitDir, "srv-library.mount")); !os.IsNotExist(err) {
t.Error("nothing new is written without the data list")
}
if s := shareOf(st, "library"); s.State != StateFailed || !strings.Contains(s.Why, "data") {
t.Errorf("library %+v", s)
}
// A pair that is already the module's stays and stays armed.
f.write(filepath.Join(UnitDir, "srv-library.mount"), Marker+"\n")
f.write(filepath.Join(UnitDir, "srv-library.automount"), Marker+"\n")
f.applier(time.Now()).Look(context.Background())
if _, err := os.Stat(filepath.Join(f.root, UnitDir, "srv-library.mount")); err != nil {
t.Error("an existing pair stays")
}
}
// The bundle asks the controller for this machine's data and leaves the paths for the process.
func TestTheBundleLeavesTheMeshsDataForTheProcess(t *testing.T) {
f := newSystem(t)
f.write(ConfigFile, libraryOnly)
answer := json.RawMessage(`{"answer":{"data":[{"machine":"workstation","module":"plex","item":"data","path":"/mnt/plex/data"}]},"ok":true}`)
paths, err := DataFrom(answer)
if err != nil || len(paths) != 1 || paths[0] != "/mnt/plex/data" {
t.Fatalf("paths %q %v", paths, err)
}
// The controller's verbs also answer as text in output.
paths, err = DataFrom(json.RawMessage(`{"ok":true,"output":"{\"data\":[{\"path\":\"/srv/x\"}]}"}`))
if err != nil || len(paths) != 1 || paths[0] != "/srv/x" {
t.Fatalf("paths from output %q %v", paths, err)
}
if _, err := DataFrom(json.RawMessage(`{"ok":false}`)); err == nil {
t.Error("an answer with no data list is an error, not an empty list")
}
}
// Finding 5: the server exports a share only to the nodes that want it, so the bundle says what this node
// wants of which server, also before the process has looked.
func TestTheBundleSaysWhatThisNodeWants(t *testing.T) {
f := newSystem(t)
f.write(ConfigFile, "shares=library=/srv/library downloads=/srv/downloads:ro\nserver=homeserver\nat=192.0.2.10\nnode=workstation\n")
var put []any
last := ""
ask := func(string) (json.RawMessage, error) { return json.RawMessage(`{"shares":[]}`), nil }
f.machine().Share("workstation", ask, func(_ string, v any) error { put = append(put, v); return nil }, &last, time.Now())
if len(put) != 1 {
t.Fatalf("the wants are put before the process has looked: %d", len(put))
}
raw, _ := json.Marshal(put[0])
var got struct {
Wants []Want `json:"wants"`
}
json.Unmarshal(raw, &got)
want := []Want{{Server: "homeserver", Share: "downloads", Access: "ro"}, {Server: "homeserver", Share: "library", Access: "rw"}}
if len(got.Wants) != 2 || got.Wants[0] != want[0] || got.Wants[1] != want[1] {
t.Errorf("wants %s", raw)
}
}
// Finding 9: the seat passes confirm as the string "true".
func TestConfirmIsTrueAsABooleanOrAString(t *testing.T) {
for _, v := range []any{true, "true"} {
if !flag(map[string]any{"confirm": v}, "confirm") {
t.Errorf("%#v is a confirmation", v)
}
}
for _, v := range []any{false, "false", "yes", "1", nil} {
if flag(map[string]any{"confirm": v}, "confirm") {
t.Errorf("%#v is not a confirmation", v)
}
}
}
// Low: a password hidden in user=name%pass, and one holding a comma written as two (",,").
func TestEveryWayAPasswordIsWrittenIsRedacted(t *testing.T) {
for _, opts := range []string{
"user=bob%" + testPassword + ",vers=3.0",
"username=bob%" + testPassword,
"password=" + testPassword + ",,tail,vers=3.0",
"password=,,," + testPassword,
"pass=" + testPassword,
} {
if got := RedactOptions(opts); strings.Contains(got, testPassword) || strings.Contains(got, "tail") {
t.Errorf("RedactOptions(%q) = %q", opts, got)
}
line := "//192.0.2.76/Share /mnt/games cifs " + opts + " 0 0"
if got := RedactLine(line); strings.Contains(got, testPassword) || strings.Contains(got, "tail") {
t.Errorf("RedactLine(%q) = %q", line, got)
}
}
if got := RedactOptions("user=bob,vers=3.0"); got != "user=bob,vers=3.0" {
t.Errorf("a user name alone is kept: %q", got)
}
}
func callVerb(t *testing.T, m *Machine, name string, args map[string]any) (any, error) {
t.Helper()
for _, tool := range Verbs(m) {
if tool.Name == Seat+"."+name {
return tool.Run(args)
}
}
t.Fatalf("no verb %s", name)
return nil, nil
}
// The bundle asks the controller, leaves the paths, and keeps the last ones when an ask fails.
func TestTheBundleKeepsTheLastDataWhenAnAskFails(t *testing.T) {
f := newSystem(t)
m := f.machine()
var asked []string
ok := func(key string, body any) (json.RawMessage, error) {
raw, _ := json.Marshal(body)
asked = append(asked, key+" "+string(raw))
return json.RawMessage(`{"answer":{"data":[{"path":"/mnt/plex/data"}]}}`), nil
}
if said := m.LeaveData("workstation", ok, time.Now()); len(said) != 0 {
t.Fatalf("said %q", said)
}
if len(asked) != 1 || asked[0] != `seat:mesh-controller.data {"machine":"workstation"}` {
t.Errorf("asked %q", asked)
}
failing := func(string, any) (json.RawMessage, error) { return nil, os.ErrDeadlineExceeded }
said := m.LeaveData("workstation", failing, time.Now())
d, err := readJSON[DataPaths](m, DataFile)
if err != nil || len(d.Paths) != 1 || d.Paths[0] != "/mnt/plex/data" || d.Error == "" || len(said) == 0 {
t.Errorf("a failed ask keeps the last paths and says why: %+v %v %q", d, err, said)
}
}
// The server decides each node's access (finding 5): a share it exports read-write may reach this node
// read-only, and a share it exports to other nodes only does not reach this one at all. The mount takes
// the access the server gave this node, never the share's own.
func TestTheServersAccessForThisNodeIsTheMountsAccess(t *testing.T) {
library := ServerShare{Name: "library", Path: "/srv/library", Access: "rw", Live: true,
To: []ShareClient{{Node: "ws", Address: "10.0.0.3", Access: "rw"}, {Node: "laptop", Address: "10.0.0.4", Access: "ro"}}}
got, ok := library.ForNode("laptop")
if !ok || got.Access != "ro" {
t.Errorf("the laptop, granted read-only, gets %q (exported to it: %v)", got.Access, ok)
}
if p := NFSPair(Wanted{Name: "library", Mountpoint: "/srv/library"}, "192.0.2.10", got); !strings.Contains(p.Mount, "Options=ro,") {
t.Errorf("a read-only grant mounted read-write:\n%s", p.Mount)
}
if got, ok := library.ForNode("ws"); !ok || got.Access != "rw" {
t.Errorf("the workstation, granted read-write, gets %q (%v)", got.Access, ok)
}
if _, ok := library.ForNode("control"); ok {
t.Errorf("a node the server does not export to is treated as exported to")
}
}