The grants part copies them from the manifest-accepted entry, so the tool runner checks an operation against its type without reading another part's state (ADR 0596; the lead's ruling on issue 1966 B1).
125 lines
6.0 KiB
Go
125 lines
6.0 KiB
Go
package membership
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// A credential is read out of a bus address, and the bare address carries no credential and a scheme.
|
|
func TestACredentialIsReadOutOfABusAddress(t *testing.T) {
|
|
for _, c := range []struct{ in, user, password, bare, schemed string }{
|
|
{"nats://controller:secret@127.0.0.1:4222", "controller", "secret", "nats://127.0.0.1:4222", "nats://127.0.0.1:4222"},
|
|
{"controller:secret@127.0.0.1:4222", "controller", "secret", "127.0.0.1:4222", "nats://127.0.0.1:4222"},
|
|
{"nats://127.0.0.1:4222", "", "", "nats://127.0.0.1:4222", "nats://127.0.0.1:4222"},
|
|
{"127.0.0.1:4222", "", "", "127.0.0.1:4222", "nats://127.0.0.1:4222"},
|
|
// A password containing an at-sign: split on the last one.
|
|
{"nats://controller:a@b@127.0.0.1:4222", "controller", "a@b", "nats://127.0.0.1:4222", "nats://127.0.0.1:4222"},
|
|
} {
|
|
user, password, bare := CredentialIn(c.in)
|
|
if user != c.user || password != c.password || bare != c.bare {
|
|
t.Errorf("%q read as %q/%q at %q; wanted %q/%q at %q", c.in, user, password, bare, c.user, c.password, c.bare)
|
|
}
|
|
if got := BareAddress(c.in); got != c.schemed {
|
|
t.Errorf("BareAddress(%q) = %q, want %q", c.in, got, c.schemed)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAMembershipIsPublishedAndReadAtTheSubjectDerivedFromTheTwoNames(t *testing.T) {
|
|
if got := Subject("ace", "route-proxy"); got != "mesh.assignment.ace.route-proxy" {
|
|
t.Errorf("subject: %q", got)
|
|
}
|
|
if got := DirectGetSubject("ace", "route-proxy"); got != "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.ace.route-proxy" {
|
|
t.Errorf("direct get: %q", got)
|
|
}
|
|
if AssignmentsStream != "ASSIGNMENTS" {
|
|
t.Errorf("stream: %q", AssignmentsStream)
|
|
}
|
|
}
|
|
|
|
// The shape on the wire is the controller's: a membership as issued reads back whole, with the routes kept raw.
|
|
func TestAMembershipRoundTripsAsIssued(t *testing.T) {
|
|
in := Membership{Node: "ace", Module: "route-proxy", Emits: "e", Tools: "t",
|
|
Serves: []Served{{Subject: "s", Queue: "q"}},
|
|
Receives: map[string]json.RawMessage{"route": json.RawMessage(`[{"name":"a"}]`)},
|
|
Mesh: []string{"10.10.0.7"},
|
|
State: []StateIssued{{Name: "n", Bucket: "b", Writes: true, Key: "ace"}},
|
|
SeatTraffic: &SeatTraffic{Publish: []string{"p"}, Kinds: []KindHeld{{Seat: "channel", Kind: "tg"}}}}
|
|
raw, err := json.Marshal(in)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var out Membership
|
|
if err := json.Unmarshal(raw, &out); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(in, out) {
|
|
t.Fatalf("round trip changed it:\n%+v\n%+v", in, out)
|
|
}
|
|
// 1966/AC4: a membership with a ledger binding, its types among it, round-trips; one without bindings encodes as before, byte for byte.
|
|
bound := in
|
|
bound.Bindings = []BusBinding{{Local: "records", Storage: "ledger", Template: "ledger", History: "forge_records",
|
|
Subject: "forge_records.>", State: "forge_records_state", Indexes: map[string]string{"search": "forge_records_search"},
|
|
Snapshot: "forge_records_snapshot.>",
|
|
Types: []catalogue.LedgerType{{Name: "decision", Schema: map[string]catalogue.LedgerField{"title": {Type: "text"}},
|
|
Operations: []catalogue.LedgerOperation{{Name: "propose", Effect: catalogue.EffectSetFields, Fields: []string{"title"}}}}}}}
|
|
boundRaw, err := json.Marshal(bound)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var boundBack Membership
|
|
if err := json.Unmarshal(boundRaw, &boundBack); err != nil || !reflect.DeepEqual(bound, boundBack) {
|
|
t.Fatalf("a membership with a ledger binding read back as %+v (%v)", boundBack, err)
|
|
}
|
|
before, _ := json.Marshal(Membership{Node: "ace", Module: "forge", Tools: "t", Emits: "e", Serves: []Served{{Subject: "s"}},
|
|
State: []StateIssued{{Name: "repos", Bucket: "forge_repos", Writes: true}}})
|
|
if string(before) != `{"node":"ace","module":"forge","serves":[{"subject":"s"}],"emits":"e","tools":"t","state":[{"name":"repos","bucket":"forge_repos","writes":true}]}` {
|
|
t.Fatalf("a membership without bindings encodes otherwise than before: %s", before)
|
|
}
|
|
var wire map[string]json.RawMessage
|
|
_ = json.Unmarshal(raw, &wire)
|
|
for _, k := range []string{"receives", "mesh", "seat-traffic"} {
|
|
if _, ok := wire[k]; !ok {
|
|
t.Errorf("field %q is not on the wire under that name", k)
|
|
}
|
|
}
|
|
|
|
// The grants part's memberships hold the bare membership through this package's codec; the wrapper written before
|
|
// 2026-10-30 still reads when it says the membership was published or unchanged, and anything else is an error,
|
|
// never an empty membership or one the part held back (issue 774).
|
|
m := Membership{Node: "ace", Module: "forge", Tools: "mesh.mod.forge.tool", Serves: []Served{{Subject: "mesh.mod.forge.tool.{tool}"}},
|
|
State: []StateIssued{{Name: "repos", Bucket: "forge_repos"}}}
|
|
inner, _ := json.Marshal(m)
|
|
wrapped := func(published string) []byte {
|
|
return []byte(`{"node":"ace","module":"forge","digest":"sha256:x","membership":` + string(inner) + published + `,"retired":["a b"]}`)
|
|
}
|
|
for _, c := range []struct {
|
|
name string
|
|
raw []byte
|
|
ok bool
|
|
}{
|
|
{"bare, as written", EncodeGrantsMembership(m), true},
|
|
{"wrapped and published, as the grants part wrote it before", wrapped(`,"published":"published"`), true},
|
|
{"wrapped and unchanged", wrapped(`,"published":"unchanged"`), true},
|
|
{"wrapped and held back", wrapped(`,"published":"not published, a shadow difference: publishing would lose x"`), false},
|
|
{"wrapped, saying nothing of its publishing", wrapped(""), false},
|
|
{"not json", []byte("garbage"), false},
|
|
{"an array", []byte(`[1,2]`), false},
|
|
{"an object naming no node or module", []byte(`{"serves":[{"subject":"s"}]}`), false},
|
|
{"a wrapper whose membership is no object", []byte(`{"node":"ace","module":"forge","membership":"x","published":"published"}`), false},
|
|
{"a wrapper whose membership names nothing", []byte(`{"node":"ace","module":"forge","membership":{},"published":"published"}`), false},
|
|
} {
|
|
got, err := DecodeGrantsMembership(c.raw)
|
|
if (err == nil) != c.ok {
|
|
t.Errorf("%s: err %v", c.name, err)
|
|
continue
|
|
}
|
|
if c.ok && !reflect.DeepEqual(got, m) {
|
|
t.Errorf("%s: read %+v, want %+v", c.name, got, m)
|
|
}
|
|
}
|
|
}
|