Skip to content

Commit

Permalink
Add more Web Platform Tests (#120)
Browse files Browse the repository at this point in the history
* Add rest of Web Platform Tests

* Expose `assert.deepEqual` to Web Platform Tests

* `__Secure` prefixed cookies are secure

* Set `sameSite` to `lax` when item is secure

* Set path to `/` when name is prefixed with __Host

* Default `changed` and `deleted` to empty arrays

* Make sure to fail when name is empty and value includes `=`

* Expires can be a date

* Remove unneeded serviceworker test setup file

* Move karma single run option into config file

* Setup service worker registration for tests

* CookieMatchType is unused

* Fix service worker tests

* Implement CookieStoreManager for service workers

* Make sure all the servive worker tests run

* Don't throw errors in `getAll`

* Clean up ordered cookies after test run

* Allow passing of empty object to `getAll`

* Add ability to skip tests that we can't pass right now

* Don't expose CookieStore on the global object

* Update README

* Fix tests

* Update Web Platform Test

* Set return type of get Symbol method
  • Loading branch information
koddsson committed Dec 6, 2021
1 parent d5d2332 commit 5e27abe
Show file tree
Hide file tree
Showing 13 changed files with 813 additions and 45 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![Build Status](https://travis-ci.org/mkay581/cookie-store.svg?branch=master)](https://travis-ci.org/mkay581/cookie-store)
[![npm version](https://badge.fury.io/js/cookie-store.svg)](https://www.npmjs.com/package/cookie-store)

A polyfill to allow use of the [Cookie Store API](https://wicg.github.io/cookie-store/) in modern browsers that don't support it natively, including IE11. Also compatible with TypeScript.
A ponyfill to allow use of the [Cookie Store API](https://wicg.github.io/cookie-store/) in modern browsers that don't support it natively, including IE11. Also compatible with TypeScript.

## Installation

Expand All @@ -15,7 +15,7 @@ npm install cookie-store

```js
// import polyfill and declare types
import 'cookie-store';
import {cookieStore} from 'cookie-store';

// set a cookie
await cookieStore.set('forgive', 'me');
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"scripts": {
"pretest": "npm run build",
"test": "npm run test:ts && eslint 'src/**/*' && npm run prettier",
"test:ts": "karma start test/karma.conf.cjs --single-run",
"test:ts": "karma start test/karma.conf.cjs",
"prettier": "prettier --check 'src/**/*'",
"preversion": "npm test",
"banner": "banner-cli dist/index.js",
Expand Down
137 changes: 115 additions & 22 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ function tryDecode(
}
}

type CookieMatchType = 'equals';

interface Cookie {
domain?: string;
expires?: number;
Expand All @@ -34,7 +32,6 @@ interface CookieStoreDeleteOptions {
interface CookieStoreGetOptions {
name?: string;
url?: string;
matchType?: CookieMatchType;
}

interface ParseOptions {
Expand All @@ -52,7 +49,7 @@ interface CookieListItem {
value?: string;
domain: string | null;
path?: string;
expires: number | null;
expires: Date | number | null;
secure?: boolean;
sameSite?: CookieSameSite;
}
Expand Down Expand Up @@ -120,15 +117,15 @@ class CookieChangeEvent extends Event {
eventInitDict: CookieChangeEventInit = { changed: [], deleted: [] }
) {
super(type, eventInitDict);
this.changed = eventInitDict.changed;
this.deleted = eventInitDict.deleted;
this.changed = eventInitDict.changed || [];
this.deleted = eventInitDict.deleted || [];
}
}

class CookieStore extends EventTarget {
onchange?: (event: CookieChangeEvent) => void;

get [Symbol.toStringTag]() {
get [Symbol.toStringTag](): 'CookieStore' {
return 'CookieStore';
}

Expand Down Expand Up @@ -177,9 +174,15 @@ class CookieStore extends EventTarget {
if (item.domain && item.domain !== window.location.hostname) {
throw new TypeError('Cookie domain must domain-match current host');
}
if (item.name === '' && item.value && item.value.includes('=')) {

if (item.name?.startsWith('__Host') && item.domain) {
throw new TypeError(
'Cookie domain must not be specified for host cookies'
);
}
if (item.name?.startsWith('__Host') && item.path != '/') {
throw new TypeError(
"Cookie value cannot contain '=' if the name is empty"
'Cookie path must not be specified for host cookies'
);
}

Expand All @@ -191,22 +194,35 @@ class CookieStore extends EventTarget {
}
}

if (item.name === '' && item.value && item.value.includes('=')) {
throw new TypeError(
"Cookie value cannot contain '=' if the name is empty"
);
}

if (item.name && item.name.startsWith('__Host')) {
item.secure = true;
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let cookieString = `${item.name}=${encodeURIComponent(item.value!)}`;

if (item.domain) {
cookieString += '; Domain=' + item.domain;
}

if (item.path && item.path !== '/') {
if (item.path) {
cookieString += '; Path=' + item.path;
}

if (typeof item.expires === 'number') {
cookieString += '; Expires=' + new Date(item.expires).toUTCString();
} else if (item.expires instanceof Date) {
cookieString += '; Expires=' + item.expires.toUTCString();
}

if (item.secure) {
if ((item.name && item.name.startsWith('__Secure')) || item.secure) {
item.sameSite = CookieSameSite.lax;
cookieString += '; Secure';
}

Expand Down Expand Up @@ -245,14 +261,9 @@ class CookieStore extends EventTarget {
init?: CookieStoreGetOptions['name'] | CookieStoreGetOptions
): Promise<Cookie[]> {
const cookies = parse(document.cookie);
if (!init || Object.keys(init).length === 0) {
if (init == null || Object.keys(init).length === 0) {
return cookies;
}
if (init == null) {
throw new TypeError('CookieStoreGetOptions must not be empty');
} else if (init instanceof Object && !Object.keys(init).length) {
throw new TypeError('CookieStoreGetOptions must not be empty');
}
let name: string | undefined;
let url;
if (typeof init === 'string') {
Expand Down Expand Up @@ -298,18 +309,100 @@ class CookieStore extends EventTarget {
}
}

if (!window.cookieStore) {
window.CookieStore = CookieStore;
window.cookieStore = Object.create(CookieStore.prototype);
window.CookieChangeEvent = CookieChangeEvent;
interface CookieStoreGetOptions {
name?: string;
url?: string;
}

const workerSubscriptions = new WeakMap<
CookieStoreManager,
CookieStoreGetOptions[]
>();

const registrations = new WeakMap<
CookieStoreManager,
ServiceWorkerRegistration
>();

class CookieStoreManager {
get [Symbol.toStringTag]() {
return 'CookieStoreManager';
}

constructor() {
throw new TypeError('Illegal Constructor');
}

async subscribe(subscriptions: CookieStoreGetOptions[]): Promise<void> {
const currentSubcriptions = workerSubscriptions.get(this) || [];
const worker = registrations.get(this);
if (!worker) throw new TypeError('Illegal invocation');
for (const subscription of subscriptions) {
const name = subscription.name;
const url = new URL(subscription.url || '', worker.scope).toString();

if (currentSubcriptions.some((x) => x.name === name && x.url === url))
continue;
currentSubcriptions.push({
name: subscription.name,
url,
});
}
workerSubscriptions.set(this, currentSubcriptions);
}

async getSubscriptions(): Promise<CookieStoreGetOptions[]> {
return (workerSubscriptions.get(this) || []).map(({ name, url }) => ({
name,
url,
}));
}

async unsubscribe(subscriptions: CookieStoreGetOptions[]): Promise<void> {
let currentSubcriptions = workerSubscriptions.get(this) || [];

const worker = registrations.get(this);
if (!worker) throw new TypeError('Illegal invocation');

for (const subscription of subscriptions) {
const name = subscription.name;
// TODO: Parse the url with the relevant settings objects API base URL.
// https://wicg.github.io/cookie-store/#CookieStoreManager-unsubscribe
const url = new URL(subscription.url || '', worker.scope).toString();
currentSubcriptions = currentSubcriptions.filter((x) => {
if (x.name !== name) return true;
if (x.url !== url) return true;
return false;
});
}
workerSubscriptions.set(this, currentSubcriptions);
}
}

if (!ServiceWorkerRegistration.prototype.cookies) {
Object.defineProperty(ServiceWorkerRegistration.prototype, 'cookies', {
configurable: true,
enumerable: true,
get() {
const manager = Object.create(CookieStoreManager.prototype);
registrations.set(manager, this);
Object.defineProperty(this, 'cookies', { value: manager });
return manager;
},
});
}

declare global {
interface Window {
CookieStore: typeof CookieStore;
cookieStore: CookieStore;
CookieChangeEvent: typeof CookieChangeEvent;
CookieStoreManager: typeof CookieStoreManager;
}
interface ServiceWorkerRegistration {
cookies: CookieStoreManager;
}
}

export {};
const cookieStore = Object.create(CookieStore.prototype);
export { cookieStore, CookieStore, CookieChangeEvent };
6 changes: 6 additions & 0 deletions test/index.tests.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
/* global expect */

import {cookieStore, CookieStore, CookieChangeEvent} from '../dist/index.js'

window.cookieStore = cookieStore
window.CookieStore = CookieStore
window.CookieChangeEvent = CookieChangeEvent

describe('Cookie Store', () => {
beforeEach(() => {
Object.defineProperty(document, 'cookie', {
Expand Down
19 changes: 14 additions & 5 deletions test/karma.conf.cjs
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
module.exports = function (config) {
config.set({
basePath: '..',
files: [
// Include the compiled library
{ pattern: '../dist/index.js', type: 'module' },
{ pattern: './dist/index.js', type: 'module', included: false },
// Include the compiled service worker polyfill
{ pattern: './dist/service-worker.js', included: false },
// Set up test environment to be able to run WPT tests
{ pattern: './wpt-setup/*.js', type: 'module' },
{ pattern: './test/wpt-setup/*.js', type: 'module' },
// Our tests
{ pattern: './index.tests.js', type: 'module' },
{ pattern: './test/index.tests.js', type: 'module' },
// Web Platform Tests
{ pattern: './wpt/*.js', type: 'module' },
{ pattern: './test/wpt/*.js', type: 'module' },
// Resources
{ pattern: './test/resources/*', included: false },
],
plugins: ['karma-*'],
reporters: ['progress'],
Expand All @@ -19,6 +24,10 @@ module.exports = function (config) {
browsers: ['FirefoxHeadless'],
concurrency: Infinity,
hostname: 'foo.bar.localhost',
urlRoot: '/test',
urlRoot: '/cookie-store/',
singleRun: true,
proxies: {
'/cookie-store/resources/': '/base/test/resources/',
},
});
};
1 change: 1 addition & 0 deletions test/resources/empty_sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Empty service worker
Loading

0 comments on commit 5e27abe

Please sign in to comment.