increase auto validation, update tests

This commit is contained in:
einhorn_b 2023-08-11 10:39:53 +02:00
parent 60b0eed582
commit e2f65cddc3
22 changed files with 439 additions and 233 deletions

View File

@ -1,14 +1,16 @@
import { MaxLength, MinLength } from 'class-validator' import { IsEmail, MaxLength, MinLength } from 'class-validator'
import { Decimal } from 'decimal.js-light' import { Decimal } from 'decimal.js-light'
import { ArgsType, Field, InputType } from 'type-graphql' import { ArgsType, Field, InputType } from 'type-graphql'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const'
import { isValidDateString } from '@/graphql/validator/DateString'
import { IsPositiveDecimal } from '@/graphql/validator/Decimal' import { IsPositiveDecimal } from '@/graphql/validator/Decimal'
@InputType() @InputType()
@ArgsType() @ArgsType()
export class AdminCreateContributionArgs { export class AdminCreateContributionArgs {
@Field(() => String) @Field(() => String)
@IsEmail()
email: string email: string
@Field(() => Decimal) @Field(() => Decimal)
@ -21,5 +23,6 @@ export class AdminCreateContributionArgs {
memo: string memo: string
@Field(() => String) @Field(() => String)
@isValidDateString()
creationDate: string creationDate: string
} }

View File

@ -1,13 +1,15 @@
import { MaxLength, MinLength } from 'class-validator' import { IsPositive, MaxLength, MinLength } from 'class-validator'
import { Decimal } from 'decimal.js-light' import { Decimal } from 'decimal.js-light'
import { ArgsType, Field, Int } from 'type-graphql' import { ArgsType, Field, Int } from 'type-graphql'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const'
import { isValidDateString } from '@/graphql/validator/DateString'
import { IsPositiveDecimal } from '@/graphql/validator/Decimal' import { IsPositiveDecimal } from '@/graphql/validator/Decimal'
@ArgsType() @ArgsType()
export class AdminUpdateContributionArgs { export class AdminUpdateContributionArgs {
@Field(() => Int) @Field(() => Int)
@IsPositive()
id: number id: number
@Field(() => Decimal) @Field(() => Decimal)
@ -20,5 +22,6 @@ export class AdminUpdateContributionArgs {
memo: string memo: string
@Field(() => String) @Field(() => String)
@isValidDateString()
creationDate: string creationDate: string
} }

View File

@ -3,6 +3,7 @@ import { Decimal } from 'decimal.js-light'
import { ArgsType, Field, InputType } from 'type-graphql' import { ArgsType, Field, InputType } from 'type-graphql'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const'
import { isValidDateString } from '@/graphql/validator/DateString'
import { IsPositiveDecimal } from '@/graphql/validator/Decimal' import { IsPositiveDecimal } from '@/graphql/validator/Decimal'
@InputType() @InputType()
@ -18,5 +19,6 @@ export class ContributionArgs {
memo: string memo: string
@Field(() => String) @Field(() => String)
@isValidDateString()
creationDate: string creationDate: string
} }

View File

@ -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 { Decimal } from 'decimal.js-light'
import { ArgsType, Field, Int } from 'type-graphql' import { ArgsType, Field, Int } from 'type-graphql'
@ -8,6 +8,7 @@ import {
CONTRIBUTIONLINK_NAME_MIN_CHARS, CONTRIBUTIONLINK_NAME_MIN_CHARS,
CONTRIBUTIONLINK_NAME_MAX_CHARS, CONTRIBUTIONLINK_NAME_MAX_CHARS,
} from '@/graphql/resolver/const/const' } from '@/graphql/resolver/const/const'
import { isValidDateString } from '@/graphql/validator/DateString'
import { IsPositiveDecimal } from '@/graphql/validator/Decimal' import { IsPositiveDecimal } from '@/graphql/validator/Decimal'
@ArgsType() @ArgsType()
@ -27,17 +28,22 @@ export class ContributionLinkArgs {
memo: string memo: string
@Field(() => String) @Field(() => String)
@IsString()
cycle: string cycle: string
@Field(() => String, { nullable: true }) @Field(() => String, { nullable: true })
@isValidDateString()
validFrom?: string | null validFrom?: string | null
@Field(() => String, { nullable: true }) @Field(() => String, { nullable: true })
@isValidDateString()
validTo?: string | null validTo?: string | null
@Field(() => Decimal, { nullable: true }) @Field(() => Decimal, { nullable: true })
@IsPositiveDecimal()
maxAmountPerMonth?: Decimal | null maxAmountPerMonth?: Decimal | null
@Field(() => Int) @Field(() => Int)
@IsPositive()
maxPerCycle: number maxPerCycle: number
} }

