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) } }