diff --git a/src/helper/cookies.js b/src/helper/cookies.js new file mode 100644 index 00000000..56ffb036 --- /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 cbea77b5..4e08c6ff 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 00000000..e34a8721 --- /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 00000000..ceadbc8d --- /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 00000000..d27c40fe --- /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; diff --git a/test/helper/cookies.test.js b/test/helper/cookies.test.js new file mode 100644 index 00000000..f29c6901 --- /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 00000000..0edd9aa9 --- /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 86af80f5..7f1956e7 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 5039dca8..e9e1a238 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) {