View File

@ -1,3 +1,4 @@
import { IsInt, IsString, IsEnum } from 'class-validator'
import { ArgsType, Field, Int, InputType } from 'type-graphql' import { ArgsType, Field, Int, InputType } from 'type-graphql'
import { ContributionMessageType } from '@enum/ContributionMessageType' import { ContributionMessageType } from '@enum/ContributionMessageType'
@ -6,11 +7,14 @@ import { ContributionMessageType } from '@enum/ContributionMessageType'
@ArgsType() @ArgsType()
export class ContributionMessageArgs { export class ContributionMessageArgs {
@Field(() => Int) @Field(() => Int)
@IsInt()
contributionId: number contributionId: number
@Field(() => String) @Field(() => String)
@IsString()
message: string message: string
@Field(() => ContributionMessageType, { defaultValue: ContributionMessageType.DIALOG }) @Field(() => ContributionMessageType, { defaultValue: ContributionMessageType.DIALOG })
@IsEnum(ContributionMessageType)
messageType: ContributionMessageType messageType: ContributionMessageType
} }

View File

@ -1,25 +1,33 @@
import { IsEmail, IsInt, IsString } from 'class-validator'
import { ArgsType, Field, Int } from 'type-graphql' import { ArgsType, Field, Int } from 'type-graphql'
@ArgsType() @ArgsType()
export class CreateUserArgs { export class CreateUserArgs {
@Field(() => String, { nullable: true }) @Field(() => String, { nullable: true })
@IsString()
alias?: string | null alias?: string | null
@Field(() => String) @Field(() => String)
@IsEmail()
email: string email: string
@Field(() => String) @Field(() => String)
@IsString()
firstName: string firstName: string
@Field(() => String) @Field(() => String)
@IsString()
lastName: string lastName: string
@Field(() => String, { nullable: true }) @Field(() => String, { nullable: true })
@IsString()
language?: string | null language?: string | null
@Field(() => Int, { nullable: true }) @Field(() => Int, { nullable: true })
@IsInt()
publisherId?: number | null publisherId?: number | null
@Field(() => String, { nullable: true }) @Field(() => String, { nullable: true })
@IsString()
redeemCode?: string | null redeemCode?: string | null
} }

View File

@ -1,4 +1,5 @@
/* eslint-disable type-graphql/invalid-nullable-input-type */ /* eslint-disable type-graphql/invalid-nullable-input-type */
import { IsPositive, IsEnum } from 'class-validator'
import { ArgsType, Field, Int } from 'type-graphql' import { ArgsType, Field, Int } from 'type-graphql'
import { Order } from '@enum/Order' import { Order } from '@enum/Order'
@ -6,11 +7,14 @@ import { Order } from '@enum/Order'
@ArgsType() @ArgsType()
export class Paginated { export class Paginated {
@Field(() => Int, { nullable: true }) @Field(() => Int, { nullable: true })
@IsPositive()
currentPage?: number currentPage?: number
@Field(() => Int, { nullable: true }) @Field(() => Int, { nullable: true })
@IsPositive()
pageSize?: number pageSize?: number
@Field(() => Order, { nullable: true }) @Field(() => Order, { nullable: true })
@IsEnum(Order)
order?: Order order?: Order
} }

View File

