After the operator removes by hand what the last search found, no apply says so and nothing searched again until the next day. node setuid-search <node> signs the ask as a hand-over is, under its own context, on a subject only the node's engine hears and only the controller may publish.
170 lines
7.6 KiB
Go
170 lines
7.6 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// A hand-over's line is judged before anything is asked (novox/hq issue 356): a node's name and the directory's
|
|
// absolute path exactly as the engine states it — no `..`, no doubled or trailing separator, nothing relative.
|
|
func TestAHandOverLineIsJudgedBeforeItIsAsked(t *testing.T) {
|
|
for _, args := range [][]string{
|
|
{},
|
|
{"laptop"},
|
|
{"laptop", "/srv/notes", "extra"},
|
|
{"", "/srv/notes"},
|
|
{"--node", "/srv/notes"},
|
|
{"laptop", "srv/notes"},
|
|
{"laptop", "/srv/../etc"},
|
|
{"laptop", "/srv//notes"},
|
|
{"laptop", "/srv/notes/"},
|
|
{"laptop", "/srv/notes/."},
|
|
} {
|
|
if _, _, err := handOverLine(args); err == nil {
|
|
t.Errorf("%q was taken", args)
|
|
}
|
|
}
|
|
node, path, err := handOverLine([]string{"laptop", "/srv/notes"})
|
|
if err != nil || node != "laptop" || path != "/srv/notes" {
|
|
t.Fatalf("read as %q %q %v", node, path, err)
|
|
}
|
|
}
|
|
|
|
// Who hands over is the line's caller in the words every verb's caller is recorded in — the operator through
|
|
// mesh-cli — or the controller's terminal when nobody is named.
|
|
func TestAHandOverNamesWhoAsked(t *testing.T) {
|
|
t.Setenv(link.CallerVar, " jo through mesh-cli on anchor ")
|
|
if by := handOverBy(); by != "jo through mesh-cli on anchor" {
|
|
t.Fatalf("by %q", by)
|
|
}
|
|
t.Setenv(link.CallerVar, "")
|
|
if by := handOverBy(); by != "the controller's terminal" {
|
|
t.Fatalf("by %q", by)
|
|
}
|
|
}
|
|
|
|
// **A hand-over is the controller's terminal's alone** (novox/hq issue 356, ADR 0266): through any verb, and
|
|
// through mesh-cli outside the terminal, `node hand-over` is refused and nothing runs — at the next apply root
|
|
// gives the directory to the account the module declares, and whoever may call a verb includes agents.
|
|
func TestAHandOverIsRefusedThroughEveryVerb(t *testing.T) {
|
|
line := []string{"node", "hand-over", "laptop", "/srv/notes"}
|
|
if err := terminalOnly(line); err == nil {
|
|
t.Fatal("node hand-over passed as a verb's line")
|
|
}
|
|
if _, err := argvFor("command", map[string]any{"command": "node hand-over laptop /srv/notes"}); err == nil {
|
|
t.Fatal("the command verb composed node hand-over")
|
|
}
|
|
if _, err := ordinaryLine(line); err == nil {
|
|
t.Fatal("node hand-over composed as an ordinary mesh-cli line")
|
|
}
|
|
if _, err := argvFor("node", map[string]any{"node": "laptop", "hand-over": "/srv/notes"}); err == nil {
|
|
t.Fatal("the node verb composed a hand-over")
|
|
}
|
|
}
|
|
|
|
// The condition's words are plain and name no machine's binary; the operator's line, with the node and the path
|
|
// (novox/hq ADR 0272), is in the summary the module's health gives (moduleHealthWord) and in the evidence.
|
|
func TestTheUsedAsFoundConditionNamesTheOperatorsLine(t *testing.T) {
|
|
rs := []inventory.ResourceHealth{foundDirectory("notes", time.Now().Add(-24*time.Hour))}
|
|
o := usedAsFoundObservation("notes", "laptop", "notes on laptop uses notes.data as found", rs)
|
|
if strings.Contains(o.Needs, "mesh-host") || strings.Contains(o.Explanation, "mesh-host") ||
|
|
!strings.Contains(o.Needs, "control-node") {
|
|
t.Fatalf("the condition's words: %q %q", o.Needs, o.Explanation)
|
|
}
|
|
if why, ok := conditions.PlainWords(conditions.Words{Headline: o.Headline, Explanation: o.Explanation, Needs: o.Needs,
|
|
Resolved: o.Resolved}, "laptop"); !ok {
|
|
t.Fatalf("not plain: %s", why)
|
|
}
|
|
f := gateFacts{now: time.Now(), health: map[string]inventory.NodeHealth{"laptop": {Node: "laptop", HeardAt: time.Now(),
|
|
Resources: rs}}}
|
|
h, why := moduleHealthWord("notes", "laptop", time.Now().Add(-time.Hour), f)
|
|
if h != healthPerson || !strings.Contains(why, "`nox node hand-over laptop <directory>` on the control-node") ||
|
|
strings.Contains(why, "mesh-host") || strings.Contains(why, "/srv/") {
|
|
t.Fatalf("the module's health reads %v %q; want the operator's line", h, why)
|
|
}
|
|
}
|
|
|
|
// **Nothing is asked of a node the mesh does not know, and the engine's refusal is the command's failure**
|
|
// (review of issue 356): a refused hand-over never exits as a success.
|
|
func TestAHandOverAsksOnlyAKnownNodeAndFailsOnARefusal(t *testing.T) {
|
|
t.Setenv(link.CallerVar, "jo through mesh-cli on anchor")
|
|
asked := 0
|
|
ask := func(answer link.HandOverAnswer) func(node, path, by string) (link.HandOverAnswer, error) {
|
|
return func(node, path, by string) (link.HandOverAnswer, error) {
|
|
asked++
|
|
if node != "laptop" || path != "/srv/notes" || by != "jo through mesh-cli on anchor" {
|
|
t.Fatalf("asked %q %q %q", node, path, by)
|
|
}
|
|
return answer, nil
|
|
}
|
|
}
|
|
unknown := func(string) error { return errors.New("no node called laptop") }
|
|
known := func(string) error { return nil }
|
|
var out bytes.Buffer
|
|
err := handOverAsked([]string{"laptop", "/srv/notes"}, unknown, ask(link.HandOverAnswer{Said: "x"}), &out)
|
|
if err == nil || asked != 0 || !strings.Contains(err.Error(), "nothing was asked") {
|
|
t.Fatalf("an unknown node: %v, asked %d", err, asked)
|
|
}
|
|
err = handOverAsked([]string{"laptop", "/srv/../etc"}, known, ask(link.HandOverAnswer{Said: "x"}), &out)
|
|
if err == nil || asked != 0 {
|
|
t.Fatalf("a refused line was asked: %v, asked %d", err, asked)
|
|
}
|
|
err = handOverAsked([]string{"laptop", "/srv/notes"}, known,
|
|
ask(link.HandOverAnswer{Refused: "/srv/notes is not used as found; nothing was handed over"}), &out)
|
|
if err == nil || !strings.Contains(err.Error(), "laptop refused: /srv/notes is not used as found") || out.Len() != 0 {
|
|
t.Fatalf("a refusal: %v, printed %q", err, out.String())
|
|
}
|
|
err = handOverAsked([]string{"laptop", "/srv/notes"}, known, ask(link.HandOverAnswer{Said: "handed over"}), &out)
|
|
if err != nil || !strings.HasPrefix(out.String(), "handed over\n") || !strings.Contains(out.String(), "`nox push laptop`") {
|
|
t.Fatalf("a record: %v, printed %q", err, out.String())
|
|
}
|
|
failing := func(string, string, string) (link.HandOverAnswer, error) {
|
|
return link.HandOverAnswer{}, errors.New("no engine")
|
|
}
|
|
if err := handOverAsked([]string{"laptop", "/srv/notes"}, known, failing, &out); err == nil {
|
|
t.Fatal("an ask that failed was a success")
|
|
}
|
|
}
|
|
|
|
// The setuid search's line asks only a known node, one name and nothing else, and fails on a refusal
|
|
// (novox/hq issue 361).
|
|
func TestASetuidSearchAsksOnlyAKnownNodeAndFailsOnARefusal(t *testing.T) {
|
|
t.Setenv(link.CallerVar, "jo through mesh-cli on anchor")
|
|
asked := 0
|
|
ask := func(answer link.HandOverAnswer) func(node, by string) (link.HandOverAnswer, error) {
|
|
return func(node, by string) (link.HandOverAnswer, error) {
|
|
asked++
|
|
if node != "novox" || by != "jo through mesh-cli on anchor" {
|
|
t.Fatalf("asked %q %q", node, by)
|
|
}
|
|
return answer, nil
|
|
}
|
|
}
|
|
known := func(string) error { return nil }
|
|
var out bytes.Buffer
|
|
for _, args := range [][]string{nil, {"novox", "extra"}, {"-x"}} {
|
|
if err := setuidSearchAsked(args, known, ask(link.HandOverAnswer{Said: "x"}), &out); err == nil || asked != 0 {
|
|
t.Fatalf("%v was asked: %v", args, err)
|
|
}
|
|
}
|
|
if err := setuidSearchAsked([]string{"novox"}, func(string) error { return errors.New("no node called novox") },
|
|
ask(link.HandOverAnswer{Said: "x"}), &out); err == nil || asked != 0 {
|
|
t.Fatalf("an unknown node: %v", err)
|
|
}
|
|
if err := setuidSearchAsked([]string{"novox"}, known, ask(link.HandOverAnswer{Refused: "no; no search was started"}),
|
|
&out); err == nil || !strings.Contains(err.Error(), "novox refused") || out.Len() != 0 {
|
|
t.Fatalf("a refusal: %v, printed %q", err, out.String())
|
|
}
|
|
if err := setuidSearchAsked([]string{"novox"}, known, ask(link.HandOverAnswer{Said: "a new search starts"}),
|
|
&out); err != nil || !strings.HasPrefix(out.String(), "a new search starts\n") {
|
|
t.Fatalf("a start: %v, printed %q", err, out.String())
|
|
}
|
|
}
|