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
68 lines
2.9 KiB
Go
68 lines
2.9 KiB
Go
package broker_test
|
|
|
|
import (
|
|
"regexp"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// The names are written twice, so a test keeps them agreeing.
|
|
//
|
|
// `link` imports `broker`, so `broker` cannot import `link` — the queue and exchange names
|
|
// therefore exist in both. A scoped account naming a queue nothing publishes to produces a
|
|
// builder that takes no work and says nothing about why, which is the worst kind of silence.
|
|
//
|
|
// An external test package, because it may import both without either importing the other.
|
|
func TestTheNamesTheBrokerScopesAreTheNamesTheLinkUses(t *testing.T) {
|
|
for _, agreed := range []struct {
|
|
what string
|
|
scoped string
|
|
actually string
|
|
}{
|
|
{"the build queue", broker.BuildQueueName, link.BuildQueue},
|
|
{"the exchange", broker.ExchangeName, link.Exchange},
|
|
{"a node's queue", broker.QueueFor("somewhere"), link.QueueFor("somewhere")},
|
|
} {
|
|
if agreed.scoped != agreed.actually {
|
|
t.Errorf("%s: the broker scopes %q and the link uses %q",
|
|
agreed.what, agreed.scoped, agreed.actually)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestABuilderMayWriteToAReplyQueueAndReadNoNodesDeclarations(t *testing.T) {
|
|
// The scoping, checked as patterns rather than by connecting: what it may write must include
|
|
// the queue an asker actually waits on, and what it may read must not include any node's.
|
|
//
|
|
// This exists because the first version scoped writes to `amq.gen-*` — the name one broker
|
|
// happens to generate — and the builder built, could not answer, and the connection simply
|
|
// closed saying only "not allowed to publish to exchange \'\'". Answering through the
|
|
// exchange is what removed the need for any of that.
|
|
write := regexp.MustCompile("^" + regexp.QuoteMeta(broker.ExchangeName) + "$")
|
|
if !write.MatchString(broker.ExchangeName) {
|
|
t.Error("a builder may not write to the exchange, so it can take work and never answer")
|
|
}
|
|
// And not the default exchange, where permission is per exchange rather than per queue — a
|
|
// builder allowed to use it could publish into any node's queue.
|
|
//
|
|
// Confirmed against a real broker as well, and worth recording how that nearly went wrong:
|
|
// an unconfirmed publish is asynchronous, so a refusal arrives as a channel close afterwards
|
|
// and a naive check reports success. With publisher confirms the broker's refusal is
|
|
// immediate. **A negative security assertion made against an asynchronous call is not an
|
|
// assertion.**
|
|
if write.MatchString("") {
|
|
t.Error("a builder may publish to the default exchange, and so into any node's queue")
|
|
}
|
|
|
|
read := regexp.MustCompile("^" + regexp.QuoteMeta(broker.BuildQueueName) + "$")
|
|
if !read.MatchString(broker.BuildQueueName) {
|
|
t.Error("a builder may not read the build queue")
|
|
}
|
|
if read.MatchString(link.QueueFor("someone-else")) {
|
|
// A build machine is not a node, and a node's queue carries its declarations.
|
|
t.Error("a builder may read another machine's declarations")
|
|
}
|
|
}
|