From da28f1f364f96eb4140c3877293799330734ba73 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 16 Jun 2021 13:40:27 +0200 Subject: [PATCH 01/34] setuo password component --- .../src/components/Inputs/InputPassword.vue | 63 +++++++++++++++++++ frontend/src/locales/de.json | 3 +- frontend/src/locales/en.json | 3 +- frontend/src/views/Pages/Login.vue | 52 ++++----------- 4 files changed, 80 insertions(+), 41 deletions(-) create mode 100644 frontend/src/components/Inputs/InputPassword.vue diff --git a/frontend/src/components/Inputs/InputPassword.vue b/frontend/src/components/Inputs/InputPassword.vue new file mode 100644 index 000000000..638eb6eb3 --- /dev/null +++ b/frontend/src/components/Inputs/InputPassword.vue @@ -0,0 +1,63 @@ + + diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 8554996bb..a1cdf985b 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -64,7 +64,8 @@ "change_username_info": "Einmal gespeichert, kann der Username ncht mehr geändert werden!" }, "error": { - "error":"Fehler" + "error":"Fehler", + "no-account": "Leider konnten wir keinen Account finden mit diesen Daten!" }, "transaction":{ "show_all":"Alle {count} Transaktionen ansehen", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 7b97c2240..6e39a04b7 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -64,7 +64,8 @@ "change_username_info": "Once saved, the username cannot be changed again!" }, "error": { - "error":"Error" + "error":"Error", + "no-account": "Unfortunately we could not find an account to the given data!" }, "transaction":{ "show_all":"View all {count} transactions.", diff --git a/frontend/src/views/Pages/Login.vue b/frontend/src/views/Pages/Login.vue index 4619dddf2..d08171c1f 100755 --- a/frontend/src/views/Pages/Login.vue +++ b/frontend/src/views/Pages/Login.vue @@ -46,52 +46,25 @@ - - - - - - - - - - - - - {{ validationContext.errors[0] }} - - - + - Leider konnten wir keinen Account finden mit diesen Daten! + {{ $t('error.no-account') }} -
+
{{ $t('login') }}
@@ -118,9 +91,13 @@ + diff --git a/frontend/src/views/Pages/Login.vue b/frontend/src/views/Pages/Login.vue index 25c40df5a..2d4ce2345 100755 --- a/frontend/src/views/Pages/Login.vue +++ b/frontend/src/views/Pages/Login.vue @@ -13,7 +13,6 @@
- @@ -22,47 +21,14 @@
{{ $t('login') }}
- - - - - - - - - {{ validationContext.errors[0] }} - - - - + + + - - - - - - - {{ $t('error.no-account') }} - - - - -
{{ $t('login') }}
@@ -91,31 +57,26 @@ import loginAPI from '../../apis/loginAPI' import CONFIG from '../../config' import InputPassword from '../../components/Inputs/InputPassword' +import InputEmail from '../../components/Inputs/InputEmail' export default { name: 'login', components: { InputPassword, + InputEmail, }, data() { return { form: { email: '', password: '', - // rememberMe: false }, - loginfail: false, allowRegister: CONFIG.ALLOW_REGISTER, passwordVisible: false, } }, methods: { - getValidationState({ dirty, validated, valid = null }) { - return dirty || validated ? valid : null - }, async onSubmit() { - // error info ausschalten - this.loginfail = false const loader = this.$loading.show({ container: this.$refs.submitButton, }) @@ -129,7 +90,7 @@ export default { loader.hide() } else { loader.hide() - this.loginfail = true + this.$toast.error(this.$t('error.no-account')) } }, }, From 789808ea408997ab66fd9954fa7dd4ee5fc58049 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 29 Jun 2021 14:02:09 +0200 Subject: [PATCH 07/34] Complete test for login page --- frontend/src/components/Inputs/InputEmail.vue | 2 +- frontend/src/views/Pages/Login.spec.js | 119 ++++++++++++++++-- 2 files changed, 110 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/Inputs/InputEmail.vue b/frontend/src/components/Inputs/InputEmail.vue index adcd66345..41338fd8c 100644 --- a/frontend/src/components/Inputs/InputEmail.vue +++ b/frontend/src/components/Inputs/InputEmail.vue @@ -37,7 +37,7 @@ export default { } }, }, - name: { type: String, default: 'email' }, + name: { type: String, default: 'Email' }, label: { type: String, default: 'Email' }, placeholder: { type: String, default: 'Email' }, value: { required: true, type: String }, diff --git a/frontend/src/views/Pages/Login.spec.js b/frontend/src/views/Pages/Login.spec.js index 0d94840b0..a4ff79bed 100644 --- a/frontend/src/views/Pages/Login.spec.js +++ b/frontend/src/views/Pages/Login.spec.js @@ -1,10 +1,37 @@ import { mount, RouterLinkStub } from '@vue/test-utils' import flushPromises from 'flush-promises' - +import loginAPI from '../../apis/loginAPI' import Login from './Login' +jest.mock('../../apis/loginAPI') + const localVue = global.localVue +const mockLoginCall = jest.fn() +mockLoginCall.mockReturnValue({ + success: true, + result: { + data: { + session_id: 1, + user: { + name: 'Peter Lustig', + }, + }, + }, +}) + +loginAPI.login = mockLoginCall + +const toastErrorMock = jest.fn() +const mockStoreDispach = jest.fn() +const mockRouterPush = jest.fn() +const spinnerHideMock = jest.fn() +const spinnerMock = jest.fn(() => { + return { + hide: spinnerHideMock, + } +}) + describe('Login', () => { let wrapper @@ -13,6 +40,18 @@ describe('Login', () => { locale: 'en', }, $t: jest.fn((t) => t), + $store: { + dispatch: mockStoreDispach, + }, + $loading: { + show: spinnerMock, + }, + $router: { + push: mockRouterPush, + }, + $toast: { + error: toastErrorMock, + }, } const stubs = { @@ -76,16 +115,76 @@ describe('Login', () => { it('has a Submit button', () => { expect(wrapper.find('button[type="submit"]').exists()).toBeTruthy() }) - - it('shows a warning when no valid Email is entered', async () => { - wrapper.find('input[placeholder="Email"]').setValue('no_valid@Email') - await flushPromises() - await expect(wrapper.find('.invalid-feedback').text()).toEqual( - 'The Email field must be a valid email', - ) - }) }) - // to do: test submit button + describe('submit', () => { + describe('no data', () => { + it('displays a message that Email is required', async () => { + await wrapper.find('form').trigger('submit') + await flushPromises() + expect(wrapper.findAll('div.invalid-feedback').at(0).text()).toBe( + 'The Email field is required', + ) + }) + + it('displays a message that password is required', async () => { + await wrapper.find('form').trigger('submit') + await flushPromises() + expect(wrapper.findAll('div.invalid-feedback').at(1).text()).toBe( + 'The password field is required', + ) + }) + }) + + describe('valid data', () => { + beforeEach(async () => { + jest.clearAllMocks() + await wrapper.find('input[placeholder="Email"]').setValue('user@example.org') + await wrapper.find('input[placeholder="form.password"]').setValue('1234') + await flushPromises() + await wrapper.find('form').trigger('submit') + await flushPromises() + }) + + it('calls the API with the given data', () => { + expect(mockLoginCall).toBeCalledWith('user@example.org', '1234') + }) + + it('creates a spinner', () => { + expect(spinnerMock).toBeCalled() + }) + + describe('login success', () => { + it('dispatches server response to store', () => { + expect(mockStoreDispach).toBeCalledWith('login', { + sessionId: 1, + user: { name: 'Peter Lustig' }, + }) + }) + + it('redirects to overview page', () => { + expect(mockRouterPush).toBeCalledWith('/overview') + }) + + it('hides the spinner', () => { + expect(spinnerHideMock).toBeCalled() + }) + }) + + describe('login fails', () => { + beforeEach(() => { + mockLoginCall.mockReturnValue({ success: false }) + }) + + it('hides the spinner', () => { + expect(spinnerHideMock).toBeCalled() + }) + + it('toasts an error message', () => { + expect(toastErrorMock).toBeCalledWith('error.no-account') + }) + }) + }) + }) }) }) From d3382620fa551ae2f2587bba404565cfecbead37 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 29 Jun 2021 14:09:34 +0200 Subject: [PATCH 08/34] frontend coverage to 33% --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 14b507354..329b1dd60 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -212,7 +212,7 @@ jobs: report_name: Coverage Frontend type: lcov result_path: ./coverage/lcov.info - min_coverage: 31 + min_coverage: 33 token: ${{ github.token }} ############################################################################## From d9f444b0bb6cc8b68e383e14dbfc6ed7ebfcda7f Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 30 Jun 2021 20:25:50 +0200 Subject: [PATCH 09/34] refactor: Update Store Tests --- frontend/src/store/store.test.js | 122 +++++++++++++++++++++++++------ 1 file changed, 99 insertions(+), 23 deletions(-) diff --git a/frontend/src/store/store.test.js b/frontend/src/store/store.test.js index 6bc004273..4f276feec 100644 --- a/frontend/src/store/store.test.js +++ b/frontend/src/store/store.test.js @@ -1,6 +1,6 @@ import { mutations, actions } from './store' -const { language, email, sessionId } = mutations +const { language, email, sessionId, username, firstName, lastName, description } = mutations const { login, logout } = actions describe('Vuex store', () => { @@ -28,51 +28,102 @@ describe('Vuex store', () => { expect(state.sessionId).toEqual('1234') }) }) + + describe('username', () => { + it('sets the state of username', () => { + const state = { username: null } + username(state, 'user') + expect(state.username).toEqual('user') + }) + }) + + describe('firstName', () => { + it('sets the state of firstName', () => { + const state = { firstName: null } + firstName(state, 'Peter') + expect(state.firstName).toEqual('Peter') + }) + }) + + describe('lastName', () => { + it('sets the state of lastName', () => { + const state = { lastName: null } + lastName(state, 'Lustig') + expect(state.lastName).toEqual('Lustig') + }) + }) + + describe('description', () => { + it('sets the state of description', () => { + const state = { description: null } + description(state, 'Nickelbrille') + expect(state.description).toEqual('Nickelbrille') + }) + }) }) describe('actions', () => { describe('login', () => { const commit = jest.fn() const state = {} + const commitedData = { + sessionId: 1234, + user: { + email: 'someone@there.is', + language: 'en', + username: 'user', + first_name: 'Peter', + last_name: 'Lustig', + description: 'Nickelbrille', + }, + } - it('calls three commits', () => { - login( - { commit, state }, - { sessionId: 1234, user: { email: 'someone@there.is', language: 'en' } }, - ) + it('calls seven commits', () => { + login({ commit, state }, commitedData) expect(commit).toHaveBeenCalledTimes(7) }) it('commits sessionId', () => { - login( - { commit, state }, - { sessionId: 1234, user: { email: 'someone@there.is', language: 'en' } }, - ) + login({ commit, state }, commitedData) expect(commit).toHaveBeenNthCalledWith(1, 'sessionId', 1234) }) it('commits email', () => { - login( - { commit, state }, - { sessionId: 1234, user: { email: 'someone@there.is', language: 'en' } }, - ) + login({ commit, state }, commitedData) expect(commit).toHaveBeenNthCalledWith(2, 'email', 'someone@there.is') }) it('commits language', () => { - login( - { commit, state }, - { sessionId: 1234, user: { email: 'someone@there.is', language: 'en' } }, - ) + login({ commit, state }, commitedData) expect(commit).toHaveBeenNthCalledWith(3, 'language', 'en') }) + + it('commits username', () => { + login({ commit, state }, commitedData) + expect(commit).toHaveBeenNthCalledWith(4, 'username', 'user') + }) + + it('commits firstName', () => { + login({ commit, state }, commitedData) + expect(commit).toHaveBeenNthCalledWith(5, 'firstName', 'Peter') + }) + + it('commits lastName', () => { + login({ commit, state }, commitedData) + expect(commit).toHaveBeenNthCalledWith(6, 'lastName', 'Lustig') + }) + + it('commits description', () => { + login({ commit, state }, commitedData) + expect(commit).toHaveBeenNthCalledWith(7, 'description', 'Nickelbrille') + }) }) describe('logout', () => { const commit = jest.fn() const state = {} - it('calls two commits', () => { + it('calls six commits', () => { logout({ commit, state }) expect(commit).toHaveBeenCalledTimes(6) }) @@ -87,11 +138,36 @@ describe('Vuex store', () => { expect(commit).toHaveBeenNthCalledWith(2, 'email', null) }) - // how can I get this working? - it.skip('calls sessionStorage.clear()', () => { + it('commits username', () => { logout({ commit, state }) - const spy = jest.spyOn(sessionStorage, 'clear') - expect(spy).toHaveBeenCalledTimes(1) + expect(commit).toHaveBeenNthCalledWith(3, 'username', '') + }) + + it('commits firstName', () => { + logout({ commit, state }) + expect(commit).toHaveBeenNthCalledWith(4, 'firstName', '') + }) + + it('commits lastName', () => { + logout({ commit, state }) + expect(commit).toHaveBeenNthCalledWith(5, 'lastName', '') + }) + + it('commits description', () => { + logout({ commit, state }) + expect(commit).toHaveBeenNthCalledWith(6, 'description', '') + }) + + // how to get this working? + it.skip('calls sessionStorage.clear()', () => { + const clearStorageMock = jest.fn() + global.sessionStorage = jest.fn(() => { + return { + clear: clearStorageMock, + } + }) + logout({ commit, state }) + expect(clearStorageMock).toBeCalled() }) }) }) From 555b783d745db0be54927442569a6dbdd7cd3d5b Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 30 Jun 2021 20:45:06 +0200 Subject: [PATCH 10/34] refactor: Forgot Password Form --- .../src/views/Pages/ForgotPassword.spec.js | 5 ++-- frontend/src/views/Pages/ForgotPassword.vue | 29 ++++--------------- 2 files changed, 7 insertions(+), 27 deletions(-) diff --git a/frontend/src/views/Pages/ForgotPassword.spec.js b/frontend/src/views/Pages/ForgotPassword.spec.js index 9e33c785e..4e6c3b834 100644 --- a/frontend/src/views/Pages/ForgotPassword.spec.js +++ b/frontend/src/views/Pages/ForgotPassword.spec.js @@ -83,7 +83,7 @@ describe('ForgotPassword', () => { }) it('displays an error', () => { - expect(form.find('#reset-pwd--live-feedback').text()).toEqual( + expect(form.find('div.invalid-feedback').text()).toEqual( 'The Email field must be a valid email', ) }) @@ -96,8 +96,7 @@ describe('ForgotPassword', () => { describe('valid Email', () => { beforeEach(async () => { await form.find('input').setValue('user@example.org') - form.trigger('submit') - await wrapper.vm.$nextTick() + await form.trigger('submit') await flushPromises() }) diff --git a/frontend/src/views/Pages/ForgotPassword.vue b/frontend/src/views/Pages/ForgotPassword.vue index e407f3590..a16ea2446 100644 --- a/frontend/src/views/Pages/ForgotPassword.vue +++ b/frontend/src/views/Pages/ForgotPassword.vue @@ -19,27 +19,7 @@ - - - - - - {{ validationContext.errors[0] }} - - - - +
{{ $t('site.password.reset_now') }} @@ -59,9 +39,13 @@ - From bcf24d9a413f3e8ed637e8dbbea6d4f4a20ce0dd Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 1 Jul 2021 13:20:37 +0200 Subject: [PATCH 13/34] component for password confirmation --- .../Inputs/InputPasswordConfirmation.spec.js | 79 +++++++++++++++++++ .../Inputs/InputPasswordConfirmation.vue | 68 ++++++++++++++++ .../UserProfile/UserCard_FormUserPasswort.vue | 42 +++------- 3 files changed, 156 insertions(+), 33 deletions(-) create mode 100644 frontend/src/components/Inputs/InputPasswordConfirmation.spec.js create mode 100644 frontend/src/components/Inputs/InputPasswordConfirmation.vue diff --git a/frontend/src/components/Inputs/InputPasswordConfirmation.spec.js b/frontend/src/components/Inputs/InputPasswordConfirmation.spec.js new file mode 100644 index 000000000..ef3b5d64e --- /dev/null +++ b/frontend/src/components/Inputs/InputPasswordConfirmation.spec.js @@ -0,0 +1,79 @@ +import { mount } from '@vue/test-utils' +import { extend } from 'vee-validate' + +import InputPasswordConfirmation from './InputPasswordConfirmation' + +const rules = [ + 'containsLowercaseCharacter', + 'containsUppercaseCharacter', + 'containsNumericCharacter', + 'atLeastEightCharactera', + 'samePassword', +] + +rules.forEach((rule) => { + extend(rule, { + validate(value) { + return true + }, + }) +}) + +const localVue = global.localVue + +describe('InputPasswordConfirmation', () => { + let wrapper + + const propsData = { + value: { + password: '', + passwordRepeat: '', + }, + } + + const mocks = { + $t: jest.fn((t) => t), + } + + const Wrapper = () => { + return mount(InputPasswordConfirmation, { localVue, propsData, mocks }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('has two input fields', () => { + expect(wrapper.findAll('input')).toHaveLength(2) + }) + + describe('input values ', () => { + it('emits input with new value for first input field', async () => { + await wrapper.findAll('input').at(0).setValue('1234') + expect(wrapper.emitted('input')).toBeTruthy() + expect(wrapper.emitted('input')).toEqual([ + [ + { + password: '1234', + passwordRepeat: '', + }, + ], + ]) + }) + + it('emits input with new value for second input field', async () => { + await wrapper.findAll('input').at(1).setValue('1234') + expect(wrapper.emitted('input')).toBeTruthy() + expect(wrapper.emitted('input')).toEqual([ + [ + { + password: '', + passwordRepeat: '1234', + }, + ], + ]) + }) + }) + }) +}) diff --git a/frontend/src/components/Inputs/InputPasswordConfirmation.vue b/frontend/src/components/Inputs/InputPasswordConfirmation.vue new file mode 100644 index 000000000..08efaccfd --- /dev/null +++ b/frontend/src/components/Inputs/InputPasswordConfirmation.vue @@ -0,0 +1,68 @@ + + diff --git a/frontend/src/views/Pages/UserProfile/UserCard_FormUserPasswort.vue b/frontend/src/views/Pages/UserProfile/UserCard_FormUserPasswort.vue index ba49662fd..00cacfd21 100644 --- a/frontend/src/views/Pages/UserProfile/UserCard_FormUserPasswort.vue +++ b/frontend/src/views/Pages/UserProfile/UserCard_FormUserPasswort.vue @@ -28,36 +28,8 @@ > - - - - - - - - - - - + +
@@ -75,11 +47,13 @@