node-tools: a tool call names its caller, and the module is told it from the subject (hq issue 365)
mesh/merge-gate pass: builds mesh-tools, node-tools → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of …
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery-group group fix/365-a-tool-call-names-its-caller delivering: 0 of 4 delivered
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds mesh-tools, node-tools → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of …
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery-group group fix/365-a-tool-call-names-its-caller delivering: 0 of 4 delivered
mesh/delivery superseded: a newer head of the same pull request
Each tool and seat verb is also served on its caller-named subject, kind call with the caller's bus user last, which the controller grants each user with its own name alone; the runtime hands the module that caller in the call's _meta, never from the body. Calls through the runtime (the console and modules' invokes) ask the caller-named subject first and the old one only when the first reached nobody: nothing serves it yet, or the bus refused it because the grant is not delivered yet, which ends the call at once rather than after 30 s. The old subjects stay served and say no caller for one release (hq issue 464).
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
package bus_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats-server/v2/server"
|
||||
"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"
|
||||
)
|
||||
|
||||
// A bus whose users are granted as the controller grants them (mesh-controller internal/broker): each named
|
||||
// user publishes the subjects listed for it.
|
||||
func grantedBus(t *testing.T, publish map[string][]string) string {
|
||||
t.Helper()
|
||||
var users []*server.User
|
||||
for user, allow := range publish {
|
||||
// And the membership's direct get every connection asks once, answered "no responders" here.
|
||||
allow = append(allow, "$JS.API.DIRECT.GET.>")
|
||||
users = append(users, &server.User{Username: user, Password: "pw", Permissions: &server.Permissions{
|
||||
Publish: &server.SubjectPermission{Allow: allow},
|
||||
Subscribe: &server.SubjectPermission{Allow: []string{">"}},
|
||||
}})
|
||||
}
|
||||
s, err := server.NewServer(&server.Options{Host: "127.0.0.1", Port: server.RANDOM_PORT, NoLog: true, NoSigs: true,
|
||||
Users: users})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go s.Start()
|
||||
if !s.ReadyForConnections(10 * time.Second) {
|
||||
t.Fatal("the bus did not come up")
|
||||
}
|
||||
t.Cleanup(s.Shutdown)
|
||||
return s.ClientURL()
|
||||
}
|
||||
|
||||
func dial(t *testing.T, url, module, node, user string) *bus.Conn {
|
||||
t.Helper()
|
||||
c, err := bus.Connect(bus.Credential{URL: url, Module: module, Node: node, User: user, Password: "pw"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.Logf = func(string, ...any) {}
|
||||
t.Cleanup(c.Close)
|
||||
return c
|
||||
}
|
||||
|
||||
type heard struct {
|
||||
mu sync.Mutex
|
||||
callers []string
|
||||
}
|
||||
|
||||
func (h *heard) handler(caller string, _ json.RawMessage) (any, error) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.callers = append(h.callers, caller)
|
||||
return map[string]any{"caller": caller}, nil
|
||||
}
|
||||
|
||||
func (h *heard) all() []string {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
return append([]string(nil), h.callers...)
|
||||
}
|
||||
|
||||
// A caller asks on the caller-named subject first; where nothing serves it yet — a runtime from before issue
|
||||
// 365 — the same call is asked once on the subject that names no caller, at once, and done once.
|
||||
func TestACallerFallsBackAtOnceWhereNothingServesTheCallerNamedSubject(t *testing.T) {
|
||||
url := mt.URL(t)
|
||||
server := dial(t, url, "ledger", "anchor", "")
|
||||
h := &heard{}
|
||||
// An older runtime: the subject that names no caller, alone.
|
||||
stop, err := server.HandleSubject("mesh.mod.ledger.tool.who", func(body json.RawMessage) (any, error) {
|
||||
return h.handler("", body)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stop()
|
||||
server.Flush()
|
||||
asker := dial(t, url, "node-tools", "shanks", "shanks.node-tools")
|
||||
began := time.Now()
|
||||
answered, err := asker.Ask("ledger.who", map[string]any{}, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if took := time.Since(began); took > 2*time.Second {
|
||||
t.Errorf("the fall-back took %s", took)
|
||||
}
|
||||
if got := h.all(); len(got) != 1 || got[0] != "" || !strings.Contains(string(answered.Result), `"caller":""`) {
|
||||
t.Errorf("the call was answered %v, heard %v; want once, on the old subject", string(answered.Result), got)
|
||||
}
|
||||
}
|
||||
|
||||
// Where the bus does not grant the caller-named subject yet — the controller's grants not yet delivered — the
|
||||
// refusal ends the call at once and it is asked on the subject that names no caller: no caller waits out
|
||||
// RequestTimeout for a grant that is on its way.
|
||||
func TestACallerTheBusRefusesTheCallerNamedSubjectFallsBackAtOnce(t *testing.T) {
|
||||
url := grantedBus(t, map[string][]string{
|
||||
"anchor.node-tools": {"_INBOX.>"},
|
||||
"shanks.node-tools": {"mesh.mod.*.tool.>"},
|
||||
})
|
||||
server := dial(t, url, "node-tools", "anchor", "anchor.node-tools")
|
||||
h := &heard{}
|
||||
stop, err := server.HandleSubjectCalled("mesh.mod.ledger.tool.who", h.handler)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stop()
|
||||
server.Flush()
|
||||
asker := dial(t, url, "node-tools", "shanks", "shanks.node-tools")
|
||||
var said []string
|
||||
var mu sync.Mutex
|
||||
asker.Logf = func(f string, a ...any) { mu.Lock(); said = append(said, fmt.Sprintf(f, a...)); mu.Unlock() }
|
||||
began := time.Now()
|
||||
if _, err := asker.Ask("ledger.who", map[string]any{}, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mu.Lock()
|
||||
if !strings.Contains(strings.Join(said, "\n"), "the bus refused this account the caller-named mesh.mod.ledger.call.who.shanks~node-tools") {
|
||||
t.Errorf("the refusal was not said: %v", said)
|
||||
}
|
||||
mu.Unlock()
|
||||
if took := time.Since(began); took > 3*time.Second {
|
||||
t.Errorf("the refused caller-named call held the caller %s", took)
|
||||
}
|
||||
if got := h.all(); len(got) != 1 || got[0] != "" {
|
||||
t.Errorf("heard %v; want one call, on the subject that names no caller", got)
|
||||
}
|
||||
}
|
||||
|
||||
// With the controller's grant — the caller-named subjects with its own name last — a caller is heard as itself;
|
||||
// and a caller cannot publish on a subject naming another, however it asks.
|
||||
func TestAGrantedCallerIsHeardAsItselfAndCannotNameAnother(t *testing.T) {
|
||||
url := grantedBus(t, map[string][]string{
|
||||
"anchor.node-tools": {"_INBOX.>"},
|
||||
"shanks.node-tools": {"mesh.mod.*.call.*.shanks~node-tools", "mesh.mod.*.call.*.*.shanks~node-tools"},
|
||||
})
|
||||
server := dial(t, url, "node-tools", "anchor", "anchor.node-tools")
|
||||
h := &heard{}
|
||||
stop, err := server.HandleSubjectCalled("mesh.mod.ledger.tool.who", h.handler)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stop()
|
||||
server.Flush()
|
||||
asker := dial(t, url, "node-tools", "shanks", "shanks.node-tools")
|
||||
if _, err := asker.Ask("ledger.who", map[string]any{}, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Naming another on the subject itself: refused by the bus, so nothing is heard.
|
||||
refusals := make(chan error, 4)
|
||||
forger, err := nats.Connect(url, nats.UserInfo("shanks.node-tools", "pw"),
|
||||
nats.ErrorHandler(func(_ *nats.Conn, _ *nats.Subscription, err error) { refusals <- err }))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer forger.Close()
|
||||
if err := forger.Publish("mesh.mod.ledger.call.who.novox~node-tools", []byte(`{}`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = forger.Flush()
|
||||
select {
|
||||
case err := <-refusals:
|
||||
if !errors.Is(err, nats.ErrPermissionViolation) {
|
||||
t.Errorf("the bus said %v, want a permissions violation", err)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Error("the bus did not refuse a call naming another caller")
|
||||
}
|
||||
if got := h.all(); len(got) != 1 || got[0] != "shanks.node-tools" {
|
||||
t.Errorf("heard %v; want the asker alone, as itself", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user