From 283e09ea3db3eeec9bd68345efcf2784d773f277 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Tue, 7 Feb 2023 11:43:04 +0100 Subject: [PATCH] separate error cases, have some basic testing for transaction links, reorder tests --- .../resolver/TransactionLinkResolver.test.ts | 514 ++++++++++-------- .../resolver/TransactionLinkResolver.ts | 6 +- 2 files changed, 293 insertions(+), 227 deletions(-) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts index 0eca609de..af2e4fd59 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts @@ -16,6 +16,7 @@ import { redeemTransactionLink, createContribution, updateContribution, + createTransactionLink, } from '@/seeds/graphql/mutations' import { listTransactionLinksAdmin } from '@/seeds/graphql/queries' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' @@ -51,6 +52,69 @@ afterAll(async () => { }) describe('TransactionLinkResolver', () => { + describe('createTransactionLink', () => { + beforeAll(async () => { + await mutate({ + mutation: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + }) + + 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('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)) + }) + }) + describe('redeemTransactionLink', () => { describe('contributionLink', () => { describe('input not valid', () => { @@ -408,22 +472,52 @@ describe('TransactionLinkResolver', () => { }) }) }) + }) - describe('transaction links list', () => { - const variables = { - userId: 1, // dummy, may be replaced - filters: null, - currentPage: 1, - pageSize: 5, - } + describe('listTransactionLinksAdmin', () => { + const variables = { + userId: 1, // dummy, may be replaced + filters: null, + currentPage: 1, + pageSize: 5, + } - // TODO: there is a test not cleaning up after itself! Fix it! - beforeAll(async () => { - await cleanDB() - resetToken() + // TODO: there is a test not cleaning up after itself! Fix it! + beforeAll(async () => { + await cleanDB() + resetToken() + }) + + describe('unauthenticated', () => { + it('returns an error', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) }) + }) + + describe('authenticated', () => { + describe('without admin rights', () => { + beforeAll(async () => { + user = await userFactory(testEnv, bibiBloxberg) + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + afterAll(async () => { + await cleanDB() + resetToken() + }) - describe('unauthenticated', () => { it('returns an error', async () => { await expect( query({ @@ -438,22 +532,40 @@ describe('TransactionLinkResolver', () => { }) }) - describe('authenticated', () => { - describe('without admin rights', () => { - beforeAll(async () => { - user = await userFactory(testEnv, bibiBloxberg) - await mutate({ - mutation: login, - variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, - }) - }) + describe('with admin rights', () => { + beforeAll(async () => { + // admin 'peter@lustig.de' has to exists for 'creationFactory' + await userFactory(testEnv, peterLustig) - afterAll(async () => { - await cleanDB() - resetToken() - }) + user = await userFactory(testEnv, bibiBloxberg) + variables.userId = user.id + variables.pageSize = 25 + // bibi needs GDDs + const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de') + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await creationFactory(testEnv, bibisCreation!) + // bibis transaktion links + const bibisTransaktionLinks = transactionLinks.filter( + (transactionLink) => transactionLink.email === 'bibi@bloxberg.de', + ) + for (let i = 0; i < bibisTransaktionLinks.length; i++) { + await transactionLinkFactory(testEnv, bibisTransaktionLinks[i]) + } - it('returns an error', async () => { + // admin: only now log in + await mutate({ + mutation: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + }) + + afterAll(async () => { + await cleanDB() + resetToken() + }) + + describe('without any filters', () => { + it('finds 6 open transaction links and no deleted or redeemed', async () => { await expect( query({ query: listTransactionLinksAdmin, @@ -461,219 +573,169 @@ describe('TransactionLinkResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('401 Unauthorized')], + data: { + listTransactionLinksAdmin: { + linkCount: 6, + linkList: expect.not.arrayContaining([ + expect.objectContaining({ + memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', + createdAt: expect.any(String), + }), + expect.objectContaining({ + memo: 'Da habe ich mich wohl etwas übernommen.', + deletedAt: expect.any(String), + }), + ]), + }, + }, }), ) }) }) - describe('with admin rights', () => { - beforeAll(async () => { - // admin 'peter@lustig.de' has to exists for 'creationFactory' - await userFactory(testEnv, peterLustig) - - user = await userFactory(testEnv, bibiBloxberg) - variables.userId = user.id - variables.pageSize = 25 - // bibi needs GDDs - const bibisCreation = creations.find( - (creation) => creation.email === 'bibi@bloxberg.de', + describe('all filters are null', () => { + it('finds 6 open transaction links and no deleted or redeemed', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables: { + ...variables, + filters: { + withDeleted: null, + withExpired: null, + withRedeemed: null, + }, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listTransactionLinksAdmin: { + linkCount: 6, + linkList: expect.not.arrayContaining([ + expect.objectContaining({ + memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', + createdAt: expect.any(String), + }), + expect.objectContaining({ + memo: 'Da habe ich mich wohl etwas übernommen.', + deletedAt: expect.any(String), + }), + ]), + }, + }, + }), ) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await creationFactory(testEnv, bibisCreation!) - // bibis transaktion links - const bibisTransaktionLinks = transactionLinks.filter( - (transactionLink) => transactionLink.email === 'bibi@bloxberg.de', + }) + }) + + describe('filter with deleted', () => { + it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables: { + ...variables, + filters: { + withDeleted: true, + }, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listTransactionLinksAdmin: { + linkCount: 7, + linkList: expect.arrayContaining([ + expect.not.objectContaining({ + memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', + createdAt: expect.any(String), + }), + expect.objectContaining({ + memo: 'Da habe ich mich wohl etwas übernommen.', + deletedAt: expect.any(String), + }), + ]), + }, + }, + }), ) - for (let i = 0; i < bibisTransaktionLinks.length; i++) { - await transactionLinkFactory(testEnv, bibisTransaktionLinks[i]) - } - - // admin: only now log in - await mutate({ - mutation: login, - variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, - }) }) + }) - afterAll(async () => { - await cleanDB() - resetToken() + describe('filter by expired', () => { + it('finds 5 open transaction links, 1 expired, and no redeemed', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables: { + ...variables, + filters: { + withExpired: true, + }, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listTransactionLinksAdmin: { + linkCount: 7, + linkList: expect.arrayContaining([ + expect.objectContaining({ + memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', + createdAt: expect.any(String), + }), + expect.not.objectContaining({ + memo: 'Da habe ich mich wohl etwas übernommen.', + deletedAt: expect.any(String), + }), + ]), + }, + }, + }), + ) }) + }) - describe('without any filters', () => { - it('finds 6 open transaction links and no deleted or redeemed', async () => { - await expect( - query({ - query: listTransactionLinksAdmin, - variables, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listTransactionLinksAdmin: { - linkCount: 6, - linkList: expect.not.arrayContaining([ - expect.objectContaining({ - memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', - createdAt: expect.any(String), - }), - expect.objectContaining({ - memo: 'Da habe ich mich wohl etwas übernommen.', - deletedAt: expect.any(String), - }), - ]), - }, + // TODO: works not as expected, because 'redeemedAt' and 'redeemedBy' have to be added to the transaktion link factory + describe.skip('filter by redeemed', () => { + it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables: { + ...variables, + filters: { + withDeleted: null, + withExpired: null, + withRedeemed: true, }, - }), - ) - }) - }) - - describe('all filters are null', () => { - it('finds 6 open transaction links and no deleted or redeemed', async () => { - await expect( - query({ - query: listTransactionLinksAdmin, - variables: { - ...variables, - filters: { - withDeleted: null, - withExpired: null, - withRedeemed: null, - }, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listTransactionLinksAdmin: { + linkCount: 6, + linkList: expect.arrayContaining([ + expect.not.objectContaining({ + memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', + createdAt: expect.any(String), + }), + expect.objectContaining({ + memo: 'Yeah, eingelöst!', + redeemedAt: expect.any(String), + redeemedBy: expect.any(Number), + }), + expect.not.objectContaining({ + memo: 'Da habe ich mich wohl etwas übernommen.', + deletedAt: expect.any(String), + }), + ]), }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listTransactionLinksAdmin: { - linkCount: 6, - linkList: expect.not.arrayContaining([ - expect.objectContaining({ - memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', - createdAt: expect.any(String), - }), - expect.objectContaining({ - memo: 'Da habe ich mich wohl etwas übernommen.', - deletedAt: expect.any(String), - }), - ]), - }, - }, - }), - ) - }) - }) - - describe('filter with deleted', () => { - it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => { - await expect( - query({ - query: listTransactionLinksAdmin, - variables: { - ...variables, - filters: { - withDeleted: true, - }, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listTransactionLinksAdmin: { - linkCount: 7, - linkList: expect.arrayContaining([ - expect.not.objectContaining({ - memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', - createdAt: expect.any(String), - }), - expect.objectContaining({ - memo: 'Da habe ich mich wohl etwas übernommen.', - deletedAt: expect.any(String), - }), - ]), - }, - }, - }), - ) - }) - }) - - describe('filter by expired', () => { - it('finds 5 open transaction links, 1 expired, and no redeemed', async () => { - await expect( - query({ - query: listTransactionLinksAdmin, - variables: { - ...variables, - filters: { - withExpired: true, - }, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listTransactionLinksAdmin: { - linkCount: 7, - linkList: expect.arrayContaining([ - expect.objectContaining({ - memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', - createdAt: expect.any(String), - }), - expect.not.objectContaining({ - memo: 'Da habe ich mich wohl etwas übernommen.', - deletedAt: expect.any(String), - }), - ]), - }, - }, - }), - ) - }) - }) - - // TODO: works not as expected, because 'redeemedAt' and 'redeemedBy' have to be added to the transaktion link factory - describe.skip('filter by redeemed', () => { - it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => { - await expect( - query({ - query: listTransactionLinksAdmin, - variables: { - ...variables, - filters: { - withDeleted: null, - withExpired: null, - withRedeemed: true, - }, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listTransactionLinksAdmin: { - linkCount: 6, - linkList: expect.arrayContaining([ - expect.not.objectContaining({ - memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', - createdAt: expect.any(String), - }), - expect.objectContaining({ - memo: 'Yeah, eingelöst!', - redeemedAt: expect.any(String), - redeemedBy: expect.any(Number), - }), - expect.not.objectContaining({ - memo: 'Da habe ich mich wohl etwas übernommen.', - deletedAt: expect.any(String), - }), - ]), - }, - }, - }), - ) - }) + }, + }), + ) }) }) }) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 50e6d6146..696c51d97 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -64,12 +64,16 @@ export class TransactionLinkResolver { const createdDate = new Date() const validUntil = transactionLinkExpireDate(createdDate) + if (amount.lessThanOrEqualTo(0)) { + throw new LogError('Amount must be a positive number', amount) + } + const holdAvailableAmount = amount.minus(calculateDecay(amount, createdDate, validUntil).decay) // validate amount const sendBalance = await calculateBalance(user.id, holdAvailableAmount.mul(-1), createdDate) if (!sendBalance) { - throw new Error("user hasn't enough GDD or amount is < 0") + throw new LogError('User has not enough GDD', user.id) } const transactionLink = DbTransactionLink.create()