package protocol import ( "encoding/json" "testing" ) func decodeString(t *testing.T, s string) *Envelope { t.Helper() env, err := Decode([]byte(s)) if err != nil { t.Fatalf("Decode(%q) error: %v", s, err) } return env } func TestDecodeRequest(t *testing.T) { // [message, numericId, "R"] -> a request that wants an "E"-terminated reply. env := decodeString(t, `[{"type":"my/socketid"}, 7, "R"]`) if env.Message.Type() != "my/socketid" { t.Fatalf("type = %q", env.Message.Type()) } if !env.HasID { t.Fatal("expected HasID") } flag, ok := env.WantsReply() if !ok || flag != FlagEnd { t.Fatalf("WantsReply = (%q,%v), want (E,true)", flag, ok) } if env.IsBroadcast() { t.Fatal("request must not be a broadcast") } } func TestDecodeStream(t *testing.T) { env := decodeString(t, `[{"type":"sub"}, 9, "S"]`) flag, ok := env.WantsReply() if !ok || flag != FlagContinue { t.Fatalf("WantsReply = (%q,%v), want (C,true)", flag, ok) } } func TestDecodeFireAndForget(t *testing.T) { // The SDK's SendOnly path: [message, "R"]. The "R" sits in the id slot as a // string, so a handler runs but no reply is produced. env := decodeString(t, `[{"type":"connection/packsending","value":0}, "R"]`) if !env.HasID { t.Fatal("string id should set HasID") } if _, ok := env.WantsReply(); ok { t.Fatal("fire-and-forget must not want a reply") } if env.IsBroadcast() { t.Fatal("fire-and-forget is not a broadcast") } if env.Message.Truthy("value") { t.Fatal("value:0 should be falsey") } } func TestDecodeBroadcast(t *testing.T) { // A bare [message] frame: no id -> the broadcast branch. env := decodeString(t, `[{"type":"hello"}]`) if env.HasID { t.Fatal("bare frame should not have an id") } if !env.IsBroadcast() { t.Fatal("bare frame should be a broadcast") } if _, ok := env.WantsReply(); ok { t.Fatal("broadcast must not want a reply") } } func TestDecodeMissingTypeObject(t *testing.T) { // arr[0] not an object -> Message is nil and Type() is empty. env := decodeString(t, `["not-an-object", 1, "R"]`) if env.Message != nil { t.Fatalf("expected nil Message, got %v", env.Message) } if env.Message.Type() != "" { t.Fatal("nil message type should be empty") } } func TestDecodeErrors(t *testing.T) { if _, err := Decode([]byte(`{"type":"x"}`)); err == nil { t.Fatal("object (not array) should error") } if _, err := Decode([]byte(`[]`)); err != ErrEmptyFrame { t.Fatalf("empty array err = %v, want ErrEmptyFrame", err) } if _, err := Decode([]byte(`not json`)); err == nil { t.Fatal("invalid json should error") } } func TestNumericIDRoundTripsAsInteger(t *testing.T) { // A numeric id must come back out as an integer, not "7.0", so the SDK's // integer-keyed event pool matches it. env := decodeString(t, `[{"type":"x"}, 7, "R"]`) reply := Reply("ok", env.ID, FlagEnd) b, err := json.Marshal(reply) if err != nil { t.Fatal(err) } if got, want := string(b), `["ok",7,"E"]`; got != want { t.Fatalf("reply = %s, want %s", got, want) } } func TestSignalShape(t *testing.T) { b, err := json.Marshal(Signal("room/joined", map[string]any{"id": "abc"})) if err != nil { t.Fatal(err) } if got, want := string(b), `[{"id":"abc"},"room/joined"]`; got != want { t.Fatalf("signal = %s, want %s", got, want) } } func TestMessageAccessors(t *testing.T) { m := Message{"type": "t", "to": "peer-1", "n": float64(42), "b": true, "s": "x"} if m.Str("to") != "peer-1" { t.Fatal("Str") } if m.Int("n") != 42 { t.Fatal("Int") } if !m.Bool("b") { t.Fatal("Bool") } if !m.Truthy("s") || m.Truthy("missing") { t.Fatal("Truthy") } if !m.Has("to") || m.Has("nope") { t.Fatal("Has") } }