gradido/backend/src/graphql/resolver/semaphore.test.ts

319 lines
9.8 KiB
TypeScript

import { ApolloServerTestClient } from 'apollo-server-testing'
import { Community as DbCommunity } from 'database'
import { Decimal } from 'decimal.js-light'
import { GraphQLError } from 'graphql'
import { DataSource } from 'typeorm'
import { v4 as uuidv4 } from 'uuid'
import { cleanDB, contributionDateFormatter, testEnvironment } from '@test/helpers'
import { creationFactory, nMonthsBefore } from '@/seeds/factory/creation'
import { userFactory } from '@/seeds/factory/user'
import {
confirmContribution,
createContribution,
createContributionLink,
createTransactionLink,
login,
redeemTransactionLink,
sendCoins,
} from '@/seeds/graphql/mutations'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
import { peterLustig } from '@/seeds/users/peter-lustig'
import { CONFIG } from '@/config'
import { CONFIG as CORE_CONFIG } from 'core'
// import { TRANSACTIONS_LOCK } from 'database'
import { Mutex } from 'redis-semaphore'
import { AppDatabase } from 'database'
jest.mock('@/password/EncryptorUtils')
CONFIG.DLT_ACTIVE = false
CORE_CONFIG.EMAIL = false
let mutate: ApolloServerTestClient['mutate']
let con: DataSource
let testEnv: {
mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query']
con: DataSource
db: AppDatabase
}
beforeAll(async () => {
testEnv = await testEnvironment()
mutate = testEnv.mutate
con = testEnv.con
await cleanDB()
})
afterAll(async () => {
await cleanDB()
await con.destroy()
await testEnv.db.getRedisClient().quit()
})
type WorkData = { start: number, end: number }
async function fakeWork(workData: WorkData[], index: number) {
// const releaseLock = await TRANSACTIONS_LOCK.acquire()
// create a new mutex for every function call, like in production code
const mutex = new Mutex(testEnv.db.getRedisClient(), 'TRANSACTIONS_LOCK')
await mutex.acquire()
const startDate = new Date()
await new Promise((resolve) => setTimeout(resolve, Math.random() * 50))
const endDate = new Date()
workData[index] = { start: startDate.getTime(), end: endDate.getTime() }
// releaseLock()
await mutex.release()
}
describe('semaphore', () => {
it("didn't should run in parallel", async () => {
const workData: WorkData[] = []
const promises: Promise<void>[] = []
for(let i = 0; i < 20; i++) {
promises.push(fakeWork(workData, i))
}
await Promise.all(promises)
workData.sort((a, b) => a.start - b.start)
workData.forEach((work, index) => {
expect(work.start).toBeLessThan(work.end)
if(index < workData.length - 1) {
expect(work.start).toBeLessThan(workData[index + 1].start)
expect(work.end).toBeLessThanOrEqual(workData[index + 1].start)
}
})
})
})
describe('semaphore fullstack', () => {
let contributionLinkCode = ''
let bobsTransactionLinkCode = ''
let bibisTransactionLinkCode = ''
let bibisOpenContributionId = -1
let bobsOpenContributionId = -1
let homeCom: DbCommunity
beforeAll(async () => {
const now = new Date()
homeCom = DbCommunity.create()
homeCom.communityUuid = uuidv4()
homeCom.creationDate = new Date('2000-01-01')
homeCom.description = 'homeCom description'
homeCom.foreign = false
homeCom.name = 'homeCom name'
homeCom.privateKey = Buffer.from('homeCom privateKey')
homeCom.publicKey = Buffer.from('homeCom publicKey')
homeCom.url = 'homeCom url'
homeCom = await DbCommunity.save(homeCom)
await userFactory(testEnv, bibiBloxberg)
await userFactory(testEnv, peterLustig)
await userFactory(testEnv, bobBaumeister)
await creationFactory(testEnv, {
email: 'bibi@bloxberg.de',
amount: 1000,
memo: 'Herzlich Willkommen bei Gradido!',
contributionDate: nMonthsBefore(new Date()),
confirmed: true,
})
await creationFactory(testEnv, {
email: 'bob@baumeister.de',
amount: 1000,
memo: 'Herzlich Willkommen bei Gradido!',
contributionDate: nMonthsBefore(new Date()),
confirmed: true,
})
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
const {
data: { createContributionLink: contributionLink },
} = await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(200),
name: 'Test Contribution Link',
memo: 'Danke für deine Teilnahme an dem Test der Contribution Links',
cycle: 'ONCE',
validFrom: new Date(2022, 5, 18).toISOString(),
validTo: new Date(now.getFullYear() + 1, 7, 14).toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
contributionLinkCode = `CL-${contributionLink.code}`
await mutate({
mutation: login,
variables: { email: 'bob@baumeister.de', password: 'Aa12345_' },
})
const {
data: { createTransactionLink: bobsLink },
} = await mutate({
mutation: createTransactionLink,
variables: {
email: 'bob@baumeister.de',
amount: 20,
memo: 'Bobs Link',
},
})
const {
data: { createContribution: bobsContribution },
} = await mutate({
mutation: createContribution,
variables: {
contributionDate: contributionDateFormatter(new Date()),
amount: 200,
memo: 'Bobs Contribution',
},
})
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
const {
data: { createTransactionLink: bibisLink },
} = await mutate({
mutation: createTransactionLink,
variables: {
amount: 20,
memo: 'Bibis Link',
},
})
const {
data: { createContribution: bibisContribution },
} = await mutate({
mutation: createContribution,
variables: {
contributionDate: contributionDateFormatter(new Date()),
amount: 200,
memo: 'Bibis Contribution',
},
})
bobsTransactionLinkCode = bobsLink.code
bibisTransactionLinkCode = bibisLink.code
bibisOpenContributionId = bibisContribution.id
bobsOpenContributionId = bobsContribution.id
})
it('creates a lot of transactions without errors', async () => {
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
const bibiRedeemContributionLink = mutate({
mutation: redeemTransactionLink,
variables: { code: contributionLinkCode },
})
const redeemBobsLink = mutate({
mutation: redeemTransactionLink,
variables: { code: bobsTransactionLinkCode },
})
const bibisTransaction = mutate({
mutation: sendCoins,
variables: {
recipientCommunityIdentifier: homeCom.communityUuid,
recipientIdentifier: 'bob@baumeister.de',
amount: '50',
memo: 'Das ist für dich, Bob',
},
})
await mutate({
mutation: login,
variables: { email: 'bob@baumeister.de', password: 'Aa12345_' },
})
const bobRedeemContributionLink = mutate({
mutation: redeemTransactionLink,
variables: { code: contributionLinkCode },
})
const redeemBibisLink = mutate({
mutation: redeemTransactionLink,
variables: { code: bibisTransactionLinkCode },
})
const bobsTransaction = mutate({
mutation: sendCoins,
variables: {
recipientCommunityIdentifier: homeCom.communityUuid,
recipientIdentifier: 'bibi@bloxberg.de',
amount: '50',
memo: 'Das ist für dich, Bibi',
},
})
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
const confirmBibisContribution = mutate({
mutation: confirmContribution,
variables: { id: bibisOpenContributionId },
})
const confirmBobsContribution = mutate({
mutation: confirmContribution,
variables: { id: bobsOpenContributionId },
})
await expect(bibiRedeemContributionLink).resolves.toMatchObject({ errors: undefined })
await expect(redeemBobsLink).resolves.toMatchObject({ errors: undefined })
await expect(bibisTransaction).resolves.toMatchObject({ errors: undefined })
await expect(bobRedeemContributionLink).resolves.toMatchObject({ errors: undefined })
await expect(redeemBibisLink).resolves.toMatchObject({ errors: undefined })
await expect(bobsTransaction).resolves.toMatchObject({ errors: undefined })
await expect(confirmBibisContribution).resolves.toMatchObject({ errors: undefined })
await expect(confirmBobsContribution).resolves.toMatchObject({ errors: undefined })
})
describe('redeem transaction link twice', () => {
let myCode: string
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
const {
data: { createTransactionLink: bibisLink },
} = await mutate({
mutation: createTransactionLink,
variables: {
amount: 20,
memo: 'Bibis Link',
},
})
myCode = bibisLink.code
await mutate({
mutation: login,
variables: { email: 'bob@baumeister.de', password: 'Aa12345_' },
})
})
it('does throw error on second redeem call', async () => {
const result = await Promise.all([
mutate({
mutation: redeemTransactionLink,
variables: {
code: myCode,
},
}),
mutate({
mutation: redeemTransactionLink,
variables: {
code: myCode,
},
}),
])
expect(result).toContainEqual(
expect.objectContaining({
errors: [new GraphQLError('Transaction link already redeemed')],
}),
)
expect(result).toContainEqual(
expect.objectContaining({
errors: undefined,
}),
)
})
})
})