Repository: auth0/auth0.js
Pull request #270 (2016-12-15 14:01:46)

Cookie fallback for storage

codecov-io: ## [Current coverage](https://codecov.io/gh/auth0/auth0.js/pull/270?src=pr) is 98.92% (diff: 88.63%)
> Merging [#270](https://codecov.io/gh/auth0/auth0.js/pull/270?src=pr) into [v8](https://codecov.io/gh/auth0/auth0.js/branch/v8?src=pr) will decrease coverage by **1.07%**

```diff
@@            v8       #270   diff @@
=====================================
  Files       26         30     +4   
  Lines      847        927    +80   
  Methods    151        164    +13   
  Messages     0          0          
  Branches   123        133    +10   
=====================================
+ Hits       847        917    +70   
- Misses       0         10    +10   
  Partials     0          0          
```

> Powered by [Codecov](https://codecov.io?src=pr). Last update [48cc54a...9c7a76d](https://codecov.io/gh/auth0/auth0.js/compare/48cc54a3b8011e574bbf043adad9115e82ef1bf4...9c7a76dd35dbce06fac9ca38a286f8802e39337c?src=pr)

Patch:
diff --git a/src/helper/cookies.js b/src/helper/cookies.js
new file mode 100644
index 0000000..56ffb03
--- /dev/null
+++ b/src/helper/cookies.js
@@ -0,0 +1,58 @@
+var windowHandler = require('./window');
+var base64Url = require('./base64_url');
+
+function create(name, value, days) {
+  var date;
+  var expires;
+
+  if (windowHandler.getDocument().cookie === undefined
+      || windowHandler.getDocument().cookie === null) {
+    throw new Error('cookie storage not available');
+  }
+
+  if (days) {
+    date = new Date();
+    date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
+    expires = '; expires=' + date.toGMTString();
+  } else {
+    expires = '';
+  }
+
+  windowHandler.getDocument().cookie = name + '=' + base64Url.encode(value) + expires + '; path=/';
+}
+
+function read(name) {
+  var i;
+  var cookie;
+  var cookies;
+  var nameEQ = name + '=';
+
+  if (windowHandler.getDocument().cookie === undefined
+      || windowHandler.getDocument().cookie === null) {
+    throw new Error('cookie storage not available');
+  }
+
+  cookies = windowHandler.getDocument().cookie.split(';');
+
+  for (i = 0; i < cookies.length; i++) {
+    cookie = cookies[i];
+    while (cookie.charAt(0) === ' ') {
+      cookie = cookie.substring(1, cookie.length);
+    }
+    if (cookie.indexOf(nameEQ) === 0) {
+      return base64Url.decode(cookie.substring(nameEQ.length, cookie.length));
+    }
+  }
+
+  return null;
+}
+
+function erase(name) {
+  create(name, '', -1);
+}
+
+module.exports = {
+  create: create,
+  read: read,
+  erase: erase
+};
diff --git a/src/helper/storage.js b/src/helper/storage.js
index cbea77b..4e08c6f 100644
--- a/src/helper/storage.js
+++ b/src/helper/storage.js
@@ -1,18 +1,17 @@
-var windowHandler = require('./window');
+var StorageHandler = require('./storage/handler');
+var storage;
 
-function DummyStorage() {}
-DummyStorage.prototype.getItem = function (key) { return null; };
-DummyStorage.prototype.removeItem = function (key) {};
-DummyStorage.prototype.setItem = function (key, value) {};
-
-function getStorage() {
-  return windowHandler.getWindow().localStorage || new DummyStorage();
+function getStorage(force) {
+  if (!storage || force) {
+    storage = new StorageHandler();
+  }
+  return storage;
 }
 
 module.exports = {
   getItem: function (key) {
     var value = getStorage().getItem(key);
-    return JSON.parse(value);
+    return value ? JSON.parse(value) : value;
   },
   removeItem: function (key) {
     return getStorage().removeItem(key);
@@ -20,5 +19,8 @@ module.exports = {
   setItem: function (key, value) {
     var json = JSON.stringify(value);
     return getStorage().setItem(key, json);
+  },
+  reload: function() {
+    getStorage(true);
   }
 };
diff --git a/src/helper/storage/cookie.js b/src/helper/storage/cookie.js
new file mode 100644
index 0000000..e34a872
--- /dev/null
+++ b/src/helper/storage/cookie.js
@@ -0,0 +1,18 @@
+var windowHandler = require('../window');
+var cookies = require('../cookies');
+
+function CookieStorage() {}
+
+CookieStorage.prototype.getItem = function (key) {
+  return cookies.read(key);
+};
+
+CookieStorage.prototype.removeItem = function (key) {
+  cookies.erase(key);
+};
+
+CookieStorage.prototype.setItem = function (key, value) {
+  cookies.create(key, value, 1);
+};
+
+module.exports = CookieStorage;
\ No newline at end of file
diff --git a/src/helper/storage/dummy.js b/src/helper/storage/dummy.js
new file mode 100644
index 0000000..ceadbc8
--- /dev/null
+++ b/src/helper/storage/dummy.js
@@ -0,0 +1,9 @@
+function DummyStorage() {}
+
+DummyStorage.prototype.getItem = function (key) { return null; };
+
+DummyStorage.prototype.removeItem = function (key) {};
+
+DummyStorage.prototype.setItem = function (key, value) {};
+
+module.exports = DummyStorage;
\ No newline at end of file
diff --git a/src/helper/storage/handler.js b/src/helper/storage/handler.js
new file mode 100644
index 0000000..d27c40f
--- /dev/null
+++ b/src/helper/storage/handler.js
@@ -0,0 +1,54 @@
+var windowHandler = require('../window');
+var DummyStorage = require('./dummy');
+var CookieStorage = require('./cookie');
+var Warn = require('../warn');
+
+function StorageHandler() {
+  this.warn = new Warn({});
+  this.storage = windowHandler.getWindow().localStorage || new CookieStorage();
+}
+
+StorageHandler.prototype.failover = function () {
+  if (this.storage instanceof DummyStorage) {
+    this.warn.warning('DummyStorage: ignore failover');
+    return;
+  } else if (this.storage instanceof CookieStorage) {
+    this.warn.warning('CookieStorage: failing over DummyStorage');
+    this.storage = new DummyStorage();
+  } else {
+    this.warn.warning('LocalStorage: failing over CookieStorage');
+    this.storage = new CookieStorage();
+  }
+};
+
+StorageHandler.prototype.getItem = function (key) {
+  try {
+    return this.storage.getItem(key);
+  } catch (e) {
+    this.warn.warning(e);
+    this.failover();
+    return this.getItem(key);
+  }
+};
+
+StorageHandler.prototype.removeItem = function (key) {
+  try {
+    return this.storage.removeItem(key);
+  } catch (e) {
+    this.warn.warning(e);
+    this.failover();
+    return this.removeItem(key);
+  }
+};
+
+StorageHandler.prototype.setItem = function (key, value) {
+  try {
+    return this.storage.setItem(key, value);
+  } catch (e) {
+    this.warn.warning(e);
+    this.failover();
+    return this.setItem(key, value);
+  }
+};
+
+module.exports = StorageHandler;


Test patch:
diff --git a/test/helper/cookies.test.js b/test/helper/cookies.test.js
new file mode 100644
index 0000000..f29c690
--- /dev/null
+++ b/test/helper/cookies.test.js
@@ -0,0 +1,108 @@
+var expect = require('expect.js');
+var stub = require('sinon').stub;
+
+var windowHandler = require('../../src/helper/window');
+var cookies = require('../../src/helper/cookies');
+
+describe('helpers cookies', function () {
+
+  beforeEach(function(){
+    var document = {
+      cookie: ''
+    }
+
+    stub(windowHandler, 'getDocument', function (message) {
+      return document;
+    });
+    stub(Date.prototype, 'getTime', function (message) {
+      return 0;
+    });
+  })
+
+  afterEach(function(){
+    windowHandler.getDocument.restore();
+    Date.prototype.getTime.restore();
+  })
+
+  it('create a cookie with exp', function () {
+
+    cookies.create('cookie_name', 'cookie value', 1);
+    expect(windowHandler.getDocument().cookie).to.be.eql('cookie_name=Y29va2llIHZhbHVl; expires=Fri, 02 Jan 1970 00:00:00 GMT; path=/')
+
+  });
+
+  it('create a cookie without exp', function () {
+
+    cookies.create('cookie_name', 'cookie value');
+    expect(windowHandler.getDocument().cookie).to.be.eql('cookie_name=Y29va2llIHZhbHVl; path=/')
+
+  });
+
+  it('returns null if the cookie does not exist', function () {
+
+    var value = cookies.read('cookie_name');
+    expect(value).to.be.eql(null);
+
+  });
+
+  it('returns the cookie value', function () {
+
+    cookies.create('cookie_name', 'cookie value');
+    expect(windowHandler.getDocument().cookie).to.be.eql('cookie_name=Y29va2llIHZhbHVl; path=/')
+
+    var value = cookies.read('cookie_name');
+    expect(value).to.be.eql('cookie value')
+
+  });
+
+
+  it('should handle multiple cookies', function () {
+
+    windowHandler.getDocument.restore();
+    stub(windowHandler, 'getDocument', function (message) {
+      return {
+        cookie: 'cookie_name=Y29va2llIHZhbHVl; path=/; cookie_name2=Y29va2llIHZhbHVlMg; path=/'
+      };
+    });
+
+    var value = cookies.read('cookie_name2');
+    expect(value).to.be.eql('cookie value2')
+  });
+
+  it('returns the cookie value (with ;)', function () {
+
+    cookies.create('cookie_name', 'cookie; value');
+    expect(windowHandler.getDocument().cookie).to.be.eql('cookie_name=Y29va2llOyB2YWx1ZQ; path=/')
+
+    var value = cookies.read('cookie_name');
+    expect(value).to.be.eql('cookie; value')
+
+  });
+
+  it('should reset the expiration', function () {
+
+    cookies.create('cookie_name', 'cookie; value');
+    expect(windowHandler.getDocument().cookie).to.be.eql('cookie_name=Y29va2llOyB2YWx1ZQ; path=/')
+
+    cookies.erase('cookie_name');
+
+    expect(windowHandler.getDocument().cookie).to.be.eql('cookie_name=; expires=Wed, 31 Dec 1969 00:00:00 GMT; path=/')
+  });
+
+  it('handle cookie not available', function () {
+
+    windowHandler.getDocument.restore();
+    stub(windowHandler, 'getDocument', function (message) {
+      return {};
+    });
+    expect(function () {
+      cookies.create('cookie_name', 'cookie; value');
+    }).to.throwError();
+
+    expect(function () {
+      cookies.read('cookie_name');
+    }).to.throwError();
+
+  });
+
+});
diff --git a/test/helper/storage-handler.test.js b/test/helper/storage-handler.test.js
new file mode 100644
index 0000000..0edd9aa
--- /dev/null
+++ b/test/helper/storage-handler.test.js
@@ -0,0 +1,145 @@
+var expect = require('expect.js');
+var stub = require('sinon').stub;
+
+var windowHandler = require('../../src/helper/window');
+var StorageHandler = require('../../src/helper/storage/handler');
+var CookieStorage = require('../../src/helper/storage/cookie');
+var DummyStorage = require('../../src/helper/storage/dummy');
+
+function MockLocalStorage() {}
+MockLocalStorage.prototype.getItem = function() { throw new Error('fail'); }
+MockLocalStorage.prototype.removeItem = function() { throw new Error('fail'); }
+MockLocalStorage.prototype.setItem = function() { throw new Error('fail'); }
+
+describe('helpers storage handler', function () {
+  it('should use localStorage by default', function () {
+    stub(windowHandler, 'getWindow', function (message) {
+      return {
+        localStorage: new MockLocalStorage()
+      };
+    });
+
+    var handler = new StorageHandler();
+    expect(handler.storage).to.be.a(MockLocalStorage);
+
+    windowHandler.getWindow.restore();
+  });
+
+  it('should use cookie storage is localstorage is not available', function () {
+    stub(windowHandler, 'getWindow', function (message) {
+      return {};
+    });
+
+    var handler = new StorageHandler();
+    expect(handler.storage).to.be.a(CookieStorage);
+
+    windowHandler.getWindow.restore();
+  });
+
+  it('should use cookie storage is localstorage fails with getItem', function () {
+    stub(windowHandler, 'getWindow', function (message) {
+      return {
+        localStorage: new MockLocalStorage()
+      };
+    });
+    stub(windowHandler, 'getDocument', function (message) {
+      return {
+        cookie: ''
+      };
+    });
+
+    var handler = new StorageHandler();
+
+    expect(handler.storage).to.be.a(MockLocalStorage);
+    handler.getItem('pepe');
+
+    expect(handler.storage).to.be.a(CookieStorage);
+
+    windowHandler.getWindow.restore();
+    windowHandler.getDocument.restore();
+  });
+
+  it('should use cookie storage is localstorage fails with setItem', function () {
+    var document = {
+      cookie: ''
+    };
+    stub(windowHandler, 'getWindow', function (message) {
+      return {
+        localStorage: new MockLocalStorage()
+      };
+    });
+    stub(windowHandler, 'getDocument', function (message) {
+      return document;
+    });
+    stub(Date.prototype, 'getTime', function (message) {
+      return 0;
+    });
+
+    var handler = new StorageHandler();
+
+    expect(handler.storage).to.be.a(MockLocalStorage);
+    handler.setItem('some', 'value');
+
+    expect(handler.storage).to.be.a(CookieStorage);
+    expect(document.cookie).to.be('some=dmFsdWU; expires=Fri, 02 Jan 1970 00:00:00 GMT; path=/');
+
+    windowHandler.getWindow.restore();
+    windowHandler.getDocument.restore();
+    Date.prototype.getTime.restore();
+  });
+
+  it('should use cookie storage is localstorage fails with removeItem', function () {
+    var document = {
+      cookie: ''
+    };
+    stub(windowHandler, 'getWindow', function (message) {
+      return {
+        localStorage: new MockLocalStorage()
+      };
+    });
+    stub(windowHandler, 'getDocument', function (message) {
+      return document;
+    });
+    stub(Date.prototype, 'getTime', function (message) {
+      return 0;
+    });
+
+    var handler = new StorageHandler();
+
+    expect(handler.storage).to.be.a(MockLocalStorage);
+    handler.removeItem('some');
+
+    expect(handler.storage).to.be.a(CookieStorage);
+    expect(document.cookie).to.be('some=; expires=Wed, 31 Dec 1969 00:00:00 GMT; path=/');
+
+    windowHandler.getWindow.restore();
+    windowHandler.getDocument.restore();
+    Date.prototype.getTime.restore();
+  });
+
+  it('should failover to dummy', function () {
+    var document = {
+      cookie: ''
+    };
+    stub(windowHandler, 'getWindow', function (message) {
+      return {
+        localStorage: new MockLocalStorage()
+      };
+    });
+
+    var handler = new StorageHandler();
+
+    expect(handler.storage).to.be.a(MockLocalStorage);
+    handler.failover();
+
+    expect(handler.storage).to.be.a(CookieStorage);
+    handler.failover();
+
+    expect(handler.storage).to.be.a(DummyStorage);
+    handler.failover();
+
+    expect(handler.storage).to.be.a(DummyStorage);
+
+    windowHandler.getWindow.restore();
+  });
+});
diff --git a/test/helper/storage.test.js b/test/helper/storage.test.js
index 86af80f..7f1956e 100644
--- a/test/helper/storage.test.js
+++ b/test/helper/storage.test.js
@@ -5,8 +5,11 @@ var windowHandler = require('../../src/helper/window');
 var storage = require('../../src/helper/storage');
 
 describe('helpers storage', function () {
-  describe('with localstorage', function () {
+  beforeEach(function(){
+    storage.reload();
+  })
 
+  describe('with localstorage', function () {
     before(function(){
       var data = {};
       stub(windowHandler, 'getWindow', function () {
@@ -33,15 +36,52 @@ describe('helpers storage', function () {
     });
   });
 
-  describe('without localstorage', function () {
+  describe('without localstorage and with cookies', function () {
     before(function(){
+      var document = {
+        cookie: ''
+      };
       stub(windowHandler, 'getWindow', function () {
         return {
+          localStorage: {
+            getItem: function(key) { throw new Error('localStorage not available') }
+          }
         };
       });
+      stub(windowHandler, 'getDocument', function () {
+        return document;
+      });
+    });
+
+    after(function(){
+      windowHandler.getDocument.restore();
+      windowHandler.getWindow.restore();
+    });
+
+    it('should store stuff', function () {
+      expect(storage.getItem('data')).to.be(null);
+      storage.setItem('data', 'text');
+      expect(storage.getItem('data')).to.eql('text');
+      storage.removeItem('data');
+      // Cookies mock does not delete the cookie since it works as a variable not an actual method.
+      // When it depetes the cookie it stays as an empty string. The browser should delete it
+      // for real and return null instead
+      expect(storage.getItem('data')).to.be('');
+    });
+  });
+
+  describe('with dummy storage', function () {
+    before(function(){
+      stub(windowHandler, 'getWindow', function () {
+        return {};
+      });
+      stub(windowHandler, 'getDocument', function () {
+        return {};
+      });
     });
 
     after(function(){
+      windowHandler.getDocument.restore();
       windowHandler.getWindow.restore();
     });
 
diff --git a/test/web-auth/web-auth.test.js b/test/web-auth/web-auth.test.js
index 5039dca..e9e1a23 100644
--- a/test/web-auth/web-auth.test.js
+++ b/test/web-auth/web-auth.test.js
@@ -2,6 +2,7 @@ var expect = require('expect.js');
 var stub = require('sinon').stub;
 var request = require('superagent');
 
+var storage = require('../../src/helper/storage');
 var IframeHandler = require('../../src/helper/iframe-handler');
 
 var RequestMock = require('../mock/request-mock');
@@ -29,6 +30,7 @@ describe('auth0.WebAuth', function () {
           appState: null
         });
       };
+      storage.reload();
     })
 
     it('should fail if the nonce is not valid', function (done) {