<!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>
