Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
})