diff --git a/federation/src/config/index.ts b/federation/src/config/index.ts index aceb15e98..74a53ed1b 100644 --- a/federation/src/config/index.ts +++ b/federation/src/config/index.ts @@ -1,14 +1,12 @@ // ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env) +import { Decimal } from 'decimal.js-light' import dotenv from 'dotenv' dotenv.config() -/* -import Decimal from 'decimal.js-light' Decimal.set({ precision: 25, rounding: Decimal.ROUND_HALF_UP, }) -*/ const constants = { DB_VERSION: '0071-add-pending_transactions-table', diff --git a/federation/src/graphql/api/1_0/model/SendCoinsArgs.ts b/federation/src/graphql/api/1_0/model/SendCoinsArgs.ts index 3d15c04b1..545aab822 100644 --- a/federation/src/graphql/api/1_0/model/SendCoinsArgs.ts +++ b/federation/src/graphql/api/1_0/model/SendCoinsArgs.ts @@ -9,8 +9,8 @@ export class SendCoinsArgs { @Field(() => String) userReceiverIdentifier: string - @Field(() => Date) - creationDate: Date + @Field(() => String) + creationDate: string @Field(() => Decimal) amount: Decimal diff --git a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.test.ts b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.test.ts new file mode 100644 index 000000000..a9ea7e068 --- /dev/null +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.test.ts @@ -0,0 +1,193 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { ApolloServerTestClient } from 'apollo-server-testing' +import { Community as DbCommunity } from '@entity/Community' +import CONFIG from '@/config' +import { User as DbUser } from '@entity/User' +import { fullName } from '@/graphql/util/fullName' +import { GraphQLError } from 'graphql' +import { cleanDB, testEnvironment } from '@test/helpers' +import { logger } from '@test/testSetup' +import { Connection } from '@dbTools/typeorm' + +let mutate: ApolloServerTestClient['mutate'], con: Connection +// let query: ApolloServerTestClient['query'] + +let testEnv: { + mutate: ApolloServerTestClient['mutate'] + query: ApolloServerTestClient['query'] + con: Connection +} + +CONFIG.FEDERATION_API = '1_0' + +beforeAll(async () => { + testEnv = await testEnvironment(logger) + mutate = testEnv.mutate + // query = testEnv.query + con = testEnv.con + + // const server = await createServer() + // con = server.con + // query = createTestClient(server.apollo).query + // mutate = createTestClient(server.apollo).mutate + // DbCommunity.clear() + // DbUser.clear() + await cleanDB() +}) + +afterAll(async () => { + // await cleanDB() + await con.destroy() +}) + +describe('SendCoinsResolver', () => { + const voteForSendCoinsMutation = ` + mutation ( + $communityReceiverIdentifier: String! + $userReceiverIdentifier: String! + $creationDate: String! + $amount: Decimal! + $memo: String! + $communitySenderIdentifier: String! + $userSenderIdentifier: String! + $userSenderName: String! + ) { + voteForSendCoins( + communityReceiverIdentifier: $communityReceiverIdentifier + userReceiverIdentifier: $userReceiverIdentifier + creationDate: $creationDate + amount: $amount + memo: $memo + communitySenderIdentifier: $communitySenderIdentifier + userSenderIdentifier: $userSenderIdentifier + userSenderName: $userSenderName + ) + } +` + + describe('voteForSendCoins', () => { + let homeCom: DbCommunity + let foreignCom: DbCommunity + let sendUser: DbUser + let recipUser: DbUser + + beforeEach(async () => { + await cleanDB() + homeCom = DbCommunity.create() + homeCom.foreign = false + homeCom.url = 'homeCom-url' + homeCom.name = 'homeCom-Name' + homeCom.description = 'homeCom-Description' + homeCom.creationDate = new Date() + homeCom.publicKey = Buffer.from('homeCom-publicKey') + homeCom.communityUuid = 'homeCom-UUID' + await DbCommunity.insert(homeCom) + + foreignCom = DbCommunity.create() + foreignCom.foreign = true + foreignCom.url = 'foreignCom-url' + foreignCom.name = 'foreignCom-Name' + foreignCom.description = 'foreignCom-Description' + foreignCom.creationDate = new Date() + foreignCom.publicKey = Buffer.from('foreignCom-publicKey') + foreignCom.communityUuid = 'foreignCom-UUID' + await DbCommunity.insert(foreignCom) + + sendUser = DbUser.create() + sendUser.alias = 'sendUser-alias' + sendUser.firstName = 'sendUser-FirstName' + sendUser.gradidoID = 'sendUser-GradidoID' + sendUser.lastName = 'sendUser-LastName' + await DbUser.insert(sendUser) + + recipUser = DbUser.create() + recipUser.alias = 'recipUser-alias' + recipUser.firstName = 'recipUser-FirstName' + recipUser.gradidoID = 'recipUser-GradidoID' + recipUser.lastName = 'recipUser-LastName' + await DbUser.insert(recipUser) + }) + + describe('unknown recipient community', () => { + it('throws an error', async () => { + jest.clearAllMocks() + expect( + await mutate({ + mutation: voteForSendCoinsMutation, + variables: { + communityReceiverIdentifier: 'invalid foreignCom', + userReceiverIdentifier: recipUser.gradidoID, + creationDate: new Date().toISOString(), + amount: 100, + memo: 'X-Com-TX memo', + communitySenderIdentifier: homeCom.communityUuid, + userSenderIdentifier: sendUser.gradidoID, + userSenderName: fullName(sendUser.firstName, sendUser.lastName), + }, + }), + ).toEqual( + expect.objectContaining({ + errors: [new GraphQLError('voteForSendCoins with wrong communityReceiverIdentifier')], + }), + ) + }) + }) + + describe('unknown recipient user', () => { + it('throws an error', async () => { + jest.clearAllMocks() + expect( + await mutate({ + mutation: voteForSendCoinsMutation, + variables: { + communityReceiverIdentifier: foreignCom.communityUuid, + userReceiverIdentifier: 'invalid recipient', + creationDate: new Date().toISOString(), + amount: 100, + memo: 'X-Com-TX memo', + communitySenderIdentifier: homeCom.communityUuid, + userSenderIdentifier: sendUser.gradidoID, + userSenderName: fullName(sendUser.firstName, sendUser.lastName), + }, + }), + ).toEqual( + expect.objectContaining({ + errors: [ + new GraphQLError( + 'voteForSendCoins with unknown userReceiverIdentifier in the community=', + ), + ], + }), + ) + }) + }) + + describe('valid X-Com-TX voted', () => { + it('throws an error', async () => { + jest.clearAllMocks() + expect( + await mutate({ + mutation: voteForSendCoinsMutation, + variables: { + communityReceiverIdentifier: foreignCom.communityUuid, + userReceiverIdentifier: recipUser.gradidoID, + creationDate: new Date().toISOString(), + amount: 100, + memo: 'X-Com-TX memo', + communitySenderIdentifier: homeCom.communityUuid, + userSenderIdentifier: sendUser.gradidoID, + userSenderName: fullName(sendUser.firstName, sendUser.lastName), + }, + }), + ).toEqual( + expect.objectContaining({ + data: { + voteForSendCoins: 'recipUser-FirstName recipUser-LastName', + }, + }), + ) + }) + }) + }) +}) diff --git a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts index ba23ae530..4af6c005b 100644 --- a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts @@ -15,7 +15,7 @@ import { fullName } from '@/graphql/util/fullName' @Resolver() // eslint-disable-next-line @typescript-eslint/no-unused-vars export class SendCoinsResolver { - @Mutation(() => Boolean) + @Mutation(() => String) async voteForSendCoins( @Args() { @@ -31,30 +31,31 @@ export class SendCoinsResolver { ): Promise { logger.debug(`voteForSendCoins() via apiVersion=1_0 ...`) let result: string | null = null + // first check if receiver community is correct + const homeCom = await DbCommunity.findOneBy({ + communityUuid: communityReceiverIdentifier, + }) + if (!homeCom) { + throw new LogError( + `voteForSendCoins with wrong communityReceiverIdentifier`, + communityReceiverIdentifier, + ) + } + // second check if receiver user exists in this community + const receiverUser = await DbUser.findOneBy({ gradidoID: userReceiverIdentifier }) + if (!receiverUser) { + throw new LogError( + `voteForSendCoins with unknown userReceiverIdentifier in the community=`, + homeCom.name, + ) + } try { - // first check if receiver community is correct - const homeCom = await DbCommunity.findOneBy({ - communityUuid: communityReceiverIdentifier, - }) - if (!homeCom) { - throw new LogError( - `voteForSendCoins with wrong communityReceiverIdentifier`, - communityReceiverIdentifier, - ) - } - // second check if receiver user exists in this community - const receiverUser = await DbUser.findOneBy({ gradidoID: userReceiverIdentifier }) - if (!receiverUser) { - throw new LogError( - `voteForSendCoins with unknown userReceiverIdentifier in the community=`, - homeCom.name, - ) - } - const receiveBalance = await calculateRecepientBalance(receiverUser.id, amount, creationDate) + const txDate = new Date(creationDate) + const receiveBalance = await calculateRecepientBalance(receiverUser.id, amount, txDate) const pendingTx = DbPendingTransaction.create() pendingTx.amount = amount pendingTx.balance = receiveBalance ? receiveBalance.balance : new Decimal(0) - pendingTx.balanceDate = creationDate + pendingTx.balanceDate = txDate pendingTx.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0) pendingTx.decayStart = receiveBalance ? receiveBalance.decay.start : null pendingTx.linkedUserCommunityUuid = communitySenderIdentifier @@ -64,6 +65,7 @@ export class SendCoinsResolver { pendingTx.previous = receiveBalance ? receiveBalance.lastTransactionId : null pendingTx.state = PendingTransactionState.NEW pendingTx.typeId = TransactionTypeId.RECEIVE + pendingTx.userId = receiverUser.id pendingTx.userCommunityUuid = communityReceiverIdentifier pendingTx.userGradidoID = userReceiverIdentifier pendingTx.userName = fullName(receiverUser.firstName, receiverUser.lastName) diff --git a/federation/test/helpers.test.ts b/federation/test/helpers.test.ts new file mode 100644 index 000000000..69d8f3fa4 --- /dev/null +++ b/federation/test/helpers.test.ts @@ -0,0 +1,7 @@ +import { contributionDateFormatter } from '@test/helpers' + +describe('contributionDateFormatter', () => { + it('formats the date correctly', () => { + expect(contributionDateFormatter(new Date('Thu Feb 29 2024 13:12:11'))).toEqual('2/29/2024') + }) +}) diff --git a/federation/test/helpers.ts b/federation/test/helpers.ts new file mode 100644 index 000000000..542906f49 --- /dev/null +++ b/federation/test/helpers.ts @@ -0,0 +1,63 @@ +/* eslint-disable @typescript-eslint/unbound-method */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ + +import { entities } from '@entity/index' +import { createTestClient } from 'apollo-server-testing' + +import createServer from '@/server/createServer' + +import { logger } from './testSetup' + +export const headerPushMock = jest.fn((t) => { + context.token = t.value +}) + +const context = { + token: '', + setHeaders: { + push: headerPushMock, + forEach: jest.fn(), + }, + clientTimezoneOffset: 0, +} + +export const cleanDB = async () => { + // this only works as long we do not have foreign key constraints + for (const entity of entities) { + await resetEntity(entity) + } +} + +export const testEnvironment = async (testLogger = logger) => { + const server = await createServer(testLogger) // context, testLogger, testI18n) + const con = server.con + const testClient = createTestClient(server.apollo) + const mutate = testClient.mutate + const query = testClient.query + return { mutate, query, con } +} + +export const resetEntity = async (entity: any) => { + const items = await entity.find({ withDeleted: true }) + if (items.length > 0) { + const ids = items.map((e: any) => e.id) + await entity.delete(ids) + } +} + +export const resetToken = () => { + context.token = '' +} + +// format date string as it comes from the frontend for the contribution date +export const contributionDateFormatter = (date: Date): string => { + return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}` +} + +export const setClientTimezoneOffset = (offset: number): void => { + context.clientTimezoneOffset = offset +}