@ -1,10 +1,13 @@
import { IsBoolean } from 'class-validator'
import { Field, InputType } from 'type-graphql' import { Field, InputType } from 'type-graphql'
@InputType() @InputType()
export class SearchUsersFilters { export class SearchUsersFilters {
@Field(() => Boolean, { nullable: true, defaultValue: null }) @Field(() => Boolean, { nullable: true, defaultValue: null })
@IsBoolean()
byActivated?: boolean | null byActivated?: boolean | null
@Field(() => Boolean, { nullable: true, defaultValue: null }) @Field(() => Boolean, { nullable: true, defaultValue: null })
@IsBoolean()
byDeleted?: boolean | null byDeleted?: boolean | null
} }

View File

@ -1,3 +1,4 @@
import { IsPositive, IsEnum } from 'class-validator'
import { ArgsType, Field, Int, InputType } from 'type-graphql' import { ArgsType, Field, Int, InputType } from 'type-graphql'
import { RoleNames } from '@enum/RoleNames' import { RoleNames } from '@enum/RoleNames'
@ -6,8 +7,10 @@ import { RoleNames } from '@enum/RoleNames'
@ArgsType() @ArgsType()
export class SetUserRoleArgs { export class SetUserRoleArgs {
@Field(() => Int) @Field(() => Int)
@IsPositive()
userId: number userId: number
@Field(() => RoleNames, { nullable: true }) @Field(() => RoleNames, { nullable: true })
@IsEnum(RoleNames)
role: RoleNames | null | undefined role: RoleNames | null | undefined
} }

View File

@ -1,4 +1,4 @@
import { IsAlpha, MaxLength, MinLength } from 'class-validator' import { MaxLength, MinLength } from 'class-validator'
import { Decimal } from 'decimal.js-light' import { Decimal } from 'decimal.js-light'
import { ArgsType, Field } from 'type-graphql' import { ArgsType, Field } from 'type-graphql'

View File

@ -1,14 +1,18 @@
/* eslint-disable type-graphql/invalid-nullable-input-type */ /* eslint-disable type-graphql/invalid-nullable-input-type */
import { IsBoolean } from 'class-validator'
import { Field, InputType } from 'type-graphql' import { Field, InputType } from 'type-graphql'
@InputType() @InputType()
export class TransactionLinkFilters { export class TransactionLinkFilters {
@Field(() => Boolean, { nullable: true }) @Field(() => Boolean, { nullable: true })
@IsBoolean()
withDeleted?: boolean withDeleted?: boolean
@Field(() => Boolean, { nullable: true }) @Field(() => Boolean, { nullable: true })
@IsBoolean()
withExpired?: boolean withExpired?: boolean
@Field(() => Boolean, { nullable: true }) @Field(() => Boolean, { nullable: true })
@IsBoolean()
withRedeemed?: boolean withRedeemed?: boolean
} }

View File

