diff options
Diffstat (limited to 'packages/meshbay-hub/tests/harness')
9 files changed, 74 insertions, 4 deletions
diff --git a/packages/meshbay-hub/tests/harness/cast_session_probe.py b/packages/meshbay-hub/tests/harness/cast_session_probe.py index 573dd950..901d27cd 100644 --- a/packages/meshbay-hub/tests/harness/cast_session_probe.py +++ b/packages/meshbay-hub/tests/harness/cast_session_probe.py @@ -99,6 +99,7 @@ window.MeshBayTransport = function () { // One chunk per photo: every photo is the same picture. async fetchChunk() { return { nonce: 'n', ct: await photoBytes() }; }, addReconnectListener() { return () => {}; }, + addReconnectFailureListener() { return () => {}; }, close() {}, }; return new Proxy(self, { diff --git a/packages/meshbay-hub/tests/harness/group_tab_probe.py b/packages/meshbay-hub/tests/harness/group_tab_probe.py index e23a9462..a430c625 100644 --- a/packages/meshbay-hub/tests/harness/group_tab_probe.py +++ b/packages/meshbay-hub/tests/harness/group_tab_probe.py @@ -73,6 +73,7 @@ window.MeshBayTransport = class { // unmount; a stub without this throws inside connect() and the page // renders its error state instead of a tab bar. addReconnectListener() { return () => {}; } + addReconnectFailureListener() { return () => {}; } // What the node should hold of our identity, settled after connecting. async settleNodeBundle() {} close() {} diff --git a/packages/meshbay-hub/tests/harness/music_grid_probe.py b/packages/meshbay-hub/tests/harness/music_grid_probe.py index 4f5b0ce7..1995b2a1 100644 --- a/packages/meshbay-hub/tests/harness/music_grid_probe.py +++ b/packages/meshbay-hub/tests/harness/music_grid_probe.py @@ -81,6 +81,7 @@ window.MeshBayTransport = function () { // Real, not left to the Proxy below: that would hand back a promise // where an unsubscribe belongs, and the page calls it on unmount. addReconnectListener() { return () => {}; }, + addReconnectFailureListener() { return () => {}; }, close() {}, }; return new Proxy(self, { diff --git a/packages/meshbay-hub/tests/harness/music_queue_probe.py b/packages/meshbay-hub/tests/harness/music_queue_probe.py index 9be440b3..83c2972e 100755 --- a/packages/meshbay-hub/tests/harness/music_queue_probe.py +++ b/packages/meshbay-hub/tests/harness/music_queue_probe.py @@ -80,6 +80,7 @@ window.MeshBayTransport = function () { // Real, not left to the Proxy below: that would hand back a promise // where an unsubscribe belongs, and the page calls it on unmount. addReconnectListener() { return () => {}; }, + addReconnectFailureListener() { return () => {}; }, close() {}, }; return new Proxy(self, { diff --git a/packages/meshbay-hub/tests/harness/music_untagged_probe.py b/packages/meshbay-hub/tests/harness/music_untagged_probe.py index f4c484c3..7af1efc7 100644 --- a/packages/meshbay-hub/tests/harness/music_untagged_probe.py +++ b/packages/meshbay-hub/tests/harness/music_untagged_probe.py @@ -110,6 +110,7 @@ window.MeshBayTransport = function () { async fetchChatHistory() { return { messages: [], hasMore: false }; }, async fetchLinkPreview() { return { ok: false }; }, addReconnectListener() { return () => {}; }, + addReconnectFailureListener() { return () => {}; }, close() {}, }; return new Proxy(self, { diff --git a/packages/meshbay-hub/tests/harness/network_change_harness.mjs b/packages/meshbay-hub/tests/harness/network_change_harness.mjs index e0201125..a8bd0809 100644 --- a/packages/meshbay-hub/tests/harness/network_change_harness.mjs +++ b/packages/meshbay-hub/tests/harness/network_change_harness.mjs @@ -12,7 +12,8 @@ // // Usage: node network_change_harness.mjs <transport sources> <json> // {"scenario": "change-dead" | "change-alive" | "backoff" | "ice-dead" | "ice-alive" -// | "events" | "closed"} +// | "events" | "closed" | "refused" +// | "refused-during"} import { readFileSync } from 'fs'; import { delimiter } from 'path'; @@ -98,12 +99,21 @@ window.MeshBayCrypto = { }; const SDP = 'v=0\r\na=fingerprint:sha-256 AA:BB\r\n'; +const candidate = (address, kind) => `a=candidate:1 1 udp 2130706431 ${address} 50000 typ ${kind}\r\n`; +// On the node's Wi-Fi without the permission (test_local_network_refused.py): +// the phone's offer and the node's answer as measured, and the node's address +// on the phone's subnet missing from what the browser paired. +const WIFI_OFFER = SDP + candidate('192.168.1.13', 'host') + candidate('90.112.206.172', 'srflx'); +const NODE_ANSWER = SDP + candidate('192.168.1.22', 'host') + candidate('90.112.206.172', 'srflx'); +const KEPT_WHEN_REFUSED = ['90.112.206.172']; let offline = false; +let lanRefused = false; globalThis.fetch = async (url) => { if (offline) throw new TypeError('Failed to fetch'); return { ok: true, status: 200, headers: { get: () => null }, - json: async () => (url.endsWith('/mnp-token') ? { mnp_token: 't' } : { sdp: SDP }), + json: async () => (url.endsWith('/mnp-token') ? { mnp_token: 't' } + : { sdp: lanRefused ? NODE_ANSWER : SDP }), }; }; @@ -167,10 +177,30 @@ class FakePC { pcs.push(this); } createDataChannel() { this.channel = new FakeChannel(); return this.channel; } - async createOffer() { return { type: 'offer', sdp: SDP }; } + async createOffer() { return { type: 'offer', sdp: lanRefused ? WIFI_OFFER : SDP }; } async setLocalDescription(d) { this.localDescription = d; } - async getStats() { return new Map(); } + async getStats() { + const reports = new Map(); + if (this.refused) { + KEPT_WHEN_REFUSED.forEach((address, i) => { + reports.set(`R${i}`, { id: `R${i}`, type: 'remote-candidate', address }); + }); + } + return reports; + } async setRemoteDescription() { + if (lanRefused) { + // ICE gives up, and the channel goes with it, as Chrome reports it: + // there after thirty seconds, here after fifteen. + this.refused = true; + setTimeout(() => { + if (this.connectionState === 'closed') return; + this.iceConnectionState = 'failed'; + this.connectionState = 'failed'; + this.channel.close(); + }, 15000); + return; + } setTimeout(() => { if (this.connectionState === 'closed') return; this.channel.session = new NodeSession(this.channel); @@ -218,6 +248,8 @@ if (scenario === 'events') { const main = new Transport('https://hub.invalid', 'token'); let reconnectedAt = null; main.addReconnectListener(() => { if (reconnectedAt === null) reconnectedAt = now; }); + const failures = []; + main.addReconnectFailureListener((err) => { failures.push(err.reason || null); }); let connected = false; const identity = { pkEdB64: 'ed', signAs: async () => 'sig', native: false }; main.connect('node', 'token', 'group', new Uint8Array(32), identity, null, 'u', 'uid', @@ -252,6 +284,36 @@ if (scenario === 'events') { connection.dispatch('change'); out.backAt = now; await run(80000); + } else if (scenario === 'refused-during') { + // The same, with the permission granted while an attempt set up before it + // is still waiting on ICE: that attempt is given up, the next goes at once. + lanRefused = true; + first.channel.session.cut = true; + connection.type = 'wifi'; + connection.dispatch('change'); + await run(40000); + const attempts = pcs.length; + while (pcs.length === attempts) await run(now + 100); + await run(now + 2000); + lanRefused = false; + out.backAt = now; + main.retryNow(); + await run(now + 40000); + } else if (scenario === 'refused') { + // Connected on mobile data, then on the node's Wi-Fi without the local + // network permission: every attempt is refused, and says so. The + // permission is granted after a while, and the page asks to try now. + lanRefused = true; + first.channel.session.cut = true; + connection.type = 'wifi'; + connection.dispatch('change'); + await run(60000); + out.failures = [...failures]; + out.stillReconnecting = main.reconnecting; + lanRefused = false; + out.backAt = now; + main.retryNow(); + await run(80000); } else if (scenario === 'closed') { main.close(); connection.type = 'wifi'; diff --git a/packages/meshbay-hub/tests/harness/playlist_ui_probe.py b/packages/meshbay-hub/tests/harness/playlist_ui_probe.py index 6e3be1e2..0d6e4134 100644 --- a/packages/meshbay-hub/tests/harness/playlist_ui_probe.py +++ b/packages/meshbay-hub/tests/harness/playlist_ui_probe.py @@ -76,6 +76,7 @@ window.MeshBayTransport = function () { // Real, not left to the Proxy below: that would hand back a promise // where an unsubscribe belongs, and the page calls it on unmount. addReconnectListener() { return () => {}; }, + addReconnectFailureListener() { return () => {}; }, close() {}, }; return new Proxy(self, { diff --git a/packages/meshbay-hub/tests/harness/sticky_header_probe.py b/packages/meshbay-hub/tests/harness/sticky_header_probe.py index 3e75494b..89fb2827 100755 --- a/packages/meshbay-hub/tests/harness/sticky_header_probe.py +++ b/packages/meshbay-hub/tests/harness/sticky_header_probe.py @@ -240,6 +240,7 @@ window.MeshBayTransport = function () { // Real, not left to the Proxy below: that would hand back a promise // where an unsubscribe belongs, and the page calls it on unmount. addReconnectListener() { return () => {}; }, + addReconnectFailureListener() { return () => {}; }, close() {}, }; return new Proxy(self, { diff --git a/packages/meshbay-hub/tests/harness/video_series_probe.py b/packages/meshbay-hub/tests/harness/video_series_probe.py index f161c305..2b9ade30 100644 --- a/packages/meshbay-hub/tests/harness/video_series_probe.py +++ b/packages/meshbay-hub/tests/harness/video_series_probe.py @@ -75,6 +75,7 @@ window.MeshBayTransport = function () { // what TMDB would have said. async fetchMediaMeta() { return { confidence: 0 }; }, addReconnectListener() { return () => {}; }, + addReconnectFailureListener() { return () => {}; }, close() {}, }; // Everything else the player asks for never answers: it sits on its |