From e2f65cddc3079c28b5cc29927630d089e63bad39 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Fri, 11 Aug 2023 10:39:53 +0200 Subject: [PATCH] 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}` + }, + }, + }) + } +}