Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

<!doctype html>
<title>RTCIceCandidate constructor</title>
<script src=/resources/testharness.js></script>
<script src=/resources/testharnessreport.js></script>
<script>
'use strict';
const candidateString = 'candidate:1905690388 1 udp 2113937151 192.168.0.1 58041 typ host generation 0 ufrag thC8 network-cost 50';
const candidateString2 = 'candidate:435653019 2 tcp 1845501695 192.168.0.196 4444 typ srflx raddr www.example.com rport 22222 tcptype active';
const arbitraryString = '<arbitrary string[0] content>;';
test(t => {
// The argument for RTCIceCandidateInit is optional (w3c/webrtc-pc #1153 #1166),
// but the constructor throws because both sdpMid and sdpMLineIndex are null by default.
// Note that current browsers pass this test but may throw TypeError for
// different reason, i.e. they don't accept empty argument.
// Further tests below are used to differentiate the errors.
assert_throws_js(TypeError, () => new RTCIceCandidate());
}, 'new RTCIceCandidate()');
test(t => {
// All fields in RTCIceCandidateInit are optional,
// but the constructor throws because both sdpMid and sdpMLineIndex are null by default.
// Note that current browsers pass this test but may throw TypeError for
// different reason, i.e. they don't allow undefined candidate string.
// Further tests below are used to differentiate the errors.
assert_throws_js(TypeError, () => new RTCIceCandidate({}));
}, 'new RTCIceCandidate({})');
test(t => {
// Checks that manually filling the default values for RTCIceCandidateInit
// still throws because both sdpMid and sdpMLineIndex are null
assert_throws_js(TypeError,
() => new RTCIceCandidate({
candidate: '',
sdpMid: null,
sdpMLineIndex: null,
usernameFragment: undefined
}));
}, 'new RTCIceCandidate({ ... }) with manually filled default values');
test(t => {
// Checks that explicitly setting both sdpMid and sdpMLineIndex null should throw
assert_throws_js(TypeError,
() => new RTCIceCandidate({
sdpMid: null,
sdpMLineIndex: null
}));
}, 'new RTCIceCandidate({ sdpMid: null, sdpMLineIndex: null })');
test(t => {
// Throws because both sdpMid and sdpMLineIndex are null by default
assert_throws_js(TypeError,
() => new RTCIceCandidate({
candidate: ''
}));
}, `new RTCIceCandidate({ candidate: '' })`);
test(t => {
// Throws because both sdpMid and sdpMLineIndex are null by default
assert_throws_js(TypeError,
() => new RTCIceCandidate({
candidate: null
}));
}, `new RTCIceCandidate({ candidate: null })`);
test(t => {
// Throws because both sdpMid and sdpMLineIndex are null by default
assert_throws_js(TypeError,
() => new RTCIceCandidate({
candidate: candidateString
}));
}, 'new RTCIceCandidate({ ... }) with valid candidate string only');
test(t => {
const candidate = new RTCIceCandidate({ sdpMid: 'audio' });
assert_equals(candidate.candidate, '', 'candidate');
assert_equals(candidate.sdpMid, 'audio', 'sdpMid');
assert_equals(candidate.sdpMLineIndex, null, 'sdpMLineIndex');
assert_equals(candidate.usernameFragment, null, 'usernameFragment');
}, `new RTCIceCandidate({ sdpMid: 'audio' })`);
test(t => {
const candidate = new RTCIceCandidate({ sdpMLineIndex: 0 });
assert_equals(candidate.candidate, '', 'candidate');
assert_equals(candidate.sdpMid, null, 'sdpMid');
assert_equals(candidate.sdpMLineIndex, 0, 'sdpMLineIndex');
assert_equals(candidate.usernameFragment, null, 'usernameFragment');
}, 'new RTCIceCandidate({ sdpMLineIndex: 0 })');
test(t => {
const candidate = new RTCIceCandidate({
sdpMid: 'audio',
sdpMLineIndex: 0
});
assert_equals(candidate.candidate, '', 'candidate');
assert_equals(candidate.sdpMid, 'audio', 'sdpMid');
assert_equals(candidate.sdpMLineIndex, 0, 'sdpMLineIndex');
assert_equals(candidate.usernameFragment, null, 'usernameFragment');
}, `new RTCIceCandidate({ sdpMid: 'audio', sdpMLineIndex: 0 })`);
test(t => {
const candidate = new RTCIceCandidate({
candidate: '',
sdpMid: 'audio'
});
assert_equals(candidate.candidate, '', 'candidate');
assert_equals(candidate.sdpMid, 'audio', 'sdpMid');
assert_equals(candidate.sdpMLineIndex, null, 'sdpMLineIndex');
assert_equals(candidate.usernameFragment, null, 'usernameFragment');
}, `new RTCIceCandidate({ candidate: '', sdpMid: 'audio' }`);
test(t => {
const candidate = new RTCIceCandidate({
candidate: null,
sdpMLineIndex: 0
});
assert_equals(candidate.candidate, 'null', 'candidate');
assert_equals(candidate.sdpMid, null, 'sdpMid', 'sdpMid');
assert_equals(candidate.sdpMLineIndex, 0, 'sdpMLineIndex');
assert_equals(candidate.usernameFragment, null, 'usernameFragment');
}, `new RTCIceCandidate({ candidate: null, sdpMLineIndex: 0 }`);
test(t => {
const candidate = new RTCIceCandidate({
candidate: candidateString,
sdpMid: 'audio'
});
assert_equals(candidate.candidate, candidateString, 'candidate');
assert_equals(candidate.sdpMid, 'audio', 'sdpMid');
assert_equals(candidate.sdpMLineIndex, null, 'sdpMLineIndex');
assert_equals(candidate.usernameFragment, null, 'usernameFragment');
}, 'new RTCIceCandidate({ ... }) with valid candidate string and sdpMid');
test(t => {
// candidate string is not validated in RTCIceCandidate
const candidate = new RTCIceCandidate({
candidate: arbitraryString,
sdpMid: 'audio'
});
assert_equals(candidate.candidate, arbitraryString, 'candidate');
assert_equals(candidate.sdpMid, 'audio', 'sdpMid');
assert_equals(candidate.sdpMLineIndex, null, 'sdpMLineIndex');
assert_equals(candidate.usernameFragment, null, 'usernameFragment');
}, 'new RTCIceCandidate({ ... }) with invalid candidate string and sdpMid');
["most", "all"].forEach(howMany => {
test(t => {
const candidate = new RTCIceCandidate({
candidate: candidateString,
sdpMid: 'video',
sdpMLineIndex: 1,
usernameFragment: 'test',
relayProtocol: 'udp',
url: 'stun:stun.example.org'
});
assert_equals(candidate.candidate, candidateString, 'candidate');
assert_equals(candidate.sdpMid, 'video', 'sdpMid');
assert_equals(candidate.sdpMLineIndex, 1, 'sdpMLineIndex');
assert_equals(candidate.usernameFragment, 'test', 'usernameFragment');
// The following fields should match those in the candidate field
assert_equals(candidate.foundation, '1905690388', 'foundation');
assert_equals(candidate.component, 'rtp', 'component');
assert_equals(candidate.priority, 2113937151, 'priority');
assert_equals(candidate.address, '192.168.0.1', 'address');
assert_equals(candidate.protocol, 'udp', 'protocol');
assert_equals(candidate.port, 58041, 'port');
assert_equals(candidate.type, 'host', 'type');
assert_equals(candidate.tcpType, null, 'tcpType');
assert_equals(candidate.relatedAddress, null, 'relatedAddress');
assert_equals(candidate.relatedPort, null, 'relatedPort');
// The remaining fields are newer, so test them seperately
if (howMany != "most") {
assert_equals(candidate.relayProtocol, 'udp', 'relayProtocol');
assert_equals(candidate.url, 'stun:stun.example.org', 'url');
}
}, `new RTCIceCandidate({ ... }) with nondefault values for ${howMany} fields`);
test(t => {
const candidate = new RTCIceCandidate({
candidate: candidateString2,
sdpMid: 'video',
sdpMLineIndex: 1,
usernameFragment: 'user1',
relayProtocol: 'tcp',
url: 'turn:turn.example.net'
});
assert_equals(candidate.candidate, candidateString2, 'candidate');
assert_equals(candidate.sdpMid, 'video', 'sdpMid');
assert_equals(candidate.sdpMLineIndex, 1, 'sdpMLineIndex');
assert_equals(candidate.usernameFragment, 'user1', 'usernameFragment');
// The following fields should match those in the candidate field
assert_equals(candidate.foundation, '435653019', 'foundation');
assert_equals(candidate.component, 'rtcp', 'component');
assert_equals(candidate.priority, 1845501695, 'priority');
assert_equals(candidate.address, '192.168.0.196', 'address');
assert_equals(candidate.protocol, 'tcp', 'protocol');
assert_equals(candidate.port, 4444, 'port');
assert_equals(candidate.type, 'srflx', 'type');
assert_equals(candidate.tcpType, 'active', 'tcpType');
assert_equals(candidate.relatedAddress, 'www.example.com', 'relatedAddress');
assert_equals(candidate.relatedPort, 22222, 'relatedPort');
// The remaining fields are newer, so test them seperately
if (howMany != "most") {
assert_equals(candidate.url, 'turn:turn.example.net', 'url');
assert_equals(candidate.relayProtocol, 'tcp', 'relayProtocol');
}
}, `new RTCIceCandidate({ ... }) with nondefault values for ${howMany} fields, tcp candidate`);
});
test(t => {
let candidate = new RTCIceCandidate(new RTCIceCandidate({
sdpMid: 'video',
relayProtocol: 'tls',
url: 'turn:turn.example.net'
}));
assert_equals(candidate.relayProtocol, 'tls', 'relayProtocol cloned');
assert_equals(candidate.url, 'turn:turn.example.net', 'url cloned');
candidate = new RTCIceCandidate(JSON.parse(JSON.stringify(candidate)));
assert_equals(candidate.relayProtocol, null, 'relayProtocol not signaled');
assert_equals(candidate.url, null, 'url not signaled');
}, `new RTCIceCandidate({ relayProtocol, url }) cloned vs signaled`);
test(t => {
// sdpMid is not validated in RTCIceCandidate
const candidate = new RTCIceCandidate({
sdpMid: arbitraryString
});
assert_equals(candidate.candidate, '', 'candidate');
assert_equals(candidate.sdpMid, arbitraryString, 'sdpMid');
assert_equals(candidate.sdpMLineIndex, null, 'sdpMLineIndex');
assert_equals(candidate.usernameFragment, null, 'usernameFragment');
}, 'new RTCIceCandidate({ ... }) with invalid sdpMid');
test(t => {
// Some arbitrary large out of bound line index that practically
// do not reference any m= line in SDP.
// However sdpMLineIndex is not validated in RTCIceCandidate
// and it has no knowledge of the SDP it is associated with.
const candidate = new RTCIceCandidate({
sdpMLineIndex: 65535
});
assert_equals(candidate.candidate, '', 'candidate');
assert_equals(candidate.sdpMid, null, 'sdpMid');
assert_equals(candidate.sdpMLineIndex, 65535, 'sdpMLineIndex');
assert_equals(candidate.usernameFragment, null, 'usernameFragment');
}, 'new RTCIceCandidate({ ... }) with invalid sdpMLineIndex');
// Helpers for the parser-coverage tests below.
function makeCandidate(candidateStr) {
return new RTCIceCandidate({ candidate: candidateStr, sdpMid: 'audio' });
}
// For a candidate string that fails to parse, the parse-derived
// attributes should all be null while the input is preserved verbatim.
function assertParseFailed(c) {
assert_equals(c.foundation, null, 'foundation');
assert_equals(c.component, null, 'component');
assert_equals(c.priority, null, 'priority');
assert_equals(c.address, null, 'address');
assert_equals(c.protocol, null, 'protocol');
assert_equals(c.port, null, 'port');
assert_equals(c.type, null, 'type');
assert_equals(c.tcpType, null, 'tcpType');
assert_equals(c.relatedAddress, null, 'relatedAddress');
assert_equals(c.relatedPort, null, 'relatedPort');
}
// ---- Positive parsing cases ----
test(t => {
const c = makeCandidate(
'candidate:1 1 udp 2113937151 2001:db8::1 58041 typ host');
assert_equals(c.address, '2001:db8::1', 'IPv6 compressed');
assert_equals(c.type, 'host');
}, 'IPv6 connection-address parses');
test(t => {
const c = makeCandidate(
'candidate:1 1 udp 2113937151 host.example.com 58041 typ host');
assert_equals(c.address, 'host.example.com', 'FQDN address');
assert_equals(c.type, 'host');
}, 'FQDN connection-address parses');
test(t => {
const c = makeCandidate(
'candidate:1 1 udp 2113937151 abcd1234.local 58041 typ host');
assert_equals(c.address, 'abcd1234.local', 'mDNS-style FQDN');
assert_equals(c.type, 'host');
}, 'mDNS-style .local connection-address parses');
['srflx', 'prflx', 'relay'].forEach(type => {
test(t => {
const c = makeCandidate(
`candidate:1 1 udp 100 1.2.3.4 5678 typ ${type} raddr 5.6.7.8 rport 9012`);
assert_equals(c.type, type, 'type');
assert_equals(c.relatedAddress, '5.6.7.8', 'relatedAddress');
assert_equals(c.relatedPort, 9012, 'relatedPort');
}, `${type} candidate with raddr/rport parses`);
});
['active', 'passive', 'so'].forEach(tcpType => {
test(t => {
const c = makeCandidate(
`candidate:1 1 tcp 100 1.2.3.4 5678 typ host tcptype ${tcpType}`);
assert_equals(c.protocol, 'tcp', 'protocol');
assert_equals(c.tcpType, tcpType, 'tcpType');
}, `TCP host candidate with tcptype ${tcpType} parses`);
});
test(t => {
const c = makeCandidate(
'candidate:1 1 udp 100 1.2.3.4 5678 typ host generation 0 ufrag thC8 network-cost 50');
assert_equals(c.type, 'host', 'type still parses');
assert_equals(c.foundation, '1', 'foundation');
}, 'trailing extension attributes are ignored');
// ---- Negative parsing cases ----
test(t => {
assertParseFailed(makeCandidate('foobar'));
}, '"foobar" fails to parse');
test(t => {
assertParseFailed(makeCandidate('candidate'));
}, 'bare "candidate" fails to parse');
test(t => {
assertParseFailed(makeCandidate('candidate:'));
}, 'bare "candidate:" fails to parse');
test(t => {
assertParseFailed(makeCandidate(
'candidate:1 1 udp 100 1.2.3.4 5678 host'));
}, 'missing "typ" keyword fails to parse');
test(t => {
assertParseFailed(makeCandidate('candidate:1 1 udp'));
}, 'truncated candidate (after transport) fails to parse');
test(t => {
assertParseFailed(makeCandidate('candidate:1 1 udp 100 1.2.3.4 5678'));
}, 'truncated candidate (before type) fails to parse');
test(t => {
assertParseFailed(makeCandidate(
'candidate:1 abc udp 100 1.2.3.4 5678 typ host'));
}, 'non-numeric component-id fails to parse');
test(t => {
assertParseFailed(makeCandidate(
'candidate:1 1 udp abc 1.2.3.4 5678 typ host'));
}, 'non-numeric priority fails to parse');
test(t => {
assertParseFailed(makeCandidate(
'candidate:1 1 udp 100 1.2.3.4 abc typ host'));
}, 'non-numeric port fails to parse');
test(t => {
assertParseFailed(makeCandidate(
'candidate:1 0 udp 100 1.2.3.4 5678 typ host'));
}, 'component-id of 0 fails to parse (prose requires >= 1)');
test(t => {
assertParseFailed(makeCandidate(
'candidate:1 257 udp 100 1.2.3.4 5678 typ host'));
}, 'component-id of 257 fails to parse (prose requires <= 256)');
['srflx', 'prflx', 'relay'].forEach(type => {
test(t => {
// Per RFC 8839, raddr/rport are required for non-host candidates.
assertParseFailed(makeCandidate(
`candidate:1 1 udp 100 1.2.3.4 5678 typ ${type}`));
}, `${type} candidate without raddr/rport fails to parse`);
});
test(t => {
assertParseFailed(makeCandidate(
'candidate:1 1 udp 100 1.2.3.4 5678 typ srflx raddr 5.6.7.8'));
}, 'raddr without rport fails to parse');
// ---- Case normalization on enum-typed fields ----
test(t => {
const c = makeCandidate(
'candidate:1 1 UDP 100 1.2.3.4 5678 typ HOST');
assert_equals(c.protocol, 'udp', 'protocol lowercased');
assert_equals(c.type, 'host', 'type lowercased');
assert_equals(c.component, 'rtp', 'component normalized');
}, 'enum-typed fields are lowercased regardless of input case');
test(t => {
const c = makeCandidate(
'candidate:1 1 TCP 100 1.2.3.4 5678 typ HOST tcptype ACTIVE');
assert_equals(c.protocol, 'tcp', 'protocol lowercased');
assert_equals(c.tcpType, 'active', 'tcpType lowercased');
}, 'tcpType is lowercased regardless of input case');
// ---- Numeric tokens reject trailing garbage ----
// RFC 8839's grammar uses 1*DIGIT for each of these fields; "100abc"
// is not a valid numeric token.
test(t => {
assertParseFailed(makeCandidate(
'candidate:1 1abc udp 100 1.2.3.4 5678 typ host'));
}, 'component with trailing non-digit fails to parse');
test(t => {
assertParseFailed(makeCandidate(
'candidate:1 1 udp 100abc 1.2.3.4 5678 typ host'));
}, 'priority with trailing non-digit fails to parse');
test(t => {
assertParseFailed(makeCandidate(
'candidate:1 1 udp 100 1.2.3.4 5678abc typ host'));
}, 'port with trailing non-digit fails to parse');
test(t => {
assertParseFailed(makeCandidate(
'candidate:1 1 udp 100 1.2.3.4 5678 typ srflx ' +
'raddr 5.6.7.8 rport 9012abc'));
}, 'rport with trailing non-digit fails to parse');
// ---- Priority out of range ----
test(t => {
assertParseFailed(makeCandidate(
'candidate:1 1 udp 0 1.2.3.4 5678 typ host'));
}, 'priority of 0 fails to parse (prose requires >= 1)');
test(t => {
// More than 10 digits; max uint32 is 10 digits.
assertParseFailed(makeCandidate(
'candidate:1 1 udp 99999999999 1.2.3.4 5678 typ host'));
}, 'priority with > 10 digits fails to parse');
test(t => {
// 10 digits but above 2^31-1 (2147483647) -- prose limits to 2^31-1.
assertParseFailed(makeCandidate(
'candidate:1 1 udp 2147483648 1.2.3.4 5678 typ host'));
}, 'priority above 2^31-1 fails to parse (prose limits to 2^31-1)');
// ---- Foundation: length and char-class ----
test(t => {
// 33 ice-chars; spec ice-char production limits to 32.
const foundation33 = 'a'.repeat(33);
assertParseFailed(makeCandidate(
`candidate:${foundation33} 1 udp 100 1.2.3.4 5678 typ host`));
}, 'foundation longer than 32 chars fails to parse');
test(t => {
// Single test cycling through chars that aren't ice-char
// (ice-char = ALPHA / DIGIT / "+" / "/").
const badChars = ['-', '_', '.', ':', '@', '#', '!', '(', ')', '*', '='];
for (const ch of badChars) {
const c = makeCandidate(
`candidate:foo${ch}bar 1 udp 100 1.2.3.4 5678 typ host`);
assert_equals(c.foundation, null, `foundation with '${ch}' fails`);
}
}, 'foundation with non-ice-char characters fails to parse');
// ---- Framing / whitespace ----
test(t => {
// The candidate-attribute production in RFC 8839 does not include "a=";
// that's added by the SDP framing.
assertParseFailed(makeCandidate(
'a=candidate:1 1 udp 100 1.2.3.4 5678 typ host'));
}, 'leading "a=" prefix fails to parse');
test(t => {
assertParseFailed(makeCandidate(
' candidate:1 1 udp 100 1.2.3.4 5678 typ host'));
}, 'leading whitespace fails to parse');
test(t => {
assertParseFailed(makeCandidate(
'candidate :1 1 udp 100 1.2.3.4 5678 typ host'));
}, 'whitespace before ":" in candidate keyword fails to parse');
test(t => {
assertParseFailed(makeCandidate(
'candidate: 1 1 udp 100 1.2.3.4 5678 typ host'));
}, 'whitespace after ":" before foundation fails to parse');
// ---- TCP candidate failures ----
test(t => {
assertParseFailed(makeCandidate(
'candidate:1 1 tcp 100 1.2.3.4 5678 typ host'));
}, 'TCP non-relay candidate without tcptype fails to parse');
test(t => {
assertParseFailed(makeCandidate(
'candidate:1 1 tcp 100 1.2.3.4 5678 typ host tcptype'));
}, 'TCP candidate with tcptype keyword but no value fails to parse');
test(t => {
assertParseFailed(makeCandidate(
'candidate:1 1 tcp 100 1.2.3.4 5678 typ host tcptype connect'));
}, 'TCP candidate with unknown tcptype value fails to parse');
test(t => {
// RFC 6544 BNF places tcp-type-ext before generic extensions, so a
// TCP candidate with another extension before "tcptype" doesn't conform.
// This might be a spec bug.
assertParseFailed(makeCandidate(
'candidate:1 1 tcp 100 1.2.3.4 5678 typ host generation 0 tcptype active'));
}, 'TCP candidate with extension before tcptype fails to parse');
</script>