package runtime import ( "encoding/json" "strings" "testing" "time" "git.novox.be/novox/mesh-sdk/go/approval" "git.novox.be/novox/mesh-sdk/go/seatdef" "github.com/nats-io/nats.go" "github.com/novox/mesh-tools/node-tools/internal/bus" mt "github.com/novox/mesh-tools/node-tools/internal/meshtest" ) type saidCaller struct { Caller string `json:"caller"` Call string `json:"call"` Arguments map[string]any `json:"arguments"` } // **A module is told who called it, from the subject the call arrived on** (novox/hq issue 365): a call on the // caller-named subject reaches the bundle with that caller in the call's `_meta`, for a module's tool, on its // machine's address, and for a seat's verb; a caller named in the arguments is only an argument. func TestACallOnTheCallerNamedSubjectReachesTheModuleWithItsCaller(t *testing.T) { mesh := mt.New(t) mesh.Issue(t, mt.MembershipOf("ledger", "anchor", true, map[string][]string{"node-desk": {"who"}})) nodeTools := connect(t, "node-tools", "anchor") stop, err := Run(nodeTools, []Served{{"ledger", []string{mt.Fixture("caller-echo.py")}}}, nil, func(string, ...any) {}, deskDefined) if err != nil { t.Fatal(err) } defer stop() // returns once the bundle's exit is handled (novox/hq issue 476) asker, err := bus.Connect(bus.Credential{URL: mt.URL(t), Module: "node-tools", Node: "shanks", User: "shanks.node-tools", Password: "unused"}) if err != nil { t.Fatal(err) } asker.Logf = func(string, ...any) {} defer asker.Close() calls := map[string]bool{} for _, key := range []string{"ledger.who", "ledger.who@anchor", "seat:node-desk.who@anchor"} { var got saidCaller mt.Until(t, func() error { answered, err := asker.Ask(key, map[string]any{"caller": "novox.node-tools", "_meta": map[string]any{"mesh/caller": "novox.controller"}}, "") if err == nil { err = json.Unmarshal(answered.Result, &got) } return err }) if got.Caller != "shanks.node-tools" { t.Errorf("%s reached the module with caller %q, want the asker's bus user", key, got.Caller) } // And the id of the call in flight, one per call, which a ledger's apply names (novox/hq ADR 0597 §7). if got.Call == "" || calls[got.Call] { t.Errorf("%s reached the module with call id %q, want one of its own", key, got.Call) } calls[got.Call] = true if got.Arguments["caller"] != "novox.node-tools" { t.Errorf("%s: the arguments did not reach the module as sent: %v", key, got.Arguments) } } } // deskDefined is the seat list's definition of the fixture seat node-desk, its verb who taking its class's default. var deskDefined = WithSeatDefinitions(func() map[string]seatdef.Definition { return map[string]seatdef.Definition{"node-desk": {Seat: "node-desk", Verbs: []seatdef.Verb{{Name: "who"}}}} }) // The subject that names no caller is still served for a module's tool, for one release (retired by hq issue 464), and says // so: the module is named no caller, whatever the body claims. A seat's verb refuses it before the bundle runs (design 47 G // §610; lead answers A-165, A-179), and refuses every caller while the seat's definition is unread, names no such verb or // opens it to none. func TestTheSubjectThatNamesNoCallerStillWorksAndNamesNone(t *testing.T) { mesh := mt.New(t) mesh.Issue(t, mt.MembershipOf("ledger", "anchor", true, map[string][]string{"node-desk": {"who"}})) nodeTools := connect(t, "node-tools", "anchor") stop, err := Run(nodeTools, []Served{{"ledger", []string{mt.Fixture("caller-echo.py")}}}, nil, func(string, ...any) {}, deskDefined) if err != nil { t.Fatal(err) } defer stop() // returns once the bundle's exit is handled (novox/hq issue 476) old, err := nats.Connect(mt.URL(t)) if err != nil { t.Fatal(err) } defer old.Close() body, _ := json.Marshal(map[string]any{"_meta": map[string]any{"mesh/caller": "novox.controller"}}) for _, subject := range []string{"mesh.mod.ledger.tool.who", "mesh.mod.ledger.tool.who.anchor", "mesh.seat.node-desk.tool.who.anchor"} { var reply struct { Result saidCaller `json:"result"` Error string `json:"error"` } mt.Until(t, func() error { msg, err := old.Request(subject, body, 5*time.Second) if err == nil { err = json.Unmarshal(msg.Data, &reply) } return err }) if strings.HasPrefix(subject, "mesh.seat.") { if !strings.Contains(reply.Error, "node-desk.who takes no unproven caller") || reply.Result.Call != "" { t.Errorf("%s: an unproven caller of a seat's verb was answered %+v (error %q), want refused before the bundle", subject, reply.Result, reply.Error) } continue } if reply.Error != "" || reply.Result.Caller != "" { t.Errorf("%s: the module was named caller %q (error %q), want none", subject, reply.Result.Caller, reply.Error) } } for _, c := range []struct { defs map[string]seatdef.Definition says string }{ {nil, "is not yet read"}, {map[string]seatdef.Definition{"node-desk": {Seat: "node-desk"}}, "names no verb who"}, {map[string]seatdef.Definition{"node-desk": {Seat: "node-desk", Verbs: []seatdef.Verb{{Name: "who", Callers: &seatdef.Callers{List: []string{}}}}}}, "open to no caller"}, {map[string]seatdef.Definition{"node-desk": {Seat: "node-desk", Verbs: []seatdef.Verb{{Name: "who", Class: seatdef.ClassAct}}}}, ""}, } { defs := c.defs err := (&consumers{definitions: func() map[string]seatdef.Definition { return defs }}).admitted("node-desk", "who", "workstation.node-tools") if c.says == "" && err != nil || c.says != "" && (err == nil || !strings.Contains(err.Error(), c.says)) { t.Errorf("a proven caller with the definitions %v: %v, want %q", c.defs, err, c.says) } } // Design 47 G A-249: a bundle's seat call is received by the runner in chunks only for a verb whose definition says `chunks`, // of any machine: a verb a node holds is asked of the node that ran the ask (A-254). t.Run("OnlyAChunkedSeatVerbIsAskedInChunks", func(t *testing.T) { c := &consumers{definitions: func() map[string]seatdef.Definition { return map[string]seatdef.Definition{"mesh-catalog": {Verbs: []seatdef.Verb{{Name: "input", Chunks: true}, {Name: "entry"}}}} }} for key, want := range map[string]bool{ "seat:mesh-catalog.input": true, "seat:mesh-catalog.entry": false, "seat:mesh-catalog.input@home-server": true, "seat:mesh-catalog.entry@home-server": false, "seat:mesh-unread.input": false, "mesh-delivery-indexer.indexer": false, } { if got := c.chunked(key); got != want { t.Errorf("chunked(%s) = %v, want %v", key, got, want) } } if (&consumers{}).chunked("seat:mesh-catalog.input") { t.Error("a runner that holds no seat definitions asks no chunks") } }) // **The tool runner answers a held seat's marked verb by caller and serves the seat's `approvals`** (design 47 G §677; lead // answers A-265, A-271), with zsh's node-login-shell `execute` as the case: the marked verb reaches the gate with the caller // its subject names, and `approvals` is answered from the runner's own record of the calls its gate holds, with the // arguments for the call's own caller only, though no bundle names the verb. t.Run("TheRunnerServesApprovalsForTheSeatsItsBundlesHold", func(t *testing.T) { mesh := mt.New(t) mesh.Issue(t, mt.MembershipOf("zsh", "anchor", true, map[string][]string{"node-login-shell": {"execute", seatdef.ApprovalsVerb}})) nodeTools := connect(t, "node-tools", "anchor") gate := &gateHolding{} defined := WithSeatDefinitions(func() map[string]seatdef.Definition { return map[string]seatdef.Definition{"node-login-shell": {Seat: "node-login-shell", Verbs: []seatdef.Verb{ {Name: "execute", NeedsApproval: true}, {Name: seatdef.ApprovalsVerb}}}} }) stop, err := Run(nodeTools, []Served{{"zsh", []string{mt.Fixture("login-shell-execute.py")}}}, nil, func(string, ...any) {}, defined, WithGate(gate)) if err != nil { t.Fatal(err) } defer stop() asker, err := bus.Connect(bus.Credential{URL: mt.URL(t), Module: "node-tools", Node: "workstation", User: "workstation.node-tools", Password: "unused"}) if err != nil { t.Fatal(err) } asker.Logf = func(string, ...any) {} defer asker.Close() mt.Until(t, func() error { _, err := asker.Ask("seat:node-login-shell.execute@anchor", map[string]any{"command": "ls"}, "") return err }) if len(gate.calls) != 1 || gate.calls[0].Seat != "node-login-shell" || gate.calls[0].Name != "execute" || gate.calls[0].Caller != "workstation.node-tools" { t.Fatalf("the marked verb reached the gate as %+v, want the seat's execute by workstation.node-tools", gate.calls) } var got approval.Approvals mt.Until(t, func() error { answered, err := asker.Ask("seat:node-login-shell.approvals@anchor", map[string]any{}, "") if err == nil { err = json.Unmarshal(answered.Result, &got) } return err }) if len(got.Calls) != 1 || got.Calls[0].Caller != "workstation.node-tools" || got.Calls[0].Verb != "node-login-shell.execute" || string(got.Calls[0].Arguments) != `{"command":"ls"}` { t.Fatalf("approvals answered %+v, want the one call the gate holds, with its caller's arguments", got) } }) }