From 7c4d55928fa3a1eeeaecbc5f3f26aaa23deb11db Mon Sep 17 00:00:00 2001 From: Einhornimmond Date: Wed, 9 Aug 2023 13:29:41 +0200 Subject: [PATCH 01/22] move memo length validation into args object like it is recommended for typed graphql --- .../src/graphql/arg/AdminCreateContributionArgs.ts | 5 +++++ .../src/graphql/arg/AdminUpdateContributionArgs.ts | 5 +++++ backend/src/graphql/arg/ContributionArgs.ts | 5 +++++ backend/src/graphql/arg/ContributionLinkArgs.ts | 5 +++++ backend/src/graphql/arg/TransactionLinkArgs.ts | 5 +++++ backend/src/graphql/arg/TransactionSendArgs.ts | 5 +++++ .../graphql/resolver/ContributionLinkResolver.ts | 13 +------------ .../src/graphql/resolver/ContributionResolver.ts | 13 ------------- backend/src/graphql/resolver/TransactionResolver.ts | 9 --------- 9 files changed, 31 insertions(+), 34 deletions(-) diff --git a/backend/src/graphql/arg/AdminCreateContributionArgs.ts b/backend/src/graphql/arg/AdminCreateContributionArgs.ts index 8e2fa28da..1c6007c48 100644 --- a/backend/src/graphql/arg/AdminCreateContributionArgs.ts +++ b/backend/src/graphql/arg/AdminCreateContributionArgs.ts @@ -1,6 +1,9 @@ +import { MaxLength, MinLength } from 'class-validator' import { Decimal } from 'decimal.js-light' import { ArgsType, Field, InputType } from 'type-graphql' +import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const' + @InputType() @ArgsType() export class AdminCreateContributionArgs { @@ -11,6 +14,8 @@ export class AdminCreateContributionArgs { amount: Decimal @Field(() => String) + @MaxLength(MEMO_MAX_CHARS) + @MinLength(MEMO_MIN_CHARS) memo: string @Field(() => String) diff --git a/backend/src/graphql/arg/AdminUpdateContributionArgs.ts b/backend/src/graphql/arg/AdminUpdateContributionArgs.ts index e79260c63..4958b4346 100644 --- a/backend/src/graphql/arg/AdminUpdateContributionArgs.ts +++ b/backend/src/graphql/arg/AdminUpdateContributionArgs.ts @@ -1,6 +1,9 @@ +import { MaxLength, MinLength } from 'class-validator' import { Decimal } from 'decimal.js-light' import { ArgsType, Field, Int } from 'type-graphql' +import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const' + @ArgsType() export class AdminUpdateContributionArgs { @Field(() => Int) @@ -10,6 +13,8 @@ export class AdminUpdateContributionArgs { amount: Decimal @Field(() => String) + @MaxLength(MEMO_MAX_CHARS) + @MinLength(MEMO_MIN_CHARS) memo: string @Field(() => String) diff --git a/backend/src/graphql/arg/ContributionArgs.ts b/backend/src/graphql/arg/ContributionArgs.ts index db688d811..e2449ea95 100644 --- a/backend/src/graphql/arg/ContributionArgs.ts +++ b/backend/src/graphql/arg/ContributionArgs.ts @@ -1,6 +1,9 @@ +import { MaxLength, MinLength } from 'class-validator' import { Decimal } from 'decimal.js-light' import { ArgsType, Field, InputType } from 'type-graphql' +import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const' + @InputType() @ArgsType() export class ContributionArgs { @@ -8,6 +11,8 @@ export class ContributionArgs { amount: Decimal @Field(() => String) + @MaxLength(MEMO_MAX_CHARS) + @MinLength(MEMO_MIN_CHARS) memo: string @Field(() => String) diff --git a/backend/src/graphql/arg/ContributionLinkArgs.ts b/backend/src/graphql/arg/ContributionLinkArgs.ts index cef72148a..9d12a6128 100644 --- a/backend/src/graphql/arg/ContributionLinkArgs.ts +++ b/backend/src/graphql/arg/ContributionLinkArgs.ts @@ -1,6 +1,9 @@ +import { MaxLength, MinLength } from 'class-validator' import { Decimal } from 'decimal.js-light' import { ArgsType, Field, Int } from 'type-graphql' +import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const' + @ArgsType() export class ContributionLinkArgs { @Field(() => Decimal) @@ -10,6 +13,8 @@ export class ContributionLinkArgs { name: string @Field(() => String) + @MaxLength(MEMO_MAX_CHARS) + @MinLength(MEMO_MIN_CHARS) memo: string @Field(() => String) diff --git a/backend/src/graphql/arg/TransactionLinkArgs.ts b/backend/src/graphql/arg/TransactionLinkArgs.ts index f44ef0356..2ffd91d33 100644 --- a/backend/src/graphql/arg/TransactionLinkArgs.ts +++ b/backend/src/graphql/arg/TransactionLinkArgs.ts @@ -1,11 +1,16 @@ +import { MaxLength, MinLength } from 'class-validator' import { Decimal } from 'decimal.js-light' import { ArgsType, Field } from 'type-graphql' +import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const' + @ArgsType() export class TransactionLinkArgs { @Field(() => Decimal) amount: Decimal @Field(() => String) + @MaxLength(MEMO_MAX_CHARS) + @MinLength(MEMO_MIN_CHARS) memo: string } diff --git a/backend/src/graphql/arg/TransactionSendArgs.ts b/backend/src/graphql/arg/TransactionSendArgs.ts index ecda848d1..1279f2051 100644 --- a/backend/src/graphql/arg/TransactionSendArgs.ts +++ b/backend/src/graphql/arg/TransactionSendArgs.ts @@ -1,6 +1,9 @@ +import { MaxLength, MinLength } from 'class-validator' import { Decimal } from 'decimal.js-light' import { ArgsType, Field } from 'type-graphql' +import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const' + @ArgsType() export class TransactionSendArgs { @Field(() => String) @@ -10,5 +13,7 @@ export class TransactionSendArgs { amount: Decimal @Field(() => String) + @MaxLength(MEMO_MAX_CHARS) + @MinLength(MEMO_MIN_CHARS) memo: string } diff --git a/backend/src/graphql/resolver/ContributionLinkResolver.ts b/backend/src/graphql/resolver/ContributionLinkResolver.ts index 808bd584e..42593dec1 100644 --- a/backend/src/graphql/resolver/ContributionLinkResolver.ts +++ b/backend/src/graphql/resolver/ContributionLinkResolver.ts @@ -18,12 +18,7 @@ import { import { Context, getUser } from '@/server/context' import { LogError } from '@/server/LogError' -import { - CONTRIBUTIONLINK_NAME_MAX_CHARS, - CONTRIBUTIONLINK_NAME_MIN_CHARS, - MEMO_MAX_CHARS, - MEMO_MIN_CHARS, -} from './const/const' +import { CONTRIBUTIONLINK_NAME_MAX_CHARS, CONTRIBUTIONLINK_NAME_MIN_CHARS } from './const/const' import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' import { isStartEndDateValid } from './util/creations' @@ -52,12 +47,6 @@ export class ContributionLinkResolver { if (name.length > CONTRIBUTIONLINK_NAME_MAX_CHARS) { throw new LogError('The value of name is too long', name.length) } - if (memo.length < MEMO_MIN_CHARS) { - throw new LogError('The value of memo is too short', memo.length) - } - if (memo.length > MEMO_MAX_CHARS) { - throw new LogError('The value of memo is too long', memo.length) - } if (!new Decimal(amount).isPositive()) { throw new LogError('The amount must be a positiv value', amount) } diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 80ea3e783..7b4c21708 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -45,7 +45,6 @@ import { calculateDecay } from '@/util/decay' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { fullName } from '@/util/utilities' -import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { getUserCreation, validateContribution, @@ -65,12 +64,6 @@ export class ContributionResolver { @Ctx() context: Context, ): Promise { const clientTimezoneOffset = getClientTimezoneOffset(context) - if (memo.length < MEMO_MIN_CHARS) { - throw new LogError('Memo text is too short', memo.length) - } - if (memo.length > MEMO_MAX_CHARS) { - throw new LogError('Memo text is too long', memo.length) - } const user = getUser(context) const creations = await getUserCreation(user.id, clientTimezoneOffset) @@ -186,12 +179,6 @@ export class ContributionResolver { @Ctx() context: Context, ): Promise { const clientTimezoneOffset = getClientTimezoneOffset(context) - if (memo.length < MEMO_MIN_CHARS) { - throw new LogError('Memo text is too short', memo.length) - } - if (memo.length > MEMO_MAX_CHARS) { - throw new LogError('Memo text is too long', memo.length) - } const user = getUser(context) diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 5f42fb36e..721cba8fa 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -33,7 +33,6 @@ import { calculateBalance } from '@/util/validate' import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions' import { BalanceResolver } from './BalanceResolver' -import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { findUserByIdentifier } from './util/findUserByIdentifier' import { getLastTransaction } from './util/getLastTransaction' import { getTransactionList } from './util/getTransactionList' @@ -55,14 +54,6 @@ export const executeTransaction = async ( throw new LogError('Sender and Recipient are the same', sender.id) } - if (memo.length < MEMO_MIN_CHARS) { - throw new LogError('Memo text is too short', memo.length) - } - - if (memo.length > MEMO_MAX_CHARS) { - throw new LogError('Memo text is too long', memo.length) - } - // validate amount const receivedCallDate = new Date() const sendBalance = await calculateBalance( From 51d1c6b64bf95ce7fc388c8870f10fba0437bb39 Mon Sep 17 00:00:00 2001 From: Einhornimmond Date: Wed, 9 Aug 2023 13:55:46 +0200 Subject: [PATCH 02/22] add logging for automatic validation --- backend/src/graphql/schema.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/backend/src/graphql/schema.ts b/backend/src/graphql/schema.ts index f14276c86..98af37159 100644 --- a/backend/src/graphql/schema.ts +++ b/backend/src/graphql/schema.ts @@ -1,9 +1,12 @@ import path from 'path' +import { validate } from 'class-validator' import { Decimal } from 'decimal.js-light' import { GraphQLSchema } from 'graphql' import { buildSchema } from 'type-graphql' +import { LogError } from '@/server/LogError' + import { isAuthorized } from './directive/isAuthorized' import { DecimalScalar } from './scalar/Decimal' @@ -12,5 +15,20 @@ export const schema = async (): Promise => { resolvers: [path.join(__dirname, 'resolver', `!(*.test).{js,ts}`)], authChecker: isAuthorized, scalarsMap: [{ type: Decimal, scalar: DecimalScalar }], + validate: (argValue) => { + if (argValue) { + validate(argValue) + .then((errors) => { + if (errors.length > 0) { + throw new LogError('validation failed. errors: ', errors) + } else { + return true + } + }) + .catch((e) => { + throw new LogError('validation throw an exception: ', e) + }) + } + }, }) } From 191103a812a4e9b8dbc0e764dbd3153d65860dec Mon Sep 17 00:00:00 2001 From: Einhornimmond Date: Wed, 9 Aug 2023 13:59:29 +0200 Subject: [PATCH 03/22] move CONTRIBUTIONLINK_NAME validation into arg --- backend/src/graphql/arg/ContributionLinkArgs.ts | 9 ++++++++- backend/src/graphql/resolver/ContributionLinkResolver.ts | 8 +------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/backend/src/graphql/arg/ContributionLinkArgs.ts b/backend/src/graphql/arg/ContributionLinkArgs.ts index 9d12a6128..59688a969 100644 --- a/backend/src/graphql/arg/ContributionLinkArgs.ts +++ b/backend/src/graphql/arg/ContributionLinkArgs.ts @@ -2,7 +2,12 @@ import { MaxLength, MinLength } from 'class-validator' import { Decimal } from 'decimal.js-light' import { ArgsType, Field, Int } from 'type-graphql' -import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const' +import { + MEMO_MAX_CHARS, + MEMO_MIN_CHARS, + CONTRIBUTIONLINK_NAME_MIN_CHARS, + CONTRIBUTIONLINK_NAME_MAX_CHARS, +} from '@/graphql/resolver/const/const' @ArgsType() export class ContributionLinkArgs { @@ -10,6 +15,8 @@ export class ContributionLinkArgs { amount: Decimal @Field(() => String) + @MaxLength(CONTRIBUTIONLINK_NAME_MAX_CHARS) + @MinLength(CONTRIBUTIONLINK_NAME_MIN_CHARS) name: string @Field(() => String) diff --git a/backend/src/graphql/resolver/ContributionLinkResolver.ts b/backend/src/graphql/resolver/ContributionLinkResolver.ts index 42593dec1..e8a694a55 100644 --- a/backend/src/graphql/resolver/ContributionLinkResolver.ts +++ b/backend/src/graphql/resolver/ContributionLinkResolver.ts @@ -18,7 +18,6 @@ import { import { Context, getUser } from '@/server/context' import { LogError } from '@/server/LogError' -import { CONTRIBUTIONLINK_NAME_MAX_CHARS, CONTRIBUTIONLINK_NAME_MIN_CHARS } from './const/const' import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' import { isStartEndDateValid } from './util/creations' @@ -41,12 +40,7 @@ export class ContributionLinkResolver { @Ctx() context: Context, ): Promise { isStartEndDateValid(validFrom, validTo) - if (name.length < CONTRIBUTIONLINK_NAME_MIN_CHARS) { - throw new LogError('The value of name is too short', name.length) - } - if (name.length > CONTRIBUTIONLINK_NAME_MAX_CHARS) { - throw new LogError('The value of name is too long', name.length) - } + if (!new Decimal(amount).isPositive()) { throw new LogError('The amount must be a positiv value', amount) } From 5e3d732e20eb6d0dbde98dda491e0d55c189e676 Mon Sep 17 00:00:00 2001 From: Einhornimmond Date: Wed, 9 Aug 2023 14:08:29 +0200 Subject: [PATCH 04/22] add validator for positive amount value with Decimal Type --- .../arg/AdminCreateContributionArgs.ts | 2 ++ .../arg/AdminUpdateContributionArgs.ts | 2 ++ backend/src/graphql/arg/ContributionArgs.ts | 2 ++ .../src/graphql/arg/ContributionLinkArgs.ts | 2 ++ .../src/graphql/arg/TransactionLinkArgs.ts | 4 +++- .../src/graphql/arg/TransactionSendArgs.ts | 2 ++ .../resolver/ContributionLinkResolver.ts | 4 ---- .../resolver/TransactionLinkResolver.ts | 4 ---- .../graphql/resolver/TransactionResolver.ts | 3 --- backend/src/graphql/validator/Decimal.ts | 22 +++++++++++++++++++ 10 files changed, 35 insertions(+), 12 deletions(-) create mode 100644 backend/src/graphql/validator/Decimal.ts diff --git a/backend/src/graphql/arg/AdminCreateContributionArgs.ts b/backend/src/graphql/arg/AdminCreateContributionArgs.ts index 1c6007c48..558821921 100644 --- a/backend/src/graphql/arg/AdminCreateContributionArgs.ts +++ b/backend/src/graphql/arg/AdminCreateContributionArgs.ts @@ -3,6 +3,7 @@ import { Decimal } from 'decimal.js-light' import { ArgsType, Field, InputType } from 'type-graphql' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const' +import { IsPositiveDecimal } from '@/graphql/validator/Decimal' @InputType() @ArgsType() @@ -11,6 +12,7 @@ export class AdminCreateContributionArgs { email: string @Field(() => Decimal) + @IsPositiveDecimal() amount: Decimal @Field(() => String) diff --git a/backend/src/graphql/arg/AdminUpdateContributionArgs.ts b/backend/src/graphql/arg/AdminUpdateContributionArgs.ts index 4958b4346..c5f560c9a 100644 --- a/backend/src/graphql/arg/AdminUpdateContributionArgs.ts +++ b/backend/src/graphql/arg/AdminUpdateContributionArgs.ts @@ -3,6 +3,7 @@ import { Decimal } from 'decimal.js-light' import { ArgsType, Field, Int } from 'type-graphql' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const' +import { IsPositiveDecimal } from '@/graphql/validator/Decimal' @ArgsType() export class AdminUpdateContributionArgs { @@ -10,6 +11,7 @@ export class AdminUpdateContributionArgs { id: number @Field(() => Decimal) + @IsPositiveDecimal() amount: Decimal @Field(() => String) diff --git a/backend/src/graphql/arg/ContributionArgs.ts b/backend/src/graphql/arg/ContributionArgs.ts index e2449ea95..d4c9e639b 100644 --- a/backend/src/graphql/arg/ContributionArgs.ts +++ b/backend/src/graphql/arg/ContributionArgs.ts @@ -3,11 +3,13 @@ import { Decimal } from 'decimal.js-light' import { ArgsType, Field, InputType } from 'type-graphql' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const' +import { IsPositiveDecimal } from '@/graphql/validator/Decimal' @InputType() @ArgsType() export class ContributionArgs { @Field(() => Decimal) + @IsPositiveDecimal() amount: Decimal @Field(() => String) diff --git a/backend/src/graphql/arg/ContributionLinkArgs.ts b/backend/src/graphql/arg/ContributionLinkArgs.ts index 59688a969..4aa268d28 100644 --- a/backend/src/graphql/arg/ContributionLinkArgs.ts +++ b/backend/src/graphql/arg/ContributionLinkArgs.ts @@ -8,10 +8,12 @@ import { CONTRIBUTIONLINK_NAME_MIN_CHARS, CONTRIBUTIONLINK_NAME_MAX_CHARS, } from '@/graphql/resolver/const/const' +import { IsPositiveDecimal } from '@/graphql/validator/Decimal' @ArgsType() export class ContributionLinkArgs { @Field(() => Decimal) + @IsPositiveDecimal() amount: Decimal @Field(() => String) diff --git a/backend/src/graphql/arg/TransactionLinkArgs.ts b/backend/src/graphql/arg/TransactionLinkArgs.ts index 2ffd91d33..6dfdda15e 100644 --- a/backend/src/graphql/arg/TransactionLinkArgs.ts +++ b/backend/src/graphql/arg/TransactionLinkArgs.ts @@ -1,12 +1,14 @@ -import { MaxLength, MinLength } from 'class-validator' +import { IsAlpha, MaxLength, MinLength } from 'class-validator' import { Decimal } from 'decimal.js-light' import { ArgsType, Field } from 'type-graphql' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const' +import { IsPositiveDecimal } from '@/graphql/validator/Decimal' @ArgsType() export class TransactionLinkArgs { @Field(() => Decimal) + @IsPositiveDecimal() amount: Decimal @Field(() => String) diff --git a/backend/src/graphql/arg/TransactionSendArgs.ts b/backend/src/graphql/arg/TransactionSendArgs.ts index 1279f2051..d8a556b99 100644 --- a/backend/src/graphql/arg/TransactionSendArgs.ts +++ b/backend/src/graphql/arg/TransactionSendArgs.ts @@ -3,6 +3,7 @@ import { Decimal } from 'decimal.js-light' import { ArgsType, Field } from 'type-graphql' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const' +import { IsPositiveDecimal } from '@/graphql/validator/Decimal' @ArgsType() export class TransactionSendArgs { @@ -10,6 +11,7 @@ export class TransactionSendArgs { identifier: string @Field(() => Decimal) + @IsPositiveDecimal() amount: Decimal @Field(() => String) diff --git a/backend/src/graphql/resolver/ContributionLinkResolver.ts b/backend/src/graphql/resolver/ContributionLinkResolver.ts index e8a694a55..1f9e5ce26 100644 --- a/backend/src/graphql/resolver/ContributionLinkResolver.ts +++ b/backend/src/graphql/resolver/ContributionLinkResolver.ts @@ -41,10 +41,6 @@ export class ContributionLinkResolver { ): Promise { isStartEndDateValid(validFrom, validTo) - if (!new Decimal(amount).isPositive()) { - throw new LogError('The amount must be a positiv value', amount) - } - const dbContributionLink = new DbContributionLink() dbContributionLink.amount = amount dbContributionLink.name = name diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 282e6662a..cc0484588 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -73,10 +73,6 @@ 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 diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 721cba8fa..525be81fa 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -308,9 +308,6 @@ export class TransactionResolver { @Ctx() context: Context, ): Promise { logger.info(`sendCoins(identifier=${identifier}, amount=${amount}, memo=${memo})`) - if (amount.lte(0)) { - throw new LogError('Amount to send must be positive', amount) - } const senderUser = getUser(context) diff --git a/backend/src/graphql/validator/Decimal.ts b/backend/src/graphql/validator/Decimal.ts new file mode 100644 index 000000000..caf13143d --- /dev/null +++ b/backend/src/graphql/validator/Decimal.ts @@ -0,0 +1,22 @@ +import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator' +import { Decimal } from 'decimal.js-light' + +export function IsPositiveDecimal(validationOptions?: ValidationOptions) { + return function (object: NonNullable, propertyName: string) { + registerDecorator({ + name: 'isPositiveDecimal', + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + target: object.constructor, + propertyName, + options: validationOptions, + validator: { + validate(value: Decimal) { + return value.greaterThan(0) + }, + defaultMessage(args: ValidationArguments) { + return `The ${propertyName} must be a positive value ${args.property}` + }, + }, + }) + } +} From 6d6c520f63d58401ead752341e6277e930a8021f Mon Sep 17 00:00:00 2001 From: Einhornimmond Date: Wed, 9 Aug 2023 14:22:02 +0200 Subject: [PATCH 05/22] remove not longer needed import --- backend/src/graphql/resolver/ContributionLinkResolver.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/graphql/resolver/ContributionLinkResolver.ts b/backend/src/graphql/resolver/ContributionLinkResolver.ts index 1f9e5ce26..0a04d387d 100644 --- a/backend/src/graphql/resolver/ContributionLinkResolver.ts +++ b/backend/src/graphql/resolver/ContributionLinkResolver.ts @@ -1,6 +1,5 @@ import { MoreThan, IsNull } from '@dbTools/typeorm' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' -import { Decimal } from 'decimal.js-light' import { Resolver, Args, Arg, Authorized, Mutation, Query, Int, Ctx } from 'type-graphql' import { ContributionLinkArgs } from '@arg/ContributionLinkArgs' From 60b0eed582894b4a2049e6124ec1cf4ff5cc6449 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Wed, 9 Aug 2023 14:55:21 +0200 Subject: [PATCH 06/22] fix problem with decorator --- backend/src/graphql/validator/Decimal.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/validator/Decimal.ts b/backend/src/graphql/validator/Decimal.ts index caf13143d..09e8fb4bd 100644 --- a/backend/src/graphql/validator/Decimal.ts +++ b/backend/src/graphql/validator/Decimal.ts @@ -2,10 +2,10 @@ import { registerDecorator, ValidationOptions, ValidationArguments } from 'class import { Decimal } from 'decimal.js-light' export function IsPositiveDecimal(validationOptions?: ValidationOptions) { - return function (object: NonNullable, propertyName: string) { + // eslint-disable-next-line @typescript-eslint/ban-types + return function (object: Object, propertyName: string) { registerDecorator({ name: 'isPositiveDecimal', - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment target: object.constructor, propertyName, options: validationOptions, From e2f65cddc3079c28b5cc29927630d089e63bad39 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Fri, 11 Aug 2023 10:39:53 +0200 Subject: [PATCH 07/22] increase auto validation, update tests --- .../arg/AdminCreateContributionArgs.ts | 5 +- .../arg/AdminUpdateContributionArgs.ts | 5 +- backend/src/graphql/arg/ContributionArgs.ts | 2 + .../src/graphql/arg/ContributionLinkArgs.ts | 8 +- .../graphql/arg/ContributionMessageArgs.ts | 4 + backend/src/graphql/arg/CreateUserArgs.ts | 8 + backend/src/graphql/arg/Paginated.ts | 4 + backend/src/graphql/arg/SearchUsersFilters.ts | 3 + backend/src/graphql/arg/SetUserRoleArgs.ts | 3 + .../src/graphql/arg/TransactionLinkArgs.ts | 2 +- .../src/graphql/arg/TransactionLinkFilters.ts | 4 + .../src/graphql/arg/TransactionSendArgs.ts | 3 +- backend/src/graphql/arg/UnsecureLoginArgs.ts | 4 + .../src/graphql/arg/UpdateUserInfosArgs.ts | 10 + .../resolver/ContributionLinkResolver.test.ts | 195 +++++++++++------- .../resolver/ContributionResolver.test.ts | 158 ++++++++++---- .../graphql/resolver/ContributionResolver.ts | 4 - .../resolver/TransactionLinkResolver.test.ts | 74 ++++--- .../resolver/TransactionResolver.test.ts | 131 +++++++----- backend/src/graphql/schema.ts | 24 +-- backend/src/graphql/validator/Alias.ts | 0 backend/src/graphql/validator/DateString.ts | 21 ++ 22 files changed, 439 insertions(+), 233 deletions(-) create mode 100644 backend/src/graphql/validator/Alias.ts create mode 100644 backend/src/graphql/validator/DateString.ts diff --git a/backend/src/graphql/arg/AdminCreateContributionArgs.ts b/backend/src/graphql/arg/AdminCreateContributionArgs.ts index 558821921..e734fc514 100644 --- a/backend/src/graphql/arg/AdminCreateContributionArgs.ts +++ b/backend/src/graphql/arg/AdminCreateContributionArgs.ts @@ -1,14 +1,16 @@ -import { MaxLength, MinLength } from 'class-validator' +import { IsEmail, MaxLength, MinLength } from 'class-validator' import { Decimal } from 'decimal.js-light' import { ArgsType, Field, InputType } from 'type-graphql' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const' +import { isValidDateString } from '@/graphql/validator/DateString' import { IsPositiveDecimal } from '@/graphql/validator/Decimal' @InputType() @ArgsType() export class AdminCreateContributionArgs { @Field(() => String) + @IsEmail() email: string @Field(() => Decimal) @@ -21,5 +23,6 @@ export class AdminCreateContributionArgs { memo: string @Field(() => String) + @isValidDateString() creationDate: string } diff --git a/backend/src/graphql/arg/AdminUpdateContributionArgs.ts b/backend/src/graphql/arg/AdminUpdateContributionArgs.ts index c5f560c9a..f7288cb28 100644 --- a/backend/src/graphql/arg/AdminUpdateContributionArgs.ts +++ b/backend/src/graphql/arg/AdminUpdateContributionArgs.ts @@ -1,13 +1,15 @@ -import { MaxLength, MinLength } from 'class-validator' +import { IsPositive, MaxLength, MinLength } from 'class-validator' import { Decimal } from 'decimal.js-light' import { ArgsType, Field, Int } from 'type-graphql' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const' +import { isValidDateString } from '@/graphql/validator/DateString' import { IsPositiveDecimal } from '@/graphql/validator/Decimal' @ArgsType() export class AdminUpdateContributionArgs { @Field(() => Int) + @IsPositive() id: number @Field(() => Decimal) @@ -20,5 +22,6 @@ export class AdminUpdateContributionArgs { memo: string @Field(() => String) + @isValidDateString() creationDate: string } diff --git a/backend/src/graphql/arg/ContributionArgs.ts b/backend/src/graphql/arg/ContributionArgs.ts index d4c9e639b..9f3951ac2 100644 --- a/backend/src/graphql/arg/ContributionArgs.ts +++ b/backend/src/graphql/arg/ContributionArgs.ts @@ -3,6 +3,7 @@ import { Decimal } from 'decimal.js-light' import { ArgsType, Field, InputType } from 'type-graphql' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const' +import { isValidDateString } from '@/graphql/validator/DateString' import { IsPositiveDecimal } from '@/graphql/validator/Decimal' @InputType() @@ -18,5 +19,6 @@ export class ContributionArgs { memo: string @Field(() => String) + @isValidDateString() creationDate: string } diff --git a/backend/src/graphql/arg/ContributionLinkArgs.ts b/backend/src/graphql/arg/ContributionLinkArgs.ts index 4aa268d28..97cf3dfdd 100644 --- a/backend/src/graphql/arg/ContributionLinkArgs.ts +++ b/backend/src/graphql/arg/ContributionLinkArgs.ts @@ -1,4 +1,4 @@ -import { MaxLength, MinLength } from 'class-validator' +import { IsPositive, IsString, MaxLength, MinLength } from 'class-validator' import { Decimal } from 'decimal.js-light' import { ArgsType, Field, Int } from 'type-graphql' @@ -8,6 +8,7 @@ import { CONTRIBUTIONLINK_NAME_MIN_CHARS, CONTRIBUTIONLINK_NAME_MAX_CHARS, } from '@/graphql/resolver/const/const' +import { isValidDateString } from '@/graphql/validator/DateString' import { IsPositiveDecimal } from '@/graphql/validator/Decimal' @ArgsType() @@ -27,17 +28,22 @@ export class ContributionLinkArgs { memo: string @Field(() => String) + @IsString() cycle: string @Field(() => String, { nullable: true }) + @isValidDateString() validFrom?: string | null @Field(() => String, { nullable: true }) + @isValidDateString() validTo?: string | null @Field(() => Decimal, { nullable: true }) + @IsPositiveDecimal() maxAmountPerMonth?: Decimal | null @Field(() => Int) + @IsPositive() maxPerCycle: number } diff --git a/backend/src/graphql/arg/ContributionMessageArgs.ts b/backend/src/graphql/arg/ContributionMessageArgs.ts index 6482793aa..847cb5b33 100644 --- a/backend/src/graphql/arg/ContributionMessageArgs.ts +++ b/backend/src/graphql/arg/ContributionMessageArgs.ts @@ -1,3 +1,4 @@ +import { IsInt, IsString, IsEnum } from 'class-validator' import { ArgsType, Field, Int, InputType } from 'type-graphql' import { ContributionMessageType } from '@enum/ContributionMessageType' @@ -6,11 +7,14 @@ import { ContributionMessageType } from '@enum/ContributionMessageType' @ArgsType() export class ContributionMessageArgs { @Field(() => Int) + @IsInt() contributionId: number @Field(() => String) + @IsString() message: string @Field(() => ContributionMessageType, { defaultValue: ContributionMessageType.DIALOG }) + @IsEnum(ContributionMessageType) messageType: ContributionMessageType } diff --git a/backend/src/graphql/arg/CreateUserArgs.ts b/backend/src/graphql/arg/CreateUserArgs.ts index c348b9b4c..28105559d 100644 --- a/backend/src/graphql/arg/CreateUserArgs.ts +++ b/backend/src/graphql/arg/CreateUserArgs.ts @@ -1,25 +1,33 @@ +import { IsEmail, IsInt, IsString } from 'class-validator' import { ArgsType, Field, Int } from 'type-graphql' @ArgsType() export class CreateUserArgs { @Field(() => String, { nullable: true }) + @IsString() alias?: string | null @Field(() => String) + @IsEmail() email: string @Field(() => String) + @IsString() firstName: string @Field(() => String) + @IsString() lastName: string @Field(() => String, { nullable: true }) + @IsString() language?: string | null @Field(() => Int, { nullable: true }) + @IsInt() publisherId?: number | null @Field(() => String, { nullable: true }) + @IsString() redeemCode?: string | null } diff --git a/backend/src/graphql/arg/Paginated.ts b/backend/src/graphql/arg/Paginated.ts index a1e792ef7..c63416e7e 100644 --- a/backend/src/graphql/arg/Paginated.ts +++ b/backend/src/graphql/arg/Paginated.ts @@ -1,4 +1,5 @@ /* eslint-disable type-graphql/invalid-nullable-input-type */ +import { IsPositive, IsEnum } from 'class-validator' import { ArgsType, Field, Int } from 'type-graphql' import { Order } from '@enum/Order' @@ -6,11 +7,14 @@ import { Order } from '@enum/Order' @ArgsType() export class Paginated { @Field(() => Int, { nullable: true }) + @IsPositive() currentPage?: number @Field(() => Int, { nullable: true }) + @IsPositive() pageSize?: number @Field(() => Order, { nullable: true }) + @IsEnum(Order) order?: Order } diff --git a/backend/src/graphql/arg/SearchUsersFilters.ts b/backend/src/graphql/arg/SearchUsersFilters.ts index a6ea09268..d82500aea 100644 --- a/backend/src/graphql/arg/SearchUsersFilters.ts +++ b/backend/src/graphql/arg/SearchUsersFilters.ts @@ -1,10 +1,13 @@ +import { IsBoolean } from 'class-validator' import { Field, InputType } from 'type-graphql' @InputType() export class SearchUsersFilters { @Field(() => Boolean, { nullable: true, defaultValue: null }) + @IsBoolean() byActivated?: boolean | null @Field(() => Boolean, { nullable: true, defaultValue: null }) + @IsBoolean() byDeleted?: boolean | null } diff --git a/backend/src/graphql/arg/SetUserRoleArgs.ts b/backend/src/graphql/arg/SetUserRoleArgs.ts index c076fc8cf..039d190cd 100644 --- a/backend/src/graphql/arg/SetUserRoleArgs.ts +++ b/backend/src/graphql/arg/SetUserRoleArgs.ts @@ -1,3 +1,4 @@ +import { IsPositive, IsEnum } from 'class-validator' import { ArgsType, Field, Int, InputType } from 'type-graphql' import { RoleNames } from '@enum/RoleNames' @@ -6,8 +7,10 @@ import { RoleNames } from '@enum/RoleNames' @ArgsType() export class SetUserRoleArgs { @Field(() => Int) + @IsPositive() userId: number @Field(() => RoleNames, { nullable: true }) + @IsEnum(RoleNames) role: RoleNames | null | undefined } diff --git a/backend/src/graphql/arg/TransactionLinkArgs.ts b/backend/src/graphql/arg/TransactionLinkArgs.ts index 6dfdda15e..039686bca 100644 --- a/backend/src/graphql/arg/TransactionLinkArgs.ts +++ b/backend/src/graphql/arg/TransactionLinkArgs.ts @@ -1,4 +1,4 @@ -import { IsAlpha, MaxLength, MinLength } from 'class-validator' +import { MaxLength, MinLength } from 'class-validator' import { Decimal } from 'decimal.js-light' import { ArgsType, Field } from 'type-graphql' diff --git a/backend/src/graphql/arg/TransactionLinkFilters.ts b/backend/src/graphql/arg/TransactionLinkFilters.ts index de8643260..831a30834 100644 --- a/backend/src/graphql/arg/TransactionLinkFilters.ts +++ b/backend/src/graphql/arg/TransactionLinkFilters.ts @@ -1,14 +1,18 @@ /* eslint-disable type-graphql/invalid-nullable-input-type */ +import { IsBoolean } from 'class-validator' import { Field, InputType } from 'type-graphql' @InputType() export class TransactionLinkFilters { @Field(() => Boolean, { nullable: true }) + @IsBoolean() withDeleted?: boolean @Field(() => Boolean, { nullable: true }) + @IsBoolean() withExpired?: boolean @Field(() => Boolean, { nullable: true }) + @IsBoolean() withRedeemed?: boolean } diff --git a/backend/src/graphql/arg/TransactionSendArgs.ts b/backend/src/graphql/arg/TransactionSendArgs.ts index d8a556b99..026a87eef 100644 --- a/backend/src/graphql/arg/TransactionSendArgs.ts +++ b/backend/src/graphql/arg/TransactionSendArgs.ts @@ -1,4 +1,4 @@ -import { MaxLength, MinLength } from 'class-validator' +import { MaxLength, MinLength, IsString } from 'class-validator' import { Decimal } from 'decimal.js-light' import { ArgsType, Field } from 'type-graphql' @@ -8,6 +8,7 @@ import { IsPositiveDecimal } from '@/graphql/validator/Decimal' @ArgsType() export class TransactionSendArgs { @Field(() => String) + @IsString() identifier: string @Field(() => Decimal) diff --git a/backend/src/graphql/arg/UnsecureLoginArgs.ts b/backend/src/graphql/arg/UnsecureLoginArgs.ts index ad5a934f9..5e82f12f3 100644 --- a/backend/src/graphql/arg/UnsecureLoginArgs.ts +++ b/backend/src/graphql/arg/UnsecureLoginArgs.ts @@ -1,13 +1,17 @@ +import { IsEmail, IsInt, IsString } from 'class-validator' import { ArgsType, Field, Int } from 'type-graphql' @ArgsType() export class UnsecureLoginArgs { @Field(() => String) + @IsEmail() email: string @Field(() => String) + @IsString() password: string @Field(() => Int, { nullable: true }) + @IsInt() publisherId?: number | null } diff --git a/backend/src/graphql/arg/UpdateUserInfosArgs.ts b/backend/src/graphql/arg/UpdateUserInfosArgs.ts index e57d4ec82..6b2ab1032 100644 --- a/backend/src/graphql/arg/UpdateUserInfosArgs.ts +++ b/backend/src/graphql/arg/UpdateUserInfosArgs.ts @@ -1,31 +1,41 @@ +import { IsBoolean, IsInt, IsString } from 'class-validator' import { ArgsType, Field, Int } from 'type-graphql' @ArgsType() export class UpdateUserInfosArgs { @Field({ nullable: true }) + @IsString() firstName?: string @Field({ nullable: true }) + @IsString() lastName?: string @Field({ nullable: true }) + @IsString() alias?: string @Field({ nullable: true }) + @IsString() language?: string @Field(() => Int, { nullable: true }) + @IsInt() publisherId?: number | null @Field({ nullable: true }) + @IsString() password?: string @Field({ nullable: true }) + @IsString() passwordNew?: string @Field({ nullable: true }) + @IsBoolean() hideAmountGDD?: boolean @Field({ nullable: true }) + @IsBoolean() hideAmountGDT?: boolean } diff --git a/backend/src/graphql/resolver/ContributionLinkResolver.test.ts b/backend/src/graphql/resolver/ContributionLinkResolver.test.ts index 9605378e2..23be529d3 100644 --- a/backend/src/graphql/resolver/ContributionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionLinkResolver.test.ts @@ -339,107 +339,142 @@ describe('Contribution Links', () => { it('returns an error if name is shorter than 5 characters', async () => { jest.clearAllMocks() - await expect( - mutate({ - mutation: createContributionLink, - variables: { - ...variables, - name: '123', + const { errors: errorObjects } = await mutate({ + mutation: createContributionLink, + variables: { + ...variables, + name: '123', + }, + }) + expect(errorObjects).toMatchObject([ + { + message: 'Argument Validation Error', + extensions: { + exception: { + validationErrors: [ + { + property: 'name', + constraints: { + minLength: 'name must be longer than or equal to 5 characters', + }, + }, + ], + }, }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('The value of name is too short')], - }), - ) - }) - - it('logs the error "The value of name is too short"', () => { - expect(logger.error).toBeCalledWith('The value of name is too short', 3) + }, + ]) }) it('returns an error if name is longer than 100 characters', async () => { jest.clearAllMocks() - await expect( - mutate({ - mutation: createContributionLink, - variables: { - ...variables, - name: '12345678901234567892123456789312345678941234567895123456789612345678971234567898123456789912345678901', + const { errors: errorObjects } = await mutate({ + mutation: createContributionLink, + variables: { + ...variables, + name: '12345678901234567892123456789312345678941234567895123456789612345678971234567898123456789912345678901', + }, + }) + expect(errorObjects).toMatchObject([ + { + message: 'Argument Validation Error', + extensions: { + exception: { + validationErrors: [ + { + property: 'name', + constraints: { + maxLength: 'name must be shorter than or equal to 100 characters', + }, + }, + ], + }, }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('The value of name is too long')], - }), - ) - }) - - it('logs the error "The value of name is too long"', () => { - expect(logger.error).toBeCalledWith('The value of name is too long', 101) + }, + ]) }) it('returns an error if memo is shorter than 5 characters', async () => { jest.clearAllMocks() - await expect( - mutate({ - mutation: createContributionLink, - variables: { - ...variables, - memo: '123', + const { errors: errorObjects } = await mutate({ + mutation: createContributionLink, + variables: { + ...variables, + memo: '123', + }, + }) + expect(errorObjects).toMatchObject([ + { + message: 'Argument Validation Error', + extensions: { + exception: { + validationErrors: [ + { + property: 'memo', + constraints: { + minLength: 'memo must be longer than or equal to 5 characters', + }, + }, + ], + }, }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('The value of memo is too short')], - }), - ) - }) - - it('logs the error "The value of memo is too short"', () => { - expect(logger.error).toBeCalledWith('The value of memo is too short', 3) + }, + ]) }) it('returns an error if memo is longer than 255 characters', async () => { jest.clearAllMocks() - await expect( - mutate({ - mutation: createContributionLink, - variables: { - ...variables, - memo: '1234567890123456789212345678931234567894123456789512345678961234567897123456789812345678991234567890123456789012345678921234567893123456789412345678951234567896123456789712345678981234567899123456789012345678901234567892123456789312345678941234567895123456', + const { errors: errorObjects } = await mutate({ + mutation: createContributionLink, + variables: { + ...variables, + memo: '1234567890123456789212345678931234567894123456789512345678961234567897123456789812345678991234567890123456789012345678921234567893123456789412345678951234567896123456789712345678981234567899123456789012345678901234567892123456789312345678941234567895123456', + }, + }) + expect(errorObjects).toMatchObject([ + { + message: 'Argument Validation Error', + extensions: { + exception: { + validationErrors: [ + { + property: 'memo', + constraints: { + maxLength: 'memo must be shorter than or equal to 255 characters', + }, + }, + ], + }, }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('The value of memo is too long')], - }), - ) - }) - - it('logs the error "The value of memo is too long"', () => { - expect(logger.error).toBeCalledWith('The value of memo is too long', 256) + }, + ]) }) it('returns an error if amount is not positive', async () => { jest.clearAllMocks() - await expect( - mutate({ - mutation: createContributionLink, - variables: { - ...variables, - amount: new Decimal(0), + const { errors: errorObjects } = await mutate({ + mutation: createContributionLink, + variables: { + ...variables, + amount: new Decimal(0), + }, + }) + expect(errorObjects).toMatchObject([ + { + message: 'Argument Validation Error', + extensions: { + exception: { + validationErrors: [ + { + property: 'amount', + constraints: { + isPositiveDecimal: 'The amount must be a positive value amount', + }, + }, + ], + }, }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('The amount must be a positiv value')], - }), - ) - }) - - it('logs the error "The amount must be a positiv value"', () => { - expect(logger.error).toBeCalledWith('The amount must be a positiv value', new Decimal(0)) + }, + ]) }) }) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index cbb92eff8..167edfe74 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -201,6 +201,7 @@ describe('ContributionResolver', () => { it('throws error when memo length smaller than 5 chars', async () => { jest.clearAllMocks() const date = new Date() + const { errors: errorObjects } = await mutate({ mutation: createContribution, variables: { @@ -209,12 +210,23 @@ describe('ContributionResolver', () => { creationDate: date.toString(), }, }) - - expect(errorObjects).toEqual([new GraphQLError('Memo text is too short')]) - }) - - it('logs the error "Memo text is too short"', () => { - expect(logger.error).toBeCalledWith('Memo text is too short', 4) + expect(errorObjects).toMatchObject([ + { + message: 'Argument Validation Error', + extensions: { + exception: { + validationErrors: [ + { + property: 'memo', + constraints: { + minLength: 'memo must be longer than or equal to 5 characters', + }, + }, + ], + }, + }, + }, + ]) }) it('throws error when memo length greater than 255 chars', async () => { @@ -228,11 +240,23 @@ describe('ContributionResolver', () => { creationDate: date.toString(), }, }) - expect(errorObjects).toEqual([new GraphQLError('Memo text is too long')]) - }) - - it('logs the error "Memo text is too long"', () => { - expect(logger.error).toBeCalledWith('Memo text is too long', 259) + expect(errorObjects).toMatchObject([ + { + message: 'Argument Validation Error', + extensions: { + exception: { + validationErrors: [ + { + property: 'memo', + constraints: { + maxLength: 'memo must be shorter than or equal to 255 characters', + }, + }, + ], + }, + }, + }, + ]) }) it('throws error when creationDate not-valid', async () => { @@ -245,27 +269,35 @@ describe('ContributionResolver', () => { creationDate: 'not-valid', }, }) - expect(errorObjects).toEqual([ - new GraphQLError('No information for available creations for the given date'), + expect(errorObjects).toMatchObject([ + { + message: 'Argument Validation Error', + extensions: { + exception: { + validationErrors: [ + { + property: 'creationDate', + constraints: { + isValidDateString: 'creationDate must be a valid date string, creationDate', + }, + }, + ], + }, + }, + }, ]) }) - it('logs the error "No information for available creations for the given date"', () => { - expect(logger.error).toBeCalledWith( - 'No information for available creations for the given date', - expect.any(Date), - ) - }) - it('throws error when creationDate 3 month behind', async () => { jest.clearAllMocks() const date = new Date() + date.setMonth(date.getMonth() - 3) const { errors: errorObjects } = await mutate({ mutation: createContribution, variables: { amount: 100.0, memo: 'Test env contribution', - creationDate: date.setMonth(date.getMonth() - 3).toString(), + creationDate: date.toString(), }, }) expect(errorObjects).toEqual([ @@ -346,11 +378,23 @@ describe('ContributionResolver', () => { creationDate: date.toString(), }, }) - expect(errorObjects).toEqual([new GraphQLError('Memo text is too short')]) - }) - - it('logs the error "Memo text is too short"', () => { - expect(logger.error).toBeCalledWith('Memo text is too short', 4) + expect(errorObjects).toMatchObject([ + { + message: 'Argument Validation Error', + extensions: { + exception: { + validationErrors: [ + { + property: 'memo', + constraints: { + minLength: 'memo must be longer than or equal to 5 characters', + }, + }, + ], + }, + }, + }, + ]) }) }) @@ -367,11 +411,23 @@ describe('ContributionResolver', () => { creationDate: date.toString(), }, }) - expect(errorObjects).toEqual([new GraphQLError('Memo text is too long')]) - }) - - it('logs the error "Memo text is too long"', () => { - expect(logger.error).toBeCalledWith('Memo text is too long', 259) + expect(errorObjects).toMatchObject([ + { + message: 'Argument Validation Error', + extensions: { + exception: { + validationErrors: [ + { + property: 'memo', + constraints: { + maxLength: 'memo must be shorter than or equal to 255 characters', + }, + }, + ], + }, + }, + }, + ]) }) }) @@ -551,13 +607,14 @@ describe('ContributionResolver', () => { it('throws an error', async () => { jest.clearAllMocks() const date = new Date() + date.setMonth(date.getMonth() - 3) const { errors: errorObjects } = await mutate({ mutation: updateContribution, variables: { contributionId: pendingContribution.data.createContribution.id, amount: 10.0, memo: 'Test env contribution', - creationDate: date.setMonth(date.getMonth() - 3).toString(), + creationDate: date.toString(), }, }) expect(errorObjects).toEqual([ @@ -1979,17 +2036,28 @@ describe('ContributionResolver', () => { describe('date of creation is not a date string', () => { it('throws an error', async () => { jest.clearAllMocks() - await expect( - mutate({ mutation: adminCreateContribution, variables }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('CreationDate is invalid')], - }), - ) - }) - - it('logs the error "CreationDate is invalid"', () => { - expect(logger.error).toBeCalledWith('CreationDate is invalid', 'invalid-date') + const { errors: errorObjects } = await mutate({ + mutation: adminCreateContribution, + variables, + }) + expect(errorObjects).toMatchObject([ + { + message: 'Argument Validation Error', + extensions: { + exception: { + validationErrors: [ + { + property: 'creationDate', + constraints: { + isValidDateString: + 'creationDate must be a valid date string, creationDate', + }, + }, + ], + }, + }, + }, + ]) }) }) @@ -2181,7 +2249,7 @@ describe('ContributionResolver', () => { mutate({ mutation: adminUpdateContribution, variables: { - id: -1, + id: 728, amount: new Decimal(300), memo: 'Danke Bibi!', creationDate: contributionDateFormatter(new Date()), @@ -2195,7 +2263,7 @@ describe('ContributionResolver', () => { }) it('logs the error "Contribution not found"', () => { - expect(logger.error).toBeCalledWith('Contribution not found', -1) + expect(logger.error).toBeCalledWith('Contribution not found', 728) }) }) diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 7b4c21708..64206af2e 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -49,7 +49,6 @@ import { getUserCreation, validateContribution, updateCreations, - isValidDateString, getOpenCreations, } from './util/creations' import { findContributions } from './util/findContributions' @@ -256,9 +255,6 @@ export class ContributionResolver { `adminCreateContribution(email=${email}, amount=${amount.toString()}, memo=${memo}, creationDate=${creationDate})`, ) const clientTimezoneOffset = getClientTimezoneOffset(context) - if (!isValidDateString(creationDate)) { - throw new LogError('CreationDate is invalid', creationDate) - } const emailContact = await UserContact.findOne({ where: { email }, withDeleted: true, diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts index ac76bdecf..59e8047c6 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts @@ -94,38 +94,58 @@ describe('TransactionLinkResolver', () => { 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')], + const { errors: errorObjects } = await mutate({ + mutation: createTransactionLink, + variables: { + amount: 0, + memo: 'Test Test', + }, }) - }) - it('logs the error "Amount must be a positive number" - 0', () => { - expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(0)) + expect(errorObjects).toMatchObject([ + { + message: 'Argument Validation Error', + extensions: { + exception: { + validationErrors: [ + { + property: 'amount', + constraints: { + isPositiveDecimal: 'The amount must be a positive value amount', + }, + }, + ], + }, + }, + }, + ]) }) 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')], + const { errors: errorObjects } = await mutate({ + mutation: createTransactionLink, + variables: { + amount: -10, + memo: 'Test Test', + }, }) - }) - it('logs the error "Amount must be a positive number" - -10', () => { - expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(-10)) + expect(errorObjects).toMatchObject([ + { + message: 'Argument Validation Error', + extensions: { + exception: { + validationErrors: [ + { + property: 'amount', + constraints: { + isPositiveDecimal: 'The amount must be a positive value amount', + }, + }, + ], + }, + }, + }, + ]) }) it('throws error when user has not enough GDD', async () => { @@ -135,7 +155,7 @@ describe('TransactionLinkResolver', () => { mutation: createTransactionLink, variables: { amount: 1001, - memo: 'Test', + memo: 'Test Test', }, }), ).resolves.toMatchObject({ diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index 60445e239..e91efee6f 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -93,7 +93,7 @@ describe('send coins', () => { variables: { identifier: 'wrong@email.com', amount: 100, - memo: 'test', + memo: 'test test', }, }), ).toEqual( @@ -121,7 +121,7 @@ describe('send coins', () => { variables: { identifier: 'stephen@hawking.uk', amount: 100, - memo: 'test', + memo: 'test test', }, }), ).toEqual( @@ -150,7 +150,7 @@ describe('send coins', () => { variables: { identifier: 'garrick@ollivander.com', amount: 100, - memo: 'test', + memo: 'test test', }, }), ).toEqual( @@ -186,7 +186,7 @@ describe('send coins', () => { variables: { identifier: 'bob@baumeister.de', amount: 100, - memo: 'test', + memo: 'test test', }, }), ).toEqual( @@ -204,48 +204,62 @@ describe('send coins', () => { describe('memo text is too short', () => { it('throws an error', async () => { jest.clearAllMocks() - expect( - await mutate({ - mutation: sendCoins, - variables: { - identifier: 'peter@lustig.de', - amount: 100, - memo: 'test', + const { errors: errorObjects } = await mutate({ + mutation: sendCoins, + variables: { + identifier: 'peter@lustig.de', + amount: 100, + memo: 'Test', + }, + }) + expect(errorObjects).toMatchObject([ + { + message: 'Argument Validation Error', + extensions: { + exception: { + validationErrors: [ + { + property: 'memo', + constraints: { + minLength: 'memo must be longer than or equal to 5 characters', + }, + }, + ], + }, }, - }), - ).toEqual( - expect.objectContaining({ - errors: [new GraphQLError('Memo text is too short')], - }), - ) - }) - - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('Memo text is too short', 4) + }, + ]) }) }) describe('memo text is too long', () => { it('throws an error', async () => { jest.clearAllMocks() - expect( - await mutate({ - mutation: sendCoins, - variables: { - identifier: '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', + const { errors: errorObjects } = await mutate({ + mutation: sendCoins, + variables: { + identifier: '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', + }, + }) + expect(errorObjects).toMatchObject([ + { + message: 'Argument Validation Error', + extensions: { + exception: { + validationErrors: [ + { + property: 'memo', + constraints: { + maxLength: 'memo must be shorter than or equal to 255 characters', + }, + }, + ], + }, }, - }), - ).toEqual( - expect.objectContaining({ - errors: [new GraphQLError('Memo text is too long')], - }), - ) - }) - - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('Memo text is too long', 256) + }, + ]) }) }) @@ -302,24 +316,31 @@ describe('send coins', () => { describe('trying to send negative amount', () => { it('throws an error', async () => { jest.clearAllMocks() - expect( - await mutate({ - mutation: sendCoins, - variables: { - identifier: 'peter@lustig.de', - amount: -50, - memo: 'testing negative', + const { errors: errorObjects } = await mutate({ + mutation: sendCoins, + variables: { + identifier: 'peter@lustig.de', + amount: -50, + memo: 'testing negative', + }, + }) + expect(errorObjects).toMatchObject([ + { + message: 'Argument Validation Error', + extensions: { + exception: { + validationErrors: [ + { + property: 'amount', + constraints: { + isPositiveDecimal: 'The amount must be a positive value amount', + }, + }, + ], + }, }, - }), - ).toEqual( - expect.objectContaining({ - errors: [new GraphQLError('Amount to send must be positive')], - }), - ) - }) - - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('Amount to send must be positive', new Decimal(-50)) + }, + ]) }) }) diff --git a/backend/src/graphql/schema.ts b/backend/src/graphql/schema.ts index 98af37159..878bb747c 100644 --- a/backend/src/graphql/schema.ts +++ b/backend/src/graphql/schema.ts @@ -1,12 +1,9 @@ import path from 'path' -import { validate } from 'class-validator' import { Decimal } from 'decimal.js-light' import { GraphQLSchema } from 'graphql' import { buildSchema } from 'type-graphql' -import { LogError } from '@/server/LogError' - import { isAuthorized } from './directive/isAuthorized' import { DecimalScalar } from './scalar/Decimal' @@ -15,20 +12,13 @@ export const schema = async (): Promise => { resolvers: [path.join(__dirname, 'resolver', `!(*.test).{js,ts}`)], authChecker: isAuthorized, scalarsMap: [{ type: Decimal, scalar: DecimalScalar }], - validate: (argValue) => { - if (argValue) { - validate(argValue) - .then((errors) => { - if (errors.length > 0) { - throw new LogError('validation failed. errors: ', errors) - } else { - return true - } - }) - .catch((e) => { - throw new LogError('validation throw an exception: ', e) - }) - } + validate: { + validationError: { target: false }, + skipMissingProperties: true, + skipNullProperties: true, + skipUndefinedProperties: true, + forbidUnknownValues: false, + stopAtFirstError: false, }, }) } diff --git a/backend/src/graphql/validator/Alias.ts b/backend/src/graphql/validator/Alias.ts new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/graphql/validator/DateString.ts b/backend/src/graphql/validator/DateString.ts new file mode 100644 index 000000000..4ee23b51a --- /dev/null +++ b/backend/src/graphql/validator/DateString.ts @@ -0,0 +1,21 @@ +import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator' + +export function isValidDateString(validationOptions?: ValidationOptions) { + // eslint-disable-next-line @typescript-eslint/ban-types + return function (object: Object, propertyName: string) { + registerDecorator({ + name: 'isValidDateString', + target: object.constructor, + propertyName, + options: validationOptions, + validator: { + validate(value: string) { + return new Date(value).toString() !== 'Invalid Date' + }, + defaultMessage(args: ValidationArguments) { + return `${propertyName} must be a valid date string, ${args.property}` + }, + }, + }) + } +} From a74d4697cf6c849ce3d4981247e76966321d53c4 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Fri, 11 Aug 2023 10:43:27 +0200 Subject: [PATCH 08/22] remove not longer needed import --- backend/src/graphql/resolver/TransactionResolver.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index e91efee6f..1957c680f 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -6,7 +6,6 @@ import { Event as DbEvent } from '@entity/Event' import { Transaction } from '@entity/Transaction' import { User } from '@entity/User' import { ApolloServerTestClient } from 'apollo-server-testing' -import { Decimal } from 'decimal.js-light' import { GraphQLError } from 'graphql' import { cleanDB, testEnvironment } from '@test/helpers' From 8f6d2b5f3c46925f1f7eabeb233ef9cb80b55957 Mon Sep 17 00:00:00 2001 From: Einhornimmond Date: Wed, 16 Aug 2023 13:16:49 +0200 Subject: [PATCH 09/22] apply suggestions --- .../resolver/TransactionLinkResolver.test.ts | 57 +++++++++++++++++++ backend/src/graphql/schema.ts | 8 +-- backend/src/graphql/validator/Alias.ts | 0 3 files changed, 61 insertions(+), 4 deletions(-) delete mode 100644 backend/src/graphql/validator/Alias.ts diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts index 59e8047c6..fe047b35a 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts @@ -148,6 +148,63 @@ describe('TransactionLinkResolver', () => { ]) }) + it('throws error when memo text is too short', async () => { + jest.clearAllMocks() + const { errors: errorObjects } = await mutate({ + mutation: createTransactionLink, + variables: { + amount: 100, + memo: 'Test', + }, + }) + expect(errorObjects).toMatchObject([ + { + message: 'Argument Validation Error', + extensions: { + exception: { + validationErrors: [ + { + property: 'memo', + constraints: { + minLength: 'memo must be longer than or equal to 5 characters', + }, + }, + ], + }, + }, + }, + ]) + }) + + it('throws error when memo text is too long', async () => { + jest.clearAllMocks() + const { errors: errorObjects } = await mutate({ + mutation: createTransactionLink, + variables: { + identifier: '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', + }, + }) + expect(errorObjects).toMatchObject([ + { + message: 'Argument Validation Error', + extensions: { + exception: { + validationErrors: [ + { + property: 'memo', + constraints: { + maxLength: 'memo must be shorter than or equal to 255 characters', + }, + }, + ], + }, + }, + }, + ]) + }) + it('throws error when user has not enough GDD', async () => { jest.clearAllMocks() await expect( diff --git a/backend/src/graphql/schema.ts b/backend/src/graphql/schema.ts index 878bb747c..a2e4ca71f 100644 --- a/backend/src/graphql/schema.ts +++ b/backend/src/graphql/schema.ts @@ -14,11 +14,11 @@ export const schema = async (): Promise => { scalarsMap: [{ type: Decimal, scalar: DecimalScalar }], validate: { validationError: { target: false }, - skipMissingProperties: true, + skipMissingProperties: false, skipNullProperties: true, - skipUndefinedProperties: true, - forbidUnknownValues: false, - stopAtFirstError: false, + skipUndefinedProperties: false, + forbidUnknownValues: true, + stopAtFirstError: true, }, }) } diff --git a/backend/src/graphql/validator/Alias.ts b/backend/src/graphql/validator/Alias.ts deleted file mode 100644 index e69de29bb..000000000 From 20fbdaf2ea17ae58988e4c8ba8591b57a8522907 Mon Sep 17 00:00:00 2001 From: Einhornimmond Date: Wed, 16 Aug 2023 13:46:08 +0200 Subject: [PATCH 10/22] change parameter to fix broken tests --- backend/src/graphql/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/schema.ts b/backend/src/graphql/schema.ts index a2e4ca71f..18214861f 100644 --- a/backend/src/graphql/schema.ts +++ b/backend/src/graphql/schema.ts @@ -14,7 +14,7 @@ export const schema = async (): Promise => { scalarsMap: [{ type: Decimal, scalar: DecimalScalar }], validate: { validationError: { target: false }, - skipMissingProperties: false, + skipMissingProperties: true, skipNullProperties: true, skipUndefinedProperties: false, forbidUnknownValues: true, From 6fc2203ce5fa0fd10194761ee7508e50634bfbf4 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Sat, 19 Aug 2023 11:52:50 +0200 Subject: [PATCH 11/22] add custom validator for ContributionStatus Array --- .../arg/SearchContributionsFilterArgs.ts | 6 ++++++ .../resolver/ContributionResolver.test.ts | 19 ++++++++++++++++- backend/src/graphql/schema.ts | 3 ++- .../validator/ContributionStatusArray.ts | 21 +++++++++++++++++++ backend/src/seeds/graphql/queries.ts | 2 ++ 5 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 backend/src/graphql/validator/ContributionStatusArray.ts diff --git a/backend/src/graphql/arg/SearchContributionsFilterArgs.ts b/backend/src/graphql/arg/SearchContributionsFilterArgs.ts index e256fa663..29446f1df 100644 --- a/backend/src/graphql/arg/SearchContributionsFilterArgs.ts +++ b/backend/src/graphql/arg/SearchContributionsFilterArgs.ts @@ -1,18 +1,24 @@ +import { IsBoolean, IsPositive, IsString } from 'class-validator' import { Field, ArgsType, Int } from 'type-graphql' import { ContributionStatus } from '@enum/ContributionStatus' +import { isContributionStatusArray } from '@/graphql/validator/ContributionStatusArray' @ArgsType() export class SearchContributionsFilterArgs { @Field(() => [ContributionStatus], { nullable: true, defaultValue: null }) + @isContributionStatusArray() statusFilter?: ContributionStatus[] | null @Field(() => Int, { nullable: true }) + @IsPositive() userId?: number | null @Field(() => String, { nullable: true, defaultValue: '' }) + @IsString() query?: string | null @Field(() => Boolean, { nullable: true }) + @IsBoolean() noHashtag?: boolean | null } diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 167edfe74..52dd04339 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -2792,11 +2792,28 @@ describe('ContributionResolver', () => { resetToken() }) + it('throw error for invalid ContributionStatus in statusFilter array', async() => { + const { errors: errorObjects } = await query({ + query: adminListContributions, + variables: { + statusFilter:["INVALID_STATUS"] + } + }) + expect(errorObjects).toMatchObject([ + { + message: "Variable \"$statusFilter\" got invalid value \"INVALID_STATUS\" at \"statusFilter[0]\"; Value \"INVALID_STATUS\" does not exist in \"ContributionStatus\" enum.", + extensions: { + code: "BAD_USER_INPUT" + }, + }, + ]) + }) + it('returns 17 creations in total', async () => { const { data: { adminListContributions: contributionListObject }, } = await query({ - query: adminListContributions, + query: adminListContributions }) expect(contributionListObject.contributionList).toHaveLength(17) expect(contributionListObject).toMatchObject({ diff --git a/backend/src/graphql/schema.ts b/backend/src/graphql/schema.ts index 18214861f..caca1b5bc 100644 --- a/backend/src/graphql/schema.ts +++ b/backend/src/graphql/schema.ts @@ -12,8 +12,9 @@ export const schema = async (): Promise => { resolvers: [path.join(__dirname, 'resolver', `!(*.test).{js,ts}`)], authChecker: isAuthorized, scalarsMap: [{ type: Decimal, scalar: DecimalScalar }], + emitSchemaFile: true, validate: { - validationError: { target: false }, + validationError: { target: true }, skipMissingProperties: true, skipNullProperties: true, skipUndefinedProperties: false, diff --git a/backend/src/graphql/validator/ContributionStatusArray.ts b/backend/src/graphql/validator/ContributionStatusArray.ts new file mode 100644 index 000000000..da718f70c --- /dev/null +++ b/backend/src/graphql/validator/ContributionStatusArray.ts @@ -0,0 +1,21 @@ +import { registerDecorator, ValidationOptions } from 'class-validator' + +import { ContributionStatus } from '@enum/ContributionStatus' + +export function isContributionStatusArray(validationOptions?: ValidationOptions) { + // eslint-disable-next-line @typescript-eslint/ban-types + return function (object: Object, propertyName: string) { + registerDecorator({ + name: 'isContributionStatusArray', + target: object.constructor, + propertyName, + options: validationOptions, + validator: { + validate(value: ContributionStatus[]): boolean { + const validValues = Object.values(ContributionStatus) + return value.every((item) => validValues.includes(item)) + }, + }, + }) + } +} diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 3dda2633c..949ed86d7 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -251,6 +251,7 @@ export const adminListContributions = gql` $statusFilter: [ContributionStatus!] $userId: Int $query: String + $noHashtag: Boolean ) { adminListContributions( currentPage: $currentPage @@ -259,6 +260,7 @@ export const adminListContributions = gql` statusFilter: $statusFilter userId: $userId query: $query + noHashtag: $noHashtag ) { contributionCount contributionList { From 49ea8a1c0be49f419686fca3cf216589e3f38f0a Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Sat, 19 Aug 2023 11:55:06 +0200 Subject: [PATCH 12/22] take back change vor debugging --- backend/src/graphql/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/schema.ts b/backend/src/graphql/schema.ts index caca1b5bc..64e2f7825 100644 --- a/backend/src/graphql/schema.ts +++ b/backend/src/graphql/schema.ts @@ -14,7 +14,7 @@ export const schema = async (): Promise => { scalarsMap: [{ type: Decimal, scalar: DecimalScalar }], emitSchemaFile: true, validate: { - validationError: { target: true }, + validationError: { target: false }, skipMissingProperties: true, skipNullProperties: true, skipUndefinedProperties: false, From 688872649455fd14e6127ce3e77e07f80cf3342d Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Sat, 19 Aug 2023 11:55:24 +0200 Subject: [PATCH 13/22] take back change vor debugging2 --- backend/src/graphql/schema.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/graphql/schema.ts b/backend/src/graphql/schema.ts index 64e2f7825..18214861f 100644 --- a/backend/src/graphql/schema.ts +++ b/backend/src/graphql/schema.ts @@ -12,7 +12,6 @@ export const schema = async (): Promise => { resolvers: [path.join(__dirname, 'resolver', `!(*.test).{js,ts}`)], authChecker: isAuthorized, scalarsMap: [{ type: Decimal, scalar: DecimalScalar }], - emitSchemaFile: true, validate: { validationError: { target: false }, skipMissingProperties: true, From 4d09d14d8be6142acf710b80ae8d7f3c4950a8bb Mon Sep 17 00:00:00 2001 From: Einhornimmond Date: Sat, 19 Aug 2023 15:12:50 +0200 Subject: [PATCH 14/22] fix linting --- .../graphql/arg/SearchContributionsFilterArgs.ts | 1 + .../graphql/resolver/ContributionResolver.test.ts | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/backend/src/graphql/arg/SearchContributionsFilterArgs.ts b/backend/src/graphql/arg/SearchContributionsFilterArgs.ts index 29446f1df..f1142d758 100644 --- a/backend/src/graphql/arg/SearchContributionsFilterArgs.ts +++ b/backend/src/graphql/arg/SearchContributionsFilterArgs.ts @@ -2,6 +2,7 @@ import { IsBoolean, IsPositive, IsString } from 'class-validator' import { Field, ArgsType, Int } from 'type-graphql' import { ContributionStatus } from '@enum/ContributionStatus' + import { isContributionStatusArray } from '@/graphql/validator/ContributionStatusArray' @ArgsType() diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 52dd04339..ef70565f3 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -2792,18 +2792,19 @@ describe('ContributionResolver', () => { resetToken() }) - it('throw error for invalid ContributionStatus in statusFilter array', async() => { + it('throw error for invalid ContributionStatus in statusFilter array', async () => { const { errors: errorObjects } = await query({ query: adminListContributions, variables: { - statusFilter:["INVALID_STATUS"] - } + statusFilter: ['INVALID_STATUS'], + }, }) expect(errorObjects).toMatchObject([ { - message: "Variable \"$statusFilter\" got invalid value \"INVALID_STATUS\" at \"statusFilter[0]\"; Value \"INVALID_STATUS\" does not exist in \"ContributionStatus\" enum.", + message: + 'Variable "$statusFilter" got invalid value "INVALID_STATUS" at "statusFilter[0]"; Value "INVALID_STATUS" does not exist in "ContributionStatus" enum.', extensions: { - code: "BAD_USER_INPUT" + code: 'BAD_USER_INPUT', }, }, ]) @@ -2813,7 +2814,7 @@ describe('ContributionResolver', () => { const { data: { adminListContributions: contributionListObject }, } = await query({ - query: adminListContributions + query: adminListContributions, }) expect(contributionListObject.contributionList).toHaveLength(17) expect(contributionListObject).toMatchObject({ From 354af5864d293282071843b5ab7450dd382e7f91 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 29 Aug 2023 11:12:43 +0200 Subject: [PATCH 15/22] add working community switch to send coins --- .../src/graphql/arg/TransactionSendArgs.ts | 5 +- .../src/graphql/resolver/CommunityResolver.ts | 2 +- .../graphql/resolver/TransactionResolver.ts | 2 +- frontend/src/components/CommunitySwitch.vue | 58 +++++++++++++++++++ .../GddSend/TransactionConfirmationSend.vue | 4 +- .../components/GddSend/TransactionForm.vue | 16 ++++- frontend/src/graphql/mutations.js | 14 ++++- frontend/src/graphql/queries.js | 5 +- frontend/src/pages/Send.vue | 6 +- 9 files changed, 98 insertions(+), 14 deletions(-) create mode 100644 frontend/src/components/CommunitySwitch.vue diff --git a/backend/src/graphql/arg/TransactionSendArgs.ts b/backend/src/graphql/arg/TransactionSendArgs.ts index ecda848d1..80f25b817 100644 --- a/backend/src/graphql/arg/TransactionSendArgs.ts +++ b/backend/src/graphql/arg/TransactionSendArgs.ts @@ -1,5 +1,5 @@ import { Decimal } from 'decimal.js-light' -import { ArgsType, Field } from 'type-graphql' +import { ArgsType, Field, Int } from 'type-graphql' @ArgsType() export class TransactionSendArgs { @@ -11,4 +11,7 @@ export class TransactionSendArgs { @Field(() => String) memo: string + + @Field(() => Int) + targetCommunity: number } diff --git a/backend/src/graphql/resolver/CommunityResolver.ts b/backend/src/graphql/resolver/CommunityResolver.ts index 4c6c8e785..09553bf24 100644 --- a/backend/src/graphql/resolver/CommunityResolver.ts +++ b/backend/src/graphql/resolver/CommunityResolver.ts @@ -26,7 +26,7 @@ export class CommunityResolver { @Authorized([RIGHTS.COMMUNITIES]) @Query(() => [Community]) - async getCommunitySelections(): Promise { + async communities(): Promise { const dbCommunities: DbCommunity[] = await DbCommunity.find({ order: { name: 'ASC', diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index ba5d6e155..52a240c28 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -317,7 +317,7 @@ export class TransactionResolver { @Authorized([RIGHTS.SEND_COINS]) @Mutation(() => Boolean) async sendCoins( - @Args() { identifier, amount, memo }: TransactionSendArgs, + @Args() { identifier, amount, memo, targetCommunity }: TransactionSendArgs, @Ctx() context: Context, ): Promise { logger.info(`sendCoins(identifier=${identifier}, amount=${amount}, memo=${memo})`) diff --git a/frontend/src/components/CommunitySwitch.vue b/frontend/src/components/CommunitySwitch.vue new file mode 100644 index 000000000..10decbcdf --- /dev/null +++ b/frontend/src/components/CommunitySwitch.vue @@ -0,0 +1,58 @@ + + diff --git a/frontend/src/components/GddSend/TransactionConfirmationSend.vue b/frontend/src/components/GddSend/TransactionConfirmationSend.vue index 95a06ea3c..4a29d00d3 100644 --- a/frontend/src/components/GddSend/TransactionConfirmationSend.vue +++ b/frontend/src/components/GddSend/TransactionConfirmationSend.vue @@ -6,7 +6,7 @@ {{ $t('form.recipientCommunity') }} - {{ communityName }} + {{ targetCommunity.name }} {{ $t('form.recipient') }} @@ -76,11 +76,11 @@ export default { amount: { type: Number, required: true }, memo: { type: String, required: true }, userName: { type: String, default: '' }, + targetCommunity: { type: Object, default: { id: 0, name: COMMUNITY_NAME }}, }, data() { return { disabled: false, - communityName: COMMUNITY_NAME, } }, } diff --git a/frontend/src/components/GddSend/TransactionForm.vue b/frontend/src/components/GddSend/TransactionForm.vue index d5b67d547..1a4873b15 100644 --- a/frontend/src/components/GddSend/TransactionForm.vue +++ b/frontend/src/components/GddSend/TransactionForm.vue @@ -54,7 +54,12 @@ {{ $t('form.recipientCommunity') }} - {{ communityName }} + + + @@ -137,6 +142,7 @@ import { SEND_TYPES } from '@/pages/Send' import InputIdentifier from '@/components/Inputs/InputIdentifier' import InputAmount from '@/components/Inputs/InputAmount' import InputTextarea from '@/components/Inputs/InputTextarea' +import CommunitySwitch from '@/components/CommunitySwitch.vue' import { user as userQuery } from '@/graphql/queries' import { isEmpty } from 'lodash' import { COMMUNITY_NAME } from '@/config' @@ -147,6 +153,7 @@ export default { InputIdentifier, InputAmount, InputTextarea, + CommunitySwitch, }, props: { balance: { type: Number, default: 0 }, @@ -154,6 +161,7 @@ export default { amount: { type: Number, default: 0 }, memo: { type: String, default: '' }, selected: { type: String, default: 'send' }, + targetCommunity: { type: Object, default: { id: 0, name: COMMUNITY_NAME } }, }, data() { return { @@ -161,10 +169,10 @@ export default { identifier: this.identifier, amount: this.amount ? String(this.amount) : '', memo: this.memo, + targetCommunity: this.targetCommunity, }, radioSelected: this.selected, - userName: '', - communityName: COMMUNITY_NAME, + userName: '' } }, methods: { @@ -179,6 +187,7 @@ export default { amount: Number(this.form.amount.replace(',', '.')), memo: this.form.memo, userName: this.userName, + targetCommunity: this.form.targetCommunity, }) }, onReset(event) { @@ -186,6 +195,7 @@ export default { this.form.identifier = '' this.form.amount = '' this.form.memo = '' + this.form.targetCommunity = { id: 0, name: COMMUNITY_NAME } this.$refs.formValidator.validate() if (this.$route.query && !isEmpty(this.$route.query)) this.$router.replace({ query: undefined }) diff --git a/frontend/src/graphql/mutations.js b/frontend/src/graphql/mutations.js index 2f6b53ac9..91873e627 100644 --- a/frontend/src/graphql/mutations.js +++ b/frontend/src/graphql/mutations.js @@ -71,8 +71,18 @@ export const createUser = gql` ` export const sendCoins = gql` - mutation($identifier: String!, $amount: Decimal!, $memo: String!) { - sendCoins(identifier: $identifier, amount: $amount, memo: $memo) + mutation( + $identifier: String! + $amount: Decimal! + $memo: String! + $targetCommunity: Int! + ) { + sendCoins( + identifier: $identifier, + amount: $amount, + memo: $memo, + targetCommunity: $targetCommunity + ) } ` diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index c7e1e9067..7e23f5f03 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -72,14 +72,13 @@ export const listGDTEntriesQuery = gql` } ` -export const communities = gql` +export const selectCommunities = gql` query { communities { id name - url description - registerUrl + foreign } } ` diff --git a/frontend/src/pages/Send.vue b/frontend/src/pages/Send.vue index 30ffc06ed..924eb7388 100644 --- a/frontend/src/pages/Send.vue +++ b/frontend/src/pages/Send.vue @@ -122,7 +122,11 @@ export default { this.$apollo .mutate({ mutation: sendCoins, - variables: this.transactionData, + variables: { + ...this.transactionData, + // from target community we need only the id + targetCommunity: this.transactionData.targetCommunity.id, + }, }) .then(() => { this.error = false From 67d48a2eec7880bea3074ed7257af5b7bc1e7d4f Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Tue, 29 Aug 2023 16:30:33 +0200 Subject: [PATCH 16/22] linting, add claus peters suggestions --- backend/src/graphql/arg/TransactionSendArgs.ts | 11 ++++++----- .../src/graphql/resolver/TransactionResolver.ts | 9 ++++++--- frontend/src/components/CommunitySwitch.vue | 14 +++++++++----- .../GddSend/TransactionConfirmationSend.vue | 7 ++++++- .../src/components/GddSend/TransactionForm.vue | 15 ++++++++++----- frontend/src/graphql/mutations.js | 12 ++++++------ frontend/src/graphql/queries.js | 2 +- frontend/src/pages/Send.vue | 8 +++++--- 8 files changed, 49 insertions(+), 29 deletions(-) diff --git a/backend/src/graphql/arg/TransactionSendArgs.ts b/backend/src/graphql/arg/TransactionSendArgs.ts index 7691d7b80..5bd8b89f7 100644 --- a/backend/src/graphql/arg/TransactionSendArgs.ts +++ b/backend/src/graphql/arg/TransactionSendArgs.ts @@ -1,6 +1,6 @@ import { MaxLength, MinLength, IsString } from 'class-validator' import { Decimal } from 'decimal.js-light' -import { ArgsType, Field, Int } from 'type-graphql' +import { ArgsType, Field } from 'type-graphql' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const' import { IsPositiveDecimal } from '@/graphql/validator/Decimal' @@ -9,7 +9,11 @@ import { IsPositiveDecimal } from '@/graphql/validator/Decimal' export class TransactionSendArgs { @Field(() => String) @IsString() - identifier: string + recipientCommunityIdentifier: string + + @Field(() => String) + @IsString() + recipientIdentifier: string @Field(() => Decimal) @IsPositiveDecimal() @@ -19,7 +23,4 @@ export class TransactionSendArgs { @MaxLength(MEMO_MAX_CHARS) @MinLength(MEMO_MIN_CHARS) memo: string - - @Field(() => Int) - targetCommunity: number } diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 264a8e219..32b453ae1 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -308,15 +308,18 @@ export class TransactionResolver { @Authorized([RIGHTS.SEND_COINS]) @Mutation(() => Boolean) async sendCoins( - @Args() { identifier, amount, memo, targetCommunity }: TransactionSendArgs, + @Args() + { /* recipientCommunityIdentifier, */ recipientIdentifier, amount, memo }: TransactionSendArgs, @Ctx() context: Context, ): Promise { - logger.info(`sendCoins(identifier=${identifier}, amount=${amount}, memo=${memo})`) + logger.info( + `sendCoins(recipientIdentifier=${recipientIdentifier}, amount=${amount}, memo=${memo})`, + ) const senderUser = getUser(context) // validate recipient user - const recipientUser = await findUserByIdentifier(identifier) + const recipientUser = await findUserByIdentifier(recipientIdentifier) if (!recipientUser) { throw new LogError('The recipient user was not found', recipientUser) } diff --git a/frontend/src/components/CommunitySwitch.vue b/frontend/src/components/CommunitySwitch.vue index 10decbcdf..257d3ef83 100644 --- a/frontend/src/components/CommunitySwitch.vue +++ b/frontend/src/components/CommunitySwitch.vue @@ -6,7 +6,7 @@ @click.prevent="updateCommunity(community)" :key="community.id" :title="community.description" - :active="value.id === community.id" + :active="value.uuid === community.uuid" > {{ community.name }} @@ -20,7 +20,12 @@ import { COMMUNITY_NAME } from '@/config' export default { name: 'CommunitySwitch', props: { - value: { type: Object, default: { id: 0, name: COMMUNITY_NAME } }, + value: { + type: Object, + default: function () { + return { uuid: '', name: COMMUNITY_NAME } + }, + }, }, data() { return { @@ -29,13 +34,12 @@ export default { }, methods: { updateCommunity(community) { - this.value = community - this.$emit('input', this.value) + this.$emit('input', community) }, setDefaultCommunity() { // set default community, the only one which isn't foreign // we assume it is only one entry with foreign = false - if (!this.value.id && this.communities.length) { + if (this.value.uuid === '' && this.communities.length) { const foundCommunity = this.communities.find((community) => !community.foreign) if (foundCommunity) { this.updateCommunity(foundCommunity) diff --git a/frontend/src/components/GddSend/TransactionConfirmationSend.vue b/frontend/src/components/GddSend/TransactionConfirmationSend.vue index 4a29d00d3..caa40d128 100644 --- a/frontend/src/components/GddSend/TransactionConfirmationSend.vue +++ b/frontend/src/components/GddSend/TransactionConfirmationSend.vue @@ -76,7 +76,12 @@ export default { amount: { type: Number, required: true }, memo: { type: String, required: true }, userName: { type: String, default: '' }, - targetCommunity: { type: Object, default: { id: 0, name: COMMUNITY_NAME }}, + targetCommunity: { + type: Object, + default: function () { + return { uuid: '', name: COMMUNITY_NAME } + }, + }, }, data() { return { diff --git a/frontend/src/components/GddSend/TransactionForm.vue b/frontend/src/components/GddSend/TransactionForm.vue index 1a4873b15..7304137ee 100644 --- a/frontend/src/components/GddSend/TransactionForm.vue +++ b/frontend/src/components/GddSend/TransactionForm.vue @@ -55,8 +55,8 @@ - @@ -161,7 +161,12 @@ export default { amount: { type: Number, default: 0 }, memo: { type: String, default: '' }, selected: { type: String, default: 'send' }, - targetCommunity: { type: Object, default: { id: 0, name: COMMUNITY_NAME } }, + targetCommunity: { + type: Object, + default: function () { + return { uuid: '', name: COMMUNITY_NAME } + }, + }, }, data() { return { @@ -172,7 +177,7 @@ export default { targetCommunity: this.targetCommunity, }, radioSelected: this.selected, - userName: '' + userName: '', } }, methods: { @@ -195,7 +200,7 @@ export default { this.form.identifier = '' this.form.amount = '' this.form.memo = '' - this.form.targetCommunity = { id: 0, name: COMMUNITY_NAME } + this.form.targetCommunity = { uuid: '', name: COMMUNITY_NAME } this.$refs.formValidator.validate() if (this.$route.query && !isEmpty(this.$route.query)) this.$router.replace({ query: undefined }) diff --git a/frontend/src/graphql/mutations.js b/frontend/src/graphql/mutations.js index 91873e627..b4f96179f 100644 --- a/frontend/src/graphql/mutations.js +++ b/frontend/src/graphql/mutations.js @@ -72,16 +72,16 @@ export const createUser = gql` export const sendCoins = gql` mutation( - $identifier: String! + $recipientCommunityIdentifier: String! + $recipientIdentifier: String! $amount: Decimal! $memo: String! - $targetCommunity: Int! ) { sendCoins( - identifier: $identifier, - amount: $amount, - memo: $memo, - targetCommunity: $targetCommunity + recipientCommunityIdentifier: $recipientCommunityIdentifier + recipientIdentifier: $recipientIdentifier + amount: $amount + memo: $memo ) } ` diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index 7e23f5f03..6ef5d56d6 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -75,7 +75,7 @@ export const listGDTEntriesQuery = gql` export const selectCommunities = gql` query { communities { - id + uuid name description foreign diff --git a/frontend/src/pages/Send.vue b/frontend/src/pages/Send.vue index 924eb7388..607c7e36c 100644 --- a/frontend/src/pages/Send.vue +++ b/frontend/src/pages/Send.vue @@ -123,9 +123,11 @@ export default { .mutate({ mutation: sendCoins, variables: { - ...this.transactionData, - // from target community we need only the id - targetCommunity: this.transactionData.targetCommunity.id, + // from target community we need only the uuid + recipientCommunityIdentifier: this.transactionData.targetCommunity.uuid, + recipientIdentifier: this.transactionData.identifier, + amount: this.transactionData.amount, + memo: this.transactionData.memo, }, }) .then(() => { From fef424b93d4401272cb05c4fef9679b4a9287885 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Tue, 29 Aug 2023 16:54:09 +0200 Subject: [PATCH 17/22] remove not existing query, update existing along with resolver --- backend/src/seeds/graphql/queries.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 949ed86d7..1b5a84d45 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -118,17 +118,6 @@ export const listGDTEntriesQuery = gql` } ` -export const communityInfo = gql` - query { - getCommunityInfo { - name - description - registerUrl - url - } - } -` - export const communities = gql` query { communities { @@ -159,7 +148,7 @@ export const getCommunities = gql` export const getCommunitySelections = gql` query { - getCommunitySelections { + communities { id foreign name From 9a8fac54aec6138acc917453a402bb279b43453d Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Wed, 30 Aug 2023 18:00:26 +0200 Subject: [PATCH 18/22] fix frontend tests --- frontend/src/components/CommunitySwitch.vue | 4 --- .../GddSend/TransactionForm.spec.js | 31 ++++++++++++++++++- frontend/src/pages/Send.spec.js | 12 +++---- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/CommunitySwitch.vue b/frontend/src/components/CommunitySwitch.vue index 257d3ef83..dd4b159aa 100644 --- a/frontend/src/components/CommunitySwitch.vue +++ b/frontend/src/components/CommunitySwitch.vue @@ -15,16 +15,12 @@