diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index 4d03766bc..55660d366 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -44,6 +44,19 @@ const logout = () => { cy.location('pathname').should('contain', '/login') // we're out } +const lastColumnIsSortedInDescendingOrder = () => { + cy.get('tbody') + .find('tr td:last-child') + .then(last_column => { + cy.wrap(last_column) + const values = last_column + .map((i, td) => parseInt(td.textContent)) + .toArray() + const ordered_descending = values.slice(0).sort((a, b) => b - a) + return cy.wrap(values).should('deep.eq', ordered_descending) + }) +} + Given('I am logged in', () => { login('admin@example.org', 1234) }) @@ -52,11 +65,11 @@ Given('we have a selection of tags and categories as well as posts', () => { // TODO: use db factories instead of seed data }) -Given('my account has the following details:', (table) => { +Given('my account has the following details:', table => { // TODO: use db factories instead of seed data }) -Given('my user account has the role {string}', (role) => { +Given('my user account has the role {string}', role => { // TODO: use db factories instead of seed data }) @@ -127,28 +140,22 @@ When('I navigate to the administration dashboard', () => { .click() }) -When('I click on {string}', linkOrButton => { +When(`I click on {string}`, linkOrButton => { cy.contains(linkOrButton).click() }) Then('I can see a list of categories ordered by post count:', table => { // TODO: match the table in the feature with the html table - cy.get('thead').find('tr th').should('have.length', 3) - const last_column = cy.get('tbody').find('tr td:last-child').then((last_column) => { - cy.wrap(last_column) - const values = last_column.map((i, td) => parseInt(td.textContent)).toArray() - const ordered_descending = values.slice(0).sort((a,b) => b - a) - return cy.wrap(values).should('deep.eq', ordered_descending) - }) + cy.get('thead') + .find('tr th') + .should('have.length', 3) + lastColumnIsSortedInDescendingOrder() }) Then('I can see a list of tags ordered by user and post count:', table => { // TODO: match the table in the feature with the html table - cy.get('thead').find('tr th').should('have.length', 4) - const last_column = cy.get('tbody').find('tr td:last-child').then((last_column) => { - cy.wrap(last_column) - const values = last_column.map((i, td) => parseInt(td.textContent)).toArray() - const ordered_descending = values.slice(0).sort((a,b) => b - a) - return cy.wrap(values).should('deep.eq', ordered_descending) - }) + cy.get('thead') + .find('tr th') + .should('have.length', 4) + lastColumnIsSortedInDescendingOrder() }) diff --git a/store/auth.js b/store/auth.js index c93ce87e2..e3ed9f383 100644 --- a/store/auth.js +++ b/store/auth.js @@ -13,11 +13,6 @@ export const mutations = { SET_USER(state, user) { state.user = user || null }, - SET_USER_SETTINGS(state, userSettings) { - // state.user = Object.assign(state.user, { - // userSettings: Object.assign(this.getters['auth/userSettings'], userSettings) - // }) - }, SET_TOKEN(state, token) { state.token = token || null }, @@ -27,15 +22,15 @@ export const mutations = { } export const getters = { + isAuthenticated(state) { + return !!state.token + }, isLoggedIn(state) { return !!(state.user && state.token) }, pending(state) { return !!state.pending }, - isVerified(state) { - return !!state.user && state.user.isVerified && !!state.user.name - }, isAdmin(state) { return !!state.user && state.user.role === 'admin' }, @@ -51,20 +46,6 @@ export const getters = { token(state) { return state.token } - // userSettings(state, getters, rootState, rootGetters) { - // const userSettings = (state.user && state.user.userSettings) ? state.user.userSettings : {} - // - // const defaultLanguage = (state.user && state.user.language) ? state.user.language : rootGetters['i18n/locale'] - // let contentLanguages = !isEmpty(userSettings.contentLanguages) ? userSettings.contentLanguages : [] - // if (isEmpty(contentLanguages)) { - // contentLanguages = userSettings.uiLanguage ? [userSettings.uiLanguage] : [defaultLanguage] - // } - // - // return Object.assign({ - // uiLanguage: defaultLanguage, - // contentLanguages: contentLanguages - // }, userSettings) - // } } export const actions = { @@ -97,10 +78,8 @@ export const actions = { return getters.isLoggedIn }, async login({ commit }, { email, password }) { + commit('SET_PENDING', true) try { - commit('SET_PENDING', true) - commit('SET_USER', null) - commit('SET_TOKEN', null) const res = await this.app.apolloProvider.defaultClient .mutate({ mutation: gql(` @@ -120,22 +99,15 @@ export const actions = { }) .then(({ data }) => data && data.login) - if (res && res.token) { - await this.app.$apolloHelpers.onLogin(res.token) - commit('SET_TOKEN', res.token) - delete res.token - commit('SET_USER', res) - commit('SET_PENDING', false) - return true - } else { - commit('SET_PENDING', false) - throw new Error('THERE IS AN ERROR') - } + await this.app.$apolloHelpers.onLogin(res.token) + commit('SET_TOKEN', res.token) + const userData = Object.assign({}, res) + delete userData.token + commit('SET_USER', userData) } catch (err) { - commit('SET_USER', null) - commit('SET_TOKEN', null) - commit('SET_PENDING', false) throw new Error(err) + } finally { + commit('SET_PENDING', false) } }, async logout({ commit }) { diff --git a/store/auth.test.js b/store/auth.test.js new file mode 100644 index 000000000..98bd1ce13 --- /dev/null +++ b/store/auth.test.js @@ -0,0 +1,160 @@ +import { getters, mutations, actions } from './auth.js' + +let state +let commit + +const token = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUzIiwic2x1ZyI6Implbm55LXJvc3RvY2siLCJuYW1lIjoiSmVubnkgUm9zdG9jayIsImF2YXRhciI6Imh0dHBzOi8vczMuYW1hem9uYXdzLmNvbS91aWZhY2VzL2ZhY2VzL3R3aXR0ZXIvbXV0dV9rcmlzaC8xMjguanBnIiwiZW1haWwiOiJ1c2VyQGV4YW1wbGUub3JnIiwicm9sZSI6InVzZXIiLCJpYXQiOjE1NDUxNDQ2ODgsImV4cCI6MTYzMTU0NDY4OCwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo0MDAwIiwic3ViIjoidTMifQ.s5_JeQN9TaUPfymAXPOpbMAwhmTIg9cnOvNEcj4z75k' +const successfulLoginResponse = { + data: { + login: { + id: 'u3', + name: 'Jenny Rostock', + slug: 'jenny-rostock', + email: 'user@example.org', + avatar: + 'https://s3.amazonaws.com/uifaces/faces/twitter/mutu_krish/128.jpg', + role: 'user', + token + } + } +} +const incorrectPasswordResponse = { + data: { + login: null + }, + errors: [ + { + message: 'Incorrect password.', + locations: [ + { + line: 2, + column: 3 + } + ], + path: ['login'] + } + ] +} + +beforeEach(() => { + commit = jest.fn() +}) + +describe('getters', () => { + describe('isAuthenticated', () => { + describe('given JWT Bearer token', () => { + test('true', () => { + state = { token } + expect(getters.isAuthenticated(state)).toBe(true) + }) + }) + }) +}) + +describe('actions', () => { + let action + + describe('login', () => { + describe('given valid credentials and a successful response', () => { + beforeEach(async () => { + const response = Object.assign({}, successfulLoginResponse) + const mutate = jest.fn(() => Promise.resolve(response)) + const onLogin = jest.fn(() => Promise.resolve()) + const module = { + app: { + apolloProvider: { defaultClient: { mutate } }, + $apolloHelpers: { onLogin } + } + } + action = actions.login.bind(module) + await action( + { commit }, + { email: 'user@example.org', password: '1234' } + ) + }) + + afterEach(() => { + action = null + }) + + it('saves the JWT Bearer token', () => { + expect(commit.mock.calls).toEqual( + expect.arrayContaining([['SET_TOKEN', token]]) + ) + }) + + it('saves user data without token', () => { + expect(commit.mock.calls).toEqual( + expect.arrayContaining([ + [ + 'SET_USER', + { + id: 'u3', + name: 'Jenny Rostock', + slug: 'jenny-rostock', + email: 'user@example.org', + avatar: + 'https://s3.amazonaws.com/uifaces/faces/twitter/mutu_krish/128.jpg', + role: 'user' + } + ] + ]) + ) + }) + + it('saves pending flags in order', () => { + expect(commit.mock.calls).toEqual( + expect.arrayContaining([ + ['SET_PENDING', true], + ['SET_PENDING', false] + ]) + ) + }) + }) + + describe('given invalid credentials and incorrect password response', () => { + let onLogin + let mutate + + beforeEach(() => { + mutate = jest.fn(() => Promise.reject('This error is expected.')) + onLogin = jest.fn(() => Promise.resolve()) + const module = { + app: { + apolloProvider: { defaultClient: { mutate } }, + $apolloHelpers: { onLogin } + } + } + action = actions.login.bind(module) + }) + + afterEach(() => { + action = null + }) + + it('populates error messages', async () => { + expect( + action({ commit }, { email: 'user@example.org', password: 'wrong' }) + ).rejects.toThrowError('This error is expected.') + expect(mutate).toHaveBeenCalled() + expect(onLogin).not.toHaveBeenCalled() + }) + + it('saves pending flags in order', async () => { + try { + await action( + { commit }, + { email: 'user@example.org', password: 'wrong' } + ) + } catch (err) {} // ignore + expect(commit.mock.calls).toEqual( + expect.arrayContaining([ + ['SET_PENDING', true], + ['SET_PENDING', false] + ]) + ) + }) + }) + }) +})