One name per thing, per the HQ glossary: the module/container/image/binary/repo becomes mesh-controller, the seat the-controller, and the store+broker pair the foundation (embedded base bundles, default template and example lock renamed with their go:embed directives). No behaviour change — a pure vocabulary rename. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
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-controller/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 0043).
|
|
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
|
|
}
|