diff --git a/backend/src/graphql/arg/AdminCreateContributionArgs.ts b/backend/src/graphql/arg/AdminCreateContributionArgs.ts index 8e2fa28da..e734fc514 100644 --- a/backend/src/graphql/arg/AdminCreateContributionArgs.ts +++ b/backend/src/graphql/arg/AdminCreateContributionArgs.ts @@ -1,18 +1,28 @@ +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) + @IsPositiveDecimal() amount: Decimal @Field(() => String) + @MaxLength(MEMO_MAX_CHARS) + @MinLength(MEMO_MIN_CHARS) memo: string @Field(() => String) + @isValidDateString() creationDate: string } diff --git a/backend/src/graphql/arg/AdminUpdateContributionArgs.ts b/backend/src/graphql/arg/AdminUpdateContributionArgs.ts index e79260c63..f7288cb28 100644 --- a/backend/src/graphql/arg/AdminUpdateContributionArgs.ts +++ b/backend/src/graphql/arg/AdminUpdateContributionArgs.ts @@ -1,17 +1,27 @@ +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) + @IsPositiveDecimal() amount: Decimal @Field(() => String) + @MaxLength(MEMO_MAX_CHARS) + @MinLength(MEMO_MIN_CHARS) memo: string @Field(() => String) + @isValidDateString() creationDate: string } diff --git a/backend/src/graphql/arg/ContributionArgs.ts b/backend/src/graphql/arg/ContributionArgs.ts index db688d811..9f3951ac2 100644 --- a/backend/src/graphql/arg/ContributionArgs.ts +++ b/backend/src/graphql/arg/ContributionArgs.ts @@ -1,15 +1,24 @@ +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' +import { isValidDateString } from '@/graphql/validator/DateString' +import { IsPositiveDecimal } from '@/graphql/validator/Decimal' + @InputType() @ArgsType() export class ContributionArgs { @Field(() => Decimal) + @IsPositiveDecimal() amount: Decimal @Field(() => String) + @MaxLength(MEMO_MAX_CHARS) + @MinLength(MEMO_MIN_CHARS) memo: string @Field(() => String) + @isValidDateString() creationDate: string } diff --git a/backend/src/graphql/arg/ContributionLinkArgs.ts b/backend/src/graphql/arg/ContributionLinkArgs.ts index cef72148a..97cf3dfdd 100644 --- a/backend/src/graphql/arg/ContributionLinkArgs.ts +++ b/backend/src/graphql/arg/ContributionLinkArgs.ts @@ -1,29 +1,49 @@ +import { IsPositive, IsString, 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, + 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() export class ContributionLinkArgs { @Field(() => Decimal) + @IsPositiveDecimal() amount: Decimal @Field(() => String) + @MaxLength(CONTRIBUTIONLINK_NAME_MAX_CHARS) + @MinLength(CONTRIBUTIONLINK_NAME_MIN_CHARS) name: string @Field(() => String) + @MaxLength(MEMO_MAX_CHARS) + @MinLength(MEMO_MIN_CHARS) 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 58cd77be0..6bb31a6b8 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, { defaultValue: 1 }) + @IsPositive() currentPage: number @Field(() => Int, { defaultValue: 3 }) + @IsPositive() pageSize: number @Field(() => Order, { defaultValue: Order.DESC }) + @IsEnum(Order) order: Order } diff --git a/backend/src/graphql/arg/SearchContributionsFilterArgs.ts b/backend/src/graphql/arg/SearchContributionsFilterArgs.ts index e256fa663..f1142d758 100644 --- a/backend/src/graphql/arg/SearchContributionsFilterArgs.ts +++ b/backend/src/graphql/arg/SearchContributionsFilterArgs.ts @@ -1,18 +1,25 @@ +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/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 f44ef0356..039686bca 100644 --- a/backend/src/graphql/arg/TransactionLinkArgs.ts +++ b/backend/src/graphql/arg/TransactionLinkArgs.ts @@ -1,11 +1,18 @@ +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' +import { IsPositiveDecimal } from '@/graphql/validator/Decimal' + @ArgsType() export class TransactionLinkArgs { @Field(() => Decimal) + @IsPositiveDecimal() amount: Decimal @Field(() => String) + @MaxLength(MEMO_MAX_CHARS) + @MinLength(MEMO_MIN_CHARS) memo: string } 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 ecda848d1..48827be8d 100644 --- a/backend/src/graphql/arg/TransactionSendArgs.ts +++ b/backend/src/graphql/arg/TransactionSendArgs.ts @@ -1,14 +1,26 @@ +import { MaxLength, MinLength, IsString } 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 TransactionSendArgs { + @Field(() => String, { nullable: true }) + @IsString() + recipientCommunityIdentifier?: string | null | undefined + @Field(() => String) - identifier: string + @IsString() + recipientIdentifier: string @Field(() => Decimal) + @IsPositiveDecimal() amount: Decimal @Field(() => String) + @MaxLength(MEMO_MAX_CHARS) + @MinLength(MEMO_MIN_CHARS) memo: string } 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/CommunityResolver.test.ts b/backend/src/graphql/resolver/CommunityResolver.test.ts index 4b4101e66..0ded14405 100644 --- a/backend/src/graphql/resolver/CommunityResolver.test.ts +++ b/backend/src/graphql/resolver/CommunityResolver.test.ts @@ -12,7 +12,7 @@ import { ApolloServerTestClient } from 'apollo-server-testing' import { cleanDB, testEnvironment } from '@test/helpers' -import { getCommunities, getCommunitySelections } from '@/seeds/graphql/queries' +import { getCommunities, communities } from '@/seeds/graphql/queries' // to do: We need a setup for the tests that closes the connection let query: ApolloServerTestClient['query'], con: Connection @@ -234,7 +234,7 @@ describe('CommunityResolver', () => { }) }) - describe('getCommunitySelections', () => { + describe('communities', () => { let homeCom1: DbCommunity let foreignCom1: DbCommunity let foreignCom2: DbCommunity @@ -248,9 +248,9 @@ describe('CommunityResolver', () => { it('returns no community entry', async () => { // const result: Community[] = await query({ query: getCommunities }) // expect(result.length).toEqual(0) - await expect(query({ query: getCommunitySelections })).resolves.toMatchObject({ + await expect(query({ query: communities })).resolves.toMatchObject({ data: { - getCommunitySelections: [], + communities: [], }, }) }) @@ -275,9 +275,9 @@ describe('CommunityResolver', () => { }) it('returns 1 home-community entry', async () => { - await expect(query({ query: getCommunitySelections })).resolves.toMatchObject({ + await expect(query({ query: communities })).resolves.toMatchObject({ data: { - getCommunitySelections: [ + communities: [ { id: expect.any(Number), foreign: homeCom1.foreign, @@ -337,9 +337,9 @@ describe('CommunityResolver', () => { }) it('returns 3 community entries', async () => { - await expect(query({ query: getCommunitySelections })).resolves.toMatchObject({ + await expect(query({ query: communities })).resolves.toMatchObject({ data: { - getCommunitySelections: [ + communities: [ { id: expect.any(Number), foreign: homeCom1.foreign, 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/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/ContributionLinkResolver.ts b/backend/src/graphql/resolver/ContributionLinkResolver.ts index 808bd584e..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' @@ -18,12 +17,6 @@ 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 { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' import { isStartEndDateValid } from './util/creations' @@ -46,21 +39,6 @@ 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 (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) - } const dbContributionLink = new DbContributionLink() dbContributionLink.amount = amount diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index a678c7531..e6cb485a3 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) }) }) @@ -2732,6 +2800,24 @@ 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 18 creations in total', async () => { const { data: { adminListContributions: contributionListObject }, diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 5c103f188..1ffa53b27 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -45,12 +45,10 @@ 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, updateCreations, - isValidDateString, getOpenCreations, } from './util/creations' import { findContributions } from './util/findContributions' @@ -66,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) @@ -181,12 +173,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) @@ -264,9 +250,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..fe047b35a 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts @@ -94,38 +94,115 @@ 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', + }, }) + expect(errorObjects).toMatchObject([ + { + message: 'Argument Validation Error', + extensions: { + exception: { + validationErrors: [ + { + property: 'amount', + constraints: { + isPositiveDecimal: 'The amount must be a positive value amount', + }, + }, + ], + }, + }, + }, + ]) }) - it('logs the error "Amount must be a positive number" - -10', () => { - expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(-10)) + + 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 () => { @@ -135,7 +212,7 @@ describe('TransactionLinkResolver', () => { mutation: createTransactionLink, variables: { amount: 1001, - memo: 'Test', + memo: 'Test Test', }, }), ).resolves.toMatchObject({ diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index fa91e4bdd..32ec5a654 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -74,10 +74,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.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index 380386795..bd7d0f2a8 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -7,7 +7,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' @@ -92,9 +91,9 @@ describe('send coins', () => { await mutate({ mutation: sendCoins, variables: { - identifier: 'wrong@email.com', + recipientIdentifier: 'wrong@email.com', amount: 100, - memo: 'test', + memo: 'test test', }, }), ).toEqual( @@ -120,9 +119,9 @@ describe('send coins', () => { await mutate({ mutation: sendCoins, variables: { - identifier: 'stephen@hawking.uk', + recipientIdentifier: 'stephen@hawking.uk', amount: 100, - memo: 'test', + memo: 'test test', }, }), ).toEqual( @@ -149,9 +148,9 @@ describe('send coins', () => { await mutate({ mutation: sendCoins, variables: { - identifier: 'garrick@ollivander.com', + recipientIdentifier: 'garrick@ollivander.com', amount: 100, - memo: 'test', + memo: 'test test', }, }), ).toEqual( @@ -185,9 +184,9 @@ describe('send coins', () => { await mutate({ mutation: sendCoins, variables: { - identifier: 'bob@baumeister.de', + recipientIdentifier: 'bob@baumeister.de', amount: 100, - memo: 'test', + memo: 'test test', }, }), ).toEqual( @@ -205,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: { + recipientIdentifier: '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: { + recipientIdentifier: '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) + }, + ]) }) }) @@ -257,7 +270,7 @@ describe('send coins', () => { await mutate({ mutation: sendCoins, variables: { - identifier: 'peter@lustig.de', + recipientIdentifier: 'peter@lustig.de', amount: 100, memo: 'testing', }, @@ -303,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: { + recipientIdentifier: '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)) + }, + ]) }) }) @@ -330,7 +350,7 @@ describe('send coins', () => { await mutate({ mutation: sendCoins, variables: { - identifier: 'peter@lustig.de', + recipientIdentifier: 'peter@lustig.de', amount: 50, memo: 'unrepeatable memo', }, @@ -436,7 +456,7 @@ describe('send coins', () => { mutate({ mutation: sendCoins, variables: { - identifier: peter?.gradidoID, + recipientIdentifier: peter?.gradidoID, amount: 10, memo: 'send via gradido ID', }, @@ -476,7 +496,7 @@ describe('send coins', () => { mutate({ mutation: sendCoins, variables: { - identifier: 'bob', + recipientIdentifier: 'bob', amount: 6.66, memo: 'send via alias', }, @@ -544,7 +564,7 @@ describe('send coins', () => { mutate({ mutation: sendCoins, variables: { - identifier: 'peter@lustig.de', + recipientIdentifier: 'peter@lustig.de', amount: 10, memo: 'first transaction', }, @@ -560,7 +580,7 @@ describe('send coins', () => { mutate({ mutation: sendCoins, variables: { - identifier: 'peter@lustig.de', + recipientIdentifier: 'peter@lustig.de', amount: 20, memo: 'second transaction', }, @@ -576,7 +596,7 @@ describe('send coins', () => { mutate({ mutation: sendCoins, variables: { - identifier: 'peter@lustig.de', + recipientIdentifier: 'peter@lustig.de', amount: 30, memo: 'third transaction', }, @@ -592,7 +612,7 @@ describe('send coins', () => { mutate({ mutation: sendCoins, variables: { - identifier: 'peter@lustig.de', + recipientIdentifier: 'peter@lustig.de', amount: 40, memo: 'fourth transaction', }, diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index ba5d6e155..32b453ae1 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' @@ -56,14 +55,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( @@ -317,18 +308,18 @@ export class TransactionResolver { @Authorized([RIGHTS.SEND_COINS]) @Mutation(() => Boolean) async sendCoins( - @Args() { identifier, amount, memo }: TransactionSendArgs, + @Args() + { /* recipientCommunityIdentifier, */ recipientIdentifier, amount, memo }: TransactionSendArgs, @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) - } + 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/backend/src/graphql/resolver/semaphore.test.ts b/backend/src/graphql/resolver/semaphore.test.ts index 366b94d72..37331d832 100644 --- a/backend/src/graphql/resolver/semaphore.test.ts +++ b/backend/src/graphql/resolver/semaphore.test.ts @@ -156,7 +156,11 @@ describe('semaphore', () => { }) const bibisTransaction = mutate({ mutation: sendCoins, - variables: { identifier: 'bob@baumeister.de', amount: '50', memo: 'Das ist für dich, Bob' }, + variables: { + recipientIdentifier: 'bob@baumeister.de', + amount: '50', + memo: 'Das ist für dich, Bob', + }, }) await mutate({ mutation: login, @@ -172,7 +176,11 @@ describe('semaphore', () => { }) const bobsTransaction = mutate({ mutation: sendCoins, - variables: { identifier: 'bibi@bloxberg.de', amount: '50', memo: 'Das ist für dich, Bibi' }, + variables: { + recipientIdentifier: 'bibi@bloxberg.de', + amount: '50', + memo: 'Das ist für dich, Bibi', + }, }) await mutate({ mutation: login, diff --git a/backend/src/graphql/schema.ts b/backend/src/graphql/schema.ts index f14276c86..18214861f 100644 --- a/backend/src/graphql/schema.ts +++ b/backend/src/graphql/schema.ts @@ -12,5 +12,13 @@ export const schema = async (): Promise => { resolvers: [path.join(__dirname, 'resolver', `!(*.test).{js,ts}`)], authChecker: isAuthorized, scalarsMap: [{ type: Decimal, scalar: DecimalScalar }], + validate: { + validationError: { target: false }, + skipMissingProperties: true, + skipNullProperties: true, + skipUndefinedProperties: false, + forbidUnknownValues: true, + stopAtFirstError: true, + }, }) } 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/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}` + }, + }, + }) + } +} diff --git a/backend/src/graphql/validator/Decimal.ts b/backend/src/graphql/validator/Decimal.ts new file mode 100644 index 000000000..09e8fb4bd --- /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) { + // eslint-disable-next-line @typescript-eslint/ban-types + return function (object: Object, propertyName: string) { + registerDecorator({ + name: 'isPositiveDecimal', + 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}` + }, + }, + }) + } +} diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index 87231531f..965b52bec 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -79,8 +79,8 @@ export const sendActivationEmail = gql` ` export const sendCoins = gql` - mutation ($identifier: String!, $amount: Decimal!, $memo: String!) { - sendCoins(identifier: $identifier, amount: $amount, memo: $memo) + mutation ($recipientIdentifier: String!, $amount: Decimal!, $memo: String!) { + sendCoins(recipientIdentifier: $recipientIdentifier, amount: $amount, memo: $memo) } ` diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 949ed86d7..b7ef87aa8 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -118,25 +118,17 @@ export const listGDTEntriesQuery = gql` } ` -export const communityInfo = gql` - query { - getCommunityInfo { - name - description - registerUrl - url - } - } -` - export const communities = gql` query { communities { id + foreign name - url description - registerUrl + url + creationDate + uuid + authenticatedAt } } ` @@ -157,21 +149,6 @@ export const getCommunities = gql` } ` -export const getCommunitySelections = gql` - query { - getCommunitySelections { - id - foreign - name - description - url - creationDate - uuid - authenticatedAt - } - } -` - export const queryTransactionLink = gql` query ($code: String!) { queryTransactionLink(code: $code) { diff --git a/e2e-tests/cypress/support/step_definitions/send_coin_steps.ts b/e2e-tests/cypress/support/step_definitions/send_coin_steps.ts index f3bcfe36c..a114c20c5 100644 --- a/e2e-tests/cypress/support/step_definitions/send_coin_steps.ts +++ b/e2e-tests/cypress/support/step_definitions/send_coin_steps.ts @@ -49,8 +49,13 @@ When('the user submits the transaction by confirming', () => { cy.wrap(interception.request.body).should( 'have.property', 'query', - `mutation ($identifier: String!, $amount: Decimal!, $memo: String!) { - sendCoins(identifier: $identifier, amount: $amount, memo: $memo) + `mutation ($recipientCommunityIdentifier: String!, $recipientIdentifier: String!, $amount: Decimal!, $memo: String!) { + sendCoins( + recipientCommunityIdentifier: $recipientCommunityIdentifier + recipientIdentifier: $recipientIdentifier + amount: $amount + memo: $memo + ) } `, ) diff --git a/frontend/src/components/CommunitySwitch.vue b/frontend/src/components/CommunitySwitch.vue new file mode 100644 index 000000000..dd4b159aa --- /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..caa40d128 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,16 @@ export default { amount: { type: Number, required: true }, memo: { type: String, required: true }, userName: { type: String, default: '' }, + targetCommunity: { + type: Object, + default: function () { + return { uuid: '', name: COMMUNITY_NAME } + }, + }, }, data() { return { disabled: false, - communityName: COMMUNITY_NAME, } }, } diff --git a/frontend/src/components/GddSend/TransactionForm.spec.js b/frontend/src/components/GddSend/TransactionForm.spec.js index e4cee20be..bc9dfa93f 100644 --- a/frontend/src/components/GddSend/TransactionForm.spec.js +++ b/frontend/src/components/GddSend/TransactionForm.spec.js @@ -4,7 +4,7 @@ import flushPromises from 'flush-promises' import { SEND_TYPES } from '@/pages/Send' import { createMockClient } from 'mock-apollo-client' import VueApollo from 'vue-apollo' -import { user as userQuery } from '@/graphql/queries' +import { user as userQuery, selectCommunities as selectCommunitiesQuery } from '@/graphql/queries' const mockClient = createMockClient() const apolloProvider = new VueApollo({ @@ -61,6 +61,28 @@ describe('TransactionForm', () => { }), ) + mockClient.setRequestHandler( + selectCommunitiesQuery, + jest.fn().mockResolvedValue({ + data: { + communities: [ + { + uuid: '8f4c146a-79b5-413f-89ed-53f624ec49b2', + name: 'Gradido Entwicklung', + description: 'Gradido-Community einer lokalen Entwicklungsumgebung.', + foreign: false, + }, + { + uuid: 'ashasas', + name: 'Hunde-Community', + description: 'Hier geht es um Hunde', + foreign: true, + }, + ], + }, + }), + ) + describe('mount', () => { beforeEach(() => { wrapper = Wrapper() @@ -352,6 +374,12 @@ Die ganze Welt bezwingen.“`) memo: 'Long enough', selected: 'send', userName: '', + targetCommunity: { + description: 'Gradido-Community einer lokalen Entwicklungsumgebung.', + foreign: false, + name: 'Gradido Entwicklung', + uuid: '8f4c146a-79b5-413f-89ed-53f624ec49b2', + }, }, ], ]) diff --git a/frontend/src/components/GddSend/TransactionForm.vue b/frontend/src/components/GddSend/TransactionForm.vue index d5b67d547..7304137ee 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,12 @@ export default { amount: { type: Number, default: 0 }, memo: { type: String, default: '' }, selected: { type: String, default: 'send' }, + targetCommunity: { + type: Object, + default: function () { + return { uuid: '', name: COMMUNITY_NAME } + }, + }, }, data() { return { @@ -161,10 +174,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, } }, methods: { @@ -179,6 +192,7 @@ export default { amount: Number(this.form.amount.replace(',', '.')), memo: this.form.memo, userName: this.userName, + targetCommunity: this.form.targetCommunity, }) }, onReset(event) { @@ -186,6 +200,7 @@ export default { this.form.identifier = '' this.form.amount = '' this.form.memo = '' + 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 2f6b53ac9..b4f96179f 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( + $recipientCommunityIdentifier: String! + $recipientIdentifier: String! + $amount: Decimal! + $memo: String! + ) { + sendCoins( + recipientCommunityIdentifier: $recipientCommunityIdentifier + recipientIdentifier: $recipientIdentifier + amount: $amount + memo: $memo + ) } ` diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index c7e1e9067..6ef5d56d6 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 + uuid name - url description - registerUrl + foreign } } ` diff --git a/frontend/src/pages/Send.spec.js b/frontend/src/pages/Send.spec.js index 1001d0c58..9427e32f8 100644 --- a/frontend/src/pages/Send.spec.js +++ b/frontend/src/pages/Send.spec.js @@ -1,5 +1,5 @@ import { mount } from '@vue/test-utils' -import Send, { SEND_TYPES } from './Send' +import Send from './Send' import { toastErrorSpy, toastSuccessSpy } from '@test/testSetup' import { TRANSACTION_STEPS } from '@/components/GddSend' import { sendCoins, createTransactionLink } from '@/graphql/mutations.js' @@ -118,11 +118,10 @@ describe('Send', () => { expect.objectContaining({ mutation: sendCoins, variables: { - identifier: 'user@example.org', + recipientIdentifier: 'user@example.org', amount: 23.45, memo: 'Make the best of it!', - selected: SEND_TYPES.send, - userName: '', + recipientCommunityIdentifier: '', }, }), ) @@ -217,11 +216,10 @@ describe('Send', () => { expect.objectContaining({ mutation: sendCoins, variables: { - identifier: 'gradido-ID', + recipientIdentifier: 'gradido-ID', amount: 34.56, memo: 'Make the best of it!', - selected: SEND_TYPES.send, - userName: '', + recipientCommunityIdentifier: '', }, }), ) diff --git a/frontend/src/pages/Send.vue b/frontend/src/pages/Send.vue index 30ffc06ed..607c7e36c 100644 --- a/frontend/src/pages/Send.vue +++ b/frontend/src/pages/Send.vue @@ -122,7 +122,13 @@ export default { this.$apollo .mutate({ mutation: sendCoins, - variables: this.transactionData, + variables: { + // 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(() => { this.error = false