Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(be): implement transaction rollback in unit test #1562

Merged
merged 13 commits into from
Mar 18, 2024
Merged
67 changes: 21 additions & 46 deletions apps/backend/apps/client/src/group/group.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CACHE_MANAGER } from '@nestjs/cache-manager'
import { ConfigService } from '@nestjs/config'
import { Test, type TestingModule } from '@nestjs/testing'
import { Prisma } from '@prisma/client'
import { Prisma, PrismaClient } from '@prisma/client'
import type { Cache } from 'cache-manager'
import { expect } from 'chai'
import * as chai from 'chai'
Expand All @@ -12,21 +12,29 @@ import {
ConflictFoundException,
EntityNotExistException
} from '@libs/exception'
import { PrismaService } from '@libs/prisma'
import { PrismaService, type FlatTransactionClient } from '@libs/prisma'
import { transactionExtension } from '@libs/prisma'
import { GroupService } from './group.service'
import type { UserGroupData } from './interface/user-group-data.interface'

chai.use(chaiExclude)

describe('GroupService', () => {
let service: GroupService
let cache: Cache
let prisma: PrismaService
beforeEach(async () => {
let tx: FlatTransactionClient

const prisma = new PrismaClient().$extends(transactionExtension)

beforeEach(async function () {
// TODO: CI 테스트에서 timeout이 걸리는 문제를 우회하기 위해서 timeout을 0으로 설정 (timeout disabled)
// local에서는 timeout을 disable 하지 않아도 테스트가 정상적으로 동작함 (default setting: 2000ms)
this.timeout(0)
//transaction client
tx = await prisma.$begin()
const module: TestingModule = await Test.createTestingModule({
providers: [
GroupService,
PrismaService,
{ provide: PrismaService, useValue: tx },
ConfigService,
{
provide: CACHE_MANAGER,
Expand All @@ -39,7 +47,6 @@ describe('GroupService', () => {
}).compile()
service = module.get<GroupService>(GroupService)
cache = module.get<Cache>(CACHE_MANAGER)
prisma = module.get<PrismaService>(PrismaService)
})

it('should be defined', () => {
Expand Down Expand Up @@ -166,9 +173,8 @@ describe('GroupService', () => {
describe('joinGroupById', () => {
let groupId: number
const userId = 4

beforeEach(async () => {
const group = await prisma.group.create({
const group = await tx.group.create({
data: {
groupName: 'test',
description: 'test',
Expand All @@ -182,26 +188,7 @@ describe('GroupService', () => {
})

afterEach(async () => {
try {
await prisma.userGroup.delete({
where: {
// eslint-disable-next-line @typescript-eslint/naming-convention
userId_groupId: { userId, groupId }
}
})
} catch {
/* 삭제할 내용이 없는 경우 예외 무시 */
}

try {
await prisma.group.delete({
where: {
id: groupId
}
})
} catch {
/* 삭제할 내용 없을 경우 예외 무시 */
}
await tx.$rollback()
})

it('should return {isJoined: true} when group not set as requireApprovalBeforeJoin', async () => {
Expand All @@ -225,7 +212,7 @@ describe('GroupService', () => {
})

it('should return {isJoined: false} when group set as requireApprovalBeforeJoin', async () => {
await prisma.group.update({
await tx.group.update({
where: {
id: groupId
},
Expand All @@ -250,7 +237,7 @@ describe('GroupService', () => {
})

it('should throw ConflictFoundException when user is already group memeber', async () => {
await prisma.userGroup.create({
await tx.userGroup.create({
data: {
userId,
groupId,
Expand All @@ -270,7 +257,7 @@ describe('GroupService', () => {
{ userId, expiresAt: Date.now() + JOIN_GROUP_REQUEST_EXPIRE_TIME }
])

await prisma.group.update({
await tx.group.update({
where: {
id: groupId
},
Expand All @@ -291,9 +278,8 @@ describe('GroupService', () => {
describe('leaveGroup', () => {
const groupId = 3
const userId = 4

beforeEach(async () => {
await prisma.userGroup.createMany({
await tx.userGroup.createMany({
data: [
{
userId,
Expand All @@ -310,18 +296,7 @@ describe('GroupService', () => {
})

afterEach(async () => {
try {
await prisma.userGroup.deleteMany({
where: {
OR: [
{ AND: [{ userId }, { groupId }] },
{ AND: [{ userId: 5 }, { groupId }] }
]
}
})
} catch {
return
}
await tx.$rollback()
})

it('should return deleted userGroup when valid userId and groupId passed', async () => {
Expand Down
1 change: 1 addition & 0 deletions apps/backend/libs/prisma/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './prisma.module'
export * from './prisma.service'
export * from './transaction.extension'
71 changes: 71 additions & 0 deletions apps/backend/libs/prisma/src/transaction.extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Prisma } from '@prisma/client'
import { PrismaService } from './prisma.service'

export type FlatTransactionClient = Prisma.TransactionClient & {
$commit: () => Promise<void>
$rollback: () => Promise<void>
}

const ROLLBACK = { [Symbol.for('prisma.client.extension.rollback')]: true }

export const transactionExtension = Prisma.defineExtension({
client: {
async $begin() {
const prisma = Prisma.getExtensionContext(this)
let setTxClient: (txClient: Prisma.TransactionClient) => void
let commit: () => void
let rollback: () => void

// a promise for getting the tx inner client
const txClient = new Promise<Prisma.TransactionClient>((res) => {
setTxClient = res
})

// a promise for controlling the transaction
const txPromise = new Promise((_res, _rej) => {
commit = () => _res(undefined)
rollback = () => _rej(ROLLBACK)
})

// opening a transaction to control externally
if (
'$transaction' in prisma &&
typeof prisma.$transaction === 'function'
) {
const tx = prisma
.$transaction((txClient) => {
setTxClient(txClient as unknown as Prisma.TransactionClient)
return txPromise
})
.catch((e) => {
if (e === ROLLBACK) {
return
}
throw e
})

// return a proxy TransactionClient with `$commit` and `$rollback` methods
return new Proxy(await txClient, {
get(target, prop) {
if (prop === '$commit') {
return () => {
commit()
return tx
}
}
if (prop === '$rollback') {
return () => {
rollback()
return tx
}
}
return target[prop as keyof typeof target]
}
}) as FlatTransactionClient
}

throw new Error('Transactions are not supported by this client')
},
getPaginator: PrismaService.prototype.getPaginator
}
})
Loading