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") } }