Merge branch 'master' into 2650-feature-federation-harmonize-and-sync-modules-of-federation

This commit is contained in:
clauspeterhuebner 2023-02-14 17:11:09 +01:00 committed by GitHub
commit 18636c8018
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 406 additions and 293 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'
@ -24,6 +25,7 @@ import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
import Decimal from 'decimal.js-light' import Decimal from 'decimal.js-light'
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import { logger } from '@test/testSetup'
// mock semaphore to allow use fake timers // mock semaphore to allow use fake timers
jest.mock('@/util/TRANSACTIONS_LOCK') jest.mock('@/util/TRANSACTIONS_LOCK')
@ -50,7 +52,75 @@ 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', () => {
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('contributionLink', () => { describe('contributionLink', () => {
describe('input not valid', () => { describe('input not valid', () => {
beforeAll(async () => { beforeAll(async () => {
@ -61,6 +131,7 @@ describe('TransactionLinkResolver', () => {
}) })
it('throws error when link does not exists', async () => { it('throws error when link does not exists', async () => {
jest.clearAllMocks()
await expect( await expect(
mutate({ mutate({
mutation: redeemTransactionLink, mutation: redeemTransactionLink,
@ -69,16 +140,26 @@ describe('TransactionLinkResolver', () => {
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [ errors: [new GraphQLError('Creation from contribution link was not successful')],
new GraphQLError(
'Creation from contribution link was not successful. Error: No contribution link found to given code: CL-123456',
),
],
}) })
}) })
it('throws error when link is not valid yet', async () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'No contribution link found to given code',
'CL-123456',
)
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('No contribution link found to given code'),
)
})
const now = new Date() const now = new Date()
const validFrom = new Date(now.getFullYear() + 1, 0, 1)
it('throws error when link is not valid yet', async () => {
jest.clearAllMocks()
const { const {
data: { createContributionLink: contributionLink }, data: { createContributionLink: contributionLink },
} = await mutate({ } = await mutate({
@ -88,7 +169,7 @@ describe('TransactionLinkResolver', () => {
name: 'Daily Contribution Link', name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community', memo: 'Thank you for contribute daily to the community',
cycle: 'DAILY', cycle: 'DAILY',
validFrom: new Date(now.getFullYear() + 1, 0, 1).toISOString(), validFrom: validFrom.toISOString(),
validTo: new Date(now.getFullYear() + 1, 11, 31, 23, 59, 59, 999).toISOString(), validTo: new Date(now.getFullYear() + 1, 11, 31, 23, 59, 59, 999).toISOString(),
maxAmountPerMonth: new Decimal(200), maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1, maxPerCycle: 1,
@ -102,16 +183,21 @@ describe('TransactionLinkResolver', () => {
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [ errors: [new GraphQLError('Creation from contribution link was not successful')],
new GraphQLError(
'Creation from contribution link was not successful. Error: Contribution link not valid yet',
),
],
}) })
await resetEntity(DbContributionLink) await resetEntity(DbContributionLink)
}) })
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution link is not valid yet', validFrom)
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link is not valid yet'),
)
})
it('throws error when contributionLink cycle is invalid', async () => { it('throws error when contributionLink cycle is invalid', async () => {
jest.clearAllMocks()
const now = new Date() const now = new Date()
const { const {
data: { createContributionLink: contributionLink }, data: { createContributionLink: contributionLink },
@ -136,17 +222,22 @@ describe('TransactionLinkResolver', () => {
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [ errors: [new GraphQLError('Creation from contribution link was not successful')],
new GraphQLError(
'Creation from contribution link was not successful. Error: Contribution link has unknown cycle',
),
],
}) })
await resetEntity(DbContributionLink) await resetEntity(DbContributionLink)
}) })
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution link has unknown cycle', 'INVALID')
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link has unknown cycle'),
)
})
const validTo = new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 0)
it('throws error when link is no longer valid', async () => { it('throws error when link is no longer valid', async () => {
const now = new Date() jest.clearAllMocks()
const { const {
data: { createContributionLink: contributionLink }, data: { createContributionLink: contributionLink },
} = await mutate({ } = await mutate({
@ -157,7 +248,7 @@ describe('TransactionLinkResolver', () => {
memo: 'Thank you for contribute daily to the community', memo: 'Thank you for contribute daily to the community',
cycle: 'DAILY', cycle: 'DAILY',
validFrom: new Date(now.getFullYear() - 1, 0, 1).toISOString(), validFrom: new Date(now.getFullYear() - 1, 0, 1).toISOString(),
validTo: new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 999).toISOString(), validTo: validTo.toISOString(),
maxAmountPerMonth: new Decimal(200), maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1, maxPerCycle: 1,
}, },
@ -170,14 +261,18 @@ describe('TransactionLinkResolver', () => {
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [ errors: [new GraphQLError('Creation from contribution link was not successful')],
new GraphQLError(
'Creation from contribution link was not successful. Error: Contribution link is no longer valid',
),
],
}) })
await resetEntity(DbContributionLink) await resetEntity(DbContributionLink)
}) })
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution link is no longer valid', validTo)
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link is no longer valid'),
)
})
}) })
// TODO: have this test separated into a transactionLink and a contributionLink part // TODO: have this test separated into a transactionLink and a contributionLink part
@ -250,6 +345,7 @@ describe('TransactionLinkResolver', () => {
}) })
it('does not allow the user to redeem the contribution link', async () => { it('does not allow the user to redeem the contribution link', async () => {
jest.clearAllMocks()
await expect( await expect(
mutate({ mutate({
mutation: redeemTransactionLink, mutation: redeemTransactionLink,
@ -258,13 +354,18 @@ describe('TransactionLinkResolver', () => {
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [ errors: [new GraphQLError('Creation from contribution link was not successful')],
new GraphQLError(
'Creation from contribution link was not successful. Error: The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.',
),
],
}) })
}) })
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error(
'The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.',
),
)
})
}) })
describe('user has no pending contributions that would not allow to redeem the link', () => { describe('user has no pending contributions that would not allow to redeem the link', () => {
@ -301,6 +402,7 @@ describe('TransactionLinkResolver', () => {
}) })
it('does not allow the user to redeem the contribution link a second time on the same day', async () => { it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
jest.clearAllMocks()
await expect( await expect(
mutate({ mutate({
mutation: redeemTransactionLink, mutation: redeemTransactionLink,
@ -309,14 +411,17 @@ describe('TransactionLinkResolver', () => {
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [ errors: [new GraphQLError('Creation from contribution link was not successful')],
new GraphQLError(
'Creation from contribution link was not successful. Error: Contribution link already redeemed today',
),
],
}) })
}) })
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link already redeemed today'),
)
})
describe('after one day', () => { describe('after one day', () => {
beforeAll(async () => { beforeAll(async () => {
jest.useFakeTimers() jest.useFakeTimers()
@ -349,6 +454,7 @@ describe('TransactionLinkResolver', () => {
}) })
it('does not allow the user to redeem the contribution link a second time on the same day', async () => { it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
jest.clearAllMocks()
await expect( await expect(
mutate({ mutate({
mutation: redeemTransactionLink, mutation: redeemTransactionLink,
@ -357,11 +463,15 @@ describe('TransactionLinkResolver', () => {
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [ errors: [new GraphQLError('Creation from contribution link was not successful')],
new GraphQLError( })
'Creation from contribution link was not successful. Error: Contribution link already redeemed today', })
),
], it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link already redeemed today'),
)
}) })
}) })
}) })
@ -369,7 +479,7 @@ 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,
@ -377,8 +487,7 @@ describe('TransactionLinkResolver', () => {
pageSize: 5, pageSize: 5,
} }
// TODO: there is a test not cleaning up after itself! Fix it! afterAll(async () => {
beforeAll(async () => {
await cleanDB() await cleanDB()
resetToken() resetToken()
}) })
@ -436,9 +545,7 @@ describe('TransactionLinkResolver', () => {
variables.userId = user.id variables.userId = user.id
variables.pageSize = 25 variables.pageSize = 25
// bibi needs GDDs // bibi needs GDDs
const bibisCreation = creations.find( const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de')
(creation) => creation.email === 'bibi@bloxberg.de',
)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await creationFactory(testEnv, bibisCreation!) await creationFactory(testEnv, bibisCreation!)
// bibis transaktion links // bibis transaktion links
@ -638,7 +745,6 @@ describe('TransactionLinkResolver', () => {
}) })
}) })
}) })
})
describe('transactionLinkCode', () => { describe('transactionLinkCode', () => {
const date = new Date() const date = new Date()

View File

@ -32,6 +32,7 @@ import { getUserCreation, validateContribution } from './util/creations'
import { executeTransaction } from './TransactionResolver' import { executeTransaction } from './TransactionResolver'
import QueryLinkResult from '@union/QueryLinkResult' import QueryLinkResult from '@union/QueryLinkResult'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import LogError from '@/server/LogError'
import { getLastTransaction } from './util/getLastTransaction' import { getLastTransaction } from './util/getLastTransaction'
@ -65,12 +66,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()
@ -186,24 +191,15 @@ export class TransactionLinkResolver {
.where('contributionLink.code = :code', { code: code.replace('CL-', '') }) .where('contributionLink.code = :code', { code: code.replace('CL-', '') })
.getOne() .getOne()
if (!contributionLink) { if (!contributionLink) {
logger.error('no contribution link found to given code:', code) throw new LogError('No contribution link found to given code', code)
throw new Error(`No contribution link found to given code: ${code}`)
} }
logger.info('...contribution link found with id', contributionLink.id) logger.info('...contribution link found with id', contributionLink.id)
if (new Date(contributionLink.validFrom).getTime() > now.getTime()) { if (new Date(contributionLink.validFrom).getTime() > now.getTime()) {
logger.error( throw new LogError('Contribution link is not valid yet', contributionLink.validFrom)
'contribution link is not valid yet. Valid from: ',
contributionLink.validFrom,
)
throw new Error('Contribution link not valid yet')
} }
if (contributionLink.validTo) { if (contributionLink.validTo) {
if (new Date(contributionLink.validTo).setHours(23, 59, 59) < now.getTime()) { if (new Date(contributionLink.validTo).setHours(23, 59, 59) < now.getTime()) {
logger.error( throw new LogError('Contribution link is no longer valid', contributionLink.validTo)
'contribution link is no longer valid. Valid to: ',
contributionLink.validTo,
)
throw new Error('Contribution link is no longer valid')
} }
} }
let alreadyRedeemed: DbContribution | undefined let alreadyRedeemed: DbContribution | undefined
@ -219,11 +215,7 @@ export class TransactionLinkResolver {
}) })
.getOne() .getOne()
if (alreadyRedeemed) { if (alreadyRedeemed) {
logger.error( throw new LogError('Contribution link already redeemed', user.id)
'contribution link with rule ONCE already redeemed by user with id',
user.id,
)
throw new Error('Contribution link already redeemed')
} }
break break
} }
@ -248,17 +240,12 @@ export class TransactionLinkResolver {
) )
.getOne() .getOne()
if (alreadyRedeemed) { if (alreadyRedeemed) {
logger.error( throw new LogError('Contribution link already redeemed today', user.id)
'contribution link with rule DAILY already redeemed by user with id',
user.id,
)
throw new Error('Contribution link already redeemed today')
} }
break break
} }
default: { default: {
logger.error('contribution link has unknown cycle', contributionLink.cycle) throw new LogError('Contribution link has unknown cycle', contributionLink.cycle)
throw new Error('Contribution link has unknown cycle')
} }
} }
@ -308,8 +295,7 @@ export class TransactionLinkResolver {
logger.info('creation from contribution link commited successfuly.') logger.info('creation from contribution link commited successfuly.')
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
logger.error(`Creation from contribution link was not successful: ${e}`) throw new LogError('Creation from contribution link was not successful', e)
throw new Error(`Creation from contribution link was not successful. ${e}`)
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }

