package services import ( "encoding/json" "testing" "time" "git.saqut.com/saqut/mwse/internal/protocol" "git.saqut.com/saqut/mwse/internal/testutil" "git.saqut.com/saqut/mwse/internal/ws" ) // ---- test helpers -------------------------------------------------------- func newHub() *ws.Hub { hub := ws.NewHub() Register(hub) return hub } // connect attaches a client (with services' connect hooks firing) and returns it // along with its captured connection. func connect(hub *ws.Hub, id string) (*ws.Client, *testutil.FakeConn) { fc := testutil.NewFakeConn() c := ws.NewClient(fc, id) hub.Connect(c) return c, fc } func waitFor(t *testing.T, cond func() bool) { t.Helper() for i := 0; i < 500; i++ { if cond() { return } time.Sleep(time.Millisecond) } t.Fatal("condition not met within timeout") } // findSignal scans the captured frames for a [payload, name] signal. func findSignal(fc *testutil.FakeConn, name string) (map[string]any, bool) { for _, raw := range fc.Writes() { var arr []any if json.Unmarshal(raw, &arr) != nil || len(arr) < 2 { continue } if s, ok := arr[1].(string); ok && s == name { payload, _ := arr[0].(map[string]any) return payload, true } } return nil, false } func waitSignal(t *testing.T, fc *testutil.FakeConn, name string) map[string]any { t.Helper() waitFor(t, func() bool { _, ok := findSignal(fc, name); return ok }) p, _ := findSignal(fc, name) return p } func asMap(t *testing.T, v any) map[string]any { t.Helper() m, ok := v.(map[string]any) if !ok { t.Fatalf("expected map, got %T (%v)", v, v) } return m } // msg builds a protocol.Message from a type and key/value pairs. func msg(typ string, kv ...any) protocol.Message { m := protocol.Message{"type": typ} for i := 0; i+1 < len(kv); i += 2 { m[kv[i].(string)] = kv[i+1] } return m } // ---- tests --------------------------------------------------------------- func TestConnectAnnouncesIDAndPrivateRoom(t *testing.T) { hub := newHub() c, fc := connect(hub, "alice") idPayload := waitSignal(t, fc, "id") if idPayload["value"] != "alice" { t.Fatalf("id signal value = %v, want alice", idPayload["value"]) } if _, ok := hub.Room("alice"); !ok { t.Fatal("private room named after the client id should exist") } if !c.InRoom("alice") { t.Fatal("client should be a member of its private room") } } func TestPairingFlow(t *testing.T) { hub := newHub() a, fa := connect(hub, "a") b, fb := connect(hub, "b") // a requests pairing with b. resp := asMap(t, hub.Handle(a, msg("request/pair", "to", "b"))) if resp["message"] != "REQUESTED" { t.Fatalf("request/pair = %v", resp) } req := waitSignal(t, fb, "request/pair") if req["from"] != "a" { t.Fatalf("request/pair from = %v, want a", req["from"]) } // b accepts. resp = asMap(t, hub.Handle(b, msg("accept/pair", "to", "a"))) if resp["status"] != "success" { t.Fatalf("accept/pair = %v", resp) } acc := waitSignal(t, fa, "accepted/pair") if acc["from"] != "b" { t.Fatalf("accepted/pair from = %v, want b", acc["from"]) } if !a.HasPair("b") || !b.HasPair("a") { t.Fatal("both clients should be mutually paired") } } func TestRoomCreateJoinAndPackRoom(t *testing.T) { hub := newHub() a, _ := connect(hub, "a") b, fb := connect(hub, "b") created := asMap(t, hub.Handle(a, msg("create-room", "accessType", "public", "joinType", "free", "notifyActionInvite", false, "notifyActionJoined", true, "notifyActionEjected", true, "description", "d", "name", "R1", ))) if created["status"] != "success" { t.Fatalf("create-room = %v", created) } roomID := asMap(t, created["room"])["id"].(string) joined := asMap(t, hub.Handle(b, msg("joinroom", "name", "R1"))) if joined["status"] != "success" { t.Fatalf("joinroom = %v", joined) } if !b.InRoom(roomID) { t.Fatal("b should be in the room") } // a relays a pack to the room; b should receive it. res := asMap(t, hub.Handle(a, msg("pack/room", "to", roomID, "pack", map[string]any{"x": float64(1)}, "handshake", true))) if res["type"] != "success" { t.Fatalf("pack/room = %v", res) } got := waitSignal(t, fb, "pack/room") if got["sender"] != "a" { t.Fatalf("pack/room sender = %v, want a", got["sender"]) } } func TestDataTransferPackToAutoPairs(t *testing.T) { hub := newHub() a, _ := connect(hub, "a") b, fb := connect(hub, "b") res := asMap(t, hub.Handle(a, msg("pack/to", "to", "b", "pack", map[string]any{"hi": true}, "handshake", true))) if res["type"] != "success" { t.Fatalf("pack/to = %v", res) } got := waitSignal(t, fb, "pack") if got["from"] != "a" { t.Fatalf("pack from = %v, want a", got["from"]) } // Auto-pairing should have linked both sides. if !a.HasPair("b") || !b.HasPair("a") { t.Fatal("pack/to should auto-pair when the target does not require pairing") } } func TestSessionFlagGatesPackReadable(t *testing.T) { hub := newHub() a, _ := connect(hub, "a") connect(hub, "b") // a disables sending; pack/to must now fail the handshake. if r := asMap(t, hub.Handle(a, msg("connection/packsending", "value", float64(0)))); r["status"] != "success" { t.Fatalf("connection/packsending = %v", r) } res := asMap(t, hub.Handle(a, msg("pack/to", "to", "b", "pack", map[string]any{}, "handshake", true))) if res["type"] != "fail" { t.Fatalf("pack/to after disabling send = %v, want fail", res) } } func TestIPPressureAllocatesUniqueAddresses(t *testing.T) { hub := newHub() a, _ := connect(hub, "a") b, _ := connect(hub, "b") na := asMap(t, hub.Handle(a, msg("alloc/APNumber")))["number"].(int) nb := asMap(t, hub.Handle(b, msg("alloc/APNumber")))["number"].(int) if na == nb { t.Fatalf("AP numbers must be unique, both got %d", na) } who := asMap(t, hub.Handle(a, msg("whois/APNumber", "whois", float64(na)))) if who["socket"] != "a" { t.Fatalf("whois APNumber = %v, want a", who) } ipA := asMap(t, hub.Handle(a, msg("alloc/APIPAddress")))["ip"].(string) ipB := asMap(t, hub.Handle(b, msg("alloc/APIPAddress")))["ip"].(string) if ipA == ipB { t.Fatalf("AP ips must be unique, both got %s", ipA) } // Releasing frees the number for reuse. if r := asMap(t, hub.Handle(a, msg("release/APNumber"))); r["status"] != "success" { t.Fatalf("release/APNumber = %v", r) } if a.APNumber() != 0 { t.Fatal("released number should be cleared on the client") } } func TestDisconnectCleansRoomsAndPairs(t *testing.T) { hub := newHub() a, _ := connect(hub, "a") b, fb := connect(hub, "b") // Put both in a shared room and pair them. hub.Handle(a, msg("create-room", "accessType", "public", "joinType", "free", "notifyActionInvite", false, "notifyActionJoined", true, "notifyActionEjected", true, "description", "d", "name", "R1", )) hub.Handle(b, msg("joinroom", "name", "R1")) hub.Handle(a, msg("request/pair", "to", "b")) hub.Handle(b, msg("accept/pair", "to", "a")) if !a.HasPair("b") { t.Fatal("precondition: a should be paired with b") } hub.Disconnect(a) waitSignal(t, fb, "peer/disconnect") waitSignal(t, fb, "room/ejected") if _, ok := hub.Client("a"); ok { t.Fatal("disconnected client should be removed from the hub") } if b.HasPair("a") { t.Fatal("pair edge to the disconnected client should be gone") } }