| <!doctype html> | |
| <meta charset=utf-8> | |
| <title>RTCIceTransport-extensions.https.html</title> | |
| <script src="/resources/testharness.js"></script> | |
| <script src="/resources/testharnessreport.js"></script> | |
| <script src="RTCIceTransport-extension-helper.js"></script> | |
| <script> | |
| 'use strict'; | |
| // These tests are based on the following extension specification: | |
| // https://w3c.github.io/webrtc-ice/ | |
| // The following helper functions are called from | |
| // RTCIceTransport-extension-helper.js: | |
| // makeIceTransport | |
| // makeGatherAndStartTwoIceTransports | |
| const ICE_UFRAG = 'u'.repeat(4); | |
| const ICE_PWD = 'p'.repeat(22); | |
| test(() => { | |
| const iceTransport = new RTCIceTransport(); | |
| }, 'RTCIceTransport constructor does not throw'); | |
| test(() => { | |
| const iceTransport = new RTCIceTransport(); | |
| assert_equals(iceTransport.role, null, 'Expect role to be null'); | |
| assert_equals(iceTransport.state, 'new', `Expect state to be 'new'`); | |
| assert_equals(iceTransport.gatheringState, 'new', | |
| `Expect gatheringState to be 'new'`); | |
| assert_array_equals(iceTransport.getLocalCandidates(), [], | |
| 'Expect no local candidates'); | |
| assert_array_equals(iceTransport.getRemoteCandidates(), [], | |
| 'Expect no remote candidates'); | |
| assert_equals(iceTransport.getSelectedCandidatePair(), null, | |
| 'Expect no selected candidate pair'); | |
| assert_not_equals(iceTransport.getLocalParameters(), null, | |
| 'Expect local parameters generated'); | |
| assert_equals(iceTransport.getRemoteParameters(), null, | |
| 'Expect no remote parameters'); | |
| }, 'RTCIceTransport initial properties are set'); | |
| test(t => { | |
| const iceTransport = makeIceTransport(t); | |
| assert_throws_js(TypeError, () => | |
| iceTransport.gather({ iceServers: null })); | |
| }, 'gather() with { iceServers: null } should throw TypeError'); | |
| test(t => { | |
| const iceTransport = makeIceTransport(t); | |
| iceTransport.gather({ iceServers: undefined }); | |
| }, 'gather() with { iceServers: undefined } should succeed'); | |
| test(t => { | |
| const iceTransport = makeIceTransport(t); | |
| iceTransport.gather({ iceServers: [{ | |
| urls: ['turns:turn.example.org', 'turn:turn.example.net'], | |
| username: 'user', | |
| credential: 'cred', | |
| }] }); | |
| }, 'gather() with one turns server, one turn server, username, credential' + | |
| ' should succeed'); | |
| test(t => { | |
| const iceTransport = makeIceTransport(t); | |
| iceTransport.gather({ iceServers: [{ | |
| urls: ['stun:stun1.example.net', 'stun:stun2.example.net'], | |
| }] }); | |
| }, 'gather() with 2 stun servers should succeed'); | |
| test(t => { | |
| const iceTransport = makeIceTransport(t); | |
| iceTransport.stop(); | |
| assert_throws_dom('InvalidStateError', () => iceTransport.gather({})); | |
| }, 'gather() throws if closed'); | |
| test(t => { | |
| const iceTransport = makeIceTransport(t); | |
| iceTransport.gather({}); | |
| assert_equals(iceTransport.gatheringState, 'gathering'); | |
| }, `gather() transitions gatheringState to 'gathering'`); | |
| test(t => { | |
| const iceTransport = makeIceTransport(t); | |
| iceTransport.gather({}); | |
| assert_throws_dom('InvalidStateError', () => iceTransport.gather({})); | |
| }, 'gather() throws if called twice'); | |
| promise_test(async t => { | |
| const iceTransport = makeIceTransport(t); | |
| const watcher = new EventWatcher(t, iceTransport, 'gatheringstatechange'); | |
| iceTransport.gather({}); | |
| await watcher.wait_for('gatheringstatechange'); | |
| assert_equals(iceTransport.gatheringState, 'complete'); | |
| }, `eventually transition gatheringState to 'complete'`); | |
| promise_test(async t => { | |
| const iceTransport = makeIceTransport(t); | |
| const watcher = new EventWatcher(t, iceTransport, | |
| [ 'icecandidate', 'gatheringstatechange' ]); | |
| iceTransport.gather({}); | |
| let candidate; | |
| do { | |
| (({ candidate } = await watcher.wait_for('icecandidate'))); | |
| } while (candidate !== null); | |
| assert_equals(iceTransport.gatheringState, 'gathering'); | |
| await watcher.wait_for('gatheringstatechange'); | |
| assert_equals(iceTransport.gatheringState, 'complete'); | |
| }, 'onicecandidate fires with null candidate before gatheringState' + | |
| ` transitions to 'complete'`); | |
| promise_test(async t => { | |
| const iceTransport = makeIceTransport(t); | |
| const watcher = new EventWatcher(t, iceTransport, 'icecandidate'); | |
| iceTransport.gather({}); | |
| const { candidate } = await watcher.wait_for('icecandidate'); | |
| assert_not_equals(candidate.candidate, ''); | |
| assert_array_equals(iceTransport.getLocalCandidates(), [candidate]); | |
| }, 'gather() returns at least one host candidate'); | |
| promise_test(async t => { | |
| const iceTransport = makeIceTransport(t); | |
| const watcher = new EventWatcher(t, iceTransport, 'icecandidate'); | |
| iceTransport.gather({ gatherPolicy: 'relay' }); | |
| const { candidate } = await watcher.wait_for('icecandidate'); | |
| assert_equals(candidate, null); | |
| assert_array_equals(iceTransport.getLocalCandidates(), []); | |
| }, `gather() returns no candidates with { gatherPolicy: 'relay'} and no turn` + | |
| ' servers'); | |
| const dummyRemoteParameters = { | |
| usernameFragment: ICE_UFRAG, | |
| password: ICE_PWD, | |
| }; | |
| test(() => { | |
| const iceTransport = new RTCIceTransport(); | |
| iceTransport.stop(); | |
| assert_throws_dom('InvalidStateError', | |
| () => iceTransport.start(dummyRemoteParameters)); | |
| assert_equals(iceTransport.getRemoteParameters(), null); | |
| }, `start() throws if closed`); | |
| test(() => { | |
| const iceTransport = new RTCIceTransport(); | |
| assert_throws_js(TypeError, () => iceTransport.start({})); | |
| assert_throws_js(TypeError, | |
| () => iceTransport.start({ usernameFragment: ICE_UFRAG })); | |
| assert_throws_js(TypeError, | |
| () => iceTransport.start({ password: ICE_PWD })); | |
| assert_equals(iceTransport.getRemoteParameters(), null); | |
| }, 'start() throws if usernameFragment or password not set'); | |
| test(() => { | |
| const TEST_CASES = [ | |
| {usernameFragment: '2sh', description: 'less than 4 characters long'}, | |
| { | |
| usernameFragment: 'x'.repeat(257), | |
| description: 'greater than 256 characters long', | |
| }, | |
| {usernameFragment: '123\n', description: 'illegal character'}, | |
| ]; | |
| for (const {usernameFragment, description} of TEST_CASES) { | |
| const iceTransport = new RTCIceTransport(); | |
| assert_throws_dom( | |
| 'SyntaxError', | |
| () => iceTransport.start({ usernameFragment, password: ICE_PWD }), | |
| `illegal usernameFragment (${description}) should throw a SyntaxError`); | |
| } | |
| }, 'start() throws if usernameFragment does not conform to syntax'); | |
| test(() => { | |
| const TEST_CASES = [ | |
| {password: 'x'.repeat(21), description: 'less than 22 characters long'}, | |
| { | |
| password: 'x'.repeat(257), | |
| description: 'greater than 256 characters long', | |
| }, | |
| {password: ('x'.repeat(21) + '\n'), description: 'illegal character'}, | |
| ]; | |
| for (const {password, description} of TEST_CASES) { | |
| const iceTransport = new RTCIceTransport(); | |
| assert_throws_dom( | |
| 'SyntaxError', | |
| () => iceTransport.start({ usernameFragment: ICE_UFRAG, password }), | |
| `illegal password (${description}) should throw a SyntaxError`); | |
| } | |
| }, 'start() throws if password does not conform to syntax'); | |
| const assert_ice_parameters_equals = (a, b) => { | |
| assert_equals(a.usernameFragment, b.usernameFragment, | |
| 'usernameFragments are equal'); | |
| assert_equals(a.password, b.password, 'passwords are equal'); | |
| }; | |
| test(t => { | |
| const iceTransport = makeIceTransport(t); | |
| iceTransport.start(dummyRemoteParameters); | |
| assert_equals(iceTransport.state, 'new'); | |
| assert_ice_parameters_equals(iceTransport.getRemoteParameters(), | |
| dummyRemoteParameters); | |
| }, `start() does not transition state to 'checking' if no remote candidates ` + | |
| 'added'); | |
| test(t => { | |
| const iceTransport = makeIceTransport(t); | |
| iceTransport.start(dummyRemoteParameters); | |
| assert_equals(iceTransport.role, 'controlled'); | |
| }, `start() with default role sets role attribute to 'controlled'`); | |
| test(t => { | |
| const iceTransport = makeIceTransport(t); | |
| iceTransport.start(dummyRemoteParameters, 'controlling'); | |
| assert_equals(iceTransport.role, 'controlling'); | |
| }, `start() sets role attribute to 'controlling'`); | |
| const candidate1 = new RTCIceCandidate({ | |
| candidate: 'candidate:1 1 udp 2113929471 203.0.113.100 10100 typ host', | |
| sdpMid: '', | |
| }); | |
| test(() => { | |
| const iceTransport = new RTCIceTransport(); | |
| iceTransport.stop(); | |
| assert_throws_dom('InvalidStateError', | |
| () => iceTransport.addRemoteCandidate(candidate1)); | |
| assert_array_equals(iceTransport.getRemoteCandidates(), []); | |
| }, 'addRemoteCandidate() throws if closed'); | |
| test(() => { | |
| const iceTransport = new RTCIceTransport(); | |
| assert_throws_dom('OperationError', | |
| () => iceTransport.addRemoteCandidate( | |
| new RTCIceCandidate({ candidate: 'invalid', sdpMid: '' }))); | |
| assert_array_equals(iceTransport.getRemoteCandidates(), []); | |
| }, 'addRemoteCandidate() throws on invalid candidate'); | |
| test(t => { | |
| const iceTransport = makeIceTransport(t); | |
| iceTransport.addRemoteCandidate(candidate1); | |
| iceTransport.start(dummyRemoteParameters); | |
| assert_equals(iceTransport.state, 'checking'); | |
| assert_array_equals(iceTransport.getRemoteCandidates(), [candidate1]); | |
| }, `start() transitions state to 'checking' if one remote candidate had been ` + | |
| 'added'); | |
| test(t => { | |
| const iceTransport = makeIceTransport(t); | |
| iceTransport.start(dummyRemoteParameters); | |
| iceTransport.addRemoteCandidate(candidate1); | |
| assert_equals(iceTransport.state, 'checking'); | |
| assert_array_equals(iceTransport.getRemoteCandidates(), [candidate1]); | |
| }, `addRemoteCandidate() transitions state to 'checking' if start() had been ` + | |
| 'called before'); | |
| test(t => { | |
| const iceTransport = makeIceTransport(t); | |
| iceTransport.start(dummyRemoteParameters); | |
| assert_throws_dom('InvalidStateError', | |
| () => iceTransport.start(dummyRemoteParameters, 'controlling')); | |
| }, 'start() throws if later called with a different role'); | |
| test(t => { | |
| const iceTransport = makeIceTransport(t); | |
| iceTransport.start({ | |
| usernameFragment: '1'.repeat(4), | |
| password: '1'.repeat(22), | |
| }); | |
| iceTransport.addRemoteCandidate(candidate1); | |
| const changedRemoteParameters = { | |
| usernameFragment: '2'.repeat(4), | |
| password: '2'.repeat(22), | |
| }; | |
| iceTransport.start(changedRemoteParameters); | |
| assert_equals(iceTransport.state, 'new'); | |
| assert_array_equals(iceTransport.getRemoteCandidates(), []); | |
| assert_ice_parameters_equals(iceTransport.getRemoteParameters(), | |
| changedRemoteParameters); | |
| }, `start() flushes remote candidates and transitions state to 'new' if ` + | |
| 'later called with different remote parameters'); | |
| promise_test(async t => { | |
| const [ localTransport, remoteTransport ] = | |
| makeGatherAndStartTwoIceTransports(t); | |
| const localWatcher = new EventWatcher(t, localTransport, 'statechange'); | |
| const remoteWatcher = new EventWatcher(t, remoteTransport, 'statechange'); | |
| await Promise.all([ | |
| localWatcher.wait_for('statechange').then(() => { | |
| assert_equals(localTransport.state, 'connected'); | |
| }), | |
| remoteWatcher.wait_for('statechange').then(() => { | |
| assert_equals(remoteTransport.state, 'connected'); | |
| }), | |
| ]); | |
| }, 'Two RTCIceTransports connect to each other'); | |
| ['controlling', 'controlled'].forEach(role => { | |
| promise_test(async t => { | |
| const [ localTransport, remoteTransport ] = | |
| makeAndGatherTwoIceTransports(t); | |
| localTransport.start(remoteTransport.getLocalParameters(), role); | |
| remoteTransport.start(localTransport.getLocalParameters(), role); | |
| const localWatcher = new EventWatcher(t, localTransport, 'statechange'); | |
| const remoteWatcher = new EventWatcher(t, remoteTransport, 'statechange'); | |
| await Promise.all([ | |
| localWatcher.wait_for('statechange').then(() => { | |
| assert_equals(localTransport.state, 'connected'); | |
| }), | |
| remoteWatcher.wait_for('statechange').then(() => { | |
| assert_equals(remoteTransport.state, 'connected'); | |
| }), | |
| ]); | |
| }, `Two RTCIceTransports configured with the ${role} role resolve the ` + | |
| 'conflict in band and still connect.'); | |
| }); | |
| promise_test(async t => { | |
| async function waitForSelectedCandidatePairChangeThenConnected(t, transport, | |
| transportName) { | |
| const watcher = new EventWatcher(t, transport, | |
| [ 'statechange', 'selectedcandidatepairchange' ]); | |
| await watcher.wait_for('selectedcandidatepairchange'); | |
| const selectedCandidatePair = transport.getSelectedCandidatePair(); | |
| assert_not_equals(selectedCandidatePair, null, | |
| `${transportName} selected candidate pair should not be null once ` + | |
| 'the selectedcandidatepairchange event fires'); | |
| assert_true( | |
| transport.getLocalCandidates().some( | |
| ({ candidate }) => | |
| candidate === selectedCandidatePair.local.candidate), | |
| `${transportName} selected candidate pair local should be in the ` + | |
| 'list of local candidates'); | |
| assert_true( | |
| transport.getRemoteCandidates().some( | |
| ({ candidate }) => | |
| candidate === selectedCandidatePair.remote.candidate), | |
| `${transportName} selected candidate pair local should be in the ` + | |
| 'list of remote candidates'); | |
| await watcher.wait_for('statechange'); | |
| assert_equals(transport.state, 'connected', | |
| `${transportName} state should be 'connected'`); | |
| } | |
| const [ localTransport, remoteTransport ] = | |
| makeGatherAndStartTwoIceTransports(t); | |
| await Promise.all([ | |
| waitForSelectedCandidatePairChangeThenConnected(t, localTransport, | |
| 'local transport'), | |
| waitForSelectedCandidatePairChangeThenConnected(t, remoteTransport, | |
| 'remote transport'), | |
| ]); | |
| }, 'Selected candidate pair changes once the RTCIceTransports connect.'); | |
| promise_test(async t => { | |
| const [ transport, ] = makeGatherAndStartTwoIceTransports(t); | |
| const watcher = new EventWatcher(t, transport, 'selectedcandidatepairchange'); | |
| await watcher.wait_for('selectedcandidatepairchange'); | |
| transport.stop(); | |
| assert_equals(transport.getSelectedCandidatePair(), null); | |
| }, 'getSelectedCandidatePair() returns null once the RTCIceTransport is ' + | |
| 'stopped.'); | |
| </script> |