View File

@ -1,5 +1,9 @@
<template> <template>
<div class="decayinformation-startblock"> <div class="decayinformation-startblock">
<div class="my-4">
<div class="font-weight-bold pb-2">{{ $t('form.memo') }}</div>
<div>{{ memo }}</div>
</div>
<div class="mt-3 mb-3 text-center"> <div class="mt-3 mb-3 text-center">
<b>{{ $t('decay.before_startblock_transaction') }}</b> <b>{{ $t('decay.before_startblock_transaction') }}</b>
</div> </div>
@ -8,5 +12,11 @@
<script> <script>
export default { export default {
name: 'DecayInformation-StartBlock', name: 'DecayInformation-StartBlock',
props: {
memo: {
type: String,
required: true,
},
},
} }
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="decay-information-box"> <div class="decay-information-box">
<decay-information-before-startblock v-if="decay.start === null" /> <decay-information-before-startblock v-if="decay.start === null" :memo="memo" />
<decay-information-decay-startblock <decay-information-decay-startblock
v-else-if="isStartBlock" v-else-if="isStartBlock"
:amount="amount" :amount="amount"

View File

@ -179,6 +179,17 @@ export default {
}, },
}, },
computed: { computed: {
disabled() {
if (
this.form.email.length > 5 &&
parseInt(this.form.amount) <= parseInt(this.balance) &&
this.form.memo.length > 5 &&
this.form.memo.length <= 255
) {
return false
}
return true
},
isBalanceDisabled() { isBalanceDisabled() {
return this.balance <= 0 ? 'disabled' : false return this.balance <= 0 ? 'disabled' : false
}, },