Files
mesh-host/cmd/mesh-host/found_test.go
T
jochen fc6c5a92e6
mesh/merge-gate pass: builds mesh-host → ace, g14, novox, 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
mesh/delivery superseded: a newer head of the same pull request
Believe a hand-over only signed by the mesh, and give it through no link (hq issue 356, review)
Two holes the review proved. The ask's subject proved nothing — a responder can reach it through a reply — so the engine now verifies the mesh's signature over a fixed context with the key it verifies declarations with, before reading anything, and refuses an ask for another node, expired, good too long, without a nonce, or replayed. And the apply followed a link the hand-over had refused: a component swapped for a link after the record carried root's chmod and chown to its target. A handed-over directory must now still resolve to itself, and is changed only through one descriptor opened from / with no link followed (fchmod, fchown). Tests for the reply guard, the run loop's hook, the fallback renewal on proof and a non-regular fallback; 27 checks each removed in turn, each failing a test.
2026-10-09 19:45:00 +02:00

178 lines
7.5 KiB
Go

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 <directory>` 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)
}
foundNode.Store("")
if r := withFoundDirectories(&link.Health{}, foundDirectories(known)).Resources[0]; !strings.Contains(r.Reason,
"`nox node hand-over <node> <directory>`") {
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)
}
}