diff options
Diffstat (limited to 'testing/web-platform/tests/webrtc-ice')
3 files changed, 407 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webrtc-ice/META.yml b/testing/web-platform/tests/webrtc-ice/META.yml new file mode 100644 index 0000000000..e683349e3c --- /dev/null +++ b/testing/web-platform/tests/webrtc-ice/META.yml @@ -0,0 +1,3 @@ +spec: https://w3c.github.io/webrtc-ice/ +suggested_reviewers: + - alvestrand diff --git a/testing/web-platform/tests/webrtc-ice/RTCIceTransport-extension-helper.js b/testing/web-platform/tests/webrtc-ice/RTCIceTransport-extension-helper.js new file mode 100644 index 0000000000..659ec59b8d --- /dev/null +++ b/testing/web-platform/tests/webrtc-ice/RTCIceTransport-extension-helper.js @@ -0,0 +1,42 @@ +'use strict'; + +// Construct an RTCIceTransport instance. The instance will automatically be +// cleaned up when the test finishes. +function makeIceTransport(t) { + const iceTransport = new RTCIceTransport(); + t.add_cleanup(() => iceTransport.stop()); + return iceTransport; +} + +// Construct two RTCIceTransport instances, configure them to exchange +// candidates, then gather() them. +// Returns a 2-list: [ RTCIceTransport, RTCIceTransport ] +function makeAndGatherTwoIceTransports(t) { + const localTransport = makeIceTransport(t); + const remoteTransport = makeIceTransport(t); + localTransport.onicecandidate = e => { + if (e.candidate) { + remoteTransport.addRemoteCandidate(e.candidate); + } + }; + remoteTransport.onicecandidate = e => { + if (e.candidate) { + localTransport.addRemoteCandidate(e.candidate); + } + }; + localTransport.gather({}); + remoteTransport.gather({}); + return [ localTransport, remoteTransport ]; +} + +// Construct two RTCIceTransport instances, configure them to exchange +// candidates and parameters, then gather() and start() them. +// Returns a 2-list: +// [ controlling RTCIceTransport, +// controlled RTCIceTransport ] +function makeGatherAndStartTwoIceTransports(t) { + const [ localTransport, remoteTransport ] = makeAndGatherTwoIceTransports(t); + localTransport.start(remoteTransport.getLocalParameters(), 'controlling'); + remoteTransport.start(localTransport.getLocalParameters(), 'controlled'); + return [ localTransport, remoteTransport ]; +} diff --git a/testing/web-platform/tests/webrtc-ice/RTCIceTransport-extension.https.html b/testing/web-platform/tests/webrtc-ice/RTCIceTransport-extension.https.html new file mode 100644 index 0000000000..bb4d52adce --- /dev/null +++ b/testing/web-platform/tests/webrtc-ice/RTCIceTransport-extension.https.html @@ -0,0 +1,362 @@ +<!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> |