From 17bfbd6281a65d98075af256b3d1dfb12fd01a82 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 21 Mar 2022 14:29:00 +0100 Subject: [PATCH 1/5] fix test for optional code on login on register --- frontend/src/routes/router.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/routes/router.test.js b/frontend/src/routes/router.test.js index 24916bb05..85f765c69 100644 --- a/frontend/src/routes/router.test.js +++ b/frontend/src/routes/router.test.js @@ -99,14 +99,14 @@ describe('router', () => { describe('login', () => { it('loads the "Login" page', async () => { - const component = await routes.find((r) => r.path === '/login').component() + const component = await routes.find((r) => r.path === '/login/:code?').component() expect(component.default.name).toBe('Login') }) }) describe('register', () => { it('loads the "register" page', async () => { - const component = await routes.find((r) => r.path === '/register').component() + const component = await routes.find((r) => r.path === '/register/:code?').component() expect(component.default.name).toBe('Register') }) }) From c57b40adc10506263abe5b0d5902d0e249cce8cb Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 21 Mar 2022 14:56:37 +0100 Subject: [PATCH 2/5] test Register with Redeem Code --- frontend/src/pages/Register.spec.js | 44 ++++++++++++++++++++++++++++- frontend/src/pages/Register.vue | 2 +- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/Register.spec.js b/frontend/src/pages/Register.spec.js index c29909123..446c97928 100644 --- a/frontend/src/pages/Register.spec.js +++ b/frontend/src/pages/Register.spec.js @@ -32,6 +32,9 @@ describe('Register', () => { $router: { push: routerPushMock, }, + $route: { + params: {}, + }, $apollo: { mutate: registerUserMutationMock, query: apolloQueryMock, @@ -312,6 +315,45 @@ describe('Register', () => { }) }) }) - // TODO: line 157 + + describe('redeem code', () => { + describe('no redeem code', () => { + it('has no redeem code', () => { + expect(wrapper.vm.redeemCode).toBe(undefined) + }) + }) + }) + + describe('with redeem code', () => { + beforeEach(async () => { + jest.clearAllMocks() + mocks.$route.params = { + code: 'some-code', + } + wrapper = Wrapper() + wrapper.find('#registerFirstname').setValue('Max') + wrapper.find('#registerLastname').setValue('Mustermann') + wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net') + wrapper.find('.language-switch-select').findAll('option').at(1).setSelected() + wrapper.find('#registerCheckbox').setChecked() + await wrapper.find('form').trigger('submit') + await flushPromises() + }) + + it('sends the redeem code to the server', () => { + expect(registerUserMutationMock).toBeCalledWith( + expect.objectContaining({ + variables: { + email: 'max.mustermann@gradido.net', + firstName: 'Max', + lastName: 'Mustermann', + language: 'en', + publisherId: 12345, + redeemCode: 'some-code', + }, + }), + ) + }) + }) }) }) diff --git a/frontend/src/pages/Register.vue b/frontend/src/pages/Register.vue index 2dfbac441..cadc0f34c 100755 --- a/frontend/src/pages/Register.vue +++ b/frontend/src/pages/Register.vue @@ -236,7 +236,7 @@ export default { lastName: this.form.lastname, language: this.language, publisherId: this.$store.state.publisherId, - redeemCode: this.redeem, + redeemCode: this.redeemCode, }, }) .then(() => { From 5baa14b359878954a7f598541e8f9c381dfc68cc Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 21 Mar 2022 16:25:17 +0100 Subject: [PATCH 3/5] test transaction link page --- frontend/src/pages/TransactionLink.spec.js | 363 ++++++++++++++++++--- frontend/src/pages/TransactionLink.vue | 39 +-- 2 files changed, 338 insertions(+), 64 deletions(-) diff --git a/frontend/src/pages/TransactionLink.spec.js b/frontend/src/pages/TransactionLink.spec.js index f31812a10..8c41d0a92 100644 --- a/frontend/src/pages/TransactionLink.spec.js +++ b/frontend/src/pages/TransactionLink.spec.js @@ -1,83 +1,360 @@ import { mount } from '@vue/test-utils' import TransactionLink from './TransactionLink' import { queryTransactionLink } from '@/graphql/queries' +import { redeemTransactionLink } from '@/graphql/mutations' +import { toastSuccessSpy, toastErrorSpy } from '@test/testSetup' const localVue = global.localVue -const errorHandler = jest.fn() const apolloQueryMock = jest.fn() +const apolloMutateMock = jest.fn() +const routerPushMock = jest.fn() -localVue.config.errorHandler = errorHandler +const now = new Date().toISOString() + +const transactionLinkValidExpireDate = () => { + const validUntil = new Date() + return new Date(validUntil.setDate(new Date().getDate() + 14)).toISOString() +} apolloQueryMock.mockResolvedValue({ data: { - listTransactionLinks: [ - { - id: 92, - amount: '22', - memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ', - createdAt: '2022-03-17T16:10:28.000Z', - validUntil: '2022-03-31T16:10:28.000Z', - redeemedAt: '2022-03-18T10:08:43.000Z', - deletedAt: null, - user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de', __typename: 'User' }, - }, - ], + queryTransactionLink: { + id: 92, + amount: '22', + memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ', + createdAt: '2022-03-17T16:10:28.000Z', + validUntil: transactionLinkValidExpireDate(), + redeemedAt: '2022-03-18T10:08:43.000Z', + deletedAt: null, + user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' }, + }, }, }) -const createMockObject = (code) => { - return { - localVue, - mocks: { - $t: jest.fn((t) => t), - $i18n: { - locale: () => 'en', - }, - $store: { - state: { - email: 'bibi@bloxberg.de', - }, - }, - $apollo: { - query: apolloQueryMock, - }, - $route: { - params: { - code: 'c0000c0000c0000', - }, - }, +const mocks = { + $t: jest.fn((t, obj = null) => (obj ? [t, obj.date].join('; ') : t)), + $store: { + state: { + token: null, + email: 'bibi@bloxberg.de', }, - } + }, + $apollo: { + query: apolloQueryMock, + mutate: apolloMutateMock, + }, + $route: { + params: { + code: 'some-code', + }, + }, + $router: { + push: routerPushMock, + }, } describe('TransactionLink', () => { let wrapper - const Wrapper = (functionN) => { - return mount(TransactionLink, functionN) + const Wrapper = () => { + return mount(TransactionLink, { localVue, mocks }) } describe('mount', () => { beforeEach(() => { - wrapper = Wrapper(createMockObject()) + jest.clearAllMocks() + wrapper = Wrapper() }) it('renders the component', () => { - expect(wrapper.find('div.show-transaction-link-informations').exists()).toBeTruthy() + expect(wrapper.find('div.show-transaction-link-informations').exists()).toBe(true) }) it('calls the queryTransactionLink query', () => { expect(apolloQueryMock).toBeCalledWith({ query: queryTransactionLink, variables: { - code: 'c0000c0000c0000', + code: 'some-code', }, }) }) - it('has link one information link data', () => { - expect(apolloQueryMock.mockResolvedValue).toHaveLength(1) + describe('deleted link', () => { + beforeEach(() => { + apolloQueryMock.mockResolvedValue({ + data: { + queryTransactionLink: { + id: 92, + amount: '22', + memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ', + createdAt: '2022-03-17T16:10:28.000Z', + validUntil: transactionLinkValidExpireDate(), + redeemedAt: '2022-03-18T10:08:43.000Z', + deletedAt: now, + user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' }, + }, + }, + }) + wrapper = Wrapper() + }) + + it('has a component RedeemedTextBox', () => { + expect(wrapper.findComponent({ name: 'RedeemedTextBox' }).exists()).toBe(true) + }) + + it('has a link deleted text in text box', () => { + expect(wrapper.findComponent({ name: 'RedeemedTextBox' }).text()).toContain( + 'gdd_per_link.link-deleted; ' + now, + ) + }) + }) + + describe('expired link', () => { + beforeEach(() => { + apolloQueryMock.mockResolvedValue({ + data: { + queryTransactionLink: { + id: 92, + amount: '22', + memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ', + createdAt: '2022-03-17T16:10:28.000Z', + validUntil: '2020-03-18T10:08:43.000Z', + redeemedAt: '2022-03-18T10:08:43.000Z', + deletedAt: null, + user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' }, + }, + }, + }) + wrapper = Wrapper() + }) + + it('has a component RedeemedTextBox', () => { + expect(wrapper.findComponent({ name: 'RedeemedTextBox' }).exists()).toBe(true) + }) + + it('has a link deleted text in text box', () => { + expect(wrapper.findComponent({ name: 'RedeemedTextBox' }).text()).toContain( + 'gdd_per_link.link-expired; 2020-03-18T10:08:43.000Z', + ) + }) + }) + + describe('redeemed link', () => { + beforeEach(() => { + apolloQueryMock.mockResolvedValue({ + data: { + queryTransactionLink: { + id: 92, + amount: '22', + memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ', + createdAt: '2022-03-17T16:10:28.000Z', + validUntil: transactionLinkValidExpireDate(), + redeemedAt: '2022-03-18T10:08:43.000Z', + deletedAt: null, + user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' }, + }, + }, + }) + wrapper = Wrapper() + }) + + it('has a component RedeemedTextBox', () => { + expect(wrapper.findComponent({ name: 'RedeemedTextBox' }).exists()).toBe(true) + }) + + it('has a link deleted text in text box', () => { + expect(wrapper.findComponent({ name: 'RedeemedTextBox' }).text()).toContain( + 'gdd_per_link.redeemed-at; 2022-03-18T10:08:43.000Z', + ) + }) + }) + + describe('no token in store', () => { + beforeEach(() => { + apolloQueryMock.mockResolvedValue({ + data: { + queryTransactionLink: { + id: 92, + amount: '22', + memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ', + createdAt: '2022-03-17T16:10:28.000Z', + validUntil: transactionLinkValidExpireDate(), + redeemedAt: null, + deletedAt: null, + user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' }, + }, + }, + }) + wrapper = Wrapper() + }) + + it('has a RedeemLoggedOut component', () => { + expect(wrapper.findComponent({ name: 'RedeemLoggedOut' }).exists()).toBe(true) + }) + + it('has a link to register with code', () => { + expect(wrapper.find('a[href="/register/some-code"]').exists()).toBe(true) + }) + + it('has a link to login with code', () => { + expect(wrapper.find('a[href="/login/some-code"]').exists()).toBe(true) + }) + }) + + describe('token in store and own link', () => { + beforeEach(() => { + mocks.$store.state.token = 'token' + apolloQueryMock.mockResolvedValue({ + data: { + queryTransactionLink: { + id: 92, + amount: '22', + memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ', + createdAt: '2022-03-17T16:10:28.000Z', + validUntil: transactionLinkValidExpireDate(), + redeemedAt: null, + deletedAt: null, + user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' }, + }, + }, + }) + wrapper = Wrapper() + }) + + it('has a RedeemSelfCreator component', () => { + expect(wrapper.findComponent({ name: 'RedeemSelfCreator' }).exists()).toBe(true) + }) + + it('has a no redeem text', () => { + expect(wrapper.findComponent({ name: 'RedeemSelfCreator' }).text()).toContain( + 'gdd_per_link.no-redeem', + ) + }) + + it('has a link to transaction page', () => { + expect(wrapper.find('a[to="/transactions"]').exists()).toBe(true) + }) + }) + + describe('valid link', () => { + beforeEach(() => { + mocks.$store.state.token = 'token' + apolloQueryMock.mockResolvedValue({ + data: { + queryTransactionLink: { + id: 92, + amount: '22', + memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ', + createdAt: '2022-03-17T16:10:28.000Z', + validUntil: transactionLinkValidExpireDate(), + redeemedAt: null, + deletedAt: null, + user: { firstName: 'Peter', publisherId: 0, email: 'peter@listig.de' }, + }, + }, + }) + wrapper = Wrapper() + }) + + it('has a RedeemValid component', () => { + expect(wrapper.findComponent({ name: 'RedeemValid' }).exists()).toBe(true) + }) + + it('has a button with redeem text', () => { + expect(wrapper.findComponent({ name: 'RedeemValid' }).find('button').text()).toBe( + 'gdd_per_link.redeem', + ) + }) + + describe('redeem link with success', () => { + let spy + + beforeEach(async () => { + spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') + apolloMutateMock.mockResolvedValue() + spy.mockImplementation(() => Promise.resolve(true)) + await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click') + }) + + it('opens the modal', () => { + expect(spy).toBeCalledWith('gdd_per_link.redeem-text') + }) + + it('calls the API', () => { + expect(apolloMutateMock).toBeCalledWith( + expect.objectContaining({ + mutation: redeemTransactionLink, + variables: { + code: 'some-code', + }, + }), + ) + }) + + it('toasts a success message', () => { + expect(mocks.$t).toBeCalledWith('gdd_per_link.redeemed', { n: '22' }) + expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.redeemed; ') + }) + + it('pushes the route to overview', () => { + expect(routerPushMock).toBeCalledWith('/overview') + }) + }) + + describe('cancel redeem link', () => { + let spy + + beforeEach(async () => { + spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') + apolloMutateMock.mockResolvedValue() + spy.mockImplementation(() => Promise.resolve(false)) + await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click') + }) + + it('does not call the API', () => { + expect(apolloMutateMock).not.toBeCalled() + }) + + it('does not toasts a success message', () => { + expect(mocks.$t).not.toBeCalledWith('gdd_per_link.redeemed', { n: '22' }) + expect(toastSuccessSpy).not.toBeCalled() + }) + + it('does not push the route', () => { + expect(routerPushMock).not.toBeCalled() + }) + }) + + describe('redeem link with error', () => { + let spy + + beforeEach(async () => { + spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') + apolloMutateMock.mockRejectedValue({ message: 'Oh Noo!' }) + spy.mockImplementation(() => Promise.resolve(true)) + await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click') + }) + + it('toasts an error message', () => { + expect(toastErrorSpy).toBeCalledWith('Oh Noo!') + }) + + it('pushes the route to overview', () => { + expect(routerPushMock).toBeCalledWith('/overview') + }) + }) + }) + + describe('error on transaction link query', () => { + beforeEach(() => { + apolloQueryMock.mockRejectedValue({ message: 'Ouchh!' }) + wrapper = Wrapper() + }) + + it('toasts an error message', () => { + expect(toastErrorSpy).toBeCalledWith('Ouchh!') + }) }) }) }) diff --git a/frontend/src/pages/TransactionLink.vue b/frontend/src/pages/TransactionLink.vue index 08601a977..def8e2e7c 100644 --- a/frontend/src/pages/TransactionLink.vue +++ b/frontend/src/pages/TransactionLink.vue @@ -65,9 +65,8 @@ export default { .then((result) => { this.linkData = result.data.queryTransactionLink }) - .catch(() => { - this.itemType = 'X4' - this.linkData.deletedAt = true + .catch((err) => { + this.toastError(err.message) }) }, redeemLink(amount) { @@ -97,7 +96,6 @@ export default { }, computed: { itemType() { - // logged out // link wurde gelöscht: am, von if (this.linkData.deletedAt) { // eslint-disable-next-line vue/no-side-effects-in-computed-properties @@ -105,24 +103,23 @@ export default { date: this.linkData.deletedAt, }) return `TEXT` - } else { - // link ist abgelaufen, nicht gelöscht - if (new Date(this.linkData.validUntil) < new Date()) { - // eslint-disable-next-line vue/no-side-effects-in-computed-properties - this.redeemedBoxText = this.$t('gdd_per_link.link-expired', { - date: this.linkData.validUntil, - }) - return `TEXT` - } + } + // link ist abgelaufen, nicht gelöscht + if (new Date(this.linkData.validUntil) < new Date()) { + // eslint-disable-next-line vue/no-side-effects-in-computed-properties + this.redeemedBoxText = this.$t('gdd_per_link.link-expired', { + date: this.linkData.validUntil, + }) + return `TEXT` + } - // der link wurde eingelöst, nicht gelöscht - if (this.linkData.redeemedAt) { - // eslint-disable-next-line vue/no-side-effects-in-computed-properties - this.redeemedBoxText = this.$t('gdd_per_link.redeemed-at', { - date: this.linkData.redeemedAt, - }) - return `TEXT` - } + // der link wurde eingelöst, nicht gelöscht + if (this.linkData.redeemedAt) { + // eslint-disable-next-line vue/no-side-effects-in-computed-properties + this.redeemedBoxText = this.$t('gdd_per_link.redeemed-at', { + date: this.linkData.redeemedAt, + }) + return `TEXT` } if (this.$store.state.token) { From be8e3f489ed5a1360c05bcd402d20725b06c9862 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 21 Mar 2022 16:37:44 +0100 Subject: [PATCH 4/5] test with param code --- frontend/src/pages/Login.spec.js | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/Login.spec.js b/frontend/src/pages/Login.spec.js index 732bc1301..e26888d49 100644 --- a/frontend/src/pages/Login.spec.js +++ b/frontend/src/pages/Login.spec.js @@ -52,6 +52,9 @@ describe('Login', () => { $router: { push: mockRouterPush, }, + $route: { + params: {}, + }, $apollo: { query: apolloQueryMock, }, @@ -224,13 +227,33 @@ describe('Login', () => { expect(mockStoreDispach).toBeCalledWith('login', 'token') }) - it('redirects to overview page', () => { - expect(mockRouterPush).toBeCalledWith('/overview') - }) - it('hides the spinner', () => { expect(spinnerHideMock).toBeCalled() }) + + describe('without code parameter', () => { + it('redirects to overview page', () => { + expect(mockRouterPush).toBeCalledWith('/overview') + }) + }) + + describe('with code parameter', () => { + beforeEach(async () => { + mocks.$route.params = { + code: 'some-code', + } + wrapper = Wrapper() + 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('redirects to overview page', () => { + expect(mockRouterPush).toBeCalledWith('/redeem/some-code') + }) + }) }) describe('login fails', () => { From eaa621cc3a3248c36d2ba4da5bd7b71ad66b0f1d Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 21 Mar 2022 16:39:13 +0100 Subject: [PATCH 5/5] coverage unit tests forntend tp 95% --- .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 e9762b4bb..18d1143db 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -438,7 +438,7 @@ jobs: report_name: Coverage Frontend type: lcov result_path: ./coverage/lcov.info - min_coverage: 94 + min_coverage: 95 token: ${{ github.token }} ##############################################################################