158 lines
5.5 KiB
Go
158 lines
5.5 KiB
Go
package services
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.saqut.com/saqut/mwse/internal/testutil"
|
|
"git.saqut.com/saqut/mwse/internal/ws"
|
|
)
|
|
|
|
// findReply scans the captured frames for a [payload, id] frame whose id slot is
|
|
// the given number. This is how a peer's response/to answer reaches the original
|
|
// requester: the numeric request id sits in the signal slot so the SDK's event
|
|
// pool resolves the matching pending promise.
|
|
func findReply(fc *testutil.FakeConn, id float64) (map[string]any, bool) {
|
|
for _, raw := range fc.Writes() {
|
|
var arr []any
|
|
if json.Unmarshal(raw, &arr) != nil || len(arr) < 2 {
|
|
continue
|
|
}
|
|
if n, ok := arr[1].(float64); ok && n == id {
|
|
payload, _ := arr[0].(map[string]any)
|
|
return payload, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// TestRequestResponseRoundTrip is the #30 (data tunneling) + #33 (WOM) core: a
|
|
// request/to is answered out-of-band by the peer's response/to carrying the same
|
|
// id. The request/to handler itself must return nil so the engine sends no
|
|
// premature reply that would clobber the pending request.
|
|
func TestRequestResponseRoundTrip(t *testing.T) {
|
|
hub := newHub()
|
|
a, fa := connect(hub, "a")
|
|
b, fb := connect(hub, "b")
|
|
|
|
// a sends a request to b with request id 7.
|
|
if r := hub.Handle(a, msg("request/to", "to", "b", "pack", map[string]any{"q": "ping"})); r != nil {
|
|
t.Fatalf("request/to must return nil (answered out-of-band), got %v", r)
|
|
}
|
|
|
|
// b receives the request signal, with a's id and the payload, but NO source IP.
|
|
req := waitSignal(t, fb, "request")
|
|
if req["from"] != "a" {
|
|
t.Fatalf("request from = %v, want a", req["from"])
|
|
}
|
|
assertNoAddressLeak(t, req)
|
|
|
|
// b answers with response/to using the original request id.
|
|
if r := hub.Handle(b, msg("response/to", "to", "a", "id", float64(7), "pack", map[string]any{"a": "pong"})); r != nil {
|
|
t.Fatalf("response/to must return nil, got %v", r)
|
|
}
|
|
|
|
// a receives [ {from:"b", pack:{a:"pong"}}, 7 ] — resolving request id 7.
|
|
waitFor(t, func() bool { _, ok := findReply(fa, 7); return ok })
|
|
ans, _ := findReply(fa, 7)
|
|
if ans["from"] != "b" {
|
|
t.Fatalf("answer from = %v, want b", ans["from"])
|
|
}
|
|
pack := asMap(t, ans["pack"])
|
|
if pack["a"] != "pong" {
|
|
t.Fatalf("answer pack = %v, want {a:pong}", pack)
|
|
}
|
|
_ = b
|
|
}
|
|
|
|
// TestTunnelDoesNotLeakSourceAddress verifies the virtualization requirement of
|
|
// #30: a relayed pack carries only the logical sender id and the payload, never
|
|
// the sender's real IP or device type.
|
|
func TestTunnelDoesNotLeakSourceAddress(t *testing.T) {
|
|
hub := newHub()
|
|
a, _ := connect(hub, "a")
|
|
_, fb := connect(hub, "b")
|
|
|
|
hub.Handle(a, msg("pack/to", "to", "b", "pack", map[string]any{"hi": true}))
|
|
got := waitSignal(t, fb, "pack")
|
|
assertNoAddressLeak(t, got)
|
|
if got["from"] != "a" {
|
|
t.Fatalf("pack from = %v, want a", got["from"])
|
|
}
|
|
}
|
|
|
|
// TestTunnelLargePayloadIntact verifies #30's large-payload requirement: a big
|
|
// pack is relayed byte-for-byte (the engine never truncates or mangles it). The
|
|
// transport's MaxMessageSize default (16 MiB) comfortably covers chunked file
|
|
// transfer frames.
|
|
func TestTunnelLargePayloadIntact(t *testing.T) {
|
|
hub := newHub()
|
|
a, _ := connect(hub, "a")
|
|
_, fb := connect(hub, "b")
|
|
|
|
big := strings.Repeat("x", 1<<20) // 1 MiB chunk
|
|
hub.Handle(a, msg("pack/to", "to", "b", "pack", map[string]any{"chunk": big}))
|
|
|
|
got := waitSignal(t, fb, "pack")
|
|
pack := asMap(t, got["pack"])
|
|
if s, _ := pack["chunk"].(string); len(s) != len(big) {
|
|
t.Fatalf("relayed chunk length = %d, want %d", len(s), len(big))
|
|
}
|
|
}
|
|
|
|
// TestWebRTCSignalingRelay is the #31 parity test. WebRTC signaling is not a
|
|
// distinct engine concept: the SDK tunnels offer/answer/ICE as opaque
|
|
// {type:":rtcpack:", payload:{...}} packs over pack/to. The engine must carry
|
|
// them through unchanged, in both directions, without inspecting the RTC payload.
|
|
func TestWebRTCSignalingRelay(t *testing.T) {
|
|
hub := newHub()
|
|
a, fa := connect(hub, "a")
|
|
b, fb := connect(hub, "b")
|
|
|
|
signal := func(from *ws.Client, to string, rtc map[string]any) {
|
|
hub.Handle(from, msg("pack/to", "to", to,
|
|
"pack", map[string]any{"type": ":rtcpack:", "payload": rtc}))
|
|
}
|
|
|
|
// A → B: SDP offer.
|
|
signal(a, "b", map[string]any{"type": "offer", "value": map[string]any{"sdp": "v=0...offer"}})
|
|
offerPack := asMap(t, waitSignal(t, fb, "pack")["pack"])
|
|
if offerPack["type"] != ":rtcpack:" {
|
|
t.Fatalf("offer relayed as %v, want :rtcpack:", offerPack["type"])
|
|
}
|
|
if rtc := asMap(t, offerPack["payload"]); rtc["type"] != "offer" {
|
|
t.Fatalf("inner signaling type = %v, want offer", rtc["type"])
|
|
}
|
|
|
|
// B → A: SDP answer.
|
|
signal(b, "a", map[string]any{"type": "answer", "value": map[string]any{"sdp": "v=0...answer"}})
|
|
answerPack := asMap(t, waitSignal(t, fa, "pack")["pack"])
|
|
if rtc := asMap(t, answerPack["payload"]); rtc["type"] != "answer" {
|
|
t.Fatalf("inner signaling type = %v, want answer", rtc["type"])
|
|
}
|
|
|
|
// A → B: ICE candidate — payload carried verbatim.
|
|
cand := map[string]any{"candidate": "candidate:842163049 1 udp ...", "sdpMLineIndex": float64(0)}
|
|
signal(a, "b", map[string]any{"type": "icecandidate", "value": cand})
|
|
waitFor(t, func() bool {
|
|
for _, raw := range fb.Writes() {
|
|
if strings.Contains(string(raw), "icecandidate") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
})
|
|
}
|
|
|
|
// assertNoAddressLeak fails if a relayed payload exposes anything resembling the
|
|
// sender's real network identity.
|
|
func assertNoAddressLeak(t *testing.T, payload map[string]any) {
|
|
t.Helper()
|
|
for _, k := range []string{"ip", "address", "remoteAddr", "host", "device", "deviceType"} {
|
|
if _, ok := payload[k]; ok {
|
|
t.Fatalf("relayed payload leaked %q: %v", k, payload)
|
|
}
|
|
}
|
|
}
|