@ -1,4 +1,4 @@
import { MaxLength, MinLength } from 'class-validator' import { MaxLength, MinLength, IsString } from 'class-validator'
import { Decimal } from 'decimal.js-light' import { Decimal } from 'decimal.js-light'
import { ArgsType, Field } from 'type-graphql' import { ArgsType, Field } from 'type-graphql'
@ -8,6 +8,7 @@ import { IsPositiveDecimal } from '@/graphql/validator/Decimal'
@ArgsType() @ArgsType()
export class TransactionSendArgs { export class TransactionSendArgs {
@Field(() => String) @Field(() => String)
@IsString()
identifier: string identifier: string
@Field(() => Decimal) @Field(() => Decimal)

View File

@ -1,13 +1,17 @@
import { IsEmail, IsInt, IsString } from 'class-validator'
import { ArgsType, Field, Int } from 'type-graphql' import { ArgsType, Field, Int } from 'type-graphql'
@ArgsType() @ArgsType()
export class UnsecureLoginArgs { export class UnsecureLoginArgs {
@Field(() => String) @Field(() => String)
@IsEmail()
email: string email: string
@Field(() => String) @Field(() => String)
@IsString()
password: string password: string
@Field(() => Int, { nullable: true }) @Field(() => Int, { nullable: true })
@IsInt()
publisherId?: number | null publisherId?: number | null
} }

View File

@ -1,31 +1,41 @@
import { IsBoolean, IsInt, IsString } from 'class-validator'
import { ArgsType, Field, Int } from 'type-graphql' import { ArgsType, Field, Int } from 'type-graphql'
@ArgsType() @ArgsType()
export class UpdateUserInfosArgs { export class UpdateUserInfosArgs {
@Field({ nullable: true }) @Field({ nullable: true })
@IsString()
firstName?: string firstName?: string
@Field({ nullable: true }) @Field({ nullable: true })
@IsString()
lastName?: string lastName?: string
@Field({ nullable: true }) @Field({ nullable: true })
@IsString()
alias?: string alias?: string
@Field({ nullable: true }) @Field({ nullable: true })
@IsString()
language?: string language?: string
@Field(() => Int, { nullable: true }) @Field(() => Int, { nullable: true })
@IsInt()
publisherId?: number | null publisherId?: number | null
@Field({ nullable: true }) @Field({ nullable: true })
@IsString()
password?: string password?: string
@Field({ nullable: true }) @Field({ nullable: true })
@IsString()
passwordNew?: string passwordNew?: string
@Field({ nullable: true }) @Field({ nullable: true })
@IsBoolean()
hideAmountGDD?: boolean hideAmountGDD?: boolean
@Field({ nullable: true }) @Field({ nullable: true })
@IsBoolean()
hideAmountGDT?: boolean hideAmountGDT?: boolean
} }

View File

@ -339,107 +339,142 @@ describe('Contribution Links', () => {
it('returns an error if name is shorter than 5 characters', async () => { it('returns an error if name is shorter than 5 characters', async () => {
jest.clearAllMocks() jest.clearAllMocks()
await expect( const { errors: errorObjects } = await mutate({
mutate({ mutation: createContributionLink,
mutation: createContributionLink, variables: {
variables: { ...variables,
...variables, name: '123',
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 () => { it('returns an error if name is longer than 100 characters', async () => {
jest.clearAllMocks() jest.clearAllMocks()
await expect( const { errors: errorObjects } = await mutate({
mutate({ mutation: createContributionLink,
mutation: createContributionLink, variables: {
variables: { ...variables,
...variables, name: '12345678901234567892123456789312345678941234567895123456789612345678971234567898123456789912345678901',
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 () => { it('returns an error if memo is shorter than 5 characters', async () => {
jest.clearAllMocks() jest.clearAllMocks()
await expect( const { errors: errorObjects } = await mutate({
mutate({ mutation: createContributionLink,
mutation: createContributionLink, variables: {
variables: { ...variables,
...variables, memo: '123',
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 () => { it('returns an error if memo is longer than 255 characters', async () => {
jest.clearAllMocks() jest.clearAllMocks()
await expect( const { errors: errorObjects } = await mutate({
mutate({ mutation: createContributionLink,
mutation: createContributionLink, variables: {
variables: { ...variables,
...variables, memo: '1234567890123456789212345678931234567894123456789512345678961234567897123456789812345678991234567890123456789012345678921234567893123456789412345678951234567896123456789712345678981234567899123456789012345678901234567892123456789312345678941234567895123456',
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 () => { it('returns an error if amount is not positive', async () => {
jest.clearAllMocks() jest.clearAllMocks()
await expect( const { errors: errorObjects } = await mutate({
mutate({ mutation: createContributionLink,
mutation: createContributionLink, variables: {
variables: { ...variables,
...variables, amount: new Decimal(0),
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))
}) })
}) })

View File

@ -201,6 +201,7 @@ describe('ContributionResolver', () => {
it('throws error when memo length smaller than 5 chars', async () => { it('throws error when memo length smaller than 5 chars', async () => {
jest.clearAllMocks() jest.clearAllMocks()
const date = new Date() const date = new Date()
const { errors: errorObjects } = await mutate({ const { errors: errorObjects } = await mutate({
mutation: createContribution, mutation: createContribution,
variables: { variables: {
@ -209,12 +210,23 @@ describe('ContributionResolver', () => {
creationDate: date.toString(), creationDate: date.toString(),
}, },
}) })
expect(errorObjects).toMatchObject([
expect(errorObjects).toEqual([new GraphQLError('Memo text is too short')]) {
}) message: 'Argument Validation Error',
extensions: {
it('logs the error "Memo text is too short"', () => { exception: {
expect(logger.error).toBeCalledWith('Memo text is too short', 4) 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 () => { it('throws error when memo length greater than 255 chars', async () => {
@ -228,11 +240,23 @@ describe('ContributionResolver', () => {
creationDate: date.toString(), creationDate: date.toString(),
}, },
}) })
expect(errorObjects).toEqual([new GraphQLError('Memo text is too long')]) expect(errorObjects).toMatchObject([
}) {
message: 'Argument Validation Error',
it('logs the error "Memo text is too long"', () => { extensions: {
expect(logger.error).toBeCalledWith('Memo text is too long', 259) exception: {
validationErrors: [
{
property: 'memo',
constraints: {
maxLength: 'memo must be shorter than or equal to 255 characters',
},
},
],
},
},
},
])
}) })
it('throws error when creationDate not-valid', async () => { it('throws error when creationDate not-valid', async () => {
@ -245,27 +269,35 @@ describe('ContributionResolver', () => {
creationDate: 'not-valid', creationDate: 'not-valid',
}, },
}) })
expect(errorObjects).toEqual([ expect(errorObjects).toMatchObject([
new GraphQLError('No information for available creations for the given date'), {
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 () => { it('throws error when creationDate 3 month behind', async () => {
jest.clearAllMocks() jest.clearAllMocks()
const date = new Date() const date = new Date()
date.setMonth(date.getMonth() - 3)
const { errors: errorObjects } = await mutate({ const { errors: errorObjects } = await mutate({
mutation: createContribution, mutation: createContribution,
variables: { variables: {
amount: 100.0, amount: 100.0,
memo: 'Test env contribution', memo: 'Test env contribution',
creationDate: date.setMonth(date.getMonth() - 3).toString(), creationDate: date.toString(),
}, },
}) })
expect(errorObjects).toEqual([ expect(errorObjects).toEqual([
@ -346,11 +378,23 @@ describe('ContributionResolver', () => {
creationDate: date.toString(), creationDate: date.toString(),
}, },
}) })
expect(errorObjects).toEqual([new GraphQLError('Memo text is too short')]) expect(errorObjects).toMatchObject([
}) {
message: 'Argument Validation Error',
it('logs the error "Memo text is too short"', () => { extensions: {
expect(logger.error).toBeCalledWith('Memo text is too short', 4) 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(), creationDate: date.toString(),
}, },
}) })
expect(errorObjects).toEqual([new GraphQLError('Memo text is too long')]) expect(errorObjects).toMatchObject([
}) {
message: 'Argument Validation Error',
it('logs the error "Memo text is too long"', () => { extensions: {
expect(logger.error).toBeCalledWith('Memo text is too long', 259) 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 () => { it('throws an error', async () => {
jest.clearAllMocks() jest.clearAllMocks()
const date = new Date() const date = new Date()
date.setMonth(date.getMonth() - 3)
const { errors: errorObjects } = await mutate({ const { errors: errorObjects } = await mutate({
mutation: updateContribution, mutation: updateContribution,
variables: { variables: {
contributionId: pendingContribution.data.createContribution.id, contributionId: pendingContribution.data.createContribution.id,
amount: 10.0, amount: 10.0,
memo: 'Test env contribution', memo: 'Test env contribution',
creationDate: date.setMonth(date.getMonth() - 3).toString(), creationDate: date.toString(),
}, },
}) })
expect(errorObjects).toEqual([ expect(errorObjects).toEqual([
@ -1979,17 +2036,28 @@ describe('ContributionResolver', () => {
describe('date of creation is not a date string', () => { describe('date of creation is not a date string', () => {
it('throws an error', async () => { it('throws an error', async () => {
jest.clearAllMocks() jest.clearAllMocks()
await expect( const { errors: errorObjects } = await mutate({
mutate({ mutation: adminCreateContribution, variables }), mutation: adminCreateContribution,
).resolves.toEqual( variables,
expect.objectContaining({ })
errors: [new GraphQLError('CreationDate is invalid')], expect(errorObjects).toMatchObject([
}), {
) message: 'Argument Validation Error',
}) extensions: {
exception: {
it('logs the error "CreationDate is invalid"', () => { validationErrors: [
expect(logger.error).toBeCalledWith('CreationDate is invalid', 'invalid-date') {
property: 'creationDate',
constraints: {
isValidDateString:
'creationDate must be a valid date string, creationDate',
},
},
],
},
},
},
])
}) })
}) })
@ -2181,7 +2249,7 @@ describe('ContributionResolver', () => {
mutate({ mutate({
mutation: adminUpdateContribution, mutation: adminUpdateContribution,
variables: { variables: {
id: -1, id: 728,
amount: new Decimal(300), amount: new Decimal(300),
memo: 'Danke Bibi!', memo: 'Danke Bibi!',
creationDate: contributionDateFormatter(new Date()), creationDate: contributionDateFormatter(new Date()),
@ -2195,7 +2263,7 @@ describe('ContributionResolver', () => {
}) })
it('logs the error "Contribution not found"', () => { it('logs the error "Contribution not found"', () => {
expect(logger.error).toBeCalledWith('Contribution not found', -1) expect(logger.error).toBeCalledWith('Contribution not found', 728)
}) })
}) })

View File

@ -49,7 +49,6 @@ import {
getUserCreation, getUserCreation,
validateContribution, validateContribution,
updateCreations, updateCreations,
isValidDateString,
getOpenCreations, getOpenCreations,
} from './util/creations' } from './util/creations'
import { findContributions } from './util/findContributions' import { findContributions } from './util/findContributions'
@ -256,9 +255,6 @@ export class ContributionResolver {
`adminCreateContribution(email=${email}, amount=${amount.toString()}, memo=${memo}, creationDate=${creationDate})`, `adminCreateContribution(email=${email}, amount=${amount.toString()}, memo=${memo}, creationDate=${creationDate})`,
) )
const clientTimezoneOffset = getClientTimezoneOffset(context) const clientTimezoneOffset = getClientTimezoneOffset(context)
if (!isValidDateString(creationDate)) {
throw new LogError('CreationDate is invalid', creationDate)
}
const emailContact = await UserContact.findOne({ const emailContact = await UserContact.findOne({
where: { email }, where: { email },
withDeleted: true, withDeleted: true,

View File

@ -94,38 +94,58 @@ describe('TransactionLinkResolver', () => {
it('throws error when amount is zero', async () => { it('throws error when amount is zero', async () => {
jest.clearAllMocks() jest.clearAllMocks()
await expect( const { errors: errorObjects } = await mutate({
mutate({ mutation: createTransactionLink,
mutation: createTransactionLink, variables: {
variables: { amount: 0,
amount: 0, memo: 'Test Test',
memo: 'Test', },
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Amount must be a positive number')],
}) })
}) expect(errorObjects).toMatchObject([
it('logs the error "Amount must be a positive number" - 0', () => { {
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(0)) 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 () => { it('throws error when amount is negative', async () => {
jest.clearAllMocks() jest.clearAllMocks()
await expect( const { errors: errorObjects } = await mutate({
mutate({ mutation: createTransactionLink,
mutation: createTransactionLink, variables: {
variables: { amount: -10,
amount: -10, memo: 'Test Test',
memo: 'Test', },
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Amount must be a positive number')],
}) })
}) expect(errorObjects).toMatchObject([
it('logs the error "Amount must be a positive number" - -10', () => { {
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(-10)) 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 () => { it('throws error when user has not enough GDD', async () => {
@ -135,7 +155,7 @@ describe('TransactionLinkResolver', () => {
mutation: createTransactionLink, mutation: createTransactionLink,
variables: { variables: {
amount: 1001, amount: 1001,
memo: 'Test', memo: 'Test Test',
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({

View File

@ -93,7 +93,7 @@ describe('send coins', () => {
variables: { variables: {
identifier: 'wrong@email.com', identifier: 'wrong@email.com',
amount: 100, amount: 100,
memo: 'test', memo: 'test test',
}, },
}), }),
).toEqual( ).toEqual(
@ -121,7 +121,7 @@ describe('send coins', () => {
variables: { variables: {
identifier: 'stephen@hawking.uk', identifier: 'stephen@hawking.uk',
amount: 100, amount: 100,
memo: 'test', memo: 'test test',
}, },
}), }),
).toEqual( ).toEqual(
@ -150,7 +150,7 @@ describe('send coins', () => {
variables: { variables: {
identifier: 'garrick@ollivander.com', identifier: 'garrick@ollivander.com',
amount: 100, amount: 100,
memo: 'test', memo: 'test test',
}, },
}), }),
).toEqual( ).toEqual(
@ -186,7 +186,7 @@ describe('send coins', () => {
variables: { variables: {
identifier: 'bob@baumeister.de', identifier: 'bob@baumeister.de',
amount: 100, amount: 100,
memo: 'test', memo: 'test test',
}, },
}), }),
).toEqual( ).toEqual(
@ -204,48 +204,62 @@ describe('send coins', () => {
describe('memo text is too short', () => { describe('memo text is too short', () => {
it('throws an error', async () => { it('throws an error', async () => {
jest.clearAllMocks() jest.clearAllMocks()
expect( const { errors: errorObjects } = await mutate({
await mutate({ mutation: sendCoins,
mutation: sendCoins, variables: {
variables: { identifier: 'peter@lustig.de',
identifier: 'peter@lustig.de', amount: 100,
amount: 100, memo: 'Test',
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', () => { describe('memo text is too long', () => {
it('throws an error', async () => { it('throws an error', async () => {
jest.clearAllMocks() jest.clearAllMocks()
expect( const { errors: errorObjects } = await mutate({
await mutate({ mutation: sendCoins,
mutation: sendCoins, variables: {
variables: { identifier: 'peter@lustig.de',
identifier: 'peter@lustig.de', amount: 100,
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',
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', () => { describe('trying to send negative amount', () => {
it('throws an error', async () => { it('throws an error', async () => {
jest.clearAllMocks() jest.clearAllMocks()
expect( const { errors: errorObjects } = await mutate({
await mutate({ mutation: sendCoins,
mutation: sendCoins, variables: {
variables: { identifier: 'peter@lustig.de',
identifier: 'peter@lustig.de', amount: -50,
amount: -50, memo: 'testing negative',
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))
}) })
}) })

View File

@ -1,12 +1,9 @@
import path from 'path' import path from 'path'
import { validate } from 'class-validator'
import { Decimal } from 'decimal.js-light' import { Decimal } from 'decimal.js-light'
import { GraphQLSchema } from 'graphql' import { GraphQLSchema } from 'graphql'
import { buildSchema } from 'type-graphql' import { buildSchema } from 'type-graphql'
import { LogError } from '@/server/LogError'
import { isAuthorized } from './directive/isAuthorized' import { isAuthorized } from './directive/isAuthorized'
import { DecimalScalar } from './scalar/Decimal' import { DecimalScalar } from './scalar/Decimal'
@ -15,20 +12,13 @@ export const schema = async (): Promise<GraphQLSchema> => {
resolvers: [path.join(__dirname, 'resolver', `!(*.test).{js,ts}`)], resolvers: [path.join(__dirname, 'resolver', `!(*.test).{js,ts}`)],
authChecker: isAuthorized, authChecker: isAuthorized,
scalarsMap: [{ type: Decimal, scalar: DecimalScalar }], scalarsMap: [{ type: Decimal, scalar: DecimalScalar }],
validate: (argValue) => { validate: {
if (argValue) { validationError: { target: false },
validate(argValue) skipMissingProperties: true,
.then((errors) => { skipNullProperties: true,
if (errors.length > 0) { skipUndefinedProperties: true,
throw new LogError('validation failed. errors: ', errors) forbidUnknownValues: false,
} else { stopAtFirstError: false,
return true
}
})
.catch((e) => {
throw new LogError('validation throw an exception: ', e)
})
}
}, },
}) })
} }

View File

View File

@ -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}`
},
},
})
}
}