Every grant #456 gives was already feat/r6's, by the SDK's names: each conflict takes feat/r6's side, with main's test of the user list before the switch kept beside feat/r6's, and the duplicate assertion of the digests' bucket dropped.
204 lines
8.6 KiB
Go
204 lines
8.6 KiB
Go
package broker
|
|
|
|
import (
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
)
|
|
|
|
// 1945/AC1: a node's composition bucket keeps five revisions of a key and is named for its node; the bucket of digests
|
|
// likewise; a node name a subject would widen is refused.
|
|
func TestTheCompositionBuckets(t *testing.T) {
|
|
cfg, err := ComposedConfig("ace")
|
|
if err != nil || cfg.Bucket != "composed-ace" || cfg.History != 5 {
|
|
t.Fatalf("ComposedConfig(ace) = %+v, %v", cfg, err)
|
|
}
|
|
if d := ComposedDigestsConfig(); d.Bucket != delivery.ControllerComposedBucket || d.History != 5 {
|
|
t.Errorf("ComposedDigestsConfig = %+v", d)
|
|
}
|
|
if _, err := ComposedConfig("a.b"); err == nil {
|
|
t.Errorf("a dotted node got a bucket")
|
|
}
|
|
}
|
|
|
|
// 1945/AC1: a node-engine reads its own compositions and its own wants and nothing of another node's, nor the digests;
|
|
// the controller writes a node's bucket by its own subjects.
|
|
func TestANodeReadsOnlyItsOwnCompositions(t *testing.T) {
|
|
reads, err := ComposedReads("ace")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
all := strings.Join(reads, " ")
|
|
for _, want := range []string{"KV_composed-ace", "$KV.mesh-delivery-assigner_wants.want.ace.>"} {
|
|
if !strings.Contains(all, want) {
|
|
t.Errorf("a node-engine of ace cannot reach %s: %v", want, reads)
|
|
}
|
|
}
|
|
for _, never := range []string{"composed-g14", delivery.ControllerComposedBucket, "$KV.composed-ace", "want.*", "want.>"} {
|
|
if strings.Contains(all, never) {
|
|
t.Errorf("a node-engine of ace is granted %s: %v", never, reads)
|
|
}
|
|
}
|
|
if w, err := ComposedWrites("ace"); err != nil || w != "$KV.composed-ace.>" {
|
|
t.Errorf("ComposedWrites(ace) = %q, %v", w, err)
|
|
}
|
|
if _, err := ComposedReads("a*"); err == nil {
|
|
t.Errorf("a node name with a wildcard was granted")
|
|
}
|
|
}
|
|
|
|
// 1945 (for issue 1944): a node-engine asks its own machine's backup holder to back a data module up before it moves
|
|
// and when it last was, and no other machine's.
|
|
func TestANodeAgentAsksItsOwnBackupHolder(t *testing.T) {
|
|
asks, err := NodeAgentAsks("ace")
|
|
if err != nil || strings.Join(asks, " ") != "mesh.seat.node-backup.tool.now.ace mesh.seat.node-backup.tool.backed-up.ace" {
|
|
t.Errorf("NodeAgentAsks(ace) = %v, %v", asks, err)
|
|
}
|
|
p, err := PermissionsFor(Principal{Kind: KindNode, Node: "ace", PasswordHash: "x"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
all := strings.Join(p.Publish, " ")
|
|
for _, want := range append(asks, "$JS.API.DIRECT.GET.KV_composed-ace.>") {
|
|
if !strings.Contains(all, want) {
|
|
t.Errorf("a node-engine of ace may not publish %s", want)
|
|
}
|
|
}
|
|
if !strings.Contains(strings.Join(p.Subscribe, " "), "mesh.mod.restic.event.backed-up") {
|
|
t.Errorf("a node-engine of ace does not hear a backup taken: %v", p.Subscribe)
|
|
}
|
|
if strings.Contains(all, "node-backup.tool.now.*") || strings.Contains(all, "KV_composed-g14") {
|
|
t.Errorf("a node-engine of ace reaches another machine: %v", p.Publish)
|
|
}
|
|
}
|
|
|
|
// recordingRaiser records what a raise asserts.
|
|
type recordingRaiser struct{ streams, consumers []string }
|
|
|
|
func (r *recordingRaiser) EnsureStream(s Stream) error {
|
|
r.streams = append(r.streams, s.Name)
|
|
return nil
|
|
}
|
|
func (r *recordingRaiser) EnsureConsumer(c Consumer) error {
|
|
r.consumers = append(r.consumers, c.Stream+"/"+c.Name)
|
|
return nil
|
|
}
|
|
|
|
// 1945/AC14 (unit half): once the declarations' stream is retired, a raise asserts every stream of the mesh but it,
|
|
// and no node's consumer of a declaration; before, a raise asserts both (ADR 0539 clause 11).
|
|
func TestARaiseAfterTheDeclarationsAssertsNoneOfThem(t *testing.T) {
|
|
before := &recordingRaiser{}
|
|
if err := Raise(before, []string{"ace"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(strings.Join(before.streams, " "), DeclarationsStream) || !strings.Contains(strings.Join(before.consumers, " "), "NODES/ace") {
|
|
t.Fatalf("before the retirement a raise asserts %v and %v", before.streams, before.consumers)
|
|
}
|
|
after := &recordingRaiser{}
|
|
if err := RaiseAfterDeclarations(after); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(strings.Join(after.streams, " "), DeclarationsStream) || strings.Contains(strings.Join(after.consumers, " "), "NODES/") {
|
|
t.Errorf("after the retirement a raise asserts %v and %v", after.streams, after.consumers)
|
|
}
|
|
if len(after.streams) != len(before.streams)-1 {
|
|
t.Errorf("after the retirement %d streams are asserted, before %d", len(after.streams), len(before.streams))
|
|
}
|
|
}
|
|
|
|
// 1945 (the review of A1, (b) and H1): the operator's account, a person who may invoke every tool, writes release 6's
|
|
// hand path: a want of any node, each node's compositions, the digests, and a node's object bucket with what replacing
|
|
// an object needs; a person with fewer tools writes none of it.
|
|
func TestTheOperatorWritesTheHandPath(t *testing.T) {
|
|
users, err := Users(Records{Nodes: []string{"ace", "novox"}, People: map[string][]string{"jochen": {"*"}, "guest": {"mesh-controller.status"}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, u := range users {
|
|
if u.Kind != KindPerson {
|
|
continue
|
|
}
|
|
perms, err := PermissionsFor(u)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, s := range []string{"$KV.mesh-delivery-assigner_wants.want.>", "$KV.composed-ace.>", "$KV.composed-novox.>",
|
|
"$KV.mesh-controller_composed.>", "$O.declaration-files-ace.>", "$JS.API.DIRECT.GET.OBJ_declaration-files-ace.>",
|
|
"$JS.API.STREAM.MSG.GET.OBJ_declaration-files-ace", "$JS.API.STREAM.PURGE.OBJ_declaration-files-ace"} {
|
|
found := false
|
|
for _, p := range perms.Publish {
|
|
found = found || p == s
|
|
}
|
|
if found != (u.Module == "jochen") {
|
|
t.Errorf("%s may publish %s: %v", u.Module, s, found)
|
|
}
|
|
}
|
|
}
|
|
c, err := PermissionsFor(Principal{Kind: KindController, PasswordHash: "x"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
found := false
|
|
for _, p := range c.Publish {
|
|
found = found || p == "mesh.seat.mesh-controller.event.bucket-deleted"
|
|
}
|
|
if !found {
|
|
t.Errorf("the controller may not say bucket-deleted")
|
|
}
|
|
}
|
|
|
|
// 1945 (the review's A1): the bus's user list today's controller composes, which is the one the bus runs after release
|
|
// 6's switch, already grants every subject the new controller, every new node-engine and the assigner use: the
|
|
// controller writes each node's compositions and the digests; a node-engine reads its own compositions and wants, asks
|
|
// its own backup holder and hears its word, and nothing of another node's; the assigner reads the digests, announced
|
|
// readable.
|
|
func TestTheUserListGrantsWhatReleaseSixUses(t *testing.T) {
|
|
controller, err := PermissionsFor(Principal{Kind: KindController, PasswordHash: "x", FilesFor: []string{"ace", "g14"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, s := range []string{"$KV.composed-ace.>", "$KV.composed-g14.>", "$KV.mesh-controller_composed.>",
|
|
"mesh.seat.mesh-controller.event.bucket-deleted"} {
|
|
has(t, controller.Publish, s)
|
|
}
|
|
node, err := PermissionsFor(Principal{Kind: KindNode, Node: "ace", PasswordHash: "x"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, s := range []string{
|
|
"$JS.API.STREAM.INFO.KV_composed-ace", "$JS.API.DIRECT.GET.KV_composed-ace.>", "$JS.API.CONSUMER.CREATE.KV_composed-ace.>",
|
|
"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.ace.>",
|
|
"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.ace.>",
|
|
"mesh.seat.node-backup.tool.now.ace", "mesh.seat.node-backup.tool.backed-up.ace",
|
|
} {
|
|
has(t, node.Publish, s)
|
|
}
|
|
has(t, node.Subscribe, "mesh.mod.restic.event.backed-up")
|
|
for _, p := range node.Publish {
|
|
if strings.Contains(p, "composed-g14") || strings.Contains(p, "mesh-controller_composed") || strings.Contains(p, "want.g14") ||
|
|
strings.HasPrefix(p, "$KV.composed-") || strings.HasSuffix(p, "node-backup.tool.now.*") {
|
|
t.Errorf("a node-engine of ace may publish %s", p)
|
|
}
|
|
}
|
|
if !slices.Contains(controllerReadableStates(), ControllerComposed) ||
|
|
!slices.Contains(ControllerBuckets(), delivery.ControllerComposedBucket) || !IsControllerBucket(delivery.ControllerComposedBucket) {
|
|
t.Fatalf("the digests are not announced readable among the controller's buckets")
|
|
}
|
|
if !ControllerAnnouncement("novox").Readable(ControllerComposed) {
|
|
t.Fatalf("the controller does not announce %s readable", ControllerComposed)
|
|
}
|
|
assigner, err := PermissionsFor(Principal{Kind: KindModule, Node: "novox", Module: "mesh-delivery-assigner", PasswordHash: "x",
|
|
Reads: []string{"mesh-controller.composed"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
has(t, assigner.Publish, "$JS.API.STREAM.INFO.KV_mesh-controller_composed")
|
|
has(t, assigner.Publish, "$JS.API.CONSUMER.CREATE.KV_mesh-controller_composed.>")
|
|
if c := ComposedDigestsConfig(); c.Bucket != "mesh-controller_composed" || c.History != 5 || c.Storage != jetstream.FileStorage {
|
|
t.Errorf("the digests' bucket is %+v", c)
|
|
}
|
|
}
|