(ns ird.p2p.wrtc-test
"Two real WebRTC data channels inside one process, always on: two rooms
signal through the in-process fake relay while their peer connections —
genuine node-datachannel (libdatachannel) connections from
ird.p2p.impl.wrtc/wrtc-factory — negotiate SDP through the room, trickle
ICE and connect over loopback. Proves both peers reach :peer-ready
:via :direct, then round-trips one small binary frame, one 50,000-byte
binary payload (chunked over the channel) and one JSON frame. Set
IRD_P2P_NO_WRTC=1 to skip where node-datachannel has no prebuilt binary."
(:require [cljs.test :refer [deftest is]]
[cljs.core.async :as async :refer [<!]]
[ird.p2p :as p2p]
[ird.p2p.crypto :as crypto]
[ird.p2p.descriptor :as desc]
[ird.p2p.fake-relay :as fake]
[ird.p2p.impl.bytes :as bytes]
[ird.p2p.impl.wrtc :as wrtc]
[ird.p2p.test-util :as tu :refer [deftest-async]]
[ird.p2p.transport.ws :as ws]))
(def low "00000000000000000000000000000001")
(def high "0f1e2d3c4b5a69788796a5b4c3d2e1f0")
(deftest ice-servers-translate-to-libdatachannel-entries
(is (= [{:hostname "stun.example" :port 3478}
{:hostname "turn.example" :port 3478 :username "u" :password "p" :relayType "TurnUdp"}
{:hostname "turn.example" :port 3478 :username "u" :password "p" :relayType "TurnTcp"}
{:hostname "turn.example" :port 5349 :username "u" :password "p" :relayType "TurnTls"}]
(wrtc/ice-server-entries {:urls ["stun:stun.example" "turn:turn.example:3478?transport=udp"
"turn:turn.example:3478?transport=tcp" "turns:turn.example:5349"
"not a url"]
:username "u" :credential "p"}))))
(deftest-async two-real-data-channels-over-loopback
{:timeout-ms 45000}
(if (tu/env "IRD_P2P_NO_WRTC")
(is true "IRD_P2P_NO_WRTC set — skipping the real WebRTC data-channel test")
(let [relay (fake/make-relay)
factory (wrtc/wrtc-factory)
;; the fallback descriptor points TURN credentials at https://h/…; answer
;; that with a failure so the ICE configuration degrades to no servers
app (fn [] (p2p/make-app {:host "h" :app-salt "wrtc/v1"
:descriptor (desc/fallback "h")
:http-fn (tu/responding 503 {:error "no turn"})
:transport (ws/transport {:socket-fn (fake/socket-fn relay)})
:pc-factory factory}))
secret (crypto/random-secret)
ra (<! (p2p/join (app) secret {:peer-id low :options {:pacing? false}}))
ca (tu/collector (p2p/events ra))
rb (<! (p2p/join (app) secret {:peer-id high :options {:pacing? false}}))
cb (tu/collector (p2p/events rb))]
(is (not (tu/error? ra)) (some-> ra ex-message))
(is (not (tu/error? rb)) (some-> rb ex-message))
;; membership over the relay, then the real data channel comes up on both sides
(is (some? (<! (tu/wait-for ca #(= {:type :peer-ready :peer high :via :relayed} %) 10000))))
(is (some? (<! (tu/wait-for ca #(= {:type :peer-ready :peer high :via :direct} %) 20000))))
(is (some? (<! (tu/wait-for cb #(= {:type :peer-ready :peer low :via :direct} %) 20000))))
(is (= :direct (p2p/transport-of ra high)))
(is (= :direct (p2p/transport-of rb low)))
;; a small binary frame (one data-channel message) both ways
(let [small (bytes/bytes->b64 (map #(mod % 251) (range 3000)))]
(is (true? (<! (p2p/send ra high (p2p/data small)))))
(is (some? (<! (tu/wait-for cb #(= {:kind :data :payload small} (:message %)) 10000))))
(is (true? (<! (p2p/send rb low (p2p/data small)))))
(is (some? (<! (tu/wait-for ca #(= {:kind :data :payload small} (:message %)) 10000)))))
;; a 50,000-byte payload, chunked over the channel like the Swift smoke test's
(let [big (bytes/bytes->b64 (map #(mod % 253) (range 50000)))]
(is (= 50000 (bytes/byte-length big)))
(is (true? (<! (p2p/send ra high (p2p/data big)))))
(is (some? (<! (tu/wait-for cb #(= {:kind :data :payload big} (:message %)) 20000)))))
;; and a JSON frame
(is (true? (<! (p2p/send rb low (p2p/json {:text "over a real data channel"})))))
(is (some? (<! (tu/wait-for ca #(= "over a real data channel" (get-in % [:message :value :text])) 10000))))
(<! (p2p/leave ra))
(<! (p2p/leave rb)))))