CreateModuleAccount gives an assigned module its own broker account whose permissions ARE its manifest: declare and read its own <node>.<module>.events queue, read the events exchange to bind onto if it consumes, write the events exchange only if it emits. The account name carries the node (sealed per machine), the permissions carry the module (one cannot read another's queue). The builder becomes one instance of this rule rather than a separate kind. EnsureEventExchanges declares the bus the substrate owns — mesh.events, mesh.rpc, mesh.events.dead + a retention queue — idempotently, since a module account may not declare an exchange. Scope tested as patterns (no broker needed), and every management call verified against a real LavinMQ. Honest limit recorded in the code: LavinMQ has no topic permissions, so ADR 0047's emit-origin reservation (module.<self>.*) is stamped by the sdk, not enforced by the broker; a pure consumer like the audit logger is unaffected. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
97 lines
3.6 KiB
Go
97 lines
3.6 KiB
Go
package broker_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-control/internal/broker"
|
|
)
|
|
|
|
// The audit logger consumes everything and emits nothing. Its account must let it declare and read
|
|
// its own queue and read the events exchange to bind onto — and must not reach another module's
|
|
// queue, nor grant any write to the events exchange (novox/hq ADR 0048).
|
|
func TestAConsumerReadsItsOwnQueueAndTheEventsExchangeAndNoOthers(t *testing.T) {
|
|
// Rebuilt through CreateModuleAccount's own path by asking for the same scope it would apply.
|
|
// The queue this module reads:
|
|
mine := broker.ModuleQueueFor("anchor", "audit-logger")
|
|
other := broker.ModuleQueueFor("anchor", "plex")
|
|
|
|
read := scope(t, "read", "anchor", "audit-logger", nil, []string{"#"})
|
|
if !read.MatchString(mine) {
|
|
t.Error("the audit logger may not read its own queue, so it consumes nothing")
|
|
}
|
|
if !read.MatchString(broker.EventsExchangeName) {
|
|
t.Error("the audit logger may not read the events exchange, so it cannot bind onto it")
|
|
}
|
|
if read.MatchString(other) {
|
|
t.Error("the audit logger may read another module's queue")
|
|
}
|
|
|
|
// It emits nothing, so it is granted no write to the events exchange — only its own queue, to bind.
|
|
write := scope(t, "write", "anchor", "audit-logger", nil, []string{"#"})
|
|
if write.MatchString(broker.EventsExchangeName) {
|
|
t.Error("a pure consumer was granted write to the events exchange")
|
|
}
|
|
if !write.MatchString(mine) {
|
|
t.Error("the audit logger may not write to its own queue, so it cannot bind it")
|
|
}
|
|
}
|
|
|
|
// An emitter is granted the events exchange to write; a consumer is not.
|
|
func TestAnEmitterMayWriteTheEventsExchangeAndAConsumerMayNot(t *testing.T) {
|
|
emitter := scope(t, "write", "anchor", "umami", []string{"module.umami.site.created"}, nil)
|
|
if !emitter.MatchString(broker.EventsExchangeName) {
|
|
t.Error("an emitting module may not write the events exchange, so it cannot emit")
|
|
}
|
|
}
|
|
|
|
// scope reconstructs one of the three permission patterns CreateModuleAccount would apply, by
|
|
// reading it back from a captured request against a stub management API.
|
|
func scope(t *testing.T, which, node, module string, emits, consumes []string) *regexp.Regexp {
|
|
t.Helper()
|
|
pat := capturePermission(t, which, node, module, emits, consumes)
|
|
re, err := regexp.Compile(pat)
|
|
if err != nil {
|
|
t.Fatalf("the %s pattern does not compile: %v", which, err)
|
|
}
|
|
return re
|
|
}
|
|
|
|
// capturePermission runs CreateModuleAccount against a stub management API and returns the pattern
|
|
// it set for `which` ("configure"/"write"/"read"). The scope is tested where it is applied, not
|
|
// reconstructed by the test — so a change to the mapping cannot pass a test that hard-codes the old
|
|
// one.
|
|
func capturePermission(t *testing.T, which, node, module string, emits, consumes []string) string {
|
|
t.Helper()
|
|
var captured map[string]string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasPrefix(r.URL.Path, "/api/permissions/") {
|
|
_ = json.NewDecoder(r.Body).Decode(&captured)
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
defer server.Close()
|
|
|
|
t.Setenv(broker.ManagementVar, server.URL)
|
|
m, err := broker.ManagementFromEnvironment()
|
|
if err != nil {
|
|
t.Fatalf("stub management not usable: %v", err)
|
|
}
|
|
if _, err := m.CreateModuleAccount(context.Background(), node, module, "pw", emits, consumes); err != nil {
|
|
t.Fatalf("CreateModuleAccount: %v", err)
|
|
}
|
|
if captured == nil {
|
|
t.Fatal("no permissions were set")
|
|
}
|
|
pattern, ok := captured[which]
|
|
if !ok {
|
|
t.Fatalf("no %s permission was set; got %v", which, captured)
|
|
}
|
|
return pattern
|
|
}
|