package main import ( "bytes" "os" "path/filepath" "strings" "testing" "time" "github.com/novox/mesh-host/internal/apply" "github.com/novox/mesh-host/internal/link" "github.com/novox/mesh-host/internal/store" ) // Every directory used as found is its module's resource of kind directory, unhealthy, naming its owner, on // every statement (novox/hq issue 339): the controller raises that as the module's condition. The way out it // names is the operator's line, with this node's name (novox/hq ADR 0272, issue 356). func TestAFoundDirectoryIsStatedUnhealthy(t *testing.T) { dir := t.TempDir() known := store.State{Resources: []store.Applied{ {ID: "notes.data", Type: "directory", Target: dir, AsFound: true, Declared: "root, mode 755", AppliedAt: time.Now()}, {ID: "notes.cache", Type: "directory", Target: dir}, }} foundNode.Store("laptop") t.Cleanup(func() { foundNode.Store("") }) h := withFoundDirectories(&link.Health{}, foundDirectories(known)) if len(h.Resources) != 1 { t.Fatalf("stated: %+v", h.Resources) } r := h.Resources[0] if r.Module != "notes" || r.Kind != link.KindDirectory || r.State != link.StateUnhealthy || r.Target != dir || !strings.HasPrefix(r.Reason, link.ReasonUsedAsFound) || !strings.Contains(r.Reason, "owned by ") || !strings.Contains(r.Reason, "root, mode 755") { t.Fatalf("stated as: %+v", r) } // The path is the target, kept as evidence, and never in the reason: the operator's channel withholds a path // (novox/hq ADR 0234 §6), and the controller says the reason there. if !strings.Contains(r.Reason, "`nox node hand-over laptop ` on the control-node") || strings.Contains(r.Reason, "mesh-host") || strings.Contains(r.Reason, dir) { t.Fatalf("the way out is not the operator's line, or carries the path: %q", r.Reason) } // ADR 0563 §5: from the lease write the remedy is the node seat's hand-over, answered on the operator's channel, and // no longer the controller's terminal line; the lease is the one the engine hears. handed := newMeshTrust(nil) handed.handedAway = true foundTrust.Store(handed) if r := withFoundDirectories(&link.Health{}, foundDirectories(known)).Resources[0]; !strings.Contains(r.Reason, "`laptop/node-engine.hand-over`") || strings.Contains(r.Reason, "nox node hand-over") || strings.Contains(r.Reason, dir) { t.Fatalf("after the lease write the way out is %q", r.Reason) } foundTrust.Store(newMeshTrust(nil)) if r := withFoundDirectories(&link.Health{}, foundDirectories(known)).Resources[0]; !strings.Contains(r.Reason, "`nox node hand-over laptop `") { t.Fatalf("while the controller holds its lease the way out is %q", r.Reason) } foundTrust.Store((*meshTrust)(nil)) foundNode.Store("") if r := withFoundDirectories(&link.Health{}, foundDirectories(known)).Resources[0]; !strings.Contains(r.Reason, "`nox node hand-over `") { t.Fatalf("before enrolment the line names a placeholder: %q", r.Reason) } } // realDir is a temporary directory with every link in its path resolved, so a judgement of links is about the // links the test makes. func realDir(t *testing.T) string { t.Helper() dir, err := filepath.EvalSymlinks(t.TempDir()) if err != nil { t.Fatal(err) } return dir } // A hand-over records a directory used as found, and refuses one that is not — through the engine's own // subcommand and through the controller's ask alike, which are one judgement. func TestHandOverRecordsOnlyAFoundDirectory(t *testing.T) { dir := realDir(t) statePath := filepath.Join(dir, "state.json") found := filepath.Join(dir, "found") if err := os.Mkdir(found, 0o700); err != nil { t.Fatal(err) } if err := os.Mkdir(filepath.Join(dir, "mine"), 0o755); err != nil { t.Fatal(err) } if err := store.Save(statePath, store.State{Resources: []store.Applied{ {ID: "notes.data", Type: "directory", Target: found, AsFound: true, Declared: "root, mode 755"}, {ID: "notes.mine", Type: "directory", Target: filepath.Join(dir, "mine")}, }}); err != nil { t.Fatal(err) } var out bytes.Buffer if err := handOverCommand(options{state: statePath, file: filepath.Join(dir, "mine"), out: &out}); err == nil { t.Fatal("a directory the mesh made was handed over") } if err := handOverCommand(options{state: statePath, file: found + "/", out: &out}); err != nil { t.Fatal(err) } all, err := store.LoadHandedOver(statePath) if err != nil || len(all) != 1 || all[0].Path != found || !strings.HasSuffix(all[0].By, " at the machine") { t.Fatalf("recorded: %+v %v", all, err) } handedOverFrom(statePath) if !apply.HandedOver()[apply.HandOverKey("notes.data", found)] { t.Fatal("the apply does not see the hand-over") } if !strings.Contains(out.String(), "root, mode 755") { t.Fatalf("said: %s", out.String()) } // The controller's ask is the same judgement, recording who asked in the controller's words. _ = os.Remove(store.HandedOverPath(statePath)) said, err := handOverFrom(statePath)(found, "jo through mesh-cli on anchor") if err != nil || !strings.Contains(said, "by jo through mesh-cli on anchor") || !strings.Contains(said, "root, mode 755") { t.Fatalf("the ask answered %q, %v", said, err) } all, _ = store.LoadHandedOver(statePath) if len(all) != 1 || all[0].By != "jo through mesh-cli on anchor" || all[0].ID != "notes.data" { t.Fatalf("recorded: %+v", all) } } // Every value of a hand-over is judged before anything is recorded, and each refusal records nothing: a relative // path, a path that is not the engine's statement of the directory (`..`, a doubled or trailing separator), one // not used as found, a link at the path, a link in the path, a file, a directory that is gone. func TestAHandOverIsRefusedForEveryPathThatIsNotTheFoundDirectory(t *testing.T) { dir := realDir(t) statePath := filepath.Join(dir, "state.json") found := filepath.Join(dir, "found") if err := os.Mkdir(found, 0o700); err != nil { t.Fatal(err) } // A link at the path of a second found directory, and a found directory below a linked directory. linked := filepath.Join(dir, "linked") if err := os.Symlink(found, linked); err != nil { t.Fatal(err) } through := filepath.Join(dir, "via", "data") if err := os.Symlink(dir, filepath.Join(dir, "via")); err != nil { t.Fatal(err) } if err := os.Mkdir(filepath.Join(dir, "data"), 0o700); err != nil { t.Fatal(err) } file := filepath.Join(dir, "file") if err := os.WriteFile(file, nil, 0o600); err != nil { t.Fatal(err) } gone := filepath.Join(dir, "gone") if err := store.Save(statePath, store.State{Resources: []store.Applied{ {ID: "notes.data", Type: "directory", Target: found, AsFound: true}, {ID: "notes.linked", Type: "directory", Target: linked, AsFound: true}, {ID: "notes.through", Type: "directory", Target: through, AsFound: true}, {ID: "notes.file", Type: "directory", Target: file, AsFound: true}, {ID: "notes.gone", Type: "directory", Target: gone, AsFound: true}, }}); err != nil { t.Fatal(err) } // Each refused by its own check, which the reason names. refused := map[string][2]string{ "a relative path": {"found", "not one"}, "a path with ..": {dir + "/x/../found", "as the engine states it"}, "a doubled separator": {dir + "//found", "as the engine states it"}, "a trailing separator": {found + "/", "as the engine states it"}, "a directory not used as found": {filepath.Join(dir, "other"), "not a directory this machine uses as found"}, "a link at the path": {linked, "is a link"}, "a link in the path": {through, "reached through a link"}, "a file": {file, "is not a directory on this machine"}, "a directory that is gone": {gone, "cannot be read on this machine"}, } for name, c := range refused { said, err := handOverFound(statePath, c[0], "jo") if err == nil || said != "" { t.Fatalf("%s (%s) was handed over: %q", name, c[0], said) } if !strings.Contains(err.Error(), "nothing was handed over") || !strings.Contains(err.Error(), c[1]) { t.Fatalf("%s's refusal does not say %q and that nothing was handed over: %v", name, c[1], err) } } if all, err := store.LoadHandedOver(statePath); err != nil || len(all) != 0 { t.Fatalf("a refusal recorded something: %+v %v", all, err) } // The found directory itself, stated exactly, is taken. if _, err := handOverFound(statePath, found, "jo"); err != nil { t.Fatal(err) } if all, _ := store.LoadHandedOver(statePath); len(all) != 1 || all[0].Path != found { t.Fatalf("recorded: %+v", all) } }