Files
mesh-delivery/modules/mesh-grants/grants_test.go
T
jschoubben ce600df98a
mesh/delivery merged: source.main moved to novox/mesh-delivery@1d416827157d
mesh/check being carried to ce600df98afc on 1d416827157d
Say the held-list state on a part's first pass, so a warning raised before a restart is cleared
2026-10-26 13:37:31 +01:00

1517 lines
64 KiB
Go

package manifest
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"testing"
"time"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/asker"
"git.novox.be/novox/mesh-sdk/go/asks"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/seal"
"mesh-grants/internal/grants"
)
// fakeBus is the bus as the part sees it: other parts' states to read (refusing those named), the bus module's read
// tools, the part's own states and its events. It records every write, call and event, so a row can prove nothing was
// published or asserted.
type fakeBus struct {
mu sync.Mutex
states map[string]map[string][]byte
refused map[string]bool
loaded []grants.User
streams []map[string]any
own map[string]map[string][]byte
revs map[string]uint64
refuseTo map[string]bool
calls []string
events []string
writes []string
// The assignments stream: each subject's last membership and sequence, every publish, and whether it is refused.
stream map[string][]byte
seqs map[string]uint64
seq uint64
publishes []string
refusePub bool
// hold, when set, keeps every nats_users call waiting until it is closed: a pass waiting on the bus.
hold chan struct{}
// creates are the streams the part asked to create, refused when refuseCreate.
creates []string
refuseCreate bool
// rootFree answers the controller's root-free verb: nil is no answerer at all (no responders).
rootFree func(machines []string) (any, error)
}
func (b *fakeBus) LastMembership(node, module string) ([]byte, uint64, bool, error) {
b.mu.Lock()
defer b.mu.Unlock()
subject := "mesh.assignment." + node + "." + module
v, ok := b.stream[subject]
return v, b.seqs[subject], ok, nil
}
func (b *fakeBus) PublishMembership(node, module string, body []byte, after uint64) error {
b.mu.Lock()
defer b.mu.Unlock()
subject := "mesh.assignment." + node + "." + module
if b.refusePub {
return errors.New("Permissions Violation for Publish to " + subject)
}
if b.seqs[subject] != after {
return fmt.Errorf("wrong last sequence for %s: %d", subject, b.seqs[subject])
}
if b.stream == nil {
b.stream, b.seqs = map[string][]byte{}, map[string]uint64{}
}
b.seq++
b.stream[subject], b.seqs[subject] = body, b.seq
b.publishes = append(b.publishes, subject)
return nil
}
// CreateDeclared is the bus's create of a stream: refused when refuseCreate, held when the bus holds it, and otherwise
// held from now on, as nats_streams then reads it.
func (b *fakeBus) CreateDeclared(cfg jetstream.StreamConfig) error {
b.mu.Lock()
defer b.mu.Unlock()
b.creates = append(b.creates, cfg.Name)
if b.refuseCreate {
return errors.New("Permissions Violation for Publish to $JS.API.STREAM.CREATE." + cfg.Name)
}
for _, s := range b.streams {
if s["name"] == cfg.Name {
return grants.ErrHeld
}
}
b.streams = append(b.streams, map[string]any{"name": cfg.Name, "subjects": cfg.Subjects, "retention": "limits",
"max_age_hours": int(cfg.MaxAge.Hours()), "max_per_subject": int(cfg.MaxMsgsPerSubject)})
return nil
}
func newFakeBus() *fakeBus {
return &fakeBus{states: map[string]map[string][]byte{}, refused: map[string]bool{}, own: map[string]map[string][]byte{},
revs: map[string]uint64{}, refuseTo: map[string]bool{}}
}
func (b *fakeBus) put(state, key string, v any) {
raw, err := json.Marshal(v)
if err != nil {
panic(err)
}
if b.states[state] == nil {
b.states[state] = map[string][]byte{}
}
b.states[state][key] = raw
}
func (b *fakeBus) Keys(state string) ([]string, error) {
b.mu.Lock()
defer b.mu.Unlock()
if b.refused[state] {
return nil, errors.New("Permissions Violation for Publish to $JS.API.STREAM.INFO.KV_" + state)
}
var out []string
for k := range b.states[state] {
out = append(out, k)
}
slices.Sort(out)
return out, nil
}
func (b *fakeBus) Get(state, key string) ([]byte, bool, error) {
b.mu.Lock()
defer b.mu.Unlock()
if b.refused[state] {
return nil, false, errors.New("refused")
}
v, ok := b.states[state][key]
return v, ok, nil
}
func (b *fakeBus) Call(key string, body any) (json.RawMessage, error) {
b.mu.Lock()
hold := b.hold
b.mu.Unlock()
if hold != nil && key == grants.CallUsers {
<-hold
}
b.mu.Lock()
defer b.mu.Unlock()
b.calls = append(b.calls, key)
args, _ := body.(map[string]any)
switch key {
case grants.CallUsers:
part, _ := args["user"].(string)
if part == "" {
var names []map[string]any
for _, u := range b.loaded {
names = append(names, map[string]any{"user": u.User, "publish_grants": len(u.Publish)})
}
return json.Marshal(map[string]any{"users": names})
}
var out []grants.User
for _, u := range b.loaded {
if strings.Contains(u.User, part) {
out = append(out, u)
}
}
return json.Marshal(map[string]any{"users": out})
case grants.CallStreams:
return json.Marshal(map[string]any{"detail": b.streams})
case grants.CallRootFree:
if b.rootFree == nil {
return nil, errors.New("nats: no responders available for request")
}
out, err := b.rootFree(args["machines"].([]string))
if err != nil {
return nil, err
}
return json.Marshal(out)
}
return nil, fmt.Errorf("no tool %s", key)
}
func (b *fakeBus) ownGet(state, key string) ([]byte, uint64, bool, error) {
b.mu.Lock()
defer b.mu.Unlock()
v, ok := b.own[state][key]
return v, b.revs[state+"/"+key], ok, nil
}
type ownStore struct{ b *fakeBus }
func (s ownStore) Get(state, key string) ([]byte, uint64, bool, error) { return s.b.ownGet(state, key) }
func (s ownStore) Put(state, key string, value []byte) (uint64, error) {
b := s.b
b.mu.Lock()
defer b.mu.Unlock()
if b.refuseTo[state] {
return 0, errors.New("Permissions Violation for Publish to $KV.mesh-grants_" + state)
}
b.writes = append(b.writes, state)
if b.own[state] == nil {
b.own[state] = map[string][]byte{}
}
b.own[state][key] = value
b.revs[state+"/"+key]++
return b.revs[state+"/"+key], nil
}
func (b *fakeBus) Emit(event string, _ any) error {
b.mu.Lock()
defer b.mu.Unlock()
b.events = append(b.events, event)
return nil
}
func (b *fakeBus) count(key string) int {
b.mu.Lock()
defer b.mu.Unlock()
n := 0
for _, c := range b.calls {
if c == key {
n++
}
}
return n
}
// manifestOf is a fixture manifest of one module, in the indexer's two buckets.
func (b *fakeBus) manifestOf(module, raw string) {
digest := delivery.ManifestDigest([]byte(raw))
b.put(grants.ReadManifests, digest, delivery.Manifest{V: delivery.Version, Module: module, JSON: raw})
b.put(grants.ReadModules, module, delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32), Manifest: digest, Repository: "novox/x", Commit: "c"})
}
func (b *fakeBus) assign(node, module string) {
b.put(grants.ReadAssignments, node+"."+module, map[string]any{"node": node, "module": module, "assigned": "2026-10-25T20:00:00Z"})
}
const (
natsManifest = `{"module":"nats","version":"1","claims":[{"name":"mesh-broker"}],"own-secrets":{"broker":"/var/lib/nats/broker"}}`
xManifest = `{"module":"mod-x","version":"1","own-secrets":{"broker":"/var/lib/x/broker"},"state":[{"name":"kept","history":3}],` +
`"streams":[{"name":"things","events":["thing.added"],"max-age-days":2,"max-mib":5}],"logs":[{"name":"history"}]}`
yManifest = `{"module":"mod-y","version":"1","own-secrets":{"broker":"/var/lib/y/broker"},"reads":["mod-x.kept"]}`
)
// fixture is a mesh of two machines: a runs the bus and mod-x, b runs mod-y; the bus node's agent loaded a list.
func fixture() *fakeBus {
b := newFakeBus()
b.manifestOf("nats", natsManifest)
b.manifestOf("mod-x", xManifest)
b.manifestOf("mod-y", yManifest)
b.assign("a", "nats")
b.assign("a", "mod-x")
b.assign("b", "mod-y")
b.put(grants.ReadAssignments, "b.gone", map[string]any{"node": "b", "module": "gone", "assigned": "2026-10-01T00:00:00Z", "ended": "2026-10-02T00:00:00Z"})
b.put(grants.ReadHave, "a.node", map[string]any{"v": 1})
b.put(grants.ReadHave, "b.node", map[string]any{"v": 1})
b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK,
UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Wanted: "sha256:aa", Digest: "sha256:aa", Users: 6}})
b.put(grants.ReadControllerGrant, "mod-x.0123456789ab0123456789ab0123456789ab0123456789ab0123456789ab0123", delivery.ControllerGrant{V: delivery.Version, Accounts: []string{"a.mod-x"}})
return b
}
func partOn(b *fakeBus) *grants.Part {
return &grants.Part{Read: b, Call: b, Store: ownStore{b}, Emit: b, Publish: b, Now: func() time.Time { return time.Date(2026, 10, 25, 22, 0, 0, 0, time.UTC) }}
}
// shadowOf is the list the part composes from b's records now.
func shadowOf(t *testing.T, b *fakeBus) []grants.User {
t.Helper()
recs, _ := grants.Records(grants.ReadInputs(b, &grants.Manifests{}))
users, err := grants.ComposeUsers(recs)
if err != nil {
t.Fatal(err)
}
return users
}
func names(us []grants.User) []string {
var out []string
for _, u := range us {
out = append(out, u.User)
}
return out
}
func gapNamed(gs []grants.Gap, input string) bool {
return slices.ContainsFunc(gs, func(g grants.Gap) bool { return strings.Contains(g.Input, input) })
}
// TestGrants is the grants part's one table of checks (ADR 0547 clauses 5 to 8 in shadow, issue 1945): its manifest,
// the shadow list composed from bus values, the compare, each tool, that nothing is published or asserted, and that a
// refused read or write never stops it.
func TestGrants(t *testing.T) {
t.Run("manifest", func(t *testing.T) {
raw, err := os.ReadFile("module.json")
if err != nil {
t.Fatal(err)
}
m, err := catalogue.ParseManifest(raw)
if err != nil {
t.Fatalf("mesh-sdk's manifest reader refuses the manifest: %v", err)
}
if m.Module != "mesh-grants" || len(m.Claims) != 0 {
t.Errorf("module %q claims %v: the part claims no seat until mesh-sdk's seat list names mesh-grants", m.Module, m.Claims)
}
for _, row := range []struct {
what string
got, want []string
}{
{"reads", m.Reads, grants.Reads},
{"invokes", m.Invokes, grants.Invokes},
{"consumes", m.Consumes, grants.Consumes},
{"emits", m.Emits, grants.Emits},
{"tools", m.Tools, grants.Tools},
} {
if !slices.Equal(slices.Sorted(slices.Values(row.got)), slices.Sorted(slices.Values(row.want))) {
t.Errorf("%s = %v, the program's are %v", row.what, row.got, row.want)
}
}
var states []string
for _, s := range m.State {
states = append(states, s.Name)
}
if !slices.Equal(slices.Sorted(slices.Values(states)), grants.States) {
t.Errorf("state = %v, want %v", states, grants.States)
}
if _, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 || m.SecretsOwner != "mesh-grants" {
t.Errorf("own-secrets %v owner %q: a credential of its own, broker", m.OwnSecrets, m.SecretsOwner)
}
})
t.Run("no timer, no update or delete, a publish only of memberships, a create only of declared streams", func(t *testing.T) {
forbidden := []string{"time.Tick", "time.NewTicker", "time.Sleep", "time.After", "time.NewTimer", "time.AfterFunc",
"PublishMsg", "CreateKeyValue", "CreateOrUpdate", "UpdateStream", "UpdateKeyValue", "DeleteStream", "PurgeStream",
"DeleteKeyValue", "CreateObjectStore", "EnsureStream", "RaiseBuckets", "RaiseLogs", ".Create("}
// The one publish: a membership, on its own subject, in publish.go; the one create: a declared stream the bus lacks,
// in create.go (issue 1790).
publishOnly := filepath.Join("cmd", "mesh-grants", "publish.go")
createOnly := filepath.Join("cmd", "mesh-grants", "create.go")
// And the operator's asks and conditions, on the seat operator-channel's subjects, in asking.go (ADR 0547 clause 8).
asksOnly := filepath.Join("cmd", "mesh-grants", "asking.go")
sealedOnly := filepath.Join("cmd", "mesh-grants", "sealed.go")
files := 0
for _, root := range []string{"cmd", "internal"} {
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return err
}
files++
src, err := os.ReadFile(path)
if path != publishOnly && path != asksOnly && (strings.Contains(string(src), ".Publish(") || strings.Contains(string(src), "mesh.assignment")) {
t.Errorf("%s publishes: only publish.go publishes memberships, and asking.go the operator's asks", path)
}
if path == asksOnly && (strings.Count(string(src), ".Publish(") != 2 || strings.Contains(string(src), "mesh.assignment")) {
t.Errorf("asking.go publishes other than an ask, a cancel and a condition, through js.Publish twice")
}
if path == publishOnly && strings.Count(string(src), ".Publish(") != 1 {
t.Errorf("publish.go publishes other than one membership")
}
if n := strings.Count(string(src), "CreateStream"); (path == createOnly && n != 1) || (path != createOnly && n != 0) {
t.Errorf("%s holds CreateStream %d times: only create.go creates, once", path, n)
}
// The one object bucket: the part's own, of the user list sealed to the bus node, in sealed.go (ADR 0547 clause 7).
if path == sealedOnly {
if strings.Count(string(src), "CreateObjectStore(") != 1 || !strings.Contains(string(src), "delivery.GrantsUserListObjects") {
t.Errorf("sealed.go creates other than the part's one object bucket")
}
src = []byte(strings.ReplaceAll(string(src), "CreateObjectStore", ""))
}
for _, f := range forbidden {
if strings.Contains(string(src), f) {
t.Errorf("%s holds %s: the part holds no timer and creates or asserts nothing", path, f)
}
}
return err
})
if err != nil {
t.Fatal(err)
}
}
if files < 8 {
t.Fatalf("read %d source files, want the program's", files)
}
})
t.Run("shadow list from bus values", func(t *testing.T) {
for _, row := range []struct {
name string
bend func(*fakeBus)
users []string
gap string
}{
{"every record on the bus", func(*fakeBus) {},
[]string{"a.mod-x", "a.nats", "b.mod-y", "controller", "node.a", "node.b"}, "enrolment tokens"},
{"a manifest missing is a gap and its user is left out", func(b *fakeBus) { delete(b.states[grants.ReadModules], "mod-y") },
[]string{"a.mod-x", "a.nats", "controller", "node.a", "node.b"}, "manifest of mod-y"},
{"a want's built manifest is composed, not the trunk's", func(b *fakeBus) {
built := strings.Replace(xManifest, `"history":3}]`, `"history":3},{"name":"newer"}]`, 1)
digest := delivery.ManifestDigest([]byte(built))
b.put(grants.ReadManifests, digest, delivery.Manifest{V: delivery.Version, Module: "mod-x", JSON: built})
fp := "src2:" + strings.Repeat("cd", 32)
b.put(grants.ReadWants, "want.a.mod-x", delivery.Want{V: delivery.Version, FP: fp, Composed: "sha256:" + strings.Repeat("ef", 32)})
key, _ := delivery.BuilderBuildKey("mod-x", fp)
b.put(grants.ReadBuilds, key, map[string]any{"v": delivery.Version, "state": "done", "manifest": digest})
}, []string{"a.mod-x", "a.nats", "b.mod-y", "controller", "node.a", "node.b"}, "enrolment tokens"},
{"placement refused is a gap; the machines come from the haves", func(b *fakeBus) { b.refused[grants.ReadAssignments] = true },
[]string{"controller", "node.a", "node.b"}, grants.ReadAssignments},
} {
t.Run(row.name, func(t *testing.T) {
b := fixture()
row.bend(b)
in := grants.ReadInputs(b, &grants.Manifests{})
recs, left := grants.Records(in)
users, err := grants.ComposeUsers(recs)
if err != nil {
t.Fatal(err)
}
if got := names(users); !slices.Equal(got, row.users) {
t.Errorf("users = %v, want %v", got, row.users)
}
if !gapNamed(append(in.Gaps, left...), row.gap) {
t.Errorf("gaps %v name no %q", in.Gaps, row.gap)
}
if strings.HasPrefix(row.name, "a want's") {
x := users[slices.IndexFunc(users, func(u grants.User) bool { return u.User == "a.mod-x" })]
if !slices.Contains(x.Publish, "$KV.mod-x_newer.>") {
t.Errorf("a.mod-x is not granted the state its wanted build declares: %v", x.Publish)
}
}
})
}
// A module whose only runtime-loaded part is a helper bundle (route-proxy's route-provider) says `loads` in
// the repository manifest's build.artifacts; the indexer's manifest has no derived Bundles (issue 1945).
for _, row := range []struct {
name, build string
carried bool
}{
{"a bundle that says loads is carried by node-tools", `,"build":{"artifacts":[{"name":"helper","kind":"bundle","language":"go","system":"arch","from":"cmd/h","binary":"h","loads":["h"]}]}`, true},
{"a module with no loading bundle is left to its own account", ``, false},
} {
t.Run(row.name, func(t *testing.T) {
b := fixture()
b.manifestOf("mod-z", `{"module":"mod-z","version":"1","own-secrets":{"broker":"/var/lib/z/broker"}`+row.build+`}`)
b.assign("a", "mod-z")
b.manifestOf("node-tools", `{"module":"node-tools","version":"1","own-secrets":{"broker":"/var/lib/nt/broker"}}`)
b.assign("a", "node-tools")
users := shadowOf(t, b)
tools := users[slices.IndexFunc(users, func(u grants.User) bool { return u.User == "a.node-tools" })]
if got := slices.Contains(tools.Subscribe, "mesh.mod.mod-z.tool.>"); got != row.carried {
t.Errorf("a.node-tools subscribes to mod-z's tools = %v, want %v: %v", got, row.carried, tools.Subscribe)
}
})
}
b := fixture()
users := shadowOf(t, b)
y := users[slices.IndexFunc(users, func(u grants.User) bool { return u.User == "b.mod-y" })]
if !slices.Contains(y.Publish, "$JS.API.DIRECT.GET.KV_mod-x_kept.>") {
t.Errorf("b.mod-y reads mod-x.kept and is not granted its direct get: %v", y.Publish)
}
})
t.Run("root-free: the controller's word fills the composition, failing closed (issue 1945)", func(t *testing.T) {
const router = `{"module":"messenger","version":"1","runs-as":"messenger","resources":[{"id":"account","type":"user","name":"messenger","shell":"/usr/bin/nologin","home":"/var/lib/messenger"}],"own-secrets":{"broker":"/var/lib/m/broker"},"secrets-owner":"messenger",` +
`"seats":[{"name":"operator-channel","scope":"mesh","serves":["open"]},{"name":"channel","scope":"mesh","kinded":true,"accepts":["show"]}],` +
`"claims":[{"name":"operator-channel","scope":"mesh","serves":["open"]}]}`
const telegram = `{"module":"telegram","version":"1","runs-as":"telegram","resources":[{"id":"account","type":"user","name":"telegram","shell":"/usr/bin/nologin","home":"/var/lib/telegram"}],"own-secrets":{"broker":"/var/lib/t/broker"},"secrets-owner":"telegram",` +
`"claims":[{"name":"channel","scope":"mesh","kind":"telegram","capabilities":["deliver","verified-sender"]}]}`
answer := func(free ...string) func([]string) (any, error) {
return func(machines []string) (any, error) {
var vs []map[string]any
for _, m := range machines {
ok := slices.Contains(free, m)
why := "judged free"
if !ok {
why = "its node-engine judged nothing"
}
vs = append(vs, map[string]any{"machine": m, "free": ok, "why": why})
}
return map[string]any{"machines": vs}, nil
}
}
for _, row := range []struct {
name string
routerOn []string
telegramOn string
ask func([]string) (any, error)
keeps bool
note string
}{
{"router and telegram on one free machine", []string{"a"}, "a", answer("a"), true, ""},
{"router and telegram on two free machines", []string{"a"}, "b", answer("a", "b"), true, ""},
{"the holder's machine is not free", []string{"a"}, "b", answer("a"), false, "b: not free, its node-engine judged nothing"},
{"the router's machine is not free", []string{"a"}, "b", answer("b"), false, "a: not free"},
{"an error is not free", []string{"a"}, "a", func([]string) (any, error) { return nil, errors.New("boom") }, false, "boom"},
{"no answerer is not free", []string{"a"}, "a", nil, false, "no responders"},
{"a machine left out of the answer is not free", []string{"a"}, "a",
func([]string) (any, error) { return map[string]any{"machines": []any{}}, nil }, false, "named no verdict"},
{"the router on two machines frees nothing", []string{"a", "b"}, "a", answer("a", "b"), false, ""},
} {
t.Run(row.name, func(t *testing.T) {
b := fixture()
b.manifestOf("messenger", router)
b.manifestOf("telegram", telegram)
for _, node := range row.routerOn {
b.assign(node, "messenger")
}
b.assign(row.telegramOn, "telegram")
b.rootFree = row.ask
res := partOn(b).Compose()
recs, _ := grants.Records(grants.ReadInputs(b, &grants.Manifests{}))
grants.WithRootFree(&recs, b)
kept := false
for _, k := range buscompose.PlacementsOf(recs, recs.Interchangeable).Kinds {
kept = kept || slices.Contains(k.Capabilities, "verified-sender")
}
if kept != row.keeps {
t.Errorf("verified-sender kept = %v, want %v (said %v)", kept, row.keeps, res.RootNotFree)
}
if !strings.Contains(strings.Join(res.RootNotFree, " | "), row.note) || (row.note == "" && row.keeps && len(res.RootNotFree) != 0) {
t.Errorf("the pass says %v, want it to name %q", res.RootNotFree, row.note)
}
})
}
// A root-not-free condition raised or cleared wakes a pass; another kind does not (never a timer).
wb := fixture()
var handle func(string, []byte)
wf := &grants.Follower{Part: partOn(wb), Watch: &fakeWatcher{opened: make(chan string, 16)}, Logf: func(string, ...any) {},
Hear: func(h func(string, []byte)) error { handle = h; return nil }}
wctx, wcancel := context.WithCancel(context.Background())
wdone := make(chan struct{})
go func() { wf.Run(wctx); close(wdone) }()
waitFor(t, func() bool { return wb.count(grants.CallUsers) > 0 && handle != nil })
passes := func() int { return wb.count(grants.CallUsers) }
before := passes()
handle(grants.EventConditionRaised, []byte(`{"kind":"disk-full","key":"machine.a.disk-full"}`))
time.Sleep(100 * time.Millisecond)
if passes() != before {
t.Errorf("a condition of another kind woke a pass")
}
handle(grants.EventConditionCleared, []byte(`{"kind":"root-not-free","key":"machine.a.root-not-free"}`))
waitFor(t, func() bool { return passes() > before })
wcancel()
<-wdone
if !grants.IsRootNotFree([]byte(`{"kind":"root-not-free"}`)) || !grants.IsRootNotFree([]byte(`{"key":"machine.novox.root-not-free"}`)) ||
grants.IsRootNotFree([]byte(`{"kind":"disk-full","key":"machine.novox.disk-full"}`)) {
t.Errorf("only a root-not-free condition wakes a pass")
}
})
t.Run("a predefined-key fallback is read with no list; every refused manifest listed", func(t *testing.T) {
// mod-p's value falls back to a predefined key, as claude-code's managed_settings does, and no list is on the bus
// (issue 1945); mod-z's manifest carries a key mesh-sdk's reader refuses, and mod-z is assigned nowhere.
const pManifest = `{"module":"mod-p","version":"1","own-secrets":{"broker":"/var/lib/p/broker"},` +
`"config":{"size":{"type":"number","description":"The size text shows in, in points.","predefined":"font-size","default":12,"runtime":true}}}`
const zManifest = `{"module":"mod-z","version":"1","no-such-key":true}`
b := fixture()
b.manifestOf("mod-p", pManifest)
b.manifestOf("mod-z", zManifest)
b.assign("b", "mod-p")
// mesh-wireguard is provided by the mesh, not read from a repository: no manifest in the indexer, none refused,
// no gap, and its manifest is the one mesh-sdk provides (issue 1945).
b.assign("b", "mesh-wireguard")
in := grants.ReadInputs(b, &grants.Manifests{})
// An indexer row of the same name never wins: the provided manifest is used and the row is said as a gap.
bw := fixture()
bw.manifestOf("mesh-wireguard", `{"module":"mesh-wireguard","version":"9","own-secrets":{"broker":"/x"}}`)
bw.assign("b", "mesh-wireguard")
if iw := grants.ReadInputs(bw, &grants.Manifests{}); !strings.HasPrefix(iw.Digests["mesh-wireguard"], "provided:") || !gapNamed(iw.Gaps, "manifest of mesh-wireguard") || len(iw.Refused) != 0 {
t.Errorf("an indexer manifest of mesh-wireguard won or went unsaid: %v %v", iw.Digests["mesh-wireguard"], iw.Gaps)
}
if _, ok := in.Manifests["mesh-wireguard"]; !ok || gapNamed(in.Gaps, "manifest of mesh-wireguard") || !strings.HasPrefix(in.Digests["mesh-wireguard"], "provided:") {
t.Errorf("mesh-wireguard's manifest is not taken from mesh-sdk: gaps %v", in.Gaps)
}
if len(in.Refused) != 1 || in.Refused[0].Input != "manifest of mod-z" || !gapNamed(in.Gaps, "manifest of mod-z") {
t.Errorf("refused = %v: only mod-z's, and named in the gaps", in.Refused)
}
recs, _ := grants.Records(in)
users, err := grants.ComposeUsers(recs)
if err != nil {
t.Fatal(err)
}
if !slices.Contains(names(users), "b.mod-p") {
t.Errorf("b.mod-p is not composed, so its module counts as placed nowhere: %v", names(users))
}
// The pass, the tool listing held memberships, and the log each say the refusal.
part := partOn(b)
res := part.Pass()
if len(res.Refused) != 1 || !strings.HasPrefix(res.Refused[0], "manifest of mod-z: ") {
t.Errorf("the pass says refused %v", res.Refused)
}
held, err := part.Held()
if err != nil {
t.Fatal(err)
}
if got, _ := held.(map[string]any)["refused"].([]string); len(got) != 1 {
t.Errorf("grants_membership lists refused %v", got)
}
var logged []string
var lmu sync.Mutex
f := &grants.Follower{Part: partOn(b), Watch: &fakeWatcher{opened: make(chan string, 16)}, Logf: func(format string, args ...any) {
lmu.Lock()
logged = append(logged, fmt.Sprintf(format, args...))
lmu.Unlock()
}}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() { f.Run(ctx); close(done) }()
waitFor(t, func() bool {
lmu.Lock()
defer lmu.Unlock()
return strings.Contains(strings.Join(logged, "\n"), "refused manifest of mod-z: ")
})
cancel()
<-done
})
t.Run("credentials", func(t *testing.T) {
shadow := []grants.User{{User: "a.held"}, {User: "a.new"}, {User: "a.bare"}}
loaded := []grants.User{{User: "a.held"}, {User: "a.bare"}, {User: "enrol.x"}, {User: "person.op"}}
hashes := map[string]grants.HeldHash{"a.held": {Hash: "$2a$10$h1", Generation: 2}, "a.new": {Hash: "$2a$10$h2", Generation: 1},
"person.op": {Hash: "$2a$10$h3", Adopted: true}}
c := grants.CredentialsOf(shadow, loaded, hashes)
for _, row := range []struct {
user, would string
held, adopted bool
}{
{"a.bare", grants.WouldLeaveOut, false, false},
{"a.held", grants.WouldSame, true, false},
{"a.new", grants.WouldAdd, true, false},
{"enrol.x", grants.WouldLoseLogin, false, false},
{"person.op", grants.WouldNotComposed, true, true},
} {
i := slices.IndexFunc(c.Users, func(u grants.Credential) bool { return u.User == row.user })
if i < 0 || c.Users[i].Would != row.would || c.Users[i].HashHeld != row.held || c.Users[i].Adopted != row.adopted {
t.Errorf("%s: %+v, want %s held %v", row.user, c.Users, row.would, row.held)
}
}
if c.HashesHeld != 3 || !slices.Equal(c.LeftOut, []string{"a.bare"}) || !slices.Equal(c.LosesLogin, []string{"enrol.x"}) ||
!slices.Equal(c.Adds, []string{"a.new"}) {
t.Errorf("credentials = %+v", c)
}
// Composed from the vault's state: a hash changes the list's state (written on the event), and no hash is ever
// in what the part writes or says.
b := fixture()
b.loaded = shadowOf(t, b)
p := partOn(b)
before := p.Compose()
b.put(grants.ReadAccounts, "a.mod-x", map[string]any{"user": "a.mod-x", "hash": "$2a$10$secrethashbytes", "generation": 1})
after := p.Compose()
cr := after.UserList.Credentials
if !after.Written || after.Revision != before.Revision+1 || cr.HashesHeld != 1 || slices.Contains(cr.LeftOut, "a.mod-x") ||
!slices.Contains(cr.LeftOut, "b.mod-y") {
t.Errorf("a hash heard = %+v", after.UserList.Credentials)
}
why := strings.Join(p.Decide(), "; ")
if !strings.Contains(why, "no hash for") || strings.Contains(why, "a.mod-x,") {
t.Errorf("take-over is refused for %q, want the users with no hash named", why)
}
for state, keys := range b.own {
for k, v := range keys {
if strings.Contains(string(v), "secrethashbytes") {
t.Errorf("the hash is written in %s %s", state, k)
}
}
}
})
t.Run("compare", func(t *testing.T) {
b := fixture()
shadow := shadowOf(t, b)
in := grants.ReadInputs(b, &grants.Manifests{})
changed := slices.Clone(shadow)
x := slices.IndexFunc(changed, func(u grants.User) bool { return u.User == "a.mod-x" })
changed[x].Publish = append(slices.Clone(changed[x].Publish[1:]), "extra.subject")
for _, row := range []struct {
name string
loaded []grants.User
added, dropped, gone []string
differ []string
equal bool
}{
{"equal", shadow, nil, nil, nil, nil, true},
{"one user the bus lacks", shadow[1:], []string{shadow[0].User}, nil, nil, nil, false},
{"one loaded user still placed", append(slices.Clone(shadow), grants.User{User: "a.mod-x.worker"}), nil, []string{"a.mod-x.worker"}, nil, nil, false},
{"one loaded user placement no longer names", append(slices.Clone(shadow), grants.User{User: "b.gone"}), nil, []string{"b.gone"}, []string{"b.gone"}, nil, true},
{"grants differ", changed, nil, nil, nil, []string{"a.mod-x"}, false},
} {
t.Run(row.name, func(t *testing.T) {
c := grants.Compare(shadow, row.loaded, grants.Placed(in))
var differ []string
for _, d := range c.Differ {
differ = append(differ, d.User)
}
if !slices.Equal(c.Added, nonNil(row.added)) || !slices.Equal(c.Dropped, nonNil(row.dropped)) ||
!slices.Equal(c.Gone, row.gone) || !slices.Equal(differ, row.differ) || c.Equal() != row.equal {
t.Errorf("compare = %+v", c)
}
if row.name == "grants differ" && (len(c.Differ[0].PublishAdded) != 1 || c.Differ[0].PublishDropped[0] != "extra.subject") {
t.Errorf("the grant difference is not named: %+v", c.Differ[0])
}
})
}
})
t.Run("compare excuses a retired verb and a multiply claimed mesh seat's added subscriptions on its machines, nothing else", func(t *testing.T) {
const retired, store, storeInfo = "mesh.seat.mesh-delivery.tool.mark-ready", "mesh.seat.mesh-store.tool.databases", "$SRV.INFO.mesh-store.>"
two := map[string][]string{"mesh-store": {"a", "n"}}
for _, row := range []struct {
name string
user string
subShadow, subBus, pubShadow []string
multi map[string][]string
differs bool
excused int
}{
{"a retired verb the shadow dropped", "a.node-tools", []string{"x"}, []string{"x", retired}, nil, nil, false, 1},
{"a verb of a live seat the shadow lost", "a.node-tools", []string{"x"}, []string{"x", store}, nil, two, true, 0},
{"a seat claimed on two machines, added to a user on one", "a.node-tools", []string{"x", store, storeInfo}, []string{"x"}, nil, two, false, 2},
{"the same added to a user on a machine that does not claim it", "q.node-tools", []string{"x", store}, []string{"x"}, nil, two, true, 0},
{"a publish of the seat added is not excused", "a.node-tools", []string{"x"}, []string{"x"}, []string{store}, two, true, 0},
{"the same added for a seat claimed once", "a.node-tools", []string{"x", store}, []string{"x"}, nil, nil, true, 0},
} {
c := grants.Compare([]grants.User{{User: row.user, Subscribe: row.subShadow, Publish: row.pubShadow}}, []grants.User{{User: row.user, Subscribe: row.subBus}}, nil).
Excusing(grants.ExcusedBy(row.multi))
if c.Equal() == row.differs || len(c.Excused) != row.excused {
t.Errorf("%s: equal %v, excused %v", row.name, c.Equal(), c.Excused)
}
}
})
t.Run("tools", func(t *testing.T) {
b := fixture()
b.loaded = shadowOf(t, b)
b.streams = []map[string]any{{"name": "KV_mod-x_kept", "subjects": []string{"$KV.mod-x_kept.>"}, "retention": "limits", "max_per_subject": 3}}
p := partOn(b)
// The bus node says its key in its have of the bus module; the process puts the sealed list (ADR 0547 clause 7).
busKey, err := seal.GenerateKeyPair()
if err != nil {
t.Fatal(err)
}
withKey := func(w delivery.UserListWritten) {
w.Key = busKey.PublicKey
b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, UserList: &w})
}
loadedBefore := delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Wanted: "sha256:aa", Digest: "sha256:aa", Users: 6}
withKey(loadedBefore)
objs := &fakeObjects{}
p.Objects = objs
open := func(g delivery.GrantsUserList) string {
t.Helper()
list, err := delivery.OpenUserList(objs.objects[g.Object], g.Digest, func(bx seal.Box) ([]byte, error) {
out, err := seal.Open(bx, busKey.PrivateKey)
return []byte(out), err
})
if err != nil {
t.Fatalf("the sealed list does not open with the bus node's key: %v", err)
}
return string(list)
}
got, _ := p.UserList()
if !strings.Contains(fmt.Sprint(got), "nothing composed yet") {
t.Errorf("user-list before any composition = %v", got)
}
first := p.Compose()
if !first.Written || first.Revision != 1 || !first.UserList.Compare.Equal() || first.UserList.Users != 6 {
t.Fatalf("first composition = %+v", first)
}
// Written in shadow, the bus node told to follow the controller's list; every user left out while the vault holds
// no hash; a tool's part (no object bucket) writes none.
if g, _, had := p.Sealed(); !had || g.Follow != delivery.FollowController || g.Revision != 1 || g.Node != "a" ||
g.Users != 0 || len(g.LeftOut) != 6 || len(objs.objects) != 1 || !strings.Contains(open(g), "accounts {") {
t.Errorf("the sealed list in shadow = %+v, %d objects", g, len(objs.objects))
}
if _, _, had := partOn(fixture()).Sealed(); had {
t.Error("a part read before any pass holds a sealed list")
}
if tp := partOn(fixture()); tp.Compose().Sealed == nil {
t.Error("a tool's pass does not say the sealed list")
} else if _, _, had := tp.Sealed(); had {
t.Error("a tool's part wrote the sealed list")
}
if len(first.Published) != 3 || len(b.publishes) != 3 {
t.Errorf("the first composition published %v, want the three assignments' memberships", first.Published)
}
// The second pass says each membership unchanged (published at the first); the third writes nothing at all.
if again := p.Compose(); again.Written || again.Revision != 1 || len(again.Published) != 0 || again.AssertionsWritten != 0 {
t.Errorf("a composition with no input changed wrote or published: %+v", again)
}
if third := p.Compose(); third.Written || third.MembershipsWritten != 0 || len(third.Published) != 0 || len(b.publishes) != 3 {
t.Errorf("a membership equal to the one last published was published again, or a state written: %+v", third)
}
// mod-x placed on b too: b.mod-x is added and its first membership published; a.mod-x's would lose the module's
// plain subject (mod-x now runs on two machines), a loss, so it stays a shadow difference.
b.assign("b", "mod-x")
moved := p.Compose()
if !moved.Written || moved.Revision != 2 || !slices.Equal(moved.Published, []string{"b.mod-x"}) || len(moved.NotPublished) != 1 || !strings.HasPrefix(moved.NotPublished[0], "a.mod-x: "+grants.ShadowDifference) || moved.UserList.Compare.Equal() ||
!slices.Equal(moved.UserList.Compare.Added, []string{"b.mod-x"}) {
t.Errorf("a composition after placement moved = %+v", moved)
}
got, _ = p.UserList()
ul, _ := got.(map[string]any)
if st, ok := ul["written"].(grants.UserListState); !ok || st.Loaded == nil || st.Loaded.Digest != "sha256:aa" || st.BusNode != "a" ||
ul["revision"] != uint64(2) || !gapNamed(st.Gaps, "people") {
t.Errorf("user-list = %+v", got)
}
a, err := p.AssertOne("mod-x")
am, _ := a.(map[string]any)
as, _ := am["assertion"].(grants.Assertion)
if err != nil || as.Asserted || as.Says != grants.ShadowAsserts || len(as.Items) != 3 || am["asked"] == nil {
t.Fatalf("assert = %+v, %v", a, err)
}
for _, it := range as.Items {
if it.Stream == "KV_mod-x_kept" && len(it.Differences) != 0 {
t.Errorf("a bucket the bus holds as composed differs: %v", it.Differences)
}
if it.Stream == "THINGS" && !slices.Contains(it.Differences, "the bus holds no stream THINGS") {
t.Errorf("a stream the bus lacks is not named: %+v", it)
}
}
ms, _ := p.MembershipOf("a", "mod-x")
if mo, _ := ms.(map[string]any); !strings.Contains(fmt.Sprint(mo["published"]), "last_pass") || mo["composed"] == nil {
t.Errorf("membership = %v", ms)
}
gr, _ := p.Grants("mod-x")
if g, _ := gr.(map[string]any); g["writes"] != grants.WritesNoGrant || g["count"] != 1 {
t.Errorf("grants = %v", gr)
}
b.loaded = append(b.loaded, grants.User{User: "b.gone"})
fa := &fakeAsker{wheres: map[string]*asks.Warrant{}}
var forgot []string
n := 0
p.Nonce, p.Forget = func() string { n++; return fmt.Sprintf("%04d", n) }, func(id string) error { forgot = append(forgot, id); return nil }
var said []asks.Condition
p.Asker, p.Say = fa, func(c asks.Condition) error { said = append(said, c); return nil }
warrant := func(id string) asks.Warrant { return asks.Warrant{Ask: id, Outcome: asks.OutcomeChosen} }
last := func() grants.Act {
raw, _, _, _ := b.ownGet(grants.StateTakeover, grants.KeySwitch)
var s grants.Switch
_ = json.Unmarshal(raw, &s)
if s.Open != "" && s.Acts[len(s.Acts)-1].Said != "asked" {
t.Errorf("an ask is open and its act is not asked: %+v", s)
}
return s.Acts[len(s.Acts)-1]
}
asked := func(act string) string {
t.Helper()
var res struct{ Asked string }
var err error
var out any
if act == "take-over" {
out, err = p.TakeOver("shanks.node-tools", "a row")
} else {
out, err = p.HandBack("shanks.node-tools", "a row")
}
if err != nil {
t.Fatalf("%s: %v", act, err)
}
raw, _ := json.Marshal(out)
_ = json.Unmarshal(raw, &res)
return res.Asked
}
yes, no := asks.Option{ID: "yes"}, asks.Option{ID: "no"}
for _, row := range []struct {
name, act string
option asks.Option
setup func()
said, never []string // the refusal's words and the users it must not name
state string
outcome string
}{
{"a yes while a user differs is refused and said, naming it", "take-over", yes, func() {}, []string{"users differ", "b.mod-x"}, []string{"b.gone"}, grants.Shadow, "refused"},
{"a no leaves the switch", "take-over", no, func() {}, nil, nil, grants.Shadow, "declined"},
{"a yes with the lists agreeing and every hash held records the switch", "take-over", yes, func() {
b.states[grants.ReadAssignments] = nil
b.assign("a", "nats")
b.assign("a", "mod-x")
b.assign("b", "mod-y")
b.loaded = append(shadowOf(t, b), grants.User{User: "b.gone"})
for _, u := range shadowOf(t, b) {
b.put(grants.ReadAccounts, u.User, map[string]any{"user": u.User, "hash": "$2a$10$x", "generation": 1})
}
}, nil, nil, grants.TookOver, "recorded"},
{"a yes to hand back records the switch back", "hand-back", yes, func() {}, nil, nil, grants.Shadow, "recorded"},
} {
t.Run(row.name, func(t *testing.T) {
row.setup()
before := len(fa.asks)
id := asked(row.act)
if _, err := p.TakeOver("shanks.node-tools", "again"); err == nil || !strings.Contains(err.Error(), "already asked") {
t.Errorf("a second ask while one is open = %v", err)
}
p.Compose()
p.Compose()
if len(fa.asks) != before+1 || fa.asks[before].ID != id {
t.Fatalf("the operator was asked %d times, want once: %+v", len(fa.asks)-before, fa.asks)
}
ask := fa.asks[before]
if err := ask.Check(time.Date(2026, 10, 25, 22, 0, 0, 0, time.UTC)); err != nil {
t.Errorf("the router would refuse the ask: %v", err)
}
if yesOpt, ok := ask.Option("yes"); !ok || yesOpt.Level != asks.Approve || !strings.HasPrefix(yesOpt.Binds, "sha256:") {
t.Errorf("the yes does not approve and bind its act: %+v", yesOpt)
}
said = nil
if err := p.Warranted(row.option, warrant(id)); err != nil {
t.Fatal(err)
}
l := last()
if l.Said != row.outcome || l.Ask != id || p.SwitchState() != row.state {
t.Errorf("the act = %+v, the switch %s; want %s and %s", l, p.SwitchState(), row.outcome, row.state)
}
for _, w := range row.said {
if !strings.Contains(l.Refused, w) || (row.outcome == "refused" && (len(said) != 1 || !strings.Contains(said[0].Explanation, w))) {
t.Errorf("the refusal %q (said %v) does not say %q", l.Refused, said, w)
}
}
for _, w := range row.never {
if strings.Contains(l.Refused, w) {
t.Errorf("the refusal names %q", w)
}
}
// A recorded switch says no refusal and clears the one a refused yes left raised.
if row.outcome == "recorded" && row.act == "take-over" && (len(said) != 1 || said[0].State != asks.StateCleared || said[0].Key != "mesh-grants.take-over-refused") {
t.Errorf("a recorded switch did not clear the refusal: %v", said)
}
if row.outcome == "recorded" && !strings.Contains(fmt.Sprint(p.TakeOverState()), "bus_node_loaded_grants_list:not yet") {
t.Errorf("the state claims the bus node follows with no loaded list: %v", fmt.Sprint(p.TakeOverState()))
}
// The key the bus node follows turns with the switch at once, and the state says switched only once the bus
// node loaded the grants part's written list.
g, _, _ := p.Sealed()
if want := map[string]string{grants.TookOver: delivery.FollowGrants, grants.Shadow: delivery.FollowController}[p.SwitchState()]; g.Follow != want {
t.Errorf("the key says follow %s with the switch %s", g.Follow, p.SwitchState())
}
effect := func() string { st, _ := p.TakeOverState(); return fmt.Sprint(st.(map[string]any)["effect"]) }
if row.outcome == "recorded" && row.act == "take-over" {
if e := effect(); strings.HasPrefix(e, "switched") || !strings.Contains(e, "not in effect") {
t.Errorf("effect before the bus node loaded = %s", e)
}
if users := strings.Count(open(g), "{ user: "); users != g.Users || g.Users == 0 || len(g.LeftOut) != 0 {
t.Errorf("the sealed list holds %d users, the key says %d, left out %v", users, g.Users, g.LeftOut)
}
withKey(delivery.UserListWritten{Source: delivery.GrantsUserListSource, Revision: g.Revision, Wanted: g.Digest, Digest: g.Digest, Users: g.Users, Follows: delivery.FollowGrants})
if e := effect(); !strings.HasPrefix(e, "switched") {
t.Errorf("effect once the bus node loaded the written list = %s", e)
}
withKey(loadedBefore)
// After take-over a list composed past a failed read, or one dropping a user of the list in force for
// want of a hash, is not sealed: the list in force stays, and the pass says why.
b.refused[grants.ReadAccounts] = true
if res := p.Compose(); res.Sealed == nil || !strings.Contains(res.Sealed.Why, "a read of this pass failed") {
t.Errorf("a pass past a failed read = %+v", res.Sealed)
}
// While the hold lasts the warning stands, said once however many passes find it; it names why.
p.Compose()
nHeld := 0
for _, c := range said {
if c.Key == grants.HeldCondition && c.State == asks.StateRaised && c.Severity == asks.SeverityWarning && strings.Contains(c.Summary, "a read of this pass failed") {
nHeld++
}
}
if nHeld != 1 {
t.Errorf("the held list raised the warning %d times, want once: %+v", nHeld, said)
}
b.refused[grants.ReadAccounts] = false
if now, _, _ := p.Sealed(); now.Digest != g.Digest || now.Object != g.Object {
t.Errorf("the list in force moved past a failed read: %s, was %s", now.Digest, g.Digest)
}
b.mu.Lock()
hash := b.states[grants.ReadAccounts]["a.mod-x"]
delete(b.states[grants.ReadAccounts], "a.mod-x")
b.mu.Unlock()
if res := p.Compose(); res.Sealed == nil || !strings.Contains(res.Sealed.Why, "a.mod-x would be left out") {
t.Errorf("a pass that drops a.mod-x's hash = %+v", res.Sealed)
}
if now, _, _ := p.Sealed(); now.Digest != g.Digest {
t.Errorf("the list in force moved without a.mod-x: %s", now.Digest)
}
// A list in force written before the part kept its users (no sealed-users): checked against the users the
// bus loaded, so the first list after take-over has the same protection.
b.mu.Lock()
delete(b.own[grants.StateUserList], grants.KeyWrittenUsers)
b.mu.Unlock()
if res := p.Compose(); res.Sealed == nil || !strings.Contains(res.Sealed.Why, "a.mod-x would be left out") {
t.Errorf("a pass with no kept users that drops a.mod-x's hash = %+v", res.Sealed)
}
if now, _, _ := p.Sealed(); now.Digest != g.Digest {
t.Errorf("the list in force moved without a.mod-x and no kept users: %s", now.Digest)
}
b.put(grants.ReadAccounts, "a.mod-x", json.RawMessage(hash))
if p.Compose(); func() bool { now, _, _ := p.Sealed(); return now.Digest != g.Digest }() {
t.Errorf("the list in force moved with every read and hash back")
}
if last := said[len(said)-1]; last.Key != grants.HeldCondition || last.State != asks.StateCleared {
t.Errorf("the held warning is not cleared once nothing is held: %+v", last)
}
// A part started while the warning stood (the said state is in memory only) clears it on its first pass.
var fresh []asks.Condition
fp := partOn(b)
fp.Objects = p.Objects
fp.Asker, fp.Say = fa, func(c asks.Condition) error { fresh = append(fresh, c); return nil }
fp.Compose()
if len(fresh) == 0 || fresh[0].Key != grants.HeldCondition || fresh[0].State != asks.StateCleared {
t.Errorf("a fresh part's first pass does not say the held warning is cleared: %+v", fresh)
}
}
})
}
t.Run("hand-back in shadow is refused at once, with nothing asked", func(t *testing.T) {
n := len(fa.asks)
if _, err := p.HandBack("shanks.node-tools", "a row"); err == nil || !strings.Contains(err.Error(), "nothing to hand back") {
t.Errorf("hand-back in shadow = %v", err)
}
p.Compose()
if len(fa.asks) != n {
t.Errorf("a refused hand-back asked the operator")
}
})
t.Run("an ask that can stay open is closed every way and can be asked again", func(t *testing.T) {
// A warrant the process missed is caught up from the router's record, whatever its outcome.
id := asked("take-over")
p.Compose()
fa.wheres[id] = &asks.Warrant{Ask: id, Outcome: asks.OutcomeChosen, Option: "no"}
p.Compose()
if l := last(); l.Ask != id || l.Said != "declined" {
t.Errorf("a missed warrant was not caught up: %+v", l)
}
// The router ended it with no choice, and the process was not told.
id = asked("take-over")
p.Compose()
fa.wheres[id] = &asks.Warrant{Ask: id, Outcome: asks.OutcomeCancelled}
p.Compose()
if l := last(); l.Ask != id || !strings.Contains(l.Said, "cancelled") {
t.Errorf("an ask the router ended was not closed: %+v", l)
}
// Said closes on any outcome for the open id, and none for another id.
id = asked("take-over")
p.Said(asker.Refused, asks.Warrant{Ask: "other", Outcome: asks.OutcomeRefused}, nil)
if _, err := p.TakeOver("x", "y"); err == nil {
t.Errorf("a warrant for another ask closed the open one")
}
p.Said(asker.Refused, asks.Warrant{Ask: id, Outcome: asks.OutcomeRefused}, nil)
// A warrant for an ask that is not the open one switches nothing.
if err := p.Warranted(yes, warrant("take-over-other")); err == nil {
t.Errorf("a warrant for an ask that is not open was acted on")
}
// Past its expiry and a margin the part closes it itself; the clock moves, not a timer.
id = asked("take-over")
now := p.Now
p.Now = func() time.Time { return now().Add(25 * time.Hour) }
p.Compose()
p.Now = now
if l := last(); l.Ask != id || l.Said != string(asks.OutcomeExpired) {
t.Errorf("an expired ask was not closed: %+v", l)
}
// An ask that could not be published is closed as refused, its half-made record taken away, and is not retried.
fa.fail = true
id = asked("take-over")
p.Compose()
p.Compose()
if l := last(); l.Ask != id || l.Said != "refused" || !slices.Contains(forgot, id) || !slices.Contains(fa.cancels, id) {
t.Errorf("a failed ask stays open, or is not taken back at the router: %+v, forgot %v, cancelled %v", l, forgot, fa.cancels)
}
fa.fail = false
// An answer the part's own record cannot take yet (its state out of reach) leaves the ask open, closed by
// nothing, and is taken at a later pass.
id = asked("take-over")
p.Compose()
fa.wheres[id] = &asks.Warrant{Ask: id, Outcome: asks.OutcomeChosen, Option: "no"}
fa.takeErr = errors.New("nats: no responders available for request")
p.Compose()
if l := last(); l.Ask != id || l.Said != "asked" {
t.Errorf("an answer not taken closed the ask: %+v", l)
}
fa.takeErr = nil
p.Compose()
if l := last(); l.Ask != id || l.Said != "declined" {
t.Errorf("the answer was not taken once its record could be reached: %+v", l)
}
asked("take-over")
})
t.Run("only memberships published, nothing asserted", func(t *testing.T) {
for _, s := range b.publishes {
if !strings.HasPrefix(s, "mesh.assignment.") {
t.Errorf("published on %s", s)
}
}
for _, w := range b.writes {
if !slices.Contains(grants.States, w) {
t.Errorf("wrote %s, which is not the part's own state", w)
}
}
for _, c := range b.calls {
if c != grants.CallUsers && c != grants.CallStreams {
t.Errorf("called %s: the part calls only the bus module's read tools", c)
}
}
for _, e := range b.events {
if !slices.Contains(grants.Emits, e) {
t.Errorf("said %s", e)
}
}
})
})
t.Run("memberships: a first or an additive one published, a loss never", func(t *testing.T) {
const key = "a.mod-x"
composed := func(b *fakeBus) map[string]any {
recs, _ := grants.Records(grants.ReadInputs(b, &grants.Manifests{}))
raw, _ := json.Marshal(grants.Memberships(recs)[key].Membership)
var m map[string]any
_ = json.Unmarshal(raw, &m)
return m
}
for _, row := range []struct {
name string
last func(map[string]any) map[string]any
published bool
names string
}{
{"a first membership is published", nil, true, ""},
{"an additive change is published, the private network carried", func(m map[string]any) map[string]any {
delete(m, "state")
m["mesh"] = []any{"10.0.0.1"}
return m
}, true, ""},
{"a change that drops verified-sender is not published, and named", func(m map[string]any) map[string]any {
m["seat-traffic"] = map[string]any{"kinds": []any{map[string]any{"seat": "channel", "kind": "telegram", "capabilities": []any{"verified-sender"}}}}
return m
}, false, "verified-sender"},
{"a change that drops any other field is not published", func(m map[string]any) map[string]any {
m["serves"] = append(m["serves"].([]any), map[string]any{"subject": "mesh.mod.mod-x.tool.gone"})
return m
}, false, "tool.gone"},
// A verb the seat catalogue retired on purpose (mark-ready, mesh-sdk d506f21, ADR 0421) is dropped and named; a
// verb of a seat the catalogue does not define is still a loss (issue 1945).
{"a change that drops only a retired seat verb is published, the verb named", func(m map[string]any) map[string]any {
m["seats"] = []any{map[string]any{"seat": "mesh-delivery", "verb": "mark-ready", "subject": "mesh.seat.mesh-delivery.tool.mark-ready"}}
return m
}, true, "mesh-delivery mark-ready"},
{"a retired verb and a module seat's verb dropped: not published, the module seat's named", func(m map[string]any) map[string]any {
m["seats"] = []any{map[string]any{"seat": "mesh-delivery", "verb": "mark-ready", "subject": "mesh.seat.mesh-delivery.tool.mark-ready"},
map[string]any{"seat": "telegram-sender", "verb": "send", "subject": "mesh.seat.telegram-sender.tool.send"}}
return m
}, false, "telegram-sender"},
} {
t.Run(row.name, func(t *testing.T) {
b := fixture()
b.loaded = shadowOf(t, b)
if row.last != nil {
raw, _ := json.Marshal(row.last(composed(b)))
b.stream = map[string][]byte{"mesh.assignment." + key: raw}
b.seqs = map[string]uint64{"mesh.assignment." + key: 4}
}
res := partOn(b).Compose()
if got := slices.Contains(res.Published, key); got != row.published {
t.Fatalf("published %v, not published %v", res.Published, res.NotPublished)
}
if row.published {
if row.names == "" && row.last != nil && !strings.Contains(string(b.stream["mesh.assignment."+key]), "10.0.0.1") {
t.Errorf("the private network of the last membership is not carried: %s", b.stream["mesh.assignment."+key])
}
if raw, _, _, _ := b.ownGet(grants.StateMemberships, key); !strings.Contains(string(raw), row.names) {
t.Errorf("the pass does not name %q: %s", row.names, raw)
}
return
}
said := strings.Join(res.NotPublished, "\n")
if !strings.Contains(said, grants.ShadowDifference) || !strings.Contains(said, row.names) {
t.Errorf("the loss is not named: %s", said)
}
ms, _ := partOn(b).MembershipOf("a", "mod-x")
if !strings.Contains(fmt.Sprint(ms), row.names) {
t.Errorf("grants_membership does not name what would be lost: %v", ms)
}
})
}
t.Run("a refused publish: no exit, said, published at the next event", func(t *testing.T) {
b := fixture()
b.loaded = shadowOf(t, b)
b.refusePub = true
p := partOn(b)
refused := p.Compose()
if len(refused.Published) != 0 || len(refused.NotPublished) != 3 || !strings.Contains(refused.NotPublished[0], "Permissions Violation") {
t.Errorf("a refused publish = %+v: said, and nothing published", refused)
}
b.refusePub = false
if next := p.Compose(); !slices.Equal(next.Published, []string{"a.mod-x", "a.nats", "b.mod-y"}) {
t.Errorf("the next event published %v", next.Published)
}
})
t.Run("a tool answers while a pass waits on the bus", func(t *testing.T) {
b := fixture()
b.loaded = shadowOf(t, b)
b.hold = make(chan struct{})
p := partOn(b)
done := make(chan struct{})
go func() { p.Compose(); close(done) }()
answered := make(chan struct{})
go func() {
_, _ = p.UserList()
_, _ = p.MembershipOf("a", "mod-x")
_, _ = p.Grants("")
_ = p.Last()
close(answered)
}()
select {
case <-answered:
case <-time.After(10 * time.Second):
t.Error("a tool waited on the pass's round trip")
}
close(b.hold)
<-done
})
})
t.Run("assertions: what the bus lacks is created once, what it holds is never touched", func(t *testing.T) {
b := fixture()
b.loaded = shadowOf(t, b)
// The bus holds mod-x's bucket with another history, and neither its log nor its stream.
b.streams = []map[string]any{{"name": "KV_mod-x_kept", "subjects": []string{"$KV.mod-x_kept.>"}, "retention": "limits", "max_per_subject": 2}}
p := partOn(b)
p.Create = b
assertion := func() grants.Assertion {
raw, _, _, _ := b.ownGet(grants.StateAssertions, "mod-x")
var a grants.Assertion
_ = json.Unmarshal(raw, &a)
return a
}
first := p.Compose()
if !slices.Equal(b.creates, []string{"LOG_mod-x_history", "THINGS"}) || len(first.Asserted) != 1 ||
!strings.HasPrefix(first.Asserted[0], "mod-x at ") || !slices.Contains(b.events, grants.EventAsserted) {
t.Fatalf("the first pass created %v, said %v, events %v", b.creates, first.Asserted, b.events)
}
a := assertion()
if !a.Asserted || a.Says != grants.CreatesMissing {
t.Errorf("the assertion kept = %+v", a)
}
for _, it := range a.Items {
switch it.Stream {
case "KV_mod-x_kept":
if it.Created != "" || !slices.Contains(it.Differences, "history 2 held, 3 composed") {
t.Errorf("a bucket the bus holds was touched, or its difference not said: %+v", it)
}
default:
if it.Created != grants.ItemCreated {
t.Errorf("%s: %q, want created", it.Stream, it.Created)
}
}
}
if again := p.Compose(); len(again.Asserted) != 0 || len(b.creates) != 2 {
t.Errorf("a pass with everything held created again: %v, %v", again.Asserted, b.creates)
}
// THINGS deleted since: not made again at the same manifest, said; a hand assert (the tool, asking the process)
// makes it again.
b.mu.Lock()
b.streams = slices.DeleteFunc(b.streams, func(s map[string]any) bool { return s["name"] == "THINGS" })
b.mu.Unlock()
gone := p.Compose()
if len(b.creates) != 2 || !strings.Contains(strings.Join(gone.NotAsserted, " "), "THINGS: created at this manifest and gone since") {
t.Errorf("a stream gone since was made again by itself, or not said: %v, %v", b.creates, gone.NotAsserted)
}
tool := partOn(b)
if got, err := tool.AssertOne("mod-x"); err != nil || fmt.Sprint(got) == "" || len(b.creates) != 2 {
t.Fatalf("the tool = %v, %v, creates %v: it asks, and creates nothing itself", got, err, b.creates)
}
if hand := p.Compose(); len(hand.Asserted) != 1 || b.creates[len(b.creates)-1] != "THINGS" {
t.Errorf("the hand assert did not create THINGS again: %v, %v", hand.Asserted, b.creates)
}
if after := p.Compose(); len(after.Asserted) != 0 {
t.Errorf("the hand ask was acted on twice: %v", after.Asserted)
}
for _, c := range b.creates {
if c == "KV_mod-x_kept" {
t.Error("the bucket the bus holds was asked to be created")
}
}
})
t.Run("assertions: a refused create is said, never stops it, and is tried at the next event", func(t *testing.T) {
b := fixture()
b.loaded = shadowOf(t, b)
b.refuseCreate = true
p := partOn(b)
p.Create = b
refused := p.Compose()
if len(refused.Asserted) != 0 || !strings.Contains(strings.Join(refused.NotAsserted, " "), "Permissions Violation") ||
slices.Contains(b.events, grants.EventAsserted) {
t.Fatalf("a refused create = %+v", refused)
}
b.refuseCreate = false
if next := p.Compose(); len(next.Asserted) != 1 || !strings.Contains(next.Asserted[0], "KV_mod-x_kept") {
t.Errorf("the next event created %v", next.Asserted)
}
// What the bus holds unread: nothing is created.
c := fixture()
c.loaded = shadowOf(t, c)
q := partOn(c)
q.Create = c
q.Call = refusingStreams{c}
if r := q.Compose(); len(c.creates) != 0 || len(r.Asserted) != 0 {
t.Errorf("created %v with what the bus holds unread", c.creates)
}
})
t.Run("memberships held back: every one is said and listed", func(t *testing.T) {
b := fixture()
b.loaded = shadowOf(t, b)
recs, _ := grants.Records(grants.ReadInputs(b, &grants.Manifests{}))
b.stream, b.seqs = map[string][]byte{}, map[string]uint64{}
for i, key := range []string{"a.mod-x", "b.mod-y"} {
raw, _ := json.Marshal(grants.Memberships(recs)[key].Membership)
var m map[string]any
_ = json.Unmarshal(raw, &m)
m["serves"] = append(m["serves"].([]any), map[string]any{"subject": "mesh.mod.lost.tool.gone"})
raw, _ = json.Marshal(m)
subject := "mesh.assignment." + key
b.stream[subject], b.seqs[subject] = raw, uint64(i+1)
}
var logged []string
var lmu sync.Mutex
w := &fakeWatcher{opened: make(chan string, 16)}
f := &grants.Follower{Part: partOn(b), Watch: w, Logf: func(format string, args ...any) {
lmu.Lock()
logged = append(logged, fmt.Sprintf(format, args...))
lmu.Unlock()
}}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() { f.Run(ctx); close(done) }()
waitFor(t, func() bool {
lmu.Lock()
defer lmu.Unlock()
all := strings.Join(logged, "\n")
return strings.Contains(all, "held a.mod-x: ") && strings.Contains(all, "held b.mod-y: ")
})
cancel()
<-done
held, err := partOn(b).MembershipOf("", "")
h, _ := held.(map[string]any)
if err != nil || h["count"] != 2 || !strings.Contains(fmt.Sprint(h["held"]), "tool.gone") {
t.Errorf("grants_membership with neither node nor module = %v, %v", held, err)
}
if _, err := partOn(b).MembershipOf("a", ""); err == nil {
t.Error("a node without a module was answered")
}
})
t.Run("a refused read or write never stops it", func(t *testing.T) {
b := fixture()
b.loaded = shadowOf(t, b)
b.refuseTo[grants.StateMemberships] = true
res := partOn(b).Compose()
if !res.Written || len(res.Errors) == 0 || !strings.Contains(strings.Join(res.Errors, " "), "memberships") {
t.Errorf("a refused write of memberships = %+v: the user list is written all the same, the refusal said", res)
}
w := &fakeWatcher{refuse: map[string]int{grants.ReadModules: 1}, opened: make(chan string, 16)}
var logged []string
var lmu sync.Mutex
f := &grants.Follower{Part: partOn(b), Watch: w, Logf: func(format string, args ...any) {
lmu.Lock()
logged = append(logged, fmt.Sprintf(format, args...))
lmu.Unlock()
}}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() { f.Run(ctx); close(done) }()
waitOpened(t, w, grants.ReadAssignments)
if r := f.Refused(); r[grants.ReadModules] == "" {
waitFor(t, func() bool { return f.Refused()[grants.ReadModules] != "" })
}
w.change(grants.ReadAssignments, "a.mod-x")
waitOpened(t, w, grants.ReadModules)
waitFor(t, func() bool { return len(f.Refused()) == 0 })
select {
case <-done:
t.Fatal("the follower stopped")
default:
}
cancel()
<-done
lmu.Lock()
defer lmu.Unlock()
if !strings.Contains(strings.Join(logged, "\n"), "cannot be watched yet") {
t.Errorf("the refusal was not said: %v", logged)
}
})
}
type fakeAsker struct {
asks []asks.Ask
wheres map[string]*asks.Warrant // the router's warrants, by ask
fail bool
cancels []string
// takeErr makes Take fail as the asker does when the part's own record cannot be reached: no handling, an error.
takeErr error
}
func (f *fakeAsker) Cancel(id string) error { f.cancels = append(f.cancels, id); return nil }
// fakeObjects is the part's object bucket of sealed lists.
type fakeObjects struct{ objects map[string][]byte }
func (o *fakeObjects) PutObject(name string, data []byte) error {
if o.objects == nil {
o.objects = map[string][]byte{}
}
o.objects[name] = data
return nil
}
func (o *fakeObjects) DeleteObject(name string) error { delete(o.objects, name); return nil }
func (f *fakeAsker) Ask(a asks.Ask) error {
if f.fail {
return errors.New("the bus refused the publish")
}
f.asks = append(f.asks, a)
return nil
}
func (f *fakeAsker) Where(id string) (string, *asks.Warrant, error) { return "open", f.wheres[id], nil }
// Take hands the warrant to act as the SDK's asker does for one that chose, and records any other.
func (f *fakeAsker) Take(w asks.Warrant, act func(asks.Option, asks.Warrant) error) (asker.Handled, error) {
if f.takeErr != nil {
return "", f.takeErr
}
if w.Outcome != asks.OutcomeChosen {
return asker.Recorded, nil
}
return asker.Acted, act(asks.Option{ID: w.Option}, w)
}
func nonNil(s []string) []string {
if s == nil {
return []string{}
}
return s
}
// fakeWatcher refuses a state's first watches as told, and hands changes to the watches it opened.
// refusingStreams is the bus with nats_streams refused: what the bus holds cannot be read.
type refusingStreams struct{ *fakeBus }
func (r refusingStreams) Call(key string, body any) (json.RawMessage, error) {
if key == grants.CallStreams {
return nil, errors.New("refused")
}
return r.fakeBus.Call(key, body)
}
type fakeWatcher struct {
mu sync.Mutex
refuse map[string]int
changes map[string]func(string, []byte)
opened chan string
}
func (w *fakeWatcher) Watch(state string, changed func(string, []byte), _ func(error)) error {
w.mu.Lock()
defer w.mu.Unlock()
if w.refuse[state] > 0 {
w.refuse[state]--
return errors.New("Permissions Violation for Publish to $JS.API.STREAM.INFO.KV_" + state)
}
if w.changes == nil {
w.changes = map[string]func(string, []byte){}
}
w.changes[state] = changed
w.opened <- state
return nil
}
func (w *fakeWatcher) change(state, key string) {
w.mu.Lock()
h := w.changes[state]
w.mu.Unlock()
h(key, []byte(`{}`))
}
func waitOpened(t *testing.T, w *fakeWatcher, state string) {
t.Helper()
deadline := time.After(10 * time.Second)
for {
select {
case s := <-w.opened:
if s == state {
return
}
case <-deadline:
t.Fatalf("%s was never watched", state)
}
}
}
func waitFor(t *testing.T, ok func() bool) {
t.Helper()
for end := time.Now().Add(10 * time.Second); time.Now().Before(end); time.Sleep(10 * time.Millisecond) {
if ok() {
return
}
}
t.Fatal("never came")
}