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 }