//go:build integration package bus_test import ( "errors" "fmt" "reflect" "sort" "strings" "sync" "testing" "github.com/novox/mesh-tools/node-tools/internal/bus" mt "github.com/novox/mesh-tools/node-tools/internal/meshtest" ) // **A state its owner keeps one bucket per node is served to a bundle as one state** (novox/hq ADR 0593 ยง6: each node's // engine writes its own `composed-`, and the provisioner reads them all). Get goes to the bucket the key's first // part names, Keys and Watch over every bucket of the prefix, a bucket created during the watch included, and a bucket // that only starts like the prefix is never read. func TestAPerNodeStateIsServedAcrossEveryNodesBucket(t *testing.T) { mesh := mt.New(t) a, b := mesh.Bucket(t, "composed-a"), mesh.Bucket(t, "composed-b") other := mesh.Bucket(t, "composedx-z") for kv, key := range map[interface { Put(string, []byte) (uint64, error) }]string{a: "a.m1.f1", b: "b.m2.f2", other: "z.m3.f3"} { if _, err := kv.Put(key, []byte(`{"v":1}`)); err != nil { t.Fatal(err) } } prov := mt.MembershipOf("mesh-provisioner", "anchor", false, nil) prov.State = []bus.StateIssued{{Name: "mesh-host.composed", Bucket: "composed-", PerNode: true}} mesh.Issue(t, prov) conn, err := bus.Connect(bus.Credential{URL: mt.URL(t), Module: "node-tools", Node: "anchor"}) if err != nil { t.Fatal(err) } conn.Logf = func(string, ...any) {} t.Cleanup(conn.Close) conn.Follow("mesh-provisioner") mt.Until(t, func() error { if conn.Membership("mesh-provisioner") == nil { return errors.New("not issued yet") } return nil }) if e, err := conn.StateGet("mesh-provisioner", "mesh-host.composed", "b.m2.f2"); err != nil || e == nil || e.Key != "b.m2.f2" { t.Fatalf("get from the second node's bucket: %+v, %v", e, err) } if e, err := conn.StateGet("mesh-provisioner", "mesh-host.composed", "q.m.f"); err != nil || e != nil { t.Fatalf("a key of a node with no bucket should read as none: %+v, %v", e, err) } if e, err := conn.StateGet("mesh-provisioner", "mesh-host.composed", "z.m3.f3"); err != nil || e != nil { t.Fatalf("the look-alike bucket was read: %+v, %v", e, err) } if _, err := conn.StateGet("mesh-provisioner", "mesh-host.composed", "nonode"); err == nil { t.Fatal("a key naming no node should be refused") } if keys, err := conn.StateKeys("mesh-provisioner", "mesh-host.composed"); err != nil || !reflect.DeepEqual(keys, []string{"a.m1.f1", "b.m2.f2"}) { t.Fatalf("keys across the buckets: %v, %v", keys, err) } if _, err := conn.StatePut("mesh-provisioner", "mesh-host.composed", "a.m1.f9", []byte(`{}`)); err == nil { t.Fatal("a per-node state is read only") } var mu sync.Mutex var current, live []string stop, err := conn.StateWatch("mesh-provisioner", "mesh-host.composed", "", func(ch bus.StateChange) error { mu.Lock() defer mu.Unlock() if ch.Current { current = append(current, ch.Key) } else { live = append(live, ch.Key) } return nil }) if err != nil { t.Fatal(err) } t.Cleanup(stop) mu.Lock() sort.Strings(current) if !reflect.DeepEqual(current, []string{"a.m1.f1", "b.m2.f2"}) { t.Fatalf("current values: %v", current) } mu.Unlock() // A third node's bucket, created during the watch, and a change in a first one. c := mesh.Bucket(t, "composed-c") if _, err := c.Put("c.m4.f4", []byte(`{"v":1}`)); err != nil { t.Fatal(err) } if _, err := a.Put("a.m1.f5", []byte(`{"v":1}`)); err != nil { t.Fatal(err) } mt.Until(t, func() error { mu.Lock() defer mu.Unlock() got := strings.Join(append(append([]string(nil), current...), live...), ",") for _, want := range []string{"a.m1.f5", "c.m4.f4"} { if !strings.Contains(got, want) { return fmt.Errorf("%s not heard yet: current %v, changes %v", want, current, live) } } return nil }) }