diff --git a/admin/src/main.js b/admin/src/main.js index 6a59f1cc7..2743e0f9a 100644 --- a/admin/src/main.js +++ b/admin/src/main.js @@ -11,11 +11,8 @@ import addNavigationGuards from './router/guards' import i18n from './i18n' -import { ApolloClient, ApolloLink, InMemoryCache, HttpLink } from 'apollo-boost' import VueApollo from 'vue-apollo' -import CONFIG from './config' - import { BootstrapVue, IconsPlugin } from 'bootstrap-vue' import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue/dist/bootstrap-vue.css' @@ -23,37 +20,7 @@ import 'bootstrap-vue/dist/bootstrap-vue.css' import moment from 'vue-moment' import Toasted from 'vue-toasted' -const httpLink = new HttpLink({ uri: CONFIG.GRAPHQL_URI }) - -const authLink = new ApolloLink((operation, forward) => { - const token = store.state.token - - operation.setContext({ - headers: { - Authorization: token && token.length > 0 ? `Bearer ${token}` : '', - }, - }) - return forward(operation).map((response) => { - if (response.errors && response.errors[0].message === '403.13 - Client certificate revoked') { - response.errors[0].message = i18n.t('error.session-expired') - store.dispatch('logout', null) - if (router.currentRoute.path !== '/logout') router.push('/logout') - return response - } - const newToken = operation.getContext().response.headers.get('token') - if (newToken) store.commit('token', newToken) - return response - }) -}) - -const apolloClient = new ApolloClient({ - link: authLink.concat(httpLink), - cache: new InMemoryCache(), -}) - -const apolloProvider = new VueApollo({ - defaultClient: apolloClient, -}) +import { apolloProvider } from './plugins/apolloProvider' Vue.use(BootstrapVue) diff --git a/admin/src/main.test.js b/admin/src/main.test.js index 747ef5d2a..06efa8b65 100644 --- a/admin/src/main.test.js +++ b/admin/src/main.test.js @@ -101,77 +101,4 @@ describe('main', () => { }), ) }) - - describe('ApolloLink', () => { - // mock store - const storeDispatchMock = jest.fn() - store.state = { - token: 'some-token', - } - store.dispatch = storeDispatchMock - - // mock i18n.t - i18n.t = jest.fn((t) => t) - - // mock apllo response - const responseMock = { - errors: [{ message: '403.13 - Client certificate revoked' }], - } - - // mock router - const routerPushMock = jest.fn() - router.push = routerPushMock - router.currentRoute = { - path: '/overview', - } - - // mock context - const setContextMock = jest.fn() - const getContextMock = jest.fn(() => { - return { - response: { - headers: { - get: jest.fn(), - }, - }, - } - }) - - // mock apollo link function params - const operationMock = { - setContext: setContextMock, - getContext: getContextMock, - } - - const forwardMock = jest.fn(() => { - return [responseMock] - }) - - // get apollo link callback - const middleware = ApolloLink.mock.calls[0][0] - - beforeEach(() => { - jest.clearAllMocks() - // run the callback with mocked params - middleware(operationMock, forwardMock) - }) - - it('sets authorization header', () => { - expect(setContextMock).toBeCalledWith({ - headers: { - Authorization: 'Bearer some-token', - }, - }) - }) - - describe('apollo response is 403.13', () => { - it.skip('dispatches logout', () => { - expect(storeDispatchMock).toBeCalledWith('logout', null) - }) - - it.skip('redirects to logout', () => { - expect(routerPushMock).toBeCalledWith('/logout') - }) - }) - }) }) diff --git a/admin/src/plugins/apolloProvider.js b/admin/src/plugins/apolloProvider.js new file mode 100644 index 000000000..ec7df38f1 --- /dev/null +++ b/admin/src/plugins/apolloProvider.js @@ -0,0 +1,37 @@ +import { ApolloClient, ApolloLink, InMemoryCache, HttpLink } from 'apollo-boost' +import VueApollo from 'vue-apollo' +import CONFIG from '../config' +import store from '../store/store' +import router from '../router/router' +import i18n from '../i18n' + +const httpLink = new HttpLink({ uri: CONFIG.GRAPHQL_URI }) + +const authLink = new ApolloLink((operation, forward) => { + const token = store.state.token + operation.setContext({ + headers: { + Authorization: token && token.length > 0 ? `Bearer ${token}` : '', + }, + }) + return forward(operation).map((response) => { + if (response.errors && response.errors[0].message === '403.13 - Client certificate revoked') { + response.errors[0].message = i18n.t('error.session-expired') + store.dispatch('logout', null) + if (router.currentRoute.path !== '/login') router.push('/login') + return response + } + const newToken = operation.getContext().response.headers.get('token') + if (newToken) store.commit('token', newToken) + return response + }) +}) + +const apolloClient = new ApolloClient({ + link: authLink.concat(httpLink), + cache: new InMemoryCache(), +}) + +export const apolloProvider = new VueApollo({ + defaultClient: apolloClient, +}) diff --git a/admin/src/plugins/apolloProvider.test.js b/admin/src/plugins/apolloProvider.test.js new file mode 100644 index 000000000..3610b5dae --- /dev/null +++ b/admin/src/plugins/apolloProvider.test.js @@ -0,0 +1,178 @@ +import { ApolloClient, ApolloLink, HttpLink } from 'apollo-boost' +import './apolloProvider' +import CONFIG from '../config' + +import VueApollo from 'vue-apollo' +import store from '../store/store' +import router from '../router/router' +import i18n from '../i18n' + +jest.mock('vue-apollo') +jest.mock('../store/store') +jest.mock('../router/router') +jest.mock('../i18n') + +jest.mock('apollo-boost', () => { + return { + __esModule: true, + ApolloClient: jest.fn(), + ApolloLink: jest.fn(() => { + return { concat: jest.fn() } + }), + InMemoryCache: jest.fn(), + HttpLink: jest.fn(), + } +}) + +describe('apolloProvider', () => { + it('calls the HttpLink', () => { + expect(HttpLink).toBeCalledWith({ uri: CONFIG.GRAPHQL_URI }) + }) + + it('calls the ApolloLink', () => { + expect(ApolloLink).toBeCalled() + }) + + it('calls the ApolloClient', () => { + expect(ApolloClient).toBeCalled() + }) + + it('calls the VueApollo', () => { + expect(VueApollo).toBeCalled() + }) + + describe('ApolloLink', () => { + // mock store + const storeDispatchMock = jest.fn() + const storeCommitMock = jest.fn() + store.state = { + token: 'some-token', + } + store.dispatch = storeDispatchMock + store.commit = storeCommitMock + + // mock i18n.t + i18n.t = jest.fn((t) => t) + + // mock apllo response + const responseMock = { + errors: [{ message: '403.13 - Client certificate revoked' }], + } + + // mock router + const routerPushMock = jest.fn() + router.push = routerPushMock + router.currentRoute = { + path: '/overview', + } + + // mock context + const setContextMock = jest.fn() + const getContextMock = jest.fn(() => { + return { + response: { + headers: { + get: jest.fn(() => 'another-token'), + }, + }, + } + }) + + // mock apollo link function params + const operationMock = { + setContext: setContextMock, + getContext: getContextMock, + } + + const forwardMock = jest.fn(() => { + return [responseMock] + }) + + // get apollo link callback + const middleware = ApolloLink.mock.calls[0][0] + + describe('with token in store', () => { + it('sets authorization header with token', () => { + // run the apollo link callback with mocked params + middleware(operationMock, forwardMock) + expect(setContextMock).toBeCalledWith({ + headers: { + Authorization: 'Bearer some-token', + }, + }) + }) + }) + + describe('without token in store', () => { + beforeEach(() => { + store.state.token = null + }) + + it('sets authorization header empty', () => { + middleware(operationMock, forwardMock) + expect(setContextMock).toBeCalledWith({ + headers: { + Authorization: '', + }, + }) + }) + }) + + describe('apollo response is 403.13', () => { + beforeEach(() => { + // run the apollo link callback with mocked params + middleware(operationMock, forwardMock) + }) + + it('dispatches logout', () => { + expect(storeDispatchMock).toBeCalledWith('logout', null) + }) + + describe('current route is not login', () => { + it('redirects to logout', () => { + expect(routerPushMock).toBeCalledWith('/login') + }) + }) + + describe('current route is login', () => { + beforeEach(() => { + jest.clearAllMocks() + router.currentRoute.path = '/login' + }) + + it('does not redirect to login', () => { + expect(routerPushMock).not.toBeCalled() + }) + }) + }) + + describe('apollo response is with new token', () => { + beforeEach(() => { + delete responseMock.errors + middleware(operationMock, forwardMock) + }) + + it('commits new token to store', () => { + expect(storeCommitMock).toBeCalledWith('token', 'another-token') + }) + }) + + describe('apollo response is without new token', () => { + beforeEach(() => { + jest.clearAllMocks() + getContextMock.mockReturnValue({ + response: { + headers: { + get: jest.fn(() => null), + }, + }, + }) + middleware(operationMock, forwardMock) + }) + + it('does not commit token to store', () => { + expect(storeCommitMock).not.toBeCalled() + }) + }) + }) +})