MWSE/internal/services/parity_test.go

329 lines
11 KiB
Go

package services
import (
"fmt"
"testing"
"git.saqut.com/saqut/mwse/internal/ws"
)
// These tests cover the 1.0.0 engine-parity issues #27 (rooms), #28 (pairing) and
// #29 (virtual addressing), plus the leak-hardening done for high-scale operation.
// They share the helpers defined in services_test.go.
// ---- #27 Room system parity ---------------------------------------------
func TestRoomJoinTypes(t *testing.T) {
hub := newHub()
owner, _ := connect(hub, "owner")
create := func(name, joinType, cred string) {
fields := []any{
"accessType", "public", "joinType", joinType,
"notifyActionInvite", false, "notifyActionJoined", true, "notifyActionEjected", true,
"description", "d", "name", name,
}
if cred != "" {
fields = append(fields, "credential", cred)
}
r := asMap(t, hub.Handle(owner, msg("create-room", fields...)))
if r["status"] != "success" {
t.Fatalf("create-room(%s) = %v", joinType, r)
}
}
create("free-room", "free", "")
create("lock-room", "lock", "")
create("pw-room", "password", "secret")
joiner, _ := connect(hub, "joiner")
if r := asMap(t, hub.Handle(joiner, msg("joinroom", "name", "free-room"))); r["status"] != "success" {
t.Fatalf("free join = %v", r)
}
if r := asMap(t, hub.Handle(joiner, msg("joinroom", "name", "lock-room"))); r["message"] != "LOCKED-ROOM" {
t.Fatalf("lock join = %v, want LOCKED-ROOM", r)
}
if r := asMap(t, hub.Handle(joiner, msg("joinroom", "name", "pw-room", "credential", "wrong"))); r["message"] != "WRONG-PASSWORD" {
t.Fatalf("pw wrong = %v, want WRONG-PASSWORD", r)
}
if r := asMap(t, hub.Handle(joiner, msg("joinroom", "name", "pw-room", "credential", "secret"))); r["status"] != "success" {
t.Fatalf("pw right = %v, want success", r)
}
}
func TestRoomIfExistsJoin(t *testing.T) {
hub := newHub()
a, _ := connect(hub, "a")
b, _ := connect(hub, "b")
common := []any{
"accessType", "public", "joinType", "free",
"notifyActionInvite", false, "notifyActionJoined", true, "notifyActionEjected", true,
"description", "d", "name", "shared",
}
if r := asMap(t, hub.Handle(a, msg("create-room", common...))); r["status"] != "success" {
t.Fatalf("first create = %v", r)
}
// Without ifexistsJoin: duplicate name fails.
if r := asMap(t, hub.Handle(b, msg("create-room", common...))); r["message"] != "ALREADY-EXISTS" {
t.Fatalf("dup create = %v, want ALREADY-EXISTS", r)
}
// With ifexistsJoin: b joins the existing room instead of failing.
withFlag := append(append([]any{}, common...), "ifexistsJoin", true)
r := asMap(t, hub.Handle(b, msg("create-room", withFlag...)))
if r["status"] != "success" {
t.Fatalf("ifexistsJoin create = %v", r)
}
roomID := asMap(t, r["room"])["id"].(string)
if !b.InRoom(roomID) {
t.Fatal("b should have joined the existing room via ifexistsJoin")
}
}
func TestRoomPerConnectionNotifySuppression(t *testing.T) {
hub := newHub()
a, fa := connect(hub, "a")
created := asMap(t, hub.Handle(a, msg("create-room",
"accessType", "public", "joinType", "free",
"notifyActionInvite", false, "notifyActionJoined", true, "notifyActionEjected", true,
"description", "d", "name", "R",
)))
if created["status"] != "success" {
t.Fatalf("create = %v", created)
}
// a opts out of peer-info notifications; it must NOT receive room/joined.
hub.Handle(a, msg("connection/pairinfo", "value", float64(0)))
beforeJoined := func() bool { _, ok := findSignal(fa, "room/joined"); return ok }()
b, _ := connect(hub, "b")
if r := asMap(t, hub.Handle(b, msg("joinroom", "name", "R"))); r["status"] != "success" {
t.Fatalf("b join = %v", r)
}
// Give any (erroneous) delivery time to land, then assert nothing new arrived.
waitFor(t, func() bool { return true })
if afterJoined := func() bool { _, ok := findSignal(fa, "room/joined"); return ok }(); afterJoined && !beforeJoined {
t.Fatal("a disabled pairinfo but still received room/joined")
}
}
func TestRoomInviteFlow(t *testing.T) {
hub := newHub()
owner, fo := connect(hub, "owner")
created := asMap(t, hub.Handle(owner, msg("create-room",
"accessType", "public", "joinType", "invite",
"notifyActionInvite", false, "notifyActionJoined", true, "notifyActionEjected", true,
"description", "d", "name", "club",
)))
roomID := asMap(t, created["room"])["id"].(string)
guest, fg := connect(hub, "guest")
resp := asMap(t, hub.Handle(guest, msg("joinroom", "name", "club")))
if resp["message"] != "INVITE-REQUESTED" {
t.Fatalf("invite join = %v, want INVITE-REQUESTED", resp)
}
if inv := waitSignal(t, fo, "room/invite"); inv["id"] != "guest" {
t.Fatalf("owner invite signal = %v", inv)
}
accepted := asMap(t, hub.Handle(owner, msg("accept/invite-room", "roomId", roomID, "clientId", "guest")))
if accepted["status"] != "success" {
t.Fatalf("accept invite = %v", accepted)
}
if st := waitSignal(t, fg, "room/invite/status"); st["status"] != "accepted" {
t.Fatalf("guest status signal = %v", st)
}
if !guest.InRoom(roomID) {
t.Fatal("guest should be in the room after accepted invite")
}
}
// ---- #28 Pairing parity --------------------------------------------------
func TestPairRejectAndEnd(t *testing.T) {
hub := newHub()
a, fa := connect(hub, "a")
b, _ := connect(hub, "b")
hub.Handle(a, msg("request/pair", "to", "b"))
hub.Handle(b, msg("accept/pair", "to", "a"))
if !isPaired(a, b) {
t.Fatal("precondition: a and b paired")
}
if r := asMap(t, hub.Handle(b, msg("end/pair", "to", "a"))); r["status"] != "success" {
t.Fatalf("end/pair = %v", r)
}
waitSignal(t, fa, "end/pair")
if isPaired(a, b) || a.HasPair("b") || b.HasPair("a") {
t.Fatal("end/pair should fully unpair both sides")
}
}
func TestIsReachable(t *testing.T) {
hub := newHub()
a, _ := connect(hub, "a")
b, _ := connect(hub, "b")
// b is public by default -> reachable.
if r := hub.Handle(a, msg("is/reachable", "to", "b")); r != true {
t.Fatalf("public reachable = %v, want true", r)
}
// b goes private -> not reachable until paired.
hub.Handle(b, msg("auth/private"))
if r := hub.Handle(a, msg("is/reachable", "to", "b")); r != false {
t.Fatalf("private unpaired reachable = %v, want false", r)
}
hub.Handle(a, msg("request/pair", "to", "b"))
hub.Handle(b, msg("accept/pair", "to", "a"))
if r := hub.Handle(a, msg("is/reachable", "to", "b")); r != true {
t.Fatalf("private paired reachable = %v, want true", r)
}
}
func TestPairListMutualOnly(t *testing.T) {
hub := newHub()
a, _ := connect(hub, "a")
b, _ := connect(hub, "b")
// One-directional request: not yet mutual, so pair/list is empty.
hub.Handle(a, msg("request/pair", "to", "b"))
if list := asMap(t, hub.Handle(a, msg("pair/list")))["value"]; list != nil {
if arr, ok := list.([]string); ok && len(arr) != 0 {
t.Fatalf("pair/list before accept = %v, want empty", arr)
}
}
hub.Handle(b, msg("accept/pair", "to", "a"))
list, _ := asMap(t, hub.Handle(a, msg("pair/list")))["value"].([]string)
if len(list) != 1 || list[0] != "b" {
t.Fatalf("pair/list after accept = %v, want [b]", list)
}
}
// ---- #29 Virtual addressing (IPPressure) parity --------------------------
func TestIPPressureReallocAndRelease(t *testing.T) {
hub := newHub()
a, _ := connect(hub, "a")
n1 := asMap(t, hub.Handle(a, msg("alloc/APNumber")))["number"].(int)
// realloc gives a different number and frees the old one.
n2 := asMap(t, hub.Handle(a, msg("realloc/APNumber")))["number"].(int)
if n1 == n2 {
t.Fatalf("realloc returned same number %d", n1)
}
if who := asMap(t, hub.Handle(a, msg("whois/APNumber", "whois", float64(n1)))); who["status"] != "fail" {
t.Fatalf("old number should be free after realloc, whois = %v", who)
}
// release clears it from the client and the table.
hub.Handle(a, msg("release/APNumber"))
if a.APNumber() != 0 {
t.Fatal("number not cleared on release")
}
// shortcode + ip alloc work and are non-empty.
if code := asMap(t, hub.Handle(a, msg("alloc/APShortCode")))["code"].(string); len(code) != 3 {
t.Fatalf("shortcode = %q, want 3 letters", code)
}
if ip := asMap(t, hub.Handle(a, msg("alloc/APIPAddress")))["ip"].(string); ip == "" {
t.Fatal("ip alloc returned empty")
}
}
// ---- leak hardening (high-scale, no unbounded growth) --------------------
func TestDisconnectCleansIncomingPairEdge(t *testing.T) {
hub := newHub()
a, _ := connect(hub, "a")
b, _ := connect(hub, "b")
// One-directional pending request: a -> b. a.pairs has b; b.pairedBy has a.
hub.Handle(a, msg("request/pair", "to", "b"))
if !a.HasPair("b") {
t.Fatal("precondition: a has outgoing edge to b")
}
// b disconnects without ever responding. a must not retain a stale edge.
hub.Disconnect(b)
if a.HasPair("b") {
t.Fatal("stale pair edge to a disconnected client was left behind (leak)")
}
if len(a.PairedBy()) != 0 {
t.Fatalf("a.PairedBy() = %v, want empty", a.PairedBy())
}
}
func TestDisconnectCleansWaitingList(t *testing.T) {
hub := newHub()
owner, _ := connect(hub, "owner")
created := asMap(t, hub.Handle(owner, msg("create-room",
"accessType", "public", "joinType", "invite",
"notifyActionInvite", false, "notifyActionJoined", true, "notifyActionEjected", true,
"description", "d", "name", "club",
)))
roomID := asMap(t, created["room"])["id"].(string)
guest, _ := connect(hub, "guest")
hub.Handle(guest, msg("joinroom", "name", "club"))
room, _ := hub.Room(roomID)
if !room.IsWaiting("guest") {
t.Fatal("precondition: guest should be on the waiting list")
}
hub.Disconnect(guest)
if room.IsWaiting("guest") {
t.Fatal("disconnected client left on the room waiting list (leak)")
}
}
// TestHighChurnLeavesNoResidualState drives many connect/pair/room/disconnect
// cycles and asserts the hub holds no clients or rooms afterwards — i.e. nothing
// accumulates across churn at scale.
func TestHighChurnLeavesNoResidualState(t *testing.T) {
hub := newHub()
for cycle := 0; cycle < 20; cycle++ {
roomName := fmt.Sprintf("room-%d", cycle)
clients := make([]*ws.Client, 0, 25)
for i := 0; i < 25; i++ {
c, _ := connect(hub, fmt.Sprintf("c%d-%d", cycle, i))
clients = append(clients, c)
}
owner := clients[0]
hub.Handle(owner, msg("create-room",
"accessType", "public", "joinType", "free",
"notifyActionInvite", false, "notifyActionJoined", true, "notifyActionEjected", true,
"description", "d", "name", roomName,
))
for _, c := range clients[1:] {
hub.Handle(c, msg("joinroom", "name", roomName))
hub.Handle(c, msg("request/pair", "to", owner.ID))
hub.Handle(c, msg("alloc/APNumber"))
}
for _, c := range clients {
hub.Disconnect(c)
}
}
if n := hub.ClientCount(); n != 0 {
t.Fatalf("residual clients after churn: %d (leak)", n)
}
if rooms := hub.Rooms(); len(rooms) != 0 {
t.Fatalf("residual rooms after churn: %d (leak)", len(rooms))
}
}