A module's condition told the operator to run the node-engine's hand-over at the machine, as root (issue 339), and the mesh had no channel for it. Now node hand-over <node> <path> is the controller's terminal's — a node subcommand that is not a read, so every verb and mesh-cli outside the terminal refuse it — and asks that node's engine on mesh.node.<node>.ask.hand-over, a request only the controller may publish and only that node's engine may hear and answer (its grant gains the subject and the right to answer what it was asked). The line's node and path are judged before anything is asked; the engine's answer is printed, a refusal as a refusal. Every text addressed to the operator names the nox line (ADR 0272); the condition's words stay plain.
104 lines
3.9 KiB
Go
104 lines
3.9 KiB
Go
package link
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/testbus"
|
|
)
|
|
|
|
// The hand-over's ask and answer hold these field names; the engine's side holds the same list (mesh-host
|
|
// internal/link, TestTheHandOverAskAndAnswerKeepTheirFieldNames).
|
|
func TestTheHandOverAskAndAnswerKeepTheirFieldNames(t *testing.T) {
|
|
body, _ := json.Marshal(HandOverAsk{Path: "/srv/notes", By: "jo through mesh-cli on anchor"})
|
|
if got := keysIn(t, body); got != "by path" {
|
|
t.Fatalf("the ask's fields are %q", got)
|
|
}
|
|
body, _ = json.Marshal(HandOverAnswer{Said: "s", Refused: "r"})
|
|
if got := keysIn(t, body); got != "refused said" {
|
|
t.Fatalf("the answer's fields are %q", got)
|
|
}
|
|
if broker.AskHandOverSubject("laptop") != "mesh.node.laptop.ask.hand-over" {
|
|
t.Fatalf("the subject is %q", broker.AskHandOverSubject("laptop"))
|
|
}
|
|
}
|
|
|
|
// The ask reaches the machine's engine on its own subject and its answer comes back whole: what it recorded, or
|
|
// its refusal as the engine worded it. A machine with no engine listening is said as not answering, and an
|
|
// answer that is neither is refused rather than read as a record.
|
|
func TestAHandOverIsAskedOfTheMachineAndItsAnswerComesBack(t *testing.T) {
|
|
url := testbus.URL(t)
|
|
conn, err := nats.Connect(url)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(conn.Close)
|
|
|
|
var heard HandOverAsk
|
|
engine, err := conn.Subscribe(broker.AskHandOverSubject("laptop"), func(msg *nats.Msg) {
|
|
_ = json.Unmarshal(msg.Data, &heard)
|
|
switch heard.Path {
|
|
case "/srv/notes":
|
|
body, _ := json.Marshal(HandOverAnswer{Said: "/srv/notes (notes.data) is handed to the mesh by " + heard.By})
|
|
_ = msg.Respond(body)
|
|
case "/srv/empty":
|
|
_ = msg.Respond([]byte(`{}`))
|
|
default:
|
|
body, _ := json.Marshal(HandOverAnswer{Refused: heard.Path + " is not a directory this machine uses as found; nothing was handed over"})
|
|
_ = msg.Respond(body)
|
|
}
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = engine.Unsubscribe() })
|
|
|
|
ctx := context.Background()
|
|
a, err := AskHandOver(ctx, conn, "laptop", "/srv/notes", "jo through mesh-cli on anchor", 5*time.Second)
|
|
if err != nil || a.Refused != "" || !strings.Contains(a.Said, "handed to the mesh by jo through mesh-cli on anchor") {
|
|
t.Fatalf("answered %+v, %v", a, err)
|
|
}
|
|
if heard.Path != "/srv/notes" || heard.By != "jo through mesh-cli on anchor" {
|
|
t.Fatalf("the engine heard %+v", heard)
|
|
}
|
|
a, err = AskHandOver(ctx, conn, "laptop", "/srv/other", "jo", 5*time.Second)
|
|
if err != nil || a.Said != "" || !strings.Contains(a.Refused, "nothing was handed over") {
|
|
t.Fatalf("a refusal came back as %+v, %v", a, err)
|
|
}
|
|
if _, err := AskHandOver(ctx, conn, "laptop", "/srv/empty", "jo", 5*time.Second); err == nil ||
|
|
!strings.Contains(err.Error(), "neither") {
|
|
t.Fatalf("an empty answer was taken: %v", err)
|
|
}
|
|
if _, err := AskHandOver(ctx, conn, "anchor", "/srv/notes", "jo", 5*time.Second); err == nil ||
|
|
!strings.Contains(err.Error(), "node-engine") {
|
|
t.Fatalf("a machine with no engine listening: %v", err)
|
|
}
|
|
if _, err := AskHandOver(ctx, nil, "anchor", "/srv/notes", "jo", time.Second); err == nil {
|
|
t.Fatal("asked with no bus")
|
|
}
|
|
}
|
|
|
|
// A machine's grant hears its own hand-over ask and nobody else's, and may answer it: the one request a node is
|
|
// asked (novox/hq issue 356).
|
|
func TestAMachineHearsItsOwnHandOverAskAndMayAnswerIt(t *testing.T) {
|
|
perms, err := broker.PermissionsFor(broker.Principal{Kind: broker.KindNode, Node: "laptop", PasswordHash: "x"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var hears, other bool
|
|
for _, s := range perms.Subscribe {
|
|
hears = hears || s == "mesh.node.laptop.ask.hand-over"
|
|
other = other || s == "mesh.node.anchor.ask.hand-over"
|
|
}
|
|
if !hears || other || !perms.AllowResponses {
|
|
t.Fatalf("a machine's grant: hears its own %v, another's %v, answers %v (%v)", hears, other, perms.AllowResponses,
|
|
perms.Subscribe)
|
|
}
|
|
}
|