Files
mesh-controller/internal/broker/have_bucket_test.go
T
jschoubben 75a27ec1a4
mesh/merge-gate head 75a27ec1a4bc merged into trunk 85af245f1af1 — pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus…
mesh/repo-check head 75a27ec1a4bc merged into trunk 85af245f1af1 — pass: its merge-check.sh passed
mesh/delivery delivered
The have bucket takes a node's whole facts, as large as a chain value, never cut (issue 1162)
A node's own key carries its profile and was refused at 4 KiB on novox and g14
(nats: message size exceeds maximum allowed).
2026-10-18 23:51:56 +02:00

116 lines
5.0 KiB
Go

package broker
import (
"slices"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/nats-io/nats.go/jetstream"
)
// 1136/AC1: the controller's start asserts node-engine_have with the history delivery keeps, and the bucket is one of
// the controller's own, so raising the buckets does not name it undeclared (ADR 0429 §1).
func TestTheHaveBucketIsTheControllersAndKeepsTwentyRevisions(t *testing.T) { // 1136/AC1
cfg := HaveBucketConfig()
if cfg.Bucket != delivery.HaveBucket || cfg.History != delivery.HaveHistory || delivery.HaveHistory != 20 {
t.Fatalf("the have bucket is asserted as %q with history %d, want %q with 20", cfg.Bucket, cfg.History, delivery.HaveBucket)
}
if cfg.Storage != jetstream.FileStorage || cfg.MaxBytes <= 0 || cfg.MaxValueSize <= 0 {
t.Fatalf("the have bucket is not bounded on disk: %+v", cfg)
}
if !IsControllerBucket(delivery.HaveBucket) || !slices.Contains(ControllerBuckets(), delivery.HaveBucket) {
t.Fatalf("%s is not among the controller's buckets", delivery.HaveBucket)
}
undeclared, err := RaiseBuckets(fakeBuckets{names: []string{delivery.HaveBucket}}, nil)
if err != nil {
t.Fatal(err)
}
if len(undeclared) != 0 {
t.Fatalf("%v are raised as undeclared", undeclared)
}
}
// 1136/AC2: a node may write and read the keys of its own node of the have bucket, with what a get, create, update and
// watch of them need, and no other key of it; the controller does not write it; an unsafe node name is refused.
func TestANodeWritesAndReadsOnlyItsOwnHaveKeys(t *testing.T) { // 1136/AC2
perms, err := PermissionsFor(Principal{Kind: KindNode, Node: "one", PasswordHash: "x"})
if err != nil {
t.Fatal(err)
}
b, stream := delivery.HaveBucket, "KV_"+delivery.HaveBucket
for _, want := range []string{
"$KV." + b + ".one.>",
"$JS.API.STREAM.INFO." + stream,
"$JS.API.DIRECT.GET." + stream + ".$KV." + b + ".one.>",
"$JS.API.CONSUMER.CREATE." + stream + ".*.$KV." + b + ".one.>",
"$JS.API.CONSUMER.DELETE." + stream + ".>",
"$JS.FC." + stream + ".>",
} {
has(t, perms.Publish, want)
}
for _, p := range perms.Publish {
if !strings.Contains(p, delivery.HaveBucket) {
continue
}
if strings.Contains(p, "KV."+b+".>") || strings.Contains(p, "KV."+b+".two") || strings.HasSuffix(p, "."+b+".*") ||
strings.Contains(p, "DIRECT.GET."+stream+".>") || strings.Contains(p, "CONSUMER.CREATE."+stream+".>") {
t.Errorf("the node is granted %q, which reaches keys of other nodes", p)
}
}
if err := CheckWriters(Principal{Kind: KindNode, Node: "one"}, perms.Publish); err != nil {
t.Fatalf("the node's own have keys are refused as a second writer: %v", err)
}
controller, err := PermissionsFor(Principal{Kind: KindController, PasswordHash: "x"})
if err != nil {
t.Fatal(err)
}
for _, p := range controller.Publish {
if SubjectsOverlap(p, "$KV."+b+".one.zsh") {
t.Errorf("the controller may write the have keys through %q", p)
}
}
for _, bad := range []string{"on.e", "one.>", "*", "o>ne"} {
if _, err := PermissionsFor(Principal{Kind: KindNode, Node: bad, PasswordHash: "x"}); err == nil {
t.Errorf("node %q was accepted as part of a have subject", bad)
}
}
// A principal that is not a node writes no have key: the writers table refuses it.
if err := CheckWriters(Principal{Kind: KindModule, Module: "billing", Node: "one"}, []string{"$KV." + b + ".one.>"}); err == nil {
t.Error("a module granted the have keys of a node is not refused as a second writer")
}
if err := CheckWriters(Principal{Kind: KindNode, Node: "two"}, []string{"$KV." + b + ".one.>"}); err == nil {
t.Error("a node granted another node's have keys is not refused as a second writer")
}
}
// 1136/AC3: a module whose manifest reads node-engine.have, mesh-controller.assignments and mesh-controller.builds may
// watch and get every key of the three and write none.
func TestAModuleReadingHaveAssignmentsAndBuildsReadsAndWritesNone(t *testing.T) { // 1136/AC3
perms, err := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "mesh-delivery-view", PasswordHash: "x",
Reads: []string{"node-engine.have", "mesh-controller.assignments", "mesh-controller.builds"}})
if err != nil {
t.Fatal(err)
}
for _, b := range []string{delivery.HaveBucket, AssignmentsBucket, BuildsBucket} {
stream := "KV_" + b
has(t, perms.Publish, "$JS.API.STREAM.INFO."+stream)
has(t, perms.Publish, "$JS.API.DIRECT.GET."+stream+".>")
has(t, perms.Publish, "$JS.API.CONSUMER.CREATE."+stream+".>")
for _, p := range perms.Publish {
if SubjectsOverlap(p, "$KV."+b+".any.key") {
t.Errorf("the reading module may write %s through %q", b, p)
}
}
}
}
// A node's own key carries its whole facts (its profile among them), several kilobytes on a desktop: the have
// bucket takes any value the chain's buckets take, since a value is never cut (novox/hq ADR 0408 §6, issue 1162).
// 1162/AC1
func TestTheHaveBucketTakesAValueAsLargeAsTheChains(t *testing.T) {
if got := HaveBucketConfig().MaxValueSize; got != ChainValueBytes {
t.Fatalf("the have bucket takes values up to %d bytes, the chain's up to %d", got, ChainValueBytes)
}
}