The controller plans config, templates and moved providers' provisions, hands bus accounts to the vault and judges the new manifest words, on the feature's SDK and node-engine validator (feature 57, config and provisioning).
308 lines
13 KiB
Go
308 lines
13 KiB
Go
package broker
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// mailStream is mesh-mail's own stream as the bus holds it (novox/hq ADR 0363).
|
|
var mailStream = ModuleStream{Module: "mesh-mail", Name: "MAIL",
|
|
Events: []string{"mail.*.message.added", "mail.*.message.changed", "mail.*.message.removed"},
|
|
MaxAgeDays: 7, MaxMiB: 1024, Restricted: true}
|
|
|
|
// mailHeard is what a reader granted the account `mesh` hears of it.
|
|
var mailHeard = Heard{Stream: "MAIL", Emitter: "mesh-mail", Restricted: true, Filters: []string{
|
|
"mesh.stream.mesh-mail.event.mail.mesh.message.added", "mesh.stream.mesh-mail.event.mail.mesh.message.changed"}}
|
|
|
|
// newsStream is a module's stream without a grant: heard by whoever asks (ADR 0363 §5), the tool runner included.
|
|
var newsStream = ModuleStream{Module: "news", Name: "NEWS", Events: []string{"story.**"}, MaxAgeDays: 7, MaxMiB: 16}
|
|
|
|
var newsHeard = Heard{Stream: "NEWS", Emitter: "news", Filters: []string{"mesh.stream.news.event.story.>"}}
|
|
|
|
// A module's stream is its own: subjects EVENTS does not claim, its age and size, kept by limits (ADR 0363 §1, §2).
|
|
func TestAModuleStreamTakesSubjectsEventsDoesNot(t *testing.T) {
|
|
s := mailStream.Stream()
|
|
if s.Name != "MAIL" || s.Retention != RetentionLimits || s.MaxAge != 7*24*60*60 || s.MaxBytes != 1024<<20 || s.DiscardNew {
|
|
t.Fatalf("composed as %+v", s)
|
|
}
|
|
want := []string{"mesh.stream.mesh-mail.event.mail.*.message.added", "mesh.stream.mesh-mail.event.mail.*.message.changed",
|
|
"mesh.stream.mesh-mail.event.mail.*.message.removed"}
|
|
if !reflect.DeepEqual(s.Subjects, want) {
|
|
t.Fatalf("subjects %v", s.Subjects)
|
|
}
|
|
for _, own := range MeshStreams() {
|
|
for _, a := range own.Subjects {
|
|
for _, b := range s.Subjects {
|
|
if SubjectsOverlap(a, b) {
|
|
t.Fatalf("%s claims %s, which overlaps %s of %s: the bus would refuse the stream", s.Name, b, a, own.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if WirePattern("mail.**") != "mail.>" {
|
|
t.Fatal("`**` is spelled `>` on the wire")
|
|
}
|
|
}
|
|
|
|
// The provider publishes its stream's events on their subjects and nowhere on EVENTS for them; a consumer
|
|
// granted a part hears it through its own consumer on the stream, and one not granted has nothing there
|
|
// (ADR 0363 §2, §5).
|
|
func TestTheProviderPublishesItsStreamAndOnlyAGrantedConsumerHearsIt(t *testing.T) {
|
|
provider, err := PermissionsFor(Principal{Kind: KindModule, Node: "novox", Module: "mesh-mail",
|
|
Streams: []ModuleStream{mailStream}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !MayPublish(provider, "mesh.stream.mesh-mail.event.mail.work.message.added") {
|
|
t.Fatal("the provider may not publish its own stream's event")
|
|
}
|
|
if MayPublish(provider, "mesh.mod.mesh-mail.event.mail.work.message.added") {
|
|
t.Fatal("the provider may publish its stream's event on EVENTS")
|
|
}
|
|
if MayPublish(provider, "mesh.stream.other.event.mail.work.message.added") {
|
|
t.Fatal("the provider may publish on another module's stream")
|
|
}
|
|
|
|
reader := Principal{Kind: KindModule, Node: "ace", Module: "reader", Hears: []Heard{mailHeard}}
|
|
perms, err := PermissionsFor(reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, s := range []string{"$JS.API.CONSUMER.MSG.NEXT.MAIL.ace_reader", "$JS.API.CONSUMER.INFO.MAIL.ace_reader",
|
|
"$JS.ACK.MAIL.ace_reader.1"} {
|
|
if !MayPublish(perms, s) {
|
|
t.Errorf("the granted reader may not publish %s", s)
|
|
}
|
|
}
|
|
if !MaySubscribe(perms, "mesh.stream.mesh-mail.event.mail.mesh.message.added") ||
|
|
MaySubscribe(perms, "mesh.stream.mesh-mail.event.mail.work.message.added") {
|
|
t.Fatal("the reader's subscription is not what it was granted")
|
|
}
|
|
if MayPublish(perms, "$JS.API.CONSUMER.MSG.NEXT.MAIL.ace_other") {
|
|
t.Fatal("the reader may pull another's consumer")
|
|
}
|
|
|
|
stranger, err := PermissionsFor(Principal{Kind: KindModule, Node: "ace", Module: "stranger",
|
|
Consumes: []string{"billing.paid"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if MayPublish(stranger, "$JS.API.CONSUMER.MSG.NEXT.MAIL.ace_stranger") ||
|
|
MaySubscribe(stranger, "mesh.stream.mesh-mail.event.mail.mesh.message.added") {
|
|
t.Fatal("a module not granted reaches the stream")
|
|
}
|
|
}
|
|
|
|
// A granted consumer of its own account has a durable consumer on the stream, with its granted subjects as
|
|
// filters, and an EVENTS consumer that hears what it gives up on delivered again (ADR 0363 §5, §7); a module of its
|
|
// own account is no module the runtime carries, so the runtime reads neither (ADR 0376 §1).
|
|
func TestAGrantedConsumerHasAConsumerOnTheStream(t *testing.T) {
|
|
records := Records{Nodes: []string{"ace", "novox"}, Assigned: map[string][]Declared{
|
|
"novox": {{Module: "mesh-mail", Streams: []ModuleStream{mailStream}}},
|
|
"ace": {{Module: RuntimeModule}, {Module: "reader", RunsAs: "reader", Hears: []Heard{mailHeard}, HearsAStream: true},
|
|
{Module: "stranger", HearsAStream: true}},
|
|
}}
|
|
users, err := Users(records)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var onMail, onEvents []Consumer
|
|
for _, c := range ConsumersOf(users) {
|
|
switch c.Consumer.Stream {
|
|
case "MAIL":
|
|
onMail = append(onMail, c.Consumer)
|
|
case EventsStream:
|
|
onEvents = append(onEvents, c.Consumer)
|
|
}
|
|
}
|
|
if len(onMail) != 1 || onMail[0].Name != "ace_reader" || !slices.Equal(onMail[0].Filters, mailHeard.Filters) {
|
|
t.Fatalf("on MAIL: %+v", onMail)
|
|
}
|
|
if len(onEvents) != 1 || onEvents[0].Name != "ace_reader" || !slices.Equal(onEvents[0].Filters, []string{AgainFilter("ace_reader")}) {
|
|
t.Fatalf("on EVENTS: %+v", onEvents)
|
|
}
|
|
for _, u := range users {
|
|
if u.Node != "ace" || (u.Kind != KindNodeTools && u.Module != "reader") {
|
|
continue
|
|
}
|
|
perms, err := PermissionsFor(u)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reads := MayPublish(perms, "$JS.API.CONSUMER.MSG.NEXT.MAIL.ace_reader") && MayPublish(perms, "$JS.ACK.MAIL.ace_reader.1")
|
|
if u.Kind == KindNodeTools && reads {
|
|
t.Fatal("the runtime may read the consumer on MAIL of a module of its own account")
|
|
}
|
|
if u.Kind == KindModule && !reads {
|
|
t.Fatal("the reader's own account may not read its consumer on MAIL")
|
|
}
|
|
if MayPublish(perms, "$JS.API.CONSUMER.MSG.NEXT.MAIL.ace_stranger") {
|
|
t.Fatal("a consumer no module is granted may be read")
|
|
}
|
|
}
|
|
}
|
|
|
|
// **The tool runner never hears a restricted stream** (ADR 0376 §1, §2): a module it carries, granted part of
|
|
// MAIL, gets no consumer on MAIL, the tool runner's account no grant to ask about, pull or acknowledge one there,
|
|
// and its membership no word to bind one; a module of its own account granted the same part gets all three, on
|
|
// its own account. The grant that composes nothing is said. A stream without a grant is heard through the tool
|
|
// runner as before.
|
|
func TestTheToolRunnerNeverHearsARestrictedStream(t *testing.T) {
|
|
records := Records{Nodes: []string{"ace", "novox"}, Assigned: map[string][]Declared{
|
|
"novox": {{Module: "mesh-mail", Streams: []ModuleStream{mailStream}}, {Module: "news", Streams: []ModuleStream{newsStream}}},
|
|
"ace": {{Module: RuntimeModule},
|
|
{Module: "carried", Hears: []Heard{mailHeard, newsHeard}, HearsAStream: true},
|
|
{Module: "mailer", RunsAs: "mailer", Hears: []Heard{mailHeard}, HearsAStream: true}},
|
|
}}
|
|
users, err := Users(records)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var onMail, onNews []string
|
|
for _, c := range ConsumersOf(users) {
|
|
switch c.Consumer.Stream {
|
|
case "MAIL":
|
|
onMail = append(onMail, c.Consumer.Name)
|
|
case "NEWS":
|
|
onNews = append(onNews, c.Consumer.Name)
|
|
}
|
|
}
|
|
if !slices.Equal(onMail, []string{"ace_mailer"}) {
|
|
t.Fatalf("consumers on MAIL: %v, want only the mailer's", onMail)
|
|
}
|
|
if !slices.Equal(onNews, []string{"ace_carried"}) {
|
|
t.Fatalf("consumers on NEWS: %v, want the carried module's as before", onNews)
|
|
}
|
|
for _, u := range users {
|
|
if u.Node != "ace" {
|
|
continue
|
|
}
|
|
perms, err := PermissionsFor(u)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
switch {
|
|
case u.Kind == KindNodeTools:
|
|
for _, s := range perms.Publish {
|
|
if strings.Contains(s, ".MAIL.") {
|
|
t.Errorf("the tool runner's account may publish %s", s)
|
|
}
|
|
}
|
|
for _, s := range perms.Subscribe {
|
|
if strings.HasPrefix(s, "mesh.stream.mesh-mail.") {
|
|
t.Errorf("the tool runner's account may subscribe %s", s)
|
|
}
|
|
}
|
|
if !MayPublish(perms, "$JS.API.CONSUMER.MSG.NEXT.NEWS.ace_carried") {
|
|
t.Error("the tool runner may no longer read the carried module's consumer on a stream without a grant")
|
|
}
|
|
case u.Kind == KindModule && u.Module == "carried":
|
|
if MayPublish(perms, "$JS.API.CONSUMER.MSG.NEXT.MAIL.ace_carried") {
|
|
t.Error("a module without an account of its own may pull a consumer on MAIL")
|
|
}
|
|
case u.Kind == KindModule && u.Module == "mailer":
|
|
for _, s := range []string{"$JS.API.CONSUMER.INFO.MAIL.ace_mailer", "$JS.API.CONSUMER.MSG.NEXT.MAIL.ace_mailer",
|
|
"$JS.ACK.MAIL.ace_mailer.1"} {
|
|
if !MayPublish(perms, s) {
|
|
t.Errorf("the mailer's own account may not publish %s", s)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
where := Placements{Nodes: map[string][]string{}}
|
|
carried := MembershipFor("ace", records.Assigned["ace"][1], where)
|
|
if !reflect.DeepEqual(carried.Hears, []HeardIssued{{Stream: "NEWS", Consumer: "ace_carried"}}) {
|
|
t.Fatalf("the carried module is told to bind %+v", carried.Hears)
|
|
}
|
|
mailer := MembershipFor("ace", records.Assigned["ace"][2], where)
|
|
if !reflect.DeepEqual(mailer.Hears, []HeardIssued{{Stream: "MAIL", Consumer: "ace_mailer"}}) {
|
|
t.Fatalf("the mailer is told to bind %+v", mailer.Hears)
|
|
}
|
|
withheld := WithheldOf(records)
|
|
if len(withheld) != 1 || withheld[0] != (Withheld{Node: "ace", Module: "carried", Stream: "MAIL", Emitter: "mesh-mail"}) {
|
|
t.Fatalf("withheld %+v", withheld)
|
|
}
|
|
if said := withheld[0].Said(); !strings.Contains(said, "carried on ace") || !strings.Contains(said, "MAIL") ||
|
|
!strings.Contains(said, "runs-as") {
|
|
t.Fatalf("said %q", said)
|
|
}
|
|
}
|
|
|
|
// The membership says where a module's stream events go and which consumers on module streams to bind
|
|
// (ADR 0363 §2, §8).
|
|
func TestTheMembershipNamesTheStreamAndWhatIsHeard(t *testing.T) {
|
|
where := Placements{Nodes: map[string][]string{}}
|
|
provider := MembershipFor("novox", Declared{Module: "mesh-mail", Streams: []ModuleStream{mailStream}}, where)
|
|
if len(provider.Streams) != 1 || provider.Streams[0].Stream != "MAIL" ||
|
|
provider.Streams[0].Subject != "mesh.stream.mesh-mail.event.{event}" ||
|
|
!slices.Equal(provider.Streams[0].Events, mailStream.Events) {
|
|
t.Fatalf("the provider's membership: %+v", provider.Streams)
|
|
}
|
|
reader := MembershipFor("ace", Declared{Module: "reader", RunsAs: "reader", Hears: []Heard{mailHeard}}, where)
|
|
if !reflect.DeepEqual(reader.Hears, []HeardIssued{{Stream: "MAIL", Consumer: "ace_reader"}}) {
|
|
t.Fatalf("the reader's membership: %+v", reader.Hears)
|
|
}
|
|
if none := MembershipFor("ace", Declared{Module: "stranger", HearsAStream: true}, where); none.Hears != nil {
|
|
t.Fatalf("a module not granted is told to bind %+v", none.Hears)
|
|
}
|
|
}
|
|
|
|
type fakePruner struct {
|
|
on map[string][]string
|
|
removed []string
|
|
}
|
|
|
|
func (f *fakePruner) ConsumerNames(stream string) ([]string, error) { return f.on[stream], nil }
|
|
func (f *fakePruner) RemoveConsumer(stream, name string) error {
|
|
f.removed = append(f.removed, stream+"/"+name)
|
|
return nil
|
|
}
|
|
|
|
// A consumer that lost its grant has its consumer on the stream removed by the next assertion (ADR 0363 §6).
|
|
func TestAConsumerThatLostItsGrantIsRemoved(t *testing.T) {
|
|
f := &fakePruner{on: map[string][]string{"MAIL": {"ace_reader", "ace_former"}}}
|
|
wanted := []Consumer{{Name: "ace_reader", Stream: "MAIL"}, {Name: "ace_former", Stream: EventsStream}}
|
|
removed, err := PruneHeard(f, []ModuleStream{mailStream}, wanted)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !slices.Equal(removed, []string{"MAIL/ace_former"}) || !slices.Equal(f.removed, removed) {
|
|
t.Fatalf("removed %v", removed)
|
|
}
|
|
}
|
|
|
|
type refusingRaiser struct {
|
|
fakePruner
|
|
refuse bool
|
|
}
|
|
|
|
func (r *refusingRaiser) EnsureStream(Stream) error { return nil }
|
|
func (r *refusingRaiser) EnsureConsumer(Consumer) error {
|
|
if r.refuse {
|
|
return errors.New("consumer update refused")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// A narrowing the bus refuses on a restricted stream removes the consumer, so its wider filters deliver nothing
|
|
// more; on an unrestricted stream the refusal is only said (review of PR 338, ADR 0363 §6).
|
|
func TestARefusedNarrowingOnARestrictedStreamRemovesTheConsumer(t *testing.T) {
|
|
c := Consumer{Name: "ace_reader", Stream: "MAIL", Filters: mailHeard.Filters[:1]}
|
|
r := &refusingRaiser{refuse: true}
|
|
err := EnsureHeard(r, c, true)
|
|
if err == nil || !strings.Contains(err.Error(), "REMOVED") || !slices.Equal(r.removed, []string{"MAIL/ace_reader"}) {
|
|
t.Fatalf("restricted: %v, removed %v", err, r.removed)
|
|
}
|
|
r = &refusingRaiser{refuse: true}
|
|
if err := EnsureHeard(r, c, false); err == nil || len(r.removed) != 0 {
|
|
t.Fatalf("unrestricted: %v, removed %v", err, r.removed)
|
|
}
|
|
r = &refusingRaiser{}
|
|
if err := EnsureHeard(r, c, true); err != nil || len(r.removed) != 0 {
|
|
t.Fatalf("accepted: %v, removed %v", err, r.removed)
|
|
}
|
|
}
|