Files

98 lines
3.9 KiB
JavaScript

const {test} = require("node:test")
const assert = require("node:assert/strict")
const {startCall, sleep} = require("./harness")
// Chrome reports "disconnected" about 6 seconds after packets stop
const DISCONNECT_DETECTION = 10000
const RECONNECT = 20000
async function assertMediaFlows(a, b) {
const before = [await a.receivedBytes(), await b.receivedBytes()]
await sleep(2000)
const after = [await a.receivedBytes(), await b.receivedBytes()]
assert.ok(after[0] > before[0], `A received no media: ${before[0]} -> ${after[0]}`)
assert.ok(after[1] > before[1], `B received no media: ${before[1]} -> ${after[1]}`)
}
async function assertReconnects(a, b, outage) {
const from = [a.messages.length, b.messages.length]
await Promise.all([
a.waitForState("reconnecting", DISCONNECT_DETECTION, from[0]),
b.waitForState("reconnecting", DISCONNECT_DETECTION, from[1]),
])
await outage()
await Promise.all([a.waitForState("connected", RECONNECT, from[0]), b.waitForState("connected", RECONNECT, from[1])])
await assertMediaFlows(a, b)
assert.ok(!a.ended() && !b.ended(), "call ended")
assert.ok(!a.connectionStates().includes("disconnected"), `A states: ${a.connectionStates()}`)
assert.ok(!b.connectionStates().includes("disconnected"), `B states: ${b.connectionStates()}`)
}
async function withCall(opts, run) {
const call = await startCall(opts)
try {
await run(call)
} finally {
await call.close()
}
}
test("call reconnection", {concurrency: true}, async (t) => {
await t.test("recovers after a brief outage", () =>
withCall({}, async ({a, b, network}) => {
await assertMediaFlows(a, b)
network.down = true
await assertReconnects(a, b, async () => {
network.down = false
})
})
)
await t.test("recovers after an outage longer than ICE consent timeout", () =>
withCall({}, async ({a, b, network}) => {
network.down = true
await assertReconnects(a, b, async () => {
await sleep(15000)
network.down = false
})
assert.ok(b.sessionsSent() > 0, "offerer did not restart ICE")
assert.ok(a.sessionsSent() > 0, "answerer did not answer ICE restart")
})
)
await t.test("recovers when the network path changes", () =>
withCall({}, async ({a, b, network}) => {
network.down = true
network.changePath()
await assertReconnects(a, b, async () => {
network.down = false
})
const relay = network.relays[network.relays.length - 1]
assert.ok(!relay.dead && relay.forwarded > 0, "media does not use the new path")
})
)
await t.test("ends the call when reconnection times out", () =>
withCall({}, async ({a, b, network}) => {
await Promise.all([a.setTimeouts({reconnectTimeout: 5000}), b.setTimeouts({reconnectTimeout: 5000})])
network.down = true
await Promise.all([a.waitForState("reconnecting", DISCONNECT_DETECTION), b.waitForState("reconnecting", DISCONNECT_DETECTION)])
await Promise.all([a.waitForEnded(10000), b.waitForEnded(10000)])
assert.deepEqual(a.connectionStates().slice(-2), ["reconnecting", "disconnected"])
assert.deepEqual(b.connectionStates().slice(-2), ["reconnecting", "disconnected"])
})
)
await t.test("ends the call without reconnecting when the peer does not support ICE restart", () =>
withCall({canReconnect: false}, async ({a, b, network}) => {
network.down = true
await Promise.all([a.waitForEnded(DISCONNECT_DETECTION), b.waitForEnded(DISCONNECT_DETECTION)])
assert.ok(!a.connectionStates().includes("reconnecting"), `A states: ${a.connectionStates()}`)
assert.ok(!b.connectionStates().includes("reconnecting"), `B states: ${b.connectionStates()}`)
assert.equal(a.connectionStates().at(-1), "disconnected")
assert.equal(b.connectionStates().at(-1), "disconnected")
assert.equal(a.sessionsSent() + b.sessionsSent(), 0, "ICE was restarted")
})
)
})