From f6b386101d5c75ab1dbfdfd935a0de1b335f5cf8 Mon Sep 17 00:00:00 2001 From: joseji Date: Thu, 13 Oct 2022 12:18:38 +0200 Subject: [PATCH 01/14] test start --- .../resolver/TransactionResolver.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 backend/src/graphql/resolver/TransactionResolver.test.ts diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts new file mode 100644 index 000000000..f6475e62f --- /dev/null +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -0,0 +1,18 @@ +import { cleanDB, testEnvironment } from '@test/helpers' +import { logger } from '@test/testSetup' + +let mutate: any, query: any, con: any +let testEnv: any + +beforeAll(async () => { + testEnv = await testEnvironment(logger) + mutate = testEnv.mutate + query = testEnv.query + con = testEnv.con + await cleanDB() +}) + +afterAll(async () => { + await cleanDB() + await con.close() +}) \ No newline at end of file From 0b68f2f74b2606128c9d14ae1586e1e59e98fc14 Mon Sep 17 00:00:00 2001 From: joseji Date: Thu, 13 Oct 2022 13:44:39 +0200 Subject: [PATCH 02/14] tests up and working --- backend/src/config/index.ts | 2 +- .../resolver/TransactionResolver.test.ts | 284 +++++++++++++++++- .../graphql/resolver/TransactionResolver.ts | 9 +- 3 files changed, 290 insertions(+), 5 deletions(-) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 3e6bafd9f..b622293ad 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0049-add_user_contacts_table', + DB_VERSION: '0050-add_messageId_to_event_protocol', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index f6475e62f..87ada724d 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -1,5 +1,20 @@ -import { cleanDB, testEnvironment } from '@test/helpers' +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ + +import { userFactory } from '@/seeds/factory/user' +import { + confirmContribution, + createContribution, + createUser, + sendCoins, +} from '@/seeds/graphql/mutations' +import { login } from '@/seeds/graphql/queries' +import { bobBaumeister } from '@/seeds/users/bob-baumeister' +import { peterLustig } from '@/seeds/users/peter-lustig' +import { User } from '@entity/User' +import { cleanDB, resetToken, testEnvironment } from '@test/helpers' import { logger } from '@test/testSetup' +import { GraphQLError } from 'graphql' let mutate: any, query: any, con: any let testEnv: any @@ -15,4 +30,269 @@ beforeAll(async () => { afterAll(async () => { await cleanDB() await con.close() -}) \ No newline at end of file +}) + +let bobData: any +let peterData: any +let user: User[] + +describe('send coins', () => { + beforeAll(async () => { + await userFactory(testEnv, peterLustig) + await userFactory(testEnv, bobBaumeister) + + bobData = { + email: 'bob@baumeister.de', + password: 'Aa12345_', + publisherId: 1234, + } + + peterData = { + email: 'peter@lustig.de', + password: 'Aa12345_', + publisherId: 1234, + } + + user = await User.find({ relations: ['emailContact'] }) + }) + + afterAll(async () => { + await cleanDB() + }) + + describe('wrong recipient', () => { + it('unknown recipient', async () => { + await mutate({ + query: login, + variables: bobData, + }) + expect( + await mutate({ + mutation: sendCoins, + variables: { + email: 'wrong@email.com', + amount: 100, + memo: 'test', + }, + }), + ).toEqual( + expect.objectContaining({ + errors: [new GraphQLError('No user with this credentials')], + }), + ) + }) + + it('deleted recipient', async () => { + // delete bob + const bob = await User.findOneOrFail({ id: user[1].id }) + bob.deletedAt = new Date() + await bob.save() + + await mutate({ + query: login, + variables: peterData, + }) + + expect( + await mutate({ + mutation: sendCoins, + variables: { + email: 'bob@baumeister.de', + amount: 100, + memo: 'test', + }, + }), + ).toEqual( + expect.objectContaining({ + errors: [new GraphQLError('The recipient account was deleted')], + }), + ) + + // make bob active again + bob.deletedAt = null + await bob.save() + }) + + it('recipient account not activated', async () => { + resetToken() + + await mutate({ + mutation: createUser, + variables: { + email: 'testing@user.de', + firstName: 'testing', + lastName: 'user', + language: 'de', + publisherId: 1234, + }, + }) + + await mutate({ + query: login, + variables: peterData, + }) + + expect( + await mutate({ + mutation: sendCoins, + variables: { + email: 'testing@user.de', + amount: 100, + memo: 'test', + }, + }), + ).toEqual( + expect.objectContaining({ + errors: [new GraphQLError('The recipient account is not activated')], + }), + ) + }) + }) + + describe('errors in the transaction itself', () => { + beforeAll(async () => { + await mutate({ + query: login, + variables: bobData, + }) + }) + + describe('sender and recipient are the same', () => { + it('throws an error', async () => { + expect( + await mutate({ + mutation: sendCoins, + variables: { + email: 'bob@baumeister.de', + amount: 100, + memo: 'test', + }, + }), + ).toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Sender and Recipient are the same.')], + }), + ) + }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Sender and Recipient are the same.') + }) + }) + + describe('memo text is too long', () => { + it('throws an error', async () => { + expect( + await mutate({ + mutation: sendCoins, + variables: { + email: 'peter@lustig.de', + amount: 100, + 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 t', + }, + }), + ).toEqual( + expect.objectContaining({ + errors: [new GraphQLError('memo text is too long (255 characters maximum)')], + }), + ) + }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('memo text is too long: memo.length=256 > 255') + }) + }) + + describe('memo text is too short', () => { + it('throws an error', async () => { + expect( + await mutate({ + mutation: sendCoins, + variables: { + email: 'peter@lustig.de', + amount: 100, + memo: 'test', + }, + }), + ).toEqual( + expect.objectContaining({ + errors: [new GraphQLError('memo text is too short (5 characters minimum)')], + }), + ) + }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('memo text is too short: memo.length=4 < 5') + }) + }) + + describe('user has not enough GDD', () => { + it('throws an error', async () => { + expect( + await mutate({ + mutation: sendCoins, + variables: { + email: 'peter@lustig.de', + amount: 100, + memo: 'testing', + }, + }), + ).toEqual( + expect.objectContaining({ + errors: [new GraphQLError(`user hasn't enough GDD or amount is < 0`)], + }), + ) + }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + `user hasn't enough GDD or amount is < 0 : balance=null`, + ) + }) + }) + }) + + describe('transaction correct', () => { + it('sends the coins', async () => { + // make Peter Lustig Admin + const peter = await User.findOneOrFail({ id: user[0].id }) + peter.isAdmin = new Date() + await peter.save() + + // create contribution as user bob + const contribution = await mutate({ + mutation: createContribution, + variables: { amount: 1000, memo: 'testing', creationDate: new Date().toISOString() }, + }) + + // login as admin + await query({ query: login, variables: peterData }) + + // confirm the contribution + await mutate({ + mutation: confirmContribution, + variables: { id: contribution.data.createContribution.id }, + }) + + // login as bob again + await query({ query: login, variables: bobData }) + + expect( + await mutate({ + mutation: sendCoins, + variables: { + email: 'peter@lustig.de', + amount: 100, + memo: 'testing', + }, + }), + ).toEqual( + expect.objectContaining({ + data: { + sendCoins: 'true', + }, + }), + ) + }) + }) +}) diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index b00d84de6..91114232b 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -55,12 +55,12 @@ export const executeTransaction = async ( } if (memo.length > MEMO_MAX_CHARS) { - logger.error(`memo text is too long: memo.length=${memo.length} > (${MEMO_MAX_CHARS}`) + logger.error(`memo text is too long: memo.length=${memo.length} > ${MEMO_MAX_CHARS}`) throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`) } if (memo.length < MEMO_MIN_CHARS) { - logger.error(`memo text is too short: memo.length=${memo.length} < (${MEMO_MIN_CHARS}`) + logger.error(`memo text is too short: memo.length=${memo.length} < ${MEMO_MIN_CHARS}`) throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`) } @@ -74,6 +74,7 @@ export const executeTransaction = async ( ) logger.debug(`calculated Balance=${sendBalance}`) if (!sendBalance) { + // josejgi: wrong messages from my point of view, at this point balance is always null, should log inside calculate balance or handle returns in a different way logger.error(`user hasn't enough GDD or amount is < 0 : balance=${sendBalance}`) throw new Error("user hasn't enough GDD or amount is < 0") } @@ -316,6 +317,10 @@ export class TransactionResolver { } */ // const recipientUser = await dbUser.findOne({ id: emailContact.userId }) + + /* Code inside this if statement is unreachable (useless by so), + in findUserByEmail() an error is already thrown if the user is not found + */ if (!recipientUser) { logger.error(`unknown recipient to UserContact: email=${email}`) throw new Error('unknown recipient') From a4ded0f61e1b0bfe80b636d78bba75c01688e9db Mon Sep 17 00:00:00 2001 From: joseji Date: Thu, 13 Oct 2022 13:57:48 +0200 Subject: [PATCH 03/14] database version --- backend/src/config/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index b622293ad..3e6bafd9f 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0050-add_messageId_to_event_protocol', + DB_VERSION: '0049-add_user_contacts_table', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info From 91c05fcb9b09395da87fbecd188bb915cc3e9eb1 Mon Sep 17 00:00:00 2001 From: joseji Date: Thu, 13 Oct 2022 14:13:23 +0200 Subject: [PATCH 04/14] events implemented and tested --- .../resolver/TransactionResolver.test.ts | 20 +++++++++++++++++++ .../graphql/resolver/TransactionResolver.ts | 16 +++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index 87ada724d..088475b58 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { EventProtocolType } from '@/event/EventProtocolType' import { userFactory } from '@/seeds/factory/user' import { confirmContribution, @@ -11,6 +12,7 @@ import { import { login } from '@/seeds/graphql/queries' import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { peterLustig } from '@/seeds/users/peter-lustig' +import { EventProtocol } from '@entity/EventProtocol' import { User } from '@entity/User' import { cleanDB, resetToken, testEnvironment } from '@test/helpers' import { logger } from '@test/testSetup' @@ -294,5 +296,23 @@ describe('send coins', () => { }), ) }) + + it('stores the send transaction event in the database', async () => { + expect(EventProtocol.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventProtocolType.TRANSACTION_SEND, + userId: user[1].id, + }), + ) + }) + + it('stores the receive event in the database', async () => { + expect(EventProtocol.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventProtocolType.TRANSACTION_RECEIVE, + userId: user[0].id, + }), + ) + }) }) }) diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 91114232b..2145f2183 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -37,6 +37,8 @@ import { BalanceResolver } from './BalanceResolver' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { findUserByEmail } from './UserResolver' import { sendTransactionLinkRedeemedEmail } from '@/mailer/sendTransactionLinkRedeemed' +import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event' +import { eventProtocol } from '@/event/EventProtocolEmitter' export const executeTransaction = async ( amount: Decimal, @@ -136,6 +138,20 @@ export const executeTransaction = async ( await queryRunner.commitTransaction() logger.info(`commit Transaction successful...`) + + const eventTransactionSend = new EventTransactionSend() + eventTransactionSend.userId = transactionSend.userId + eventTransactionSend.xUserId = transactionSend.linkedUserId + eventTransactionSend.transactionId = transactionSend.id + eventTransactionSend.amount = transactionSend.amount + await eventProtocol.writeEvent(new Event().setEventTransactionSend(eventTransactionSend)) + + const eventTransactionReceive = new EventTransactionReceive() + eventTransactionReceive.userId = transactionReceive.userId + eventTransactionReceive.xUserId = transactionReceive.linkedUserId + eventTransactionReceive.transactionId = transactionReceive.id + eventTransactionReceive.amount = transactionReceive.amount + await eventProtocol.writeEvent(new Event().setEventTransactionReceive(eventTransactionReceive)) } catch (e) { await queryRunner.rollbackTransaction() logger.error(`Transaction was not successful: ${e}`) From 2fd37a75038eb0f38c7f0e5d97e0705180471678 Mon Sep 17 00:00:00 2001 From: joseji Date: Fri, 14 Oct 2022 13:59:41 +0200 Subject: [PATCH 05/14] fixed tests and negative numbers are now detected --- .../resolver/TransactionResolver.test.ts | 240 ++++++++++-------- .../graphql/resolver/TransactionResolver.ts | 1 - backend/src/util/validate.ts | 2 + 3 files changed, 137 insertions(+), 106 deletions(-) diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index 088475b58..d5077d314 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -6,15 +6,17 @@ import { userFactory } from '@/seeds/factory/user' import { confirmContribution, createContribution, - createUser, + login, sendCoins, } from '@/seeds/graphql/mutations' -import { login } from '@/seeds/graphql/queries' import { bobBaumeister } from '@/seeds/users/bob-baumeister' +import { garrickOllivander } from '@/seeds/users/garrick-ollivander' import { peterLustig } from '@/seeds/users/peter-lustig' +import { stephenHawking } from '@/seeds/users/stephen-hawking' import { EventProtocol } from '@entity/EventProtocol' +import { Transaction } from '@entity/Transaction' import { User } from '@entity/User' -import { cleanDB, resetToken, testEnvironment } from '@test/helpers' +import { cleanDB, testEnvironment } from '@test/helpers' import { logger } from '@test/testSetup' import { GraphQLError } from 'graphql' @@ -42,6 +44,8 @@ describe('send coins', () => { beforeAll(async () => { await userFactory(testEnv, peterLustig) await userFactory(testEnv, bobBaumeister) + await userFactory(testEnv, stephenHawking) + await userFactory(testEnv, garrickOllivander) bobData = { email: 'bob@baumeister.de', @@ -62,10 +66,10 @@ describe('send coins', () => { await cleanDB() }) - describe('wrong recipient', () => { - it('unknown recipient', async () => { + describe('unknown recipient', () => { + it('throws an error', async () => { await mutate({ - query: login, + mutation: login, variables: bobData, }) expect( @@ -84,77 +88,59 @@ describe('send coins', () => { ) }) - it('deleted recipient', async () => { - // delete bob - const bob = await User.findOneOrFail({ id: user[1].id }) - bob.deletedAt = new Date() - await bob.save() - - await mutate({ - query: login, - variables: peterData, - }) - - expect( + describe('deleted recipient', () => { + it('throws an error', async () => { await mutate({ - mutation: sendCoins, - variables: { - email: 'bob@baumeister.de', - amount: 100, - memo: 'test', - }, - }), - ).toEqual( - expect.objectContaining({ - errors: [new GraphQLError('The recipient account was deleted')], - }), - ) + mutation: login, + variables: peterData, + }) - // make bob active again - bob.deletedAt = null - await bob.save() + expect( + await mutate({ + mutation: sendCoins, + variables: { + email: 'stephen@hawking.uk', + amount: 100, + memo: 'test', + }, + }), + ).toEqual( + expect.objectContaining({ + errors: [new GraphQLError('The recipient account was deleted')], + }), + ) + }) }) - it('recipient account not activated', async () => { - resetToken() - - await mutate({ - mutation: createUser, - variables: { - email: 'testing@user.de', - firstName: 'testing', - lastName: 'user', - language: 'de', - publisherId: 1234, - }, - }) - - await mutate({ - query: login, - variables: peterData, - }) - - expect( + describe('recipient account not activated', () => { + it('throws an error', async () => { await mutate({ - mutation: sendCoins, - variables: { - email: 'testing@user.de', - amount: 100, - memo: 'test', - }, - }), - ).toEqual( - expect.objectContaining({ - errors: [new GraphQLError('The recipient account is not activated')], - }), - ) + mutation: login, + variables: peterData, + }) + + expect( + await mutate({ + mutation: sendCoins, + variables: { + email: 'garrick@ollivander.com', + amount: 100, + memo: 'test', + }, + }), + ).toEqual( + expect.objectContaining({ + errors: [new GraphQLError('The recipient account is not activated')], + }), + ) + }) }) }) describe('errors in the transaction itself', () => { beforeAll(async () => { await mutate({ - query: login, + mutation: login, variables: bobData, }) }) @@ -254,13 +240,8 @@ describe('send coins', () => { }) }) - describe('transaction correct', () => { - it('sends the coins', async () => { - // make Peter Lustig Admin - const peter = await User.findOneOrFail({ id: user[0].id }) - peter.isAdmin = new Date() - await peter.save() - + describe('user has some GDD', () => { + beforeAll(async () => { // create contribution as user bob const contribution = await mutate({ mutation: createContribution, @@ -268,7 +249,7 @@ describe('send coins', () => { }) // login as admin - await query({ query: login, variables: peterData }) + await query({ mutation: login, variables: peterData }) // confirm the contribution await mutate({ @@ -277,42 +258,91 @@ describe('send coins', () => { }) // login as bob again - await query({ query: login, variables: bobData }) - - expect( - await mutate({ - mutation: sendCoins, - variables: { - email: 'peter@lustig.de', - amount: 100, - memo: 'testing', - }, - }), - ).toEqual( - expect.objectContaining({ - data: { - sendCoins: 'true', - }, - }), - ) + await query({ mutation: login, variables: bobData }) }) - it('stores the send transaction event in the database', async () => { - expect(EventProtocol.find()).resolves.toContainEqual( - expect.objectContaining({ - type: EventProtocolType.TRANSACTION_SEND, + afterAll(async () => { + await cleanDB() + }) + + describe('trying to send negative amount', () => { + it('throws an error', async () => { + expect( + await mutate({ + mutation: sendCoins, + variables: { + email: 'peter@lustig.de', + amount: -50, + memo: 'testing negative', + }, + }), + ).toEqual( + expect.objectContaining({ + errors: [new GraphQLError(`user hasn't enough GDD or amount is < 0`)], + }), + ) + }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + `user hasn't enough GDD or amount is < 0 : balance=null`, + ) + }) + }) + + describe('good transaction', () => { + it('sends the coins', async () => { + expect( + await mutate({ + mutation: sendCoins, + variables: { + email: 'peter@lustig.de', + amount: 50, + memo: 'unrepeateable memo', + }, + }), + ).toEqual( + expect.objectContaining({ + data: { + sendCoins: 'true', + }, + }), + ) + }) + + it('stores the send transaction event in the database', async () => { + // Find the exact transaction (sent one is the one with user[1] as user) + const transaction = await Transaction.find({ userId: user[1].id, - }), - ) - }) + memo: 'unrepeateable memo', + }) - it('stores the receive event in the database', async () => { - expect(EventProtocol.find()).resolves.toContainEqual( - expect.objectContaining({ - type: EventProtocolType.TRANSACTION_RECEIVE, + expect(EventProtocol.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventProtocolType.TRANSACTION_SEND, + userId: user[1].id, + transactionId: transaction[0].id, + xUserId: user[0].id, + }), + ) + }) + + it('stores the receive event in the database', async () => { + // Find the exact transaction (received one is the one with user[0] as user) + const transaction = await Transaction.find({ userId: user[0].id, - }), - ) + memo: 'unrepeateable memo', + }) + + expect(EventProtocol.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventProtocolType.TRANSACTION_RECEIVE, + userId: user[0].id, + transactionId: transaction[0].id, + xUserId: user[1].id, + }), + ) + }) }) }) }) diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 2145f2183..486ed87d2 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -76,7 +76,6 @@ export const executeTransaction = async ( ) logger.debug(`calculated Balance=${sendBalance}`) if (!sendBalance) { - // josejgi: wrong messages from my point of view, at this point balance is always null, should log inside calculate balance or handle returns in a different way logger.error(`user hasn't enough GDD or amount is < 0 : balance=${sendBalance}`) throw new Error("user hasn't enough GDD or amount is < 0") } diff --git a/backend/src/util/validate.ts b/backend/src/util/validate.ts index 8d1c90ca4..df1d4b1c0 100644 --- a/backend/src/util/validate.ts +++ b/backend/src/util/validate.ts @@ -26,6 +26,8 @@ async function calculateBalance( ): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> { const lastTransaction = await Transaction.findOne({ userId }, { order: { balanceDate: 'DESC' } }) if (!lastTransaction) return null + // negative amount should not be allowed + if (amount.greaterThan(0)) return null const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time) From 1848db1ba1f922ee47f98ab083585e039258ffbf Mon Sep 17 00:00:00 2001 From: joseji Date: Tue, 18 Oct 2022 19:51:10 +0200 Subject: [PATCH 06/14] added some log tests --- .../resolver/TransactionResolver.test.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index d5077d314..3fdff837e 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -19,6 +19,7 @@ import { User } from '@entity/User' import { cleanDB, testEnvironment } from '@test/helpers' import { logger } from '@test/testSetup' import { GraphQLError } from 'graphql' +import { findUserByEmail } from './UserResolver' let mutate: any, query: any, con: any let testEnv: any @@ -88,6 +89,10 @@ describe('send coins', () => { ) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith(`UserContact with email=wrong@email.com does not exists`) + }) + describe('deleted recipient', () => { it('throws an error', async () => { await mutate({ @@ -110,6 +115,14 @@ describe('send coins', () => { }), ) }) + + it('logs the error thrown', async () => { + // find peter to check the log + const user = await findUserByEmail(peterData.email) + expect(logger.error).toBeCalledWith( + `The recipient account was deleted: recipientUser=${user}`, + ) + }) }) describe('recipient account not activated', () => { @@ -134,6 +147,14 @@ describe('send coins', () => { }), ) }) + + it('logs the error thrown', async () => { + // find peter to check the log + const user = await findUserByEmail(peterData.email) + expect(logger.error).toBeCalledWith( + `The recipient account is not activated: recipientUser=${user}`, + ) + }) }) }) From d90d366946b8fb60dc737204dcbc65d416b69cab Mon Sep 17 00:00:00 2001 From: joseji Date: Tue, 18 Oct 2022 20:24:30 +0200 Subject: [PATCH 07/14] fixed unnecessary params --- backend/src/graphql/resolver/TransactionResolver.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index 3fdff837e..b3ad36fcd 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -51,13 +51,11 @@ describe('send coins', () => { bobData = { email: 'bob@baumeister.de', password: 'Aa12345_', - publisherId: 1234, } peterData = { email: 'peter@lustig.de', password: 'Aa12345_', - publisherId: 1234, } user = await User.find({ relations: ['emailContact'] }) From 259475755c591e2f014355b9afb4cd68977f7a81 Mon Sep 17 00:00:00 2001 From: joseji Date: Tue, 18 Oct 2022 23:24:43 +0200 Subject: [PATCH 08/14] fixed wrong responsabilities of exception throwing and implementation of calculateBalance() --- .../resolver/TransactionLinkResolver.ts | 5 +- .../resolver/TransactionResolver.test.ts | 69 +++++++++---------- .../graphql/resolver/TransactionResolver.ts | 35 ++++++---- backend/src/util/utilities.ts | 12 ++++ backend/src/util/validate.ts | 37 +++++++--- 5 files changed, 96 insertions(+), 62 deletions(-) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 4724956b4..4ba5dcd0b 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -74,10 +74,7 @@ export class TransactionLinkResolver { 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") - } + await calculateBalance(user.id, holdAvailableAmount, createdDate) const transactionLink = dbTransactionLink.create() transactionLink.userId = user.id diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index b3ad36fcd..d391f8ab9 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -16,7 +16,7 @@ import { stephenHawking } from '@/seeds/users/stephen-hawking' import { EventProtocol } from '@entity/EventProtocol' import { Transaction } from '@entity/Transaction' import { User } from '@entity/User' -import { cleanDB, testEnvironment } from '@test/helpers' +import { cleanDB, resetToken, testEnvironment } from '@test/helpers' import { logger } from '@test/testSetup' import { GraphQLError } from 'graphql' import { findUserByEmail } from './UserResolver' @@ -246,21 +246,49 @@ describe('send coins', () => { }), ).toEqual( expect.objectContaining({ - errors: [new GraphQLError(`user hasn't enough GDD or amount is < 0`)], + errors: [new GraphQLError(`User has not received any GDD yet`)], }), ) }) it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - `user hasn't enough GDD or amount is < 0 : balance=null`, + `No prior transaction found for user with id: ${user[1].id}`, ) }) }) + + describe('sending negative amount', () => { + it('throws an error', async () => { + expect( + await mutate({ + mutation: sendCoins, + variables: { + email: 'peter@lustig.de', + amount: -50, + memo: 'testing negative', + }, + }), + ).toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Transaction amount must be greater than 0')], + }), + ) + }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Transaction amount must be greater than 0: -50') + }) + }) }) describe('user has some GDD', () => { beforeAll(async () => { + resetToken() + + // login as bob again + await query({ mutation: login, variables: bobData }) + // create contribution as user bob const contribution = await mutate({ mutation: createContribution, @@ -280,35 +308,6 @@ describe('send coins', () => { await query({ mutation: login, variables: bobData }) }) - afterAll(async () => { - await cleanDB() - }) - - describe('trying to send negative amount', () => { - it('throws an error', async () => { - expect( - await mutate({ - mutation: sendCoins, - variables: { - email: 'peter@lustig.de', - amount: -50, - memo: 'testing negative', - }, - }), - ).toEqual( - expect.objectContaining({ - errors: [new GraphQLError(`user hasn't enough GDD or amount is < 0`)], - }), - ) - }) - - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( - `user hasn't enough GDD or amount is < 0 : balance=null`, - ) - }) - }) - describe('good transaction', () => { it('sends the coins', async () => { expect( @@ -317,7 +316,7 @@ describe('send coins', () => { variables: { email: 'peter@lustig.de', amount: 50, - memo: 'unrepeateable memo', + memo: 'unrepeatable memo', }, }), ).toEqual( @@ -333,7 +332,7 @@ describe('send coins', () => { // Find the exact transaction (sent one is the one with user[1] as user) const transaction = await Transaction.find({ userId: user[1].id, - memo: 'unrepeateable memo', + memo: 'unrepeatable memo', }) expect(EventProtocol.find()).resolves.toContainEqual( @@ -350,7 +349,7 @@ describe('send coins', () => { // Find the exact transaction (received one is the one with user[0] as user) const transaction = await Transaction.find({ userId: user[0].id, - memo: 'unrepeateable memo', + memo: 'unrepeatable memo', }) expect(EventProtocol.find()).resolves.toContainEqual( diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 486ed87d2..afe8a7974 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -39,6 +39,7 @@ import { findUserByEmail } from './UserResolver' import { sendTransactionLinkRedeemedEmail } from '@/mailer/sendTransactionLinkRedeemed' import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event' import { eventProtocol } from '@/event/EventProtocolEmitter' +import { Decay } from '../model/Decay' export const executeTransaction = async ( amount: Decimal, @@ -68,17 +69,8 @@ export const executeTransaction = async ( // validate amount const receivedCallDate = new Date() - const sendBalance = await calculateBalance( - sender.id, - amount.mul(-1), - receivedCallDate, - transactionLink, - ) - logger.debug(`calculated Balance=${sendBalance}`) - if (!sendBalance) { - logger.error(`user hasn't enough GDD or amount is < 0 : balance=${sendBalance}`) - throw new Error("user hasn't enough GDD or amount is < 0") - } + + const sendBalance = await calculateBalance(sender.id, amount, receivedCallDate, transactionLink) const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() @@ -91,7 +83,7 @@ export const executeTransaction = async ( transactionSend.memo = memo transactionSend.userId = sender.id transactionSend.linkedUserId = recipient.id - transactionSend.amount = amount.mul(-1) + transactionSend.amount = amount transactionSend.balance = sendBalance.balance transactionSend.balanceDate = receivedCallDate transactionSend.decay = sendBalance.decay.decay @@ -108,7 +100,24 @@ export const executeTransaction = async ( transactionReceive.userId = recipient.id transactionReceive.linkedUserId = sender.id transactionReceive.amount = amount - const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate) + + // state received balance + let receiveBalance: { + balance: Decimal + decay: Decay + lastTransactionId: number + } | null + + // try received balance + try { + receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate) + } catch (e) { + logger.info( + `User with no transactions sent: ${recipient.id}, has received a transaction of ${amount} GDD from user: ${sender.id}`, + ) + receiveBalance = null + } + transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount transactionReceive.balanceDate = receivedCallDate transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0) diff --git a/backend/src/util/utilities.ts b/backend/src/util/utilities.ts index 9abb31554..65214ebb5 100644 --- a/backend/src/util/utilities.ts +++ b/backend/src/util/utilities.ts @@ -1,5 +1,17 @@ +import Decimal from 'decimal.js-light' + export const objectValuesToArray = (obj: { [x: string]: string }): Array => { return Object.keys(obj).map(function (key) { return obj[key] }) } + +// to improve code readability, as String is needed, it is handled inside this utility function +export const decimalAddition = (a: Decimal, b: Decimal): Decimal => { + return a.add(b.toString()) +} + +// to improve code readability, as String is needed, it is handled inside this utility function +export const decimalSubtraction = (a: Decimal, b: Decimal): Decimal => { + return a.minus(b.toString()) +} diff --git a/backend/src/util/validate.ts b/backend/src/util/validate.ts index df1d4b1c0..64422b64f 100644 --- a/backend/src/util/validate.ts +++ b/backend/src/util/validate.ts @@ -5,6 +5,8 @@ import { Decay } from '@model/Decay' import { getCustomRepository } from '@dbTools/typeorm' import { TransactionLinkRepository } from '@repository/TransactionLink' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' +import { decimalSubtraction, decimalAddition } from './utilities' +import { logger } from '@test/testSetup' function isStringBoolean(value: string): boolean { const lowerValue = value.toLowerCase() @@ -23,16 +25,26 @@ async function calculateBalance( amount: Decimal, time: Date, transactionLink?: dbTransactionLink | null, -): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> { +): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number }> { + // negative or empty amount should not be allowed + if (amount.lessThanOrEqualTo(0)) { + logger.error(`Transaction amount must be greater than 0: ${amount}`) + throw new Error('Transaction amount must be greater than 0') + } + + // check if user has prior transactions const lastTransaction = await Transaction.findOne({ userId }, { order: { balanceDate: 'DESC' } }) - if (!lastTransaction) return null - // negative amount should not be allowed - if (amount.greaterThan(0)) return null + + if (!lastTransaction) { + logger.error(`No prior transaction found for user with id: ${userId}`) + throw new Error('User has not received any GDD yet') + } const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time) - // TODO why we have to use toString() here? - const balance = decay.balance.add(amount.toString()) + // new balance is the old balance minus the amount used + const balance = decimalSubtraction(decay.balance, amount) + const transactionLinkRepository = getCustomRepository(TransactionLinkRepository) const { sumHoldAvailableAmount } = await transactionLinkRepository.summary(userId, time) @@ -40,11 +52,16 @@ async function calculateBalance( // else we cannot redeem links which are more or equal to half of what an account actually owns const releasedLinkAmount = transactionLink ? transactionLink.holdAvailableAmount : new Decimal(0) - if ( - balance.minus(sumHoldAvailableAmount.toString()).plus(releasedLinkAmount.toString()).lessThan(0) - ) { - return null + const availableBalance = decimalSubtraction(balance, sumHoldAvailableAmount) + + if (decimalAddition(availableBalance, releasedLinkAmount).lessThan(0)) { + logger.error( + `Not enough funds for a transaction of ${amount} GDD, user with id: ${userId} has only ${balance} GDD available`, + ) + throw new Error('Not enough funds for transaction') } + + logger.debug(`calculated Balance=${balance}`) return { balance, lastTransactionId: lastTransaction.id, decay } } From 902222b9c895e2b37f55c83125a25f6252773e96 Mon Sep 17 00:00:00 2001 From: jjimenezgarcia <99907380+jjimenezgarcia@users.noreply.github.com> Date: Mon, 24 Oct 2022 08:05:37 +0200 Subject: [PATCH 09/14] Update backend/src/util/validate.ts Co-authored-by: Moriz Wahl --- backend/src/util/validate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/util/validate.ts b/backend/src/util/validate.ts index 64422b64f..9640cc614 100644 --- a/backend/src/util/validate.ts +++ b/backend/src/util/validate.ts @@ -6,7 +6,7 @@ import { getCustomRepository } from '@dbTools/typeorm' import { TransactionLinkRepository } from '@repository/TransactionLink' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' import { decimalSubtraction, decimalAddition } from './utilities' -import { logger } from '@test/testSetup' +import { backendLogger as logger } from '@/server/logger' function isStringBoolean(value: string): boolean { const lowerValue = value.toLowerCase() From f0c39703f17ea5862b175ad99a8afa0e2d9d8b39 Mon Sep 17 00:00:00 2001 From: joseji Date: Mon, 24 Oct 2022 22:01:19 +0200 Subject: [PATCH 10/14] fixed wrong removals of negative numbers --- backend/src/graphql/resolver/TransactionResolver.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 27af4015a..f0fb2f452 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -83,7 +83,7 @@ export const executeTransaction = async ( transactionSend.memo = memo transactionSend.userId = sender.id transactionSend.linkedUserId = recipient.id - transactionSend.amount = amount + transactionSend.amount = amount.mul(-1) transactionSend.balance = sendBalance.balance transactionSend.balanceDate = receivedCallDate transactionSend.decay = sendBalance.decay.decay @@ -151,7 +151,7 @@ export const executeTransaction = async ( eventTransactionSend.userId = transactionSend.userId eventTransactionSend.xUserId = transactionSend.linkedUserId eventTransactionSend.transactionId = transactionSend.id - eventTransactionSend.amount = transactionSend.amount + eventTransactionSend.amount = transactionSend.amount.mul(-1) await eventProtocol.writeEvent(new Event().setEventTransactionSend(eventTransactionSend)) const eventTransactionReceive = new EventTransactionReceive() From fcd9e1776ce8c36571665e9c38cbdba9da71501f Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 20 Oct 2022 11:51:37 +0200 Subject: [PATCH 11/14] fix: Include Deleted Email Contacts in User Search --- backend/src/typeorm/repository/User.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/typeorm/repository/User.ts b/backend/src/typeorm/repository/User.ts index 8b3e29859..c20ef85ff 100644 --- a/backend/src/typeorm/repository/User.ts +++ b/backend/src/typeorm/repository/User.ts @@ -28,8 +28,8 @@ export class UserRepository extends Repository { ): Promise<[DbUser[], number]> { const query = this.createQueryBuilder('user') .select(select) - .leftJoinAndSelect('user.emailContact', 'emailContact') .withDeleted() + .leftJoinAndSelect('user.emailContact', 'emailContact') .where( new Brackets((qb) => { qb.where( From 7df64eac226b91cf63d4b3d88509a081816e5459 Mon Sep 17 00:00:00 2001 From: ogerly Date: Thu, 20 Oct 2022 06:20:14 +0200 Subject: [PATCH 12/14] changed text --- frontend/src/locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index a9ad47911..a9aee274d 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -34,7 +34,7 @@ "contribution": { "activity": "Tätigkeit", "alert": { - "answerQuestion": "Bitte beantworte die Nachfrage", + "answerQuestion": "Bitte beantworte die Rückfrage!", "communityNoteList": "Hier findest du alle eingereichten und bestätigten Beiträge von allen Mitgliedern aus dieser Gemeinschaft.", "confirm": "bestätigt", "in_progress": "Es gibt eine Rückfrage der Moderatoren.", From 5d3dda78def40c3ca81a574e65a025c0b3fb3a70 Mon Sep 17 00:00:00 2001 From: ogerly Date: Thu, 20 Oct 2022 11:46:19 +0200 Subject: [PATCH 13/14] change E-Mail Subject --- backend/src/mailer/sendAddedContributionMessageEmail.test.ts | 2 +- backend/src/mailer/text/contributionMessageReceived.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/mailer/sendAddedContributionMessageEmail.test.ts b/backend/src/mailer/sendAddedContributionMessageEmail.test.ts index 1151a0abc..bed8f6214 100644 --- a/backend/src/mailer/sendAddedContributionMessageEmail.test.ts +++ b/backend/src/mailer/sendAddedContributionMessageEmail.test.ts @@ -26,7 +26,7 @@ describe('sendAddedContributionMessageEmail', () => { it('calls sendEMail', () => { expect(sendEMail).toBeCalledWith({ to: `Bibi Bloxberg `, - subject: 'Gradido Frage zur Schöpfung', + subject: 'Rückfrage zu Deinem Gemeinwohl-Beitrag', text: expect.stringContaining('Hallo Bibi Bloxberg') && expect.stringContaining('Peter Lustig') && diff --git a/backend/src/mailer/text/contributionMessageReceived.ts b/backend/src/mailer/text/contributionMessageReceived.ts index b0c9c4d30..af1cabb9f 100644 --- a/backend/src/mailer/text/contributionMessageReceived.ts +++ b/backend/src/mailer/text/contributionMessageReceived.ts @@ -1,6 +1,6 @@ export const contributionMessageReceived = { de: { - subject: 'Gradido Frage zur Schöpfung', + subject: 'Rückfrage zu Deinem Gemeinwohl-Beitrag', text: (data: { senderFirstName: string senderLastName: string From 447bf02a42754310ff43090a575b6d8438a378aa Mon Sep 17 00:00:00 2001 From: ogerly Date: Thu, 20 Oct 2022 13:36:47 +0200 Subject: [PATCH 14/14] fix Pagination Contributions jumps to wrong Page --- frontend/src/pages/Community.vue | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/pages/Community.vue b/frontend/src/pages/Community.vue index c98bfff2d..786307405 100644 --- a/frontend/src/pages/Community.vue +++ b/frontend/src/pages/Community.vue @@ -231,8 +231,6 @@ export default { this.items = listContributions.contributionList if (this.items.find((item) => item.state === 'IN_PROGRESS')) { this.tabIndex = 1 - } else { - this.tabIndex = 0 } }) .catch((err) => {