MWSE/internal/services/services_test.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")
}
}