Repository: auth0/auth0-spa-js
Pull request #26 (2019-06-07 19:44:27)

Create one cookie per transaction
Using only one cookie limits us to around 9 transactions until the cookie size limit is reached. This PR changes to create one cookie per transaction instead.


Patch:
diff --git a/src/storage.ts b/src/storage.ts
index 9b17817..dafb1c3 100644
--- a/src/storage.ts
+++ b/src/storage.ts
@@ -4,6 +4,8 @@ interface ClientStorageOptions {
   daysUntilExpire: number;
 }
 
+export const getAllKeys = () => Object.keys(Cookies.getAll() || {});
+
 export const get = <T extends Object>(key: string) => {
   const value = Cookies.get(key);
   if (typeof value === 'undefined') {
diff --git a/src/transaction-manager.ts b/src/transaction-manager.ts
index 79a6d24..0459c8a 100644
--- a/src/transaction-manager.ts
+++ b/src/transaction-manager.ts
@@ -1,6 +1,7 @@
 import * as ClientStorage from './storage';
 
-const COOKIE_KEY = 'Auth0.login.transactions';
+const COOKIE_KEY = 'Auth0.spa-js.transactions.';
+const getTransactionKey = (state: string) => `${COOKIE_KEY}${state}`;
 
 interface Transaction {
   state: string;
@@ -17,11 +18,17 @@ interface Transactions {
 export default class TransactionManager {
   private transactions: Transactions;
   constructor() {
-    this.transactions = ClientStorage.get<Transactions>(COOKIE_KEY) || {};
+    this.transactions = {};
+    ClientStorage.getAllKeys()
+      .filter(k => k.startsWith(COOKIE_KEY))
+      .forEach(k => {
+        const state = k.replace(COOKIE_KEY, '');
+        this.transactions[state] = ClientStorage.get<Transaction>(k);
+      });
   }
   public create(transaction: Transaction) {
     this.transactions[transaction.state] = transaction;
-    ClientStorage.save(COOKIE_KEY, this.transactions, {
+    ClientStorage.save(getTransactionKey(transaction.state), transaction, {
       daysUntilExpire: 1
     });
   }
@@ -30,8 +37,6 @@ export default class TransactionManager {
   }
   public remove(state: string) {
     delete this.transactions[state];
-    ClientStorage.save(COOKIE_KEY, this.transactions, {
-      daysUntilExpire: 1
-    });
+    ClientStorage.remove(getTransactionKey(state));
   }
 }


Test patch:
diff --git a/__tests__/__mocks__/storage.ts b/__tests__/__mocks__/storage.ts
deleted file mode 100644
index 52d1971..0000000
--- a/__tests__/__mocks__/storage.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-export const internalStorage = {};
-
-export const get = <T extends Object>(key: string) => {
-  const value = internalStorage[key];
-  if (typeof value === 'undefined') {
-    return;
-  }
-  return <T>JSON.parse(value);
-};
-export const save = (key: string, value: any, options: any) => {
-  internalStorage[key] = JSON.stringify(value);
-};
-export const remove = (key: string) => {
-  internalStorage[key] = undefined;
-};
diff --git a/__tests__/storage.test.ts b/__tests__/storage.test.ts
index 720a87a..238a465 100644
--- a/__tests__/storage.test.ts
+++ b/__tests__/storage.test.ts
@@ -1,7 +1,7 @@
 import * as storage from '../src/storage';
 jest.mock('es-cookie');
 
-describe('cache', () => {
+describe('storage', () => {
   beforeEach(() => {
     jest.resetAllMocks();
   });
@@ -45,4 +45,21 @@ describe('cache', () => {
     storage.remove(key);
     expect(Cookie.remove).toHaveBeenCalledWith(key);
   });
+  describe('getAllKeys', () => {
+    it('returns empty array when there is no cookie', () => {
+      const Cookie = require('es-cookie');
+      Cookie.getAll.mockReturnValue(null);
+      const keys = storage.getAllKeys();
+      expect(keys.length).toBe(0);
+      expect(Cookie.getAll).toHaveBeenCalled();
+    });
+    it('gets all keys when there is a cookie', () => {
+      const Cookie = require('es-cookie');
+      Cookie.getAll.mockReturnValue({ key: 'value' });
+      const keys = storage.getAllKeys();
+      expect(keys.length).toBe(1);
+      expect(keys[0]).toBe('key');
+      expect(Cookie.getAll).toHaveBeenCalled();
+    });
+  });
 });
diff --git a/__tests__/transaction-manager.test.ts b/__tests__/transaction-manager.test.ts
index d7f4185..e88da09 100644
--- a/__tests__/transaction-manager.test.ts
+++ b/__tests__/transaction-manager.test.ts
@@ -1,4 +1,7 @@
 import TransactionManager from '../src/transaction-manager';
+
+const COOKIE_KEY = 'Auth0.spa-js.transactions.';
+
 const transaction = {
   state: 'stateIn',
   nonce: 'nonceIn',
@@ -8,61 +11,72 @@ const transaction = {
   scope: 'scopeIn',
   audience: ' audienceIn'
 };
+
 jest.mock('../src/storage');
-const getStorageMock = () => ({
-  get: require('../src/storage').get,
-  save: require('../src/storage').save,
-  remove: require('../src/storage').remove
-});
-describe('cache', () => {
+const getStorageMock = () => require('../src/storage');
+describe('transaction manager', () => {
   let tm: TransactionManager;
   beforeEach(() => {
     jest.resetAllMocks();
-    tm = new TransactionManager();
-  });
-
-  it('should try to load transactions from storage in the constructor', () => {
-    expect(getStorageMock().get).toHaveBeenCalledWith(
-      'Auth0.login.transactions'
-    );
-  });
-  it('`create` creates the transaction', () => {
-    tm.create(transaction);
-    expect(tm.get(transaction.state)).toMatchObject(transaction);
-  });
-  it('`create` saves the transaction in the storage', () => {
-    tm.create(transaction);
-    expect(getStorageMock().save).toHaveBeenCalledWith(
-      'Auth0.login.transactions',
-      { [transaction.state]: transaction },
-      {
-        daysUntilExpire: 1
-      }
-    );
-  });
-  it('`get` without a transaction should return undefined', () => {
-    expect(tm.get(transaction.state)).toBeUndefined();
-  });
-  it('`get` with a transaction should return the transaction', () => {
-    tm.create(transaction);
-    expect(tm.get(transaction.state)).toMatchObject(transaction);
   });
-  it('`remove` removes the transaction', () => {
-    tm.create(transaction);
-    tm.remove(transaction.state);
-    expect(tm.get(transaction.state)).toBeUndefined();
+  describe('constructor', () => {
+    it('loads transactions from localStorage (per key)', () => {
+      getStorageMock().getAllKeys.mockReturnValue([
+        'Auth0.spa-js.transactions.key1',
+        'Auth0.spa-js.transactions.key2'
+      ]);
+      tm = new TransactionManager();
+      expect(getStorageMock().getAllKeys).toHaveBeenCalled();
+      expect(getStorageMock().get).toHaveBeenCalledWith(`${COOKIE_KEY}key1`);
+      expect(getStorageMock().get).toHaveBeenCalledWith(`${COOKIE_KEY}key2`);
+    });
+    it('does not load transactions if none was added', () => {
+      getStorageMock().getAllKeys.mockReturnValue([]);
+      tm = new TransactionManager();
+      expect(getStorageMock().getAllKeys).toHaveBeenCalled();
+      expect(getStorageMock().get).not.toHaveBeenCalled();
+    });
   });
-  it('`remove` saves new transactions in storage', () => {
-    const secondState = 'stateIn2';
-    tm.create(transaction);
-    tm.create({ ...transaction, state: secondState });
-    tm.remove(secondState);
-    expect(getStorageMock().save).toHaveBeenLastCalledWith(
-      'Auth0.login.transactions',
-      { [transaction.state]: transaction },
-      {
-        daysUntilExpire: 1
-      }
-    );
+  describe('with empty transactions', () => {
+    beforeEach(() => {
+      getStorageMock().getAllKeys.mockReturnValue([
+        `${COOKIE_KEY}key1`,
+        `${COOKIE_KEY}key2`
+      ]);
+      tm = new TransactionManager();
+    });
+    it('`create` creates the transaction', () => {
+      tm.create(transaction);
+      expect(tm.get(transaction.state)).toMatchObject(transaction);
+    });
+    it('`create` saves the transaction in the storage', () => {
+      tm.create(transaction);
+      expect(getStorageMock().save).toHaveBeenCalledWith(
+        `Auth0.spa-js.transactions.${transaction.state}`,
+        transaction,
+        {
+          daysUntilExpire: 1
+        }
+      );
+    });
+    it('`get` without a transaction should return undefined', () => {
+      expect(tm.get(transaction.state)).toBeUndefined();
+    });
+    it('`get` with a transaction should return the transaction', () => {
+      tm.create(transaction);
+      expect(tm.get(transaction.state)).toMatchObject(transaction);
+    });
+    it('`remove` removes the transaction', () => {
+      tm.create(transaction);
+      tm.remove(transaction.state);
+      expect(tm.get(transaction.state)).toBeUndefined();
+    });
+    it('`remove` removes transaction from storage', () => {
+      tm.create(transaction);
+      tm.remove(transaction.state);
+      expect(getStorageMock().remove).toHaveBeenLastCalledWith(
+        `Auth0.spa-js.transactions.${transaction.state}`
+      );
+    });
   });
 });