Files
mesh-sdk/go/delivery/delivery_test.go
T
jschoubben 2b420603fe
mesh/merge-gate head 2b420603fe83 merged into trunk f70a99dbf485 — pass, with a note: builds mesh-sdk (+41 dependent(s)) → ace, g14, novox, shanks; no bus …
mesh/repo-check head 2b420603fe83 merged into trunk f70a99dbf485 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/delivery delivered
Give delivery's keys and values one home the node-engine and the parts read (issue 1131)
2026-10-18 18:49:24 +02:00

295 lines
9.5 KiB
Go

package delivery
import (
"encoding/json"
"errors"
"strings"
"testing"
"time"
)
// 1131/AC1: the have key forms and parses back; bad names are refused, naming them.
func TestHaveKey(t *testing.T) {
if HaveBucket != "node-engine_have" {
t.Fatalf("HaveBucket = %q", HaveBucket)
}
key, err := HaveKey("ace", "plex")
if err != nil || key != "ace.plex" {
t.Fatalf("HaveKey = %q, %v", key, err)
}
node, module, isNode, err := ParseHaveKey(key)
if err != nil || node != "ace" || module != "plex" || isNode {
t.Fatalf("ParseHaveKey = %q %q %v %v", node, module, isNode, err)
}
if got, err := HaveNodeKey("ace"); err != nil || got != "ace.node" {
t.Fatalf("HaveNodeKey = %q, %v", got, err)
}
if _, err := HaveNodeKey("a b"); err == nil {
t.Error("HaveNodeKey with a space accepted")
}
node, module, isNode, err = ParseHaveKey("ace.node")
if err != nil || node != "ace" || module != "" || !isNode {
t.Fatalf("ParseHaveKey node = %q %q %v %v", node, module, isNode, err)
}
for _, c := range []struct{ node, module, named string }{
{"ace", "node", "node"},
{"ace", "", "module"},
{"", "plex", "node"},
{"a.b", "plex", "a.b"},
{"ace", "p*", "p*"},
{"ace", "p>", "p>"},
{"ace", "p x", "p x"},
{"ace", "p\tx", "module"},
} {
_, err := HaveKey(c.node, c.module)
if err == nil || !strings.Contains(err.Error(), c.named) {
t.Errorf("HaveKey(%q, %q) = %v, want an error naming %q", c.node, c.module, err, c.named)
}
}
for _, bad := range []string{"", "ace", "ace.", ".plex", "ace.plex.x", "ace.p x"} {
if _, _, _, err := ParseHaveKey(bad); err == nil {
t.Errorf("ParseHaveKey(%q) accepted", bad)
}
}
}
// 1131/AC2: the presence keys form and parse back; a node named part is refused.
func TestPresenceKey(t *testing.T) {
if PresenceBucket != "mesh-delivery-watchman_presence" {
t.Fatalf("PresenceBucket = %q", PresenceBucket)
}
key, err := PresenceNodeKey("ace")
if err != nil || key != "ace" {
t.Fatalf("PresenceNodeKey = %q, %v", key, err)
}
node, part, instance, err := ParsePresenceKey(key)
if err != nil || node != "ace" || part != "" || instance != "" {
t.Fatalf("ParsePresenceKey node = %q %q %q %v", node, part, instance, err)
}
key, err = PresencePartKey("view", "novox")
if err != nil || key != "part.view.novox" {
t.Fatalf("PresencePartKey = %q, %v", key, err)
}
node, part, instance, err = ParsePresenceKey(key)
if err != nil || node != "" || part != "view" || instance != "novox" {
t.Fatalf("ParsePresenceKey part = %q %q %q %v", node, part, instance, err)
}
if _, err := PresenceNodeKey("part"); err == nil || !strings.Contains(err.Error(), "part") {
t.Errorf("PresenceNodeKey(part) = %v", err)
}
if _, err := PresenceNodeKey("a.b"); err == nil {
t.Error("PresenceNodeKey(a.b) accepted")
}
if _, err := PresencePartKey("", "x"); err == nil {
t.Error("PresencePartKey with empty part accepted")
}
if _, err := PresencePartKey("view", "x y"); err == nil {
t.Error("PresencePartKey with a space accepted")
}
for _, bad := range []string{"", "part", "part.view", "part.view.a.b", "a.b", "part..x"} {
if _, _, _, err := ParsePresenceKey(bad); err == nil {
t.Errorf("ParsePresenceKey(%q) accepted", bad)
}
}
}
// 1131/AC3: the pulse key splits at the first dot, the key holding dots.
func TestPulseKey(t *testing.T) {
if PulseBucket != "mesh-delivery-watchman_pulse" {
t.Fatalf("PulseBucket = %q", PulseBucket)
}
key, err := PulseKey("node-engine_have", "ace.plex")
if err != nil || key != "node-engine_have.ace.plex" {
t.Fatalf("PulseKey = %q, %v", key, err)
}
bucket, rest, err := ParsePulseKey(key)
if err != nil || bucket != "node-engine_have" || rest != "ace.plex" {
t.Fatalf("ParsePulseKey = %q %q %v", bucket, rest, err)
}
for _, c := range [][2]string{{"", "k"}, {"a.b", "k"}, {"b", ""}, {"b", "k x"}, {"b", ".k"}, {"b", "k."}, {"b", "k*"}} {
if _, err := PulseKey(c[0], c[1]); err == nil {
t.Errorf("PulseKey(%q, %q) accepted", c[0], c[1])
}
}
for _, bad := range []string{"", "nodot", ".k", "b."} {
if _, _, err := ParsePulseKey(bad); err == nil {
t.Errorf("ParsePulseKey(%q) accepted", bad)
}
}
}
// 1131/AC4: each value encodes, decodes equal, and carries "v":1.
func TestRoundTrip(t *testing.T) {
hasV1 := func(t *testing.T, b []byte) {
t.Helper()
var m map[string]any
if err := json.Unmarshal(b, &m); err != nil {
t.Fatal(err)
}
if m["v"] != float64(1) {
t.Fatalf("v = %v in %s", m["v"], b)
}
if len(b) > 500 {
t.Fatalf("value is %d bytes", len(b))
}
}
t.Run("have", func(t *testing.T) {
in := Have{Build: "b1", FP: "f1", Phase: PhaseRunning, Health: HealthOK, Detail: "up"}
b, err := EncodeHave(in)
if err != nil {
t.Fatal(err)
}
hasV1(t, b)
out, err := DecodeHave(b)
if err != nil {
t.Fatal(err)
}
in.V = Version
if out != in {
t.Fatalf("got %+v want %+v", out, in)
}
})
t.Run("have node", func(t *testing.T) {
in := HaveNode{Windows: json.RawMessage(`{"a":1}`), Busy: json.RawMessage(`true`)}
b, err := EncodeHaveNode(in)
if err != nil {
t.Fatal(err)
}
hasV1(t, b)
out, err := DecodeHaveNode(b)
if err != nil {
t.Fatal(err)
}
if !SameHaveNode(out, HaveNode{V: Version, Windows: in.Windows, Busy: in.Busy}) {
t.Fatalf("got %+v", out)
}
})
t.Run("presence", func(t *testing.T) {
b, err := EncodePresence(Presence{Online: true})
if err != nil {
t.Fatal(err)
}
hasV1(t, b)
out, err := DecodePresence(b)
if err != nil || out != (Presence{V: Version, Online: true}) {
t.Fatalf("got %+v %v", out, err)
}
})
t.Run("pulse", func(t *testing.T) {
in := Pulse{Bucket: "node-engine_have", Key: "ace.plex", Revision: 7, Owes: "ace"}
b, err := EncodePulse(in)
if err != nil {
t.Fatal(err)
}
hasV1(t, b)
out, err := DecodePulse(b)
in.V = Version
if err != nil || out != in {
t.Fatalf("got %+v %v", out, err)
}
})
}
// 1131/AC5: a missing or unknown version is refused as ErrUnknownVersion, naming both versions.
func TestUnknownVersion(t *testing.T) {
decoders := map[string]func([]byte) error{
"have": func(b []byte) error { _, err := DecodeHave(b); return err },
"have node": func(b []byte) error { _, err := DecodeHaveNode(b); return err },
"presence": func(b []byte) error { _, err := DecodePresence(b); return err },
"pulse": func(b []byte) error { _, err := DecodePulse(b); return err },
}
for name, dec := range decoders {
t.Run(name, func(t *testing.T) {
err := dec([]byte(`{"v":2,"phase":"running","health":"ok"}`))
if !errors.Is(err, ErrUnknownVersion) || !strings.Contains(err.Error(), "2") || !strings.Contains(err.Error(), "1") {
t.Errorf("v 2: %v", err)
}
err = dec([]byte(`{"phase":"running","health":"ok"}`))
if !errors.Is(err, ErrUnknownVersion) {
t.Errorf("missing v: %v", err)
}
if err := dec([]byte(`not json`)); err == nil || errors.Is(err, ErrUnknownVersion) {
t.Errorf("garbage: %v", err)
}
})
}
}
// 1131/AC6: a phase or health outside the lists is refused on encode and decode.
func TestPhaseHealthRefused(t *testing.T) {
for _, p := range []Phase{PhaseBackingUp, PhaseApplying, PhaseChecking, PhaseRunning, PhaseFailed} {
for _, h := range []Health{HealthOK, HealthFailed, HealthWaits, HealthUnknown} {
if _, err := EncodeHave(Have{Phase: p, Health: h}); err != nil {
t.Errorf("%s/%s refused: %v", p, h, err)
}
}
}
if _, err := EncodeHave(Have{Phase: "sleeping", Health: HealthOK}); err == nil || !strings.Contains(err.Error(), "sleeping") {
t.Errorf("bad phase encoded: %v", err)
}
if _, err := EncodeHave(Have{Phase: PhaseRunning, Health: "fine"}); err == nil || !strings.Contains(err.Error(), "fine") {
t.Errorf("bad health encoded: %v", err)
}
if _, err := EncodeHave(Have{Phase: PhaseRunning}); err == nil {
t.Error("empty health encoded")
}
if _, err := DecodeHave([]byte(`{"v":1,"phase":"sleeping","health":"ok"}`)); err == nil {
t.Error("bad phase decoded")
}
if _, err := DecodeHave([]byte(`{"v":1,"phase":"running","health":"fine"}`)); err == nil {
t.Error("bad health decoded")
}
}
// 1131/AC7: the pulse's time to live is 120 seconds minus the move's age; at the bound the move is stalled.
func TestPulseTTL(t *testing.T) {
for _, c := range []struct {
age time.Duration
ttl time.Duration
stalled bool
}{
{0, 120 * time.Second, false},
{30 * time.Second, 90 * time.Second, false},
{119 * time.Second, time.Second, false},
{120 * time.Second, 0, true},
{200 * time.Second, 0, true},
} {
ttl, stalled := PulseTTL(c.age)
if ttl != c.ttl || stalled != c.stalled {
t.Errorf("PulseTTL(%v) = %v, %v; want %v, %v", c.age, ttl, stalled, c.ttl, c.stalled)
}
}
if PulseMoveBound != 120*time.Second || HaveHistory != 20 || PresenceHistory != 20 {
t.Error("constants")
}
}
// 1131/AC8: equal values say same, different ones do not.
func TestSame(t *testing.T) {
a := Have{V: 1, Build: "b", Phase: PhaseRunning, Health: HealthOK}
b := a
if !SameHave(a, b) {
t.Error("equal haves differ")
}
b.Detail = "x"
if SameHave(a, b) {
t.Error("different haves same")
}
n := HaveNode{V: 1, Busy: json.RawMessage(`true`)}
if !SameHaveNode(n, HaveNode{V: 1, Busy: json.RawMessage(`true`)}) || SameHaveNode(n, HaveNode{V: 1}) {
t.Error("have node")
}
if !SamePresence(Presence{V: 1, Online: true}, Presence{V: 1, Online: true}) || SamePresence(Presence{V: 1}, Presence{V: 1, Online: true}) {
t.Error("presence")
}
p := Pulse{V: 1, Bucket: "b", Key: "k", Revision: 1, Owes: "ace"}
q := p
if !SamePulse(p, q) {
t.Error("equal pulses differ")
}
q.Revision = 2
if SamePulse(p, q) {
t.Error("different pulses same")
}
}