separate error cases, have some basic testing for transaction links, reorder tests

This commit is contained in:
Ulf Gebhardt 2023-02-07 11:43:04 +01:00
parent f38f931418
commit 283e09ea3d
Signed by: ulfgebhardt
GPG Key ID: DA6B843E748679C9
2 changed files with 293 additions and 227 deletions

View File

@ -16,6 +16,7 @@ import {
redeemTransactionLink, redeemTransactionLink,
createContribution, createContribution,
updateContribution, updateContribution,
createTransactionLink,
} from '@/seeds/graphql/mutations' } from '@/seeds/graphql/mutations'
import { listTransactionLinksAdmin } from '@/seeds/graphql/queries' import { listTransactionLinksAdmin } from '@/seeds/graphql/queries'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
@ -51,6 +52,69 @@ afterAll(async () => {
}) })
describe('TransactionLinkResolver', () => { describe('TransactionLinkResolver', () => {
describe('createTransactionLink', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
})
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')],
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(0))
})
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')],
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(-10))
})
it('throws error when user has not enough GDD', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: createTransactionLink,
variables: {
amount: 1001,
memo: 'Test',
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('User has not enough GDD')],
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('User has not enough GDD', expect.any(Number))
})
})
describe('redeemTransactionLink', () => { describe('redeemTransactionLink', () => {
describe('contributionLink', () => { describe('contributionLink', () => {
describe('input not valid', () => { describe('input not valid', () => {
@ -408,22 +472,52 @@ describe('TransactionLinkResolver', () => {
}) })
}) })
}) })
})
describe('transaction links list', () => { describe('listTransactionLinksAdmin', () => {
const variables = { const variables = {
userId: 1, // dummy, may be replaced userId: 1, // dummy, may be replaced
filters: null, filters: null,
currentPage: 1, currentPage: 1,
pageSize: 5, pageSize: 5,
} }
// TODO: there is a test not cleaning up after itself! Fix it! // TODO: there is a test not cleaning up after itself! Fix it!
beforeAll(async () => { beforeAll(async () => {
await cleanDB() await cleanDB()
resetToken() resetToken()
})
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
query({
query: listTransactionLinksAdmin,
variables,
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
}) })
})
describe('authenticated', () => {
describe('without admin rights', () => {
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('unauthenticated', () => {
it('returns an error', async () => { it('returns an error', async () => {
await expect( await expect(
query({ query({
@ -438,22 +532,40 @@ describe('TransactionLinkResolver', () => {
}) })
}) })
describe('authenticated', () => { describe('with admin rights', () => {
describe('without admin rights', () => { beforeAll(async () => {
beforeAll(async () => { // admin 'peter@lustig.de' has to exists for 'creationFactory'
user = await userFactory(testEnv, bibiBloxberg) await userFactory(testEnv, peterLustig)
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(async () => { user = await userFactory(testEnv, bibiBloxberg)
await cleanDB() variables.userId = user.id
resetToken() variables.pageSize = 25
}) // bibi needs GDDs
const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de')
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await creationFactory(testEnv, bibisCreation!)
// bibis transaktion links
const bibisTransaktionLinks = transactionLinks.filter(
(transactionLink) => transactionLink.email === 'bibi@bloxberg.de',
)
for (let i = 0; i < bibisTransaktionLinks.length; i++) {
await transactionLinkFactory(testEnv, bibisTransaktionLinks[i])
}
it('returns an error', async () => { // admin: only now log in
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('without any filters', () => {
it('finds 6 open transaction links and no deleted or redeemed', async () => {
await expect( await expect(
query({ query({
query: listTransactionLinksAdmin, query: listTransactionLinksAdmin,
@ -461,219 +573,169 @@ describe('TransactionLinkResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')], data: {
listTransactionLinksAdmin: {
linkCount: 6,
linkList: expect.not.arrayContaining([
expect.objectContaining({
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
createdAt: expect.any(String),
}),
expect.objectContaining({
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: expect.any(String),
}),
]),
},
},
}), }),
) )
}) })
}) })
describe('with admin rights', () => { describe('all filters are null', () => {
beforeAll(async () => { it('finds 6 open transaction links and no deleted or redeemed', async () => {
// admin 'peter@lustig.de' has to exists for 'creationFactory' await expect(
await userFactory(testEnv, peterLustig) query({
query: listTransactionLinksAdmin,
user = await userFactory(testEnv, bibiBloxberg) variables: {
variables.userId = user.id ...variables,
variables.pageSize = 25 filters: {
// bibi needs GDDs withDeleted: null,
const bibisCreation = creations.find( withExpired: null,
(creation) => creation.email === 'bibi@bloxberg.de', withRedeemed: null,
},
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
listTransactionLinksAdmin: {
linkCount: 6,
linkList: expect.not.arrayContaining([
expect.objectContaining({
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
createdAt: expect.any(String),
}),
expect.objectContaining({
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: expect.any(String),
}),
]),
},
},
}),
) )
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion })
await creationFactory(testEnv, bibisCreation!) })
// bibis transaktion links
const bibisTransaktionLinks = transactionLinks.filter( describe('filter with deleted', () => {
(transactionLink) => transactionLink.email === 'bibi@bloxberg.de', it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => {
await expect(
query({
query: listTransactionLinksAdmin,
variables: {
...variables,
filters: {
withDeleted: true,
},
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
listTransactionLinksAdmin: {
linkCount: 7,
linkList: expect.arrayContaining([
expect.not.objectContaining({
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
createdAt: expect.any(String),
}),
expect.objectContaining({
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: expect.any(String),
}),
]),
},
},
}),
) )
for (let i = 0; i < bibisTransaktionLinks.length; i++) {
await transactionLinkFactory(testEnv, bibisTransaktionLinks[i])
}
// admin: only now log in
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
}) })
})
afterAll(async () => { describe('filter by expired', () => {
await cleanDB() it('finds 5 open transaction links, 1 expired, and no redeemed', async () => {
resetToken() await expect(
query({
query: listTransactionLinksAdmin,
variables: {
...variables,
filters: {
withExpired: true,
},
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
listTransactionLinksAdmin: {
linkCount: 7,
linkList: expect.arrayContaining([
expect.objectContaining({
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
createdAt: expect.any(String),
}),
expect.not.objectContaining({
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: expect.any(String),
}),
]),
},
},
}),
)
}) })
})
describe('without any filters', () => { // TODO: works not as expected, because 'redeemedAt' and 'redeemedBy' have to be added to the transaktion link factory
it('finds 6 open transaction links and no deleted or redeemed', async () => { describe.skip('filter by redeemed', () => {
await expect( it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => {
query({ await expect(
query: listTransactionLinksAdmin, query({
variables, query: listTransactionLinksAdmin,
}), variables: {
).resolves.toEqual( ...variables,
expect.objectContaining({ filters: {
data: { withDeleted: null,
listTransactionLinksAdmin: { withExpired: null,
linkCount: 6, withRedeemed: true,
linkList: expect.not.arrayContaining([
expect.objectContaining({
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
createdAt: expect.any(String),
}),
expect.objectContaining({
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: expect.any(String),
}),
]),
},
}, },
}), },
) }),
}) ).resolves.toEqual(
}) expect.objectContaining({
data: {
describe('all filters are null', () => { listTransactionLinksAdmin: {
it('finds 6 open transaction links and no deleted or redeemed', async () => { linkCount: 6,
await expect( linkList: expect.arrayContaining([
query({ expect.not.objectContaining({
query: listTransactionLinksAdmin, memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
variables: { createdAt: expect.any(String),
...variables, }),
filters: { expect.objectContaining({
withDeleted: null, memo: 'Yeah, eingelöst!',
withExpired: null, redeemedAt: expect.any(String),
withRedeemed: null, redeemedBy: expect.any(Number),
}, }),
expect.not.objectContaining({
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: expect.any(String),
}),
]),
}, },
}), },
).resolves.toEqual( }),
expect.objectContaining({ )
data: {
listTransactionLinksAdmin: {
linkCount: 6,
linkList: expect.not.arrayContaining([
expect.objectContaining({
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
createdAt: expect.any(String),
}),
expect.objectContaining({
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: expect.any(String),
}),
]),
},
},
}),
)
})
})
describe('filter with deleted', () => {
it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => {
await expect(
query({
query: listTransactionLinksAdmin,
variables: {
...variables,
filters: {
withDeleted: true,
},
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
listTransactionLinksAdmin: {
linkCount: 7,
linkList: expect.arrayContaining([
expect.not.objectContaining({
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
createdAt: expect.any(String),
}),
expect.objectContaining({
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: expect.any(String),
}),
]),
},
},
}),
)
})
})
describe('filter by expired', () => {
it('finds 5 open transaction links, 1 expired, and no redeemed', async () => {
await expect(
query({
query: listTransactionLinksAdmin,
variables: {
...variables,
filters: {
withExpired: true,
},
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
listTransactionLinksAdmin: {
linkCount: 7,
linkList: expect.arrayContaining([
expect.objectContaining({
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
createdAt: expect.any(String),
}),
expect.not.objectContaining({
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: expect.any(String),
}),
]),
},
},
}),
)
})
})
// TODO: works not as expected, because 'redeemedAt' and 'redeemedBy' have to be added to the transaktion link factory
describe.skip('filter by redeemed', () => {
it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => {
await expect(
query({
query: listTransactionLinksAdmin,
variables: {
...variables,
filters: {
withDeleted: null,
withExpired: null,
withRedeemed: true,
},
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
listTransactionLinksAdmin: {
linkCount: 6,
linkList: expect.arrayContaining([
expect.not.objectContaining({
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
createdAt: expect.any(String),
}),
expect.objectContaining({
memo: 'Yeah, eingelöst!',
redeemedAt: expect.any(String),
redeemedBy: expect.any(Number),
}),
expect.not.objectContaining({
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: expect.any(String),
}),
]),
},
},
}),
)
})
}) })
}) })
}) })

View File

@ -64,12 +64,16 @@ export class TransactionLinkResolver {
const createdDate = new Date() const createdDate = new Date()
const validUntil = transactionLinkExpireDate(createdDate) 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) const holdAvailableAmount = amount.minus(calculateDecay(amount, createdDate, validUntil).decay)
// validate amount // validate amount
const sendBalance = await calculateBalance(user.id, holdAvailableAmount.mul(-1), createdDate) const sendBalance = await calculateBalance(user.id, holdAvailableAmount.mul(-1), createdDate)
if (!sendBalance) { if (!sendBalance) {
throw new Error("user hasn't enough GDD or amount is < 0") throw new LogError('User has not enough GDD', user.id)
} }
const transactionLink = DbTransactionLink.create() const transactionLink = DbTransactionLink.create()