| 1 | package activator |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | "testing" |
| 6 | ) |
| 7 | |
| 8 | // The key IS the membership record (one LIST, no GETs), so the encoding must |
| 9 | // round-trip any peer URL and reject foreign objects. |
| 10 | func TestHeartbeatKeyRoundTrip(t *testing.T) { |
| 11 | for _, peer := range []string{ |
| 12 | "http://10.0.0.5:8080", |
| 13 | "https://box-a.internal", |
| 14 | "https://box-b.mesh.example.com:8443", |
| 15 | } { |
| 16 | leaf := encodeHeartbeatKey(peer) |
| 17 | if got, err := decodeHeartbeatKey(leaf); err != nil || got != peer { |
| 18 | t.Fatalf("round-trip %q: got %q, %v", peer, got, err) |
| 19 | } |
| 20 | } |
| 21 | // A foreign object's key leaf must fail decode (List skips it) rather than |
| 22 | // turn into a phantom member. |
| 23 | if _, err := decodeHeartbeatKey("not/base64!"); err == nil { |
| 24 | t.Fatal("garbage key leaf decoded without error") |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | // statsKey must live under a SIBLING prefix, never collide with (or nest |
| 29 | // inside) the membership heartbeat's own key — the hot membership List path |
| 30 | // must never accidentally pick up a stats object. |
| 31 | func TestStatsKeySibling(t *testing.T) { |
| 32 | s := &HeartbeatStore{prefix: "prod"} |
| 33 | peer := "https://box-b.internal" |
| 34 | |
| 35 | hbKey := s.key(peer) |
| 36 | statsKey := s.statsKey(peer) |
| 37 | |
| 38 | if statsKey == hbKey { |
| 39 | t.Fatal("stats key must differ from the membership heartbeat key") |
| 40 | } |
| 41 | if strings.HasPrefix(statsKey, s.prefix+"/") { |
| 42 | t.Fatalf("stats key %q must not live under the plain membership prefix %q", statsKey, s.prefix) |
| 43 | } |
| 44 | if !strings.HasPrefix(statsKey, s.prefix+"-stats/") { |
| 45 | t.Fatalf("stats key %q should live under %q", statsKey, s.prefix+"-stats/") |
| 46 | } |
| 47 | } |
| 48 | |