blob: f9bb78f7555e0cd1f3685cbe61414e0aa0e2cb2d [file] [log] [blame]
'use strict';
// Buffered exceptions re-thrown at end of suite
let savedExceptions = [];
// Observer-based document.cookie simulator
let observer;
let observationLog = [];
let observedStore = [];
// Note on cookie naming conventions:
//
// A simple origin cookie is a cookie named with the __Host- prefix
// which is always secure-flagged, always implicit-domain, always
// /-scoped, and hence always unambiguous in the cookie jar serialization
// and origin-scoped. It can be treated as a simple key/value pair.
//
// "LEGACY" in a cookie name here means it is an old-style unprefixed
// cookie name, so you can't tell e.g. whether it is Secure-flagged or
// /-pathed just by looking at it, and its flags, domain and path may
// vary even in a single cookie jar serialization leading to apparent
// duplicate entries, ambiguities, and complexity (it cannot be
// treated as a simple key/value pair.)
//
// Cookie names used in the tests are intended to be
// realistic. Traditional session cookie names are typically
// all-upper-case for broad framework compatibility. The more modern
// "__Host-" prefix has only one allowed casing. An expected upgrade
// path from traditional "legacy" cookie names to simple origin cookie
// names is simply to prefix the traditional name with the "__Host-"
// prefix.
//
// Many of the used cookie names are non-ASCII to ensure
// straightforward internationalization is possible at every API surface.
// These work in many modern browsers, though not yet all of them.
// Approximate async observer-based equivalent to the document.cookie
// getter but with important differences: an empty cookie jar returns
// undefined. Introduces unfortunate but apparently unavoidable delays
// to ensure the observer has time to run.
//
// Timeouts here are intended to give observers enough time to sense
// a change. It can't be changed to wait indefinitely as it is
// sometimes used to verify observers were not notified of any
// change.
const getCookieStringObserved = opt_name => {
// Run later to ensure the cookie scanner (which runs one task
// later, at least in the polyfill) has a chance.
//
// We cannot use the s\u0065tTimeout identifier unescaped inside WPT
// tests (the linter does not allow it.) However we need an actual
// delay to allow batched observers to fire.
const initialLength = observationLog.length;
return (async () => {
assert_not_equals(observer, undefined, 'observer should not be undefined');
await new Promise(resolve => s\u0065tTimeout(resolve));
const lengthAfterImplicit0msSetTimeout = observationLog.length;
if (lengthAfterImplicit0msSetTimeout === initialLength) {
await new Promise(resolve => s\u0065tTimeout(resolve, 4));
const lengthAfter4msSetTimeout = observationLog.length;
if (lengthAfter4msSetTimeout === initialLength) {
let lengthAfterRequestAnimationFrame = lengthAfter4msSetTimeout;
if (typeof requestAnimationFrame !== 'undefined') {
await new Promise(resolve => requestAnimationFrame(resolve));
lengthAfterRequestAnimationFrame = observationLog.length;
}
if (lengthAfterRequestAnimationFrame === initialLength) {
await new Promise(
resolve => s\u0065tTimeout(resolve, kExtraObserverDelay));
}
}
}
let filtered = observedStore;
if (opt_name != null) filtered = filtered.filter(
cookie => cookie.name === opt_name);
return cookieString(filtered);
})();
};
const assertEmptyCookieJar = async (testCase, messageSuffix) => {
assert_equals(
await getCookieString(),
undefined,
'No cookies ' + messageSuffix);
if (!kIsStatic) assert_equals(
await getCookieStringHttp(),
undefined,
'No HTTP cookies ' + messageSuffix);
if (kHasDocument) assert_equals(
await getCookieStringDocument(),
undefined,
'No document.cookie cookies ' + messageSuffix);
};
const suite = ({testName = undefined} = {}) => {
promise_test(async testCase => {
testOverride = testName;
observer = undefined;
observationLog.length = 0;
observedStore.length = 0;
savedExceptions.length = 0;
// Start with a clean slate.
//
// Attempt testDeleteCookies first too, since otherwise an earlier
// failed test can cause all subsequent tests to fail.
await testDeleteCookies(testCase);
await assertEmptyCookieJar(testCase, 'at start of test');
let unfinished = true;
try {
if (includeTest('testObservation')) {
observer = await testObservation();
assert_equals(
await getCookieStringObserved(),
undefined,
'No observed cookies at start of test');
}
// These use the same cookie names and so cannot run interleaved
if (includeTest('testNoNameAndNoValue')) await testNoNameAndNoValue();
if (includeTest('testNoNameMultipleValues')) {
await testNoNameMultipleValues();
}
if (includeTest('testNoNameEqualsInValue')) {
await testNoNameEqualsInValue();
}
if (includeTest('testMetaHttpEquivSetCookie')) {
await testMetaHttpEquivSetCookie();
}
if (includeTest('testDocumentCookie', !kHasDocument)) {
await testDocumentCookie();
}
if (includeTest('testHttpCookieAndSetCookieHeaders', kIsStatic)) {
await testHttpCookieAndSetCookieHeaders();
}
if (includeTest('testGetSetGetAll')) {
await testGetSetGetAll();
}
if (includeTest('testOneSimpleOriginCookie')) {
await testOneSimpleOriginCookie(testCase);
}
if (includeTest('testExpiration')) {
await testExpiration(testCase);
}
await promise_rejects_when_unsecured(
testCase,
new SyntaxError(),
testThreeSimpleOriginSessionCookiesSetSequentially(),
'__Host- cookies only writable from secure contexts' +
' (testThreeSimpleOriginSessionCookiesSetSequentially)');
await promise_rejects_when_unsecured(
testCase,
new SyntaxError(),
testThreeSimpleOriginSessionCookiesSetNonsequentially(),
'__Host- cookies only writable from secure contexts' +
' (testThreeSimpleOriginSessionCookiesSetNonsequentially)');
await promise_rejects_when_unsecured(
testCase,
new SyntaxError(),
setExpiredSecureCookieWithDomainPathAndFallbackValue(),
'Secure cookies only writable from secure contexts' +
' (setExpiredSecureCookieWithDomainPathAndFallbackValue)');
await promise_rejects_when_unsecured(
testCase,
new SyntaxError(),
deleteSimpleOriginCookie(),
'__Host- cookies only writable from secure contexts' +
' (deleteSimpleOriginCookie)');
await promise_rejects_when_unsecured(
testCase,
new SyntaxError(),
deleteSecureCookieWithDomainAndPath(),
'Secure cookies only writable from secure contexts' +
' (deleteSecureCookieWithDomainAndPath)');
if (kIsUnsecured) {
assert_equals(
await getCookieString(),
includeTest('testGetSetGetAll') ? 'TEST=value' : undefined,
(includeTest('testGetSetGetAll') ?
'Only one unsecured cookie' :
'No unsecured cookies') +
' before testDeleteCookies at end of test');
if (observer) assert_equals(
await getCookieStringObserved(),
includeTest('testGetSetGetAll') ? 'TEST=value' : undefined,
(includeTest('testGetSetGetAll') ?
'Only one observed unsecured cookie' :
'No observed unsecured cookies') +
' before testDeleteCookies at end of test');
} else {
assert_equals(
await getCookieString(),
(includeTest('testGetSetGetAll') ? 'TEST=value; ' : '') +
'__Host-1🍪=🔵cookie-value1🔴; ' +
'__Host-2🌟=🌠cookie-value2🌠; ' +
'__Host-3🌱=🔶cookie-value3🔷; ' +
'__Host-unordered1🍪=🔵unordered-cookie-value1🔴; ' +
'__Host-unordered2🌟=🌠unordered-cookie-value2🌠; ' +
'__Host-unordered3🌱=🔶unordered-cookie-value3🔷',
'All residual cookies before testDeleteCookies at end of test');
if (observer) assert_equals(
await getCookieStringObserved(),
(includeTest('testGetSetGetAll') ? 'TEST=value; ' : '') +
'__Host-1🍪=🔵cookie-value1🔴; ' +
'__Host-2🌟=🌠cookie-value2🌠; ' +
'__Host-3🌱=🔶cookie-value3🔷; ' +
'__Host-unordered1🍪=🔵unordered-cookie-value1🔴; ' +
'__Host-unordered2🌟=🌠unordered-cookie-value2🌠; ' +
'__Host-unordered3🌱=🔶unordered-cookie-value3🔷',
'All residual observed cookies before testDeleteCookies ' +
'at end of test');
}
if (kIsUnsecured) {
if (!kIsStatic) assert_equals(
await getCookieStringHttp(),
includeTest('testGetSetGetAll') ? 'TEST=value' : undefined,
(includeTest('testGetSetGetAll') ?
'Only one unsecured HTTP cookie' :
'No unsecured HTTP cookies') +
' before testDeleteCookies at end of test');
} else {
if (!kIsStatic) assert_equals(
await getCookieStringHttp(),
(includeTest('testGetSetGetAll') ? 'TEST=value; ' : '') +
'__Host-1🍪=🔵cookie-value1🔴; ' +
'__Host-2🌟=🌠cookie-value2🌠; ' +
'__Host-3🌱=🔶cookie-value3🔷; ' +
'__Host-unordered1🍪=🔵unordered-cookie-value1🔴; ' +
'__Host-unordered2🌟=🌠unordered-cookie-value2🌠; ' +
'__Host-unordered3🌱=🔶unordered-cookie-value3🔷',
'All residual HTTP cookies before testDeleteCookies ' +
'at end of test');
}
if (kIsUnsecured) {
if (kHasDocument) assert_equals(
await getCookieStringDocument(),
includeTest('testGetSetGetAll') ? 'TEST=value' : undefined,
(includeTest('testGetSetGetAll') ?
'Only one unsecured document.cookie cookie' :
'No unsecured document.cookie cookies') +
' before testDeleteCookies at end of test');
} else {
if (kHasDocument) assert_equals(
await getCookieStringDocument(),
(includeTest('testGetSetGetAll') ? 'TEST=value; ' : '') +
'__Host-1🍪=🔵cookie-value1🔴; ' +
'__Host-2🌟=🌠cookie-value2🌠; ' +
'__Host-3🌱=🔶cookie-value3🔷; ' +
'__Host-unordered1🍪=🔵unordered-cookie-value1🔴; ' +
'__Host-unordered2🌟=🌠unordered-cookie-value2🌠; ' +
'__Host-unordered3🌱=🔶unordered-cookie-value3🔷',
'All residual document.cookie cookies before testDeleteCookies ' +
'at end of test');
}
unfinished = false;
assert_equals(
savedExceptions.length,
0,
'Found saved exceptions: ' + savedExceptions);
} finally {
try {
await testDeleteCookies(testCase);
if (observer) observer.disconnect();
await assertEmptyCookieJar(testCase, 'at end of test');
} catch (e) {
// only re-throw testDeleteCookies failures if finished to avoid masking
// earlier failures
if (!unfinished) throw e;
}
}
}, 'Cookie Store Tests (' + (testName || 'all') + ')');
};
// Try to clean up cookies and observers used by tests. Also
// verifies delete() behavior for secure contexts and unsecured
// contexts.
//
// Parameters:
// - testCase: (TestCase) Context in which the testDeleteCookies is run.
const testDeleteCookies = async testCase => {
let exceptions = [];
for (let resetStep of [
async () => await cookieStore.delete(''),
async () => await cookieStore.delete('TEST'),
async () => await cookieStore.delete('META-🍪'),
async () => await cookieStore.delete('DOCUMENT-🍪'),
async () => await cookieStore.delete('HTTP-🍪'),
async () => {
if (!kIsStatic) await setCookieStringHttp(
'HTTPONLY-🍪=DELETED; path=/; max-age=0; httponly');
},
async () => await promise_rejects_when_unsecured(
testCase,
new SyntaxError(),
cookieStore.delete('__Host-COOKIENAME')),
async () => await promise_rejects_when_unsecured(
testCase,
new SyntaxError(),
cookieStore.delete('__Host-1🍪')),
async () => await promise_rejects_when_unsecured(
testCase,
new SyntaxError(),
cookieStore.delete('__Host-2🌟')),
async () => await promise_rejects_when_unsecured(
testCase,
new SyntaxError(),
cookieStore.delete('__Host-3🌱')),
async () => await promise_rejects_when_unsecured(
testCase,
new SyntaxError(),
cookieStore.delete('__Host-unordered1🍪')),
async () => await promise_rejects_when_unsecured(
testCase,
new SyntaxError(),
cookieStore.delete('__Host-unordered2🌟')),
async () => await promise_rejects_when_unsecured(
testCase,
new SyntaxError(),
cookieStore.delete('__Host-unordered3🌱')),
]) {
try {
await resetStep();
} catch (x) {
exceptions.push(x);
};
}
assert_equals(
exceptions.length,
0,
'testDeleteCookies failures: ' + exceptions);
};
// Helper to verify first-of-name get using async/await.
//
// Returns the first script-visible value of the __Host-COOKIENAME cookie or
// undefined if no matching cookies are script-visible.
let getOneSimpleOriginCookie = async () => {
let cookie = await cookieStore.get('__Host-COOKIENAME');
if (!cookie) return undefined;
return cookie.value;
};
// Returns the number of script-visible cookies whose names start with
// __Host-COOKIEN
let countMatchingSimpleOriginCookies = async () => {
let cookieList = await cookieStore.getAll({
name: '__Host-COOKIEN',
matchType: 'startsWith'
});
return cookieList.length;
};
// Set the secure implicit-domain cookie __Host-COOKIENAME with value
// cookie-value on path / and session duration.
let setOneSimpleOriginSessionCookie = async () => {
await cookieStore.set('__Host-COOKIENAME', 'cookie-value');
};
// Set the secure example.org-domain cookie __Secure-COOKIENAME with
// value cookie-value on path /cgi-bin/ and 24 hour duration; domain
// and path will be rewritten below.
//
// This uses a Date object for expiration.
let setOneDaySecureCookieWithDate = async () => {
// one day ahead, ignoring a possible leap-second
let inTwentyFourHours = new Date(Date.now() + 24 * 60 * 60 * 1000);
await cookieStore.set('__Secure-COOKIENAME', 'cookie-value', {
path: '/cgi-bin/',
expires: inTwentyFourHours,
secure: true,
domain: 'example.org'
});
};
// Set the unsecured example.org-domain cookie LEGACYCOOKIENAME with
// value cookie-value on path /cgi-bin/ and 24 hour duration; domain
// and path will be rewritten below.
//
// This uses milliseconds since the start of the Unix epoch for
// expiration.
let setOneDayUnsecuredCookieWithMillisecondsSinceEpoch = async () => {
// one day ahead, ignoring a possible leap-second
let inTwentyFourHours = Date.now() + 24 * 60 * 60 * 1000;
await cookieStore.set('LEGACYCOOKIENAME', 'cookie-value', {
path: '/cgi-bin/',
expires: inTwentyFourHours,
secure: false,
domain: 'example.org'
});
};
// Delete the cookie written by
// setOneDayUnsecuredCookieWithMillisecondsSinceEpoch.
let deleteUnsecuredCookieWithDomainAndPath = async () => {
await cookieStore.delete('LEGACYCOOKIENAME', {
path: '/cgi-bin/',
secure: false,
domain: 'example.org'
});
};
// Set the secured example.org-domain cookie __Secure-COOKIENAME with
// value cookie-value on path /cgi-bin/ and expiration in June of next
// year; domain and path will be rewritten below.
//
// This uses an HTTP-style date string for expiration.
let setSecureCookieWithHttpLikeExpirationString = async () => {
const year = (new Date()).getUTCFullYear() + 1;
const date = new Date('07 Jun ' + year + ' 07:07:07 UTC');
const day = ('Sun Mon Tue Wed Thu Fri Sat'.split(' '))[date.getUTCDay()];
await cookieStore.set('__Secure-COOKIENAME', 'cookie-value', {
path: '/cgi-bin/',
expires: day + ', 07 Jun ' + year + ' 07:07:07 GMT',
secure: true,
domain: 'example.org'
});
};
// Set three simple origin session cookies sequentially and ensure
// they all end up in the cookie jar in order.
let testThreeSimpleOriginSessionCookiesSetSequentially = async () => {
await cookieStore.set('__Host-1🍪', '🔵cookie-value1🔴');
await cookieStore.set('__Host-2🌟', '🌠cookie-value2🌠');
await cookieStore.set('__Host-3🌱', '🔶cookie-value3🔷');
// NOTE: this assumes no concurrent writes from elsewhere; it also
// uses three separate cookie jar read operations where a single getAll
// would be more efficient, but this way the CookieStore does the filtering
// for us.
let matchingValues = await Promise.all([ '1🍪', '2🌟', '3🌱' ].map(
async suffix => (await cookieStore.get('__Host-' + suffix)).value));
let actual = matchingValues.join(';');
let expected = '🔵cookie-value1🔴;🌠cookie-value2🌠;🔶cookie-value3🔷';
if (actual !== expected) throw new Error(
'Expected ' + JSON.stringify(expected) +
' but got ' + JSON.stringify(actual));
};
// Set three simple origin session cookies in undefined order using
// Promise.all and ensure they all end up in the cookie jar in any
// order.
let testThreeSimpleOriginSessionCookiesSetNonsequentially = async () => {
await Promise.all([
cookieStore.set('__Host-unordered1🍪', '🔵unordered-cookie-value1🔴'),
cookieStore.set('__Host-unordered2🌟', '🌠unordered-cookie-value2🌠'),
cookieStore.set('__Host-unordered3🌱', '🔶unordered-cookie-value3🔷')
]);
// NOTE: this assumes no concurrent writes from elsewhere; it also
// uses three separate cookie jar read operations where a single getAll
// would be more efficient, but this way the CookieStore does the filtering
// for us and we do not need to sort.
let matchingCookies = await Promise.all([ '1🍪', '2🌟', '3🌱' ].map(
suffix => cookieStore.get('__Host-unordered' + suffix)));
let actual = matchingCookies.map(({ value }) => value).join(';');
let expected =
'🔵unordered-cookie-value1🔴;' +
'🌠unordered-cookie-value2🌠;' +
'🔶unordered-cookie-value3🔷';
if (actual !== expected) throw new Error(
'Expected ' + JSON.stringify(expected) +
' but got ' + JSON.stringify(actual));
};
// Set an already-expired cookie.
let setExpiredSecureCookieWithDomainPathAndFallbackValue = async () => {
let theVeryRecentPast = Date.now();
let expiredCookieSentinelValue = 'EXPIRED';
await cookieStore.set('__Secure-COOKIENAME', expiredCookieSentinelValue, {
path: '/cgi-bin/',
expires: theVeryRecentPast,
secure: true,
domain: 'example.org'
});
};
// Delete the __Host-COOKIENAME cookie created above.
let deleteSimpleOriginCookie = async () => {
await cookieStore.delete('__Host-COOKIENAME');
};
// Delete the __Secure-COOKIENAME cookie created above.
let deleteSecureCookieWithDomainAndPath = async () => {
await cookieStore.delete('__Secure-COOKIENAME', {
path: '/cgi-bin/',
domain: 'example.org',
secure: true
});
};
// Test for CookieObserver. Used in implementation of async observer-based
// document.cookie simulator. This is passed to the Promise constructor after
// rewriting.
let testObservation_ = (resolve, reject) => {
// This will get invoked (asynchronously) shortly after the
// observe(...) call to provide an initial snapshot; in that case
// the length of cookieChanges may be 0, indicating no matching
// script-visible cookies for any URL+cookieStore currently
// observed. The CookieObserver instance is passed as the second
// parameter to allow additional calls to observe or disconnect.
let callback = (cookieChanges, observer) => {
var logEntry = [];
observationLog.push(logEntry);
const cookieChangesStrings = changes => changes.map(
({type, name, value, index}) => cookieString(Object.assign(
new Array(observedStore.length),
{[index]: {
name: ((type === 'visible') ? '+' : '-') + name,
value: value
}})));
logEntry.push(['before', cookieString(observedStore)]);
logEntry.push(['changes', cookieChangesStrings(cookieChanges)]);
const newObservedStore = observedStore.slice(0);
try {
const insertions = [], deletions = [];
cookieChanges.forEach(({
cookieStore,
type,
url,
name,
value,
index,
all
}) => {
switch (type) {
case 'visible':
// Creation or modification (e.g. change in value, or
// removal of HttpOnly), or appearance to script due to
// change in policy or permissions
insertions.push([index, {name: name, value: value}]);
break;
case 'hidden':
// Deletion/expiration or disappearance (e.g. due to
// modification adding HttpOnly), or disappearance from
// script due to change in policy or permissions
assert_object_equals(
{name: name, value: value},
observedStore[index],
'Hidden cookie at index ' + index +
' was not the expected one: ' + JSON.stringify({
got: {name: name, value: value},
expected: observedStore[index]
}));
deletions.push(index);
break;
default:
savedExceptions.push('Unexpected CookieChange type ' + type);
if (reject) reject(savedExceptions[savedExceptions.length - 1]);
throw savedExceptions[savedExceptions.length - 1];
}
});
deletions.sort((a, b) => b - a).forEach(
index => newObservedStore.splice(index, 1));
let bias = 0;
insertions.sort(([a], [b]) => a - b).forEach(([ index, cookie ]) => {
if (newObservedStore[index + bias] !== undefined) {
newObservedStore.splice(index, 0, cookie);
--bias;
} else {
newObservedStore[index] = cookie;
}
});
observedStore = newObservedStore.filter(entry => entry !== undefined);
logEntry.push(['after', cookieString(observedStore)]);
const reported =
cookieChanges && cookieChanges.length ?
cookieChanges[cookieChanges.length - 1].all :
[];
assert_equals(
cookieString(reported),
cookieString(observedStore),
'Mismatch between observed store and reported store.' +
'\n observed:\n ' + cookieString(observedStore) +
'\n reported:\n ' + cookieString(reported) +
'\n log:\n ' + observationLog.map(JSON.stringify).join('\n '));
} catch (e) {
logEntry.push([' *** ⚠ *** ERROR: EXCEPTION THROWN *** ⚠ *** ']);
savedExceptions.push('Exception in observer');
savedExceptions.push(e);
if (reject) reject(e);
throw e;
}
// Resolve promise after first callback
if (resolve) resolve(observer);
resolve = null;
reject = null;
};
CookieObserver.startTimer_ = (handler, ignoredDelay) => {
var timer = {shouldRun: true, fingerPrint: Math.random()};
new Promise(resolve => s\u0065tTimeout(resolve)).then(() => {
if (!timer.shouldRun) return;
CookieObserver.stopTimer_(timer);
handler();
});
return timer;
};
CookieObserver.stopTimer_ = timer => {
timer.shouldRun = false;
};
let observer = new CookieObserver(callback);
// If null or omitted this defaults to location.pathname up to and
// including the final '/' in a document context, or worker scope up
// to and including the final '/' in a service worker context.
let url = (location.pathname).replace(/[^\/]+$/, '');
// If null or omitted this defaults to interest in all
// script-visible cookies.
let interests = [
// Interested in all secure cookies named '__Secure-COOKIENAME';
// the default matchType is 'equals' at the given URL.
{ name: '__Secure-COOKIENAME', url: url },
// Interested in all simple origin cookies named like
// /^__Host-COOKIEN.*$/ at the default URL.
{ name: '__Host-COOKIEN', matchType: 'startsWith' },
// Interested in all simple origin cookies named '__Host-1🍪'
// at the default URL.
{ name: '__Host-1🍪' },
// Interested in all cookies named 'OLDCOOKIENAME' at the given URL.
{ name: 'OLDCOOKIENAME', matchType: 'equals', url: url },
// Interested in all simple origin cookies named like
// /^__Host-AUTHTOKEN.*$/ at the given URL.
{ name: '__Host-AUTHTOKEN', matchType: 'startsWith', url: url + 'auth/' }
];
observer.observe(cookieStore, interests);
// Default interest: all script-visible changes, default URL
observer.observe(cookieStore);
};
// Rewrite testObservation_ to use a path we are allowed to see from a
// document context.
//
// FIXME: remove this once ServiceWorker support is implemented and
// path observation can actually be verified at a sub-path.
if (kHasDocument) {
testObservation_ = eval(String(testObservation_).split('auth/').join('auth'));
}
// Wrap testObservation_ to work as a promise.
const testObservation = () => new Promise(testObservation_);
// Verify behavior of no-name and no-value cookies.
let testNoNameAndNoValue = async () => {
await cookieStore.set('', 'first-value');
let actual1 =
(await cookieStore.getAll('')).map(({ value }) => value).join(';');
let expected1 = 'first-value';
if (actual1 !== expected1) throw new Error(
'Expected ' + JSON.stringify(expected1) +
' but got ' + JSON.stringify(actual1));
await cookieStore.set('', '');
let actual2 =
(await cookieStore.getAll('')).map(({ value }) => value).join(';');
let expected2 = '';
if (actual2 !== expected2) throw new Error(
'Expected ' + JSON.stringify(expected) +
' but got ' + JSON.stringify(actual));
await cookieStore.delete('');
assert_equals(
await getCookieString(),
undefined,
'Empty cookie jar after testNoNameAndNoValue');
if (!kIsStatic) assert_equals(
await getCookieStringHttp(),
undefined,
'Empty HTTP cookie jar after testNoNameAndNoValue');
if (kHasDocument) assert_equals(
await getCookieStringDocument(),
undefined,
'Empty document.cookie cookie jar after testNoNameAndNoValue');
if (observer) assert_equals(
await getCookieStringObserved(),
undefined,
'Empty observed cookie jar after testNoNameAndNoValue');
};
// Verify behavior of multiple no-name cookies.
let testNoNameMultipleValues = async () => {
await cookieStore.set('', 'first-value');
let actual1 =
(await cookieStore.getAll('')).map(({ value }) => value).join(';');
let expected1 = 'first-value';
if (actual1 !== expected1) throw new Error(
'Expected ' + JSON.stringify(expected1) +
' but got ' + JSON.stringify(actual1));
await cookieStore.set('', 'second-value');
let actual2 =
(await cookieStore.getAll('')).map(({ value }) => value).join(';');
let expected2 = 'second-value';
if (actual2 !== expected2) throw new Error(
'Expected ' + JSON.stringify(expected2) +
' but got ' + JSON.stringify(actual2));
await cookieStore.delete('');
assert_equals(
await getCookieString(),
undefined,
'Empty cookie jar after testNoNameMultipleValues');
if (!kIsStatic) assert_equals(
await getCookieStringHttp(),
undefined,
'Empty HTTP cookie jar after testNoNameMultipleValues');
if (observer) assert_equals(
await getCookieStringObserved(),
undefined,
'Empty observed cookie jar after testNoNameMultipleValues');
};
// Verify that attempting to set a cookie with no name and with '=' in
// the value does not work.
let testNoNameEqualsInValue = async () => {
await cookieStore.set('', 'first-value');
let actual1 =
(await cookieStore.getAll('')).map(({ value }) => value).join(';');
let expected1 = 'first-value';
if (actual1 !== expected1) throw new Error(
'Expected ' + JSON.stringify(expected1) +
' but got ' + JSON.stringify(actual1));
try {
await cookieStore.set('', 'suspicious-value=resembles-name-and-value');
} catch (expectedError) {
let actual2 =
(await cookieStore.getAll('')).map(({ value }) => value).join(';');
let expected2 = 'first-value';
if (actual2 !== expected2) throw new Error(
'Expected ' + JSON.stringify(expected2) +
' but got ' + JSON.stringify(actual2));
assert_equals(
await getCookieString(),
'first-value',
'Earlier cookie jar after rejected part of testNoNameEqualsInValue');
await cookieStore.delete('');
assert_equals(
await getCookieString(),
undefined,
'Empty cookie jar after cleanup in testNoNameEqualsInValue');
if (!kIsStatic) assert_equals(
await getCookieStringHttp(),
undefined,
'Empty HTTP cookie jar after cleanup in testNoNameEqualsInValue');
if (observer) assert_equals(
await getCookieStringObserved(),
undefined,
'Empty observed cookie jar after cleanup in testNoNameEqualsInValue');
return;
}
throw new Error(
'Expected promise rejection' +
' when setting a cookie with no name and "=" in value');
};
// https://github.com/whatwg/html/issues/3076#issuecomment-332920132
// proposes to remove <meta http-equiv="set-cookie" ... > but it is
// not yet an accepted part of the HTML spec.
//
// Until the feature is gone, it interacts with other cookie APIs,
// including this one.
//
// When kMetaHttpEquivSetCookieIsGone is set, verify that <meta
// http-equiv="set-cookie" ... > no longer works. Otherwise, verify
// its interoperability with other APIs.
let testMetaHttpEquivSetCookie = async () => {
await setCookieStringMeta('META-🍪=🔵; path=/');
if (kMetaHttpEquivSetCookieIsGone) {
assert_equals(
await getCookieString(),
undefined,
'Empty cookie jar after no-longer-supported' +
' <meta http-equiv="set-cookie" ... >');
if (!kIsStatic) assert_equals(
await getCookieStringHttp(),
undefined,
'Empty HTTP cookie jar after no-longer-supported' +
' <meta http-equiv="set-cookie" ... >');
if (observer) assert_equals(
await getCookieStringObserved(),
undefined,
'Empty observed cookie jar after no-longer-supported' +
' <meta http-equiv="set-cookie" ... >');
} else {
assert_equals(
await getCookieString(),
'META-🍪=🔵',
'Cookie we wrote using' +
' <meta http-equiv="set-cookie" ... > in cookie jar');
if (!kIsStatic) assert_equals(
await getCookieStringHttp(),
'META-🍪=🔵',
'Cookie we wrote using' +
' <meta http-equiv="set-cookie" ... > in HTTP cookie jar');
if (observer) assert_equals(
await getCookieStringObserved(),
'META-🍪=🔵',
'Cookie we wrote using' +
' <meta http-equiv="set-cookie" ... > in observed cookie jar');
await setCookieStringMeta('META-🍪=DELETED; path=/; max-age=0');
assert_equals(
await getCookieString(),
undefined,
'Empty cookie jar after <meta http-equiv="set-cookie" ... >' +
' cookie-clearing using max-age=0');
if (!kIsStatic) assert_equals(
await getCookieStringHttp(),
undefined,
'Empty HTTP cookie jar after <meta http-equiv="set-cookie" ... >' +
' cookie-clearing using max-age=0');
if (observer) assert_equals(
await getCookieStringObserved(),
undefined,
'Empty observed cookie jar after <meta http-equiv="set-cookie" ... >' +
' cookie-clearing using max-age=0');
}
};
// Verify interoperability of document.cookie with other APIs.
let testDocumentCookie = async () => {
await setCookieStringDocument('DOCUMENT-🍪=🔵; path=/');
assert_equals(
await getCookieString(),
'DOCUMENT-🍪=🔵',
'Cookie we wrote using document.cookie in cookie jar');
if (!kIsStatic) assert_equals(
await getCookieStringHttp(),
'DOCUMENT-🍪=🔵',
'Cookie we wrote using document.cookie in HTTP cookie jar');
assert_equals(
await getCookieStringDocument(),
'DOCUMENT-🍪=🔵',
'Cookie we wrote using document.cookie in document.cookie');
if (observer) assert_equals(
await getCookieStringObserved(),
'DOCUMENT-🍪=🔵',
'Cookie we wrote using document.cookie in observed cookie jar');
await setCookieStringDocument('DOCUMENT-🍪=DELETED; path=/; max-age=0');
assert_equals(
await getCookieString(),
undefined,
'Empty cookie jar after document.cookie' +
' cookie-clearing using max-age=0');
if (!kIsStatic) assert_equals(
await getCookieStringHttp(),
undefined,
'Empty HTTP cookie jar after document.cookie' +
' cookie-clearing using max-age=0');
assert_equals(
await getCookieStringDocument(),
undefined,
'Empty document.cookie cookie jar after document.cookie' +
' cookie-clearing using max-age=0');
if (observer) assert_equals(
await getCookieStringObserved(),
undefined,
'Empty observed cookie jar after document.cookie cookie-clearing' +
' using max-age=0');
};
// Verify interoperability of HTTP Set-Cookie: with other APIs.
let testHttpCookieAndSetCookieHeaders = async () => {
await setCookieStringHttp('HTTP-🍪=🔵; path=/');
assert_equals(
await getCookieString(),
'HTTP-🍪=🔵',
'Cookie we wrote using HTTP in cookie jar');
assert_equals(
await getCookieStringHttp(),
'HTTP-🍪=🔵',
'Cookie we wrote using HTTP in HTTP cookie jar');
if (observer) assert_equals(
await getCookieStringObserved(),
'HTTP-🍪=🔵',
'Cookie we wrote using HTTP in observed cookie jar');
await setCookieStringHttp('HTTP-🍪=DELETED; path=/; max-age=0');
assert_equals(
await getCookieString(),
undefined,
'Empty cookie jar after HTTP cookie-clearing using max-age=0');
assert_equals(
await getCookieStringHttp(),
undefined,
'Empty HTTP cookie jar after HTTP cookie-clearing using max-age=0');
if (observer) assert_equals(
await getCookieStringObserved(),
undefined,
'Empty observed cookie jar after HTTP cookie-clearing' +
' using max-age=0');
await setCookieStringHttp('HTTPONLY-🍪=🔵; path=/; httponly');
assert_equals(
await getCookieString(),
undefined,
'HttpOnly cookie we wrote using HTTP in cookie jar' +
' is invisible to script');
assert_equals(
await getCookieStringHttp(),
'HTTPONLY-🍪=🔵',
'HttpOnly cookie we wrote using HTTP in HTTP cookie jar');
if (observer) assert_equals(
await getCookieStringObserved(),
undefined,
'HttpOnly cookie we wrote using HTTP is invisible to observer');
await setCookieStringHttp(
'HTTPONLY-🍪=DELETED; path=/; max-age=0; httponly');
assert_equals(
await getCookieString(),
undefined,
'Empty cookie jar after HTTP cookie-clearing using max-age=0');
assert_equals(
await getCookieStringHttp(),
undefined,
'Empty HTTP cookie jar after HTTP cookie-clearing using max-age=0');
if (observer) assert_equals(
await getCookieStringObserved(),
undefined,
'Empty observed cookie jar after HTTP cookie-clearing' +
' using max-age=0');
// Non-UTF-8 byte sequences cause the Set-Cookie to be dropped.
await setCookieBinaryHttp(
unescape(encodeURIComponent('HTTP-🍪=🔵')) + '\xef\xbf\xbd; path=/');
assert_equals(
await getCookieString(),
'HTTP-🍪=🔵\ufffd',
'Binary cookie we wrote using HTTP in cookie jar');
assert_equals(
await getCookieStringHttp(),
'HTTP-🍪=🔵\ufffd',
'Binary cookie we wrote using HTTP in HTTP cookie jar');
assert_equals(
decodeURIComponent(escape(await getCookieBinaryHttp())),
'HTTP-🍪=🔵\ufffd',
'Binary cookie we wrote in binary HTTP cookie jar');
assert_equals(
await getCookieBinaryHttp(),
unescape(encodeURIComponent('HTTP-🍪=🔵')) + '\xef\xbf\xbd',
'Binary cookie we wrote in binary HTTP cookie jar');
if (observer) assert_equals(
await getCookieStringObserved(),
'HTTP-🍪=🔵\ufffd',
'Binary cookie we wrote using HTTP in observed cookie jar');
await setCookieBinaryHttp(
unescape(encodeURIComponent('HTTP-🍪=DELETED; path=/; max-age=0')));
assert_equals(
await getCookieString(),
undefined,
'Empty cookie jar after binary HTTP cookie-clearing using max-age=0');
assert_equals(
await getCookieStringHttp(),
undefined,
'Empty HTTP cookie jar after' +
' binary HTTP cookie-clearing using max-age=0');
assert_equals(
await getCookieBinaryHttp(),
undefined,
'Empty binary HTTP cookie jar after' +
' binary HTTP cookie-clearing using max-age=0');
if (observer) assert_equals(
await getCookieStringObserved(),
undefined,
'Empty observed cookie jar after binary HTTP cookie-clearing' +
' using max-age=0');
};
const testGetSetGetAll = async () => {
await cookieStore.set('TEST', 'value0');
assert_equals(
await getCookieString(),
'TEST=value0',
'Cookie jar contains only cookie we set');
if (!kIsStatic) assert_equals(
await getCookieStringHttp(),
'TEST=value0',
'HTTP cookie jar contains only cookie we set');
if (observer) assert_equals(
await getCookieStringObserved(),
'TEST=value0',
'Observed cookie jar contains only cookie we set');
await cookieStore.set('TEST', 'value');
assert_equals(
await getCookieString(),
'TEST=value',
'Cookie jar contains only cookie we overwrote');
if (!kIsStatic) assert_equals(
await getCookieStringHttp(),
'TEST=value',
'HTTP cookie jar contains only cookie we overwrote');
if (observer) assert_equals(
await getCookieStringObserved(),
'TEST=value',
'Observed cookie jar contains only cookie we overwrote');
let allCookies = await cookieStore.getAll();
assert_equals(
allCookies[0].name,
'TEST',
'First entry in allCookies should be named TEST');
assert_equals(
allCookies[0].value,
'value',
'First entry in allCookies should have value "value"');
assert_equals(
allCookies.length,
1,
'Only one cookie should exist in allCookies');
let firstCookie = await cookieStore.get();
assert_equals(
firstCookie.name,
'TEST',
'First cookie should be named TEST');
assert_equals(
firstCookie.value,
'value',
'First cookie should have value "value"');
let allCookies_TEST = await cookieStore.getAll('TEST');
assert_equals(
allCookies_TEST[0].name,
'TEST',
'First entry in allCookies_TEST should be named TEST');
assert_equals(
allCookies_TEST[0].value,
'value',
'First entry in allCookies_TEST should have value "value"');
assert_equals(
allCookies_TEST.length,
1,
'Only one cookie should exist in allCookies_TEST');
let firstCookie_TEST = await cookieStore.get('TEST');
assert_equals(
firstCookie_TEST.name,
'TEST',
'First TEST cookie should be named TEST');
assert_equals(
firstCookie_TEST.value,
'value',
'First TEST cookie should have value "value"');
};
const testOneSimpleOriginCookie = async testCase => {
await promise_rejects_when_unsecured(
testCase,
new SyntaxError(),
setOneSimpleOriginSessionCookie(),
'__Host- prefix only writable from' +
' secure contexts (setOneSimpleOriginSessionCookie)');
if (!kIsUnsecured) {
assert_equals(
await getOneSimpleOriginCookie(),
'cookie-value',
'__Host-COOKIENAME cookie should be found' +
' in a secure context (getOneSimpleOriginCookie)');
} else {
assert_equals(
await getOneSimpleOriginCookie(),
undefined,
'__Host-COOKIENAME cookie should not be found' +
' in an unsecured context (getOneSimpleOriginCookie)');
}
if (kIsUnsecured) {
assert_equals(
await countMatchingSimpleOriginCookies(),
0,
'No __Host-COOKIEN* cookies should be found' +
' in an unsecured context (countMatchingSimpleOriginCookies)');
} else {
assert_equals(
await countMatchingSimpleOriginCookies(),
1,
'One __Host-COOKIEN* cookie should be found' +
' in a secure context (countMatchingSimpleOriginCookies)');
}
};
const testExpiration = async testCase => {
await promise_rejects_when_unsecured(
testCase,
new SyntaxError(),
setOneDaySecureCookieWithDate(),
'Secure cookies only writable' +
' from secure contexts (setOneDaySecureCookieWithDate)');
await setOneDayUnsecuredCookieWithMillisecondsSinceEpoch();
assert_equals(
await getCookieString('LEGACYCOOKIENAME'),
'LEGACYCOOKIENAME=cookie-value',
'Ensure unsecured cookie we set is visible');
if (observer) assert_equals(
await getCookieStringObserved('LEGACYCOOKIENAME'),
'LEGACYCOOKIENAME=cookie-value',
'Ensure unsecured cookie we set is visible to observer');
await deleteUnsecuredCookieWithDomainAndPath();
await promise_rejects_when_unsecured(
testCase,
new SyntaxError(),
setSecureCookieWithHttpLikeExpirationString(),
'Secure cookies only writable from secure contexts' +
' (setSecureCookieWithHttpLikeExpirationString)');
};
// Rewrite domain and path in affected cases to match current test
// domain and directory.
//
// FIXME: remove these once ServiceWorker support and cross-domain
// testing are added and full domain and path coverage is possible.
setOneDaySecureCookieWithDate =
eval(String(setOneDaySecureCookieWithDate).split(
'/cgi-bin/').join(location.pathname.replace(/[^/]+$/, '')));
setOneDaySecureCookieWithDate =
eval(String(setOneDaySecureCookieWithDate).split(
'example.org').join(location.hostname));
setOneDayUnsecuredCookieWithMillisecondsSinceEpoch =
eval(String(setOneDayUnsecuredCookieWithMillisecondsSinceEpoch).split(
'/cgi-bin/').join(location.pathname.replace(/[^/]+$/, '')));
setOneDayUnsecuredCookieWithMillisecondsSinceEpoch =
eval(String(setOneDayUnsecuredCookieWithMillisecondsSinceEpoch).split(
'example.org').join(location.hostname));
deleteUnsecuredCookieWithDomainAndPath =
eval(String(deleteUnsecuredCookieWithDomainAndPath).split(
'/cgi-bin/').join(location.pathname.replace(/[^/]+$/, '')));
deleteUnsecuredCookieWithDomainAndPath =
eval(String(deleteUnsecuredCookieWithDomainAndPath).split(
'example.org').join(location.hostname));
setSecureCookieWithHttpLikeExpirationString =
eval(String(setSecureCookieWithHttpLikeExpirationString).split(
'/cgi-bin/').join(location.pathname.replace(/[^/]+$/, '')));
setSecureCookieWithHttpLikeExpirationString =
eval(String(setSecureCookieWithHttpLikeExpirationString).split(
'example.org').join(location.hostname));
setExpiredSecureCookieWithDomainPathAndFallbackValue =
eval(String(setExpiredSecureCookieWithDomainPathAndFallbackValue).split(
'/cgi-bin/').join(location.pathname.replace(/[^/]+$/, '')));
setExpiredSecureCookieWithDomainPathAndFallbackValue =
eval(String(setExpiredSecureCookieWithDomainPathAndFallbackValue).split(
'example.org').join(location.hostname));
deleteSecureCookieWithDomainAndPath =
eval(String(deleteSecureCookieWithDomainAndPath).split(
'/cgi-bin/').join(location.pathname.replace(/[^/]+$/, '')));
deleteSecureCookieWithDomainAndPath =
eval(String(deleteSecureCookieWithDomainAndPath).split(
'example.org').join(location.hostname));