262 lines
7.1 KiB
Go
262 lines
7.1 KiB
Go
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")
|
|
}
|
|
}
|