From 9f603bc6d27eb8ee238918721b75d9630e62f995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Oddsson?= Date: Mon, 30 Nov 2020 17:29:30 +0000 Subject: [PATCH] Run Web Platform Tests against the polyfill. (#88) * Throw TypeError if get is called without arguments See step 4 in "The get(options) method" section in the [CookieStore Spec][cookiestore]: >If options is empty, then return a promise rejected with a TypeError. [cookiestore]: https://wicg.github.io/cookie-store/#ref-for-dom-cookiestore-get-options%E2%91%A4 * Refactor sanitizeOptions to use TypeScript generics * Implement getting a cookie by URL * Add cookieStore get arguments WPT tests * Set `assert_equals` variable directly * Actually parse URL and check its origin --- src/index.ts | 38 ++++--- test/karma.conf.cjs | 8 +- test/wpt-setup/harness.js | 33 ++++++ .../serviceworker_cookieStore_cross_origin.js | 13 +++ ...Store_get_arguments.tentative.https.any.js | 107 ++++++++++++++++++ 5 files changed, 182 insertions(+), 17 deletions(-) create mode 100644 test/wpt-setup/harness.js create mode 100644 test/wpt-setup/serviceworker_cookieStore_cross_origin.js create mode 100644 test/wpt/cookieStore_get_arguments.tentative.https.any.js diff --git a/src/index.ts b/src/index.ts index e9032ae..ab43f5b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -234,14 +234,9 @@ function serialize( return str; } -function sanitizeOptions( - arg: string | CookieStoreGetOptions | undefined -): CookieStoreGetOptions | CookieStoreDeleteOptions { - if (!arg) { - return {}; - } +function sanitizeOptions(arg: string | T): T { if (typeof arg === 'string') { - return { name: arg }; + return ({ name: arg } as unknown) as T; } return arg; } @@ -256,7 +251,20 @@ const CookieStore = { async get( options?: CookieStoreGetOptions['name'] | CookieStoreGetOptions ): Promise { - const { name } = sanitizeOptions(options); + if (!options || !Object.keys(options).length) { + throw new TypeError('CookieStoreGetOptions must not be empty'); + } + const { name, url } = sanitizeOptions(options); + if (url) { + const parsedURL = new URL(url, window.location.origin); + if ( + window.location.href !== parsedURL.href || + window.location.origin !== parsedURL.origin + ) { + throw new TypeError('URL must match the document URL'); + } + return parse(document.cookie)[0]; + } return parse(document.cookie).find((cookie) => cookie.name === name); }, @@ -285,12 +293,12 @@ const CookieStore = { async getAll( options?: CookieStoreGetOptions['name'] | CookieStoreGetOptions ): Promise { - const { name } = sanitizeOptions(options); - if (name) { - const cookie = await this.get(name); - return cookie ? [cookie] : []; + if (!options) { + return parse(document.cookie); } - return parse(document.cookie); + const { name } = sanitizeOptions(options); + const cookie = await this.get(name); + return cookie ? [cookie] : []; }, /** @@ -302,9 +310,7 @@ const CookieStore = { async delete( options: CookieStoreDeleteOptions['name'] | CookieStoreDeleteOptions ): Promise { - const { name, domain } = sanitizeOptions( - options - ) as CookieStoreDeleteOptions; + const { name, domain } = sanitizeOptions(options); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const { value } = (await this.get(name))!; const serializedValue = serialize(name, value, { diff --git a/test/karma.conf.cjs b/test/karma.conf.cjs index 3d3ffea..e7d0249 100644 --- a/test/karma.conf.cjs +++ b/test/karma.conf.cjs @@ -1,8 +1,14 @@ module.exports = function (config) { config.set({ files: [ + // Include the compiled library { pattern: '../dist/index.js', type: 'module' }, - { pattern: './*.tests.js', type: 'module' } + // Set up test environment to be able to run WPT tests + { pattern: './wpt-setup/*.js', type: 'module' }, + // Our tests + { pattern: './index.tests.js', type: 'module' }, + // Web Platform Tests + { pattern: './wpt/*.js', type: 'module' } ], plugins: ['karma-*'], reporters: ['progress'], diff --git a/test/wpt-setup/harness.js b/test/wpt-setup/harness.js new file mode 100644 index 0000000..be78a8f --- /dev/null +++ b/test/wpt-setup/harness.js @@ -0,0 +1,33 @@ +/* global assert */ + +window.promise_test = async (fn, name) => { + const cleanups = []; + const testCase = { + name, + add_cleanup(fn) { + cleanups.push(fn); + }, + }; + it(name, async () => { + await fn(testCase); + for (const cleanup of cleanups) { + cleanup(); + } + }); +}; + +window.promise_rejects_js = async (testCase, expectedError, promise) => { + try { + await promise; + } catch (error) { + if (error.name !== expectedError.name) { + assert.fail( + `${testCase.name}: Promise rejected with ${error.name}, expected ${expectedError.name}` + ); + } + return; + } + assert.fail(`${testCase.name}: Promise didn't reject when it should have.`); +}; + +window.assert_equals = assert.equal; diff --git a/test/wpt-setup/serviceworker_cookieStore_cross_origin.js b/test/wpt-setup/serviceworker_cookieStore_cross_origin.js new file mode 100644 index 0000000..ac59b70 --- /dev/null +++ b/test/wpt-setup/serviceworker_cookieStore_cross_origin.js @@ -0,0 +1,13 @@ +self.GLOBAL = { + isWindow: () => false, + isWorker: () => 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/test/wpt/cookieStore_get_arguments.tentative.https.any.js b/test/wpt/cookieStore_get_arguments.tentative.https.any.js new file mode 100644 index 0000000..3d5f035 --- /dev/null +++ b/test/wpt/cookieStore_get_arguments.tentative.https.any.js @@ -0,0 +1,107 @@ +// 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');