diff --git a/package.json b/package.json index 9f42c32..00520a6 100644 --- a/package.json +++ b/package.json @@ -43,5 +43,8 @@ }, "dependencies": { "js-cookie": "^2.1.4" + }, + "jest": { + "setupFiles": ["./test/browserMocks.js"] } } diff --git a/src/middleware.js b/src/middleware.js index 91f565e..3dc3bea 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -1,4 +1,4 @@ -import createLocalStorageStore from './storage/localStorage' +import createAdaptiveStore from './storage/adaptive' import { AUTHENTICATE } from './actionTypes' import { authenticateFailed, @@ -8,7 +8,7 @@ import { } from './actions' const createAuthMiddleware = (config = {}) => { - const storage = config.storage || createLocalStorageStore() + const storage = config.storage || createAdaptiveStore() const authenticators = config.authenticators || [] const findAuthenticator = name => diff --git a/src/storage.js b/src/storage.js index d5594c6..2e7e254 100644 --- a/src/storage.js +++ b/src/storage.js @@ -1,2 +1,3 @@ +export { default as createAdaptiveStore } from './storage/adaptive' export { default as createLocalStorageStore } from './storage/localStorage' export { default as createCookieStore } from './storage/cookie' diff --git a/src/storage/adaptive.js b/src/storage/adaptive.js new file mode 100644 index 0000000..3a6bee1 --- /dev/null +++ b/src/storage/adaptive.js @@ -0,0 +1,16 @@ +import createCookieStore from './cookie' +import createLocalStorageStore from './localStorage' +import { isLocalStorageAvailable } from '../utils/localStorage' + +const createAdaptiveStore = ({ + localStorageKey: key, + cookieName: name, + cookieDomain: domain, + cookieExpires: expires, + cookieSecure: secure +} = {}) => + isLocalStorageAvailable() ? + createLocalStorageStore({ key }) : + createCookieStore({ name, domain, expires, secure }) + +export default createAdaptiveStore diff --git a/src/utils/localStorage.js b/src/utils/localStorage.js new file mode 100644 index 0000000..be8a9c4 --- /dev/null +++ b/src/utils/localStorage.js @@ -0,0 +1,11 @@ +const LOCAL_STORAGE_TEST_KEY = '_redux-simple-auth-test' + +export const isLocalStorageAvailable = () => { + try { + localStorage.setItem(LOCAL_STORAGE_TEST_KEY, true) + localStorage.removeItem(LOCAL_STORAGE_TEST_KEY) + return true + } catch (e) { + return false + } +} diff --git a/test/browserMocks.js b/test/browserMocks.js new file mode 100644 index 0000000..dfc1439 --- /dev/null +++ b/test/browserMocks.js @@ -0,0 +1,19 @@ +const createLocalStorageMock = () => { + const store = {} + + return { + setItem(key, value) { + store[key] = value.toString() + }, + getItem(key) { + return store[key] || null + }, + removeItem(key) { + delete store[key] + } + } +} + +Object.defineProperty(window, 'localStorage', { + value: createLocalStorageMock() +}) diff --git a/test/storage/adaptive.spec.js b/test/storage/adaptive.spec.js new file mode 100644 index 0000000..30ed537 --- /dev/null +++ b/test/storage/adaptive.spec.js @@ -0,0 +1,91 @@ +import { createAdaptiveStore } from '../../src/storage' +import Cookie from 'js-cookie' + +describe('Adaptive store', () => { + describe('it builds a store', () => { + const store = createAdaptiveStore() + + expect(store).toHaveProperty('persist') + expect(store).toHaveProperty('restore') + }) + + describe('when localStorage is available', () => { + it('builds localStorage store if available', () => { + const setItemSpy = jest.spyOn(localStorage, 'setItem') + const getItemSpy = jest.spyOn(localStorage, 'getItem') + + const store = createAdaptiveStore() + store.persist({ key: 'value' }) + store.restore() + + expect(setItemSpy).toHaveBeenCalled() + expect(getItemSpy).toHaveBeenCalled() + + setItemSpy.mockRestore() + getItemSpy.mockRestore() + }) + + it('honors local storage options', () => { + const setItemSpy = jest.spyOn(localStorage, 'setItem') + const getItemSpy = jest.spyOn(localStorage, 'getItem') + + const store = createAdaptiveStore({ localStorageKey: 'my-custom-key' }) + store.persist({ key: 'value' }) + store.restore() + + expect(setItemSpy).toHaveBeenCalledWith( + 'my-custom-key', + JSON.stringify({ key: 'value' }) + ) + expect(getItemSpy).toHaveBeenCalledWith('my-custom-key') + + setItemSpy.mockRestore() + getItemSpy.mockRestore() + }) + }) + + describe('when local storage is not available', () => { + beforeEach(() => { + localStorage.setItem = jest.fn(() => { throw new Error }) + }) + + it('builds cookie store', () => { + const setSpy = jest.spyOn(Cookie, 'set') + const getJSONSpy = jest.spyOn(Cookie, 'getJSON') + + const store = createAdaptiveStore() + store.persist({ key: 'value' }) + store.restore() + + expect(setSpy).toHaveBeenCalled() + expect(getJSONSpy).toHaveBeenCalled() + }) + + it('honors cookie options', () => { + const setSpy = jest.spyOn(Cookie, 'set') + const getJSONSpy = jest.spyOn(Cookie, 'getJSON') + + const store = createAdaptiveStore({ + cookieName: 'my-custom-cookie', + cookieDomain: 'example.com', + cookiePath: '/', + cookieSecure: true, + cookieExpires: 120 + }) + store.persist({ key: 'value' }) + store.restore() + + expect(setSpy).toHaveBeenCalledWith( + 'my-custom-cookie', + { key: 'value' }, + { + domain: 'example.com', + path: '/', + secure: true, + expires: expect.any(Date) + } + ) + expect(getJSONSpy).toHaveBeenCalledWith('my-custom-cookie') + }) + }) +}) diff --git a/test/sessionStores/cookie.spec.js b/test/storage/cookie.spec.js similarity index 100% rename from test/sessionStores/cookie.spec.js rename to test/storage/cookie.spec.js diff --git a/test/sessionStores/localStorage.spec.js b/test/storage/localStorage.spec.js similarity index 100% rename from test/sessionStores/localStorage.spec.js rename to test/storage/localStorage.spec.js diff --git a/test/utils/localStorage.spec.js b/test/utils/localStorage.spec.js new file mode 100644 index 0000000..5edca1f --- /dev/null +++ b/test/utils/localStorage.spec.js @@ -0,0 +1,20 @@ +import { isLocalStorageAvailable } from '../../src/utils/localStorage' + +describe('isLocalStorageAvailable', () => { + it('when local storage is available it returns true', () => { + const result = isLocalStorageAvailable() + + expect(result).toBe(true) + }) + + it('when localStorage is not available it returns false', () => { + const originalSetItem = localStorage.setItem + localStorage.setItem = jest.fn(() => { throw new Error('') }) + + const result = isLocalStorageAvailable() + + expect(result).toBe(false) + + localStorage.setItem = originalSetItem + }) +})