Files
mesh-tools/node-tools/internal/bus/caller_bus_test.go
T
jschoubben 541de48e04
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 delivered
node-tools: only a call pattern names a caller, and a fall-back for want of a responder is said (hq issue 365, review)
callerOn reads a caller only under the call kind, so a tool subscription ending
in a wildcard never yields one. A caller-named call that reached nobody is said
once per subject before it is asked on the old subject, so a runtime or grant
that drifted behind is seen rather than a quiet unproven. The caller tests wait
for their bundle's exit, which reads the launcher's restart pauses another test
sets.
2026-10-11 11:22:29 +02:00

219 lines
7.4 KiB
Go

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)
}
}
// A fall-back for want of a responder on the caller-named subject is said, once per subject, so a runtime or
// grant that drifted behind is seen rather than read as a quiet "unproven".
func TestAFallBackForWantOfAResponderIsSaidOncePerSubject(t *testing.T) {
url := mt.URL(t)
// A runtime from before issue 365: the subject that names no caller, alone.
older, err := nats.Connect(url)
if err != nil {
t.Fatal(err)
}
defer older.Close()
if _, err := older.Subscribe("mesh.mod.ledger.tool.who", func(m *nats.Msg) { _ = m.Respond([]byte(`{"result":{}}`)) }); err != nil {
t.Fatal(err)
}
_ = older.Flush()
asker := dial(t, url, "node-tools", "shanks", "shanks.node-tools")
var mu sync.Mutex
var said []string
asker.Logf = func(f string, a ...any) { mu.Lock(); said = append(said, fmt.Sprintf(f, a...)); mu.Unlock() }
for i := 0; i < 3; i++ {
if _, err := asker.Ask("ledger.who", map[string]any{}, ""); err != nil {
t.Fatal(err)
}
}
mu.Lock()
defer mu.Unlock()
n := 0
for _, s := range said {
if strings.Contains(s, "nothing answers the caller-named mesh.mod.ledger.call.who.shanks~node-tools") {
n++
}
}
if n != 1 {
t.Errorf("the fall-back was said %d times, want once: %v", n, said)
}
}