diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/cookie-store | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/cookie-store')
49 files changed, 3229 insertions, 0 deletions
diff --git a/testing/web-platform/tests/cookie-store/META.yml b/testing/web-platform/tests/cookie-store/META.yml new file mode 100644 index 0000000000..46da8a9fb6 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/META.yml @@ -0,0 +1,4 @@ +spec: https://wicg.github.io/cookie-store/ +suggested_reviewers: + - inexorabletash + - pwnall diff --git a/testing/web-platform/tests/cookie-store/README.md b/testing/web-platform/tests/cookie-store/README.md new file mode 100644 index 0000000000..b8a1d0a609 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/README.md @@ -0,0 +1,28 @@ +This directory contains tests for the +[Cookie Store API](https://github.com/WICG/cookie-store). + +## 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 (i.e. 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. diff --git a/testing/web-platform/tests/cookie-store/change_eventhandler_for_document_cookie.https.window.js b/testing/web-platform/tests/cookie-store/change_eventhandler_for_document_cookie.https.window.js new file mode 100644 index 0000000000..0a8b1bd21e --- /dev/null +++ b/testing/web-platform/tests/cookie-store/change_eventhandler_for_document_cookie.https.window.js @@ -0,0 +1,160 @@ +// META: title=Cookie Store API: Observing 'change' events in document when cookies set via document.cookie +// META: script=resources/cookie-test-helpers.js + +'use strict'; + +cookie_test(async t => { + let eventPromise = observeNextCookieChangeEvent(); + await setCookieStringDocument('DOCUMENT-cookie=value; path=/'); + assert_equals( + await getCookieString(), + 'DOCUMENT-cookie=value', + 'Cookie we wrote using document.cookie in cookie jar'); + assert_equals( + await getCookieStringHttp(), + 'DOCUMENT-cookie=value', + 'Cookie we wrote using document.cookie in HTTP cookie jar'); + assert_equals( + await getCookieStringDocument(), + 'DOCUMENT-cookie=value', + 'Cookie we wrote using document.cookie in document.cookie'); + await verifyCookieChangeEvent( + eventPromise, {changed: [{name: 'DOCUMENT-cookie', value: 'value'}]}, + 'Cookie we wrote using document.cookie is observed'); + + eventPromise = observeNextCookieChangeEvent(); + await setCookieStringDocument('DOCUMENT-cookie=new-value; path=/'); + assert_equals( + await getCookieString(), + 'DOCUMENT-cookie=new-value', + 'Cookie we overwrote using document.cookie in cookie jar'); + assert_equals( + await getCookieStringHttp(), + 'DOCUMENT-cookie=new-value', + 'Cookie we overwrote using document.cookie in HTTP cookie jar'); + assert_equals( + await getCookieStringDocument(), + 'DOCUMENT-cookie=new-value', + 'Cookie we overwrote using document.cookie in document.cookie'); + await verifyCookieChangeEvent( + eventPromise, {changed: [{name: 'DOCUMENT-cookie', value: 'new-value'}]}, + 'Cookie we overwrote using document.cookie is observed'); + + eventPromise = observeNextCookieChangeEvent(); + await setCookieStringDocument('DOCUMENT-cookie=DELETED; path=/; max-age=0'); + assert_equals( + await getCookieString(), + undefined, + 'Empty cookie jar after document.cookie' + + ' cookie-clearing using max-age=0'); + 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'); + await verifyCookieChangeEvent( + eventPromise, {deleted: [{name: 'DOCUMENT-cookie'}]}, + 'Deletion observed after document.cookie cookie-clearing' + + ' using max-age=0'); +}, 'document.cookie set/overwrite/delete observed by CookieStore'); + +cookie_test(async t => { + let eventPromise = observeNextCookieChangeEvent(); + await cookieStore.set('DOCUMENT-cookie', 'value'); + assert_equals( + await getCookieString(), + 'DOCUMENT-cookie=value', + 'Cookie we wrote using CookieStore in cookie jar'); + assert_equals( + await getCookieStringHttp(), + 'DOCUMENT-cookie=value', + 'Cookie we wrote using CookieStore in HTTP cookie jar'); + assert_equals( + await getCookieStringDocument(), + 'DOCUMENT-cookie=value', + 'Cookie we wrote using CookieStore in document.cookie'); + await verifyCookieChangeEvent( + eventPromise, {changed: [{name: 'DOCUMENT-cookie', value: 'value'}]}, + 'Cookie we wrote using CookieStore is observed'); + + eventPromise = observeNextCookieChangeEvent(); + await cookieStore.set('DOCUMENT-cookie', 'new-value'); + assert_equals( + await getCookieString(), + 'DOCUMENT-cookie=new-value', + 'Cookie we overwrote using CookieStore in cookie jar'); + assert_equals( + await getCookieStringHttp(), + 'DOCUMENT-cookie=new-value', + 'Cookie we overwrote using CookieStore in HTTP cookie jar'); + assert_equals( + await getCookieStringDocument(), + 'DOCUMENT-cookie=new-value', + 'Cookie we overwrote using CookieStore in document.cookie'); + await verifyCookieChangeEvent( + eventPromise, {changed: [{name: 'DOCUMENT-cookie', value: 'new-value'}]}, + 'Cookie we overwrote using CookieStore is observed'); + + eventPromise = observeNextCookieChangeEvent(); + await cookieStore.delete('DOCUMENT-cookie'); + assert_equals( + await getCookieString(), + undefined, + 'Empty cookie jar after CookieStore delete'); + assert_equals( + await getCookieStringHttp(), + undefined, + 'Empty HTTP cookie jar after CookieStore delete'); + assert_equals( + await getCookieStringDocument(), + undefined, + 'Empty document.cookie cookie jar after CookieStore delete'); + await verifyCookieChangeEvent( + eventPromise, {deleted: [{name: 'DOCUMENT-cookie'}]}, + 'Deletion observed after CookieStore delete'); +}, 'CookieStore set/overwrite/delete observed by document.cookie'); + + +cookie_test(async t => { + let eventPromise = observeNextCookieChangeEvent(); + await setCookieStringDocument('DOCUMENT-🍪=🔵; path=/'); + assert_equals( + await getCookieString(), + 'DOCUMENT-🍪=🔵', + 'Cookie we wrote using document.cookie in cookie jar'); + await verifyCookieChangeEvent( + eventPromise, {changed: [{name: 'DOCUMENT-🍪', value: '🔵'}]}, + 'Cookie we wrote using document.cookie is observed'); + + eventPromise = observeNextCookieChangeEvent(); + 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'); + await verifyCookieChangeEvent( + eventPromise, {deleted: [{name: 'DOCUMENT-🍪'}]}, + 'Deletion observed after document.cookie cookie-clearing' + + ' using max-age=0'); +}, 'CookieStore agrees with document.cookie on encoding non-ASCII cookies'); + + +cookie_test(async t => { + await cookieStore.set('DOCUMENT-🍪', '🔵'); + assert_equals( + await getCookieStringDocument(), + 'DOCUMENT-🍪=🔵', + 'Cookie we wrote using CookieStore in document.cookie'); + + await cookieStore.delete('DOCUMENT-🍪'); + assert_equals( + await getCookieStringDocument(), + undefined, + 'Empty cookie jar after CookieStore delete'); +}, 'document.cookie agrees with CookieStore on encoding non-ASCII cookies'); diff --git a/testing/web-platform/tests/cookie-store/change_eventhandler_for_http_cookie_and_set_cookie_headers.https.window.js b/testing/web-platform/tests/cookie-store/change_eventhandler_for_http_cookie_and_set_cookie_headers.https.window.js new file mode 100644 index 0000000000..2028df5b4b --- /dev/null +++ b/testing/web-platform/tests/cookie-store/change_eventhandler_for_http_cookie_and_set_cookie_headers.https.window.js @@ -0,0 +1,206 @@ +// META: title=Cookie Store API: Observing 'change' events in document when cookies set via Set-Cookie header +// META: script=resources/cookie-test-helpers.js + +'use strict'; + +cookie_test(async t => { + let eventPromise = observeNextCookieChangeEvent(); + await setCookieStringHttp('HTTP-cookie=value; path=/'); + assert_equals( + await getCookieString(), + 'HTTP-cookie=value', + 'Cookie we wrote using HTTP in cookie jar'); + assert_equals( + await getCookieStringHttp(), + 'HTTP-cookie=value', + 'Cookie we wrote using HTTP in HTTP cookie jar'); + await verifyCookieChangeEvent( + eventPromise, {changed: [{name: 'HTTP-cookie', value: 'value'}]}, + 'Cookie we wrote using HTTP is observed'); + + eventPromise = observeNextCookieChangeEvent(); + await setCookieStringHttp('HTTP-cookie=new-value; path=/'); + assert_equals( + await getCookieString(), + 'HTTP-cookie=new-value', + 'Cookie we overwrote using HTTP in cookie jar'); + assert_equals( + await getCookieStringHttp(), + 'HTTP-cookie=new-value', + 'Cookie we overwrote using HTTP in HTTP cookie jar'); + await verifyCookieChangeEvent( + eventPromise, {changed: [{name: 'HTTP-cookie', value: 'new-value'}]}, + 'Cookie we overwrote using HTTP is observed'); + + eventPromise = observeNextCookieChangeEvent(); + await setCookieStringHttp('HTTP-cookie=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'); + await verifyCookieChangeEvent( + eventPromise, {deleted: [{name: 'HTTP-cookie'}]}, + 'Deletion observed after HTTP cookie-clearing using max-age=0'); +}, 'HTTP set/overwrite/delete observed in CookieStore'); + + +cookie_test(async t => { + let eventPromise = observeNextCookieChangeEvent(); + await setCookieStringHttp('HTTP-🍪=🔵; path=/'); + assert_equals( + await getCookieString(), + 'HTTP-🍪=🔵', + 'Cookie we wrote using HTTP in cookie jar'); + await verifyCookieChangeEvent( + eventPromise, {changed: [{name: 'HTTP-🍪', value: '🔵'}]}, + 'Cookie we wrote using HTTP is observed'); + + eventPromise = observeNextCookieChangeEvent(); + await setCookieStringHttp('HTTP-🍪=DELETED; path=/; max-age=0'); + assert_equals( + await getCookieString(), + undefined, + 'Empty cookie jar after HTTP cookie-clearing using max-age=0'); + await verifyCookieChangeEvent( + eventPromise, {deleted: [{name: 'HTTP-🍪'}]}, + 'Deletion observed after HTTP cookie-clearing using max-age=0'); + +}, 'CookieStore agreed with HTTP headers agree on encoding non-ASCII cookies'); + + +cookie_test(async t => { + let eventPromise = observeNextCookieChangeEvent(); + await cookieStore.set('TEST', 'value0'); + assert_equals( + await getCookieString(), + 'TEST=value0', + 'Cookie jar contains only cookie we set'); + assert_equals( + await getCookieStringHttp(), + 'TEST=value0', + 'HTTP cookie jar contains only cookie we set'); + await verifyCookieChangeEvent( + eventPromise, + {changed: [{name: 'TEST', value: 'value0'}]}, + 'Observed value that was set'); + + eventPromise = observeNextCookieChangeEvent(); + await cookieStore.set('TEST', 'value'); + assert_equals( + await getCookieString(), + 'TEST=value', + 'Cookie jar contains only cookie we set'); + assert_equals( + await getCookieStringHttp(), + 'TEST=value', + 'HTTP cookie jar contains only cookie we set'); + await verifyCookieChangeEvent( + eventPromise, + {changed: [{name: 'TEST', value: 'value'}]}, + 'Observed value that was overwritten'); + + eventPromise = observeNextCookieChangeEvent(); + await cookieStore.delete('TEST'); + assert_equals( + await getCookieString(), + undefined, + 'Cookie jar does not contain cookie we deleted'); + assert_equals( + await getCookieStringHttp(), + undefined, + 'HTTP cookie jar does not contain cookie we deleted'); + await verifyCookieChangeEvent( + eventPromise, + {deleted: [{name: 'TEST'}]}, + 'Observed cookie that was deleted'); +}, 'CookieStore set/overwrite/delete observed in HTTP headers'); + + +cookie_test(async t => { + await cookieStore.set('🍪', '🔵'); + assert_equals( + await getCookieStringHttp(), + '🍪=🔵', + 'HTTP cookie jar contains only cookie we set'); + + await cookieStore.delete('🍪'); + assert_equals( + await getCookieStringHttp(), + undefined, + 'HTTP cookie jar does not contain cookie we deleted'); +}, 'HTTP headers agreed with CookieStore on encoding non-ASCII cookies'); + + +cookie_test(async t => { + // Non-UTF-8 byte sequences cause the Set-Cookie to be dropped. + let eventPromise = observeNextCookieChangeEvent(); + await setCookieBinaryHttp( + unescape(encodeURIComponent('HTTP-cookie=value')) + '\xef\xbf\xbd; path=/'); + assert_equals( + await getCookieString(), + 'HTTP-cookie=value\ufffd', + 'Binary cookie we wrote using HTTP in cookie jar'); + assert_equals( + await getCookieStringHttp(), + 'HTTP-cookie=value\ufffd', + 'Binary cookie we wrote using HTTP in HTTP cookie jar'); + assert_equals( + decodeURIComponent(escape(await getCookieBinaryHttp())), + 'HTTP-cookie=value\ufffd', + 'Binary cookie we wrote in binary HTTP cookie jar'); + assert_equals( + await getCookieBinaryHttp(), + unescape(encodeURIComponent('HTTP-cookie=value')) + '\xef\xbf\xbd', + 'Binary cookie we wrote in binary HTTP cookie jar'); + await verifyCookieChangeEvent( + eventPromise, {changed: [{name: 'HTTP-cookie', value: 'value\ufffd'}]}, + 'Binary cookie we wrote using HTTP is observed'); + + eventPromise = observeNextCookieChangeEvent(); + await setCookieBinaryHttp( + unescape(encodeURIComponent('HTTP-cookie=new-value')) + '\xef\xbf\xbd; path=/'); + assert_equals( + await getCookieString(), + 'HTTP-cookie=new-value\ufffd', + 'Binary cookie we overwrote using HTTP in cookie jar'); + assert_equals( + await getCookieStringHttp(), + 'HTTP-cookie=new-value\ufffd', + 'Binary cookie we overwrote using HTTP in HTTP cookie jar'); + assert_equals( + decodeURIComponent(escape(await getCookieBinaryHttp())), + 'HTTP-cookie=new-value\ufffd', + 'Binary cookie we overwrote in binary HTTP cookie jar'); + assert_equals( + await getCookieBinaryHttp(), + unescape(encodeURIComponent('HTTP-cookie=new-value')) + '\xef\xbf\xbd', + 'Binary cookie we overwrote in binary HTTP cookie jar'); + await verifyCookieChangeEvent( + eventPromise, {changed: [{name: 'HTTP-cookie', value: 'new-value\ufffd'}]}, + 'Binary cookie we overwrote using HTTP is observed'); + + eventPromise = observeNextCookieChangeEvent(); + await setCookieBinaryHttp( + unescape(encodeURIComponent('HTTP-cookie=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'); + await verifyCookieChangeEvent( + eventPromise, {deleted: [{name: 'HTTP-cookie'}]}, + 'Deletion observed after binary HTTP cookie-clearing using max-age=0'); +}, 'Binary HTTP set/overwrite/delete observed in CookieStore'); diff --git a/testing/web-platform/tests/cookie-store/change_eventhandler_for_no_name_and_no_value.https.window.js b/testing/web-platform/tests/cookie-store/change_eventhandler_for_no_name_and_no_value.https.window.js new file mode 100644 index 0000000000..4498caf596 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/change_eventhandler_for_no_name_and_no_value.https.window.js @@ -0,0 +1,58 @@ +// META: title=Cookie Store API: Observing 'change' events in document when modifications API is called with blank arguments +// META: script=resources/cookie-test-helpers.js + +'use strict'; + +cookie_test(async t => { + let eventPromise = observeNextCookieChangeEvent(); + await cookieStore.set('', 'first-value'); + const actual1 = + (await cookieStore.getAll('')).map(({ value }) => value).join(';'); + const expected1 = 'first-value'; + assert_equals(actual1, expected1); + await verifyCookieChangeEvent( + eventPromise, {changed: [{name: '', value: 'first-value'}]}, + 'Observed no-name change'); + + await promise_rejects_js( + t, + TypeError, + cookieStore.set('', ''), + 'Expected promise rejection when setting a cookie with' + + ' no name and no value'); + + await promise_rejects_js( + t, + TypeError, + cookieStore.set({name: '', value: ''}), + 'Expected promise rejection when setting a cookie with' + + ' no name and no value'); + + const cookies = await cookieStore.getAll(''); + assert_equals(cookies.length, 1); + assert_equals(cookies[0].name, ''); + assert_equals(cookies[0].value, 'first-value', + 'Cookie with no name should still have previous value.'); + + eventPromise = observeNextCookieChangeEvent(); + await cookieStore.delete(''); + await verifyCookieChangeEvent( + eventPromise, {deleted: [{name: ''}]}, + 'Observed no-name deletion'); + + assert_equals( + await getCookieString(), + undefined, + 'Empty cookie jar'); + assert_equals( + await getCookieStringHttp(), + undefined, + 'Empty HTTP cookie jar'); + if (kHasDocument) { + assert_equals( + await getCookieStringDocument(), + undefined, + 'Empty document.cookie cookie jar'); + } + +}, 'Verify behavior of no-name and no-value cookies.'); diff --git a/testing/web-platform/tests/cookie-store/change_eventhandler_for_no_name_equals_in_value.https.window.js b/testing/web-platform/tests/cookie-store/change_eventhandler_for_no_name_equals_in_value.https.window.js new file mode 100644 index 0000000000..13d721786c --- /dev/null +++ b/testing/web-platform/tests/cookie-store/change_eventhandler_for_no_name_equals_in_value.https.window.js @@ -0,0 +1,46 @@ +// META: title=Cookie Store API: Observing 'change' events in document when setting a cookie value containing "=" +// META: script=resources/cookie-test-helpers.js + +'use strict'; + +cookie_test(async t => { + let eventPromise = observeNextCookieChangeEvent(); + await cookieStore.set('', 'first-value'); + const initialCookies = await cookieStore.getAll(''); + assert_equals(initialCookies.length, 1); + assert_equals(initialCookies[0].name, ''); + assert_equals(initialCookies[0].value, 'first-value'); + + await verifyCookieChangeEvent( + eventPromise, {changed: [{name: '', value: 'first-value'}]}, + 'Observed no-name change'); + + await promise_rejects_js( + t, + TypeError, + cookieStore.set('', 'suspicious-value=resembles-name-and-value'), + 'Expected promise rejection when setting a cookie with' + + ' no name and "=" in value (via arguments)'); + + await promise_rejects_js( + t, + TypeError, + cookieStore.set( + {name: '', value: 'suspicious-value=resembles-name-and-value'}), + 'Expected promise rejection when setting a cookie with' + + ' no name and "=" in value (via options)'); + + const cookies = await cookieStore.getAll(''); + assert_equals(cookies.length, 1); + assert_equals(cookies[0].name, ''); + assert_equals(cookies[0].value, 'first-value', + 'Cookie with no name should still have previous value.'); + + eventPromise = observeNextCookieChangeEvent(); + await cookieStore.delete(''); + await verifyCookieChangeEvent( + eventPromise, {deleted: [{name: ''}]}, + 'Observed no-name deletion'); + +}, "Verify that attempting to set a cookie with no name and with '=' in" + + " the value does not work."); diff --git a/testing/web-platform/tests/cookie-store/change_eventhandler_for_no_name_multiple_values.https.window.js b/testing/web-platform/tests/cookie-store/change_eventhandler_for_no_name_multiple_values.https.window.js new file mode 100644 index 0000000000..60c6c16518 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/change_eventhandler_for_no_name_multiple_values.https.window.js @@ -0,0 +1,41 @@ +// META: title=Cookie Store API: Observing 'change' events in document when modifications API is called multiple times with a blank name +// META: script=resources/cookie-test-helpers.js + +'use strict'; + +cookie_test(async t => { + let eventPromise = observeNextCookieChangeEvent(); + await cookieStore.set('', 'first-value'); + let actual1 = + (await cookieStore.getAll('')).map(({ value }) => value).join(';'); + let expected1 = 'first-value'; + assert_equals(actual1, expected1); + await verifyCookieChangeEvent( + eventPromise, {changed: [{name: '', value: 'first-value'}]}, + 'Observed no-name change'); + + eventPromise = observeNextCookieChangeEvent(); + await cookieStore.set('', 'second-value'); + let actual2 = + (await cookieStore.getAll('')).map(({ value }) => value).join(';'); + let expected2 = 'second-value'; + assert_equals(actual2, expected2); + await verifyCookieChangeEvent( + eventPromise, {changed: [{name: '', value: 'second-value'}]}, + 'Observed no-name change'); + + eventPromise = observeNextCookieChangeEvent(); + await cookieStore.delete(''); + await verifyCookieChangeEvent( + eventPromise, {deleted: [{name: ''}]}, + 'Observed no-name change'); + + assert_equals( + await getCookieString(), + undefined, + 'Empty cookie jar after testNoNameMultipleValues'); + assert_equals( + await getCookieStringHttp(), + undefined, + 'Empty HTTP cookie jar after testNoNameMultipleValues'); +}, 'Verify behavior of multiple no-name cookies'); diff --git a/testing/web-platform/tests/cookie-store/cookieListItem_attributes.https.any.js b/testing/web-platform/tests/cookie-store/cookieListItem_attributes.https.any.js new file mode 100644 index 0000000000..200cbd0692 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieListItem_attributes.https.any.js @@ -0,0 +1,197 @@ +// META: title=Cookie Store API: cookieListItem attributes +// META: global=window,serviceworker + +'use strict'; + +const kCurrentHostname = (new URL(self.location.href)).hostname; + +const kOneDay = 24 * 60 * 60 * 1000; +const kFourHundredDays = 400 * kOneDay; +const kTenYears = 10 * 365 * kOneDay; +const kFourHundredDaysFromNow = Date.now() + kFourHundredDays; +const kTenYearsFromNow = Date.now() + kTenYears; + +const kCookieListItemKeys = + ['domain', 'expires', 'name', 'path', 'sameSite', 'secure', 'value'].sort(); + +promise_test(async testCase => { + await cookieStore.delete('cookie-name'); + + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); + assert_equals(cookie.domain, null); + assert_equals(cookie.path, '/'); + assert_equals(cookie.expires, null); + assert_equals(cookie.secure, true); + assert_equals(cookie.sameSite, 'strict'); + const itemKeys = Object.keys(cookie); + for (const key of kCookieListItemKeys) { + assert_in_array(key, itemKeys); + } +}, 'CookieListItem - cookieStore.set defaults with positional name and value'); + +promise_test(async testCase => { + await cookieStore.delete('cookie-name'); + + await cookieStore.set({ name: 'cookie-name', value: 'cookie-value' }); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); + assert_equals(cookie.domain, null); + assert_equals(cookie.path, '/'); + assert_equals(cookie.expires, null); + assert_equals(cookie.secure, true); + assert_equals(cookie.sameSite, 'strict'); + const itemKeys = Object.keys(cookie); + for (const key of kCookieListItemKeys) { + assert_in_array(key, itemKeys); + } +}, 'CookieListItem - cookieStore.set defaults with name and value in options'); + +promise_test(async testCase => { + await cookieStore.delete('cookie-name'); + + await cookieStore.set({ name: 'cookie-name', value: 'cookie-value', + expires: kTenYearsFromNow }); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); + assert_equals(cookie.domain, null); + assert_equals(cookie.path, '/'); + assert_approx_equals(cookie.expires, kFourHundredDaysFromNow, kOneDay); + assert_equals(cookie.secure, true); + assert_equals(cookie.sameSite, 'strict'); + const itemKeys = Object.keys(cookie); + for (const key of kCookieListItemKeys) { + assert_in_array(key, itemKeys); + } +}, 'CookieListItem - cookieStore.set with expires set to a timestamp 10 ' + + 'years in the future'); + +promise_test(async testCase => { + await cookieStore.delete('cookie-name'); + + await cookieStore.set({ name: 'cookie-name', value: 'cookie-value', + expires: new Date(kTenYearsFromNow) }); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); + assert_equals(cookie.domain, null); + assert_equals(cookie.path, '/'); + assert_approx_equals(cookie.expires, kFourHundredDaysFromNow, kOneDay); + assert_equals(cookie.secure, true); +}, 'CookieListItem - cookieStore.set with expires set to a Date 10 ' + + 'years in the future'); + +promise_test(async testCase => { + await cookieStore.delete({ name: 'cookie-name', domain: kCurrentHostname }); + + await cookieStore.set({ name: 'cookie-name', value: 'cookie-value', + domain: kCurrentHostname }); + testCase.add_cleanup(async () => { + await cookieStore.delete({ name: 'cookie-name', domain: kCurrentHostname }); + }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); + assert_equals(cookie.domain, kCurrentHostname); + assert_equals(cookie.path, '/'); + assert_equals(cookie.expires, null); + assert_equals(cookie.secure, true); + assert_equals(cookie.sameSite, 'strict'); + const itemKeys = Object.keys(cookie); + for (const key of kCookieListItemKeys) { + assert_in_array(key, itemKeys); + } +}, 'CookieListItem - cookieStore.set with domain set to the current hostname'); + +promise_test(async testCase => { + const currentUrl = new URL(self.location.href); + const currentPath = currentUrl.pathname; + const currentDirectory = + currentPath.substr(0, currentPath.lastIndexOf('/') + 1); + await cookieStore.delete({ name: 'cookie-name', path: currentDirectory }); + + await cookieStore.set({ name: 'cookie-name', value: 'cookie-value', + path: currentDirectory }); + testCase.add_cleanup(async () => { + await cookieStore.delete({ name: 'cookie-name', path: currentDirectory }); + }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); + assert_equals(cookie.domain, null); + assert_equals(cookie.path, currentDirectory); + assert_equals(cookie.expires, null); + assert_equals(cookie.secure, true); + assert_equals(cookie.sameSite, 'strict'); + const itemKeys = Object.keys(cookie); + for (const key of kCookieListItemKeys) { + assert_in_array(key, itemKeys); + } +}, 'CookieListItem - cookieStore.set with path set to the current directory'); + +promise_test(async testCase => { + const currentUrl = new URL(self.location.href); + const currentPath = currentUrl.pathname; + const currentDirectory = currentPath.substr(0, currentPath.lastIndexOf('/')); + await cookieStore.delete({ name: 'cookie-name', path: currentDirectory }); + + await cookieStore.set({ name: 'cookie-name', value: 'cookie-value', + path: currentDirectory }); + testCase.add_cleanup(async () => { + await cookieStore.delete({ name: 'cookie-name', path: currentDirectory }); + }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); + assert_equals(cookie.domain, null); + assert_equals(cookie.path, currentDirectory + '/'); + assert_equals(cookie.expires, null); + assert_equals(cookie.secure, true); + assert_equals(cookie.sameSite, 'strict'); + const itemKeys = Object.keys(cookie); + for (const key of kCookieListItemKeys) { + assert_in_array(key, itemKeys); + } +}, 'CookieListItem - cookieStore.set adds / to path if it does not end with /'); + +['strict', 'lax', 'none'].forEach(sameSiteValue => { + promise_test(async testCase => { + await cookieStore.delete('cookie-name'); + + await cookieStore.set({ + name: 'cookie-name', value: 'cookie-value', sameSite: sameSiteValue }); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); + assert_equals(cookie.domain, null); + assert_equals(cookie.path, '/'); + assert_equals(cookie.expires, null); + assert_equals(cookie.secure, true); + assert_equals(cookie.sameSite, sameSiteValue); + const itemKeys = Object.keys(cookie); + for (const key of kCookieListItemKeys) { + assert_in_array(key, itemKeys); + } + }, `CookieListItem - cookieStore.set with sameSite set to ${sameSiteValue}`); + +}); diff --git a/testing/web-platform/tests/cookie-store/cookieStoreManager_getSubscriptions_empty.https.any.js b/testing/web-platform/tests/cookie-store/cookieStoreManager_getSubscriptions_empty.https.any.js new file mode 100644 index 0000000000..8cfd732f30 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStoreManager_getSubscriptions_empty.https.any.js @@ -0,0 +1,28 @@ +// META: title=Cookie Store API: ServiceWorker without cookie change subscriptions +// META: global=window,serviceworker +// META: script=/service-workers/service-worker/resources/test-helpers.sub.js + +'use strict'; + +promise_test(async testCase => { + if (self.GLOBAL.isWindow()) { + const registration = await service_worker_unregister_and_register( + testCase, 'resources/empty_sw.js', 'resources/does/not/exist'); + testCase.add_cleanup(() => registration.unregister()); + + // Wait for this service worker to become active before snapshotting the + // subscription state, for consistency with other tests. + await wait_for_state(testCase, registration.installing, 'activated'); + + self.registration = registration; + } else { + // Wait for this service worker to become active before snapshotting the + // subscription state, for consistency with other tests. + await new Promise(resolve => { + self.addEventListener('activate', event => { resolve(); }); + }); + } + + const subscriptions = await registration.cookies.getSubscriptions(); + assert_equals(subscriptions.length, 0); +}, 'getSubscriptions returns an empty array when there are no subscriptions'); diff --git a/testing/web-platform/tests/cookie-store/cookieStoreManager_getSubscriptions_multiple.https.any.js b/testing/web-platform/tests/cookie-store/cookieStoreManager_getSubscriptions_multiple.https.any.js new file mode 100644 index 0000000000..9e153d03aa --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStoreManager_getSubscriptions_multiple.https.any.js @@ -0,0 +1,77 @@ +// META: title=Cookie Store API: ServiceWorker with multiple cookie change subscriptions +// META: global=window,serviceworker +// META: script=/service-workers/service-worker/resources/test-helpers.sub.js + +'use strict'; + +// sort() comparator that uses the < operator. +// +// This is intended to be used for sorting strings. Using < is preferred to +// localeCompare() because the latter has some implementation-dependent +// behavior. +function CompareStrings(a, b) { + return a < b ? -1 : (b < a ? 1 : 0); +} + +promise_test(async testCase => { + let scope; + + if (self.GLOBAL.isWindow()) { + scope = '/cookie-store/resources/does/not/exist'; + + const registration = await service_worker_unregister_and_register( + testCase, 'resources/empty_sw.js', scope); + testCase.add_cleanup(() => registration.unregister()); + + // Must wait for the service worker to enter the 'activated' state before + // subscribing to cookiechange events. + await wait_for_state(testCase, registration.installing, 'activated'); + + self.registration = registration; + } else { + scope = '/cookie-store/does/not/exist'; + + // Must wait for the service worker to enter the 'activated' state before + // subscribing to cookiechange events. + await new Promise(resolve => { + self.addEventListener('activate', event => { resolve(); }); + }); + } + + { + const subscriptions = [{ name: 'cookie-name1', url: `${scope}/path1` }]; + await registration.cookies.subscribe(subscriptions); + testCase.add_cleanup(() => { + // For non-ServiceWorker environments, registration.unregister() cleans up + // cookie subscriptions. + if (self.GLOBAL.isWorker()) { + return registration.cookies.unsubscribe(subscriptions); + } + }); + } + { + const subscriptions = [ + { }, // Test the default values for subscription properties. + { name: 'cookie-prefix' }, + ]; + await registration.cookies.subscribe(subscriptions); + testCase.add_cleanup(() => { + // For non-ServiceWorker environments, registration.unregister() cleans up + // cookie subscriptions. + if (self.GLOBAL.isWorker()) { + return registration.cookies.unsubscribe(subscriptions); + } + }); + } + + const subscriptions = await registration.cookies.getSubscriptions(); + assert_equals(subscriptions.length, 3); + + subscriptions.sort((a, b) => CompareStrings(`${a.name}`, `${b.name}`)); + + assert_equals(subscriptions[0].name, 'cookie-name1'); + + assert_equals(subscriptions[1].name, 'cookie-prefix'); + + assert_false('name' in subscriptions[2]); +}, 'getSubscriptions returns a subscription passed to subscribe'); diff --git a/testing/web-platform/tests/cookie-store/cookieStoreManager_getSubscriptions_single.https.any.js b/testing/web-platform/tests/cookie-store/cookieStoreManager_getSubscriptions_single.https.any.js new file mode 100644 index 0000000000..98ec19df3f --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStoreManager_getSubscriptions_single.https.any.js @@ -0,0 +1,50 @@ +// META: title=Cookie Store API: ServiceWorker with one cookie change subscription +// META: global=window,serviceworker +// META: script=/service-workers/service-worker/resources/test-helpers.sub.js + +'use strict'; + +promise_test(async testCase => { + let scope; + + if (self.GLOBAL.isWindow()) { + scope = '/cookie-store/resources/does/not/exist'; + + const registration = await service_worker_unregister_and_register( + testCase, 'resources/empty_sw.js', scope); + testCase.add_cleanup(() => registration.unregister()); + + // Must wait for the service worker to enter the 'activated' state before + // subscribing to cookiechange events. + await wait_for_state(testCase, registration.installing, 'activated'); + + self.registration = registration; + } else { + scope = '/cookie-store/does/not/exist'; + + // Must wait for the service worker to enter the 'activated' state before + // subscribing to cookiechange events. + await new Promise(resolve => { + self.addEventListener('activate', event => { resolve(); }); + }); + } + + { + const subscriptions = [{ name: 'cookie-name', url: `${scope}/path` }]; + await registration.cookies.subscribe(subscriptions); + testCase.add_cleanup(() => { + // For non-ServiceWorker environments, registration.unregister() cleans up + // cookie subscriptions. + if (self.GLOBAL.isWorker()) { + return registration.cookies.unsubscribe(subscriptions); + } + }); + } + + const subscriptions = await registration.cookies.getSubscriptions(); + assert_equals(subscriptions.length, 1); + + assert_equals(subscriptions[0].name, 'cookie-name'); + assert_equals(subscriptions[0].url, + (new URL(`${scope}/path`, self.location.href)).href); +}, 'getSubscriptions returns a subscription passed to subscribe'); diff --git a/testing/web-platform/tests/cookie-store/cookieStore_delete_arguments.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_delete_arguments.https.any.js new file mode 100644 index 0000000000..ddae23888f --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStore_delete_arguments.https.any.js @@ -0,0 +1,171 @@ +// META: title=Cookie Store API: cookieStore.delete() arguments +// META: global=window,serviceworker + +'use strict'; + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + + await cookieStore.delete('cookie-name'); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie, null); +}, 'cookieStore.delete with positional name'); + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + await cookieStore.delete({ name: 'cookie-name' }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie, null); +}, 'cookieStore.delete with name in options'); + +promise_test(async testCase => { + const currentUrl = new URL(self.location.href); + const currentDomain = currentUrl.hostname; + + await promise_rejects_js(testCase, TypeError, cookieStore.set( + { name: 'cookie-name', value: 'cookie-value', + domain: `.${currentDomain}` })); +}, 'cookieStore.delete domain starts with "."'); + +promise_test(async testCase => { + await promise_rejects_js(testCase, TypeError, cookieStore.set( + { name: 'cookie-name', value: 'cookie-value', domain: 'example.com' })); +}, 'cookieStore.delete with domain that is not equal current host'); + +promise_test(async testCase => { + const currentUrl = new URL(self.location.href); + const currentDomain = currentUrl.hostname; + await cookieStore.set( + { name: 'cookie-name', value: 'cookie-value', domain: currentDomain }); + testCase.add_cleanup(async () => { + await cookieStore.delete({ name: 'cookie-name', domain: currentDomain }); + }); + + await cookieStore.delete({ name: 'cookie-name', domain: currentDomain }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie, null); +}, 'cookieStore.delete with domain set to the current hostname'); + +promise_test(async testCase => { + const currentUrl = new URL(self.location.href); + const currentDomain = currentUrl.hostname; + const subDomain = `sub.${currentDomain}`; + + await promise_rejects_js(testCase, TypeError, cookieStore.delete( + { name: 'cookie-name', domain: subDomain })); +}, 'cookieStore.delete with domain set to a subdomain of the current hostname'); + +promise_test(async testCase => { + const currentUrl = new URL(self.location.href); + const currentDomain = currentUrl.hostname; + assert_not_equals(currentDomain[0] === '.', + 'this test assumes that the current hostname does not start with .'); + const domainSuffix = currentDomain.substr(1); + + await promise_rejects_js(testCase, TypeError, cookieStore.delete( + { name: 'cookie-name', domain: domainSuffix })); +}, 'cookieStore.delete with domain set to a non-domain-matching suffix of ' + + 'the current hostname'); + +promise_test(async testCase => { + const currentUrl = new URL(self.location.href); + const currentPath = currentUrl.pathname; + const currentDirectory = + currentPath.substr(0, currentPath.lastIndexOf('/') + 1); + await cookieStore.set( + { name: 'cookie-name', value: 'cookie-value', path: currentDirectory }); + testCase.add_cleanup(async () => { + await cookieStore.delete({ name: 'cookie-name', path: currentDirectory }); + }); + + await cookieStore.delete({ name: 'cookie-name', path: currentDirectory }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie, null); +}, 'cookieStore.delete with path set to the current directory'); + +promise_test(async testCase => { + const currentUrl = new URL(self.location.href); + const currentPath = currentUrl.pathname; + const currentDirectory = + currentPath.substr(0, currentPath.lastIndexOf('/') + 1); + const subDirectory = currentDirectory + "subdir/"; + await cookieStore.set( + { name: 'cookie-name', value: 'cookie-value', path: currentDirectory }); + testCase.add_cleanup(async () => { + await cookieStore.delete({ name: 'cookie-name', path: currentDirectory }); + }); + + await cookieStore.delete({ name: 'cookie-name', path: subDirectory }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); +}, 'cookieStore.delete with path set to subdirectory of the current directory'); + +promise_test(async testCase => { + const currentUrl = new URL(self.location.href); + const currentPath = currentUrl.pathname; + const currentDirectory = currentPath.substr(0, currentPath.lastIndexOf('/')); + await cookieStore.set( + { name: 'cookie-name', + value: 'cookie-value', + path: currentDirectory + '/' }); + testCase.add_cleanup(async () => { + await cookieStore.delete({ name: 'cookie-name', path: currentDirectory }); + }); + + await cookieStore.delete({ name: 'cookie-name', path: currentDirectory }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie, null); +}, 'cookieStore.delete with missing / at the end of path'); + +promise_test(async testCase => { + const currentUrl = new URL(self.location.href); + const currentPath = currentUrl.pathname; + const currentDirectory = + currentPath.substr(0, currentPath.lastIndexOf('/') + 1); + const invalidPath = currentDirectory.substr(1); + + await promise_rejects_js(testCase, TypeError, cookieStore.delete( + { name: 'cookie-name', path: invalidPath })); +}, 'cookieStore.delete with path that does not start with /'); + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + const cookie_attributes = await cookieStore.get('cookie-name'); + assert_equals(cookie_attributes.name, 'cookie-name'); + assert_equals(cookie_attributes.value, 'cookie-value'); + + await cookieStore.delete(cookie_attributes); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie, null); +}, 'cookieStore.delete with get result'); + +promise_test(async testCase => { + await cookieStore.set('', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete(''); + }); + + await cookieStore.delete(''); + const cookie = await cookieStore.get(''); + assert_equals(cookie, null); +}, 'cookieStore.delete with positional empty name'); + +promise_test(async testCase => { + await cookieStore.set('', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete(''); + }); + + await cookieStore.delete({ name: '' }); + const cookie = await cookieStore.get(''); + assert_equals(cookie, null); +}, 'cookieStore.delete with empty name in options'); diff --git a/testing/web-platform/tests/cookie-store/cookieStore_delete_basic.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_delete_basic.https.any.js new file mode 100644 index 0000000000..08a1fac5af --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStore_delete_basic.https.any.js @@ -0,0 +1,13 @@ +// META: title=Cookie Store API: cookieStore.delete() return type +// META: global=window,serviceworker + +'use strict'; + +promise_test(async testCase => { + const p = cookieStore.delete('cookie-name'); + assert_true(p instanceof Promise, + 'cookieStore.delete() returns a promise'); + const result = await p; + assert_equals(result, undefined, + 'cookieStore.delete() promise resolves to undefined'); +}, 'cookieStore.delete return type is Promise<void>'); diff --git a/testing/web-platform/tests/cookie-store/cookieStore_event_arguments.https.window.js b/testing/web-platform/tests/cookie-store/cookieStore_event_arguments.https.window.js new file mode 100644 index 0000000000..bcb698eeb0 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStore_event_arguments.https.window.js @@ -0,0 +1,65 @@ +'use strict'; + +test(() => { + const event = new CookieChangeEvent('change'); + assert_true(event instanceof CookieChangeEvent); + assert_equals(event.type, 'change'); + assert_equals(event.changed.length, 0); + assert_equals(event.deleted.length, 0); +}, 'CookieChangeEvent construction with default arguments'); + +test(() => { + const event = new CookieChangeEvent('change', { + changed: [ + { name: 'changed-name1', value: 'changed-value1' }, + { name: 'changed-name2', value: 'changed-value2' }, + ], + }); + assert_true(event instanceof CookieChangeEvent); + assert_equals(event.type, 'change'); + assert_equals(event.changed.length, 2); + assert_equals(event.changed[0].name, 'changed-name1'); + assert_equals(event.changed[0].value, 'changed-value1'); + assert_equals(event.changed[1].name, 'changed-name2'); + assert_equals(event.changed[1].value, 'changed-value2'); + assert_equals(event.deleted.length, 0); +}, 'CookieChangeEvent construction with changed cookie list'); + +test(() => { + const event = new CookieChangeEvent('change', { + deleted: [ + { name: 'deleted-name1', value: 'deleted-value1' }, + { name: 'deleted-name2', value: 'deleted-value2' }, + ], + }); + assert_true(event instanceof CookieChangeEvent); + assert_equals(event.type, 'change'); + assert_equals(event.changed.length, 0); + assert_equals(event.deleted.length, 2); + assert_equals(event.deleted[0].name, 'deleted-name1'); + assert_equals(event.deleted[0].value, 'deleted-value1'); + assert_equals(event.deleted[1].name, 'deleted-name2'); + assert_equals(event.deleted[1].value, 'deleted-value2'); +}, 'CookieChangeEvent construction with deleted cookie list'); + +test(() => { + const event = new CookieChangeEvent('change', { + changed: [ + { name: 'changed-name1', value: 'changed-value1' }, + { name: 'changed-name2', value: 'changed-value2' }, + ], + deleted: [ + { name: 'deleted-name1', value: 'deleted-value1' }, + ], + }); + assert_true(event instanceof CookieChangeEvent); + assert_equals(event.type, 'change'); + assert_equals(event.changed.length, 2); + assert_equals(event.changed[0].name, 'changed-name1'); + assert_equals(event.changed[0].value, 'changed-value1'); + assert_equals(event.changed[1].name, 'changed-name2'); + assert_equals(event.changed[1].value, 'changed-value2'); + assert_equals(event.deleted.length, 1); + assert_equals(event.deleted[0].name, 'deleted-name1'); + assert_equals(event.deleted[0].value, 'deleted-value1'); +}, 'CookieChangeEvent construction with changed and deleted cookie lists');
\ No newline at end of file diff --git a/testing/web-platform/tests/cookie-store/cookieStore_event_basic.https.window.js b/testing/web-platform/tests/cookie-store/cookieStore_event_basic.https.window.js new file mode 100644 index 0000000000..c0075d6adc --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStore_event_basic.https.window.js @@ -0,0 +1,24 @@ +'use strict'; + +promise_test(async testCase => { + const eventPromise = new Promise((resolve) => { + cookieStore.onchange = resolve; + }); + + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + const event = await eventPromise; + assert_true(event instanceof CookieChangeEvent); + + assert_equals(event.changed, event.changed); + assert_equals(event.deleted, event.deleted); + + assert_equals(event.type, 'change'); + assert_equals(event.changed.length, 1); + assert_equals(event.changed[0].name, 'cookie-name'); + assert_equals(event.changed[0].value, 'cookie-value'); + assert_equals(event.deleted.length, 0); +}, 'cookieStore fires change event for cookie set by cookieStore.set()'); diff --git a/testing/web-platform/tests/cookie-store/cookieStore_event_delete.https.window.js b/testing/web-platform/tests/cookie-store/cookieStore_event_delete.https.window.js new file mode 100644 index 0000000000..e8c6fc036a --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStore_event_delete.https.window.js @@ -0,0 +1,22 @@ +'use strict'; + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + const eventPromise = new Promise((resolve) => { + cookieStore.onchange = resolve; + }); + await cookieStore.delete('cookie-name'); + const event = await eventPromise; + assert_true(event instanceof CookieChangeEvent); + assert_equals(event.type, 'change'); + assert_equals(event.deleted.length, 1); + assert_equals(event.deleted[0].name, 'cookie-name'); + assert_equals( + event.deleted[0].value, undefined, + 'Cookie change events for deletions should not have cookie values'); + assert_equals(event.changed.length, 0); +}, 'cookieStore fires change event for cookie deleted by cookieStore.delete()');
\ No newline at end of file diff --git a/testing/web-platform/tests/cookie-store/cookieStore_event_overwrite.https.window.js b/testing/web-platform/tests/cookie-store/cookieStore_event_overwrite.https.window.js new file mode 100644 index 0000000000..3acffea41c --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStore_event_overwrite.https.window.js @@ -0,0 +1,22 @@ +'use strict'; + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + const eventPromise = new Promise((resolve) => { + cookieStore.onchange = resolve; + }); + + await cookieStore.set('cookie-name', 'new-cookie-value'); + + const event = await eventPromise; + assert_true(event instanceof CookieChangeEvent); + assert_equals(event.type, 'change'); + assert_equals(event.changed.length, 1); + assert_equals(event.changed[0].name, 'cookie-name'); + assert_equals(event.changed[0].value, 'new-cookie-value'); + assert_equals(event.deleted.length, 0); +}, 'cookieStore fires change event for cookie overwritten by cookieStore.set()'); diff --git a/testing/web-platform/tests/cookie-store/cookieStore_getAll_arguments.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_getAll_arguments.https.any.js new file mode 100644 index 0000000000..5055a42e5d --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStore_getAll_arguments.https.any.js @@ -0,0 +1,149 @@ +// META: title=Cookie Store API: cookieStore.getAll() arguments +// META: global=window,serviceworker + +'use strict'; + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + await cookieStore.set('cookie-name-2', 'cookie-value-2'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name-2'); + }); + + const cookies = await cookieStore.getAll(); + cookies.sort((a, b) => a.name.localeCompare(b.name)); + assert_equals(cookies.length, 2); + assert_equals(cookies[0].name, 'cookie-name'); + assert_equals(cookies[0].value, 'cookie-value'); + assert_equals(cookies[1].name, 'cookie-name-2'); + assert_equals(cookies[1].value, 'cookie-value-2'); +}, 'cookieStore.getAll with no arguments'); + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + await cookieStore.set('cookie-name-2', 'cookie-value-2'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name-2'); + }); + + const cookies = await cookieStore.getAll({}); + cookies.sort((a, b) => a.name.localeCompare(b.name)); + assert_equals(cookies.length, 2); + assert_equals(cookies[0].name, 'cookie-name'); + assert_equals(cookies[0].value, 'cookie-value'); + assert_equals(cookies[1].name, 'cookie-name-2'); + assert_equals(cookies[1].value, 'cookie-value-2'); +}, 'cookieStore.getAll with empty options'); + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + await cookieStore.set('cookie-name-2', 'cookie-value-2'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name-2'); + }); + + const cookies = await cookieStore.getAll('cookie-name'); + assert_equals(cookies.length, 1); + assert_equals(cookies[0].name, 'cookie-name'); + assert_equals(cookies[0].value, 'cookie-value'); +}, 'cookieStore.getAll with positional name'); + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + await cookieStore.set('cookie-name-2', 'cookie-value-2'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name-2'); + }); + + const cookies = await cookieStore.getAll({ name: 'cookie-name' }); + assert_equals(cookies.length, 1); + assert_equals(cookies[0].name, 'cookie-name'); + assert_equals(cookies[0].value, 'cookie-value'); +}, 'cookieStore.getAll with name in options'); + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + await cookieStore.set('cookie-name-2', 'cookie-value-2'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name-2'); + }); + + const cookies = await cookieStore.getAll('cookie-name', + { name: 'wrong-cookie-name' }); + assert_equals(cookies.length, 1); + assert_equals(cookies[0].name, 'cookie-name'); + assert_equals(cookies[0].value, 'cookie-value'); +}, 'cookieStore.getAll with name in both positional arguments and options'); + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + let target_url = self.location.href; + if (self.GLOBAL.isWorker()) { + target_url = target_url + '/path/within/scope'; + } + + const cookies = await cookieStore.getAll({ url: target_url }); + assert_equals(cookies.length, 1); + assert_equals(cookies[0].name, 'cookie-name'); + assert_equals(cookies[0].value, 'cookie-value'); +}, 'cookieStore.getAll with absolute url in options'); + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + let target_path = self.location.pathname; + if (self.GLOBAL.isWorker()) { + target_path = target_path + '/path/within/scope'; + } + + const cookies = await cookieStore.getAll({ url: target_path }); + assert_equals(cookies.length, 1); + assert_equals(cookies[0].name, 'cookie-name'); + assert_equals(cookies[0].value, 'cookie-value'); +}, 'cookieStore.getAll with relative url in options'); + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + const invalid_url = + `${self.location.protocol}//${self.location.host}/different/path`; + await promise_rejects_js(testCase, TypeError, cookieStore.getAll( + { url: invalid_url })); +}, 'cookieStore.getAll with invalid url path in options'); + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + const invalid_url = + `${self.location.protocol}//www.example.com${self.location.pathname}`; + await promise_rejects_js(testCase, TypeError, cookieStore.getAll( + { url: invalid_url })); +}, 'cookieStore.getAll with invalid url host in options'); diff --git a/testing/web-platform/tests/cookie-store/cookieStore_getAll_multiple.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_getAll_multiple.https.any.js new file mode 100644 index 0000000000..10dcacbd68 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStore_getAll_multiple.https.any.js @@ -0,0 +1,29 @@ +// META: title=Cookie Store API: cookieStore.getAll() with multiple cookies +// META: global=window,serviceworker + +'use strict'; + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + await cookieStore.set('cookie-name-2', 'cookie-value-2'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name-2'); + }); + await cookieStore.set('cookie-name-3', 'cookie-value-3'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name-3'); + }); + + const cookies = await cookieStore.getAll(); + cookies.sort((a, b) => a.name.localeCompare(b.name)); + assert_equals(cookies.length, 3); + assert_equals(cookies[0].name, 'cookie-name'); + assert_equals(cookies[0].value, 'cookie-value'); + assert_equals(cookies[1].name, 'cookie-name-2'); + assert_equals(cookies[1].value, 'cookie-value-2'); + assert_equals(cookies[2].name, 'cookie-name-3'); + assert_equals(cookies[2].value, 'cookie-value-3'); +}, 'cookieStore.getAll returns multiple cookies written by cookieStore.set'); diff --git a/testing/web-platform/tests/cookie-store/cookieStore_getAll_set_basic.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_getAll_set_basic.https.any.js new file mode 100644 index 0000000000..dee78e1867 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStore_getAll_set_basic.https.any.js @@ -0,0 +1,16 @@ +// META: title=Cookie Store API: Interaction between cookieStore.set() and cookieStore.getAll() +// META: global=window,serviceworker + +'use strict'; + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + const cookies = await cookieStore.getAll('cookie-name'); + + assert_equals(cookies.length, 1); + assert_equals(cookies[0].name, 'cookie-name'); + assert_equals(cookies[0].value, 'cookie-value'); +}, 'cookieStore.getAll returns the cookie written by cookieStore.set'); diff --git a/testing/web-platform/tests/cookie-store/cookieStore_get_arguments.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_get_arguments.https.any.js new file mode 100644 index 0000000000..a56032f03e --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStore_get_arguments.https.any.js @@ -0,0 +1,102 @@ +// META: title=Cookie Store API: cookieStore.get() arguments +// META: global=window,serviceworker + +'use strict'; + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + await promise_rejects_js(testCase, TypeError, cookieStore.get()); +}, 'cookieStore.get with no arguments returns TypeError'); + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + await promise_rejects_js(testCase, TypeError, cookieStore.get({})); +},'cookieStore.get with empty options returns TypeError'); + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); +}, 'cookieStore.get with positional name'); + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + const cookie = await cookieStore.get({ name: 'cookie-name' }); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); +}, 'cookieStore.get with name in options'); + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + const cookie = await cookieStore.get('cookie-name', + { name: 'wrong-cookie-name' }); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); +}, 'cookieStore.get with name in both positional arguments and options'); + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + let target_url = self.location.href; + if (self.GLOBAL.isWorker()) { + target_url = target_url + '/path/within/scope'; + } + + const cookie = await cookieStore.get({ url: target_url }); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); +}, 'cookieStore.get with absolute url in options'); + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + let target_path = self.location.pathname; + if (self.GLOBAL.isWorker()) { + target_path = target_path + '/path/within/scope'; + } + + const cookie = await cookieStore.get({ url: target_path }); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); +}, 'cookieStore.get with relative url in options'); + +promise_test(async testCase => { + const invalid_url = + `${self.location.protocol}//${self.location.host}/different/path`; + await promise_rejects_js(testCase, TypeError, cookieStore.get( + { url: invalid_url })); +}, 'cookieStore.get with invalid url path in options'); + +promise_test(async testCase => { + const invalid_url = + `${self.location.protocol}//www.example.com${self.location.pathname}`; + await promise_rejects_js(testCase, TypeError, cookieStore.get( + { url: invalid_url })); +}, 'cookieStore.get with invalid url host in options'); diff --git a/testing/web-platform/tests/cookie-store/cookieStore_get_delete_basic.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_get_delete_basic.https.any.js new file mode 100644 index 0000000000..9337669afd --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStore_get_delete_basic.https.any.js @@ -0,0 +1,14 @@ +// META: title=Cookie Store API: Interaction between cookieStore.set() and cookieStore.delete() +// META: global=window,serviceworker + +'use strict'; + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + await cookieStore.delete('cookie-name'); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie, null); +}, 'cookieStore.get returns null for a cookie deleted by cookieStore.delete'); diff --git a/testing/web-platform/tests/cookie-store/cookieStore_get_set_across_frames.https.html b/testing/web-platform/tests/cookie-store/cookieStore_get_set_across_frames.https.html new file mode 100644 index 0000000000..f7c737b422 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStore_get_set_across_frames.https.html @@ -0,0 +1,46 @@ +<!doctype html> +<meta charset='utf-8'> +<title>Async Cookies: cookieStore basic API across frames</title> +<link rel='help' href='https://github.com/WICG/cookie-store'> +<link rel='author' href='jarrydg@chromium.org' title='Jarryd Goodman'> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<style>iframe { display: none; }</style> +<iframe id='iframe'></iframe> +<script> +'use strict'; + +promise_test(async t => { + const iframe = document.getElementById('iframe'); + const frameCookieStore = iframe.contentWindow.cookieStore; + + const oldCookie = await frameCookieStore.get('cookie-name'); + assert_equals(oldCookie, null, + 'Precondition not met: cookie store should be empty'); + + await cookieStore.set('cookie-name', 'cookie-value'); + t.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + const frameCookie = await frameCookieStore.get('cookie-name'); + assert_equals(frameCookie.value, 'cookie-value'); +}, 'cookieStore.get() sees cookieStore.set() in frame'); + +promise_test(async t => { + const iframe = document.getElementById('iframe'); + const frameCookieStore = iframe.contentWindow.cookieStore; + + const oldCookie = await frameCookieStore.get('cookie-name'); + assert_equals(oldCookie, null, + 'Precondition not met: cookie store should be empty'); + + await frameCookieStore.set('cookie-name', 'cookie-value'); + t.add_cleanup(async () => { + await frameCookieStore.delete('cookie-name'); + }); + + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie.value, 'cookie-value'); +}, 'cookieStore.get() in frame sees cookieStore.set()') +</script> diff --git a/testing/web-platform/tests/cookie-store/cookieStore_get_set_across_origins.sub.https.html b/testing/web-platform/tests/cookie-store/cookieStore_get_set_across_origins.sub.https.html new file mode 100644 index 0000000000..c67ef98bcc --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStore_get_set_across_origins.sub.https.html @@ -0,0 +1,66 @@ +<!doctype html> +<meta charset='utf-8'> +<title>Async Cookies: cookieStore basic API across origins</title> +<link rel='help' href='https://github.com/WICG/cookie-store'> +<link rel='author' href='jarrydg@chromium.org' title='Jarryd Goodman'> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='resources/helpers.js'></script> +<style>iframe { display: none; }</style> + +<script> +'use strict'; + +const kPath = '/cookie-store/resources/helper_iframe.sub.html'; +const kCorsBase = `https://{{domains[www1]}}:{{ports[https][0]}}`; +const kCorsUrl = `${kCorsBase}${kPath}`; + +promise_test(async t => { + const iframe = await createIframe(kCorsUrl, t); + assert_true(iframe != null); + + iframe.contentWindow.postMessage({ + opname: 'set-cookie', + name: 'cookie-name', + value: 'cookie-value', + }, kCorsBase); + t.add_cleanup(async () => { + await cookieStore.delete({ name: 'cookie-name', domain: '{{host}}' }); + }); + await waitForMessage(); + + const cookies = await cookieStore.getAll(); + assert_equals(cookies.length, 1); + assert_equals(cookies[0].name, 'cookie-name'); + assert_equals(cookies[0].value, 'cookie-value'); +}, 'cookieStore.get() sees cookieStore.set() in cross-origin frame'); + +promise_test(async t => { + const iframe = await createIframe(kCorsUrl, t); + assert_true(iframe != null); + + await cookieStore.set({ + name: 'cookie-name', + value: 'cookie-value', + domain: '{{host}}', + }); + + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie.value, 'cookie-value'); + + iframe.contentWindow.postMessage({ + opname: 'get-cookie', + name: 'cookie-name', + }, kCorsBase); + t.add_cleanup(async () => { + await cookieStore.delete({ name: 'cookie-name', domain: '{{host}}' }); + }); + + const message = await waitForMessage(); + + const { frameCookie } = message; + assert_not_equals(frameCookie, null); + assert_equals(frameCookie.name, 'cookie-name'); + assert_equals(frameCookie.value, 'cookie-value'); +}, 'cookieStore.get() in cross-origin frame sees cookieStore.set()'); +</script> diff --git a/testing/web-platform/tests/cookie-store/cookieStore_get_set_basic.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_get_set_basic.https.any.js new file mode 100644 index 0000000000..127f758f5f --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStore_get_set_basic.https.any.js @@ -0,0 +1,15 @@ +// META: title=Cookie Store API: Interaction between cookieStore.set() and cookieStore.get() +// META: global=window,serviceworker + +'use strict'; + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + const cookie = await cookieStore.get('cookie-name'); + + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); +}, 'cookieStore.get returns the cookie written by cookieStore.set'); diff --git a/testing/web-platform/tests/cookie-store/cookieStore_get_set_ordering.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_get_set_ordering.https.any.js new file mode 100644 index 0000000000..6b7e73950c --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStore_get_set_ordering.https.any.js @@ -0,0 +1,42 @@ +// META: title=Cookie Store API: Cookie ordering +// META: global=window,serviceworker + +'use strict'; + +promise_test(async t => { + await cookieStore.set('ordered-1', 'cookie-value1'); + await cookieStore.set('ordered-2', 'cookie-value2'); + await cookieStore.set('ordered-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. + const matchingValues = await Promise.all(['1', '2', '3'].map( + async suffix => (await cookieStore.get('ordered-' + suffix)).value)); + const actual = matchingValues.join(';'); + const expected = 'cookie-value1;cookie-value2;cookie-value3'; + assert_equals(actual, expected); +}, 'Set three simple origin session cookies sequentially and ensure ' + + 'they all end up in the cookie jar in order.'); + +promise_test(async t => { + await Promise.all([ + cookieStore.set('ordered-unordered1', 'unordered-cookie-value1'), + cookieStore.set('ordered-unordered2', 'unordered-cookie-value2'), + cookieStore.set('ordered-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. + const matchingCookies = await Promise.all(['1', '2', '3'].map( + suffix => cookieStore.get('ordered-unordered' + suffix))); + const actual = matchingCookies.map(({ value }) => value).join(';'); + const expected = + 'unordered-cookie-value1;' + + 'unordered-cookie-value2;' + + 'unordered-cookie-value3'; + assert_equals(actual, expected); +}, '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. '); diff --git a/testing/web-platform/tests/cookie-store/cookieStore_in_detached_frame.https.html b/testing/web-platform/tests/cookie-store/cookieStore_in_detached_frame.https.html new file mode 100644 index 0000000000..08a7b5b8e4 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStore_in_detached_frame.https.html @@ -0,0 +1,19 @@ +<!doctype html> +<meta charset="utf-8"> +<title>cookieStore on DOMWindow of detached iframe (crbug.com/774626)</title> +<link rel="help" href="https://github.com/WICG/cookie-store"> +<link rel="author" href="pwnall@chromium.org" title="Victor Costan"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<iframe id="iframe"></iframe> +<script> +'use strict'; + +test(() => { + const iframe = document.getElementById('iframe'); + const frameWindow = iframe.contentWindow; + + iframe.parentNode.removeChild(iframe); + assert_equals(null, frameWindow.cookieStore); +}); +</script> diff --git a/testing/web-platform/tests/cookie-store/cookieStore_opaque_origin.https.html b/testing/web-platform/tests/cookie-store/cookieStore_opaque_origin.https.html new file mode 100644 index 0000000000..94a13fe63f --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStore_opaque_origin.https.html @@ -0,0 +1,73 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Cookie Store API: Opaque origins for cookieStore</title> +<link rel=help href="https://wicg.github.io/cookie-store/"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + +const apiCalls = { + 'get': 'cookieStore.get("cookie-name")', + 'getAll': 'cookieStore.getAll()', + 'set': 'cookieStore.set("cookie-name", "cookie-value")', + 'delete': 'cookieStore.delete("cookie-name")' +}; + +const script = ` +<script> + "use strict"; + window.onmessage = async () => { + try { + await %s; + window.parent.postMessage({result: "no exception"}, "*"); + } catch (ex) { + window.parent.postMessage({result: ex.name}, "*"); + }; + }; +<\/script> +`; + +function load_iframe(apiCall, sandbox) { + return new Promise(resolve => { + const iframe = document.createElement('iframe'); + iframe.onload = () => { resolve(iframe); }; + if (sandbox) + iframe.sandbox = sandbox; + iframe.srcdoc = script.replace("%s", apiCalls[apiCall]); + iframe.style.display = 'none'; + document.documentElement.appendChild(iframe); + }); +} + +function wait_for_message(iframe) { + return new Promise(resolve => { + self.addEventListener('message', function listener(e) { + if (e.source === iframe.contentWindow) { + resolve(e.data); + self.removeEventListener('message', listener); + } + }); + }); +} + +promise_test(async t => { + for (apiCall in apiCalls) { + const iframe = await load_iframe(apiCall); + iframe.contentWindow.postMessage({}, '*'); + const message = await wait_for_message(iframe); + assert_equals(message.result, 'no exception', + 'cookieStore ${apiCall} should not throw'); + } +}, 'cookieStore in non-sandboxed iframe should not throw'); + +promise_test(async t => { + for (apiCall in apiCalls) { + const iframe = await load_iframe(apiCall, 'allow-scripts'); + iframe.contentWindow.postMessage({}, '*'); + const message = await wait_for_message(iframe); + assert_equals(message.result, 'SecurityError', + 'cookieStore ${apiCall} should throw SecurityError'); + } +}, 'cookieStore in sandboxed iframe should throw SecurityError'); + +</script> diff --git a/testing/web-platform/tests/cookie-store/cookieStore_set_arguments.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_set_arguments.https.any.js new file mode 100644 index 0000000000..aab964d014 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStore_set_arguments.https.any.js @@ -0,0 +1,287 @@ +// META: title=Cookie Store API: cookieStore.set() arguments +// META: global=window,serviceworker + +'use strict'; + +promise_test(async testCase => { + await cookieStore.delete('cookie-name'); + + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); +}, 'cookieStore.set with positional name and value'); + +promise_test(async testCase => { + await cookieStore.delete('cookie-name'); + + await cookieStore.set({ name: 'cookie-name', value: 'cookie-value' }); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); +}, 'cookieStore.set with name and value in options'); + +promise_test(async testCase => { + await promise_rejects_js(testCase, TypeError, + cookieStore.set('', 'suspicious-value=resembles-name-and-value')); +}, "cookieStore.set with empty name and an '=' in value"); + +promise_test(async testCase => { + await cookieStore.delete('cookie-name'); + cookieStore.set('cookie-name', 'suspicious-value=resembles-name-and-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'suspicious-value=resembles-name-and-value'); +}, "cookieStore.set with normal name and an '=' in value"); + +promise_test(async testCase => { + const tenYears = 10 * 365 * 24 * 60 * 60 * 1000; + const tenYearsFromNow = Date.now() + tenYears; + await cookieStore.delete('cookie-name'); + + await cookieStore.set( + { name: 'cookie-name', + value: 'cookie-value', + expires: new Date(tenYearsFromNow) }); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); +}, 'cookieStore.set with expires set to a future Date'); + +promise_test(async testCase => { + const tenYears = 10 * 365 * 24 * 60 * 60 * 1000; + const tenYearsAgo = Date.now() - tenYears; + await cookieStore.delete('cookie-name'); + + await cookieStore.set( + { name :'cookie-name', + value: 'cookie-value', + expires: new Date(tenYearsAgo) }); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie, null); +}, 'cookieStore.set with expires set to a past Date'); + +promise_test(async testCase => { + const tenYears = 10 * 365 * 24 * 60 * 60 * 1000; + const tenYearsFromNow = Date.now() + tenYears; + await cookieStore.delete('cookie-name'); + + await cookieStore.set( + { name: 'cookie-name', value: 'cookie-value', expires: tenYearsFromNow }); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); +}, 'cookieStore.set with expires set to a future timestamp'); + +promise_test(async testCase => { + const tenYears = 10 * 365 * 24 * 60 * 60 * 1000; + const tenYearsAgo = Date.now() - tenYears; + await cookieStore.delete('cookie-name'); + + await cookieStore.set( + { name: 'cookie-name', value: 'cookie-value', expires: tenYearsAgo }); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie, null); +}, 'cookieStore.set with expires set to a past timestamp'); + +promise_test(async testCase => { + const currentUrl = new URL(self.location.href); + const currentDomain = currentUrl.hostname; + + await promise_rejects_js(testCase, TypeError, cookieStore.set( + { name: 'cookie-name', + value: 'cookie-value', + domain: `.${currentDomain}` })); +}, 'cookieStore.set domain starts with "."'); + +promise_test(async testCase => { + await promise_rejects_js(testCase, TypeError, cookieStore.set( + { name: 'cookie-name', value: 'cookie-value', domain: 'example.com' })); +}, 'cookieStore.set with domain that is not equal current host'); + +promise_test(async testCase => { + const currentUrl = new URL(self.location.href); + const currentDomain = currentUrl.hostname; + await cookieStore.delete({ name: 'cookie-name', domain: currentDomain }); + + await cookieStore.set( + { name: 'cookie-name', value: 'cookie-value', domain: currentDomain }); + testCase.add_cleanup(async () => { + await cookieStore.delete({ name: 'cookie-name', domain: currentDomain }); + }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); +}, 'cookieStore.set with domain set to the current hostname'); + +promise_test(async testCase => { + const currentUrl = new URL(self.location.href); + const currentDomain = currentUrl.hostname; + const subDomain = `sub.${currentDomain}`; + + await promise_rejects_js(testCase, TypeError, cookieStore.set( + { name: 'cookie-name', value: 'cookie-value', domain: subDomain })); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie, null); +}, 'cookieStore.set with domain set to a subdomain of the current hostname'); + +promise_test(async testCase => { + const currentUrl = new URL(self.location.href); + const currentDomain = currentUrl.hostname; + assert_not_equals(currentDomain[0] === '.', + 'this test assumes that the current hostname does not start with .'); + const domainSuffix = currentDomain.substr(1); + + await promise_rejects_js(testCase, TypeError, cookieStore.set( + { name: 'cookie-name', value: 'cookie-value', domain: domainSuffix })); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie, null); +}, 'cookieStore.set with domain set to a non-domain-matching suffix of the ' + + 'current hostname'); + +promise_test(async testCase => { + const currentUrl = new URL(self.location.href); + const currentDomain = currentUrl.hostname; + await cookieStore.delete('cookie-name'); + + await cookieStore.set('cookie-name', 'cookie-value1'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + await cookieStore.set( + { name: 'cookie-name', value: 'cookie-value2', domain: currentDomain }); + testCase.add_cleanup(async () => { + await cookieStore.delete({ name: 'cookie-name', domain: currentDomain }); + }); + + const cookies = await cookieStore.getAll('cookie-name'); + assert_equals(cookies.length, 2); + + assert_equals(cookies[0].name, 'cookie-name'); + assert_equals(cookies[1].name, 'cookie-name'); + + const values = cookies.map((cookie) => cookie.value); + values.sort(); + assert_array_equals(values, ['cookie-value1', 'cookie-value2']); +}, 'cookieStore.set default domain is null and differs from current hostname'); + +promise_test(async testCase => { + const currentUrl = new URL(self.location.href); + const currentPath = currentUrl.pathname; + const currentDirectory = + currentPath.substr(0, currentPath.lastIndexOf('/') + 1); + await cookieStore.delete({ name: 'cookie-name', path: currentDirectory }); + + await cookieStore.set( + { name: 'cookie-name', value: 'cookie-value', path: currentDirectory }); + testCase.add_cleanup(async () => { + await cookieStore.delete({ name: 'cookie-name', path: currentDirectory }); + }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); +}, 'cookieStore.set with path set to the current directory'); + +promise_test(async testCase => { + const currentUrl = new URL(self.location.href); + const currentPath = currentUrl.pathname; + const currentDirectory = + currentPath.substr(0, currentPath.lastIndexOf('/') + 1); + const subDirectory = currentDirectory + "subdir/"; + await cookieStore.delete({ name: 'cookie-name', path: currentDirectory }); + await cookieStore.delete({ name: 'cookie-name', path: subDirectory }); + + await cookieStore.set( + { name: 'cookie-name', value: 'cookie-value', path: subDirectory }); + testCase.add_cleanup(async () => { + await cookieStore.delete({ name: 'cookie-name', path: subDirectory }); + }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie, null); +}, 'cookieStore.set with path set to a subdirectory of the current directory'); + +promise_test(async testCase => { + await cookieStore.delete('cookie-name'); + + await cookieStore.set('cookie-name', 'cookie-old-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + await cookieStore.set( + { name: 'cookie-name', value: 'cookie-new-value', path: '/' }); + testCase.add_cleanup(async () => { + await cookieStore.delete({ name: 'cookie-name', path: '/' }); + }); + + const cookies = await cookieStore.getAll('cookie-name'); + assert_equals(cookies.length, 1); + assert_equals(cookies[0].name, 'cookie-name'); + assert_equals(cookies[0].value, 'cookie-new-value'); +}, 'cookieStore.set default path is /'); + +promise_test(async testCase => { + const currentUrl = new URL(self.location.href); + const currentPath = currentUrl.pathname; + const currentDirectory = currentPath.substr(0, currentPath.lastIndexOf('/')); + await cookieStore.delete({ name: 'cookie-name', path: currentDirectory }); + + await cookieStore.set( + { name: 'cookie-name', value: 'cookie-value', path: currentDirectory }); + testCase.add_cleanup(async () => { + await cookieStore.delete({ name: 'cookie-name', path: currentDirectory }); + }); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'cookie-value'); + assert_equals(cookie.path, currentDirectory + '/'); +}, 'cookieStore.set adds / to path that does not end with /'); + +promise_test(async testCase => { + const currentUrl = new URL(self.location.href); + const currentPath = currentUrl.pathname; + const currentDirectory = + currentPath.substr(0, currentPath.lastIndexOf('/') + 1); + const invalidPath = currentDirectory.substr(1); + + await promise_rejects_js(testCase, TypeError, cookieStore.set( + { name: 'cookie-name', value: 'cookie-value', path: invalidPath })); +}, 'cookieStore.set with path that does not start with /'); + +promise_test(async testCase => { + await cookieStore.set('cookie-name', 'old-cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + const cookie_attributes = await cookieStore.get('cookie-name'); + assert_equals(cookie_attributes.name, 'cookie-name'); + assert_equals(cookie_attributes.value, 'old-cookie-value'); + + cookie_attributes.value = 'new-cookie-value'; + await cookieStore.set(cookie_attributes); + const cookie = await cookieStore.get('cookie-name'); + assert_equals(cookie.name, 'cookie-name'); + assert_equals(cookie.value, 'new-cookie-value'); +}, 'cookieStore.set with get result'); diff --git a/testing/web-platform/tests/cookie-store/cookieStore_special_names.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_special_names.https.any.js new file mode 100644 index 0000000000..6b18c4f066 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStore_special_names.https.any.js @@ -0,0 +1,64 @@ +// META: title=Cookie Store API: cookieStore.set()/get()/delete() for cookies with special names +// META: global=window,serviceworker + +'use strict'; + +['__Secure-', '__Host-'].forEach(prefix => { + promise_test(async testCase => { + await cookieStore.set(`${prefix}cookie-name`, `secure-cookie-value`); + assert_equals( + (await cookieStore.get(`${prefix}cookie-name`)).value, + 'secure-cookie-value', + `Setting ${prefix} cookies should not fail in secure context`); + + try { await cookieStore.delete(`${prefix}cookie-name`); } catch (e) {} + }, `cookieStore.set with ${prefix} name on secure origin`); + + promise_test(async testCase => { + // This test is for symmetry with the non-secure case. In non-secure + // contexts, the set() should fail even if the expiration date makes + // the operation a no-op. + await cookieStore.set( + { name: `${prefix}cookie-name`, value: `secure-cookie-value`, + expires: Date.now() - (24 * 60 * 60 * 1000)}); + assert_equals(await cookieStore.get(`${prefix}cookie-name`), null); + try { await cookieStore.delete(`${prefix}cookie-name`); } catch (e) {} + }, `cookieStore.set of expired ${prefix} cookie name on secure origin`); + + promise_test(async testCase => { + assert_equals( + await cookieStore.delete(`${prefix}cookie-name`), undefined, + `Deleting ${prefix} cookies should not fail in secure context`); + }, `cookieStore.delete with ${prefix} name on secure origin`); +}); + +promise_test(async testCase => { + const currentUrl = new URL(self.location.href); + const currentDomain = currentUrl.hostname; + await promise_rejects_js(testCase, TypeError, + cookieStore.set({ name: '__Host-cookie-name', value: 'cookie-value', + domain: currentDomain })); +}, 'cookieStore.set with __Host- prefix and a domain option'); + +promise_test(async testCase => { + await cookieStore.set({ name: '__Host-cookie-name', value: 'cookie-value', + path: "/" }); + + assert_equals( + (await cookieStore.get(`__Host-cookie-name`)).value, "cookie-value"); + + await promise_rejects_js(testCase, TypeError, + cookieStore.set( { name: '__Host-cookie-name', value: 'cookie-value', + path: "/path" })); +}, 'cookieStore.set with __Host- prefix a path option'); + +promise_test(async testCase => { + let exceptionThrown = false; + try { + await cookieStore.set(unescape('cookie-name%0D1'), 'cookie-value'); + } catch (e) { + assert_equals (e.name, "TypeError", "cookieStore thrown an incorrect exception -"); + exceptionThrown = true; + } + assert_true(exceptionThrown, "No exception thrown."); +}, 'cookieStore.set with malformed name.'); diff --git a/testing/web-platform/tests/cookie-store/cookieStore_subscribe_arguments.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_subscribe_arguments.https.any.js new file mode 100644 index 0000000000..ca5f55d645 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStore_subscribe_arguments.https.any.js @@ -0,0 +1,142 @@ +// META: title=Cookie Store API: cookieStore.subscribe() arguments +// META: global=window,serviceworker +// META: script=/service-workers/service-worker/resources/test-helpers.sub.js + +'use strict'; + +promise_test(async testCase => { + if (self.GLOBAL.isWindow()) { + const registration = await service_worker_unregister_and_register( + testCase, 'resources/empty_sw.js', + '/cookie-store/resources/does/not/exist'); + testCase.add_cleanup(() => registration.unregister()); + + // Must wait for the service worker to enter the 'activated' state before + // subscribing to cookiechange events. + await wait_for_state(testCase, registration.installing, 'activated'); + + self.registration = registration; + } else { + // Must wait for the service worker to enter the 'activated' state before + // subscribing to cookiechange events. + await new Promise(resolve => { + self.addEventListener('activate', event => { resolve(); }); + }); + } + + { + const subscriptions = [{ name: 'cookie-name' }]; + await self.registration.cookies.subscribe(subscriptions); + testCase.add_cleanup(() => registration.cookies.unsubscribe(subscriptions)); + } + + const subscriptions = await registration.cookies.getSubscriptions(); + assert_equals(subscriptions.length, 1); + + assert_equals(subscriptions[0].name, 'cookie-name'); + assert_equals(subscriptions[0].url, registration.scope); +}, 'cookieStore.subscribe without url in option'); + +promise_test(async testCase => { + if (self.GLOBAL.isWindow()) { + const registration = await service_worker_unregister_and_register( + testCase, 'resources/empty_sw.js', + '/cookie-store/resources/does/not/exist'); + testCase.add_cleanup(() => registration.unregister()); + + // Must wait for the service worker to enter the 'activated' state before + // subscribing to cookiechange events. + await wait_for_state(testCase, registration.installing, 'activated'); + + self.registration = registration; + } else if (!self.registration.active) { + // If service worker is not active yet, it must wait for it to enter the + // 'activated' state before subscribing to cookiechange events. + await new Promise(resolve => { + self.addEventListener('activate', event => { resolve(); }); + }); + } + + await promise_rejects_js(testCase, TypeError, + registration.cookies.subscribe( + { name: 'cookie-name', url: '/wrong/path' })); +}, 'cookieStore.subscribe with invalid url path in option'); + +promise_test(async testCase => { + if (self.GLOBAL.isWindow()) { + const registration = await service_worker_unregister_and_register( + testCase, 'resources/empty_sw.js', + '/cookie-store/resources/does/not/exist'); + testCase.add_cleanup(() => registration.unregister()); + + // Must wait for the service worker to enter the 'activated' state before + // subscribing to cookiechange events. + await wait_for_state(testCase, registration.installing, 'activated'); + + self.registration = registration; + } else if (!self.registration.active) { + // Must wait for the service worker to enter the 'activated' state before + // subscribing to cookiechange events. + await new Promise(resolve => { + self.addEventListener('activate', event => { resolve(); }); + }); + } + + { + const subscriptions = [{ name: 'cookie-name' }]; + // Call subscribe for same subscription multiple times to verify that it is + // idempotent. + await self.registration.cookies.subscribe(subscriptions); + await self.registration.cookies.subscribe(subscriptions); + await self.registration.cookies.subscribe(subscriptions); + testCase.add_cleanup(() => registration.cookies.unsubscribe(subscriptions)); + } + + const subscriptions = await registration.cookies.getSubscriptions(); + assert_equals(subscriptions.length, 1); + + assert_equals(subscriptions[0].name, 'cookie-name'); + assert_equals(subscriptions[0].url, registration.scope); +}, 'cookieStore.subscribe is idempotent'); + +promise_test(async testCase => { + if (self.GLOBAL.isWindow()) { + const registration = await service_worker_unregister_and_register( + testCase, 'resources/empty_sw.js', + '/cookie-store/resources/does/not/exist'); + testCase.add_cleanup(() => registration.unregister()); + + // Must wait for the service worker to enter the 'activated' state before + // subscribing to cookiechange events. + await wait_for_state(testCase, registration.installing, 'activated'); + + self.registration = registration; + } else if (!self.registration.active) { + // Must wait for the service worker to enter the 'activated' state before + // subscribing to cookiechange events. + await new Promise(resolve => { + self.addEventListener('activate', event => { resolve(); }); + }); + } + + { + const subscriptions = [ + { name: 'cookie-name1' }, + { name: 'cookie-name2' }, + ]; + await self.registration.cookies.subscribe(subscriptions); + testCase.add_cleanup(() => registration.cookies.unsubscribe(subscriptions)); + + // Call unsubscribe for same subscription multiple times to verify that it + // is idempotent. + await registration.cookies.unsubscribe([subscriptions[0]]); + await registration.cookies.unsubscribe([subscriptions[0]]); + await registration.cookies.unsubscribe([subscriptions[0]]); + } + + const subscriptions = await registration.cookies.getSubscriptions(); + assert_equals(subscriptions.length, 1); + + assert_equals(subscriptions[0].name, 'cookie-name2'); + assert_equals(subscriptions[0].url, registration.scope); +}, 'CookieStore.unsubscribe is idempotent'); diff --git a/testing/web-platform/tests/cookie-store/cookieStore_subscriptions_empty.https.window.js b/testing/web-platform/tests/cookie-store/cookieStore_subscriptions_empty.https.window.js new file mode 100644 index 0000000000..907a34b4de --- /dev/null +++ b/testing/web-platform/tests/cookie-store/cookieStore_subscriptions_empty.https.window.js @@ -0,0 +1,13 @@ +// META: script=/service-workers/service-worker/resources/test-helpers.sub.js + +'use strict'; + +promise_test(async testCase => { + const registration = await service_worker_unregister_and_register( + testCase, 'resources/empty_sw.js', 'resources/does/not/exist'); + testCase.add_cleanup(() => registration.unregister()); + await wait_for_state(testCase, registration.installing, 'activated'); + + const subscriptions = await registration.cookies.getSubscriptions(); + assert_equals(0, subscriptions.length); +}, 'Newly registered and activated service worker has no subscriptions'); diff --git a/testing/web-platform/tests/cookie-store/encoding.https.any.js b/testing/web-platform/tests/cookie-store/encoding.https.any.js new file mode 100644 index 0000000000..941639bdae --- /dev/null +++ b/testing/web-platform/tests/cookie-store/encoding.https.any.js @@ -0,0 +1,19 @@ +// META: title=Cookie Store API: cookie encoding +// META: global=window,serviceworker +// META: script=resources/cookie-test-helpers.js + +'use strict'; + +cookie_test(async t => { + await setCookieStringHttp('\uFEFFcookie=value; path=/'); + const cookie = await cookieStore.get('\uFEFFcookie'); + assert_equals(cookie.name, '\uFEFFcookie'); + assert_equals(cookie.value, 'value'); +}, 'BOM not stripped from name'); + +cookie_test(async t => { + await setCookieStringHttp('cookie=\uFEFFvalue; path=/'); + const cookie = await cookieStore.get('cookie'); + assert_equals(cookie.name, 'cookie'); + assert_equals(cookie.value, '\uFEFFvalue'); +}, 'BOM not stripped from value'); diff --git a/testing/web-platform/tests/cookie-store/httponly_cookies.https.window.js b/testing/web-platform/tests/cookie-store/httponly_cookies.https.window.js new file mode 100644 index 0000000000..8a10e358ef --- /dev/null +++ b/testing/web-platform/tests/cookie-store/httponly_cookies.https.window.js @@ -0,0 +1,69 @@ +// META: script=resources/cookie-test-helpers.js + +'use strict'; + +cookie_test(async t => { + let eventPromise = observeNextCookieChangeEvent(); + await setCookieStringHttp('HTTPONLY-cookie=value; 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-cookie=value', + 'HttpOnly cookie we wrote using HTTP in HTTP cookie jar'); + + await setCookieStringHttp('HTTPONLY-cookie=new-value; path=/; httponly'); + assert_equals( + await getCookieString(), + undefined, + 'HttpOnly cookie we overwrote using HTTP in cookie jar' + + ' is invisible to script'); + assert_equals( + await getCookieStringHttp(), + 'HTTPONLY-cookie=new-value', + 'HttpOnly cookie we overwrote using HTTP in HTTP cookie jar'); + + eventPromise = observeNextCookieChangeEvent(); + await setCookieStringHttp( + 'HTTPONLY-cookie=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'); + + // HTTPONLY cookie changes should not have been observed; perform + // a dummy change to verify that nothing else was queued up. + await cookieStore.set('TEST', 'dummy'); + await verifyCookieChangeEvent( + eventPromise, {changed: [{name: 'TEST', value: 'dummy'}]}, + 'HttpOnly cookie deletion was not observed'); +}, 'HttpOnly cookies are not observed'); + + +cookie_test(async t => { + document.cookie = 'cookie1=value1; path=/'; + document.cookie = 'cookie2=value2; path=/; httponly'; + document.cookie = 'cookie3=value3; path=/'; + assert_equals( + await getCookieStringHttp(), 'cookie1=value1; cookie3=value3', + 'Trying to store an HttpOnly cookie with document.cookie fails'); +}, 'HttpOnly cookies can not be set by document.cookie'); + + +// Historical: Early iterations of the proposal included an httpOnly option. +cookie_test(async t => { + await cookieStore.set('cookie1', 'value1'); + await cookieStore.set('cookie2', 'value2', {httpOnly: true}); + await cookieStore.set('cookie3', 'value3'); + assert_equals( + await getCookieStringHttp(), + 'cookie1=value1; cookie2=value2; cookie3=value3', + 'httpOnly is not an option for CookieStore.set()'); +}, 'HttpOnly cookies can not be set by CookieStore'); diff --git a/testing/web-platform/tests/cookie-store/idlharness.tentative.https.any.js b/testing/web-platform/tests/cookie-store/idlharness.tentative.https.any.js new file mode 100644 index 0000000000..6312f3c4ba --- /dev/null +++ b/testing/web-platform/tests/cookie-store/idlharness.tentative.https.any.js @@ -0,0 +1,45 @@ +// META: global=window,worker +// META: timeout=long +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js +// META: script=/service-workers/service-worker/resources/test-helpers.sub.js +'use strict'; + +// https://wicg.github.io/cookie-store/ + +idl_test( + ['cookie-store'], + ['service-workers', 'html', 'dom'], + async (idl_array, t) => { + const isServiceWorker = 'ServiceWorkerGlobalScope' in self + && self instanceof ServiceWorkerGlobalScope; + + if (isServiceWorker) { + idl_array.add_objects({ + ExtendableCookieChangeEvent: [ + 'new ExtendableCookieChangeEvent("cookiechange")'], + ServiceWorkerGlobalScope: ['self'], + }); + } else if (self.GLOBAL.isWindow()) { + const registration = await service_worker_unregister_and_register( + t, 'resources/empty_sw.js', 'resources/does/not/exist'); + t.add_cleanup(() => registration.unregister()); + + // Global property referenced by idl_array.add_objects(). + self.registration = registration; + + idl_array.add_objects({ + CookieChangeEvent: ['new CookieChangeEvent("change")'], + Window: ['self'], + }); + } + + if (isServiceWorker || self.GLOBAL.isWindow()) { + idl_array.add_objects({ + CookieStore: ['self.cookieStore'], + CookieStoreManager: ['self.registration.cookies'], + ServiceWorkerRegistration: ['self.registration'], + }); + } + } +); diff --git a/testing/web-platform/tests/cookie-store/resources/always_changing_sw.sub.js b/testing/web-platform/tests/cookie-store/resources/always_changing_sw.sub.js new file mode 100644 index 0000000000..9fdf99848f --- /dev/null +++ b/testing/web-platform/tests/cookie-store/resources/always_changing_sw.sub.js @@ -0,0 +1,6 @@ +// This script changes every time it is fetched. + +// When used as a service worker script, this causes the Service Worker to be +// updated on every ServiceWorkerRegistration.update() call. + +// The following bytes change on every fetch: {{uuid()}} diff --git a/testing/web-platform/tests/cookie-store/resources/cookie-test-helpers.js b/testing/web-platform/tests/cookie-store/resources/cookie-test-helpers.js new file mode 100644 index 0000000000..178947ad6e --- /dev/null +++ b/testing/web-platform/tests/cookie-store/resources/cookie-test-helpers.js @@ -0,0 +1,226 @@ +'use strict'; + +// TODO(jsbell): Once ServiceWorker is supported, add arbitrary path coverage. +const kPath = location.pathname.replace(/[^/]+$/, ''); + +// True when running in a document context as opposed to a worker context +const kHasDocument = typeof document !== 'undefined'; + +// True when running on unsecured 'http:' rather than secured 'https:'. +const kIsUnsecured = location.protocol !== 'https:'; + +const kCookieHelperCgi = 'resources/cookie_helper.py'; + +// Approximate async equivalent to the document.cookie getter but with +// important differences: optional additional getAll arguments are +// forwarded, and an empty cookie jar returns undefined. +// +// This is intended primarily for verification against expected cookie +// jar contents. It should produce more readable messages using +// assert_equals in failing cases than assert_object_equals would +// using parsed cookie jar contents and also allows expectations to be +// written more compactly. +async function getCookieString(...args) { + const cookies = await cookieStore.getAll(...args); + return cookies.length + ? cookies.map(({name, value}) => + (name ? (name + '=') : '') + value).join('; ') + : undefined; +} + +// Approximate async equivalent to the document.cookie getter but from +// the server's point of view. Returns UTF-8 interpretation. Allows +// sub-path to be specified. +// +// Unlike document.cookie, this returns undefined when no cookies are +// present. +async function getCookieStringHttp(extraPath = null) { + const url = + kCookieHelperCgi + ((extraPath == null) ? '' : ('/' + extraPath)); + const response = await fetch(url, { credentials: 'include' }); + const text = await response.text(); + assert_equals( + response.ok, + true, + 'CGI should have succeeded in getCookieStringHttp\n' + text); + assert_equals( + response.headers.get('content-type'), + 'text/plain; charset=utf-8', + 'CGI did not return UTF-8 text in getCookieStringHttp'); + if (text === '') + return undefined; + assert_equals( + text.indexOf('cookie='), + 0, + 'CGI response did not begin with "cookie=" and was not empty: ' + text); + return decodeURIComponent(text.replace(/^cookie=/, '')); +} + +// Approximate async equivalent to the document.cookie getter but from +// the server's point of view. Returns binary string +// interpretation. Allows sub-path to be specified. +// +// Unlike document.cookie, this returns undefined when no cookies are +// present. +async function getCookieBinaryHttp(extraPath = null) { + const url = + kCookieHelperCgi + + ((extraPath == null) ? + '' : + ('/' + extraPath)) + '?charset=iso-8859-1'; + const response = await fetch(url, { credentials: 'include' }); + const text = await response.text(); + assert_equals( + response.ok, + true, + 'CGI should have succeeded in getCookieBinaryHttp\n' + text); + assert_equals( + response.headers.get('content-type'), + 'text/plain; charset=iso-8859-1', + 'CGI did not return ISO 8859-1 text in getCookieBinaryHttp'); + if (text === '') + return undefined; + assert_equals( + text.indexOf('cookie='), + 0, + 'CGI response did not begin with "cookie=" and was not empty: ' + text); + return unescape(text.replace(/^cookie=/, '')); +} + +// Approximate async equivalent to the document.cookie setter but from +// the server's point of view. +async function setCookieStringHttp(setCookie) { + const encodedSetCookie = encodeURIComponent(setCookie); + const url = kCookieHelperCgi; + const headers = new Headers(); + headers.set( + 'content-type', + 'application/x-www-form-urlencoded; charset=utf-8'); + const response = await fetch( + url, + { + credentials: 'include', + method: 'POST', + headers: headers, + body: 'set-cookie=' + encodedSetCookie, + }); + const text = await response.text(); + assert_equals( + response.ok, + true, + 'CGI should have succeeded in setCookieStringHttp set-cookie: ' + + setCookie + '\n' + text); + assert_equals( + response.headers.get('content-type'), + 'text/plain; charset=utf-8', + 'CGI did not return UTF-8 text in setCookieStringHttp'); + assert_equals( + text, + 'set-cookie=' + encodedSetCookie, + 'CGI did not faithfully echo the set-cookie value'); +} + +// Approximate async equivalent to the document.cookie setter but from +// the server's point of view. This version sets a binary cookie rather +// than a UTF-8 one. +async function setCookieBinaryHttp(setCookie) { + const encodedSetCookie = escape(setCookie).split('/').join('%2F'); + const url = kCookieHelperCgi + '?charset=iso-8859-1'; + const headers = new Headers(); + headers.set( + 'content-type', + 'application/x-www-form-urlencoded; charset=iso-8859-1'); + const response = await fetch(url, { + credentials: 'include', + method: 'POST', + headers: headers, + body: 'set-cookie=' + encodedSetCookie + }); + const text = await response.text(); + assert_equals( + response.ok, + true, + 'CGI should have succeeded in setCookieBinaryHttp set-cookie: ' + + setCookie + '\n' + text); + assert_equals( + response.headers.get('content-type'), + 'text/plain; charset=iso-8859-1', + 'CGI did not return Latin-1 text in setCookieBinaryHttp'); + assert_equals( + text, + 'set-cookie=' + encodedSetCookie, + 'CGI did not faithfully echo the set-cookie value'); +} + +// Async document.cookie getter; converts '' to undefined which loses +// information in the edge case where a single ''-valued anonymous +// cookie is visible. +async function getCookieStringDocument() { + if (!kHasDocument) + throw 'document.cookie not available in this context'; + return String(document.cookie || '') || undefined; +} + +// Async document.cookie setter +async function setCookieStringDocument(setCookie) { + if (!kHasDocument) + throw 'document.cookie not available in this context'; + document.cookie = setCookie; +} + +// Observe the next 'change' event on the cookieStore. Typical usage: +// +// const eventPromise = observeNextCookieChangeEvent(); +// await /* something that modifies cookies */ +// await verifyCookieChangeEvent( +// eventPromise, {changed: [{name: 'name', value: 'value'}]}); +// +function observeNextCookieChangeEvent() { + return new Promise(resolve => { + cookieStore.addEventListener('change', e => resolve(e), {once: true}); + }); +} + +async function verifyCookieChangeEvent(eventPromise, expected, description) { + description = description ? description + ': ' : ''; + expected = Object.assign({changed:[], deleted:[]}, expected); + const event = await eventPromise; + assert_equals(event.changed.length, expected.changed.length, + description + 'number of changed cookies'); + for (let i = 0; i < event.changed.length; ++i) { + assert_equals(event.changed[i].name, expected.changed[i].name, + description + 'changed cookie name'); + assert_equals(event.changed[i].value, expected.changed[i].value, + description + 'changed cookie value'); + } + assert_equals(event.deleted.length, expected.deleted.length, + description + 'number of deleted cookies'); + for (let i = 0; i < event.deleted.length; ++i) { + assert_equals(event.deleted[i].name, expected.deleted[i].name, + description + 'deleted cookie name'); + assert_equals(event.deleted[i].value, expected.deleted[i].value, + description + 'deleted cookie value'); + } +} + +// Helper function for promise_test with cookies; cookies +// named in these tests are cleared before/after the test +// body function is executed. +async function cookie_test(func, description) { + + // Wipe cookies used by tests before and after the test. + async function deleteAllCookies() { + (await cookieStore.getAll()).forEach(({name, value}) => { + cookieStore.delete(name); + }); + } + + return promise_test(async t => { + await deleteAllCookies(); + try { + return await func(t); + } finally { + await deleteAllCookies(); + } + }, description); +} diff --git a/testing/web-platform/tests/cookie-store/resources/cookie_helper.py b/testing/web-platform/tests/cookie-store/resources/cookie_helper.py new file mode 100644 index 0000000000..71dd8b82ee --- /dev/null +++ b/testing/web-platform/tests/cookie-store/resources/cookie_helper.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- + +# Active wptserve handler for cookie operations. +# +# This must support the following requests: +# +# - GET with the following query parameters: +# - charset: (optional) character set for response (default: utf-8) +# A cookie: request header (if present) is echoed in the body with a +# cookie= prefix followed by the urlencoded bytes from the header. +# Used to inspect the cookie jar from an HTTP request header context. +# - POST with form-data in the body and the following query-or-form parameters: +# - set-cookie: (optional; repeated) echoed in the set-cookie: response +# header and also echoed in the body with a set-cookie= prefix +# followed by the urlencoded bytes from the parameter; multiple occurrences +# are CRLF-delimited. +# Used to set cookies from an HTTP response header context. +# +# The response has 200 status and content-type: text/plain; charset=<charset> +import encodings, re + +from urllib.parse import parse_qs, quote + +from wptserve.utils import isomorphic_decode, isomorphic_encode + +# NOTE: These are intentionally very lax to permit testing +DISALLOWED_IN_COOKIE_NAME_RE = re.compile(br'[;\0-\x1f\x7f]') +DISALLOWED_IN_HEADER_RE = re.compile(br'[\0-\x1f\x7f]') + +# Ensure common charset names do not end up with different +# capitalization or punctuation +CHARSET_OVERRIDES = { + encodings.codecs.lookup(charset).name: charset + for charset in (u'utf-8', u'iso-8859-1',) +} + +def quote_str(cookie_str): + return isomorphic_encode(quote(isomorphic_decode(cookie_str), u'', encoding=u'iso-8859-1')) + +def parse_qs_str(query_str): + args = parse_qs(isomorphic_decode(query_str), keep_blank_values=True, encoding=u'iso-8859-1') + binary_args = {} + for key, val in args.items(): + binary_args[isomorphic_encode(key)] = [isomorphic_encode(x) for x in val] + return binary_args + +def main(request, response): + assert request.method in ( + u'GET', + u'POST', + ), u'request method was neither GET nor POST: %r' % request.method + qd = (isomorphic_encode(request.url).split(b'#')[0].split(b'?', 1) + [b''])[1] + if request.method == u'POST': + qd += b'&' + request.body + args = parse_qs_str(qd) + + charset = encodings.codecs.lookup([isomorphic_decode(x) for x in args.get(b'charset', [u'utf-8'])][-1]).name + charset = CHARSET_OVERRIDES.get(charset, charset) + headers = [(b'content-type', b'text/plain; charset=' + isomorphic_encode(charset))] + body = [] + if request.method == u'POST': + for set_cookie in args.get(b'set-cookie', []): + if b'=' in set_cookie.split(b';', 1)[0]: + name, rest = set_cookie.split(b'=', 1) + assert re.search( + DISALLOWED_IN_COOKIE_NAME_RE, + name + ) is None, b'name had disallowed characters: %r' % name + else: + rest = set_cookie + assert re.search( + DISALLOWED_IN_HEADER_RE, + rest + ) is None, b'rest had disallowed characters: %r' % rest + headers.append((b'set-cookie', set_cookie)) + body.append(b'set-cookie=' + quote_str(set_cookie)) + + else: + cookie = request.headers.get(b'cookie') + if cookie is not None: + body.append(b'cookie=' + quote_str(cookie)) + body = b'\r\n'.join(body) + headers.append((b'content-length', len(body))) + return 200, headers, body diff --git a/testing/web-platform/tests/cookie-store/resources/empty_sw.js b/testing/web-platform/tests/cookie-store/resources/empty_sw.js new file mode 100644 index 0000000000..2b0ae22612 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/resources/empty_sw.js @@ -0,0 +1 @@ +// Empty service worker script. diff --git a/testing/web-platform/tests/cookie-store/resources/helper_iframe.sub.html b/testing/web-platform/tests/cookie-store/resources/helper_iframe.sub.html new file mode 100644 index 0000000000..9017eace44 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/resources/helper_iframe.sub.html @@ -0,0 +1,31 @@ +<!doctype html> +<meta charset='utf-8'> +<link rel='author' href='jarrydg@chromium.org' title='Jarryd Goodman'> +<script> + 'use strict'; + + // Writing a cookie: + // Input: { cookieToSet: { name: 'cookie-name', value: 'cookie-value' } } + // Response: "Cookie has been set" + // + // Read a cookie. + // Command: { existingCookieName: 'cookie-name' } + // Response: Result of cookieStore.get('cookie-name'): + // { frameCookie: { name: 'cookie-name', value: 'cookie-value' } } + window.addEventListener('message', async function (event) { + const { opname } = event.data; + if (opname === 'set-cookie') { + const { name, value } = event.data + await cookieStore.set({ + name, + value, + domain: '{{host}}', + }); + event.source.postMessage('Cookie has been set', event.origin); + } else if (opname === 'get-cookie') { + const { name } = event.data + const frameCookie = await cookieStore.get(name); + event.source.postMessage({frameCookie}, event.origin); + } + }); +</script> diff --git a/testing/web-platform/tests/cookie-store/resources/helpers.js b/testing/web-platform/tests/cookie-store/resources/helpers.js new file mode 100644 index 0000000000..8d5dddef65 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/resources/helpers.js @@ -0,0 +1,72 @@ +/** + * Promise based helper function who's return promise will resolve + * once the iframe src has been loaded + * @param {string} url the url to set the iframe src + * @param {test} t a test object to add a cleanup function to + * @return {Promise} when resolved, will return the iframe + */ +self.createIframe = (url, t) => new Promise(resolve => { + const iframe = document.createElement('iframe'); + iframe.addEventListener('load', () => {resolve(iframe);}, {once: true}); + iframe.src = url; + document.documentElement.appendChild(iframe); + t.add_cleanup(() => iframe.remove()); +}); + +/** + * @description - Function unregisters any service workers in this scope + * and then creates a new registration. The function returns + * a promise that resolves when the registered service worker + * becomes activated. The resolved promise yields the + * service worker registration + * @param {testCase} t - test case to add cleanup functions to + */ +self.createServiceWorker = async (t, sw_registration_name, scope_url) => { + let registration = await navigator.serviceWorker.getRegistration(scope_url); + if (registration) + await registration.unregister(); + + registration = await navigator.serviceWorker.register(sw_registration_name, + {scope_url}); + t.add_cleanup(() => registration.unregister()); + + return new Promise(resolve => { + const serviceWorker = registration.installing || registration.active || + registration.waiting; + serviceWorker.addEventListener('statechange', event => { + if (event.target.state === 'activated') { + resolve(serviceWorker); + } + }); + }) +} + +/** + * Function that will return a promise that resolves when a message event + * is fired. Returns a promise that resolves to the message that was received + */ +self.waitForMessage = () => new Promise(resolve => { + window.addEventListener('message', event => { + resolve(event.data); + }, {once: true}); +}); + +/** + * Sends a message via MessageChannel and waits for the response + * @param {*} message + * @returns {Promise} resolves with the response payload + */ +self.sendMessageOverChannel = (message, target) => { + return new Promise(function(resolve, reject) { + const messageChannel = new MessageChannel(); + messageChannel.port1.onmessage = event => { + if (event.data.error) { + reject(event.data.error); + } else { + resolve(event.data); + } + }; + + target.postMessage(message, [messageChannel.port2]); + }) +}; diff --git a/testing/web-platform/tests/cookie-store/serviceworker_cookieStore_cross_origin.https.sub.html b/testing/web-platform/tests/cookie-store/serviceworker_cookieStore_cross_origin.https.sub.html new file mode 100644 index 0000000000..6879c5da92 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/serviceworker_cookieStore_cross_origin.https.sub.html @@ -0,0 +1,41 @@ +<!doctype html> +<meta charset='utf-8'> +<title>Async Cookies: cookieStore API in ServiceWorker across origins</title> +<link rel='help' href='https://github.com/WICG/cookie-store'> +<link rel='author' href='jarrydg@chromium.org' title='Jarryd Goodman'> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='resources/helpers.js'></script> +<style>iframe {display: none}</style> +<script> +'use strict'; + +const kPath = '/cookie-store/resources/helper_iframe.sub.html'; +const kCorsBase = `https://{{domains[www1]}}:{{ports[https][0]}}`; +const kCorsUrl = `${kCorsBase}${kPath}`; + +promise_test(async t => { + const iframe = await createIframe(kCorsUrl, t); + assert_true(iframe != null); + + const serviceWorker = await createServiceWorker(t, + 'serviceworker_cookieStore_cross_origin.js', '/does/not/exist'); + + + iframe.contentWindow.postMessage({ + opname: 'set-cookie', + name: 'cookie-name', + value: 'cookie-value', + }, kCorsBase); + t.add_cleanup(async () => { await cookieStore.delete('cookie-name'); }); + + await waitForMessage(); + + const { workerCookies } = await sendMessageOverChannel({ op: 'get-cookies' }, + serviceWorker); + + assert_equals(workerCookies.length, 1); + assert_equals(workerCookies[0].name, 'cookie-name'); + assert_equals(workerCookies[0].value, 'cookie-value'); +}, 'cookieStore.get() in ServiceWorker reads cookie set in cross-origin frame'); +</script> diff --git a/testing/web-platform/tests/cookie-store/serviceworker_cookieStore_cross_origin.js b/testing/web-platform/tests/cookie-store/serviceworker_cookieStore_cross_origin.js new file mode 100644 index 0000000000..fa1c4084fd --- /dev/null +++ b/testing/web-platform/tests/cookie-store/serviceworker_cookieStore_cross_origin.js @@ -0,0 +1,14 @@ +self.GLOBAL = { + isWindow: () => false, + isWorker: () => false, + isShadowRealm: () => false, +}; + +self.addEventListener('message', async event => { + if (event.data.op === 'get-cookies') { + const workerCookies = await cookieStore.getAll(); + event.ports[0].postMessage({ workerCookies }, { + domain: event.origin, + }); + } +}); diff --git a/testing/web-platform/tests/cookie-store/serviceworker_cookieStore_subscriptions_reset.https.html b/testing/web-platform/tests/cookie-store/serviceworker_cookieStore_subscriptions_reset.https.html new file mode 100644 index 0000000000..a1124e9220 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/serviceworker_cookieStore_subscriptions_reset.https.html @@ -0,0 +1,55 @@ +<!doctype html> +<meta charset="utf-8"> +<title>Cookie Store API: reset cookie change subscription list</title> +<link rel="help" href="https://github.com/WICG/cookie-store"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"> +</script> +<script src='resources/helpers.js'></script> +<script> +'use strict'; + +promise_test(async t => { + const registration = await service_worker_unregister_and_register( + t, 'resources/empty_sw.js', 'resources/does/not/exist'); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + await registration.cookies.subscribe( + [{ name: 'cookie-name' }]); + const original_subscriptions = await registration.cookies.getSubscriptions(); + assert_equals(original_subscriptions.length, 1, + 'subscription count before unregistration'); + + await registration.unregister(); + + const new_registration = await navigator.serviceWorker.register( + 'resources/empty_sw.js', { scope: 'resources/does/not/exist' }); + t.add_cleanup(() => new_registration.unregister()); + await wait_for_state(t, new_registration.installing, 'activated'); + + const new_subscriptions = await new_registration.cookies.getSubscriptions(); + assert_equals(new_subscriptions.length, 0, + 'subscription count after unregistration'); +}, 'cookiechange subscriptions reset across service worker unregistrations'); + +promise_test(async t => { + const registration = await service_worker_unregister_and_register( + t, 'resources/always_changing_sw.sub.js', 'resources/does/not/exist'); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + await registration.cookies.subscribe( + [{ name: 'cookie-name' }]); + const original_subscriptions = await registration.cookies.getSubscriptions(); + assert_equals(original_subscriptions.length, 1, + 'subscription count before update'); + + await registration.update(); + const worker = await wait_for_update(t, registration); + await wait_for_state(t, worker, 'activated'); + + const update_subscriptions = await registration.cookies.getSubscriptions(); + assert_equals(update_subscriptions.length, 1, + 'subscription count after update'); +}, 'cookiechange subscriptions persist across service worker updates'); +</script> diff --git a/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_mismatched_subscription.https.any.js b/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_mismatched_subscription.https.any.js new file mode 100644 index 0000000000..30d8d70940 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_mismatched_subscription.https.any.js @@ -0,0 +1,44 @@ +// META: title=Cookie Store API: cookiechange event in ServiceWorker with mismatched subscription +// META: global=serviceworker + +'use strict'; + +const kScope = '/cookie-store/does/not/exist'; + +// Resolves when the service worker receives the 'activate' event. +const kServiceWorkerActivatedPromise = new Promise((resolve) => { + self.addEventListener('activate', event => { resolve(); }); +}); + +// Resolves when a cookiechange event is received. +const kCookieChangeReceivedPromise = new Promise((resolve) => { + self.addEventListener('cookiechange', (event) => { + resolve(event); + }); +}); + +promise_test(async testCase => { + await kServiceWorkerActivatedPromise; + + const subscriptions = [{ name: 'cookie-name', url: `${kScope}/path` }]; + await registration.cookies.subscribe(subscriptions); + testCase.add_cleanup(() => registration.cookies.unsubscribe(subscriptions)); + + await cookieStore.set('another-cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('another-cookie-name'); + }); + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + const event = await kCookieChangeReceivedPromise; + assert_equals(event.type, 'cookiechange'); + assert_equals(event.changed.length, 1); + assert_equals(event.changed[0].name, 'cookie-name'); + assert_equals(event.changed[0].value, 'cookie-value'); + assert_equals(event.deleted.length, 0); + assert_true(event instanceof ExtendableCookieChangeEvent); + assert_true(event instanceof ExtendableEvent); +}, 'cookiechange not dispatched for change that does not match subscription'); diff --git a/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_multiple_subscriptions.https.any.js b/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_multiple_subscriptions.https.any.js new file mode 100644 index 0000000000..cd0657c0bc --- /dev/null +++ b/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_multiple_subscriptions.https.any.js @@ -0,0 +1,68 @@ +// META: title=Cookie Store API: cookiechange event in ServiceWorker with multiple subscriptions +// META: global=serviceworker + +'use strict'; + +const kScope = '/cookie-store/does/not/exist'; + +// Resolves when the service worker receives the 'activate' event. +const kServiceWorkerActivatedPromise = new Promise((resolve) => { + self.addEventListener('activate', event => { resolve(); }); +}); + +// Accumulates cookiechange events dispatched to the service worker. +let g_cookie_changes = []; + +// Resolved when a cookiechange event is received. Rearmed by +// RearmCookieChangeReceivedPromise(). +let g_cookie_change_received_promise = null; +let g_cookie_change_received_promise_resolver = null; +self.addEventListener('cookiechange', (event) => { + g_cookie_changes.push(event); + if (g_cookie_change_received_promise_resolver) { + g_cookie_change_received_promise_resolver(); + } +}); +function RearmCookieChangeReceivedPromise() { + g_cookie_change_received_promise = new Promise((resolve) => { + g_cookie_change_received_promise_resolver = resolve; + }); +} +RearmCookieChangeReceivedPromise(); + +promise_test(async testCase => { + await kServiceWorkerActivatedPromise; + + { + const subscriptions = [{ name: 'cookie-name1', url: `${kScope}/path1` }]; + await registration.cookies.subscribe(subscriptions); + testCase.add_cleanup(() => registration.cookies.unsubscribe(subscriptions)); + } + { + const subscriptions = [ + { }, // Test the default values for subscription properties. + { name: 'cookie-name2' }, + ]; + await registration.cookies.subscribe(subscriptions); + testCase.add_cleanup(() => registration.cookies.unsubscribe(subscriptions)); + } + + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + testCase.add_cleanup(() => { g_cookie_changes = []; }); + + await g_cookie_change_received_promise; + testCase.add_cleanup(() => RearmCookieChangeReceivedPromise()); + + assert_equals(g_cookie_changes.length, 1); + const event = g_cookie_changes[0]; + assert_equals(event.type, 'cookiechange'); + assert_equals(event.changed.length, 1); + assert_equals(event.changed[0].name, 'cookie-name'); + assert_equals(event.changed[0].value, 'cookie-value'); + assert_equals(event.deleted.length, 0); + assert_true(event instanceof ExtendableCookieChangeEvent); + assert_true(event instanceof ExtendableEvent); +}, 'cookiechange dispatched with cookie change that matches subscription'); diff --git a/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_overlapping_subscriptions.https.any.js b/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_overlapping_subscriptions.https.any.js new file mode 100644 index 0000000000..1f433aeb94 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_overlapping_subscriptions.https.any.js @@ -0,0 +1,87 @@ +// META: title=Cookie Store API: cookiechange event in ServiceWorker with overlapping subscriptions +// META: global=serviceworker + +'use strict'; + +const kScope = '/cookie-store/does/not/exist'; + +// Resolves when the service worker receives the 'activate' event. +const kServiceWorkerActivatedPromise = new Promise((resolve) => { + self.addEventListener('activate', event => { resolve(); }); +}); + +// Accumulates cookiechange events dispatched to the service worker. +let g_cookie_changes = []; + +// Resolved when a cookiechange event is received. Rearmed by +// RearmCookieChangeReceivedPromise(). +let g_cookie_change_received_promise = null; +let g_cookie_change_received_promise_resolver = null; +self.addEventListener('cookiechange', (event) => { + g_cookie_changes.push(event); + if (g_cookie_change_received_promise_resolver) { + g_cookie_change_received_promise_resolver(); + RearmCookieChangeReceivedPromise(); + } +}); +function RearmCookieChangeReceivedPromise() { + g_cookie_change_received_promise = new Promise((resolve) => { + g_cookie_change_received_promise_resolver = resolve; + }); +} +RearmCookieChangeReceivedPromise(); + +promise_test(async testCase => { + await kServiceWorkerActivatedPromise; + + const subscriptions = [ + { name: 'cookie-name' }, + { url: `${kScope}/path` } + ]; + await registration.cookies.subscribe(subscriptions); + testCase.add_cleanup(() => registration.cookies.unsubscribe(subscriptions)); + + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + testCase.add_cleanup(() => { g_cookie_changes = []; }); + + await g_cookie_change_received_promise; + testCase.add_cleanup(() => RearmCookieChangeReceivedPromise()); + + // To ensure that we are accounting for all events dispatched by the first + // cookie change, we initiate and listen for a final cookie change that we + // know will dispatch a single event. + await cookieStore.set('coo', 'coo-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('coo'); + }); + testCase.add_cleanup(() => { g_cookie_changes = []; }); + + await g_cookie_change_received_promise; + testCase.add_cleanup(() => RearmCookieChangeReceivedPromise()); + + assert_equals(g_cookie_changes.length, 2); + { + const event = g_cookie_changes[0]; + assert_equals(event.type, 'cookiechange'); + assert_equals(event.changed.length, 1); + assert_equals(event.changed[0].name, 'cookie-name'); + assert_equals(event.changed[0].value, 'cookie-value'); + assert_equals(event.deleted.length, 0); + assert_true(event instanceof ExtendableCookieChangeEvent); + assert_true(event instanceof ExtendableEvent); + } + { + const event = g_cookie_changes[1]; + assert_equals(event.type, 'cookiechange'); + assert_equals(event.changed.length, 1); + assert_equals(event.changed[0].name, 'coo'); + assert_equals(event.changed[0].value, 'coo-value'); + assert_equals(event.deleted.length, 0); + assert_true(event instanceof ExtendableCookieChangeEvent); + assert_true(event instanceof ExtendableEvent); + } +}, '1 cookiechange event dispatched with cookie change that matches multiple ' + + 'subscriptions'); diff --git a/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_single_subscription.https.any.js b/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_single_subscription.https.any.js new file mode 100644 index 0000000000..3ccb4b303d --- /dev/null +++ b/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_single_subscription.https.any.js @@ -0,0 +1,39 @@ +// META: title=Cookie Store API: cookiechange event in ServiceWorker with single subscription +// META: global=serviceworker + +'use strict'; + +const kScope = '/cookie-store/does/not/exist'; + +// Resolves when the service worker receives the 'activate' event. +const kServiceWorkerActivatedPromise = new Promise((resolve) => { + self.addEventListener('activate', event => { resolve(); }); +}); + +// Resolves when a cookiechange event is received. +const kCookieChangeReceivedPromise = new Promise(resolve => { + self.addEventListener('cookiechange', event => { resolve(event); }); +}); + +promise_test(async testCase => { + await kServiceWorkerActivatedPromise; + + const subscriptions = [{ name: 'cookie-name', url: `${kScope}/path` }]; + await registration.cookies.subscribe(subscriptions); + testCase.add_cleanup(() => registration.cookies.unsubscribe(subscriptions)); + + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + const event = await kCookieChangeReceivedPromise; + assert_equals(event.type, 'cookiechange'); + assert_equals(event.changed.length, 1); + assert_equals(event.changed[0].name, 'cookie-name'); + assert_equals(event.changed[0].value, 'cookie-value'); + assert_equals(event.deleted.length, 0); + assert_true(event instanceof ExtendableCookieChangeEvent); + assert_true(event instanceof ExtendableEvent); +}, 'cookiechange dispatched with cookie change that matches subscription ' + + 'to cookiechange event handler registered with addEventListener'); diff --git a/testing/web-platform/tests/cookie-store/serviceworker_oncookiechange_eventhandler_single_subscription.https.any.js b/testing/web-platform/tests/cookie-store/serviceworker_oncookiechange_eventhandler_single_subscription.https.any.js new file mode 100644 index 0000000000..8def244089 --- /dev/null +++ b/testing/web-platform/tests/cookie-store/serviceworker_oncookiechange_eventhandler_single_subscription.https.any.js @@ -0,0 +1,39 @@ +// META: title=Cookie Store API: oncookiechange event in ServiceWorker with single subscription +// META: global=serviceworker + +'use strict'; + +const kScope = '/cookie-store/does/not/exist'; + +// Resolves when the service worker receives the 'activate' event. +const kServiceWorkerActivatedPromise = new Promise((resolve) => { + self.addEventListener('activate', event => { resolve(); }); +}); + +// Resolves when a cookiechange event is received. +const kCookieChangeReceivedPromise = new Promise(resolve => { + self.oncookiechange = event => { resolve(event); }; +}); + +promise_test(async testCase => { + await kServiceWorkerActivatedPromise; + + const subscriptions = [{ name: 'cookie-name', url: `${kScope}/path` }]; + await registration.cookies.subscribe(subscriptions); + testCase.add_cleanup(() => registration.cookies.unsubscribe(subscriptions)); + + await cookieStore.set('cookie-name', 'cookie-value'); + testCase.add_cleanup(async () => { + await cookieStore.delete('cookie-name'); + }); + + const event = await kCookieChangeReceivedPromise; + assert_equals(event.type, 'cookiechange'); + assert_equals(event.changed.length, 1); + assert_equals(event.changed[0].name, 'cookie-name'); + assert_equals(event.changed[0].value, 'cookie-value'); + assert_equals(event.deleted.length, 0); + assert_true(event instanceof ExtendableCookieChangeEvent); + assert_true(event instanceof ExtendableEvent); +}, 'cookiechange dispatched with cookie change that matches subscription ' + + 'to cookiechange event handler registered with addEventListener'); |