From 08d8c008f6bd66c3c726785ba13e8125b9973638 Mon Sep 17 00:00:00 2001 From: elweyn Date: Tue, 31 Jan 2023 13:07:41 +0100 Subject: [PATCH 01/70] Add denyContribution mutation to the seeds. --- backend/src/seeds/graphql/mutations.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index 2b4ed6656..8c3f97f17 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -266,6 +266,12 @@ export const deleteContribution = gql` } ` +export const denyContribution = gql` + mutation ($id: Int!) { + denyContribution(id: $id) + } +` + export const createContributionMessage = gql` mutation ($contributionId: Float!, $message: String!) { createContributionMessage(contributionId: $contributionId, message: $message) { From f3f749889f08aa148b4105f465b83e417c199e3e Mon Sep 17 00:00:00 2001 From: elweyn Date: Tue, 31 Jan 2023 13:24:59 +0100 Subject: [PATCH 02/70] Add Test for denyContribution. --- .../resolver/ContributionResolver.test.ts | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index abae8e446..f17851fd8 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -10,6 +10,7 @@ import { createContribution, updateContribution, deleteContribution, + denyContribution, confirmContribution, adminCreateContribution, adminCreateContributions, @@ -671,6 +672,123 @@ describe('ContributionResolver', () => { }) }) + describe('denyContribution', () => { + describe('unauthenticated', () => { + it('returns an error', async () => { + await expect( + mutate({ + mutation: denyContribution, + variables: { + id: 1, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + await userFactory(testEnv, peterLustig) + await userFactory(testEnv, bibiBloxberg) + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + result = await mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test env contribution', + creationDate: new Date().toString(), + }, + }) + await mutate({ + mutation: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + }) + + afterAll(async () => { + await cleanDB() + resetToken() + }) + + describe('wrong contribution id', () => { + it('throws an error', async () => { + jest.clearAllMocks() + await expect( + mutate({ + mutation: denyContribution, + variables: { + id: -1, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Contribution not found for given id.')], + }), + ) + }) + + it('logs the error found', () => { + expect(logger.error).toBeCalledWith('Contribution not found for given id: -1') + }) + }) + + describe('wrong user tries to deny the contribution', () => { + beforeAll(async () => { + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + it('throws an error', async () => { + jest.clearAllMocks() + await expect( + mutate({ + mutation: denyContribution, + variables: { + id: result.data.createContribution.id, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('valid input', () => { + it('deny contribution', async () => { + await mutate({ + mutation: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + await expect( + mutate({ + mutation: denyContribution, + variables: { + id: result.data.createContribution.id, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + denyContribution: true, + }, + }), + ) + }) + }) + }) + }) + describe('listAllContribution', () => { describe('unauthenticated', () => { it('returns an error', async () => { From 147035bf5d7917f6ac13a228fee6546aa42a6a0f Mon Sep 17 00:00:00 2001 From: ogerly Date: Wed, 1 Feb 2023 10:49:08 +0100 Subject: [PATCH 03/70] logo inserted with better quality. --- frontend/public/img/brand/gradido-logo.png | Bin 0 -> 51410 bytes frontend/src/components/Auth/AuthNavbar.vue | 13 +++++++------ frontend/src/components/Menu/Navbar.vue | 8 ++++---- frontend/src/pages/TransactionLink.vue | 1 - 4 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 frontend/public/img/brand/gradido-logo.png diff --git a/frontend/public/img/brand/gradido-logo.png b/frontend/public/img/brand/gradido-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..39b4a66366141be55aec64e2c0d01c1c1e008320 GIT binary patch literal 51410 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

r}pp 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�t7TT*$VGv*nqAkt8apw|7e9X= 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 P00000NkvXXu0mjfv$x35 literal 0 HcmV?d00001 diff --git a/frontend/src/components/Auth/AuthNavbar.vue b/frontend/src/components/Auth/AuthNavbar.vue index 7d8c6f08a..f7a8b6f0a 100644 --- a/frontend/src/components/Auth/AuthNavbar.vue +++ b/frontend/src/components/Auth/AuthNavbar.vue @@ -3,16 +3,16 @@ @@ -35,7 +35,8 @@ export default { mixins: [authLinks], data() { return { - logo: '/img/brand/green.png', + background_header: '/img/template/gradido_background_header.png', + logo: '/img/brand/gradido-logo.png', sheet: '/img/template/Blaetter.png', } }, diff --git a/frontend/src/components/Menu/Navbar.vue b/frontend/src/components/Menu/Navbar.vue index 03217a5ae..73470a91b 100644 --- a/frontend/src/components/Menu/Navbar.vue +++ b/frontend/src/components/Menu/Navbar.vue @@ -4,10 +4,10 @@

@@ -60,7 +60,7 @@ export default { }, data() { return { - logo: '/img/brand/green.png', + logo: '/img/brand/gradido-logo.png', sheet: '/img/template/Blaetter.png', } }, diff --git a/frontend/src/pages/TransactionLink.vue b/frontend/src/pages/TransactionLink.vue index c3875d20e..17bd98031 100644 --- a/frontend/src/pages/TransactionLink.vue +++ b/frontend/src/pages/TransactionLink.vue @@ -45,7 +45,6 @@ export default { }, data() { return { - img: '/img/brand/green.png', linkData: { __typename: 'TransactionLink', amount: '123.45', From 4f0cfb9363a908ffba815639f4ad4d4f5b11c4a9 Mon Sep 17 00:00:00 2001 From: elweyn Date: Fri, 3 Feb 2023 13:55:08 +0100 Subject: [PATCH 04/70] Change structure of tests so that we create every data needed beforeAll unit tests. --- .../resolver/ContributionResolver.test.ts | 1166 ++++++++++------- 1 file changed, 688 insertions(+), 478 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 9c917368b..226ab63cd 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -17,6 +17,8 @@ import { adminUpdateContribution, adminDeleteContribution, login, + logout, + adminCreateContributionMessage, } from '@/seeds/graphql/mutations' import { listAllContributions, @@ -69,7 +71,13 @@ let mutate: any, query: any, con: any let testEnv: any let creation: Contribution | void let admin: User -let result: any +// let result: any +// let contribution: any +let pendingContribution: any +let inProgressContribution: any +let contributionToConfirm: any +let contributionToDeny: any +let contributionToDelete: any beforeAll(async () => { testEnv = await testEnvironment(logger, localization) @@ -86,6 +94,75 @@ afterAll(async () => { describe('ContributionResolver', () => { let bibi: any + let peter: any + + beforeAll(async () => { + bibi = await userFactory(testEnv, bibiBloxberg) + admin = peter = await userFactory(testEnv, peterLustig) + const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de') + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await creationFactory(testEnv, bibisCreation!) + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + pendingContribution = await mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test PENDING contribution', + creationDate: new Date().toString(), + }, + }) + inProgressContribution = await mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test IN_PROGESS contribution', + creationDate: new Date().toString(), + }, + }) + await mutate({ + mutation: adminCreateContributionMessage, + variables: { + contributionId: inProgressContribution.data.createContribution.id, + message: 'Test message to IN_PROGESS contribution', + }, + }) + contributionToConfirm = await mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test contribution to confirm', + creationDate: new Date().toString(), + }, + }) + contributionToDeny = await mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test contribution to deny', + creationDate: new Date().toString(), + }, + }) + contributionToDelete = await mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test contribution to delete', + creationDate: new Date().toString(), + }, + }) + await mutate({ + mutation: logout, + }) + resetToken() + }) + + afterAll(async () => { + await cleanDB() + resetToken() + }) describe('createContribution', () => { describe('unauthenticated', () => { @@ -105,8 +182,6 @@ describe('ContributionResolver', () => { describe('authenticated with valid user', () => { beforeAll(async () => { - await userFactory(testEnv, bibiBloxberg) - bibi = await mutate({ mutation: login, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, @@ -114,7 +189,6 @@ describe('ContributionResolver', () => { }) afterAll(async () => { - await cleanDB() resetToken() }) @@ -222,27 +296,14 @@ describe('ContributionResolver', () => { }) describe('valid input', () => { - let contribution: any - - beforeAll(async () => { - contribution = await mutate({ - mutation: createContribution, - variables: { - amount: 100.0, - memo: 'Test env contribution', - creationDate: new Date().toString(), - }, - }) - }) - it('creates contribution', async () => { - expect(contribution).toEqual( + expect(pendingContribution).toEqual( expect.objectContaining({ data: { createContribution: { id: expect.any(Number), amount: '100', - memo: 'Test env contribution', + memo: 'Test PENDING contribution', }, }, }), @@ -254,7 +315,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ type: EventProtocolType.CONTRIBUTION_CREATE, amount: expect.decimalEqual(100), - contributionId: contribution.data.createContribution.id, + contributionId: pendingContribution.data.createContribution.id, userId: bibi.data.login.id, }), ) @@ -263,122 +324,6 @@ describe('ContributionResolver', () => { }) }) - describe('listContributions', () => { - describe('unauthenticated', () => { - it('returns an error', async () => { - await expect( - query({ - query: listContributions, - variables: { - currentPage: 1, - pageSize: 25, - order: 'DESC', - filterConfirmed: false, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('401 Unauthorized')], - }), - ) - }) - }) - - describe('authenticated', () => { - beforeAll(async () => { - await userFactory(testEnv, bibiBloxberg) - await userFactory(testEnv, peterLustig) - const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de') - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await creationFactory(testEnv, bibisCreation!) - await mutate({ - mutation: login, - variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, - }) - await mutate({ - mutation: createContribution, - variables: { - amount: 100.0, - memo: 'Test env contribution', - creationDate: new Date().toString(), - }, - }) - }) - - afterAll(async () => { - await cleanDB() - resetToken() - }) - - describe('filter confirmed is false', () => { - it('returns creations', async () => { - await expect( - query({ - query: listContributions, - variables: { - currentPage: 1, - pageSize: 25, - order: 'DESC', - filterConfirmed: false, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listContributions: { - contributionCount: 2, - contributionList: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(Number), - memo: 'Herzlich Willkommen bei Gradido!', - amount: '1000', - }), - expect.objectContaining({ - id: expect.any(Number), - memo: 'Test env contribution', - amount: '100', - }), - ]), - }, - }, - }), - ) - }) - }) - - describe('filter confirmed is true', () => { - it('returns only unconfirmed creations', async () => { - await expect( - query({ - query: listContributions, - variables: { - currentPage: 1, - pageSize: 25, - order: 'DESC', - filterConfirmed: true, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listContributions: { - contributionCount: 1, - contributionList: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(Number), - memo: 'Test env contribution', - amount: '100', - }), - ]), - }, - }, - }), - ) - }) - }) - }) - }) - describe('updateContribution', () => { describe('unauthenticated', () => { it('returns an error', async () => { @@ -402,24 +347,13 @@ describe('ContributionResolver', () => { describe('authenticated', () => { beforeAll(async () => { - await userFactory(testEnv, peterLustig) - await userFactory(testEnv, bibiBloxberg) await mutate({ mutation: login, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, }) - result = await mutate({ - mutation: createContribution, - variables: { - amount: 100.0, - memo: 'Test env contribution', - creationDate: new Date().toString(), - }, - }) }) afterAll(async () => { - await cleanDB() resetToken() }) @@ -456,7 +390,7 @@ describe('ContributionResolver', () => { mutate({ mutation: updateContribution, variables: { - contributionId: result.data.createContribution.id, + contributionId: pendingContribution.data.createContribution.id, amount: 100.0, memo: 'Test', creationDate: date.toString(), @@ -482,7 +416,7 @@ describe('ContributionResolver', () => { mutate({ mutation: updateContribution, variables: { - contributionId: result.data.createContribution.id, + contributionId: pendingContribution.data.createContribution.id, amount: 100.0, memo: 'Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test', creationDate: date.toString(), @@ -514,7 +448,7 @@ describe('ContributionResolver', () => { mutate({ mutation: updateContribution, variables: { - contributionId: result.data.createContribution.id, + contributionId: pendingContribution.data.createContribution.id, amount: 10.0, memo: 'Test env contribution', creationDate: new Date().toString(), @@ -539,13 +473,20 @@ describe('ContributionResolver', () => { }) describe('admin tries to update a user contribution', () => { + beforeAll(async () => { + await mutate({ + mutation: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + }) + it('throws an error', async () => { jest.clearAllMocks() await expect( mutate({ mutation: adminUpdateContribution, variables: { - id: result.data.createContribution.id, + id: pendingContribution.data.createContribution.id, email: 'bibi@bloxberg.de', amount: 10.0, memo: 'Test env contribution', @@ -562,7 +503,7 @@ describe('ContributionResolver', () => { // TODO check that the error is logged (need to modify AdminResolver, avoid conflicts) }) - describe('update too much so that the limit is exceeded', () => { + describe('update to much so that the limit is exceeded', () => { beforeAll(async () => { await mutate({ mutation: login, @@ -576,7 +517,7 @@ describe('ContributionResolver', () => { mutate({ mutation: updateContribution, variables: { - contributionId: result.data.createContribution.id, + contributionId: pendingContribution.data.createContribution.id, amount: 1019.0, memo: 'Test env contribution', creationDate: new Date().toString(), @@ -586,7 +527,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ errors: [ new GraphQLError( - 'The amount (1019 GDD) to be created exceeds the amount (1000 GDD) still available for this month.', + 'The amount (1019 GDD) to be created exceeds the amount (600 GDD) still available for this month.', ), ], }), @@ -595,7 +536,7 @@ describe('ContributionResolver', () => { it('logs the error found', () => { expect(logger.error).toBeCalledWith( - 'The amount (1019 GDD) to be created exceeds the amount (1000 GDD) still available for this month.', + 'The amount (1019 GDD) to be created exceeds the amount (600 GDD) still available for this month.', ) }) }) @@ -608,7 +549,7 @@ describe('ContributionResolver', () => { mutate({ mutation: updateContribution, variables: { - contributionId: result.data.createContribution.id, + contributionId: pendingContribution.data.createContribution.id, amount: 10.0, memo: 'Test env contribution', creationDate: date.setMonth(date.getMonth() - 3).toString(), @@ -635,9 +576,9 @@ describe('ContributionResolver', () => { mutate({ mutation: updateContribution, variables: { - contributionId: result.data.createContribution.id, + contributionId: pendingContribution.data.createContribution.id, amount: 10.0, - memo: 'Test contribution', + memo: 'Test PENDING contribution update', creationDate: new Date().toString(), }, }), @@ -645,9 +586,9 @@ describe('ContributionResolver', () => { expect.objectContaining({ data: { updateContribution: { - id: result.data.createContribution.id, + id: pendingContribution.data.createContribution.id, amount: '10', - memo: 'Test contribution', + memo: 'Test PENDING contribution update', }, }, }), @@ -664,7 +605,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ type: EventProtocolType.CONTRIBUTION_UPDATE, amount: expect.decimalEqual(10), - contributionId: result.data.createContribution.id, + contributionId: pendingContribution.data.createContribution.id, userId: bibi.data.login.id, }), ) @@ -691,22 +632,36 @@ describe('ContributionResolver', () => { }) }) - describe('authenticated', () => { + describe('authenticated without admin rights', () => { beforeAll(async () => { - await userFactory(testEnv, peterLustig) - await userFactory(testEnv, bibiBloxberg) await mutate({ mutation: login, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, }) - result = await mutate({ - mutation: createContribution, - variables: { - amount: 100.0, - memo: 'Test env contribution', - creationDate: new Date().toString(), - }, - }) + }) + + afterAll(() => { + resetToken() + }) + + it('returns an error', async () => { + await expect( + mutate({ + mutation: denyContribution, + variables: { + id: 1, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('authenticated with admin rights', () => { + beforeAll(async () => { await mutate({ mutation: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, @@ -714,13 +669,11 @@ describe('ContributionResolver', () => { }) afterAll(async () => { - await cleanDB() resetToken() }) describe('wrong contribution id', () => { it('throws an error', async () => { - jest.clearAllMocks() await expect( mutate({ mutation: denyContribution, @@ -740,31 +693,6 @@ describe('ContributionResolver', () => { }) }) - describe('wrong user tries to deny the contribution', () => { - beforeAll(async () => { - await mutate({ - mutation: login, - variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, - }) - }) - - it('throws an error', async () => { - jest.clearAllMocks() - await expect( - mutate({ - mutation: denyContribution, - variables: { - id: result.data.createContribution.id, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('401 Unauthorized')], - }), - ) - }) - }) - describe('valid input', () => { it('deny contribution', async () => { await mutate({ @@ -775,7 +703,7 @@ describe('ContributionResolver', () => { mutate({ mutation: denyContribution, variables: { - id: result.data.createContribution.id, + id: contributionToDeny.data.createContribution.id, }, }), ).resolves.toEqual( @@ -790,6 +718,282 @@ describe('ContributionResolver', () => { }) }) + describe('deleteContribution', () => { + describe('unauthenticated', () => { + it('returns an error', async () => { + await expect( + query({ + query: deleteContribution, + variables: { + id: -1, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + bibi = await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + afterAll(async () => { + resetToken() + }) + + describe('wrong contribution id', () => { + it('returns an error', async () => { + await expect( + mutate({ + mutation: deleteContribution, + variables: { + id: -1, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Contribution not found for given id.')], + }), + ) + }) + + it('logs the error found', () => { + expect(logger.error).toBeCalledWith('Contribution not found for given id') + }) + }) + + describe('other user sends a deleteContribution', () => { + it('returns an error', async () => { + await mutate({ + mutation: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + await expect( + mutate({ + mutation: deleteContribution, + variables: { + id: contributionToDelete.data.createContribution.id, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Can not delete contribution of another user')], + }), + ) + }) + + it('logs the error found', () => { + expect(logger.error).toBeCalledWith('Can not delete contribution of another user') + }) + }) + + describe('User deletes own contribution', () => { + it('deletes successfully', async () => { + await expect( + mutate({ + mutation: deleteContribution, + variables: { + id: contributionToDelete.data.createContribution.id, + }, + }), + ).resolves.toBeTruthy() + }) + + it('stores the delete contribution event in the database', async () => { + const contribution = await mutate({ + mutation: createContribution, + variables: { + amount: 166.0, + memo: 'Whatever contribution', + creationDate: new Date().toString(), + }, + }) + + await mutate({ + mutation: deleteContribution, + variables: { + id: contribution.data.createContribution.id, + }, + }) + + await expect(EventProtocol.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventProtocolType.CONTRIBUTION_DELETE, + contributionId: contribution.data.createContribution.id, + amount: expect.decimalEqual(166), + userId: peter.id, + }), + ) + }) + }) + + describe('User deletes already confirmed contribution', () => { + it('throws an error', async () => { + jest.clearAllMocks() + await mutate({ + mutation: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + await mutate({ + mutation: confirmContribution, + variables: { + id: contributionToConfirm.data.createContribution.id, + }, + }) + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + await expect( + mutate({ + mutation: deleteContribution, + variables: { + id: contributionToConfirm.data.createContribution.id, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('A confirmed contribution can not be deleted')], + }), + ) + }) + + it('logs the error found', () => { + expect(logger.error).toBeCalledWith('A confirmed contribution can not be deleted') + }) + }) + }) + }) + + describe('listContributions', () => { + describe('unauthenticated', () => { + it('returns an error', async () => { + await expect( + query({ + query: listContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + filterConfirmed: false, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + afterAll(async () => { + resetToken() + }) + + describe('filter confirmed is false', () => { + it('returns creations', async () => { + await expect( + query({ + query: listContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + filterConfirmed: false, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listContributions: { + contributionCount: 6, + contributionList: expect.arrayContaining([ + expect.objectContaining({ + amount: '100', + id: expect.any(Number), + memo: 'Test contribution to confirm', + }), + expect.objectContaining({ + id: expect.any(Number), + memo: 'Test PENDING contribution update', + amount: '10', + }), + expect.objectContaining({ + id: expect.any(Number), + memo: 'Test contribution to deny', + amount: '100', + }), + expect.objectContaining({ + id: expect.any(Number), + memo: 'Test contribution to delete', + amount: '100', + }), + expect.objectContaining({ + id: expect.any(Number), + memo: 'Test IN_PROGESS contribution', + amount: '100', + }), + expect.objectContaining({ + id: expect.any(Number), + memo: 'Herzlich Willkommen bei Gradido!', + amount: '1000', + }), + ]), + }, + }, + }), + ) + }) + }) + + describe('filter confirmed is true', () => { + it('returns only unconfirmed creations', async () => { + await expect( + query({ + query: listContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + filterConfirmed: true, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listContributions: { + contributionCount: 4, + contributionList: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + memo: 'Test contribution to delete', + amount: '100', + }), + ]), + }, + }, + }), + ) + }) + }) + }) + }) + describe('listAllContribution', () => { describe('unauthenticated', () => { it('returns an error', async () => { @@ -813,27 +1017,13 @@ describe('ContributionResolver', () => { describe('authenticated', () => { beforeAll(async () => { - await userFactory(testEnv, bibiBloxberg) - await userFactory(testEnv, peterLustig) - const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de') - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await creationFactory(testEnv, bibisCreation!) await mutate({ mutation: login, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, }) - await mutate({ - mutation: createContribution, - variables: { - amount: 100.0, - memo: 'Test env contribution', - creationDate: new Date().toString(), - }, - }) }) afterAll(async () => { - await cleanDB() resetToken() }) @@ -920,20 +1110,44 @@ describe('ContributionResolver', () => { expect.objectContaining({ data: { listAllContributions: { - contributionCount: 2, + contributionCount: 6, contributionList: expect.arrayContaining([ + expect.objectContaining({ + amount: '100', + state: 'CONFIRMED', + id: expect.any(Number), + memo: 'Test contribution to confirm', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'PENDING', + memo: 'Test PENDING contribution update', + amount: '10', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Test contribution to deny', + amount: '100', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'DELETED', + memo: 'Test contribution to delete', + amount: '100', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'IN_PROGRESS', + memo: 'Test IN_PROGESS contribution', + amount: '100', + }), expect.objectContaining({ id: expect.any(Number), state: 'CONFIRMED', memo: 'Herzlich Willkommen bei Gradido!', amount: '1000', }), - expect.objectContaining({ - id: expect.any(Number), - state: 'PENDING', - memo: 'Test env contribution', - amount: '100', - }), ]), }, }, @@ -956,20 +1170,44 @@ describe('ContributionResolver', () => { expect.objectContaining({ data: { listAllContributions: { - contributionCount: 2, + contributionCount: 6, contributionList: expect.arrayContaining([ + expect.objectContaining({ + amount: '100', + state: 'CONFIRMED', + id: expect.any(Number), + memo: 'Test contribution to confirm', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'PENDING', + memo: 'Test PENDING contribution update', + amount: '10', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Test contribution to deny', + amount: '100', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'DELETED', + memo: 'Test contribution to delete', + amount: '100', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'IN_PROGRESS', + memo: 'Test IN_PROGESS contribution', + amount: '100', + }), expect.objectContaining({ id: expect.any(Number), state: 'CONFIRMED', memo: 'Herzlich Willkommen bei Gradido!', amount: '1000', }), - expect.objectContaining({ - id: expect.any(Number), - state: 'PENDING', - memo: 'Test env contribution', - amount: '100', - }), ]), }, }, @@ -992,20 +1230,44 @@ describe('ContributionResolver', () => { expect.objectContaining({ data: { listAllContributions: { - contributionCount: 2, + contributionCount: 6, contributionList: expect.arrayContaining([ + expect.objectContaining({ + amount: '100', + state: 'CONFIRMED', + id: expect.any(Number), + memo: 'Test contribution to confirm', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'PENDING', + memo: 'Test PENDING contribution update', + amount: '10', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Test contribution to deny', + amount: '100', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'DELETED', + memo: 'Test contribution to delete', + amount: '100', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'IN_PROGRESS', + memo: 'Test IN_PROGESS contribution', + amount: '100', + }), expect.objectContaining({ id: expect.any(Number), state: 'CONFIRMED', memo: 'Herzlich Willkommen bei Gradido!', amount: '1000', }), - expect.objectContaining({ - id: expect.any(Number), - state: 'PENDING', - memo: 'Test env contribution', - amount: '100', - }), ]), }, }, @@ -1028,20 +1290,44 @@ describe('ContributionResolver', () => { expect.objectContaining({ data: { listAllContributions: { - contributionCount: 1, + contributionCount: 2, contributionList: expect.arrayContaining([ + expect.objectContaining({ + amount: '100', + state: 'CONFIRMED', + id: expect.any(Number), + memo: 'Test contribution to confirm', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'PENDING', + memo: 'Test PENDING contribution update', + amount: '10', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Test contribution to deny', + amount: '100', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'DELETED', + memo: 'Test contribution to delete', + amount: '100', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'IN_PROGRESS', + memo: 'Test IN_PROGESS contribution', + amount: '100', + }), expect.objectContaining({ id: expect.any(Number), state: 'CONFIRMED', memo: 'Herzlich Willkommen bei Gradido!', amount: '1000', }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'PENDING', - memo: 'Test env contribution', - amount: '100', - }), ]), }, }, @@ -1067,17 +1353,41 @@ describe('ContributionResolver', () => { contributionCount: 1, contributionList: expect.arrayContaining([ expect.not.objectContaining({ - id: expect.any(Number), + amount: '100', state: 'CONFIRMED', - memo: 'Herzlich Willkommen bei Gradido!', - amount: '1000', + id: expect.any(Number), + memo: 'Test contribution to confirm', }), expect.objectContaining({ id: expect.any(Number), state: 'PENDING', - memo: 'Test env contribution', + memo: 'Test PENDING contribution update', + amount: '10', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Test contribution to deny', amount: '100', }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'DELETED', + memo: 'Test contribution to delete', + amount: '100', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'IN_PROGRESS', + memo: 'Test IN_PROGESS contribution', + amount: '100', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'CONFIRMED', + memo: 'Herzlich Willkommen bei Gradido!', + amount: '1000', + }), ]), }, }, @@ -1100,20 +1410,44 @@ describe('ContributionResolver', () => { expect.objectContaining({ data: { listAllContributions: { - contributionCount: 0, - contributionList: expect.not.arrayContaining([ + contributionCount: 1, + contributionList: expect.arrayContaining([ + expect.not.objectContaining({ + amount: '100', + state: 'CONFIRMED', + id: expect.any(Number), + memo: 'Test contribution to confirm', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'PENDING', + memo: 'Test PENDING contribution update', + amount: '10', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Test contribution to deny', + amount: '100', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'DELETED', + memo: 'Test contribution to delete', + amount: '100', + }), expect.objectContaining({ + id: expect.any(Number), + state: 'IN_PROGRESS', + memo: 'Test IN_PROGESS contribution', + amount: '100', + }), + expect.not.objectContaining({ id: expect.any(Number), state: 'CONFIRMED', memo: 'Herzlich Willkommen bei Gradido!', amount: '1000', }), - expect.objectContaining({ - id: expect.any(Number), - state: 'PENDING', - memo: 'Test env contribution', - amount: '100', - }), ]), }, }, @@ -1136,20 +1470,44 @@ describe('ContributionResolver', () => { expect.objectContaining({ data: { listAllContributions: { - contributionCount: 0, - contributionList: expect.not.arrayContaining([ + contributionCount: 1, + contributionList: expect.arrayContaining([ + expect.not.objectContaining({ + amount: '100', + state: 'CONFIRMED', + id: expect.any(Number), + memo: 'Test contribution to confirm', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'PENDING', + memo: 'Test PENDING contribution update', + amount: '10', + }), expect.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Test contribution to deny', + amount: '100', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'DELETED', + memo: 'Test contribution to delete', + amount: '100', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'IN_PROGRESS', + memo: 'Test IN_PROGESS contribution', + amount: '100', + }), + expect.not.objectContaining({ id: expect.any(Number), state: 'CONFIRMED', memo: 'Herzlich Willkommen bei Gradido!', amount: '1000', }), - expect.objectContaining({ - id: expect.any(Number), - state: 'PENDING', - memo: 'Test env contribution', - amount: '100', - }), ]), }, }, @@ -1173,20 +1531,7 @@ describe('ContributionResolver', () => { data: { listAllContributions: { contributionCount: 0, - contributionList: expect.not.arrayContaining([ - expect.objectContaining({ - id: expect.any(Number), - state: 'CONFIRMED', - memo: 'Herzlich Willkommen bei Gradido!', - amount: '1000', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'PENDING', - memo: 'Test env contribution', - amount: '100', - }), - ]), + contributionList: [], }, }, }), @@ -1208,20 +1553,44 @@ describe('ContributionResolver', () => { expect.objectContaining({ data: { listAllContributions: { - contributionCount: 2, + contributionCount: 3, contributionList: expect.arrayContaining([ + expect.objectContaining({ + amount: '100', + state: 'CONFIRMED', + id: expect.any(Number), + memo: 'Test contribution to confirm', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'PENDING', + memo: 'Test PENDING contribution update', + amount: '10', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Test contribution to deny', + amount: '100', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'DELETED', + memo: 'Test contribution to delete', + amount: '100', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'IN_PROGRESS', + memo: 'Test IN_PROGESS contribution', + amount: '100', + }), expect.objectContaining({ id: expect.any(Number), state: 'CONFIRMED', memo: 'Herzlich Willkommen bei Gradido!', amount: '1000', }), - expect.objectContaining({ - id: expect.any(Number), - state: 'PENDING', - memo: 'Test env contribution', - amount: '100', - }), ]), }, }, @@ -1231,173 +1600,6 @@ describe('ContributionResolver', () => { }) }) - describe('deleteContribution', () => { - describe('unauthenticated', () => { - it('returns an error', async () => { - await expect( - query({ - query: deleteContribution, - variables: { - id: -1, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('401 Unauthorized')], - }), - ) - }) - }) - - describe('authenticated', () => { - let peter: any - beforeAll(async () => { - await userFactory(testEnv, bibiBloxberg) - peter = await userFactory(testEnv, peterLustig) - - await mutate({ - mutation: login, - variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, - }) - result = await mutate({ - mutation: createContribution, - variables: { - amount: 100.0, - memo: 'Test env contribution', - creationDate: new Date().toString(), - }, - }) - }) - - afterAll(async () => { - await cleanDB() - resetToken() - }) - - describe('wrong contribution id', () => { - it('returns an error', async () => { - await expect( - mutate({ - mutation: deleteContribution, - variables: { - id: -1, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('Contribution not found for given id.')], - }), - ) - }) - - it('logs the error found', () => { - expect(logger.error).toBeCalledWith('Contribution not found for given id') - }) - }) - - describe('other user sends a deleteContribution', () => { - it('returns an error', async () => { - await mutate({ - mutation: login, - variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, - }) - await expect( - mutate({ - mutation: deleteContribution, - variables: { - id: result.data.createContribution.id, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('Can not delete contribution of another user')], - }), - ) - }) - - it('logs the error found', () => { - expect(logger.error).toBeCalledWith('Can not delete contribution of another user') - }) - }) - - describe('User deletes own contribution', () => { - it('deletes successfully', async () => { - await expect( - mutate({ - mutation: deleteContribution, - variables: { - id: result.data.createContribution.id, - }, - }), - ).resolves.toBeTruthy() - }) - - it('stores the delete contribution event in the database', async () => { - const contribution = await mutate({ - mutation: createContribution, - variables: { - amount: 166.0, - memo: 'Whatever contribution', - creationDate: new Date().toString(), - }, - }) - - await mutate({ - mutation: deleteContribution, - variables: { - id: contribution.data.createContribution.id, - }, - }) - - await expect(EventProtocol.find()).resolves.toContainEqual( - expect.objectContaining({ - type: EventProtocolType.CONTRIBUTION_DELETE, - contributionId: contribution.data.createContribution.id, - amount: expect.decimalEqual(166), - userId: peter.id, - }), - ) - }) - }) - - describe('User deletes already confirmed contribution', () => { - it('throws an error', async () => { - jest.clearAllMocks() - await mutate({ - mutation: login, - variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, - }) - await mutate({ - mutation: confirmContribution, - variables: { - id: result.data.createContribution.id, - }, - }) - await mutate({ - mutation: login, - variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, - }) - await expect( - mutate({ - mutation: deleteContribution, - variables: { - id: result.data.createContribution.id, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('A confirmed contribution can not be deleted')], - }), - ) - }) - - it('logs the error found', () => { - expect(logger.error).toBeCalledWith('A confirmed contribution can not be deleted') - }) - }) - }) - }) - describe('contributions', () => { const variables = { email: 'bibi@bloxberg.de', @@ -1505,7 +1707,6 @@ describe('ContributionResolver', () => { describe('authenticated', () => { describe('without admin rights', () => { beforeAll(async () => { - await userFactory(testEnv, bibiBloxberg) await mutate({ mutation: login, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, @@ -1513,7 +1714,6 @@ describe('ContributionResolver', () => { }) afterAll(async () => { - await cleanDB() resetToken() }) @@ -1614,7 +1814,6 @@ describe('ContributionResolver', () => { describe('with admin rights', () => { beforeAll(async () => { - admin = await userFactory(testEnv, peterLustig) await mutate({ mutation: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, @@ -1622,7 +1821,6 @@ describe('ContributionResolver', () => { }) afterAll(async () => { - await cleanDB() resetToken() }) @@ -1644,6 +1842,7 @@ describe('ContributionResolver', () => { creation = await Contribution.findOneOrFail({ where: { memo: 'Herzlich Willkommen bei Gradido!', + amount: 400, }, }) }) @@ -1651,6 +1850,7 @@ describe('ContributionResolver', () => { describe('user to create for does not exist', () => { it('throws an error', async () => { jest.clearAllMocks() + variables.email = 'some@fake.email' variables.creationDate = contributionDateFormatter( new Date(now.getFullYear(), now.getMonth() - 1, 1), ) @@ -1658,15 +1858,13 @@ describe('ContributionResolver', () => { mutate({ mutation: adminCreateContribution, variables }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('Could not find user with email: bibi@bloxberg.de')], + errors: [new GraphQLError('Could not find user with email: some@fake.email')], }), ) }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( - 'Could not find user with email: bibi@bloxberg.de', - ) + expect(logger.error).toBeCalledWith('Could not find user with email: some@fake.email') }) }) @@ -1730,7 +1928,6 @@ describe('ContributionResolver', () => { describe('valid user to create for', () => { beforeAll(async () => { - await userFactory(testEnv, bibiBloxberg) variables.email = 'bibi@bloxberg.de' variables.creationDate = 'invalid-date' }) @@ -1812,7 +2009,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ errors: [ new GraphQLError( - 'The amount (2000 GDD) to be created exceeds the amount (1000 GDD) still available for this month.', + 'The amount (2000 GDD) to be created exceeds the amount (900 GDD) still available for this month.', ), ], }), @@ -1821,7 +2018,7 @@ describe('ContributionResolver', () => { it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - 'The amount (2000 GDD) to be created exceeds the amount (1000 GDD) still available for this month.', + 'The amount (2000 GDD) to be created exceeds the amount (900 GDD) still available for this month.', ) }) }) @@ -1834,7 +2031,7 @@ describe('ContributionResolver', () => { ).resolves.toEqual( expect.objectContaining({ data: { - adminCreateContribution: [1000, 1000, 800], + adminCreateContribution: [1000, 1000, 700], }, }), ) @@ -1859,7 +2056,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ errors: [ new GraphQLError( - 'The amount (1000 GDD) to be created exceeds the amount (800 GDD) still available for this month.', + 'The amount (1000 GDD) to be created exceeds the amount (700 GDD) still available for this month.', ), ], }), @@ -1868,7 +2065,7 @@ describe('ContributionResolver', () => { it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - 'The amount (1000 GDD) to be created exceeds the amount (800 GDD) still available for this month.', + 'The amount (1000 GDD) to be created exceeds the amount (700 GDD) still available for this month.', ) }) }) @@ -2013,6 +2210,7 @@ describe('ContributionResolver', () => { describe('user email does not match creation user', () => { it('throws an error', async () => { jest.clearAllMocks() + console.log('creation', creation) await expect( mutate({ mutation: adminUpdateContribution, @@ -2170,7 +2368,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ data: { listUnconfirmedContributions: expect.arrayContaining([ - { + expect.objectContaining({ id: expect.any(Number), firstName: 'Peter', lastName: 'Lustig', @@ -2179,9 +2377,9 @@ describe('ContributionResolver', () => { memo: 'Das war leider zu Viel!', amount: '200', moderator: admin.id, - creation: ['1000', '800', '500'], - }, - { + creation: ['1000', '600', '500'], + }), + expect.objectContaining({ id: expect.any(Number), firstName: 'Peter', lastName: 'Lustig', @@ -2190,9 +2388,20 @@ describe('ContributionResolver', () => { memo: 'Grundeinkommen', amount: '500', moderator: admin.id, - creation: ['1000', '800', '500'], - }, - { + creation: ['1000', '600', '500'], + }), + expect.objectContaining({ + id: expect.any(Number), + firstName: 'Bibi', + lastName: 'Bloxberg', + email: 'bibi@bloxberg.de', + date: expect.any(String), + memo: 'Test contribution to delete', + amount: '100', + moderator: null, + creation: ['1000', '1000', '200'], + }), + expect.objectContaining({ id: expect.any(Number), firstName: 'Bibi', lastName: 'Bloxberg', @@ -2201,9 +2410,9 @@ describe('ContributionResolver', () => { memo: 'Grundeinkommen', amount: '500', moderator: admin.id, - creation: ['1000', '1000', '300'], - }, - { + creation: ['1000', '1000', '200'], + }), + expect.objectContaining({ id: expect.any(Number), firstName: 'Bibi', lastName: 'Bloxberg', @@ -2212,8 +2421,8 @@ describe('ContributionResolver', () => { memo: 'Aktives Grundeinkommen', amount: '200', moderator: admin.id, - creation: ['1000', '1000', '300'], - }, + creation: ['1000', '1000', '200'], + }), ]), }, }), @@ -2245,12 +2454,13 @@ describe('ContributionResolver', () => { }) describe('admin deletes own user contribution', () => { + let ownContribution: any beforeAll(async () => { await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, }) - result = await mutate({ + ownContribution = await mutate({ mutation: createContribution, variables: { amount: 100.0, @@ -2266,7 +2476,7 @@ describe('ContributionResolver', () => { mutate({ mutation: adminDeleteContribution, variables: { - id: result.data.createContribution.id, + id: ownContribution.data.createContribution.id, }, }), ).resolves.toEqual( From 0575c513c4f91d0bd200bac620b32edc8d514796 Mon Sep 17 00:00:00 2001 From: elweyn Date: Mon, 6 Feb 2023 08:01:29 +0100 Subject: [PATCH 05/70] End refactoring of ContributionResolver.test. --- .../resolver/ContributionResolver.test.ts | 147 ++++++++++++++---- 1 file changed, 113 insertions(+), 34 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 226ab63cd..a7b6716db 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -118,17 +118,10 @@ describe('ContributionResolver', () => { mutation: createContribution, variables: { amount: 100.0, - memo: 'Test IN_PROGESS contribution', + memo: 'Test IN_PROGRESS contribution', creationDate: new Date().toString(), }, }) - await mutate({ - mutation: adminCreateContributionMessage, - variables: { - contributionId: inProgressContribution.data.createContribution.id, - message: 'Test message to IN_PROGESS contribution', - }, - }) contributionToConfirm = await mutate({ mutation: createContribution, variables: { @@ -153,6 +146,17 @@ describe('ContributionResolver', () => { creationDate: new Date().toString(), }, }) + await mutate({ + mutation: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + await mutate({ + mutation: adminCreateContributionMessage, + variables: { + contributionId: inProgressContribution.data.createContribution.id, + message: 'Test message to IN_PROGRESS contribution', + }, + }) await mutate({ mutation: logout, }) @@ -770,11 +774,18 @@ describe('ContributionResolver', () => { }) describe('other user sends a deleteContribution', () => { - it('returns an error', async () => { + beforeAll(async () => { await mutate({ mutation: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, }) + }) + + afterAll(() => { + resetToken() + }) + + it('returns an error', async () => { await expect( mutate({ mutation: deleteContribution, @@ -795,6 +806,17 @@ describe('ContributionResolver', () => { }) describe('User deletes own contribution', () => { + beforeAll(async () => { + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + afterAll(() => { + resetToken() + }) + it('deletes successfully', async () => { await expect( mutate({ @@ -803,10 +825,21 @@ describe('ContributionResolver', () => { id: contributionToDelete.data.createContribution.id, }, }), - ).resolves.toBeTruthy() + ).resolves.toEqual( + expect.objectContaining({ + data: { + deleteContribution: true, + }, + }), + ) }) it('stores the delete contribution event in the database', async () => { + await mutate({ + mutation: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + const contribution = await mutate({ mutation: createContribution, variables: { @@ -945,7 +978,7 @@ describe('ContributionResolver', () => { }), expect.objectContaining({ id: expect.any(Number), - memo: 'Test IN_PROGESS contribution', + memo: 'Test IN_PROGRESS contribution', amount: '100', }), expect.objectContaining({ @@ -979,11 +1012,36 @@ describe('ContributionResolver', () => { listContributions: { contributionCount: 4, contributionList: expect.arrayContaining([ + expect.not.objectContaining({ + amount: '100', + id: expect.any(Number), + memo: 'Test contribution to confirm', + }), + expect.objectContaining({ + id: expect.any(Number), + memo: 'Test PENDING contribution update', + amount: '10', + }), + expect.objectContaining({ + id: expect.any(Number), + memo: 'Test contribution to deny', + amount: '100', + }), expect.objectContaining({ id: expect.any(Number), memo: 'Test contribution to delete', amount: '100', }), + expect.objectContaining({ + id: expect.any(Number), + memo: 'Test IN_PROGRESS contribution', + amount: '100', + }), + expect.not.objectContaining({ + id: expect.any(Number), + memo: 'Herzlich Willkommen bei Gradido!', + amount: '1000', + }), ]), }, }, @@ -1110,7 +1168,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ data: { listAllContributions: { - contributionCount: 6, + contributionCount: 5, contributionList: expect.arrayContaining([ expect.objectContaining({ amount: '100', @@ -1139,7 +1197,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ id: expect.any(Number), state: 'IN_PROGRESS', - memo: 'Test IN_PROGESS contribution', + memo: 'Test IN_PROGRESS contribution', amount: '100', }), expect.objectContaining({ @@ -1170,7 +1228,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ data: { listAllContributions: { - contributionCount: 6, + contributionCount: 5, contributionList: expect.arrayContaining([ expect.objectContaining({ amount: '100', @@ -1199,7 +1257,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ id: expect.any(Number), state: 'IN_PROGRESS', - memo: 'Test IN_PROGESS contribution', + memo: 'Test IN_PROGRESS contribution', amount: '100', }), expect.objectContaining({ @@ -1230,7 +1288,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ data: { listAllContributions: { - contributionCount: 6, + contributionCount: 5, contributionList: expect.arrayContaining([ expect.objectContaining({ amount: '100', @@ -1259,7 +1317,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ id: expect.any(Number), state: 'IN_PROGRESS', - memo: 'Test IN_PROGESS contribution', + memo: 'Test IN_PROGRESS contribution', amount: '100', }), expect.objectContaining({ @@ -1319,7 +1377,7 @@ describe('ContributionResolver', () => { expect.not.objectContaining({ id: expect.any(Number), state: 'IN_PROGRESS', - memo: 'Test IN_PROGESS contribution', + memo: 'Test IN_PROGRESS contribution', amount: '100', }), expect.objectContaining({ @@ -1379,7 +1437,7 @@ describe('ContributionResolver', () => { expect.not.objectContaining({ id: expect.any(Number), state: 'IN_PROGRESS', - memo: 'Test IN_PROGESS contribution', + memo: 'Test IN_PROGRESS contribution', amount: '100', }), expect.not.objectContaining({ @@ -1439,7 +1497,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ id: expect.any(Number), state: 'IN_PROGRESS', - memo: 'Test IN_PROGESS contribution', + memo: 'Test IN_PROGRESS contribution', amount: '100', }), expect.not.objectContaining({ @@ -1499,7 +1557,7 @@ describe('ContributionResolver', () => { expect.not.objectContaining({ id: expect.any(Number), state: 'IN_PROGRESS', - memo: 'Test IN_PROGESS contribution', + memo: 'Test IN_PROGRESS contribution', amount: '100', }), expect.not.objectContaining({ @@ -1582,7 +1640,7 @@ describe('ContributionResolver', () => { expect.not.objectContaining({ id: expect.any(Number), state: 'IN_PROGRESS', - memo: 'Test IN_PROGESS contribution', + memo: 'Test IN_PROGRESS contribution', amount: '100', }), expect.objectContaining({ @@ -2009,7 +2067,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ errors: [ new GraphQLError( - 'The amount (2000 GDD) to be created exceeds the amount (900 GDD) still available for this month.', + 'The amount (2000 GDD) to be created exceeds the amount (790 GDD) still available for this month.', ), ], }), @@ -2018,7 +2076,7 @@ describe('ContributionResolver', () => { it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - 'The amount (2000 GDD) to be created exceeds the amount (900 GDD) still available for this month.', + 'The amount (2000 GDD) to be created exceeds the amount (790 GDD) still available for this month.', ) }) }) @@ -2031,7 +2089,7 @@ describe('ContributionResolver', () => { ).resolves.toEqual( expect.objectContaining({ data: { - adminCreateContribution: [1000, 1000, 700], + adminCreateContribution: [1000, 1000, 590], }, }), ) @@ -2056,7 +2114,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ errors: [ new GraphQLError( - 'The amount (1000 GDD) to be created exceeds the amount (700 GDD) still available for this month.', + 'The amount (1000 GDD) to be created exceeds the amount (590 GDD) still available for this month.', ), ], }), @@ -2065,7 +2123,7 @@ describe('ContributionResolver', () => { it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - 'The amount (1000 GDD) to be created exceeds the amount (700 GDD) still available for this month.', + 'The amount (1000 GDD) to be created exceeds the amount (590 GDD) still available for this month.', ) }) }) @@ -2210,7 +2268,6 @@ describe('ContributionResolver', () => { describe('user email does not match creation user', () => { it('throws an error', async () => { jest.clearAllMocks() - console.log('creation', creation) await expect( mutate({ mutation: adminUpdateContribution, @@ -2377,7 +2434,7 @@ describe('ContributionResolver', () => { memo: 'Das war leider zu Viel!', amount: '200', moderator: admin.id, - creation: ['1000', '600', '500'], + creation: ['1000', '800', '500'], }), expect.objectContaining({ id: expect.any(Number), @@ -2388,9 +2445,9 @@ describe('ContributionResolver', () => { memo: 'Grundeinkommen', amount: '500', moderator: admin.id, - creation: ['1000', '600', '500'], + creation: ['1000', '800', '500'], }), - expect.objectContaining({ + expect.not.objectContaining({ id: expect.any(Number), firstName: 'Bibi', lastName: 'Bloxberg', @@ -2399,7 +2456,29 @@ describe('ContributionResolver', () => { memo: 'Test contribution to delete', amount: '100', moderator: null, - creation: ['1000', '1000', '200'], + creation: ['1000', '1000', '90'], + }), + expect.objectContaining({ + id: expect.any(Number), + firstName: 'Bibi', + lastName: 'Bloxberg', + email: 'bibi@bloxberg.de', + date: expect.any(String), + memo: 'Test PENDING contribution update', + amount: '10', + moderator: null, + creation: ['1000', '1000', '90'], + }), + expect.objectContaining({ + id: expect.any(Number), + firstName: 'Bibi', + lastName: 'Bloxberg', + email: 'bibi@bloxberg.de', + date: expect.any(String), + memo: 'Test IN_PROGRESS contribution', + amount: '100', + moderator: null, + creation: ['1000', '1000', '90'], }), expect.objectContaining({ id: expect.any(Number), @@ -2410,7 +2489,7 @@ describe('ContributionResolver', () => { memo: 'Grundeinkommen', amount: '500', moderator: admin.id, - creation: ['1000', '1000', '200'], + creation: ['1000', '1000', '90'], }), expect.objectContaining({ id: expect.any(Number), @@ -2421,7 +2500,7 @@ describe('ContributionResolver', () => { memo: 'Aktives Grundeinkommen', amount: '200', moderator: admin.id, - creation: ['1000', '1000', '200'], + creation: ['1000', '1000', '90'], }), ]), }, From b7343fdc3e950d1b5988dd4d2ac2c97f1f45a7c3 Mon Sep 17 00:00:00 2001 From: elweyn Date: Mon, 6 Feb 2023 09:25:44 +0100 Subject: [PATCH 06/70] Add test user raeuber hotzenplotz and deny already confirmed, deleted or denied contributions. --- .../resolver/ContributionResolver.test.ts | 263 +++++++++++++++++- 1 file changed, 257 insertions(+), 6 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index a7b6716db..eaf157edc 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -49,6 +49,7 @@ import { User } from '@entity/User' import { EventProtocolType } from '@/event/EventProtocolType' import { logger, i18n as localization } from '@test/testSetup' import { UserInputError } from 'apollo-server-express' +import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz' // mock account activation email to avoid console spam // mock account activation email to avoid console spam @@ -94,11 +95,13 @@ afterAll(async () => { describe('ContributionResolver', () => { let bibi: any + let raueber: any let peter: any beforeAll(async () => { bibi = await userFactory(testEnv, bibiBloxberg) admin = peter = await userFactory(testEnv, peterLustig) + raueber = await userFactory(testEnv, raeuberHotzenplotz) const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de') // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await creationFactory(testEnv, bibisCreation!) @@ -697,6 +700,158 @@ describe('ContributionResolver', () => { }) }) + describe('deny contribution that is already confirmed', () => { + let contribution: any + it('throws an error', async () => { + await mutate({ + mutation: login, + variables: { email: 'raeuber@hotzenplotz.de', password: 'Aa12345_' }, + }) + + contribution = await mutate({ + mutation: createContribution, + variables: { + amount: 166.0, + memo: 'Whatever contribution', + creationDate: new Date().toString(), + }, + }) + + await mutate({ + mutation: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + + await mutate({ + mutation: confirmContribution, + variables: { + id: contribution.data.createContribution.id, + }, + }) + + await expect( + mutate({ + mutation: denyContribution, + variables: { + id: contribution.data.createContribution.id, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Contribution not found for given id.')], + }), + ) + }) + + it('logs the error found', () => { + expect(logger.error).toBeCalledWith( + `Contribution not found for given id: ${contribution.data.createContribution.id}`, + ) + }) + }) + + describe('deny contribution that is already deleted', () => { + let contribution: any + + it('throws an error', async () => { + await mutate({ + mutation: login, + variables: { email: 'raeuber@hotzenplotz.de', password: 'Aa12345_' }, + }) + + contribution = await mutate({ + mutation: createContribution, + variables: { + amount: 166.0, + memo: 'Whatever contribution', + creationDate: new Date().toString(), + }, + }) + + await mutate({ + mutation: deleteContribution, + variables: { + id: contribution.data.createContribution.id, + }, + }) + + await mutate({ + mutation: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + + await expect( + mutate({ + mutation: denyContribution, + variables: { + id: contribution.data.createContribution.id, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Contribution not found for given id.')], + }), + ) + }) + + it('logs the error found', () => { + expect(logger.error).toBeCalledWith( + `Contribution not found for given id: ${contribution.data.createContribution.id}`, + ) + }) + }) + + describe('deny contribution that is already denied', () => { + let contribution: any + + it('throws an error', async () => { + await mutate({ + mutation: login, + variables: { email: 'raeuber@hotzenplotz.de', password: 'Aa12345_' }, + }) + + contribution = await mutate({ + mutation: createContribution, + variables: { + amount: 166.0, + memo: 'Whatever contribution', + creationDate: new Date().toString(), + }, + }) + + await mutate({ + mutation: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + + await mutate({ + mutation: denyContribution, + variables: { + id: contribution.data.createContribution.id, + }, + }) + + await expect( + mutate({ + mutation: denyContribution, + variables: { + id: contribution.data.createContribution.id, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Contribution not found for given id.')], + }), + ) + }) + + it('logs the error found', () => { + expect(logger.error).toBeCalledWith( + `Contribution not found for given id: ${contribution.data.createContribution.id}`, + ) + }) + }) + describe('valid input', () => { it('deny contribution', async () => { await mutate({ @@ -1168,7 +1323,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ data: { listAllContributions: { - contributionCount: 5, + contributionCount: 7, contributionList: expect.arrayContaining([ expect.objectContaining({ amount: '100', @@ -1206,6 +1361,18 @@ describe('ContributionResolver', () => { memo: 'Herzlich Willkommen bei Gradido!', amount: '1000', }), + expect.objectContaining({ + id: expect.any(Number), + state: 'CONFIRMED', + memo: 'Whatever contribution', + amount: '166', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Whatever contribution', + amount: '166', + }), ]), }, }, @@ -1228,7 +1395,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ data: { listAllContributions: { - contributionCount: 5, + contributionCount: 7, contributionList: expect.arrayContaining([ expect.objectContaining({ amount: '100', @@ -1266,6 +1433,18 @@ describe('ContributionResolver', () => { memo: 'Herzlich Willkommen bei Gradido!', amount: '1000', }), + expect.objectContaining({ + id: expect.any(Number), + state: 'CONFIRMED', + memo: 'Whatever contribution', + amount: '166', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Whatever contribution', + amount: '166', + }), ]), }, }, @@ -1288,7 +1467,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ data: { listAllContributions: { - contributionCount: 5, + contributionCount: 7, contributionList: expect.arrayContaining([ expect.objectContaining({ amount: '100', @@ -1326,6 +1505,18 @@ describe('ContributionResolver', () => { memo: 'Herzlich Willkommen bei Gradido!', amount: '1000', }), + expect.objectContaining({ + id: expect.any(Number), + state: 'CONFIRMED', + memo: 'Whatever contribution', + amount: '166', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Whatever contribution', + amount: '166', + }), ]), }, }, @@ -1348,7 +1539,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ data: { listAllContributions: { - contributionCount: 2, + contributionCount: 3, contributionList: expect.arrayContaining([ expect.objectContaining({ amount: '100', @@ -1386,6 +1577,18 @@ describe('ContributionResolver', () => { memo: 'Herzlich Willkommen bei Gradido!', amount: '1000', }), + expect.objectContaining({ + id: expect.any(Number), + state: 'CONFIRMED', + memo: 'Whatever contribution', + amount: '166', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Whatever contribution', + amount: '166', + }), ]), }, }, @@ -1446,6 +1649,18 @@ describe('ContributionResolver', () => { memo: 'Herzlich Willkommen bei Gradido!', amount: '1000', }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'CONFIRMED', + memo: 'Whatever contribution', + amount: '166', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Whatever contribution', + amount: '166', + }), ]), }, }, @@ -1506,6 +1721,18 @@ describe('ContributionResolver', () => { memo: 'Herzlich Willkommen bei Gradido!', amount: '1000', }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'CONFIRMED', + memo: 'Whatever contribution', + amount: '166', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Whatever contribution', + amount: '166', + }), ]), }, }, @@ -1528,7 +1755,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ data: { listAllContributions: { - contributionCount: 1, + contributionCount: 2, contributionList: expect.arrayContaining([ expect.not.objectContaining({ amount: '100', @@ -1566,6 +1793,18 @@ describe('ContributionResolver', () => { memo: 'Herzlich Willkommen bei Gradido!', amount: '1000', }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'CONFIRMED', + memo: 'Whatever contribution', + amount: '166', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Whatever contribution', + amount: '166', + }), ]), }, }, @@ -1611,7 +1850,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ data: { listAllContributions: { - contributionCount: 3, + contributionCount: 4, contributionList: expect.arrayContaining([ expect.objectContaining({ amount: '100', @@ -1649,6 +1888,18 @@ describe('ContributionResolver', () => { memo: 'Herzlich Willkommen bei Gradido!', amount: '1000', }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Whatever contribution', + amount: '166', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'CONFIRMED', + memo: 'Whatever contribution', + amount: '166', + }), ]), }, }, From 46e8a55d2e69cec40c2ff1e6257506b60b26dcd7 Mon Sep 17 00:00:00 2001 From: elweyn Date: Mon, 6 Feb 2023 09:48:20 +0100 Subject: [PATCH 07/70] remove unused variable raeuber. --- backend/src/graphql/resolver/ContributionResolver.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index eaf157edc..7c239e699 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -95,13 +95,12 @@ afterAll(async () => { describe('ContributionResolver', () => { let bibi: any - let raueber: any let peter: any beforeAll(async () => { bibi = await userFactory(testEnv, bibiBloxberg) admin = peter = await userFactory(testEnv, peterLustig) - raueber = await userFactory(testEnv, raeuberHotzenplotz) + await userFactory(testEnv, raeuberHotzenplotz) const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de') // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await creationFactory(testEnv, bibisCreation!) From 8c00fcd6ccb8069a7df73053164226b91ad868df Mon Sep 17 00:00:00 2001 From: elweyn Date: Mon, 6 Feb 2023 14:34:40 +0100 Subject: [PATCH 08/70] Merge new unit tests for ContributionResolver. --- admin/package.json | 5 +- backend/package.json | 3 + .../resolver/ContributionResolver.test.ts | 175 +++++++++--------- .../graphql/resolver/ContributionResolver.ts | 59 +++--- backend/src/seeds/graphql/queries.ts | 53 ++++-- database/package.json | 3 + frontend/package.json | 5 +- 7 files changed, 167 insertions(+), 136 deletions(-) diff --git a/admin/package.json b/admin/package.json index 8270c4da6..30e93239b 100644 --- a/admin/package.json +++ b/admin/package.json @@ -86,5 +86,8 @@ "> 1%", "last 2 versions", "not ie <= 10" - ] + ], + "nodemonConfig": { + "ignore": ["**/*.spec.js"] + } } diff --git a/backend/package.json b/backend/package.json index bfcd61d5b..9a36c2ff8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -72,5 +72,8 @@ "ts-node": "^10.0.0", "tsconfig-paths": "^3.14.0", "typescript": "^4.3.4" + }, + "nodemonConfig": { + "ignore": ["**/*.test.ts"] } } diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 7c239e699..c64837285 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -23,7 +23,7 @@ import { import { listAllContributions, listContributions, - listUnconfirmedContributions, + adminListAllContributions, } from '@/seeds/graphql/queries' import { // sendAccountActivationEmail, @@ -1963,11 +1963,11 @@ describe('ContributionResolver', () => { }) }) - describe('listUnconfirmedContributions', () => { + describe('adminListAllContributions', () => { it('returns an error', async () => { await expect( query({ - query: listUnconfirmedContributions, + query: adminListAllContributions, }), ).resolves.toEqual( expect.objectContaining({ @@ -2071,11 +2071,11 @@ describe('ContributionResolver', () => { }) }) - describe('listUnconfirmedContributions', () => { + describe('adminListAllContributions', () => { it('returns an error', async () => { await expect( query({ - query: listUnconfirmedContributions, + query: adminListAllContributions, }), ).resolves.toEqual( expect.objectContaining({ @@ -2665,94 +2665,97 @@ describe('ContributionResolver', () => { }) }) - describe('listUnconfirmedContributions', () => { + describe('adminListAllContributions', () => { it('returns four pending creations', async () => { await expect( query({ - query: listUnconfirmedContributions, + query: adminListAllContributions, }), ).resolves.toEqual( expect.objectContaining({ data: { - listUnconfirmedContributions: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(Number), - firstName: 'Peter', - lastName: 'Lustig', - email: 'peter@lustig.de', - date: expect.any(String), - memo: 'Das war leider zu Viel!', - amount: '200', - moderator: admin.id, - creation: ['1000', '800', '500'], - }), - expect.objectContaining({ - id: expect.any(Number), - firstName: 'Peter', - lastName: 'Lustig', - email: 'peter@lustig.de', - date: expect.any(String), - memo: 'Grundeinkommen', - amount: '500', - moderator: admin.id, - creation: ['1000', '800', '500'], - }), - expect.not.objectContaining({ - id: expect.any(Number), - firstName: 'Bibi', - lastName: 'Bloxberg', - email: 'bibi@bloxberg.de', - date: expect.any(String), - memo: 'Test contribution to delete', - amount: '100', - moderator: null, - creation: ['1000', '1000', '90'], - }), - expect.objectContaining({ - id: expect.any(Number), - firstName: 'Bibi', - lastName: 'Bloxberg', - email: 'bibi@bloxberg.de', - date: expect.any(String), - memo: 'Test PENDING contribution update', - amount: '10', - moderator: null, - creation: ['1000', '1000', '90'], - }), - expect.objectContaining({ - id: expect.any(Number), - firstName: 'Bibi', - lastName: 'Bloxberg', - email: 'bibi@bloxberg.de', - date: expect.any(String), - memo: 'Test IN_PROGRESS contribution', - amount: '100', - moderator: null, - creation: ['1000', '1000', '90'], - }), - expect.objectContaining({ - id: expect.any(Number), - firstName: 'Bibi', - lastName: 'Bloxberg', - email: 'bibi@bloxberg.de', - date: expect.any(String), - memo: 'Grundeinkommen', - amount: '500', - moderator: admin.id, - creation: ['1000', '1000', '90'], - }), - expect.objectContaining({ - id: expect.any(Number), - firstName: 'Bibi', - lastName: 'Bloxberg', - email: 'bibi@bloxberg.de', - date: expect.any(String), - memo: 'Aktives Grundeinkommen', - amount: '200', - moderator: admin.id, - creation: ['1000', '1000', '90'], - }), - ]), + adminListAllContributions: { + contributionCount: 4, + contributionList: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', + date: expect.any(String), + memo: 'Das war leider zu Viel!', + amount: '200', + moderator: admin.id, + creation: ['1000', '800', '500'], + }), + expect.objectContaining({ + id: expect.any(Number), + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', + date: expect.any(String), + memo: 'Grundeinkommen', + amount: '500', + moderator: admin.id, + creation: ['1000', '800', '500'], + }), + expect.not.objectContaining({ + id: expect.any(Number), + firstName: 'Bibi', + lastName: 'Bloxberg', + email: 'bibi@bloxberg.de', + date: expect.any(String), + memo: 'Test contribution to delete', + amount: '100', + moderator: null, + creation: ['1000', '1000', '90'], + }), + expect.objectContaining({ + id: expect.any(Number), + firstName: 'Bibi', + lastName: 'Bloxberg', + email: 'bibi@bloxberg.de', + date: expect.any(String), + memo: 'Test PENDING contribution update', + amount: '10', + moderator: null, + creation: ['1000', '1000', '90'], + }), + expect.objectContaining({ + id: expect.any(Number), + firstName: 'Bibi', + lastName: 'Bloxberg', + email: 'bibi@bloxberg.de', + date: expect.any(String), + memo: 'Test IN_PROGRESS contribution', + amount: '100', + moderator: null, + creation: ['1000', '1000', '90'], + }), + expect.objectContaining({ + id: expect.any(Number), + firstName: 'Bibi', + lastName: 'Bloxberg', + email: 'bibi@bloxberg.de', + date: expect.any(String), + memo: 'Grundeinkommen', + amount: '500', + moderator: admin.id, + creation: ['1000', '1000', '90'], + }), + expect.objectContaining({ + id: expect.any(Number), + firstName: 'Bibi', + lastName: 'Bloxberg', + email: 'bibi@bloxberg.de', + date: expect.any(String), + memo: 'Aktives Grundeinkommen', + amount: '200', + moderator: admin.id, + creation: ['1000', '1000', '90'], + }), + ]), + }, }, }), ) diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index c7946d2c8..6016a2c23 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -477,40 +477,39 @@ export class ContributionResolver { } @Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS]) - @Query(() => [UnconfirmedContribution]) - async listUnconfirmedContributions(@Ctx() context: Context): Promise { - const clientTimezoneOffset = getClientTimezoneOffset(context) - const contributions = await getConnection() + @Query(() => ContributionListResult) // [UnconfirmedContribution] + async adminListAllContributions( + @Args() + { currentPage = 1, pageSize = 3, order = Order.DESC }: Paginated, + @Arg('statusFilter', () => [ContributionStatus], { nullable: true }) + statusFilter?: ContributionStatus[], + ): Promise { + const where: { + contributionStatus?: FindOperator | null + } = {} + + if (statusFilter && statusFilter.length) { + where.contributionStatus = In(statusFilter) + } + + const [dbContributions, count] = await getConnection() .createQueryBuilder() .select('c') .from(DbContribution, 'c') - .leftJoinAndSelect('c.messages', 'm') - .where({ confirmedAt: IsNull() }) - .andWhere({ deniedAt: IsNull() }) - .getMany() + .innerJoinAndSelect('c.user', 'u') + .where(where) + .withDeleted() + .orderBy('c.createdAt', order) + .limit(pageSize) + .offset((currentPage - 1) * pageSize) + .getManyAndCount() - if (contributions.length === 0) { - return [] - } - - const userIds = contributions.map((p) => p.userId) - const userCreations = await getUserCreations(userIds, clientTimezoneOffset) - const users = await DbUser.find({ - where: { id: In(userIds) }, - withDeleted: true, - relations: ['emailContact'], - }) - - return contributions.map((contribution) => { - const user = users.find((u) => u.id === contribution.userId) - const creation = userCreations.find((c) => c.id === contribution.userId) - - return new UnconfirmedContribution( - contribution, - user, - creation ? creation.creations : FULL_CREATION_AVAILABLE, - ) - }) + console.log('dbContributions', dbContributions) + console.log('count', count) + return new ContributionListResult( + count, + dbContributions.map((contribution) => new Contribution(contribution, contribution.user)), + ) } @Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION]) diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 385a69479..423575a16 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -177,6 +177,40 @@ query ($currentPage: Int = 1, $pageSize: Int = 5, $order: Order = DESC, $statusF contributionCount contributionList { id + firstName + lastName + amount + memo + createdAt + confirmedAt + confirmedBy + contributionDate + state + messagesCount + deniedAt + deniedBy + } + } +} +` +// from admin interface + +export const adminListAllContributions = gql` + query ( + $currentPage: Int = 1 + $pageSize: Int = 3 + $order: Order = DESC + $statusFilter: [ContributionStatus!] + ) { + adminListAllContributions( + currentPage: $currentPage + pageSize: $pageSize + order: $order + statusFilter: $statusFilter + ) { + contributionCount + contributionList { + id firstName lastName amount @@ -189,24 +223,7 @@ query ($currentPage: Int = 1, $pageSize: Int = 5, $order: Order = DESC, $statusF messagesCount deniedAt deniedBy - } - } -} -` -// from admin interface - -export const listUnconfirmedContributions = gql` - query { - listUnconfirmedContributions { - id - firstName - lastName - email - amount - memo - date - moderator - creation + } } } ` diff --git a/database/package.json b/database/package.json index f4e1c7e84..dc805da93 100644 --- a/database/package.json +++ b/database/package.json @@ -47,5 +47,8 @@ "ts-mysql-migrate": "^1.0.2", "typeorm": "^0.2.38", "uuid": "^8.3.2" + }, + "nodemonConfig": { + "ignore": ["**/*.test.ts"] } } diff --git a/frontend/package.json b/frontend/package.json index 29c440988..73651327f 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -104,5 +104,8 @@ ], "author": "Gradido-Akademie - https://www.gradido.net/", "license": "Apache-2.0", - "description": "Gradido, the Natural Economy of Life, is a way to worldwide prosperity and peace in harmony with nature. - Gradido, die Natürliche Ökonomie des lebens, ist ein Weg zu weltweitem Wohlstand und Frieden in Harmonie mit der Natur." + "description": "Gradido, the Natural Economy of Life, is a way to worldwide prosperity and peace in harmony with nature. - Gradido, die Natürliche Ökonomie des lebens, ist ein Weg zu weltweitem Wohlstand und Frieden in Harmonie mit der Natur.", + "nodemonConfig": { + "ignore": ["**/*.spec.js"] + } } From ad16aff1eaedc6ea53fef3864fee5a1afa9667a9 Mon Sep 17 00:00:00 2001 From: elweyn Date: Mon, 6 Feb 2023 15:06:58 +0100 Subject: [PATCH 09/70] Change listUnconfirmedContributions to adminListAllContributions. --- .../resolver/ContributionResolver.test.ts | 271 +++++++++++++----- .../graphql/resolver/ContributionResolver.ts | 5 +- backend/src/seeds/graphql/queries.ts | 2 +- 3 files changed, 204 insertions(+), 74 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index c64837285..5ab4e531d 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -2675,84 +2675,217 @@ describe('ContributionResolver', () => { expect.objectContaining({ data: { adminListAllContributions: { - contributionCount: 4, + contributionCount: 14, contributionList: expect.arrayContaining([ expect.objectContaining({ + amount: '500', + confirmedAt: null, + confirmedBy: null, + contributionDate: expect.any(String), + createdAt: expect.any(String), + deniedAt: null, + deniedBy: null, + firstName: 'Bibi', id: expect.any(Number), - firstName: 'Peter', - lastName: 'Lustig', - email: 'peter@lustig.de', - date: expect.any(String), - memo: 'Das war leider zu Viel!', + lastName: 'Bloxberg', + memo: 'Grundeinkommen', + messagesCount: 0, + state: 'PENDING', + }), + expect.objectContaining({ amount: '200', - moderator: admin.id, - creation: ['1000', '800', '500'], - }), - expect.objectContaining({ - id: expect.any(Number), - firstName: 'Peter', - lastName: 'Lustig', - email: 'peter@lustig.de', - date: expect.any(String), - memo: 'Grundeinkommen', - amount: '500', - moderator: admin.id, - creation: ['1000', '800', '500'], - }), - expect.not.objectContaining({ - id: expect.any(Number), + confirmedAt: null, + confirmedBy: null, + contributionDate: expect.any(String), + createdAt: expect.any(String), + deniedAt: null, + deniedBy: null, firstName: 'Bibi', - lastName: 'Bloxberg', - email: 'bibi@bloxberg.de', - date: expect.any(String), - memo: 'Test contribution to delete', - amount: '100', - moderator: null, - creation: ['1000', '1000', '90'], - }), - expect.objectContaining({ id: expect.any(Number), - firstName: 'Bibi', lastName: 'Bloxberg', - email: 'bibi@bloxberg.de', - date: expect.any(String), - memo: 'Test PENDING contribution update', - amount: '10', - moderator: null, - creation: ['1000', '1000', '90'], - }), - expect.objectContaining({ - id: expect.any(Number), - firstName: 'Bibi', - lastName: 'Bloxberg', - email: 'bibi@bloxberg.de', - date: expect.any(String), - memo: 'Test IN_PROGRESS contribution', - amount: '100', - moderator: null, - creation: ['1000', '1000', '90'], - }), - expect.objectContaining({ - id: expect.any(Number), - firstName: 'Bibi', - lastName: 'Bloxberg', - email: 'bibi@bloxberg.de', - date: expect.any(String), - memo: 'Grundeinkommen', - amount: '500', - moderator: admin.id, - creation: ['1000', '1000', '90'], - }), - expect.objectContaining({ - id: expect.any(Number), - firstName: 'Bibi', - lastName: 'Bloxberg', - email: 'bibi@bloxberg.de', - date: expect.any(String), memo: 'Aktives Grundeinkommen', + messagesCount: 0, + state: 'PENDING', + }), + expect.objectContaining({ + amount: '500', + confirmedAt: null, + confirmedBy: null, + contributionDate: expect.any(String), + createdAt: expect.any(String), + deniedAt: null, + deniedBy: null, + firstName: 'Peter', + id: expect.any(Number), + lastName: 'Lustig', + memo: 'Grundeinkommen', + messagesCount: 0, + state: 'PENDING', + }), + expect.objectContaining({ amount: '200', - moderator: admin.id, - creation: ['1000', '1000', '90'], + confirmedAt: null, + confirmedBy: null, + contributionDate: expect.any(String), + createdAt: expect.any(String), + deniedAt: null, + deniedBy: null, + firstName: 'Peter', + id: expect.any(Number), + lastName: 'Lustig', + memo: 'Das war leider zu Viel!', + messagesCount: 0, + state: 'PENDING', + }), + expect.objectContaining({ + amount: '166', + confirmedAt: null, + confirmedBy: null, + contributionDate: expect.any(String), + createdAt: expect.any(String), + deniedAt: null, + deniedBy: null, + firstName: 'Peter', + id: expect.any(Number), + lastName: 'Lustig', + memo: 'Whatever contribution', + messagesCount: 0, + state: 'DELETED', + }), + expect.objectContaining({ + amount: '166', + confirmedAt: null, + confirmedBy: null, + contributionDate: expect.any(String), + createdAt: expect.any(String), + deniedAt: expect.any(String), + deniedBy: expect.any(Number), + firstName: 'Räuber', + id: expect.any(Number), + lastName: 'Hotzenplotz', + memo: 'Whatever contribution', + messagesCount: 0, + state: 'DENIED', + }), + expect.objectContaining({ + amount: '166', + confirmedAt: null, + confirmedBy: null, + contributionDate: expect.any(String), + createdAt: expect.any(String), + deniedAt: null, + deniedBy: null, + firstName: 'Räuber', + id: expect.any(Number), + lastName: 'Hotzenplotz', + memo: 'Whatever contribution', + messagesCount: 0, + state: 'DELETED', + }), + expect.objectContaining({ + amount: '166', + confirmedAt: expect.any(String), + confirmedBy: expect.any(Number), + contributionDate: expect.any(String), + createdAt: expect.any(String), + deniedAt: null, + deniedBy: null, + firstName: 'Räuber', + id: expect.any(Number), + lastName: 'Hotzenplotz', + memo: 'Whatever contribution', + messagesCount: 0, + state: 'CONFIRMED', + }), + expect.objectContaining({ + amount: '100', + confirmedAt: null, + confirmedBy: null, + contributionDate: expect.any(String), + createdAt: expect.any(String), + deniedAt: expect.any(String), + deniedBy: expect.any(Number), + firstName: 'Bibi', + id: expect.any(Number), + lastName: 'Bloxberg', + memo: 'Test contribution to deny', + messagesCount: 0, + state: 'DENIED', + }), + expect.objectContaining({ + amount: '100', + confirmedAt: expect.any(String), + confirmedBy: expect.any(Number), + contributionDate: expect.any(String), + createdAt: expect.any(String), + deniedAt: null, + deniedBy: null, + firstName: 'Bibi', + id: expect.any(Number), + lastName: 'Bloxberg', + memo: 'Test contribution to confirm', + messagesCount: 0, + state: 'CONFIRMED', + }), + expect.objectContaining({ + amount: '100', + confirmedAt: null, + confirmedBy: null, + contributionDate: expect.any(String), + createdAt: expect.any(String), + deniedAt: null, + deniedBy: null, + firstName: 'Bibi', + id: expect.any(Number), + lastName: 'Bloxberg', + memo: 'Test IN_PROGRESS contribution', + messagesCount: 0, + state: 'IN_PROGRESS', + }), + expect.objectContaining({ + amount: '10', + confirmedAt: null, + confirmedBy: null, + contributionDate: expect.any(String), + createdAt: expect.any(String), + deniedAt: null, + deniedBy: null, + firstName: 'Bibi', + id: expect.any(Number), + lastName: 'Bloxberg', + memo: 'Test PENDING contribution update', + messagesCount: 0, + state: 'PENDING', + }), + expect.objectContaining({ + amount: '100', + confirmedAt: null, + confirmedBy: null, + contributionDate: expect.any(String), + createdAt: expect.any(String), + deniedAt: null, + deniedBy: null, + firstName: 'Bibi', + id: expect.any(Number), + lastName: 'Bloxberg', + memo: 'Test contribution to delete', + messagesCount: 0, + state: 'DELETED', + }), + expect.objectContaining({ + amount: '1000', + confirmedAt: expect.any(String), + confirmedBy: expect.any(Number), + contributionDate: expect.any(String), + createdAt: expect.any(String), + deniedAt: null, + deniedBy: null, + firstName: 'Bibi', + id: expect.any(Number), + lastName: 'Bloxberg', + memo: 'Herzlich Willkommen bei Gradido!', + messagesCount: 0, + state: 'CONFIRMED', }), ]), }, diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 6016a2c23..89973906a 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -30,12 +30,11 @@ import { backendLogger as logger } from '@/server/logger' import { getCreationDates, getUserCreation, - getUserCreations, validateContribution, updateCreations, isValidDateString, } from './util/creations' -import { MEMO_MAX_CHARS, MEMO_MIN_CHARS, FULL_CREATION_AVAILABLE } from './const/const' +import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { Event, EventContributionCreate, @@ -504,8 +503,6 @@ export class ContributionResolver { .offset((currentPage - 1) * pageSize) .getManyAndCount() - console.log('dbContributions', dbContributions) - console.log('count', count) return new ContributionListResult( count, dbContributions.map((contribution) => new Contribution(contribution, contribution.user)), diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 423575a16..400d41490 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -198,7 +198,7 @@ query ($currentPage: Int = 1, $pageSize: Int = 5, $order: Order = DESC, $statusF export const adminListAllContributions = gql` query ( $currentPage: Int = 1 - $pageSize: Int = 3 + $pageSize: Int = 25 $order: Order = DESC $statusFilter: [ContributionStatus!] ) { From 6a0ccb75b85888df2ff1a044024a9863c998f561 Mon Sep 17 00:00:00 2001 From: elweyn Date: Mon, 6 Feb 2023 15:32:12 +0100 Subject: [PATCH 10/70] Remove the ignore on nodemon watch of test files. --- admin/package.json | 5 +---- backend/package.json | 3 --- database/package.json | 3 --- frontend/package.json | 5 +---- 4 files changed, 2 insertions(+), 14 deletions(-) diff --git a/admin/package.json b/admin/package.json index 30e93239b..8270c4da6 100644 --- a/admin/package.json +++ b/admin/package.json @@ -86,8 +86,5 @@ "> 1%", "last 2 versions", "not ie <= 10" - ], - "nodemonConfig": { - "ignore": ["**/*.spec.js"] - } + ] } diff --git a/backend/package.json b/backend/package.json index 9a36c2ff8..bfcd61d5b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -72,8 +72,5 @@ "ts-node": "^10.0.0", "tsconfig-paths": "^3.14.0", "typescript": "^4.3.4" - }, - "nodemonConfig": { - "ignore": ["**/*.test.ts"] } } diff --git a/database/package.json b/database/package.json index dc805da93..f4e1c7e84 100644 --- a/database/package.json +++ b/database/package.json @@ -47,8 +47,5 @@ "ts-mysql-migrate": "^1.0.2", "typeorm": "^0.2.38", "uuid": "^8.3.2" - }, - "nodemonConfig": { - "ignore": ["**/*.test.ts"] } } diff --git a/frontend/package.json b/frontend/package.json index 73651327f..29c440988 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -104,8 +104,5 @@ ], "author": "Gradido-Akademie - https://www.gradido.net/", "license": "Apache-2.0", - "description": "Gradido, the Natural Economy of Life, is a way to worldwide prosperity and peace in harmony with nature. - Gradido, die Natürliche Ökonomie des lebens, ist ein Weg zu weltweitem Wohlstand und Frieden in Harmonie mit der Natur.", - "nodemonConfig": { - "ignore": ["**/*.spec.js"] - } + "description": "Gradido, the Natural Economy of Life, is a way to worldwide prosperity and peace in harmony with nature. - Gradido, die Natürliche Ökonomie des lebens, ist ein Weg zu weltweitem Wohlstand und Frieden in Harmonie mit der Natur." } From 8eaacc669931be7dcf2debd76a16607da64b5bf2 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Tue, 7 Feb 2023 12:50:29 +0100 Subject: [PATCH 11/70] implement LogError on creations and fix all corressponding tests --- .../resolver/ContributionLinkResolver.test.ts | 18 ++++------ .../resolver/ContributionResolver.test.ts | 35 ++++++++++--------- .../resolver/TransactionLinkResolver.test.ts | 7 +++- .../src/graphql/resolver/util/creations.ts | 25 +++++-------- 4 files changed, 40 insertions(+), 45 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionLinkResolver.test.ts b/backend/src/graphql/resolver/ContributionLinkResolver.test.ts index 46296e009..2a17f0556 100644 --- a/backend/src/graphql/resolver/ContributionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionLinkResolver.test.ts @@ -257,17 +257,13 @@ describe('Contribution Links', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [ - new GraphQLError('Start-Date is not initialized. A Start-Date must be set!'), - ], + errors: [new GraphQLError('A Start-Date must be set')], }), ) }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( - 'Start-Date is not initialized. A Start-Date must be set!', - ) + expect(logger.error).toBeCalledWith('A Start-Date must be set') }) it('returns an error if missing endDate', async () => { @@ -282,15 +278,13 @@ describe('Contribution Links', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('End-Date is not initialized. An End-Date must be set!')], + errors: [new GraphQLError('An End-Date must be set')], }), ) }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( - 'End-Date is not initialized. An End-Date must be set!', - ) + expect(logger.error).toBeCalledWith('An End-Date must be set') }) it('returns an error if endDate is before startDate', async () => { @@ -307,7 +301,7 @@ describe('Contribution Links', () => { ).resolves.toEqual( expect.objectContaining({ errors: [ - new GraphQLError(`The value of validFrom must before or equals the validTo!`), + new GraphQLError(`The value of validFrom must before or equals the validTo`), ], }), ) @@ -315,7 +309,7 @@ describe('Contribution Links', () => { it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - `The value of validFrom must before or equals the validTo!`, + `The value of validFrom must before or equals the validTo`, ) }) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 1e0930d91..7f0231860 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -187,8 +187,8 @@ describe('ContributionResolver', () => { it('logs the error found', () => { expect(logger.error).toBeCalledWith( - 'No information for available creations with the given creationDate=', - 'Invalid Date', + 'No information for available creations for the given date', + expect.any(Date), ) }) @@ -215,8 +215,8 @@ describe('ContributionResolver', () => { it('logs the error found', () => { expect(logger.error).toBeCalledWith( - 'No information for available creations with the given creationDate=', - 'Invalid Date', + 'No information for available creations for the given date', + expect.any(Date), ) }) }) @@ -637,7 +637,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ errors: [ new GraphQLError( - 'The amount (1019 GDD) to be created exceeds the amount (1000 GDD) still available for this month.', + 'The amount to be created exceeds the amount still available for this month', ), ], }), @@ -646,7 +646,9 @@ describe('ContributionResolver', () => { it('logs the error found', () => { expect(logger.error).toBeCalledWith( - 'The amount (1019 GDD) to be created exceeds the amount (1000 GDD) still available for this month.', + 'The amount to be created exceeds the amount still available for this month', + new Decimal(1019), + new Decimal(1000), ) }) }) @@ -1717,8 +1719,8 @@ describe('ContributionResolver', () => { it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - 'No information for available creations with the given creationDate=', - new Date(variables.creationDate).toString(), + 'No information for available creations for the given date', + new Date(variables.creationDate), ) }) }) @@ -1742,8 +1744,8 @@ describe('ContributionResolver', () => { it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - 'No information for available creations with the given creationDate=', - new Date(variables.creationDate).toString(), + 'No information for available creations for the given date', + new Date(variables.creationDate), ) }) }) @@ -1758,7 +1760,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ errors: [ new GraphQLError( - 'The amount (2000 GDD) to be created exceeds the amount (1000 GDD) still available for this month.', + 'The amount to be created exceeds the amount still available for this month', ), ], }), @@ -1767,7 +1769,7 @@ describe('ContributionResolver', () => { it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - 'The amount (2000 GDD) to be created exceeds the amount (1000 GDD) still available for this month.', + 'The amount to be created exceeds the amount still available for this month', new Decimal(2000), new Decimal(1000) ) }) }) @@ -1798,6 +1800,7 @@ describe('ContributionResolver', () => { describe('second creation surpasses the available amount ', () => { it('returns an array of the open creations for the last three months', async () => { + jest.clearAllMocks() variables.amount = new Decimal(1000) await expect( mutate({ mutation: adminCreateContribution, variables }), @@ -1805,7 +1808,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ errors: [ new GraphQLError( - 'The amount (1000 GDD) to be created exceeds the amount (800 GDD) still available for this month.', + 'The amount to be created exceeds the amount still available for this month', ), ], }), @@ -1814,7 +1817,7 @@ describe('ContributionResolver', () => { it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - 'The amount (1000 GDD) to be created exceeds the amount (800 GDD) still available for this month.', + 'The amount to be created exceeds the amount still available for this month', new Decimal(1000), new Decimal(800) ) }) }) @@ -2007,7 +2010,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ errors: [ new GraphQLError( - 'The amount (1900 GDD) to be created exceeds the amount (1000 GDD) still available for this month.', + 'The amount to be created exceeds the amount still available for this month', ), ], }), @@ -2016,7 +2019,7 @@ describe('ContributionResolver', () => { it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - 'The amount (1900 GDD) to be created exceeds the amount (1000 GDD) still available for this month.', + 'The amount to be created exceeds the amount still available for this month', new Decimal(1900), new Decimal(1000) ) }) }) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts index af2e4fd59..0666efc8e 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts @@ -354,10 +354,15 @@ describe('TransactionLinkResolver', () => { }) it('logs the error thrown', () => { + /* expect(logger.error).toBeCalledWith( + 'The amount to be created exceeds the amount still available for this month', + new Decimal(5), + new Decimal(0), + ) */ expect(logger.error).toBeCalledWith( 'Creation from contribution link was not successful', new Error( - 'The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.', + 'The amount to be created exceeds the amount still available for this month', ), ) }) diff --git a/backend/src/graphql/resolver/util/creations.ts b/backend/src/graphql/resolver/util/creations.ts index 00137eaa1..6a47915b1 100644 --- a/backend/src/graphql/resolver/util/creations.ts +++ b/backend/src/graphql/resolver/util/creations.ts @@ -1,3 +1,4 @@ +import LogError from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' import { getConnection } from '@dbTools/typeorm' import { Contribution } from '@entity/Contribution' @@ -19,19 +20,14 @@ export const validateContribution = ( const index = getCreationIndex(creationDate.getMonth(), timezoneOffset) if (index < 0) { - logger.error( - 'No information for available creations with the given creationDate=', - creationDate.toString(), - ) - throw new Error('No information for available creations for the given date') + throw new LogError('No information for available creations for the given date', creationDate) } if (amount.greaterThan(creations[index].toString())) { - logger.error( - `The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`, - ) - throw new Error( - `The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`, + throw new LogError( + 'The amount to be created exceeds the amount still available for this month', + amount, + creations[index], ) } } @@ -126,19 +122,16 @@ export const isStartEndDateValid = ( endDate: string | null | undefined, ): void => { if (!startDate) { - logger.error('Start-Date is not initialized. A Start-Date must be set!') - throw new Error('Start-Date is not initialized. A Start-Date must be set!') + throw new LogError('A Start-Date must be set') } if (!endDate) { - logger.error('End-Date is not initialized. An End-Date must be set!') - throw new Error('End-Date is not initialized. An End-Date must be set!') + throw new LogError('An End-Date must be set') } // check if endDate is before startDate if (new Date(endDate).getTime() - new Date(startDate).getTime() < 0) { - logger.error(`The value of validFrom must before or equals the validTo!`) - throw new Error(`The value of validFrom must before or equals the validTo!`) + throw new LogError(`The value of validFrom must before or equals the validTo`) } } From ea1f637bb29d9f32c680a477d4268c180bb3e037 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Tue, 7 Feb 2023 16:48:08 +0100 Subject: [PATCH 12/70] linting --- .../graphql/resolver/ContributionResolver.test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 7f0231860..93d7d36d0 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -1769,7 +1769,9 @@ describe('ContributionResolver', () => { it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - 'The amount to be created exceeds the amount still available for this month', new Decimal(2000), new Decimal(1000) + 'The amount to be created exceeds the amount still available for this month', + new Decimal(2000), + new Decimal(1000), ) }) }) @@ -1817,7 +1819,9 @@ describe('ContributionResolver', () => { it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - 'The amount to be created exceeds the amount still available for this month', new Decimal(1000), new Decimal(800) + 'The amount to be created exceeds the amount still available for this month', + new Decimal(1000), + new Decimal(800), ) }) }) @@ -2019,7 +2023,9 @@ describe('ContributionResolver', () => { it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - 'The amount to be created exceeds the amount still available for this month', new Decimal(1900), new Decimal(1000) + 'The amount to be created exceeds the amount still available for this month', + new Decimal(1900), + new Decimal(1000), ) }) }) From d8c8d400d4001ce1193922514b0525b456f4f3f5 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Tue, 7 Feb 2023 16:55:04 +0100 Subject: [PATCH 13/70] implement LogError in EncryptorUtils --- backend/src/password/EncryptorUtils.ts | 27 ++++++++++---------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/backend/src/password/EncryptorUtils.ts b/backend/src/password/EncryptorUtils.ts index 971b6a32e..d03f5d169 100644 --- a/backend/src/password/EncryptorUtils.ts +++ b/backend/src/password/EncryptorUtils.ts @@ -1,4 +1,5 @@ import CONFIG from '@/config' +import LogError from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' import { User } from '@entity/User' import { PasswordEncryptionType } from '@enum/PasswordEncryptionType' @@ -16,11 +17,10 @@ export const SecretKeyCryptographyCreateKey = (salt: string, password: string): const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex') const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex') if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) { - logger.error( - `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`, - ) - throw new Error( - `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`, + throw new LogError( + 'ServerKey has an invalid size', + configLoginServerKey.length, + sodium.crypto_shorthash_KEYBYTES, ) } @@ -52,20 +52,13 @@ export const SecretKeyCryptographyCreateKey = (salt: string, password: string): export const getUserCryptographicSalt = (dbUser: User): string => { switch (dbUser.passwordEncryptionType) { - case PasswordEncryptionType.NO_PASSWORD: { - logger.error('Password not set for user ' + dbUser.id) - throw new Error('Password not set for user ' + dbUser.id) // user has no password - } - case PasswordEncryptionType.EMAIL: { + case PasswordEncryptionType.NO_PASSWORD: + throw new LogError('User has no password set', dbUser.id) + case PasswordEncryptionType.EMAIL: return dbUser.emailContact.email - break - } - case PasswordEncryptionType.GRADIDO_ID: { + case PasswordEncryptionType.GRADIDO_ID: return dbUser.gradidoID - break - } default: - logger.error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`) - throw new Error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`) + throw new LogError('Unknown password encryption type', dbUser.passwordEncryptionType) } } From 3563aef346a8863f13921dce0c5a2b4c99d1aec7 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Tue, 7 Feb 2023 17:14:44 +0100 Subject: [PATCH 14/70] use LogError instead of Error where applicable --- backend/src/apis/KlicktippController.ts | 2 +- backend/src/auth/JWT.ts | 2 +- backend/src/graphql/directive/isAuthorized.ts | 9 +++---- .../resolver/ContributionMessageResolver.ts | 8 +++++-- backend/src/graphql/resolver/GdtResolver.ts | 9 +++---- .../resolver/TransactionLinkResolver.ts | 24 +++++++++++-------- .../src/graphql/resolver/util/creations.ts | 2 +- backend/src/server/context.ts | 5 ++-- backend/src/util/decay.ts | 2 +- backend/src/util/klicktipp.ts | 2 +- 10 files changed, 38 insertions(+), 27 deletions(-) diff --git a/backend/src/apis/KlicktippController.ts b/backend/src/apis/KlicktippController.ts index 824d40af2..ca64f4b2e 100644 --- a/backend/src/apis/KlicktippController.ts +++ b/backend/src/apis/KlicktippController.ts @@ -31,7 +31,7 @@ export const unsubscribe = async (email: string): Promise => { if (isLogin) { return await klicktippConnector.unsubscribe(email) } - throw new Error(`Could not unsubscribe ${email}`) + throw new LogError('Could not unsubscribe', email) } export const getKlickTippUser = async (email: string): Promise => { diff --git a/backend/src/auth/JWT.ts b/backend/src/auth/JWT.ts index 8399c881b..301d2ddad 100644 --- a/backend/src/auth/JWT.ts +++ b/backend/src/auth/JWT.ts @@ -3,7 +3,7 @@ import CONFIG from '@/config/' import { CustomJwtPayload } from './CustomJwtPayload' export const decode = (token: string): CustomJwtPayload | null => { - if (!token) throw new Error('401 Unauthorized') + if (!token) throw new LogError('401 Unauthorized') try { return jwt.verify(token, CONFIG.JWT_SECRET) } catch (err) { diff --git a/backend/src/graphql/directive/isAuthorized.ts b/backend/src/graphql/directive/isAuthorized.ts index 2843225ae..59daa89f1 100644 --- a/backend/src/graphql/directive/isAuthorized.ts +++ b/backend/src/graphql/directive/isAuthorized.ts @@ -7,6 +7,7 @@ import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN } from '@/auth/ROLES' import { RIGHTS } from '@/auth/RIGHTS' import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS' import { User } from '@entity/User' +import LogError from '@/server/LogError' const isAuthorized: AuthChecker = async ({ context }, rights) => { context.role = ROLE_UNAUTHORIZED // unauthorized user @@ -17,13 +18,13 @@ const isAuthorized: AuthChecker = async ({ context }, rights) => { // Do we have a token? if (!context.token) { - throw new Error('401 Unauthorized') + throw new LogError('401 Unauthorized') } // Decode the token const decoded = decode(context.token) if (!decoded) { - throw new Error('403.13 - Client certificate revoked') + throw new LogError('403.13 - Client certificate revoked') } // Set context gradidoID context.gradidoID = decoded.gradidoID @@ -39,13 +40,13 @@ const isAuthorized: AuthChecker = async ({ context }, rights) => { context.role = user.isAdmin ? ROLE_ADMIN : ROLE_USER } catch { // in case the database query fails (user deleted) - throw new Error('401 Unauthorized') + throw new LogError('401 Unauthorized') } // check for correct rights const missingRights = (rights).filter((right) => !context.role.hasRight(right)) if (missingRights.length !== 0) { - throw new Error('401 Unauthorized') + throw new LogError('401 Unauthorized') } // set new header token diff --git a/backend/src/graphql/resolver/ContributionMessageResolver.ts b/backend/src/graphql/resolver/ContributionMessageResolver.ts index 3e6f86e53..fe6d0dd7e 100644 --- a/backend/src/graphql/resolver/ContributionMessageResolver.ts +++ b/backend/src/graphql/resolver/ContributionMessageResolver.ts @@ -33,10 +33,14 @@ export class ContributionMessageResolver { try { const contribution = await DbContribution.findOne({ id: contributionId }) if (!contribution) { - throw new Error('Contribution not found') + throw new LogError('Contribution not found', contributionId) } if (contribution.userId !== user.id) { - throw new Error('Can not send message to contribution of another user') + throw new LogError( + 'Can not send message to contribution of another user', + contribution.userId, + user.id, + ) } contributionMessage.contributionId = contributionId diff --git a/backend/src/graphql/resolver/GdtResolver.ts b/backend/src/graphql/resolver/GdtResolver.ts index 6f9691cd9..1745e7bbd 100644 --- a/backend/src/graphql/resolver/GdtResolver.ts +++ b/backend/src/graphql/resolver/GdtResolver.ts @@ -8,6 +8,7 @@ import { Context, getUser } from '@/server/context' import CONFIG from '@/config' import { apiGet, apiPost } from '@/apis/HttpRequest' import { RIGHTS } from '@/auth/RIGHTS' +import LogError from '@/server/LogError' @Resolver() export class GdtResolver { @@ -25,11 +26,11 @@ export class GdtResolver { `${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.emailContact.email}/${currentPage}/${pageSize}/${order}`, ) if (!resultGDT.success) { - throw new Error(resultGDT.data) + throw new LogError(resultGDT.data) } return new GdtEntryList(resultGDT.data) } catch (err) { - throw new Error('GDT Server is not reachable.') + throw new LogError('GDT Server is not reachable') } } @@ -42,7 +43,7 @@ export class GdtResolver { email: user.emailContact.email, }) if (!resultGDTSum.success) { - throw new Error('Call not successful') + throw new LogError('Call not successful') } return Number(resultGDTSum.data.sum) || 0 } catch (err) { @@ -59,7 +60,7 @@ export class GdtResolver { // load user const resultPID = await apiGet(`${CONFIG.GDT_API_URL}/publishers/checkPidApi/${pid}`) if (!resultPID.success) { - throw new Error(resultPID.data) + throw new LogError(resultPID.data) } return resultPID.data.pid } diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 696c51d97..5ec18112c 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -84,8 +84,8 @@ export class TransactionLinkResolver { transactionLink.code = transactionLinkCode(createdDate) transactionLink.createdAt = createdDate transactionLink.validUntil = validUntil - await DbTransactionLink.save(transactionLink).catch(() => { - throw new Error('Unable to save transaction link') + await DbTransactionLink.save(transactionLink).catch((e) => { + throw new LogError('Unable to save transaction link', e) }) return new TransactionLink(transactionLink, new User(user)) @@ -101,19 +101,23 @@ export class TransactionLinkResolver { const transactionLink = await DbTransactionLink.findOne({ id }) if (!transactionLink) { - throw new Error('Transaction Link not found!') + throw new LogError('Transaction link not found', id) } if (transactionLink.userId !== user.id) { - throw new Error('Transaction Link cannot be deleted!') + throw new LogError( + 'Transaction link cannot be deleted by another user', + transactionLink.userId, + user.id, + ) } if (transactionLink.redeemedBy) { - throw new Error('Transaction Link already redeemed!') + throw new LogError('Transaction link already redeemed', transactionLink.redeemedBy) } - await transactionLink.softRemove().catch(() => { - throw new Error('Transaction Link could not be deleted!') + await transactionLink.softRemove().catch((e) => { + throw new LogError('Transaction link could not be deleted', e) }) return true @@ -316,18 +320,18 @@ export class TransactionLinkResolver { ) if (user.id === linkedUser.id) { - throw new Error('Cannot redeem own transaction link.') + throw new LogError('Cannot redeem own transaction link', user.id) } // TODO: The now check should be done within the semaphore lock, // since the program might wait a while till it is ready to proceed // writing the transaction. if (transactionLink.validUntil.getTime() < now.getTime()) { - throw new Error('Transaction Link is not valid anymore.') + throw new LogError('Transaction link is not valid anymore', transactionLink.validUntil) } if (transactionLink.redeemedBy) { - throw new Error('Transaction Link already redeemed.') + throw new LogError('Transaction link already redeemed', transactionLink.redeemedBy) } await executeTransaction( diff --git a/backend/src/graphql/resolver/util/creations.ts b/backend/src/graphql/resolver/util/creations.ts index 6a47915b1..b9ba2e69f 100644 --- a/backend/src/graphql/resolver/util/creations.ts +++ b/backend/src/graphql/resolver/util/creations.ts @@ -143,7 +143,7 @@ export const updateCreations = ( const index = getCreationIndex(contribution.contributionDate.getMonth(), timezoneOffset) if (index < 0) { - throw new Error('You cannot create GDD for a month older than the last three months.') + throw new LogError('You cannot create GDD for a month older than the last three months') } creations[index] = creations[index].plus(contribution.amount.toString()) return creations diff --git a/backend/src/server/context.ts b/backend/src/server/context.ts index 8ba590dd3..32a765777 100644 --- a/backend/src/server/context.ts +++ b/backend/src/server/context.ts @@ -3,6 +3,7 @@ import { User as dbUser } from '@entity/User' import { Transaction as dbTransaction } from '@entity/Transaction' import Decimal from 'decimal.js-light' import { ExpressContext } from 'apollo-server-express' +import LogError from './LogError' export interface Context { token: string | null @@ -35,7 +36,7 @@ const context = (args: ExpressContext): Context => { export const getUser = (context: Context): dbUser => { if (context.user) return context.user - throw new Error('No user given in context!') + throw new LogError('No user given in context') } export const getClientTimezoneOffset = (context: Context): number => { @@ -45,7 +46,7 @@ export const getClientTimezoneOffset = (context: Context): number => { ) { return context.clientTimezoneOffset } - throw new Error('No valid client time zone offset in context!') + throw new LogError('No valid client time zone offset in context') } export default context diff --git a/backend/src/util/decay.ts b/backend/src/util/decay.ts index 48674dc50..4c09d62a5 100644 --- a/backend/src/util/decay.ts +++ b/backend/src/util/decay.ts @@ -22,7 +22,7 @@ function calculateDecay( const startBlockMs = startBlock.getTime() if (toMs < fromMs) { - throw new Error('to < from, reverse decay calculation is invalid') + throw new LogError('calculateDecay: to < from, reverse decay calculation is invalid') } // Initialize with no decay diff --git a/backend/src/util/klicktipp.ts b/backend/src/util/klicktipp.ts index 0432f196e..7dfc2c98e 100644 --- a/backend/src/util/klicktipp.ts +++ b/backend/src/util/klicktipp.ts @@ -5,7 +5,7 @@ import { User } from '@entity/User' export async function retrieveNotRegisteredEmails(): Promise { const con = await connection() if (!con) { - throw new Error('No connection to database') + throw new LogError('No connection to database') } const users = await User.find({ relations: ['emailContact'] }) const notRegisteredUser = [] From 868566f716e423e88a6000f2daf3fec70afbd5c0 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Tue, 7 Feb 2023 17:25:27 +0100 Subject: [PATCH 15/70] missing changes --- backend/src/apis/KlicktippController.ts | 2 +- backend/src/auth/JWT.ts | 1 + backend/src/util/decay.ts | 1 + backend/src/util/klicktipp.ts | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/src/apis/KlicktippController.ts b/backend/src/apis/KlicktippController.ts index ca64f4b2e..824d40af2 100644 --- a/backend/src/apis/KlicktippController.ts +++ b/backend/src/apis/KlicktippController.ts @@ -31,7 +31,7 @@ export const unsubscribe = async (email: string): Promise => { if (isLogin) { return await klicktippConnector.unsubscribe(email) } - throw new LogError('Could not unsubscribe', email) + throw new Error(`Could not unsubscribe ${email}`) } export const getKlickTippUser = async (email: string): Promise => { diff --git a/backend/src/auth/JWT.ts b/backend/src/auth/JWT.ts index 301d2ddad..3f9c052f5 100644 --- a/backend/src/auth/JWT.ts +++ b/backend/src/auth/JWT.ts @@ -1,6 +1,7 @@ import jwt from 'jsonwebtoken' import CONFIG from '@/config/' import { CustomJwtPayload } from './CustomJwtPayload' +import LogError from '@/server/LogError' export const decode = (token: string): CustomJwtPayload | null => { if (!token) throw new LogError('401 Unauthorized') diff --git a/backend/src/util/decay.ts b/backend/src/util/decay.ts index 4c09d62a5..641654756 100644 --- a/backend/src/util/decay.ts +++ b/backend/src/util/decay.ts @@ -1,6 +1,7 @@ import Decimal from 'decimal.js-light' import CONFIG from '@/config' import { Decay } from '@model/Decay' +import LogError from '@/server/LogError' // TODO: externalize all those definitions and functions into an external decay library diff --git a/backend/src/util/klicktipp.ts b/backend/src/util/klicktipp.ts index 7dfc2c98e..02bdd853b 100644 --- a/backend/src/util/klicktipp.ts +++ b/backend/src/util/klicktipp.ts @@ -1,6 +1,7 @@ import connection from '@/typeorm/connection' import { getKlickTippUser } from '@/apis/KlicktippController' import { User } from '@entity/User' +import LogError from '@/server/LogError' export async function retrieveNotRegisteredEmails(): Promise { const con = await connection() From 65488b5c5c9705144b5689dd10dfa7aa2ae2fed0 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 8 Feb 2023 11:29:38 +0100 Subject: [PATCH 16/70] Test of createContribution are now centered on the final object. --- .../resolver/ContributionResolver.test.ts | 139 +++++++----------- 1 file changed, 56 insertions(+), 83 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 7c239e699..ebf40b292 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -72,8 +72,6 @@ let mutate: any, query: any, con: any let testEnv: any let creation: Contribution | void let admin: User -// let result: any -// let contribution: any let pendingContribution: any let inProgressContribution: any let contributionToConfirm: any @@ -173,16 +171,12 @@ describe('ContributionResolver', () => { describe('createContribution', () => { describe('unauthenticated', () => { it('returns an error', async () => { - await expect( - mutate({ - mutation: createContribution, - variables: { amount: 100.0, memo: 'Test Contribution', creationDate: 'not-valid' }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('401 Unauthorized')], - }), - ) + const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({ + mutation: createContribution, + variables: { amount: 100.0, memo: 'Test Contribution', creationDate: 'not-valid' }, + }) + + expect(errorObjects).toMatchObject([new GraphQLError('401 Unauthorized')]) }) }) @@ -202,20 +196,18 @@ describe('ContributionResolver', () => { it('throws error when memo length smaller than 5 chars', async () => { jest.clearAllMocks() const date = new Date() - await expect( - mutate({ - mutation: createContribution, - variables: { - amount: 100.0, - memo: 'Test', - creationDate: date.toString(), - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('memo text is too short (5 characters minimum)')], - }), - ) + const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test', + creationDate: date.toString(), + }, + }) + + expect(errorObjects).toMatchObject([ + new GraphQLError('memo text is too short (5 characters minimum)'), + ]) }) it('logs the error found', () => { @@ -225,20 +217,17 @@ describe('ContributionResolver', () => { it('throws error when memo length greater than 255 chars', async () => { jest.clearAllMocks() const date = new Date() - await expect( - mutate({ - mutation: createContribution, - variables: { - amount: 100.0, - memo: 'Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test', - creationDate: date.toString(), - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('memo text is too long (255 characters maximum)')], - }), - ) + const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test', + creationDate: date.toString(), + }, + }) + expect(errorObjects).toMatchObject([ + new GraphQLError('memo text is too long (255 characters maximum)'), + ]) }) it('logs the error found', () => { @@ -247,22 +236,17 @@ describe('ContributionResolver', () => { it('throws error when creationDate not-valid', async () => { jest.clearAllMocks() - await expect( - mutate({ - mutation: createContribution, - variables: { - amount: 100.0, - memo: 'Test env contribution', - creationDate: 'not-valid', - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [ - new GraphQLError('No information for available creations for the given date'), - ], - }), - ) + const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test env contribution', + creationDate: 'not-valid', + }, + }) + expect(errorObjects).toMatchObject([ + new GraphQLError('No information for available creations for the given date'), + ]) }) it('logs the error found', () => { @@ -275,22 +259,17 @@ describe('ContributionResolver', () => { it('throws error when creationDate 3 month behind', async () => { jest.clearAllMocks() const date = new Date() - await expect( - mutate({ - mutation: createContribution, - variables: { - amount: 100.0, - memo: 'Test env contribution', - creationDate: date.setMonth(date.getMonth() - 3).toString(), - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [ - new GraphQLError('No information for available creations for the given date'), - ], - }), - ) + const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test env contribution', + creationDate: date.setMonth(date.getMonth() - 3).toString(), + }, + }) + expect(errorObjects).toMatchObject([ + new GraphQLError('No information for available creations for the given date'), + ]) }) it('logs the error found', () => { @@ -303,17 +282,11 @@ describe('ContributionResolver', () => { describe('valid input', () => { it('creates contribution', async () => { - expect(pendingContribution).toEqual( - expect.objectContaining({ - data: { - createContribution: { - id: expect.any(Number), - amount: '100', - memo: 'Test PENDING contribution', - }, - }, - }), - ) + expect(pendingContribution.data.createContribution).toMatchObject({ + id: expect.any(Number), + amount: '100', + memo: 'Test PENDING contribution', + }) }) it('stores the create contribution event in the database', async () => { From 4cbfd83d5ec7e7dd7aff6f5aa8e99471827d8ed8 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 8 Feb 2023 11:51:09 +0100 Subject: [PATCH 17/70] Add deconstructing variables for updateContribution. --- .../resolver/ContributionResolver.test.ts | 270 ++++++++---------- 1 file changed, 119 insertions(+), 151 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index ebf40b292..1e463dd4b 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -50,6 +50,7 @@ import { EventProtocolType } from '@/event/EventProtocolType' import { logger, i18n as localization } from '@test/testSetup' import { UserInputError } from 'apollo-server-express' import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz' +import { UnconfirmedContribution } from '../model/UnconfirmedContribution' // mock account activation email to avoid console spam // mock account activation email to avoid console spam @@ -306,21 +307,16 @@ describe('ContributionResolver', () => { describe('updateContribution', () => { describe('unauthenticated', () => { it('returns an error', async () => { - await expect( - mutate({ - mutation: updateContribution, - variables: { - contributionId: 1, - amount: 100.0, - memo: 'Test Contribution', - creationDate: 'not-valid', - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('401 Unauthorized')], - }), - ) + const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({ + mutation: updateContribution, + variables: { + contributionId: 1, + amount: 100.0, + memo: 'Test Contribution', + creationDate: 'not-valid', + }, + }) + expect(errorObjects).toMatchObject([new GraphQLError('401 Unauthorized')]) }) }) @@ -339,21 +335,18 @@ describe('ContributionResolver', () => { describe('wrong contribution id', () => { it('throws an error', async () => { jest.clearAllMocks() - await expect( - mutate({ - mutation: updateContribution, - variables: { - contributionId: -1, - amount: 100.0, - memo: 'Test env contribution', - creationDate: new Date().toString(), - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('No contribution found to given id.')], - }), - ) + const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({ + mutation: updateContribution, + variables: { + contributionId: -1, + amount: 100.0, + memo: 'Test env contribution', + creationDate: new Date().toString(), + }, + }) + expect(errorObjects).toMatchObject([ + new GraphQLError('No contribution found to given id.'), + ]) }) it('logs the error found', () => { @@ -365,21 +358,18 @@ describe('ContributionResolver', () => { it('throws error', async () => { jest.clearAllMocks() const date = new Date() - await expect( - mutate({ - mutation: updateContribution, - variables: { - contributionId: pendingContribution.data.createContribution.id, - amount: 100.0, - memo: 'Test', - creationDate: date.toString(), - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('memo text is too short (5 characters minimum)')], - }), - ) + const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({ + mutation: updateContribution, + variables: { + contributionId: pendingContribution.data.createContribution.id, + amount: 100.0, + memo: 'Test', + creationDate: date.toString(), + }, + }) + expect(errorObjects).toMatchObject([ + new GraphQLError('memo text is too short (5 characters minimum)'), + ]) }) it('logs the error found', () => { @@ -391,21 +381,18 @@ describe('ContributionResolver', () => { it('throws error', async () => { jest.clearAllMocks() const date = new Date() - await expect( - mutate({ - mutation: updateContribution, - variables: { - contributionId: pendingContribution.data.createContribution.id, - amount: 100.0, - memo: 'Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test', - creationDate: date.toString(), - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('memo text is too long (255 characters maximum)')], - }), - ) + const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({ + mutation: updateContribution, + variables: { + contributionId: pendingContribution.data.createContribution.id, + amount: 100.0, + memo: 'Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test', + creationDate: date.toString(), + }, + }) + expect(errorObjects).toMatchObject([ + new GraphQLError('memo text is too long (255 characters maximum)'), + ]) }) it('logs the error found', () => { @@ -423,25 +410,18 @@ describe('ContributionResolver', () => { it('throws an error', async () => { jest.clearAllMocks() - await expect( - mutate({ - mutation: updateContribution, - variables: { - contributionId: pendingContribution.data.createContribution.id, - amount: 10.0, - memo: 'Test env contribution', - creationDate: new Date().toString(), - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [ - new GraphQLError( - 'user of the pending contribution and send user does not correspond', - ), - ], - }), - ) + const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({ + mutation: updateContribution, + variables: { + contributionId: pendingContribution.data.createContribution.id, + amount: 10.0, + memo: 'Test env contribution', + creationDate: new Date().toString(), + }, + }) + expect(errorObjects).toMatchObject([ + new GraphQLError('user of the pending contribution and send user does not correspond'), + ]) }) it('logs the error found', () => { @@ -461,25 +441,26 @@ describe('ContributionResolver', () => { it('throws an error', async () => { jest.clearAllMocks() - await expect( - mutate({ - mutation: adminUpdateContribution, - variables: { - id: pendingContribution.data.createContribution.id, - email: 'bibi@bloxberg.de', - amount: 10.0, - memo: 'Test env contribution', - creationDate: new Date().toString(), - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('An admin is not allowed to update a user contribution.')], - }), - ) + const { errors: errorObjects }: { errors: GraphQLError[] } = await mutate({ + mutation: adminUpdateContribution, + variables: { + id: pendingContribution.data.createContribution.id, + email: 'bibi@bloxberg.de', + amount: 10.0, + memo: 'Test env contribution', + creationDate: new Date().toString(), + }, + }) + expect(errorObjects).toMatchObject([ + new GraphQLError('An admin is not allowed to update a user contribution.'), + ]) }) - // TODO check that the error is logged (need to modify AdminResolver, avoid conflicts) + it('logs the error found', () => { + expect(logger.error).toBeCalledWith( + 'An admin is not allowed to update a user contribution.', + ) + }) }) describe('update to much so that the limit is exceeded', () => { @@ -492,25 +473,20 @@ describe('ContributionResolver', () => { it('throws an error', async () => { jest.clearAllMocks() - await expect( - mutate({ - mutation: updateContribution, - variables: { - contributionId: pendingContribution.data.createContribution.id, - amount: 1019.0, - memo: 'Test env contribution', - creationDate: new Date().toString(), - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [ - new GraphQLError( - 'The amount (1019 GDD) to be created exceeds the amount (600 GDD) still available for this month.', - ), - ], - }), - ) + const { errors: errorObjects }: { errors: GraphQLError[] } = await mutate({ + mutation: updateContribution, + variables: { + contributionId: pendingContribution.data.createContribution.id, + amount: 1019.0, + memo: 'Test env contribution', + creationDate: new Date().toString(), + }, + }) + expect(errorObjects).toMatchObject([ + new GraphQLError( + 'The amount (1019 GDD) to be created exceeds the amount (600 GDD) still available for this month.', + ), + ]) }) it('logs the error found', () => { @@ -524,21 +500,18 @@ describe('ContributionResolver', () => { it('throws an error', async () => { jest.clearAllMocks() const date = new Date() - await expect( - mutate({ - mutation: updateContribution, - variables: { - contributionId: pendingContribution.data.createContribution.id, - amount: 10.0, - memo: 'Test env contribution', - creationDate: date.setMonth(date.getMonth() - 3).toString(), - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('Currently the month of the contribution cannot change.')], - }), - ) + const { errors: errorObjects }: { errors: GraphQLError[] } = await mutate({ + mutation: updateContribution, + variables: { + contributionId: pendingContribution.data.createContribution.id, + amount: 10.0, + memo: 'Test env contribution', + creationDate: date.setMonth(date.getMonth() - 3).toString(), + }, + }) + expect(errorObjects).toMatchObject([ + new GraphQLError('Currently the month of the contribution cannot change.'), + ]) }) it.skip('logs the error found', () => { @@ -551,27 +524,22 @@ describe('ContributionResolver', () => { describe('valid input', () => { it('updates contribution', async () => { - await expect( - mutate({ - mutation: updateContribution, - variables: { - contributionId: pendingContribution.data.createContribution.id, - amount: 10.0, - memo: 'Test PENDING contribution update', - creationDate: new Date().toString(), - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - updateContribution: { - id: pendingContribution.data.createContribution.id, - amount: '10', - memo: 'Test PENDING contribution update', - }, - }, - }), - ) + const { + data: { updateContribution: contribution }, + }: { data: { updateContribution: UnconfirmedContribution } } = await mutate({ + mutation: updateContribution, + variables: { + contributionId: pendingContribution.data.createContribution.id, + amount: 10.0, + memo: 'Test PENDING contribution update', + creationDate: new Date().toString(), + }, + }) + expect(contribution).toMatchObject({ + id: pendingContribution.data.createContribution.id, + amount: '10', + memo: 'Test PENDING contribution update', + }) }) it('stores the update contribution event in the database', async () => { From 712b64fd068c854d8cb83da3832d0c11c8fa9795 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 8 Feb 2023 14:25:48 +0100 Subject: [PATCH 18/70] Add destructure for denyContribution tests. --- .../resolver/ContributionResolver.test.ts | 191 +++++++----------- 1 file changed, 70 insertions(+), 121 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 1e463dd4b..11ecbcd47 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -564,18 +564,13 @@ describe('ContributionResolver', () => { describe('denyContribution', () => { describe('unauthenticated', () => { it('returns an error', async () => { - await expect( - mutate({ - mutation: denyContribution, - variables: { - id: 1, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('401 Unauthorized')], - }), - ) + const { errors: errorObjects }: { errors: GraphQLError[] } = await mutate({ + mutation: denyContribution, + variables: { + id: 1, + }, + }) + expect(errorObjects).toMatchObject([new GraphQLError('401 Unauthorized')]) }) }) @@ -592,18 +587,13 @@ describe('ContributionResolver', () => { }) it('returns an error', async () => { - await expect( - mutate({ - mutation: denyContribution, - variables: { - id: 1, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('401 Unauthorized')], - }), - ) + const { errors: errorObjects }: { errors: GraphQLError[] } = await mutate({ + mutation: denyContribution, + variables: { + id: 1, + }, + }) + expect(errorObjects).toMatchObject([new GraphQLError('401 Unauthorized')]) }) }) @@ -621,18 +611,15 @@ describe('ContributionResolver', () => { describe('wrong contribution id', () => { it('throws an error', async () => { - await expect( - mutate({ - mutation: denyContribution, - variables: { - id: -1, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('Contribution not found for given id.')], - }), - ) + const { errors: errorObjects }: { errors: GraphQLError[] } = await mutate({ + mutation: denyContribution, + variables: { + id: -1, + }, + }) + expect(errorObjects).toMatchObject([ + new GraphQLError('Contribution not found for given id.'), + ]) }) it('logs the error found', () => { @@ -669,18 +656,15 @@ describe('ContributionResolver', () => { }, }) - await expect( - mutate({ - mutation: denyContribution, - variables: { - id: contribution.data.createContribution.id, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('Contribution not found for given id.')], - }), - ) + const { errors: errorObjects }: { errors: GraphQLError[] } = await mutate({ + mutation: denyContribution, + variables: { + id: contribution.data.createContribution.id, + }, + }) + expect(errorObjects).toMatchObject([ + new GraphQLError('Contribution not found for given id.'), + ]) }) it('logs the error found', () => { @@ -720,18 +704,15 @@ describe('ContributionResolver', () => { variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, }) - await expect( - mutate({ - mutation: denyContribution, - variables: { - id: contribution.data.createContribution.id, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('Contribution not found for given id.')], - }), - ) + const { errors: errorObjects }: { errors: GraphQLError[] } = await mutate({ + mutation: denyContribution, + variables: { + id: contribution.data.createContribution.id, + }, + }) + expect(errorObjects).toMatchObject([ + new GraphQLError('Contribution not found for given id.'), + ]) }) it('logs the error found', () => { @@ -771,18 +752,15 @@ describe('ContributionResolver', () => { }, }) - await expect( - mutate({ - mutation: denyContribution, - variables: { - id: contribution.data.createContribution.id, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('Contribution not found for given id.')], - }), - ) + const { errors: errorObjects }: { errors: GraphQLError[] } = await mutate({ + mutation: denyContribution, + variables: { + id: contribution.data.createContribution.id, + }, + }) + expect(errorObjects).toMatchObject([ + new GraphQLError('Contribution not found for given id.'), + ]) }) it('logs the error found', () => { @@ -798,20 +776,15 @@ describe('ContributionResolver', () => { mutation: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, }) - await expect( - mutate({ - mutation: denyContribution, - variables: { - id: contributionToDeny.data.createContribution.id, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - denyContribution: true, - }, - }), - ) + const { + data: { denyContribution: isDenied }, + }: { data: { denyContribution: boolean } } = await mutate({ + mutation: denyContribution, + variables: { + id: contributionToDeny.data.createContribution.id, + }, + }) + expect(isDenied).toBeTruthy() }) }) }) @@ -930,32 +903,11 @@ describe('ContributionResolver', () => { }) it('stores the delete contribution event in the database', async () => { - await mutate({ - mutation: login, - variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, - }) - - const contribution = await mutate({ - mutation: createContribution, - variables: { - amount: 166.0, - memo: 'Whatever contribution', - creationDate: new Date().toString(), - }, - }) - - await mutate({ - mutation: deleteContribution, - variables: { - id: contribution.data.createContribution.id, - }, - }) - await expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.CONTRIBUTION_DELETE, - contributionId: contribution.data.createContribution.id, - amount: expect.decimalEqual(166), + contributionId: contributionToDelete.data.createContribution.id, + amount: expect.decimalEqual(100), userId: peter.id, }), ) @@ -979,18 +931,15 @@ describe('ContributionResolver', () => { mutation: login, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, }) - await expect( - mutate({ - mutation: deleteContribution, - variables: { - id: contributionToConfirm.data.createContribution.id, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('A confirmed contribution can not be deleted')], - }), - ) + const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({ + mutation: deleteContribution, + variables: { + id: contributionToConfirm.data.createContribution.id, + }, + }) + expect(errorObjects).toMatchObject([ + new GraphQLError('A confirmed contribution can not be deleted'), + ]) }) it('logs the error found', () => { From bcf63ec334d6386d546fa4b9fe29550e0c2be573 Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 9 Feb 2023 15:02:53 +0100 Subject: [PATCH 19/70] Deconstruct test for listContributions. --- .../resolver/ContributionResolver.test.ts | 290 ++++++++---------- backend/src/seeds/factory/creation.ts | 3 +- 2 files changed, 135 insertions(+), 158 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 11ecbcd47..a56e89ab0 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -51,6 +51,7 @@ import { logger, i18n as localization } from '@test/testSetup' import { UserInputError } from 'apollo-server-express' import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz' import { UnconfirmedContribution } from '../model/UnconfirmedContribution' +import { ContributionListResult } from '../model/Contribution' // mock account activation email to avoid console spam // mock account activation email to avoid console spam @@ -78,6 +79,7 @@ let inProgressContribution: any let contributionToConfirm: any let contributionToDeny: any let contributionToDelete: any +let bibiCreatedContribution: Contribution beforeAll(async () => { testEnv = await testEnvironment(logger, localization) @@ -102,7 +104,7 @@ describe('ContributionResolver', () => { await userFactory(testEnv, raeuberHotzenplotz) const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de') // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await creationFactory(testEnv, bibisCreation!) + bibiCreatedContribution = await creationFactory(testEnv, bibisCreation!) await mutate({ mutation: login, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, @@ -183,7 +185,7 @@ describe('ContributionResolver', () => { describe('authenticated with valid user', () => { beforeAll(async () => { - bibi = await mutate({ + await mutate({ mutation: login, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, }) @@ -296,7 +298,7 @@ describe('ContributionResolver', () => { type: EventProtocolType.CONTRIBUTION_CREATE, amount: expect.decimalEqual(100), contributionId: pendingContribution.data.createContribution.id, - userId: bibi.data.login.id, + userId: bibi.id, }), ) }) @@ -543,7 +545,7 @@ describe('ContributionResolver', () => { }) it('stores the update contribution event in the database', async () => { - bibi = await query({ + await query({ query: login, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, }) @@ -553,7 +555,7 @@ describe('ContributionResolver', () => { type: EventProtocolType.CONTRIBUTION_UPDATE, amount: expect.decimalEqual(10), contributionId: pendingContribution.data.createContribution.id, - userId: bibi.data.login.id, + userId: bibi.id, }), ) }) @@ -793,24 +795,19 @@ describe('ContributionResolver', () => { describe('deleteContribution', () => { describe('unauthenticated', () => { it('returns an error', async () => { - await expect( - query({ - query: deleteContribution, - variables: { - id: -1, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('401 Unauthorized')], - }), - ) + const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({ + query: deleteContribution, + variables: { + id: -1, + }, + }) + expect(errorObjects).toMatchObject([new GraphQLError('401 Unauthorized')]) }) }) describe('authenticated', () => { beforeAll(async () => { - bibi = await mutate({ + await mutate({ mutation: login, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, }) @@ -822,18 +819,15 @@ describe('ContributionResolver', () => { describe('wrong contribution id', () => { it('returns an error', async () => { - await expect( - mutate({ - mutation: deleteContribution, - variables: { - id: -1, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('Contribution not found for given id.')], - }), - ) + const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({ + mutation: deleteContribution, + variables: { + id: -1, + }, + }) + expect(errorObjects).toMatchObject([ + new GraphQLError('Contribution not found for given id.'), + ]) }) it('logs the error found', () => { @@ -854,18 +848,15 @@ describe('ContributionResolver', () => { }) it('returns an error', async () => { - await expect( - mutate({ - mutation: deleteContribution, - variables: { - id: contributionToDelete.data.createContribution.id, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('Can not delete contribution of another user')], - }), - ) + const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({ + mutation: deleteContribution, + variables: { + id: contributionToDelete.data.createContribution.id, + }, + }) + expect(errorObjects).toMatchObject([ + new GraphQLError('Can not delete contribution of another user'), + ]) }) it('logs the error found', () => { @@ -886,20 +877,15 @@ describe('ContributionResolver', () => { }) it('deletes successfully', async () => { - await expect( - mutate({ - mutation: deleteContribution, - variables: { - id: contributionToDelete.data.createContribution.id, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - deleteContribution: true, - }, - }), - ) + const { + data: { deleteContribution: isDenied }, + }: { data: { deleteContribution: boolean } } = await mutate({ + mutation: deleteContribution, + variables: { + id: contributionToDelete.data.createContribution.id, + }, + }) + expect(isDenied).toBeTruthy() }) it('stores the delete contribution event in the database', async () => { @@ -908,7 +894,7 @@ describe('ContributionResolver', () => { type: EventProtocolType.CONTRIBUTION_DELETE, contributionId: contributionToDelete.data.createContribution.id, amount: expect.decimalEqual(100), - userId: peter.id, + userId: bibi.id, }), ) }) @@ -984,113 +970,103 @@ describe('ContributionResolver', () => { describe('filter confirmed is false', () => { it('returns creations', async () => { - await expect( - query({ - query: listContributions, - variables: { - currentPage: 1, - pageSize: 25, - order: 'DESC', - filterConfirmed: false, + const { + data: { listContributions: contributionListResult }, + }: { data: { listContributions: ContributionListResult } } = await query({ + query: listContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + filterConfirmed: false, + }, + }) + expect(contributionListResult).toMatchObject({ + contributionCount: 6, + contributionList: expect.arrayContaining([ + { + amount: '100', + id: contributionToConfirm.data.createContribution.id, + memo: 'Test contribution to confirm', }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listContributions: { - contributionCount: 6, - contributionList: expect.arrayContaining([ - expect.objectContaining({ - amount: '100', - id: expect.any(Number), - memo: 'Test contribution to confirm', - }), - expect.objectContaining({ - id: expect.any(Number), - memo: 'Test PENDING contribution update', - amount: '10', - }), - expect.objectContaining({ - id: expect.any(Number), - memo: 'Test contribution to deny', - amount: '100', - }), - expect.objectContaining({ - id: expect.any(Number), - memo: 'Test contribution to delete', - amount: '100', - }), - expect.objectContaining({ - id: expect.any(Number), - memo: 'Test IN_PROGRESS contribution', - amount: '100', - }), - expect.objectContaining({ - id: expect.any(Number), - memo: 'Herzlich Willkommen bei Gradido!', - amount: '1000', - }), - ]), - }, + { + id: pendingContribution.data.createContribution.id, + memo: 'Test PENDING contribution update', + amount: '10', }, - }), - ) + { + id: contributionToDeny.data.createContribution.id, + memo: 'Test contribution to deny', + amount: '100', + }, + { + id: contributionToDelete.data.createContribution.id, + memo: 'Test contribution to delete', + amount: '100', + }, + { + id: inProgressContribution.data.createContribution.id, + memo: 'Test IN_PROGRESS contribution', + amount: '100', + }, + { + id: bibiCreatedContribution.id, + memo: 'Herzlich Willkommen bei Gradido!', + amount: '1000', + }, + ]), + }) }) }) describe('filter confirmed is true', () => { it('returns only unconfirmed creations', async () => { - await expect( - query({ - query: listContributions, - variables: { - currentPage: 1, - pageSize: 25, - order: 'DESC', - filterConfirmed: true, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listContributions: { - contributionCount: 4, - contributionList: expect.arrayContaining([ - expect.not.objectContaining({ - amount: '100', - id: expect.any(Number), - memo: 'Test contribution to confirm', - }), - expect.objectContaining({ - id: expect.any(Number), - memo: 'Test PENDING contribution update', - amount: '10', - }), - expect.objectContaining({ - id: expect.any(Number), - memo: 'Test contribution to deny', - amount: '100', - }), - expect.objectContaining({ - id: expect.any(Number), - memo: 'Test contribution to delete', - amount: '100', - }), - expect.objectContaining({ - id: expect.any(Number), - memo: 'Test IN_PROGRESS contribution', - amount: '100', - }), - expect.not.objectContaining({ - id: expect.any(Number), - memo: 'Herzlich Willkommen bei Gradido!', - amount: '1000', - }), - ]), - }, - }, - }), - ) + const { + data: { listContributions: contributionListResult }, + }: { data: { listContributions: ContributionListResult } } = await query({ + query: listContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + filterConfirmed: true, + }, + }) + expect(contributionListResult).toMatchObject({ + contributionCount: 4, + contributionList: expect.arrayContaining([ + expect.not.objectContaining({ + amount: '100', + id: contributionToConfirm.data.createContribution.id, + memo: 'Test contribution to confirm', + }), + expect.objectContaining({ + id: pendingContribution.data.createContribution.id, + memo: 'Test PENDING contribution update', + amount: '10', + }), + expect.objectContaining({ + id: contributionToDeny.data.createContribution.id, + memo: 'Test contribution to deny', + amount: '100', + }), + expect.objectContaining({ + id: contributionToDelete.data.createContribution.id, + memo: 'Test contribution to delete', + amount: '100', + }), + expect.objectContaining({ + id: inProgressContribution.data.createContribution.id, + memo: 'Test IN_PROGRESS contribution', + amount: '100', + }), + expect.not.objectContaining({ + id: bibiCreatedContribution.id, + memo: 'Herzlich Willkommen bei Gradido!', + amount: '1000', + }), + ]), + }) }) }) }) diff --git a/backend/src/seeds/factory/creation.ts b/backend/src/seeds/factory/creation.ts index 09bf981bb..69d77aa03 100644 --- a/backend/src/seeds/factory/creation.ts +++ b/backend/src/seeds/factory/creation.ts @@ -16,7 +16,7 @@ export const nMonthsBefore = (date: Date, months = 1): string => { export const creationFactory = async ( client: ApolloServerTestClient, creation: CreationInterface, -): Promise => { +): Promise => { const { mutate } = client await mutate({ mutation: login, variables: { email: creation.email, password: 'Aa12345_' } }) @@ -51,6 +51,7 @@ export const creationFactory = async ( await confirmedContribution.save() } } + return confirmedContribution } else { return contribution } From 96feb30476b515d67d42a8bc5c3d3811c7583a22 Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 9 Feb 2023 16:08:57 +0100 Subject: [PATCH 20/70] Deconstruct the listAllContributions test. --- .../resolver/ContributionResolver.test.ts | 1229 ++++++++--------- 1 file changed, 582 insertions(+), 647 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index a56e89ab0..eff7925b1 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -1075,21 +1075,16 @@ describe('ContributionResolver', () => { describe('listAllContribution', () => { describe('unauthenticated', () => { it('returns an error', async () => { - await expect( - query({ - query: listAllContributions, - variables: { - currentPage: 1, - pageSize: 25, - order: 'DESC', - statusFilter: null, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('401 Unauthorized')], - }), - ) + const { errors: errorObjects }: { errors: [GraphQLError] } = await query({ + query: listAllContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + statusFilter: null, + }, + }) + expect(errorObjects).toMatchObject([new GraphQLError('401 Unauthorized')]) }) }) @@ -1106,670 +1101,610 @@ describe('ContributionResolver', () => { }) it('throws an error with "NOT_VALID" in statusFilter', async () => { - await expect( - query({ - query: listAllContributions, - variables: { - currentPage: 1, - pageSize: 25, - order: 'DESC', - statusFilter: ['NOT_VALID'], - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [ - new UserInputError( - 'Variable "$statusFilter" got invalid value "NOT_VALID" at "statusFilter[0]"; Value "NOT_VALID" does not exist in "ContributionStatus" enum.', - ), - ], - }), - ) + const { errors: errorObjects }: { errors: [GraphQLError | UserInputError] } = await query({ + query: listAllContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + statusFilter: ['NOT_VALID'], + }, + }) + expect(errorObjects).toMatchObject([ + new UserInputError( + 'Variable "$statusFilter" got invalid value "NOT_VALID" at "statusFilter[0]"; Value "NOT_VALID" does not exist in "ContributionStatus" enum.', + ), + ]) }) it('throws an error with a null in statusFilter', async () => { - await expect( - query({ - query: listAllContributions, - variables: { - currentPage: 1, - pageSize: 25, - order: 'DESC', - statusFilter: [null], - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [ - new UserInputError( - 'Variable "$statusFilter" got invalid value null at "statusFilter[0]"; Expected non-nullable type "ContributionStatus!" not to be null.', - ), - ], - }), - ) + const { errors: errorObjects }: { errors: [Error] } = await query({ + query: listAllContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + statusFilter: [null], + }, + }) + expect(errorObjects).toMatchObject([ + new UserInputError( + 'Variable "$statusFilter" got invalid value null at "statusFilter[0]"; Expected non-nullable type "ContributionStatus!" not to be null.', + ), + ]) }) it('throws an error with null and "NOT_VALID" in statusFilter', async () => { - await expect( - query({ - query: listAllContributions, - variables: { - currentPage: 1, - pageSize: 25, - order: 'DESC', - statusFilter: [null, 'NOT_VALID'], - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [ - new UserInputError( - 'Variable "$statusFilter" got invalid value null at "statusFilter[0]"; Expected non-nullable type "ContributionStatus!" not to be null.', - ), - new UserInputError( - 'Variable "$statusFilter" got invalid value "NOT_VALID" at "statusFilter[1]"; Value "NOT_VALID" does not exist in "ContributionStatus" enum.', - ), - ], - }), - ) + const { errors: errorObjects }: { errors: [Error] } = await query({ + query: listAllContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + statusFilter: [null, 'NOT_VALID'], + }, + }) + expect(errorObjects).toMatchObject([ + new UserInputError( + 'Variable "$statusFilter" got invalid value null at "statusFilter[0]"; Expected non-nullable type "ContributionStatus!" not to be null.', + ), + new UserInputError( + 'Variable "$statusFilter" got invalid value "NOT_VALID" at "statusFilter[1]"; Value "NOT_VALID" does not exist in "ContributionStatus" enum.', + ), + ]) }) it('returns all contributions without statusFilter', async () => { - await expect( - query({ - query: listAllContributions, - variables: { - currentPage: 1, - pageSize: 25, - order: 'DESC', - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listAllContributions: { - contributionCount: 7, - contributionList: expect.arrayContaining([ - expect.objectContaining({ - amount: '100', - state: 'CONFIRMED', - id: expect.any(Number), - memo: 'Test contribution to confirm', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'PENDING', - memo: 'Test PENDING contribution update', - amount: '10', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'DENIED', - memo: 'Test contribution to deny', - amount: '100', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'DELETED', - memo: 'Test contribution to delete', - amount: '100', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'IN_PROGRESS', - memo: 'Test IN_PROGRESS contribution', - amount: '100', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'CONFIRMED', - memo: 'Herzlich Willkommen bei Gradido!', - amount: '1000', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'CONFIRMED', - memo: 'Whatever contribution', - amount: '166', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'DENIED', - memo: 'Whatever contribution', - amount: '166', - }), - ]), - }, - }, - }), - ) + const { + data: { listAllContributions: contributionListObject }, + }: { data: { listAllContributions: ContributionListResult } } = await query({ + query: listAllContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + }, + }) + expect(contributionListObject).toMatchObject({ + contributionCount: 7, + contributionList: expect.arrayContaining([ + expect.objectContaining({ + amount: '100', + state: 'CONFIRMED', + id: contributionToConfirm.data.createContribution.id, + memo: 'Test contribution to confirm', + }), + expect.objectContaining({ + id: pendingContribution.data.createContribution.id, + state: 'PENDING', + memo: 'Test PENDING contribution update', + amount: '10', + }), + expect.objectContaining({ + id: contributionToDeny.data.createContribution.id, + state: 'DENIED', + memo: 'Test contribution to deny', + amount: '100', + }), + expect.not.objectContaining({ + id: contributionToDelete.data.createContribution.id, + state: 'DELETED', + memo: 'Test contribution to delete', + amount: '100', + }), + expect.objectContaining({ + id: inProgressContribution.data.createContribution.id, + state: 'IN_PROGRESS', + memo: 'Test IN_PROGRESS contribution', + amount: '100', + }), + expect.objectContaining({ + id: bibiCreatedContribution.id, + state: 'CONFIRMED', + memo: 'Herzlich Willkommen bei Gradido!', + amount: '1000', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'CONFIRMED', + memo: 'Whatever contribution', + amount: '166', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Whatever contribution', + amount: '166', + }), + ]), + }) }) it('returns all contributions for statusFilter = null', async () => { - await expect( - query({ - query: listAllContributions, - variables: { - currentPage: 1, - pageSize: 25, - order: 'DESC', - statusFilter: null, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listAllContributions: { - contributionCount: 7, - contributionList: expect.arrayContaining([ - expect.objectContaining({ - amount: '100', - state: 'CONFIRMED', - id: expect.any(Number), - memo: 'Test contribution to confirm', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'PENDING', - memo: 'Test PENDING contribution update', - amount: '10', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'DENIED', - memo: 'Test contribution to deny', - amount: '100', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'DELETED', - memo: 'Test contribution to delete', - amount: '100', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'IN_PROGRESS', - memo: 'Test IN_PROGRESS contribution', - amount: '100', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'CONFIRMED', - memo: 'Herzlich Willkommen bei Gradido!', - amount: '1000', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'CONFIRMED', - memo: 'Whatever contribution', - amount: '166', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'DENIED', - memo: 'Whatever contribution', - amount: '166', - }), - ]), - }, - }, - }), - ) + const { + data: { listAllContributions: contributionListObject }, + }: { data: { listAllContributions: ContributionListResult } } = await query({ + query: listAllContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + statusFilter: null, + }, + }) + expect(contributionListObject).toMatchObject({ + contributionCount: 7, + contributionList: expect.arrayContaining([ + expect.objectContaining({ + amount: '100', + state: 'CONFIRMED', + id: contributionToConfirm.data.createContribution.id, + memo: 'Test contribution to confirm', + }), + expect.objectContaining({ + id: pendingContribution.data.createContribution.id, + state: 'PENDING', + memo: 'Test PENDING contribution update', + amount: '10', + }), + expect.objectContaining({ + id: contributionToDeny.data.createContribution.id, + state: 'DENIED', + memo: 'Test contribution to deny', + amount: '100', + }), + expect.not.objectContaining({ + id: contributionToDelete.data.createContribution.id, + state: 'DELETED', + memo: 'Test contribution to delete', + amount: '100', + }), + expect.objectContaining({ + id: inProgressContribution.data.createContribution.id, + state: 'IN_PROGRESS', + memo: 'Test IN_PROGRESS contribution', + amount: '100', + }), + expect.objectContaining({ + id: bibiCreatedContribution.id, + state: 'CONFIRMED', + memo: 'Herzlich Willkommen bei Gradido!', + amount: '1000', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'CONFIRMED', + memo: 'Whatever contribution', + amount: '166', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Whatever contribution', + amount: '166', + }), + ]), + }) }) it('returns all contributions for statusFilter = []', async () => { - await expect( - query({ - query: listAllContributions, - variables: { - currentPage: 1, - pageSize: 25, - order: 'DESC', - statusFilter: [], - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listAllContributions: { - contributionCount: 7, - contributionList: expect.arrayContaining([ - expect.objectContaining({ - amount: '100', - state: 'CONFIRMED', - id: expect.any(Number), - memo: 'Test contribution to confirm', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'PENDING', - memo: 'Test PENDING contribution update', - amount: '10', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'DENIED', - memo: 'Test contribution to deny', - amount: '100', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'DELETED', - memo: 'Test contribution to delete', - amount: '100', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'IN_PROGRESS', - memo: 'Test IN_PROGRESS contribution', - amount: '100', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'CONFIRMED', - memo: 'Herzlich Willkommen bei Gradido!', - amount: '1000', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'CONFIRMED', - memo: 'Whatever contribution', - amount: '166', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'DENIED', - memo: 'Whatever contribution', - amount: '166', - }), - ]), - }, - }, - }), - ) + const { + data: { listAllContributions: contributionListObject }, + }: { data: { listAllContributions: ContributionListResult } } = await query({ + query: listAllContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + statusFilter: [], + }, + }) + expect(contributionListObject).toMatchObject({ + contributionCount: 7, + contributionList: expect.arrayContaining([ + expect.objectContaining({ + amount: '100', + state: 'CONFIRMED', + id: contributionToConfirm.data.createContribution.id, + memo: 'Test contribution to confirm', + }), + expect.objectContaining({ + id: pendingContribution.data.createContribution.id, + state: 'PENDING', + memo: 'Test PENDING contribution update', + amount: '10', + }), + expect.objectContaining({ + id: contributionToDeny.data.createContribution.id, + state: 'DENIED', + memo: 'Test contribution to deny', + amount: '100', + }), + expect.not.objectContaining({ + id: contributionToDelete.data.createContribution.id, + state: 'DELETED', + memo: 'Test contribution to delete', + amount: '100', + }), + expect.objectContaining({ + id: inProgressContribution.data.createContribution.id, + state: 'IN_PROGRESS', + memo: 'Test IN_PROGRESS contribution', + amount: '100', + }), + expect.objectContaining({ + id: bibiCreatedContribution.id, + state: 'CONFIRMED', + memo: 'Herzlich Willkommen bei Gradido!', + amount: '1000', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'CONFIRMED', + memo: 'Whatever contribution', + amount: '166', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Whatever contribution', + amount: '166', + }), + ]), + }) }) it('returns all CONFIRMED contributions', async () => { - await expect( - query({ - query: listAllContributions, - variables: { - currentPage: 1, - pageSize: 25, - order: 'DESC', - statusFilter: ['CONFIRMED'], - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listAllContributions: { - contributionCount: 3, - contributionList: expect.arrayContaining([ - expect.objectContaining({ - amount: '100', - state: 'CONFIRMED', - id: expect.any(Number), - memo: 'Test contribution to confirm', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'PENDING', - memo: 'Test PENDING contribution update', - amount: '10', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'DENIED', - memo: 'Test contribution to deny', - amount: '100', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'DELETED', - memo: 'Test contribution to delete', - amount: '100', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'IN_PROGRESS', - memo: 'Test IN_PROGRESS contribution', - amount: '100', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'CONFIRMED', - memo: 'Herzlich Willkommen bei Gradido!', - amount: '1000', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'CONFIRMED', - memo: 'Whatever contribution', - amount: '166', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'DENIED', - memo: 'Whatever contribution', - amount: '166', - }), - ]), - }, - }, - }), - ) + const { + data: { listAllContributions: contributionListObject }, + }: { data: { listAllContributions: ContributionListResult } } = await query({ + query: listAllContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + statusFilter: ['CONFIRMED'], + }, + }) + expect(contributionListObject).toMatchObject({ + contributionCount: 3, + contributionList: expect.arrayContaining([ + expect.objectContaining({ + amount: '100', + state: 'CONFIRMED', + id: contributionToConfirm.data.createContribution.id, + memo: 'Test contribution to confirm', + }), + expect.not.objectContaining({ + id: pendingContribution.data.createContribution.id, + state: 'PENDING', + memo: 'Test PENDING contribution update', + amount: '10', + }), + expect.not.objectContaining({ + id: contributionToDeny.data.createContribution.id, + state: 'DENIED', + memo: 'Test contribution to deny', + amount: '100', + }), + expect.not.objectContaining({ + id: contributionToDelete.data.createContribution.id, + state: 'DELETED', + memo: 'Test contribution to delete', + amount: '100', + }), + expect.not.objectContaining({ + id: inProgressContribution.data.createContribution.id, + state: 'IN_PROGRESS', + memo: 'Test IN_PROGRESS contribution', + amount: '100', + }), + expect.objectContaining({ + id: bibiCreatedContribution.id, + state: 'CONFIRMED', + memo: 'Herzlich Willkommen bei Gradido!', + amount: '1000', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'CONFIRMED', + memo: 'Whatever contribution', + amount: '166', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Whatever contribution', + amount: '166', + }), + ]), + }) }) it('returns all PENDING contributions', async () => { - await expect( - query({ - query: listAllContributions, - variables: { - currentPage: 1, - pageSize: 25, - order: 'DESC', - statusFilter: ['PENDING'], - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listAllContributions: { - contributionCount: 1, - contributionList: expect.arrayContaining([ - expect.not.objectContaining({ - amount: '100', - state: 'CONFIRMED', - id: expect.any(Number), - memo: 'Test contribution to confirm', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'PENDING', - memo: 'Test PENDING contribution update', - amount: '10', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'DENIED', - memo: 'Test contribution to deny', - amount: '100', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'DELETED', - memo: 'Test contribution to delete', - amount: '100', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'IN_PROGRESS', - memo: 'Test IN_PROGRESS contribution', - amount: '100', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'CONFIRMED', - memo: 'Herzlich Willkommen bei Gradido!', - amount: '1000', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'CONFIRMED', - memo: 'Whatever contribution', - amount: '166', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'DENIED', - memo: 'Whatever contribution', - amount: '166', - }), - ]), - }, - }, - }), - ) + const { + data: { listAllContributions: contributionListObject }, + }: { data: { listAllContributions: ContributionListResult } } = await query({ + query: listAllContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + statusFilter: ['PENDING'], + }, + }) + expect(contributionListObject).toMatchObject({ + contributionCount: 1, + contributionList: expect.arrayContaining([ + expect.not.objectContaining({ + amount: '100', + state: 'CONFIRMED', + id: contributionToConfirm.data.createContribution.id, + memo: 'Test contribution to confirm', + }), + expect.objectContaining({ + id: pendingContribution.data.createContribution.id, + state: 'PENDING', + memo: 'Test PENDING contribution update', + amount: '10', + }), + expect.not.objectContaining({ + id: contributionToDeny.data.createContribution.id, + state: 'DENIED', + memo: 'Test contribution to deny', + amount: '100', + }), + expect.not.objectContaining({ + id: contributionToDelete.data.createContribution.id, + state: 'DELETED', + memo: 'Test contribution to delete', + amount: '100', + }), + expect.not.objectContaining({ + id: inProgressContribution.data.createContribution.id, + state: 'IN_PROGRESS', + memo: 'Test IN_PROGRESS contribution', + amount: '100', + }), + expect.not.objectContaining({ + id: bibiCreatedContribution.id, + state: 'CONFIRMED', + memo: 'Herzlich Willkommen bei Gradido!', + amount: '1000', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'CONFIRMED', + memo: 'Whatever contribution', + amount: '166', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Whatever contribution', + amount: '166', + }), + ]), + }) }) it('returns all IN_PROGRESS Creation', async () => { - await expect( - query({ - query: listAllContributions, - variables: { - currentPage: 1, - pageSize: 25, - order: 'DESC', - statusFilter: ['IN_PROGRESS'], - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listAllContributions: { - contributionCount: 1, - contributionList: expect.arrayContaining([ - expect.not.objectContaining({ - amount: '100', - state: 'CONFIRMED', - id: expect.any(Number), - memo: 'Test contribution to confirm', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'PENDING', - memo: 'Test PENDING contribution update', - amount: '10', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'DENIED', - memo: 'Test contribution to deny', - amount: '100', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'DELETED', - memo: 'Test contribution to delete', - amount: '100', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'IN_PROGRESS', - memo: 'Test IN_PROGRESS contribution', - amount: '100', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'CONFIRMED', - memo: 'Herzlich Willkommen bei Gradido!', - amount: '1000', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'CONFIRMED', - memo: 'Whatever contribution', - amount: '166', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'DENIED', - memo: 'Whatever contribution', - amount: '166', - }), - ]), - }, - }, - }), - ) + const { + data: { listAllContributions: contributionListObject }, + }: { data: { listAllContributions: ContributionListResult } } = await query({ + query: listAllContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + statusFilter: ['IN_PROGRESS'], + }, + }) + expect(contributionListObject).toMatchObject({ + contributionCount: 1, + contributionList: expect.arrayContaining([ + expect.not.objectContaining({ + amount: '100', + state: 'CONFIRMED', + id: contributionToConfirm.data.createContribution.id, + memo: 'Test contribution to confirm', + }), + expect.not.objectContaining({ + id: pendingContribution.data.createContribution.id, + state: 'PENDING', + memo: 'Test PENDING contribution update', + amount: '10', + }), + expect.not.objectContaining({ + id: contributionToDeny.data.createContribution.id, + state: 'DENIED', + memo: 'Test contribution to deny', + amount: '100', + }), + expect.not.objectContaining({ + id: contributionToDelete.data.createContribution.id, + state: 'DELETED', + memo: 'Test contribution to delete', + amount: '100', + }), + expect.objectContaining({ + id: inProgressContribution.data.createContribution.id, + state: 'IN_PROGRESS', + memo: 'Test IN_PROGRESS contribution', + amount: '100', + }), + expect.not.objectContaining({ + id: bibiCreatedContribution.id, + state: 'CONFIRMED', + memo: 'Herzlich Willkommen bei Gradido!', + amount: '1000', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'CONFIRMED', + memo: 'Whatever contribution', + amount: '166', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Whatever contribution', + amount: '166', + }), + ]), + }) }) it('returns all DENIED Creation', async () => { - await expect( - query({ - query: listAllContributions, - variables: { - currentPage: 1, - pageSize: 25, - order: 'DESC', - statusFilter: ['DENIED'], - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listAllContributions: { - contributionCount: 2, - contributionList: expect.arrayContaining([ - expect.not.objectContaining({ - amount: '100', - state: 'CONFIRMED', - id: expect.any(Number), - memo: 'Test contribution to confirm', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'PENDING', - memo: 'Test PENDING contribution update', - amount: '10', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'DENIED', - memo: 'Test contribution to deny', - amount: '100', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'DELETED', - memo: 'Test contribution to delete', - amount: '100', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'IN_PROGRESS', - memo: 'Test IN_PROGRESS contribution', - amount: '100', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'CONFIRMED', - memo: 'Herzlich Willkommen bei Gradido!', - amount: '1000', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'CONFIRMED', - memo: 'Whatever contribution', - amount: '166', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'DENIED', - memo: 'Whatever contribution', - amount: '166', - }), - ]), - }, - }, - }), - ) + const { + data: { listAllContributions: contributionListObject }, + }: { data: { listAllContributions: ContributionListResult } } = await query({ + query: listAllContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + statusFilter: ['DENIED'], + }, + }) + expect(contributionListObject).toMatchObject({ + contributionCount: 2, + contributionList: expect.arrayContaining([ + expect.not.objectContaining({ + amount: '100', + state: 'CONFIRMED', + id: contributionToConfirm.data.createContribution.id, + memo: 'Test contribution to confirm', + }), + expect.not.objectContaining({ + id: pendingContribution.data.createContribution.id, + state: 'PENDING', + memo: 'Test PENDING contribution update', + amount: '10', + }), + expect.objectContaining({ + id: contributionToDeny.data.createContribution.id, + state: 'DENIED', + memo: 'Test contribution to deny', + amount: '100', + }), + expect.not.objectContaining({ + id: contributionToDelete.data.createContribution.id, + state: 'DELETED', + memo: 'Test contribution to delete', + amount: '100', + }), + expect.not.objectContaining({ + id: inProgressContribution.data.createContribution.id, + state: 'IN_PROGRESS', + memo: 'Test IN_PROGRESS contribution', + amount: '100', + }), + expect.not.objectContaining({ + id: bibiCreatedContribution.id, + state: 'CONFIRMED', + memo: 'Herzlich Willkommen bei Gradido!', + amount: '1000', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'CONFIRMED', + memo: 'Whatever contribution', + amount: '166', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Whatever contribution', + amount: '166', + }), + ]), + }) }) - it('returns all DELETED Creation', async () => { - await expect( - query({ - query: listAllContributions, - variables: { - currentPage: 1, - pageSize: 25, - order: 'DESC', - statusFilter: ['DELETED'], - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listAllContributions: { - contributionCount: 0, - contributionList: [], - }, - }, - }), - ) + it('does not return any DELETED Creation', async () => { + const { + data: { listAllContributions: contributionListObject }, + }: { data: { listAllContributions: ContributionListResult } } = await query({ + query: listAllContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + statusFilter: ['DELETED'], + }, + }) + expect(contributionListObject).toMatchObject({ + contributionCount: 0, + contributionList: [], + }) }) it('returns all CONFIRMED and PENDING Creation', async () => { - await expect( - query({ - query: listAllContributions, - variables: { - currentPage: 1, - pageSize: 25, - order: 'DESC', - statusFilter: ['CONFIRMED', 'PENDING'], - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listAllContributions: { - contributionCount: 4, - contributionList: expect.arrayContaining([ - expect.objectContaining({ - amount: '100', - state: 'CONFIRMED', - id: expect.any(Number), - memo: 'Test contribution to confirm', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'PENDING', - memo: 'Test PENDING contribution update', - amount: '10', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'DENIED', - memo: 'Test contribution to deny', - amount: '100', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'DELETED', - memo: 'Test contribution to delete', - amount: '100', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'IN_PROGRESS', - memo: 'Test IN_PROGRESS contribution', - amount: '100', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'CONFIRMED', - memo: 'Herzlich Willkommen bei Gradido!', - amount: '1000', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'DENIED', - memo: 'Whatever contribution', - amount: '166', - }), - expect.objectContaining({ - id: expect.any(Number), - state: 'CONFIRMED', - memo: 'Whatever contribution', - amount: '166', - }), - ]), - }, - }, - }), - ) + const { + data: { listAllContributions: contributionListObject }, + }: { data: { listAllContributions: ContributionListResult } } = await query({ + query: listAllContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + statusFilter: ['CONFIRMED', 'PENDING'], + }, + }) + expect(contributionListObject).toMatchObject({ + contributionCount: 4, + contributionList: expect.arrayContaining([ + expect.objectContaining({ + amount: '100', + state: 'CONFIRMED', + id: contributionToConfirm.data.createContribution.id, + memo: 'Test contribution to confirm', + }), + expect.objectContaining({ + id: pendingContribution.data.createContribution.id, + state: 'PENDING', + memo: 'Test PENDING contribution update', + amount: '10', + }), + expect.not.objectContaining({ + id: contributionToDeny.data.createContribution.id, + state: 'DENIED', + memo: 'Test contribution to deny', + amount: '100', + }), + expect.not.objectContaining({ + id: contributionToDelete.data.createContribution.id, + state: 'DELETED', + memo: 'Test contribution to delete', + amount: '100', + }), + expect.not.objectContaining({ + id: inProgressContribution.data.createContribution.id, + state: 'IN_PROGRESS', + memo: 'Test IN_PROGRESS contribution', + amount: '100', + }), + expect.objectContaining({ + id: bibiCreatedContribution.id, + state: 'CONFIRMED', + memo: 'Herzlich Willkommen bei Gradido!', + amount: '1000', + }), + expect.not.objectContaining({ + id: expect.any(Number), + state: 'DENIED', + memo: 'Whatever contribution', + amount: '166', + }), + expect.objectContaining({ + id: expect.any(Number), + state: 'CONFIRMED', + memo: 'Whatever contribution', + amount: '166', + }), + ]), + }) }) }) }) From e5025a906cd649a3094b6822b8ea1232855fded0 Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 9 Feb 2023 16:24:18 +0100 Subject: [PATCH 21/70] Deconstruct not authenticated. --- .../resolver/ContributionResolver.test.ts | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index eff7925b1..93a31fb77 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -938,21 +938,16 @@ describe('ContributionResolver', () => { describe('listContributions', () => { describe('unauthenticated', () => { it('returns an error', async () => { - await expect( - query({ - query: listContributions, - variables: { - currentPage: 1, - pageSize: 25, - order: 'DESC', - filterConfirmed: false, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('401 Unauthorized')], - }), - ) + const { errors: errorObjects }: { errors: [GraphQLError] } = await query({ + query: listContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + filterConfirmed: false, + }, + }) + expect(errorObjects).toMatchObject([new GraphQLError('401 Unauthorized')]) }) }) From 6ff43e2d4c4355261e7c6cd6676d5adb1a53a110 Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 9 Feb 2023 17:10:27 +0100 Subject: [PATCH 22/70] Change the expect.not.objectContaining to the state instead of the whole object. --- .../resolver/ContributionResolver.test.ts | 225 ++++-------------- 1 file changed, 49 insertions(+), 176 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 93a31fb77..5b2c0fcaf 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -96,11 +96,10 @@ afterAll(async () => { describe('ContributionResolver', () => { let bibi: any - let peter: any beforeAll(async () => { bibi = await userFactory(testEnv, bibiBloxberg) - admin = peter = await userFactory(testEnv, peterLustig) + admin = await userFactory(testEnv, peterLustig) await userFactory(testEnv, raeuberHotzenplotz) const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de') // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -1163,6 +1162,9 @@ describe('ContributionResolver', () => { expect(contributionListObject).toMatchObject({ contributionCount: 7, contributionList: expect.arrayContaining([ + expect.not.objectContaining({ + state: 'DELETED', + }), expect.objectContaining({ amount: '100', state: 'CONFIRMED', @@ -1181,12 +1183,6 @@ describe('ContributionResolver', () => { memo: 'Test contribution to deny', amount: '100', }), - expect.not.objectContaining({ - id: contributionToDelete.data.createContribution.id, - state: 'DELETED', - memo: 'Test contribution to delete', - amount: '100', - }), expect.objectContaining({ id: inProgressContribution.data.createContribution.id, state: 'IN_PROGRESS', @@ -1230,6 +1226,9 @@ describe('ContributionResolver', () => { expect(contributionListObject).toMatchObject({ contributionCount: 7, contributionList: expect.arrayContaining([ + expect.not.objectContaining({ + state: 'DELETED', + }), expect.objectContaining({ amount: '100', state: 'CONFIRMED', @@ -1248,12 +1247,6 @@ describe('ContributionResolver', () => { memo: 'Test contribution to deny', amount: '100', }), - expect.not.objectContaining({ - id: contributionToDelete.data.createContribution.id, - state: 'DELETED', - memo: 'Test contribution to delete', - amount: '100', - }), expect.objectContaining({ id: inProgressContribution.data.createContribution.id, state: 'IN_PROGRESS', @@ -1297,6 +1290,9 @@ describe('ContributionResolver', () => { expect(contributionListObject).toMatchObject({ contributionCount: 7, contributionList: expect.arrayContaining([ + expect.not.objectContaining({ + state: 'DELETED', + }), expect.objectContaining({ amount: '100', state: 'CONFIRMED', @@ -1315,12 +1311,6 @@ describe('ContributionResolver', () => { memo: 'Test contribution to deny', amount: '100', }), - expect.not.objectContaining({ - id: contributionToDelete.data.createContribution.id, - state: 'DELETED', - memo: 'Test contribution to delete', - amount: '100', - }), expect.objectContaining({ id: inProgressContribution.data.createContribution.id, state: 'IN_PROGRESS', @@ -1370,30 +1360,6 @@ describe('ContributionResolver', () => { id: contributionToConfirm.data.createContribution.id, memo: 'Test contribution to confirm', }), - expect.not.objectContaining({ - id: pendingContribution.data.createContribution.id, - state: 'PENDING', - memo: 'Test PENDING contribution update', - amount: '10', - }), - expect.not.objectContaining({ - id: contributionToDeny.data.createContribution.id, - state: 'DENIED', - memo: 'Test contribution to deny', - amount: '100', - }), - expect.not.objectContaining({ - id: contributionToDelete.data.createContribution.id, - state: 'DELETED', - memo: 'Test contribution to delete', - amount: '100', - }), - expect.not.objectContaining({ - id: inProgressContribution.data.createContribution.id, - state: 'IN_PROGRESS', - memo: 'Test IN_PROGRESS contribution', - amount: '100', - }), expect.objectContaining({ id: bibiCreatedContribution.id, state: 'CONFIRMED', @@ -1407,10 +1373,16 @@ describe('ContributionResolver', () => { amount: '166', }), expect.not.objectContaining({ - id: expect.any(Number), + state: 'PENDING', + }), + expect.not.objectContaining({ state: 'DENIED', - memo: 'Whatever contribution', - amount: '166', + }), + expect.not.objectContaining({ + state: 'DELETED', + }), + expect.not.objectContaining({ + state: 'IN_PROGRESS', }), ]), }) @@ -1432,10 +1404,16 @@ describe('ContributionResolver', () => { contributionCount: 1, contributionList: expect.arrayContaining([ expect.not.objectContaining({ - amount: '100', state: 'CONFIRMED', - id: contributionToConfirm.data.createContribution.id, - memo: 'Test contribution to confirm', + }), + expect.not.objectContaining({ + state: 'DENIED', + }), + expect.not.objectContaining({ + state: 'DELETED', + }), + expect.not.objectContaining({ + state: 'IN_PROGRESS', }), expect.objectContaining({ id: pendingContribution.data.createContribution.id, @@ -1443,42 +1421,6 @@ describe('ContributionResolver', () => { memo: 'Test PENDING contribution update', amount: '10', }), - expect.not.objectContaining({ - id: contributionToDeny.data.createContribution.id, - state: 'DENIED', - memo: 'Test contribution to deny', - amount: '100', - }), - expect.not.objectContaining({ - id: contributionToDelete.data.createContribution.id, - state: 'DELETED', - memo: 'Test contribution to delete', - amount: '100', - }), - expect.not.objectContaining({ - id: inProgressContribution.data.createContribution.id, - state: 'IN_PROGRESS', - memo: 'Test IN_PROGRESS contribution', - amount: '100', - }), - expect.not.objectContaining({ - id: bibiCreatedContribution.id, - state: 'CONFIRMED', - memo: 'Herzlich Willkommen bei Gradido!', - amount: '1000', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'CONFIRMED', - memo: 'Whatever contribution', - amount: '166', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'DENIED', - memo: 'Whatever contribution', - amount: '166', - }), ]), }) }) @@ -1499,28 +1441,16 @@ describe('ContributionResolver', () => { contributionCount: 1, contributionList: expect.arrayContaining([ expect.not.objectContaining({ - amount: '100', state: 'CONFIRMED', - id: contributionToConfirm.data.createContribution.id, - memo: 'Test contribution to confirm', }), expect.not.objectContaining({ - id: pendingContribution.data.createContribution.id, state: 'PENDING', - memo: 'Test PENDING contribution update', - amount: '10', }), expect.not.objectContaining({ - id: contributionToDeny.data.createContribution.id, state: 'DENIED', - memo: 'Test contribution to deny', - amount: '100', }), expect.not.objectContaining({ - id: contributionToDelete.data.createContribution.id, state: 'DELETED', - memo: 'Test contribution to delete', - amount: '100', }), expect.objectContaining({ id: inProgressContribution.data.createContribution.id, @@ -1528,24 +1458,6 @@ describe('ContributionResolver', () => { memo: 'Test IN_PROGRESS contribution', amount: '100', }), - expect.not.objectContaining({ - id: bibiCreatedContribution.id, - state: 'CONFIRMED', - memo: 'Herzlich Willkommen bei Gradido!', - amount: '1000', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'CONFIRMED', - memo: 'Whatever contribution', - amount: '166', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'DENIED', - memo: 'Whatever contribution', - amount: '166', - }), ]), }) }) @@ -1565,54 +1477,30 @@ describe('ContributionResolver', () => { expect(contributionListObject).toMatchObject({ contributionCount: 2, contributionList: expect.arrayContaining([ - expect.not.objectContaining({ - amount: '100', - state: 'CONFIRMED', - id: contributionToConfirm.data.createContribution.id, - memo: 'Test contribution to confirm', - }), - expect.not.objectContaining({ - id: pendingContribution.data.createContribution.id, - state: 'PENDING', - memo: 'Test PENDING contribution update', - amount: '10', - }), expect.objectContaining({ id: contributionToDeny.data.createContribution.id, state: 'DENIED', memo: 'Test contribution to deny', amount: '100', }), - expect.not.objectContaining({ - id: contributionToDelete.data.createContribution.id, - state: 'DELETED', - memo: 'Test contribution to delete', - amount: '100', - }), - expect.not.objectContaining({ - id: inProgressContribution.data.createContribution.id, - state: 'IN_PROGRESS', - memo: 'Test IN_PROGRESS contribution', - amount: '100', - }), - expect.not.objectContaining({ - id: bibiCreatedContribution.id, - state: 'CONFIRMED', - memo: 'Herzlich Willkommen bei Gradido!', - amount: '1000', - }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'CONFIRMED', - memo: 'Whatever contribution', - amount: '166', - }), expect.objectContaining({ id: expect.any(Number), state: 'DENIED', memo: 'Whatever contribution', amount: '166', }), + expect.not.objectContaining({ + state: 'CONFIRMED', + }), + expect.not.objectContaining({ + state: 'DELETED', + }), + expect.not.objectContaining({ + state: 'IN_PROGRESS', + }), + expect.not.objectContaining({ + state: 'PENDING', + }), ]), }) }) @@ -1662,42 +1550,27 @@ describe('ContributionResolver', () => { memo: 'Test PENDING contribution update', amount: '10', }), - expect.not.objectContaining({ - id: contributionToDeny.data.createContribution.id, - state: 'DENIED', - memo: 'Test contribution to deny', - amount: '100', - }), - expect.not.objectContaining({ - id: contributionToDelete.data.createContribution.id, - state: 'DELETED', - memo: 'Test contribution to delete', - amount: '100', - }), - expect.not.objectContaining({ - id: inProgressContribution.data.createContribution.id, - state: 'IN_PROGRESS', - memo: 'Test IN_PROGRESS contribution', - amount: '100', - }), expect.objectContaining({ id: bibiCreatedContribution.id, state: 'CONFIRMED', memo: 'Herzlich Willkommen bei Gradido!', amount: '1000', }), - expect.not.objectContaining({ - id: expect.any(Number), - state: 'DENIED', - memo: 'Whatever contribution', - amount: '166', - }), expect.objectContaining({ id: expect.any(Number), state: 'CONFIRMED', memo: 'Whatever contribution', amount: '166', }), + expect.not.objectContaining({ + state: 'DENIED', + }), + expect.not.objectContaining({ + state: 'DELETED', + }), + expect.not.objectContaining({ + state: 'IN_PROGRESS', + }), ]), }) }) From a615c741c28bdd49c25295af237b58ec1d04f612 Mon Sep 17 00:00:00 2001 From: elweyn Date: Tue, 14 Feb 2023 07:55:38 +0100 Subject: [PATCH 23/70] Add tests that EVENT is stored in DB and has right values in userId. --- .../src/graphql/resolver/ContributionResolver.test.ts | 9 +++++++++ backend/src/graphql/resolver/ContributionResolver.ts | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index fe98c0f6f..e8cdad748 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -815,6 +815,15 @@ describe('ContributionResolver', () => { }) expect(isDenied).toBeTruthy() }) + + it('stores the admin deny contribution event in the database', async () => { + await expect(EventProtocol.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventProtocolType.ADMIN_CONTRIBUTION_DENY, + userId: admin.id, + }), + ) + }) }) }) }) diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 926742d8a..a18be7c38 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -732,7 +732,7 @@ export class ContributionResolver { const event = new Event() const eventAdminContributionDeny = new EventAdminContributionDeny() - eventAdminContributionDeny.userId = contributionToUpdate.userId + eventAdminContributionDeny.userId = moderator.id eventAdminContributionDeny.amount = contributionToUpdate.amount eventAdminContributionDeny.contributionId = contributionToUpdate.id await writeEvent(event.setEventAdminContributionDeny(eventAdminContributionDeny)) From 9ff045fdbfe7094a5e78dd6ab59a32307f66a50d Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Tue, 14 Feb 2023 20:31:56 +0100 Subject: [PATCH 24/70] test for authentication on TransactionLinkResolver --- .../resolver/TransactionLinkResolver.test.ts | 660 +++++++++--------- 1 file changed, 346 insertions(+), 314 deletions(-) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts index f60ab45d0..09f2f9a02 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts @@ -53,65 +53,81 @@ afterAll(async () => { describe('TransactionLinkResolver', () => { describe('createTransactionLink', () => { - beforeAll(async () => { - await mutate({ - mutation: login, - variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + describe('unauthenticated', () => { + it('throws an error', async () => { + jest.clearAllMocks() + resetToken() + await expect( + mutate({ mutation: createTransactionLink, variables: { amount: 0, memo: 'Test' } }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) }) }) - it('throws error when amount is zero', async () => { - jest.clearAllMocks() - await expect( - mutate({ - mutation: createTransactionLink, - variables: { - amount: 0, - memo: 'Test', - }, - }), - ).resolves.toMatchObject({ - errors: [new GraphQLError('Amount must be a positive number')], + describe('authenticated', () => { + beforeAll(async () => { + await mutate({ + mutation: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) }) - }) - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(0)) - }) - it('throws error when amount is negative', async () => { - jest.clearAllMocks() - await expect( - mutate({ - mutation: createTransactionLink, - variables: { - amount: -10, - memo: 'Test', - }, - }), - ).resolves.toMatchObject({ - errors: [new GraphQLError('Amount must be a positive number')], + it('throws error when amount is zero', async () => { + jest.clearAllMocks() + await expect( + mutate({ + mutation: createTransactionLink, + variables: { + amount: 0, + memo: 'Test', + }, + }), + ).resolves.toMatchObject({ + errors: [new GraphQLError('Amount must be a positive number')], + }) + }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(0)) }) - }) - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(-10)) - }) - it('throws error when user has not enough GDD', async () => { - jest.clearAllMocks() - await expect( - mutate({ - mutation: createTransactionLink, - variables: { - amount: 1001, - memo: 'Test', - }, - }), - ).resolves.toMatchObject({ - errors: [new GraphQLError('User has not enough GDD')], + it('throws error when amount is negative', async () => { + jest.clearAllMocks() + await expect( + mutate({ + mutation: createTransactionLink, + variables: { + amount: -10, + memo: 'Test', + }, + }), + ).resolves.toMatchObject({ + errors: [new GraphQLError('Amount must be a positive number')], + }) + }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(-10)) + }) + + it('throws error when user has not enough GDD', async () => { + jest.clearAllMocks() + await expect( + mutate({ + mutation: createTransactionLink, + variables: { + amount: 1001, + memo: 'Test', + }, + }), + ).resolves.toMatchObject({ + errors: [new GraphQLError('User has not enough GDD')], + }) + }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('User has not enough GDD', expect.any(Number)) }) - }) - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('User has not enough GDD', expect.any(Number)) }) }) @@ -121,236 +137,37 @@ describe('TransactionLinkResolver', () => { resetToken() }) - describe('contributionLink', () => { - describe('input not valid', () => { - beforeAll(async () => { - await mutate({ - mutation: login, - variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, - }) - }) - - it('throws error when link does not exists', async () => { - jest.clearAllMocks() - await expect( - mutate({ - mutation: redeemTransactionLink, - variables: { - code: 'CL-123456', - }, - }), - ).resolves.toMatchObject({ - errors: [new GraphQLError('Creation from contribution link was not successful')], - }) - }) - - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( - 'No contribution link found to given code', - 'CL-123456', - ) - expect(logger.error).toBeCalledWith( - 'Creation from contribution link was not successful', - new Error('No contribution link found to given code'), - ) - }) - - const now = new Date() - const validFrom = new Date(now.getFullYear() + 1, 0, 1) - - it('throws error when link is not valid yet', async () => { - jest.clearAllMocks() - const { - data: { createContributionLink: contributionLink }, - } = await mutate({ - mutation: createContributionLink, - variables: { - amount: new Decimal(5), - name: 'Daily Contribution Link', - memo: 'Thank you for contribute daily to the community', - cycle: 'DAILY', - validFrom: validFrom.toISOString(), - validTo: new Date(now.getFullYear() + 1, 11, 31, 23, 59, 59, 999).toISOString(), - maxAmountPerMonth: new Decimal(200), - maxPerCycle: 1, - }, - }) - await expect( - mutate({ - mutation: redeemTransactionLink, - variables: { - code: 'CL-' + contributionLink.code, - }, - }), - ).resolves.toMatchObject({ - errors: [new GraphQLError('Creation from contribution link was not successful')], - }) - await resetEntity(DbContributionLink) - }) - - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('Contribution link is not valid yet', validFrom) - expect(logger.error).toBeCalledWith( - 'Creation from contribution link was not successful', - new Error('Contribution link is not valid yet'), - ) - }) - - it('throws error when contributionLink cycle is invalid', async () => { - jest.clearAllMocks() - const now = new Date() - const { - data: { createContributionLink: contributionLink }, - } = await mutate({ - mutation: createContributionLink, - variables: { - amount: new Decimal(5), - name: 'Daily Contribution Link', - memo: 'Thank you for contribute daily to the community', - cycle: 'INVALID', - validFrom: new Date(now.getFullYear(), 0, 1).toISOString(), - validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(), - maxAmountPerMonth: new Decimal(200), - maxPerCycle: 1, - }, - }) - await expect( - mutate({ - mutation: redeemTransactionLink, - variables: { - code: 'CL-' + contributionLink.code, - }, - }), - ).resolves.toMatchObject({ - errors: [new GraphQLError('Creation from contribution link was not successful')], - }) - await resetEntity(DbContributionLink) - }) - - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('Contribution link has unknown cycle', 'INVALID') - expect(logger.error).toBeCalledWith( - 'Creation from contribution link was not successful', - new Error('Contribution link has unknown cycle'), - ) - }) - - const validTo = new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 0) - it('throws error when link is no longer valid', async () => { - jest.clearAllMocks() - const { - data: { createContributionLink: contributionLink }, - } = await mutate({ - mutation: createContributionLink, - variables: { - amount: new Decimal(5), - name: 'Daily Contribution Link', - memo: 'Thank you for contribute daily to the community', - cycle: 'DAILY', - validFrom: new Date(now.getFullYear() - 1, 0, 1).toISOString(), - validTo: validTo.toISOString(), - maxAmountPerMonth: new Decimal(200), - maxPerCycle: 1, - }, - }) - await expect( - mutate({ - mutation: redeemTransactionLink, - variables: { - code: 'CL-' + contributionLink.code, - }, - }), - ).resolves.toMatchObject({ - errors: [new GraphQLError('Creation from contribution link was not successful')], - }) - await resetEntity(DbContributionLink) - }) - - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('Contribution link is no longer valid', validTo) - expect(logger.error).toBeCalledWith( - 'Creation from contribution link was not successful', - new Error('Contribution link is no longer valid'), - ) - }) + describe('unauthenticated', () => { + it('throws an error', async () => { + jest.clearAllMocks() + resetToken() + await expect( + mutate({ mutation: redeemTransactionLink, variables: { code: 'CL-123456' } }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) }) + }) - // TODO: have this test separated into a transactionLink and a contributionLink part - describe('redeem daily Contribution Link', () => { - const now = new Date() - let contributionLink: DbContributionLink | undefined - let contribution: UnconfirmedContribution | undefined - - beforeAll(async () => { - await mutate({ - mutation: login, - variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, - }) - await mutate({ - mutation: createContributionLink, - variables: { - amount: new Decimal(5), - name: 'Daily Contribution Link', - memo: 'Thank you for contribute daily to the community', - cycle: 'DAILY', - validFrom: new Date(now.getFullYear(), 0, 1).toISOString(), - validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(), - maxAmountPerMonth: new Decimal(200), - maxPerCycle: 1, - }, - }) - }) - - it('has a daily contribution link in the database', async () => { - const cls = await DbContributionLink.find() - expect(cls).toHaveLength(1) - contributionLink = cls[0] - expect(contributionLink).toEqual( - expect.objectContaining({ - id: expect.any(Number), - name: 'Daily Contribution Link', - memo: 'Thank you for contribute daily to the community', - validFrom: new Date(now.getFullYear(), 0, 1), - validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 0), - cycle: 'DAILY', - maxPerCycle: 1, - totalMaxCountOfContribution: null, - maxAccountBalance: null, - minGapHours: null, - createdAt: expect.any(Date), - deletedAt: null, - code: expect.stringMatching(/^[0-9a-f]{24,24}$/), - linkEnabled: true, - amount: expect.decimalEqual(5), - maxAmountPerMonth: expect.decimalEqual(200), - }), - ) - }) - - describe('user has pending contribution of 1000 GDD', () => { + describe('authenticated', () => { + describe('contributionLink', () => { + describe('input not valid', () => { beforeAll(async () => { await mutate({ mutation: login, - variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, }) - const result = await mutate({ - mutation: createContribution, - variables: { - amount: new Decimal(1000), - memo: 'I was brewing potions for the community the whole month', - creationDate: now.toISOString(), - }, - }) - contribution = result.data.createContribution }) - it('does not allow the user to redeem the contribution link', async () => { + it('throws error when link does not exists', async () => { jest.clearAllMocks() await expect( mutate({ mutation: redeemTransactionLink, variables: { - code: 'CL-' + (contributionLink ? contributionLink.code : ''), + code: 'CL-123456', }, }), ).resolves.toMatchObject({ @@ -359,85 +176,247 @@ describe('TransactionLinkResolver', () => { }) it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'No contribution link found to given code', + 'CL-123456', + ) expect(logger.error).toBeCalledWith( 'Creation from contribution link was not successful', - new Error( - 'The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.', - ), + new Error('No contribution link found to given code'), ) }) - }) - describe('user has no pending contributions that would not allow to redeem the link', () => { - beforeAll(async () => { - await mutate({ - mutation: login, - variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, - }) - await mutate({ - mutation: updateContribution, - variables: { - contributionId: contribution ? contribution.id : -1, - amount: new Decimal(800), - memo: 'I was brewing potions for the community the whole month', - creationDate: now.toISOString(), - }, - }) - }) + const now = new Date() + const validFrom = new Date(now.getFullYear() + 1, 0, 1) - it('allows the user to redeem the contribution link', async () => { - await expect( - mutate({ - mutation: redeemTransactionLink, - variables: { - code: 'CL-' + (contributionLink ? contributionLink.code : ''), - }, - }), - ).resolves.toMatchObject({ - data: { - redeemTransactionLink: true, - }, - errors: undefined, - }) - }) - - it('does not allow the user to redeem the contribution link a second time on the same day', async () => { + it('throws error when link is not valid yet', async () => { jest.clearAllMocks() + const { + data: { createContributionLink: contributionLink }, + } = await mutate({ + mutation: createContributionLink, + variables: { + amount: new Decimal(5), + name: 'Daily Contribution Link', + memo: 'Thank you for contribute daily to the community', + cycle: 'DAILY', + validFrom: validFrom.toISOString(), + validTo: new Date(now.getFullYear() + 1, 11, 31, 23, 59, 59, 999).toISOString(), + maxAmountPerMonth: new Decimal(200), + maxPerCycle: 1, + }, + }) await expect( mutate({ mutation: redeemTransactionLink, variables: { - code: 'CL-' + (contributionLink ? contributionLink.code : ''), + code: 'CL-' + contributionLink.code, }, }), ).resolves.toMatchObject({ errors: [new GraphQLError('Creation from contribution link was not successful')], }) + await resetEntity(DbContributionLink) }) it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Contribution link is not valid yet', validFrom) expect(logger.error).toBeCalledWith( 'Creation from contribution link was not successful', - new Error('Contribution link already redeemed today'), + new Error('Contribution link is not valid yet'), ) }) - describe('after one day', () => { + it('throws error when contributionLink cycle is invalid', async () => { + jest.clearAllMocks() + const now = new Date() + const { + data: { createContributionLink: contributionLink }, + } = await mutate({ + mutation: createContributionLink, + variables: { + amount: new Decimal(5), + name: 'Daily Contribution Link', + memo: 'Thank you for contribute daily to the community', + cycle: 'INVALID', + validFrom: new Date(now.getFullYear(), 0, 1).toISOString(), + validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(), + maxAmountPerMonth: new Decimal(200), + maxPerCycle: 1, + }, + }) + await expect( + mutate({ + mutation: redeemTransactionLink, + variables: { + code: 'CL-' + contributionLink.code, + }, + }), + ).resolves.toMatchObject({ + errors: [new GraphQLError('Creation from contribution link was not successful')], + }) + await resetEntity(DbContributionLink) + }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Contribution link has unknown cycle', 'INVALID') + expect(logger.error).toBeCalledWith( + 'Creation from contribution link was not successful', + new Error('Contribution link has unknown cycle'), + ) + }) + + const validTo = new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 0) + it('throws error when link is no longer valid', async () => { + jest.clearAllMocks() + const { + data: { createContributionLink: contributionLink }, + } = await mutate({ + mutation: createContributionLink, + variables: { + amount: new Decimal(5), + name: 'Daily Contribution Link', + memo: 'Thank you for contribute daily to the community', + cycle: 'DAILY', + validFrom: new Date(now.getFullYear() - 1, 0, 1).toISOString(), + validTo: validTo.toISOString(), + maxAmountPerMonth: new Decimal(200), + maxPerCycle: 1, + }, + }) + await expect( + mutate({ + mutation: redeemTransactionLink, + variables: { + code: 'CL-' + contributionLink.code, + }, + }), + ).resolves.toMatchObject({ + errors: [new GraphQLError('Creation from contribution link was not successful')], + }) + await resetEntity(DbContributionLink) + }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Contribution link is no longer valid', validTo) + expect(logger.error).toBeCalledWith( + 'Creation from contribution link was not successful', + new Error('Contribution link is no longer valid'), + ) + }) + }) + + // TODO: have this test separated into a transactionLink and a contributionLink part + describe('redeem daily Contribution Link', () => { + const now = new Date() + let contributionLink: DbContributionLink | undefined + let contribution: UnconfirmedContribution | undefined + + beforeAll(async () => { + await mutate({ + mutation: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + await mutate({ + mutation: createContributionLink, + variables: { + amount: new Decimal(5), + name: 'Daily Contribution Link', + memo: 'Thank you for contribute daily to the community', + cycle: 'DAILY', + validFrom: new Date(now.getFullYear(), 0, 1).toISOString(), + validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(), + maxAmountPerMonth: new Decimal(200), + maxPerCycle: 1, + }, + }) + }) + + it('has a daily contribution link in the database', async () => { + const cls = await DbContributionLink.find() + expect(cls).toHaveLength(1) + contributionLink = cls[0] + expect(contributionLink).toEqual( + expect.objectContaining({ + id: expect.any(Number), + name: 'Daily Contribution Link', + memo: 'Thank you for contribute daily to the community', + validFrom: new Date(now.getFullYear(), 0, 1), + validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 0), + cycle: 'DAILY', + maxPerCycle: 1, + totalMaxCountOfContribution: null, + maxAccountBalance: null, + minGapHours: null, + createdAt: expect.any(Date), + deletedAt: null, + code: expect.stringMatching(/^[0-9a-f]{24,24}$/), + linkEnabled: true, + amount: expect.decimalEqual(5), + maxAmountPerMonth: expect.decimalEqual(200), + }), + ) + }) + + describe('user has pending contribution of 1000 GDD', () => { beforeAll(async () => { - jest.useFakeTimers() - setTimeout(jest.fn(), 1000 * 60 * 60 * 24) - jest.runAllTimers() await mutate({ mutation: login, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, }) + const result = await mutate({ + mutation: createContribution, + variables: { + amount: new Decimal(1000), + memo: 'I was brewing potions for the community the whole month', + creationDate: now.toISOString(), + }, + }) + contribution = result.data.createContribution }) - afterAll(() => { - jest.useRealTimers() + it('does not allow the user to redeem the contribution link', async () => { + jest.clearAllMocks() + await expect( + mutate({ + mutation: redeemTransactionLink, + variables: { + code: 'CL-' + (contributionLink ? contributionLink.code : ''), + }, + }), + ).resolves.toMatchObject({ + errors: [new GraphQLError('Creation from contribution link was not successful')], + }) }) - it('allows the user to redeem the contribution link again', async () => { + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'Creation from contribution link was not successful', + new Error( + 'The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.', + ), + ) + }) + }) + + describe('user has no pending contributions that would not allow to redeem the link', () => { + beforeAll(async () => { + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + await mutate({ + mutation: updateContribution, + variables: { + contributionId: contribution ? contribution.id : -1, + amount: new Decimal(800), + memo: 'I was brewing potions for the community the whole month', + creationDate: now.toISOString(), + }, + }) + }) + + it('allows the user to redeem the contribution link', async () => { await expect( mutate({ mutation: redeemTransactionLink, @@ -473,6 +452,59 @@ describe('TransactionLinkResolver', () => { new Error('Contribution link already redeemed today'), ) }) + + describe('after one day', () => { + beforeAll(async () => { + jest.useFakeTimers() + setTimeout(jest.fn(), 1000 * 60 * 60 * 24) + jest.runAllTimers() + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + afterAll(() => { + jest.useRealTimers() + }) + + it('allows the user to redeem the contribution link again', async () => { + await expect( + mutate({ + mutation: redeemTransactionLink, + variables: { + code: 'CL-' + (contributionLink ? contributionLink.code : ''), + }, + }), + ).resolves.toMatchObject({ + data: { + redeemTransactionLink: true, + }, + errors: undefined, + }) + }) + + it('does not allow the user to redeem the contribution link a second time on the same day', async () => { + jest.clearAllMocks() + await expect( + mutate({ + mutation: redeemTransactionLink, + variables: { + code: 'CL-' + (contributionLink ? contributionLink.code : ''), + }, + }), + ).resolves.toMatchObject({ + errors: [new GraphQLError('Creation from contribution link was not successful')], + }) + }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'Creation from contribution link was not successful', + new Error('Contribution link already redeemed today'), + ) + }) + }) }) }) }) From 34fe6b4e90a5fc6233105f1b56239459bb5e1a00 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 15 Feb 2023 09:44:00 +0100 Subject: [PATCH 25/70] Rework reviews, add the properties used in the ui for listContributions. --- .../resolver/ContributionResolver.test.ts | 61 +++++++++++-------- backend/src/seeds/graphql/queries.ts | 9 +++ 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 6031bac30..a467cd9c9 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -813,14 +813,15 @@ describe('ContributionResolver', () => { id: contributionToDeny.data.createContribution.id, }, }) - expect(isDenied).toBeTruthy() + expect(isDenied).toBe(true) }) it('stores the admin deny contribution event in the database', async () => { await expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.ADMIN_CONTRIBUTION_DENY, - userId: admin.id, + userId: bibi.id, + xUserId: admin.id, }), ) }) @@ -925,7 +926,7 @@ describe('ContributionResolver', () => { id: contributionToDelete.data.createContribution.id, }, }) - expect(isDenied).toBeTruthy() + expect(isDenied).toBe(true) }) it('stores the CONTRIBUTION_DELETE event in the database', async () => { @@ -1022,38 +1023,39 @@ describe('ContributionResolver', () => { expect(contributionListResult).toMatchObject({ contributionCount: 6, contributionList: expect.arrayContaining([ - { + expect.objectContaining({ amount: '100', id: contributionToConfirm.data.createContribution.id, memo: 'Test contribution to confirm', - }, - { + }), + expect.objectContaining({ id: pendingContribution.data.createContribution.id, memo: 'Test PENDING contribution update', amount: '10', - }, - { + }), + expect.objectContaining({ id: contributionToDeny.data.createContribution.id, memo: 'Test contribution to deny', amount: '100', - }, - { + }), + expect.objectContaining({ id: contributionToDelete.data.createContribution.id, memo: 'Test contribution to delete', amount: '100', - }, - { + }), + expect.objectContaining({ id: inProgressContribution.data.createContribution.id, memo: 'Test IN_PROGRESS contribution', amount: '100', - }, - { + }), + expect.objectContaining({ id: bibiCreatedContribution.id, memo: 'Herzlich Willkommen bei Gradido!', amount: '1000', - }, + }), ]), }) + expect(contributionListResult.contributionList).toHaveLength(6) }) }) @@ -1074,37 +1076,35 @@ describe('ContributionResolver', () => { contributionCount: 4, contributionList: expect.arrayContaining([ expect.not.objectContaining({ - amount: '100', - id: contributionToConfirm.data.createContribution.id, - memo: 'Test contribution to confirm', + state: 'CONFIRMED', }), expect.objectContaining({ id: pendingContribution.data.createContribution.id, + state: 'PENDING', memo: 'Test PENDING contribution update', amount: '10', }), expect.objectContaining({ id: contributionToDeny.data.createContribution.id, + state: 'DENIED', memo: 'Test contribution to deny', amount: '100', }), expect.objectContaining({ id: contributionToDelete.data.createContribution.id, + state: 'DELETED', memo: 'Test contribution to delete', amount: '100', }), expect.objectContaining({ id: inProgressContribution.data.createContribution.id, + state: 'IN_PROGRESS', memo: 'Test IN_PROGRESS contribution', amount: '100', }), - expect.not.objectContaining({ - id: bibiCreatedContribution.id, - memo: 'Herzlich Willkommen bei Gradido!', - amount: '1000', - }), ]), }) + expect(contributionListResult.contributionList).toHaveLength(4) }) }) }) @@ -1253,6 +1253,7 @@ describe('ContributionResolver', () => { }), ]), }) + expect(contributionListObject.contributionList).toHaveLength(7) }) it('returns all contributions for statusFilter = null', async () => { @@ -1317,6 +1318,7 @@ describe('ContributionResolver', () => { }), ]), }) + expect(contributionListObject.contributionList).toHaveLength(7) }) it('returns all contributions for statusFilter = []', async () => { @@ -1381,6 +1383,7 @@ describe('ContributionResolver', () => { }), ]), }) + expect(contributionListObject.contributionList).toHaveLength(7) }) it('returns all CONFIRMED contributions', async () => { @@ -1430,6 +1433,7 @@ describe('ContributionResolver', () => { }), ]), }) + expect(contributionListObject.contributionList).toHaveLength(3) }) it('returns all PENDING contributions', async () => { @@ -1467,6 +1471,7 @@ describe('ContributionResolver', () => { }), ]), }) + expect(contributionListObject.contributionList).toHaveLength(1) }) it('returns all IN_PROGRESS Creation', async () => { @@ -1504,6 +1509,7 @@ describe('ContributionResolver', () => { }), ]), }) + expect(contributionListObject.contributionList).toHaveLength(1) }) it('returns all DENIED Creation', async () => { @@ -1547,6 +1553,7 @@ describe('ContributionResolver', () => { }), ]), }) + expect(contributionListObject.contributionList).toHaveLength(2) }) it('does not return any DELETED Creation', async () => { @@ -1565,6 +1572,7 @@ describe('ContributionResolver', () => { contributionCount: 0, contributionList: [], }) + expect(contributionListObject.contributionList).toHaveLength(0) }) it('returns all CONFIRMED and PENDING Creation', async () => { @@ -1617,6 +1625,7 @@ describe('ContributionResolver', () => { }), ]), }) + expect(contributionListObject.contributionList).toHaveLength(4) }) }) }) @@ -2336,7 +2345,7 @@ describe('ContributionResolver', () => { await expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE, - userId: admin.id, + userId: bibi.id, }), ) }) @@ -2376,7 +2385,7 @@ describe('ContributionResolver', () => { await expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE, - userId: admin.id, + userId: bibi.id, }), ) }) @@ -2554,7 +2563,7 @@ describe('ContributionResolver', () => { await expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.ADMIN_CONTRIBUTION_DELETE, - userId: admin.id, + userId: bibi.id, }), ) }) diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 385a69479..3469c200d 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -166,6 +166,15 @@ export const listContributions = gql` id amount memo + createdAt + contributionDate + confirmedAt + confirmedBy + deletedAt + state + messagesCount + deniedAt + deniedBy } } } From d32b520145e35478270e8c4bbeddadcc832cff55 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 15 Feb 2023 10:00:30 +0100 Subject: [PATCH 26/70] Change test description to better naming. --- backend/src/graphql/resolver/ContributionResolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index a467cd9c9..1e7b451e1 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -816,7 +816,7 @@ describe('ContributionResolver', () => { expect(isDenied).toBe(true) }) - it('stores the admin deny contribution event in the database', async () => { + it('stores the ADMIN_CONTRIBUTION_DENY event in the database', async () => { await expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.ADMIN_CONTRIBUTION_DENY, From 11f19262b00c4cfaea393f68f654d21f68d05132 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 15 Feb 2023 11:40:02 +0100 Subject: [PATCH 27/70] Add expected data to Protocols tests. --- .../graphql/resolver/ContributionResolver.test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 1e7b451e1..e41c416ff 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -822,6 +822,8 @@ describe('ContributionResolver', () => { type: EventProtocolType.ADMIN_CONTRIBUTION_DENY, userId: bibi.id, xUserId: admin.id, + contributionId: contributionToDeny.data.createContribution.id, + amount: expect.decimalEqual(100), }), ) }) @@ -2080,6 +2082,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ type: EventProtocolType.ADMIN_CONTRIBUTION_CREATE, userId: admin.id, + amount: expect.decimalEqual(200), }), ) }) @@ -2345,7 +2348,8 @@ describe('ContributionResolver', () => { await expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE, - userId: bibi.id, + userId: admin.id, + amount: 300, }), ) }) @@ -2385,7 +2389,8 @@ describe('ContributionResolver', () => { await expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE, - userId: bibi.id, + userId: admin.id, + amount: expect.decimalEqual(200), }), ) }) @@ -2563,7 +2568,8 @@ describe('ContributionResolver', () => { await expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.ADMIN_CONTRIBUTION_DELETE, - userId: bibi.id, + userId: admin.id, + amount: expect.decimalEqual(400), }), ) }) From 00a45d780517ba7c61a8750f3340f676aee86ca3 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 15 Feb 2023 12:17:17 +0100 Subject: [PATCH 28/70] Corrected the expected amount from the ADMIN_CONTRIBUTION_DELETE event. --- backend/src/graphql/resolver/ContributionResolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index e41c416ff..93ddb7c83 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -2569,7 +2569,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ type: EventProtocolType.ADMIN_CONTRIBUTION_DELETE, userId: admin.id, - amount: expect.decimalEqual(400), + amount: expect.decimalEqual(200), }), ) }) From b9c46d223849298deff72bbbef7313e6ebbab8cb Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 15 Feb 2023 12:33:13 +0100 Subject: [PATCH 29/70] Correct spelling error. --- backend/src/graphql/resolver/ContributionResolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 93ddb7c83..ce37449df 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -505,7 +505,7 @@ describe('ContributionResolver', () => { }) }) - describe('update to much so that the limit is exceeded', () => { + describe('update too much so that the limit is exceeded', () => { beforeAll(async () => { await mutate({ mutation: login, From de83f241d8a430636fd5bf769e9ccc50d742a9ea Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Thu, 16 Feb 2023 14:22:56 +0100 Subject: [PATCH 30/70] Update backend/src/graphql/resolver/TransactionLinkResolver.test.ts Co-authored-by: Hannes Heine --- backend/src/graphql/resolver/TransactionLinkResolver.test.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts index c77a0bf64..2b0950a33 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts @@ -359,11 +359,6 @@ describe('TransactionLinkResolver', () => { }) it('logs the error thrown', () => { - /* expect(logger.error).toBeCalledWith( - 'The amount to be created exceeds the amount still available for this month', - new Decimal(5), - new Decimal(0), - ) */ expect(logger.error).toBeCalledWith( 'Creation from contribution link was not successful', new Error( From 7766187d296611945ad8798f82282be6217c8f09 Mon Sep 17 00:00:00 2001 From: mahula Date: Thu, 16 Feb 2023 15:06:25 +0100 Subject: [PATCH 31/70] remove docker dependendies from locales jobs --- .github/workflows/test.yml | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7819a0703..5cab713ad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -163,7 +163,6 @@ jobs: locales_frontend: name: Locales - Frontend runs-on: ubuntu-latest - needs: [build_test_frontend] steps: ########################################################################## # CHECKOUT CODE ########################################################## @@ -171,20 +170,10 @@ jobs: - name: Checkout code uses: actions/checkout@v3 ########################################################################## - # DOWNLOAD DOCKER IMAGE ################################################## - ########################################################################## - - name: Download Docker Image (Frontend) - uses: actions/download-artifact@v3 - with: - name: docker-frontend-test - path: /tmp - - name: Load Docker Image - run: docker load < /tmp/frontend.tar - ########################################################################## # LOCALES FRONTEND ####################################################### ########################################################################## - name: Frontend | Locales - run: docker run --rm gradido/frontend:test yarn run locales + run: cd frontend && yarn && yarn run locales ############################################################################## # JOB: LINT FRONTEND ######################################################### @@ -308,7 +297,6 @@ jobs: locales_admin: name: Locales - Admin Interface runs-on: ubuntu-latest - needs: [build_test_admin] steps: ########################################################################## # CHECKOUT CODE ########################################################## @@ -316,20 +304,10 @@ jobs: - name: Checkout code uses: actions/checkout@v3 ########################################################################## - # DOWNLOAD DOCKER IMAGE ################################################## - ########################################################################## - - name: Download Docker Image (Admin Interface) - uses: actions/download-artifact@v3 - with: - name: docker-admin-test - path: /tmp - - name: Load Docker Image - run: docker load < /tmp/admin.tar - ########################################################################## # LOCALES FRONTEND ####################################################### ########################################################################## - - name: admin | Locales - run: docker run --rm gradido/admin:test yarn run locales + - name: Admin | Locales + run: cd admin && yarn && yarn run locales ############################################################################## # JOB: LINT BACKEND ########################################################## @@ -366,7 +344,6 @@ jobs: locales_backend: name: Locales - Backend runs-on: ubuntu-latest - needs: [build_test_backend] steps: ########################################################################## # CHECKOUT CODE ########################################################## From f12fea24330fecd52a7011bcf78c76b9e96fe4bf Mon Sep 17 00:00:00 2001 From: mahula Date: Thu, 16 Feb 2023 15:15:25 +0100 Subject: [PATCH 32/70] remove docker dependendies from linting jobs --- .github/workflows/test.yml | 82 ++++---------------------------------- 1 file changed, 8 insertions(+), 74 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5cab713ad..9a3895093 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -181,7 +181,6 @@ jobs: lint_frontend: name: Lint - Frontend runs-on: ubuntu-latest - needs: [build_test_frontend] steps: ########################################################################## # CHECKOUT CODE ########################################################## @@ -189,20 +188,10 @@ jobs: - name: Checkout code uses: actions/checkout@v3 ########################################################################## - # DOWNLOAD DOCKER IMAGE ################################################## - ########################################################################## - - name: Download Docker Image (Frontend) - uses: actions/download-artifact@v3 - with: - name: docker-frontend-test - path: /tmp - - name: Load Docker Image - run: docker load < /tmp/frontend.tar - ########################################################################## # LINT FRONTEND ########################################################## ########################################################################## - name: Frontend | Lint - run: docker run --rm gradido/frontend:test yarn run lint + run: cd frontend && yarn && yarn run lint ############################################################################## # JOB: STYLELINT FRONTEND #################################################### @@ -210,7 +199,6 @@ jobs: stylelint_frontend: name: Stylelint - Frontend runs-on: ubuntu-latest - needs: [build_test_frontend] steps: ########################################################################## # CHECKOUT CODE ########################################################## @@ -218,20 +206,10 @@ jobs: - name: Checkout code uses: actions/checkout@v3 ########################################################################## - # DOWNLOAD DOCKER IMAGE ################################################## - ########################################################################## - - name: Download Docker Image (Frontend) - uses: actions/download-artifact@v3 - with: - name: docker-frontend-test - path: /tmp - - name: Load Docker Image - run: docker load < /tmp/frontend.tar - ########################################################################## # STYLELINT FRONTEND ##################################################### ########################################################################## - name: Frontend | Stylelint - run: docker run --rm gradido/frontend:test yarn run stylelint + run: cd frontend && yarn && yarn run stylelint ############################################################################## # JOB: LINT ADMIN INTERFACE ################################################## @@ -239,7 +217,6 @@ jobs: lint_admin: name: Lint - Admin Interface runs-on: ubuntu-latest - needs: [build_test_admin] steps: ########################################################################## # CHECKOUT CODE ########################################################## @@ -247,28 +224,17 @@ jobs: - name: Checkout code uses: actions/checkout@v3 ########################################################################## - # DOWNLOAD DOCKER IMAGE ################################################## - ########################################################################## - - name: Download Docker Image (Admin Interface) - uses: actions/download-artifact@v3 - with: - name: docker-admin-test - path: /tmp - - name: Load Docker Image - run: docker load < /tmp/admin.tar - ########################################################################## # LINT ADMIN INTERFACE ################################################### ########################################################################## - name: Admin Interface | Lint - run: docker run --rm gradido/admin:test yarn run lint + run: cd admin && yarn && yarn run lint ############################################################################## - # JOB: STYLELINT ADMIN INTERFACE ############################################## + # JOB: STYLELINT ADMIN INTERFACE ############################################# ############################################################################## stylelint_admin: name: Stylelint - Admin Interface runs-on: ubuntu-latest - needs: [build_test_admin] steps: ########################################################################## # CHECKOUT CODE ########################################################## @@ -276,20 +242,10 @@ jobs: - name: Checkout code uses: actions/checkout@v3 ########################################################################## - # DOWNLOAD DOCKER IMAGE ################################################## - ########################################################################## - - name: Download Docker Image (Admin Interface) - uses: actions/download-artifact@v3 - with: - name: docker-admin-test - path: /tmp - - name: Load Docker Image - run: docker load < /tmp/admin.tar - ########################################################################## # STYLELINT ADMIN INTERFACE ############################################## ########################################################################## - name: Admin Interface | Stylelint - run: docker run --rm gradido/admin:test yarn run stylelint + run: cd admin && yarn && yarn run stylelint ############################################################################## # JOB: LOCALES ADMIN ######################################################### @@ -315,7 +271,6 @@ jobs: lint_backend: name: Lint - Backend runs-on: ubuntu-latest - needs: [build_test_backend] steps: ########################################################################## # CHECKOUT CODE ########################################################## @@ -323,20 +278,10 @@ jobs: - name: Checkout code uses: actions/checkout@v3 ########################################################################## - # DOWNLOAD DOCKER IMAGE ################################################## - ########################################################################## - - name: Download Docker Image (Backend) - uses: actions/download-artifact@v3 - with: - name: docker-backend-test - path: /tmp - - name: Load Docker Image - run: docker load < /tmp/backend.tar - ########################################################################## # LINT BACKEND ########################################################### ########################################################################## - name: backend | Lint - run: docker run --rm gradido/backend:test yarn run lint + run: cd backend && yarn && yarn run lint ############################################################################## # JOB: LOCALES BACKEND ####################################################### @@ -362,7 +307,6 @@ jobs: lint_database_up: name: Lint - Database Up runs-on: ubuntu-latest - needs: [build_test_database_up] steps: ########################################################################## # CHECKOUT CODE ########################################################## @@ -370,20 +314,10 @@ jobs: - name: Checkout code uses: actions/checkout@v3 ########################################################################## - # DOWNLOAD DOCKER IMAGE ################################################## - ########################################################################## - - name: Download Docker Image (Backend) - uses: actions/download-artifact@v3 - with: - name: docker-database-test_up - path: /tmp - - name: Load Docker Image - run: docker load < /tmp/database_up.tar - ########################################################################## # LINT DATABASE ########################################################## ########################################################################## - - name: database | Lint - run: docker run --rm gradido/database:test_up yarn run lint + - name: Database | Lint + run: cd database && yarn && yarn run lint ############################################################################## # JOB: UNIT TEST FRONTEND ################################################### From 7f182a5927ffd5cab3ebfbb4af39ac2d4f1e1e21 Mon Sep 17 00:00:00 2001 From: ogerly Date: Thu, 16 Feb 2023 15:21:55 +0100 Subject: [PATCH 33/70] change fetchPolicy, add scripts.update --- frontend/src/pages/Community.vue | 2 +- package.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/Community.vue b/frontend/src/pages/Community.vue index 113bcd4e4..7427aa2fa 100644 --- a/frontend/src/pages/Community.vue +++ b/frontend/src/pages/Community.vue @@ -122,13 +122,13 @@ export default { query() { return listAllContributions }, - fetchPolicy: 'network-only', variables() { return { currentPage: this.currentPageAll, pageSize: this.pageSizeAll, } }, + fetchPolicy: 'no-cache', update({ listAllContributions }) { this.contributionCountAll = listAllContributions.contributionCount this.itemsAll = listAllContributions.contributionList diff --git a/package.json b/package.json index 2220c1a85..85b8dfe53 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "author": "Ulf Gebhardt ", "license": "Apache-2.0", "scripts": { - "release": "scripts/release.sh" + "release": "scripts/release.sh", + "update": "cd admin && yarn && cd ../backend && yarn && cd ../database && yarn && cd ../dht-node && yarn && cd ../e2e-tests && yarn && cd ../federation && yarn && cd ../frontend && yarn" }, "dependencies": { "auto-changelog": "^2.4.0", From 6567babd3b516b459e9a5bbf4a522120de37f807 Mon Sep 17 00:00:00 2001 From: ogerly Date: Thu, 16 Feb 2023 15:28:40 +0100 Subject: [PATCH 34/70] remove package json script update --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 85b8dfe53..959e4d6d5 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "license": "Apache-2.0", "scripts": { "release": "scripts/release.sh", - "update": "cd admin && yarn && cd ../backend && yarn && cd ../database && yarn && cd ../dht-node && yarn && cd ../e2e-tests && yarn && cd ../federation && yarn && cd ../frontend && yarn" }, "dependencies": { "auto-changelog": "^2.4.0", From 10327e9fd507342b240b4c91e5d0f00201d0342c Mon Sep 17 00:00:00 2001 From: ogerly Date: Thu, 16 Feb 2023 15:39:33 +0100 Subject: [PATCH 35/70] remove comma --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 959e4d6d5..2220c1a85 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "author": "Ulf Gebhardt ", "license": "Apache-2.0", "scripts": { - "release": "scripts/release.sh", + "release": "scripts/release.sh" }, "dependencies": { "auto-changelog": "^2.4.0", From 86e52e5d712853780e8501c02d21b2416fde4156 Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 16 Feb 2023 15:40:28 +0100 Subject: [PATCH 36/70] Change coverage from 80 to 81. --- .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 7819a0703..3ef94cfbd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -545,7 +545,7 @@ jobs: report_name: Coverage Backend type: lcov result_path: ./backend/coverage/lcov.info - min_coverage: 80 + min_coverage: 81 token: ${{ github.token }} ########################################################################## From 40b2944b2474cb1a2742de0466d622b85a9d1f51 Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 16 Feb 2023 15:41:04 +0100 Subject: [PATCH 37/70] Check toEqual. --- .../resolver/ContributionResolver.test.ts | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index ce37449df..670eaac2d 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -174,7 +174,7 @@ describe('ContributionResolver', () => { variables: { amount: 100.0, memo: 'Test Contribution', creationDate: 'not-valid' }, }) - expect(errorObjects).toMatchObject([new GraphQLError('401 Unauthorized')]) + expect(errorObjects).toEqual([new GraphQLError('401 Unauthorized')]) }) }) @@ -203,7 +203,7 @@ describe('ContributionResolver', () => { }, }) - expect(errorObjects).toMatchObject([new GraphQLError('Memo text is too short')]) + expect(errorObjects).toEqual([new GraphQLError('Memo text is too short')]) }) it('logs the error found', () => { @@ -221,7 +221,7 @@ describe('ContributionResolver', () => { creationDate: date.toString(), }, }) - expect(errorObjects).toMatchObject([new GraphQLError('Memo text is too long')]) + expect(errorObjects).toEqual([new GraphQLError('Memo text is too long')]) }) it('logs the error found', () => { @@ -238,7 +238,7 @@ describe('ContributionResolver', () => { creationDate: 'not-valid', }, }) - expect(errorObjects).toMatchObject([ + expect(errorObjects).toEqual([ new GraphQLError('No information for available creations for the given date'), ]) }) @@ -261,7 +261,7 @@ describe('ContributionResolver', () => { creationDate: date.setMonth(date.getMonth() - 3).toString(), }, }) - expect(errorObjects).toMatchObject([ + expect(errorObjects).toEqual([ new GraphQLError('No information for available creations for the given date'), ]) }) @@ -309,7 +309,7 @@ describe('ContributionResolver', () => { creationDate: 'not-valid', }, }) - expect(errorObjects).toMatchObject([new GraphQLError('401 Unauthorized')]) + expect(errorObjects).toEqual([new GraphQLError('401 Unauthorized')]) }) }) @@ -338,7 +338,7 @@ describe('ContributionResolver', () => { creationDate: date.toString(), }, }) - expect(errorObjects).toMatchObject([new GraphQLError('Memo text is too short')]) + expect(errorObjects).toEqual([new GraphQLError('Memo text is too short')]) }) it('logs the error found', () => { @@ -359,7 +359,7 @@ describe('ContributionResolver', () => { creationDate: date.toString(), }, }) - expect(errorObjects).toMatchObject([new GraphQLError('Memo text is too long')]) + expect(errorObjects).toEqual([new GraphQLError('Memo text is too long')]) }) it('logs the error found', () => { @@ -411,7 +411,7 @@ describe('ContributionResolver', () => { creationDate: new Date().toString(), }, }) - expect(errorObjects).toMatchObject([ + expect(errorObjects).toEqual([ new GraphQLError('Can not update contribution of another user'), ]) }) @@ -445,7 +445,7 @@ describe('ContributionResolver', () => { creationDate: new Date().toString(), }, }) - expect(errorObjects).toMatchObject([ + expect(errorObjects).toEqual([ new GraphQLError('An admin is not allowed to update an user contribution'), ]) }) @@ -524,7 +524,7 @@ describe('ContributionResolver', () => { creationDate: new Date().toString(), }, }) - expect(errorObjects).toMatchObject([ + expect(errorObjects).toEqual([ new GraphQLError( 'The amount (1019 GDD) to be created exceeds the amount (600 GDD) still available for this month.', ), @@ -551,7 +551,7 @@ describe('ContributionResolver', () => { creationDate: date.setMonth(date.getMonth() - 3).toString(), }, }) - expect(errorObjects).toMatchObject([ + expect(errorObjects).toEqual([ new GraphQLError('Month of contribution can not be changed'), ]) }) @@ -609,7 +609,7 @@ describe('ContributionResolver', () => { id: 1, }, }) - expect(errorObjects).toMatchObject([new GraphQLError('401 Unauthorized')]) + expect(errorObjects).toEqual([new GraphQLError('401 Unauthorized')]) }) }) @@ -632,7 +632,7 @@ describe('ContributionResolver', () => { id: 1, }, }) - expect(errorObjects).toMatchObject([new GraphQLError('401 Unauthorized')]) + expect(errorObjects).toEqual([new GraphQLError('401 Unauthorized')]) }) }) @@ -657,7 +657,7 @@ describe('ContributionResolver', () => { id: -1, }, }) - expect(errorObjects).toMatchObject([new GraphQLError('Contribution not found')]) + expect(errorObjects).toEqual([new GraphQLError('Contribution not found')]) }) it('logs the error found', () => { @@ -701,7 +701,7 @@ describe('ContributionResolver', () => { id: contribution.data.createContribution.id, }, }) - expect(errorObjects).toMatchObject([new GraphQLError('Contribution not found')]) + expect(errorObjects).toEqual([new GraphQLError('Contribution not found')]) }) it('logs the error found', () => { @@ -746,7 +746,7 @@ describe('ContributionResolver', () => { id: contribution.data.createContribution.id, }, }) - expect(errorObjects).toMatchObject([new GraphQLError('Contribution not found')]) + expect(errorObjects).toEqual([new GraphQLError('Contribution not found')]) }) it('logs the error found', () => { @@ -791,7 +791,7 @@ describe('ContributionResolver', () => { id: contribution.data.createContribution.id, }, }) - expect(errorObjects).toMatchObject([new GraphQLError('Contribution not found')]) + expect(errorObjects).toEqual([new GraphQLError('Contribution not found')]) }) it('logs the error found', () => { @@ -840,7 +840,7 @@ describe('ContributionResolver', () => { id: -1, }, }) - expect(errorObjects).toMatchObject([new GraphQLError('401 Unauthorized')]) + expect(errorObjects).toEqual([new GraphQLError('401 Unauthorized')]) }) }) @@ -865,7 +865,7 @@ describe('ContributionResolver', () => { id: -1, }, }) - expect(errorObjects).toMatchObject([new GraphQLError('Contribution not found')]) + expect(errorObjects).toEqual([new GraphQLError('Contribution not found')]) }) it('logs the error found', () => { @@ -893,7 +893,7 @@ describe('ContributionResolver', () => { id: contributionToDelete.data.createContribution.id, }, }) - expect(errorObjects).toMatchObject([ + expect(errorObjects).toEqual([ new GraphQLError('Can not delete contribution of another user'), ]) }) @@ -966,7 +966,7 @@ describe('ContributionResolver', () => { id: contributionToConfirm.data.createContribution.id, }, }) - expect(errorObjects).toMatchObject([ + expect(errorObjects).toEqual([ new GraphQLError('A confirmed contribution can not be deleted'), ]) }) @@ -993,7 +993,7 @@ describe('ContributionResolver', () => { filterConfirmed: false, }, }) - expect(errorObjects).toMatchObject([new GraphQLError('401 Unauthorized')]) + expect(errorObjects).toEqual([new GraphQLError('401 Unauthorized')]) }) }) @@ -1124,7 +1124,7 @@ describe('ContributionResolver', () => { statusFilter: null, }, }) - expect(errorObjects).toMatchObject([new GraphQLError('401 Unauthorized')]) + expect(errorObjects).toEqual([new GraphQLError('401 Unauthorized')]) }) }) @@ -1150,7 +1150,7 @@ describe('ContributionResolver', () => { statusFilter: ['NOT_VALID'], }, }) - expect(errorObjects).toMatchObject([ + expect(errorObjects).toEqual([ new UserInputError( 'Variable "$statusFilter" got invalid value "NOT_VALID" at "statusFilter[0]"; Value "NOT_VALID" does not exist in "ContributionStatus" enum.', ), @@ -1167,7 +1167,7 @@ describe('ContributionResolver', () => { statusFilter: [null], }, }) - expect(errorObjects).toMatchObject([ + expect(errorObjects).toEqual([ new UserInputError( 'Variable "$statusFilter" got invalid value null at "statusFilter[0]"; Expected non-nullable type "ContributionStatus!" not to be null.', ), @@ -1184,7 +1184,7 @@ describe('ContributionResolver', () => { statusFilter: [null, 'NOT_VALID'], }, }) - expect(errorObjects).toMatchObject([ + expect(errorObjects).toEqual([ new UserInputError( 'Variable "$statusFilter" got invalid value null at "statusFilter[0]"; Expected non-nullable type "ContributionStatus!" not to be null.', ), @@ -1570,7 +1570,7 @@ describe('ContributionResolver', () => { statusFilter: ['DELETED'], }, }) - expect(contributionListObject).toMatchObject({ + expect(contributionListObject).toEqual({ contributionCount: 0, contributionList: [], }) From baccc83f0016a571e31ef7dad649f95d74551b59 Mon Sep 17 00:00:00 2001 From: mahula Date: Thu, 16 Feb 2023 16:33:49 +0100 Subject: [PATCH 38/70] remove docker dependendies from unit test jobs --- .github/workflows/test.yml | 40 +++----------------------------------- 1 file changed, 3 insertions(+), 37 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9a3895093..a29792a7d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -325,7 +325,6 @@ jobs: unit_test_frontend: name: Unit tests - Frontend runs-on: ubuntu-latest - needs: [build_test_frontend] steps: ########################################################################## # CHECKOUT CODE ########################################################## @@ -333,30 +332,10 @@ jobs: - name: Checkout code uses: actions/checkout@v3 ########################################################################## - # DOWNLOAD DOCKER IMAGES ################################################# - ########################################################################## - - name: Download Docker Image (Frontend) - uses: actions/download-artifact@v3 - with: - name: docker-frontend-test - path: /tmp - - name: Load Docker Image - run: docker load < /tmp/frontend.tar - ########################################################################## # UNIT TESTS FRONTEND #################################################### ########################################################################## - - name: frontend | Unit tests - run: | - docker run --env NODE_ENV=test -v ~/coverage:/app/coverage --rm gradido/frontend:test yarn run test - cp -r ~/coverage ./coverage - ########################################################################## - # COVERAGE REPORT FRONTEND ############################################### - ########################################################################## - #- name: frontend | Coverage report - # uses: romeovs/lcov-reporter-action@v0.2.21 - # with: - # github-token: ${{ secrets.GITHUB_TOKEN }} - # lcov-file: ./coverage/lcov.info + - name: Frontend | Unit tests + run: cd frontend && yarn && yarn run test ########################################################################## # COVERAGE CHECK FRONTEND ################################################ ########################################################################## @@ -375,7 +354,6 @@ jobs: unit_test_admin: name: Unit tests - Admin Interface runs-on: ubuntu-latest - needs: [build_test_admin] steps: ########################################################################## # CHECKOUT CODE ########################################################## @@ -383,22 +361,10 @@ jobs: - name: Checkout code uses: actions/checkout@v3 ########################################################################## - # DOWNLOAD DOCKER IMAGES ################################################# - ########################################################################## - - name: Download Docker Image (Admin Interface) - uses: actions/download-artifact@v3 - with: - name: docker-admin-test - path: /tmp - - name: Load Docker Image - run: docker load < /tmp/admin.tar - ########################################################################## # UNIT TESTS ADMIN INTERFACE ############################################# ########################################################################## - name: Admin Interface | Unit tests - run: | - docker run -v ~/coverage:/app/coverage --rm gradido/admin:test yarn run test - cp -r ~/coverage ./coverage + run: cd admin && yarn && yarn run test ########################################################################## # COVERAGE CHECK ADMIN INTERFACE ######################################### ########################################################################## From f5c44e8ca3b40c6e85cb5f3e3a26bcdfee257052 Mon Sep 17 00:00:00 2001 From: mahula Date: Thu, 16 Feb 2023 16:59:03 +0100 Subject: [PATCH 39/70] adapt paths for unit test coverage jobs --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a29792a7d..2e55ba47e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -344,7 +344,7 @@ jobs: with: report_name: Coverage Frontend type: lcov - result_path: ./coverage/lcov.info + result_path: ./frontend/coverage/lcov.info min_coverage: 95 token: ${{ github.token }} @@ -373,7 +373,7 @@ jobs: with: report_name: Coverage Admin Interface type: lcov - result_path: ./coverage/lcov.info + result_path: ./admin/coverage/lcov.info min_coverage: 96 token: ${{ github.token }} From 72b8e2dc97c17005923f31330d5e7cd696c88b07 Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 16 Feb 2023 17:22:41 +0100 Subject: [PATCH 40/70] Revert "Change coverage from 80 to 81." This reverts commit 86e52e5d712853780e8501c02d21b2416fde4156. --- .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 61000381d..de45a35aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -545,7 +545,7 @@ jobs: report_name: Coverage Backend type: lcov result_path: ./backend/coverage/lcov.info - min_coverage: 81 + min_coverage: 80 token: ${{ github.token }} ########################################################################## From 9fbce956df6d39952465cf722a3c3b5b1385e0fc Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Thu, 16 Feb 2023 18:22:47 +0100 Subject: [PATCH 41/70] missing merge change --- backend/src/graphql/resolver/ContributionResolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 498fbaeba..b56180c45 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -526,7 +526,7 @@ describe('ContributionResolver', () => { }) expect(errorObjects).toEqual([ new GraphQLError( - 'The amount to be created exceeds the amount still available for this month',, + 'The amount to be created exceeds the amount still available for this month', ), ]) }) From a5fde7361cbbbd906a6a319bed019a0630ffb0af Mon Sep 17 00:00:00 2001 From: mahula Date: Thu, 16 Feb 2023 20:57:15 +0100 Subject: [PATCH 42/70] fix result path for admin unit test coverage ingo --- .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 b3c15ddea..c2dd3a3b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -373,7 +373,7 @@ jobs: with: report_name: Coverage Admin Interface type: lcov - result_path: ./coverage/lcov.info + result_path: ./admin/coverage/lcov.info min_coverage: 97 token: ${{ github.token }} From de6834a6f0aea5a911d9e6a44811b204d0984160 Mon Sep 17 00:00:00 2001 From: mahula Date: Fri, 17 Feb 2023 02:35:24 +0100 Subject: [PATCH 43/70] add copying of coverage info for check --- .github/workflows/test.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c2dd3a3b0..badb47e87 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -335,7 +335,9 @@ jobs: # UNIT TESTS FRONTEND #################################################### ########################################################################## - name: Frontend | Unit tests - run: cd frontend && yarn && yarn run test + run: | + cd frontend && yarn && yarn run test + cp -r ./coverage ../ ########################################################################## # COVERAGE CHECK FRONTEND ################################################ ########################################################################## @@ -364,7 +366,9 @@ jobs: # UNIT TESTS ADMIN INTERFACE ############################################# ########################################################################## - name: Admin Interface | Unit tests - run: cd admin && yarn && yarn run test + run: | + cd admin && yarn && yarn run test + cp -r ./coverage ../ ########################################################################## # COVERAGE CHECK ADMIN INTERFACE ######################################### ########################################################################## @@ -411,8 +415,9 @@ jobs: - name: backend | docker-compose database run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database - name: backend Unit tests | test - run: cd database && yarn && yarn build && cd ../backend && yarn && yarn test - # run: docker-compose -f docker-compose.yml -f docker-compose.test.yml exec -T backend yarn test + run: | + cd database && yarn && yarn build && cd ../backend && yarn && yarn test + cp -r ./coverage ../ ########################################################################## # COVERAGE CHECK BACKEND ################################################# ########################################################################## From 86742d015823c7c657d1d84d9cc2292bb837c6ec Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 17 Feb 2023 09:48:05 +0100 Subject: [PATCH 44/70] refactor(frontend): community routes --- .../Template/ContentHeader/NavCommunity.vue | 25 +- .../Template/RightSide/ContributionInfo.vue | 22 +- .../src/layouts/templates/ContentHeader.vue | 17 +- frontend/src/layouts/templates/RightSide.vue | 32 +- frontend/src/pages/Community.vue | 485 +++++++++--------- frontend/src/routes/routes.js | 11 + 6 files changed, 304 insertions(+), 288 deletions(-) diff --git a/frontend/src/components/Template/ContentHeader/NavCommunity.vue b/frontend/src/components/Template/ContentHeader/NavCommunity.vue index 31a839af4..841304b2a 100644 --- a/frontend/src/components/Template/ContentHeader/NavCommunity.vue +++ b/frontend/src/components/Template/ContentHeader/NavCommunity.vue @@ -2,19 +2,19 @@