From fec665eaf4a8185dadf1a8a10809274be679ac4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 17 Dec 2018 21:43:45 +0100 Subject: [PATCH 01/10] Implement+test auth.isAuthenticated() --- store/auth.js | 3 +++ store/auth.test.js | 14 ++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 store/auth.test.js diff --git a/store/auth.js b/store/auth.js index c93ce87e2..7a8a658c9 100644 --- a/store/auth.js +++ b/store/auth.js @@ -27,6 +27,9 @@ export const mutations = { } export const getters = { + isAuthenticated(state){ + return !!state.token + }, isLoggedIn(state) { return !!(state.user && state.token) }, diff --git a/store/auth.test.js b/store/auth.test.js new file mode 100644 index 000000000..f506d6d6e --- /dev/null +++ b/store/auth.test.js @@ -0,0 +1,14 @@ +import { getters, mutations, actions } from './auth.js' + +let state + +describe('isAuthenticated', () => { + describe('given JWT Bearer token', () => { + test('true', () => { + state = { + token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUxIiwic2x1ZyI6InBldGVyLWx1c3RpZyIsIm5hbWUiOiJQZXRlciBMdXN0aWciLCJhdmF0YXIiOiJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vdWlmYWNlcy9mYWNlcy90d2l0dGVyL25haXRhbmFtb3Jlbm8vMTI4LmpwZyIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5vcmciLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE1NDUwNjMzODcsImV4cCI6MTYzMTQ2MzM4NywiYXVkIjoiaHR0cHM6Ly9uaXRyby1zdGFnaW5nLmh1bWFuLWNvbm5lY3Rpb24ub3JnIiwiaXNzIjoiaHR0cHM6Ly9hcGktbml0cm8tc3RhZ2luZy5odW1hbi1jb25uZWN0aW9uLm9yZyIsInN1YiI6InUxIn0.BQEoC3J6uRqMvIVfHYmMbmfMR2BudiG5Xvn8mfcc0Kk' + } + expect(getters.isAuthenticated(state)).toBe(true) + }) + }) +}) From 01f5cfd481d48a4056035ef3acdd4a3bc467f97c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 17 Dec 2018 21:45:29 +0100 Subject: [PATCH 02/10] Run `yarn run lint --fix` --- cypress/integration/common/steps.js | 56 ++++++++++++++++++----------- store/auth.js | 2 +- store/auth.test.js | 3 +- 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index f6bb9f2b0..e05d6aaeb 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -30,11 +30,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 }) @@ -82,31 +82,47 @@ Then('I am still logged in', () => { When('I navigate to the administration dashboard', () => { cy.get('.avatar-menu').click() - cy.get('a').contains('Systemverwaltung').click() + cy.get('a') + .contains('Systemverwaltung') + .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) => { +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) + 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) + }) }) -Then('I can see a list of tags ordered by user and post count:', (table) => { +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) + 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) + }) }) diff --git a/store/auth.js b/store/auth.js index 7a8a658c9..6e8435c41 100644 --- a/store/auth.js +++ b/store/auth.js @@ -27,7 +27,7 @@ export const mutations = { } export const getters = { - isAuthenticated(state){ + isAuthenticated(state) { return !!state.token }, isLoggedIn(state) { diff --git a/store/auth.test.js b/store/auth.test.js index f506d6d6e..9f53701bf 100644 --- a/store/auth.test.js +++ b/store/auth.test.js @@ -6,7 +6,8 @@ describe('isAuthenticated', () => { describe('given JWT Bearer token', () => { test('true', () => { state = { - token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUxIiwic2x1ZyI6InBldGVyLWx1c3RpZyIsIm5hbWUiOiJQZXRlciBMdXN0aWciLCJhdmF0YXIiOiJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vdWlmYWNlcy9mYWNlcy90d2l0dGVyL25haXRhbmFtb3Jlbm8vMTI4LmpwZyIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5vcmciLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE1NDUwNjMzODcsImV4cCI6MTYzMTQ2MzM4NywiYXVkIjoiaHR0cHM6Ly9uaXRyby1zdGFnaW5nLmh1bWFuLWNvbm5lY3Rpb24ub3JnIiwiaXNzIjoiaHR0cHM6Ly9hcGktbml0cm8tc3RhZ2luZy5odW1hbi1jb25uZWN0aW9uLm9yZyIsInN1YiI6InUxIn0.BQEoC3J6uRqMvIVfHYmMbmfMR2BudiG5Xvn8mfcc0Kk' + token: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUxIiwic2x1ZyI6InBldGVyLWx1c3RpZyIsIm5hbWUiOiJQZXRlciBMdXN0aWciLCJhdmF0YXIiOiJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vdWlmYWNlcy9mYWNlcy90d2l0dGVyL25haXRhbmFtb3Jlbm8vMTI4LmpwZyIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5vcmciLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE1NDUwNjMzODcsImV4cCI6MTYzMTQ2MzM4NywiYXVkIjoiaHR0cHM6Ly9uaXRyby1zdGFnaW5nLmh1bWFuLWNvbm5lY3Rpb24ub3JnIiwiaXNzIjoiaHR0cHM6Ly9hcGktbml0cm8tc3RhZ2luZy5odW1hbi1jb25uZWN0aW9uLm9yZyIsInN1YiI6InUxIn0.BQEoC3J6uRqMvIVfHYmMbmfMR2BudiG5Xvn8mfcc0Kk' } expect(getters.isAuthenticated(state)).toBe(true) }) From d465db5906a52f7710a8e0eb0c9a6d55741c3535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 18 Dec 2018 15:03:46 +0100 Subject: [PATCH 03/10] Test store/auth#login --- store/auth.js | 14 ----------- store/auth.test.js | 61 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 53 insertions(+), 22 deletions(-) diff --git a/store/auth.js b/store/auth.js index 6e8435c41..13ae2b65e 100644 --- a/store/auth.js +++ b/store/auth.js @@ -54,20 +54,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 = { diff --git a/store/auth.test.js b/store/auth.test.js index 9f53701bf..fd2663f83 100644 --- a/store/auth.test.js +++ b/store/auth.test.js @@ -1,15 +1,60 @@ import { getters, mutations, actions } from './auth.js' let state +let commit +const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUxIiwic2x1ZyI6InBldGVyLWx1c3RpZyIsIm5hbWUiOiJQZXRlciBMdXN0aWciLCJhdmF0YXIiOiJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vdWlmYWNlcy9mYWNlcy90d2l0dGVyL2FudG9ueXpvdG92LzEyOC5qcGciLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUub3JnIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTQ1MTQwMTQ5LCJleHAiOjE2MzE1NDAxNDksImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMCIsInN1YiI6InUxIn0.t1nDgdRPNxXGbNzHHN6uSt5fmS4ofFNLjk_k5XnCoCs" -describe('isAuthenticated', () => { - describe('given JWT Bearer token', () => { - test('true', () => { - state = { - token: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUxIiwic2x1ZyI6InBldGVyLWx1c3RpZyIsIm5hbWUiOiJQZXRlciBMdXN0aWciLCJhdmF0YXIiOiJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vdWlmYWNlcy9mYWNlcy90d2l0dGVyL25haXRhbmFtb3Jlbm8vMTI4LmpwZyIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5vcmciLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE1NDUwNjMzODcsImV4cCI6MTYzMTQ2MzM4NywiYXVkIjoiaHR0cHM6Ly9uaXRyby1zdGFnaW5nLmh1bWFuLWNvbm5lY3Rpb24ub3JnIiwiaXNzIjoiaHR0cHM6Ly9hcGktbml0cm8tc3RhZ2luZy5odW1hbi1jb25uZWN0aW9uLm9yZyIsInN1YiI6InUxIn0.BQEoC3J6uRqMvIVfHYmMbmfMR2BudiG5Xvn8mfcc0Kk' - } - expect(getters.isAuthenticated(state)).toBe(true) +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 a successful response', () => { + let mutate + let onLogin + + beforeEach(() => { + mutate = jest.fn(() => Promise.resolve( { data: { login: { token } } })) + onLogin = jest.fn(() => Promise.resolve()) + const module = { + app: { + apolloProvider: { defaultClient: { mutate } }, + $apolloHelpers: { onLogin } + } + } + action = actions.login.bind(module) + }) + + afterEach(() => { + action = null + }) + + it('saves the JWT Bearer token', async () => { + await action({commit}, {email: 'doesnot@matter.org', password: '1234'}) + const expected = [ + ['SET_PENDING', true], + ['SET_USER', null], + ['SET_TOKEN', null], + ['SET_TOKEN', token], + ['SET_USER', { }], + ['SET_PENDING', false] + ] + expect(commit.mock.calls).toEqual(expected) + }) }) }) }) From bf8f2f887767c230438450f6cd3fc4a3ffbec374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 18 Dec 2018 15:49:09 +0100 Subject: [PATCH 04/10] Remove dead code --- store/auth.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/store/auth.js b/store/auth.js index 13ae2b65e..84a7bc28a 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 }, @@ -36,9 +31,6 @@ export const getters = { 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' }, From b17686656bcab0a1927838284f6b58c2ff370faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 19 Dec 2018 09:37:36 +0100 Subject: [PATCH 05/10] Test error path of #login procedure --- store/auth.js | 5 +- store/auth.test.js | 119 ++++++++++++++++++++++++++++++++++++++------- 2 files changed, 105 insertions(+), 19 deletions(-) diff --git a/store/auth.js b/store/auth.js index 84a7bc28a..ff60ef849 100644 --- a/store/auth.js +++ b/store/auth.js @@ -104,8 +104,9 @@ export const actions = { if (res && res.token) { await this.app.$apolloHelpers.onLogin(res.token) commit('SET_TOKEN', res.token) - delete res.token - commit('SET_USER', res) + const userData = Object.assign({}, res) + delete userData.token + commit('SET_USER', userData) commit('SET_PENDING', false) return true } else { diff --git a/store/auth.test.js b/store/auth.test.js index fd2663f83..2e7bf523f 100644 --- a/store/auth.test.js +++ b/store/auth.test.js @@ -2,7 +2,41 @@ import { getters, mutations, actions } from './auth.js' let state let commit -const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUxIiwic2x1ZyI6InBldGVyLWx1c3RpZyIsIm5hbWUiOiJQZXRlciBMdXN0aWciLCJhdmF0YXIiOiJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vdWlmYWNlcy9mYWNlcy90d2l0dGVyL2FudG9ueXpvdG92LzEyOC5qcGciLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUub3JnIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTQ1MTQwMTQ5LCJleHAiOjE2MzE1NDAxNDksImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMCIsInN1YiI6InUxIn0.t1nDgdRPNxXGbNzHHN6uSt5fmS4ofFNLjk_k5XnCoCs" + + +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() @@ -23,13 +57,59 @@ describe('actions', () => { let action describe('login', () => { - describe('given a successful response', () => { - let mutate - let onLogin + 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', () => { beforeEach(() => { - mutate = jest.fn(() => Promise.resolve( { data: { login: { token } } })) - onLogin = jest.fn(() => Promise.resolve()) + const response = Object.assign({}, incorrectPasswordResponse) + const mutate = jest.fn(() => Promise.resolve(response)) + const onLogin = jest.fn(() => Promise.resolve()) const module = { app: { apolloProvider: { defaultClient: { mutate } }, @@ -43,17 +123,22 @@ describe('actions', () => { action = null }) - it('saves the JWT Bearer token', async () => { - await action({commit}, {email: 'doesnot@matter.org', password: '1234'}) - const expected = [ - ['SET_PENDING', true], - ['SET_USER', null], - ['SET_TOKEN', null], - ['SET_TOKEN', token], - ['SET_USER', { }], - ['SET_PENDING', false] - ] - expect(commit.mock.calls).toEqual(expected) + xit('shows a user friendly error message', async () => { + await action({commit}, {email: 'user@example.org', password: 'wrong'}) + }) + + it('saves pending flags in order', async () => { + try { + await action({commit}, {email: 'user@example.org', password: 'wrong'}) + } catch(err) { + console.log(err) + } + expect(commit.mock.calls).toEqual( + expect.arrayContaining([ + ['SET_PENDING', true], + ['SET_PENDING', false] + ]) + ) }) }) }) From 48467bc69738ec4385e8e0c58dd43b4ab802b3d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 19 Dec 2018 15:24:58 +0100 Subject: [PATCH 06/10] Fix lint --- layouts/default.vue | 9 ++++--- store/auth.test.js | 66 +++++++++++++++++++++++++++------------------ 2 files changed, 46 insertions(+), 29 deletions(-) diff --git a/layouts/default.vue b/layouts/default.vue index fb922009e..f3119bb99 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -34,18 +34,21 @@ class="avatar-menu-popover" style="padding-top: .5rem; padding-bottom: .5rem;" @mouseover="popoverMouseEnter" - @mouseleave="popoveMouseLeave"> + @mouseleave="popoveMouseLeave" + > Hallo {{ user.name }} + style="margin-left: -15px; margin-right: -15px; padding-top: 1rem; padding-bottom: 1rem;" + > + @click.native="toggleMenu" + > {{ item.route.name }} diff --git a/store/auth.test.js b/store/auth.test.js index 2e7bf523f..9160d0cc5 100644 --- a/store/auth.test.js +++ b/store/auth.test.js @@ -3,17 +3,18 @@ import { getters, mutations, actions } from './auth.js' let state let commit - -const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUzIiwic2x1ZyI6Implbm55LXJvc3RvY2siLCJuYW1lIjoiSmVubnkgUm9zdG9jayIsImF2YXRhciI6Imh0dHBzOi8vczMuYW1hem9uYXdzLmNvbS91aWZhY2VzL2ZhY2VzL3R3aXR0ZXIvbXV0dV9rcmlzaC8xMjguanBnIiwiZW1haWwiOiJ1c2VyQGV4YW1wbGUub3JnIiwicm9sZSI6InVzZXIiLCJpYXQiOjE1NDUxNDQ2ODgsImV4cCI6MTYzMTU0NDY4OCwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo0MDAwIiwic3ViIjoidTMifQ.s5_JeQN9TaUPfymAXPOpbMAwhmTIg9cnOvNEcj4z75k" +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", + 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 } } @@ -24,16 +25,14 @@ const incorrectPasswordResponse = { }, errors: [ { - message: "Incorrect password.", + message: 'Incorrect password.', locations: [ { - "line": 2, - "column": 3 + line: 2, + column: 3 } ], - path: [ - "login" - ] + path: ['login'] } ] } @@ -69,7 +68,10 @@ describe('actions', () => { } } action = actions.login.bind(module) - await action({commit}, {email: 'user@example.org', password: '1234'}) + await action( + { commit }, + { email: 'user@example.org', password: '1234' } + ) }) afterEach(() => { @@ -84,14 +86,20 @@ describe('actions', () => { 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" - }]]) + 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' + } + ] + ]) ) }) @@ -124,13 +132,19 @@ describe('actions', () => { }) xit('shows a user friendly error message', async () => { - await action({commit}, {email: 'user@example.org', password: 'wrong'}) + await action( + { commit }, + { email: 'user@example.org', password: 'wrong' } + ) }) it('saves pending flags in order', async () => { try { - await action({commit}, {email: 'user@example.org', password: 'wrong'}) - } catch(err) { + await action( + { commit }, + { email: 'user@example.org', password: 'wrong' } + ) + } catch (err) { console.log(err) } expect(commit.mock.calls).toEqual( From 4236881218820a3390df5885f05065ede472433d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 19 Dec 2018 15:36:45 +0100 Subject: [PATCH 07/10] Fix CodeFactor issues --- cypress/integration/common/steps.js | 37 ++++++++++++----------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index e05d6aaeb..9ad30321d 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -22,6 +22,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) }) @@ -96,17 +109,7 @@ Then('I can see a list of categories ordered by post count:', 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) - }) + lastColumnIsSortedInDescendingOrder() }) Then('I can see a list of tags ordered by user and post count:', table => { @@ -114,15 +117,5 @@ Then('I can see a list of tags ordered by user and post count:', 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) - }) + lastColumnIsSortedInDescendingOrder() }) From f85081c3087635db434fe8c0a3ac64f65574a75e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 19 Dec 2018 17:58:03 +0100 Subject: [PATCH 08/10] Properly test the reject branch of `login` --- store/auth.test.js | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/store/auth.test.js b/store/auth.test.js index 9160d0cc5..e05becb57 100644 --- a/store/auth.test.js +++ b/store/auth.test.js @@ -114,10 +114,12 @@ describe('actions', () => { }) describe('given invalid credentials and incorrect password response', () => { + let onLogin + let mutate + beforeEach(() => { - const response = Object.assign({}, incorrectPasswordResponse) - const mutate = jest.fn(() => Promise.resolve(response)) - const onLogin = jest.fn(() => Promise.resolve()) + mutate = jest.fn(() => Promise.reject('This error is expected.')) + onLogin = jest.fn(() => Promise.resolve()) const module = { app: { apolloProvider: { defaultClient: { mutate } }, @@ -131,22 +133,18 @@ describe('actions', () => { action = null }) - xit('shows a user friendly error message', async () => { - await action( - { commit }, - { email: 'user@example.org', password: 'wrong' } - ) + 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) { - console.log(err) - } + await action({ commit }, { email: 'user@example.org', password: 'wrong' }) + } catch(err) {} // ignore expect(commit.mock.calls).toEqual( expect.arrayContaining([ ['SET_PENDING', true], From 16997ee63d2314d2bf17103e2d430e1c5dad8373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 19 Dec 2018 18:02:11 +0100 Subject: [PATCH 09/10] Refactor `login` with confidence This is why I :heart: testing. First I wrote several tests to get familiar with the existing code and specified everything that I believe to be required. Then I refactor the entire method at once and the tests pass. Note that I do **not** believe that setting the user data to `null` is really relevant. --- store/auth.js | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/store/auth.js b/store/auth.js index ff60ef849..e3ed9f383 100644 --- a/store/auth.js +++ b/store/auth.js @@ -78,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(` @@ -101,23 +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) - const userData = Object.assign({}, res) - delete userData.token - commit('SET_USER', userData) - 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 }) { From b320ef30fcce842bee7d77d8ae69af13395c70e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 19 Dec 2018 18:12:10 +0100 Subject: [PATCH 10/10] Fix lint --- store/auth.test.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/store/auth.test.js b/store/auth.test.js index e05becb57..98bd1ce13 100644 --- a/store/auth.test.js +++ b/store/auth.test.js @@ -134,17 +134,20 @@ describe('actions', () => { }) it('populates error messages', async () => { - expect(action({ commit }, { email: 'user@example.org', password: 'wrong' })) - .rejects - .toThrowError('This error is expected.') + 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 + await action( + { commit }, + { email: 'user@example.org', password: 'wrong' } + ) + } catch (err) {} // ignore expect(commit.mock.calls).toEqual( expect.arrayContaining([ ['SET_PENDING', true],