From b80404bf2501acad210a40d146df1b45700a41dc Mon Sep 17 00:00:00 2001 From: minisun Date: Sat, 8 Jun 2024 23:42:34 +0900 Subject: [PATCH 01/14] =?UTF-8?q?refac:=20=EC=A3=BC=EC=9E=85=EB=90=98?= =?UTF-8?q?=EB=8A=94=20Bean=EB=93=A4=20private=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/kotlin/kpring/chat/chat/api/v1/ChatController.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chat/api/v1/ChatController.kt b/chat/src/main/kotlin/kpring/chat/chat/api/v1/ChatController.kt index 770a971a..e1d5f408 100644 --- a/chat/src/main/kotlin/kpring/chat/chat/api/v1/ChatController.kt +++ b/chat/src/main/kotlin/kpring/chat/chat/api/v1/ChatController.kt @@ -17,8 +17,8 @@ import org.springframework.web.bind.annotation.* @RequestMapping("/api/v1") class ChatController( private val chatService: ChatService, - val authClient: AuthClient, - val serverClient: ServerClient, + private val authClient: AuthClient, + private val serverClient: ServerClient, ) { @PostMapping("/chat") fun createChat( From e151f6aed3e7785c5e5d098dc7dc369ecef072bc Mon Sep 17 00:00:00 2001 From: minisun Date: Sat, 8 Jun 2024 23:43:27 +0900 Subject: [PATCH 02/14] =?UTF-8?q?refac:=20ChatRoom=EC=9D=98=20members=20Li?= =?UTF-8?q?st=EC=97=90=EC=84=9C=20Set=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt b/chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt index c309123f..a1b36d8c 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt @@ -9,16 +9,20 @@ class ChatRoom : BaseTime() { @Id var id: String? = null - var members: MutableList = mutableListOf() + var members: MutableSet = mutableSetOf() fun getUsers(): List { - return members + return members.stream().toList() } fun addUsers(list: List) { members.addAll(list) } + fun addUser(userId: String) { + members.add(userId) + } + fun removeUser(userId: String) { members.remove(userId) } From 57ca844a70ec69ad13e538493e02375c6e8ff931 Mon Sep 17 00:00:00 2001 From: minisun Date: Sat, 8 Jun 2024 23:43:54 +0900 Subject: [PATCH 03/14] =?UTF-8?q?feat:=20inviteToChatRoomByUserId=20Contro?= =?UTF-8?q?ller=20Layer=EC=97=90=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/chatroom/api/v1/ChatRoomController.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/api/v1/ChatRoomController.kt b/chat/src/main/kotlin/kpring/chat/chatroom/api/v1/ChatRoomController.kt index 85829ec3..6e6f1097 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/api/v1/ChatRoomController.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/api/v1/ChatRoomController.kt @@ -3,6 +3,7 @@ package kpring.chat.chatroom.api.v1 import kpring.chat.chatroom.service.ChatRoomService import kpring.core.auth.client.AuthClient import kpring.core.chat.chatroom.dto.request.CreateChatRoomRequest +import kpring.core.global.dto.response.ApiResponse import org.springframework.http.ResponseEntity import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.* @@ -34,4 +35,15 @@ class ChatRoomController( val result = chatRoomService.exitChatRoom(chatRoomId, userId) return ResponseEntity.ok().body(result) } + + @PatchMapping("/chatroom/{chatRoomId}/invite/{userId}") + fun inviteToChatRoomByUserId( + @PathVariable("userId") userId: String, + @PathVariable("chatRoomId") chatRoomId: String, + @RequestHeader("Authorization") token: String, + ): ResponseEntity<*> { + val inviterId = authClient.getTokenInfo(token).data!!.userId + chatRoomService.inviteToChatRoomByUserIdWithLock(userId, inviterId, chatRoomId) + return ResponseEntity.ok().body(ApiResponse(status = 201)) + } } From 734f133470c16f540cf257ab24415a2a9045043c Mon Sep 17 00:00:00 2001 From: minisun Date: Sat, 8 Jun 2024 23:44:22 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20inviteToChatRoomByUserId=20Servic?= =?UTF-8?q?e=20Layer=EC=97=90=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/chatroom/service/ChatRoomService.kt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt index f277d55f..090b4e33 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt @@ -6,10 +6,12 @@ import kpring.chat.global.exception.ErrorCode import kpring.chat.global.exception.GlobalException import kpring.core.chat.chatroom.dto.request.CreateChatRoomRequest import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional @Service class ChatRoomService( private val chatRoomRepository: ChatRoomRepository, + private val lockService: DistributedLockService, ) { fun createChatRoom( request: CreateChatRoomRequest, @@ -30,6 +32,31 @@ class ChatRoomService( chatRoomRepository.save(chatRoom) } + @Transactional + fun inviteToChatRoomByUserIdWithLock( + userId: String, + inviterId: String, + chatRoomId: String, + ): Boolean { + val lock = lockService.getLock(chatRoomId) + + inviteToChatRoomByUserId(userId, inviterId, chatRoomId) + + lockService.releaseLock(lock.lockId, lock.owner) + return true + } + + fun inviteToChatRoomByUserId( + userId: String, + inviterId: String, + chatRoomId: String, + ) { + verifyChatRoomAccess(chatRoomId, inviterId) + val chatRoom: ChatRoom = getChatRoom(chatRoomId) + chatRoom.addUser(userId) + chatRoomRepository.save(chatRoom) + } + fun verifyChatRoomAccess( chatRoomId: String, userId: String, From e806dc07ccfc6a546ccaf9f0e85f7b80b7d00425 Mon Sep 17 00:00:00 2001 From: minisun Date: Sat, 8 Jun 2024 23:44:44 +0900 Subject: [PATCH 05/14] =?UTF-8?q?feat:=20DistributedLock=20model=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chatroom/model/DistributedLock.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 chat/src/main/kotlin/kpring/chat/chatroom/model/DistributedLock.kt diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/model/DistributedLock.kt b/chat/src/main/kotlin/kpring/chat/chatroom/model/DistributedLock.kt new file mode 100644 index 00000000..ff193cb0 --- /dev/null +++ b/chat/src/main/kotlin/kpring/chat/chatroom/model/DistributedLock.kt @@ -0,0 +1,12 @@ +package kpring.chat.chatroom.model + +import org.springframework.data.annotation.Id +import org.springframework.data.mongodb.core.mapping.Document + +@Document(collection = "distributedLocks") +data class DistributedLock( + @Id + val id: String, + val owner: String, + val expiresAt: Long, +) From 61c0001dc4e31a2393efda54ffdd95486a7679c2 Mon Sep 17 00:00:00 2001 From: minisun Date: Sat, 8 Jun 2024 23:44:55 +0900 Subject: [PATCH 06/14] =?UTF-8?q?feat:=20DistributedLockRepository=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/chatroom/repository/DistributedLockRepository.kt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 chat/src/main/kotlin/kpring/chat/chatroom/repository/DistributedLockRepository.kt diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/repository/DistributedLockRepository.kt b/chat/src/main/kotlin/kpring/chat/chatroom/repository/DistributedLockRepository.kt new file mode 100644 index 00000000..5d5e7165 --- /dev/null +++ b/chat/src/main/kotlin/kpring/chat/chatroom/repository/DistributedLockRepository.kt @@ -0,0 +1,8 @@ +package kpring.chat.chatroom.repository + +import kpring.chat.chatroom.model.DistributedLock +import org.springframework.data.mongodb.repository.MongoRepository +import org.springframework.stereotype.Repository + +@Repository +interface DistributedLockRepository : MongoRepository From 53e549e1d48c7d0180df7d319861e9b92a4f604a Mon Sep 17 00:00:00 2001 From: minisun Date: Sat, 8 Jun 2024 23:45:52 +0900 Subject: [PATCH 07/14] =?UTF-8?q?feat:=20DistributedLockService=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chatroom/service/DistributedLockService.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 chat/src/main/kotlin/kpring/chat/chatroom/service/DistributedLockService.kt diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/service/DistributedLockService.kt b/chat/src/main/kotlin/kpring/chat/chatroom/service/DistributedLockService.kt new file mode 100644 index 00000000..555bda49 --- /dev/null +++ b/chat/src/main/kotlin/kpring/chat/chatroom/service/DistributedLockService.kt @@ -0,0 +1,16 @@ +package kpring.chat.chatroom.service + +import kpring.chat.chatroom.dto.Lock +import kpring.chat.chatroom.model.DistributedLock +import kpring.chat.chatroom.repository.DistributedLockRepository +import kpring.chat.global.exception.ErrorCode +import kpring.chat.global.exception.GlobalException +import org.springframework.stereotype.Service +import java.util.* + +@Service +class DistributedLockService( + private val lockRepository: DistributedLockRepository, +) { + +} From 8e12a1ad541621009a2a5740b4fa43a235d67307 Mon Sep 17 00:00:00 2001 From: minisun Date: Sat, 8 Jun 2024 23:47:04 +0900 Subject: [PATCH 08/14] =?UTF-8?q?feat:=20DistributedLockService=EC=97=90?= =?UTF-8?q?=20acquireLock=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/DistributedLockService.kt | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/service/DistributedLockService.kt b/chat/src/main/kotlin/kpring/chat/chatroom/service/DistributedLockService.kt index 555bda49..3dce2f5f 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/service/DistributedLockService.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/service/DistributedLockService.kt @@ -1,10 +1,7 @@ package kpring.chat.chatroom.service -import kpring.chat.chatroom.dto.Lock import kpring.chat.chatroom.model.DistributedLock import kpring.chat.chatroom.repository.DistributedLockRepository -import kpring.chat.global.exception.ErrorCode -import kpring.chat.global.exception.GlobalException import org.springframework.stereotype.Service import java.util.* @@ -12,5 +9,26 @@ import java.util.* class DistributedLockService( private val lockRepository: DistributedLockRepository, ) { + fun acquireLock( + lockId: String, + owner: String, + expireInMillis: Long, + ): Boolean { + val now = System.currentTimeMillis() + val expiresAt = now + expireInMillis + val optionalLock = lockRepository.findById(lockId) + return if (optionalLock.isPresent) { + val lock = optionalLock.get() + if (lock.expiresAt < now) { + lockRepository.save(DistributedLock(lockId, owner, expiresAt)) + true + } else { + false + } + } else { + lockRepository.save(DistributedLock(lockId, owner, expiresAt)) + true + } + } } From 7b8bed96b200d30790a92048fc2f46820a6baad4 Mon Sep 17 00:00:00 2001 From: minisun Date: Sat, 8 Jun 2024 23:47:33 +0900 Subject: [PATCH 09/14] =?UTF-8?q?feat:=20DistributedLockService=EC=97=90?= =?UTF-8?q?=20releaseLock=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/chatroom/service/DistributedLockService.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/service/DistributedLockService.kt b/chat/src/main/kotlin/kpring/chat/chatroom/service/DistributedLockService.kt index 3dce2f5f..e0c4633b 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/service/DistributedLockService.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/service/DistributedLockService.kt @@ -31,4 +31,14 @@ class DistributedLockService( true } } + + fun releaseLock( + lockId: String, + owner: String, + ) { + val optionalLock = lockRepository.findById(lockId) + if (optionalLock.isPresent && optionalLock.get().owner == owner) { + lockRepository.deleteById(lockId) + } + } } From f3e85a36fd639e8297e7c57e43b5bb1d98ba14c0 Mon Sep 17 00:00:00 2001 From: minisun Date: Sat, 8 Jun 2024 23:48:06 +0900 Subject: [PATCH 10/14] =?UTF-8?q?feat:=20DistributedLockService=EC=97=90?= =?UTF-8?q?=20getLock=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chatroom/service/DistributedLockService.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/service/DistributedLockService.kt b/chat/src/main/kotlin/kpring/chat/chatroom/service/DistributedLockService.kt index e0c4633b..606b94ba 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/service/DistributedLockService.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/service/DistributedLockService.kt @@ -1,7 +1,10 @@ package kpring.chat.chatroom.service +import kpring.chat.chatroom.dto.Lock import kpring.chat.chatroom.model.DistributedLock import kpring.chat.chatroom.repository.DistributedLockRepository +import kpring.chat.global.exception.ErrorCode +import kpring.chat.global.exception.GlobalException import org.springframework.stereotype.Service import java.util.* @@ -41,4 +44,15 @@ class DistributedLockService( lockRepository.deleteById(lockId) } } + + fun getLock(chatRoomId: String): Lock { + val lockId = "chatRoom:$chatRoomId:lock" + val owner = UUID.randomUUID().toString() + + val lockAcquired = acquireLock(lockId, owner, 10000) // 10초 동안 잠금 유지 + if (!lockAcquired) { + throw GlobalException(ErrorCode.CONCURRENCY_CONFLICTION) + } + return Lock(lockId = lockId, owner = owner, acquired = lockAcquired) + } } From 479006999b8718afb5af2635f7fb163ad1ba02cd Mon Sep 17 00:00:00 2001 From: minisun Date: Sat, 8 Jun 2024 23:48:22 +0900 Subject: [PATCH 11/14] =?UTF-8?q?feat:=20Lock=20DTO=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/main/kotlin/kpring/chat/chatroom/dto/Lock.kt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 chat/src/main/kotlin/kpring/chat/chatroom/dto/Lock.kt diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/dto/Lock.kt b/chat/src/main/kotlin/kpring/chat/chatroom/dto/Lock.kt new file mode 100644 index 00000000..2fdf1198 --- /dev/null +++ b/chat/src/main/kotlin/kpring/chat/chatroom/dto/Lock.kt @@ -0,0 +1,7 @@ +package kpring.chat.chatroom.dto + +data class Lock( + val lockId: String, + val owner: String, + val acquired: Boolean, +) From bbcd4bcb1eb8c4651e560a67dffc8dcdc9fca1d8 Mon Sep 17 00:00:00 2001 From: minisun Date: Sat, 8 Jun 2024 23:48:37 +0900 Subject: [PATCH 12/14] =?UTF-8?q?feat:=20=EB=8F=99=EC=8B=9C=EC=84=B1=20?= =?UTF-8?q?=EC=B6=A9=EB=8F=8C=20=EC=97=90=EB=9F=AC=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt b/chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt index 09798d7a..042df163 100644 --- a/chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt +++ b/chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt @@ -12,4 +12,7 @@ enum class ErrorCode(val httpStatus: Int, val message: String) { // 404 CHATROOM_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 id로 chatroom을 찾을 수 없습니다"), + + // 500 + CONCURRENCY_CONFLICTION(HttpStatus.CONFLICT.value(), "동시성 충돌이 발생했습니다."), } From ea2c2799adfab8a861c8cf754a131264728fbd7c Mon Sep 17 00:00:00 2001 From: minisun Date: Sat, 8 Jun 2024 23:50:10 +0900 Subject: [PATCH 13/14] =?UTF-8?q?feat:=20ChatRoomControllerTest=EC=97=90?= =?UTF-8?q?=20inviteToChatRoomByUserId=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chatroom/api/v1/ChatRoomControllerTest.kt | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt diff --git a/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt b/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt new file mode 100644 index 00000000..28cc701f --- /dev/null +++ b/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt @@ -0,0 +1,120 @@ +package kpring.chat.chat.api.v1 + +import com.fasterxml.jackson.databind.ObjectMapper +import com.ninjasquad.springmockk.MockkBean +import io.kotest.core.spec.style.DescribeSpec +import io.mockk.every +import io.mockk.junit5.MockKExtension +import kpring.chat.chatroom.api.v1.ChatRoomController +import kpring.chat.chatroom.service.ChatRoomService +import kpring.chat.global.ChatRoomTest +import kpring.chat.global.CommonTest +import kpring.core.auth.client.AuthClient +import kpring.core.auth.dto.response.TokenInfo +import kpring.core.auth.enums.TokenType +import kpring.core.global.dto.response.ApiResponse +import kpring.test.restdoc.dsl.restDoc +import kpring.test.restdoc.json.JsonDataType +import kpring.test.web.URLBuilder +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.restdocs.ManualRestDocumentation +import org.springframework.restdocs.RestDocumentationExtension +import org.springframework.restdocs.operation.preprocess.Preprocessors +import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.client.MockMvcWebTestClient +import org.springframework.web.context.WebApplicationContext + +@WebMvcTest(controllers = [ChatRoomController::class]) +@ExtendWith(RestDocumentationExtension::class) +@ExtendWith(SpringExtension::class) +@ExtendWith(MockKExtension::class) +class ChatRoomControllerTest( + private val om: ObjectMapper, + webContext: WebApplicationContext, + @MockkBean val chatRoomService: ChatRoomService, + @MockkBean val authClient: AuthClient, +) : DescribeSpec({ + + val restDocument = ManualRestDocumentation() + val webTestClient: WebTestClient = + MockMvcWebTestClient.bindToApplicationContext(webContext).configureClient() + .baseUrl("http://localhost:8081") + .filter( + WebTestClientRestDocumentation.documentationConfiguration(restDocument).operationPreprocessors() + .withRequestDefaults(Preprocessors.prettyPrint()).withResponseDefaults(Preprocessors.prettyPrint()), + ) + .build() + + beforeSpec { restDocument.beforeTest(this.javaClass, "chatroom controller") } + + afterSpec { restDocument.afterTest() } + + describe("PATCH /api/v1/chatroom : inviteToChatRoomByUserId api test") { + + val url = "/api/v1/chatroom/{chatRoomId}/invite/{userId}" + it("inviteToChatRoomByUserId api test") { + + // Given + val userId = CommonTest.TEST_ANOTHER_USER_ID + val inviterId = CommonTest.TEST_USER_ID + val chatRoomId = ChatRoomTest.TEST_ROOM_ID + val data = true + + every { authClient.getTokenInfo(any()) } returns + ApiResponse( + data = + TokenInfo( + type = TokenType.ACCESS, userId = CommonTest.TEST_USER_ID, + ), + ) + + every { + chatRoomService.inviteToChatRoomByUserIdWithLock( + userId, + inviterId, + chatRoomId, + ) + } returns data + + // When + val result = + webTestClient.patch().uri( + URLBuilder(url) + .build(), + chatRoomId, + userId, + ) + .header("Authorization", "Bearer mock_token") + .exchange() + + val docs = + result + .expectStatus() + .isOk + .expectBody() + .json(om.writeValueAsString(ApiResponse(status = 201))) + + // Then + docs.restDoc( + identifier = "invite_to_chatroom_by_user_id_201", + description = "채팅방에 user id로 초대하기 api", + ) { + request { + path { + "chatRoomId" mean "채팅방 id" + "userId" mean "초대받는 user id" + } + } + + response { + body { + "status" type JsonDataType.Integers mean "상태 코드" + } + } + } + } + } + }) From 31a5584f4e752dd99ce65c441bdaa099bccee768 Mon Sep 17 00:00:00 2001 From: minisun Date: Sat, 8 Jun 2024 23:50:31 +0900 Subject: [PATCH 14/14] =?UTF-8?q?feat:=20ChatRoomServiceTest=EC=97=90=20in?= =?UTF-8?q?viteToChatRoomByUserId=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/chatroom/ChatRoomServiceTest.kt | 72 ++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/chat/src/test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt b/chat/src/test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt index 49d0ba99..d118e384 100644 --- a/chat/src/test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt +++ b/chat/src/test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt @@ -1,22 +1,30 @@ package kpring.chat.chatroom +import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.collections.shouldNotContain +import io.kotest.matchers.shouldBe import io.mockk.every import io.mockk.mockk import io.mockk.verify +import kpring.chat.chatroom.dto.Lock import kpring.chat.chatroom.model.ChatRoom import kpring.chat.chatroom.repository.ChatRoomRepository import kpring.chat.chatroom.service.ChatRoomService +import kpring.chat.chatroom.service.DistributedLockService import kpring.chat.global.ChatRoomTest import kpring.chat.global.CommonTest +import kpring.chat.global.exception.ErrorCode +import kpring.chat.global.exception.GlobalException import kpring.core.chat.chatroom.dto.request.CreateChatRoomRequest import java.util.* class ChatRoomServiceTest : FunSpec({ val chatRoomRepository = mockk() - val chatRoomService = ChatRoomService(chatRoomRepository) + val lockService = mockk() + val chatRoomService = ChatRoomService(chatRoomRepository, lockService) test("createChatRoom 는 새 ChatRoom을 저장해야 한다") { // Given @@ -49,4 +57,66 @@ class ChatRoomServiceTest : FunSpec({ verify { chatRoomRepository.save(chatRoom) } chatRoom.members.shouldNotContain(CommonTest.TEST_USER_ID) } + + test("inviteToChatRoomByUserIdWithLock 는 동시성 잠금을 획득한 후 사용자를 초대해야 한다") { + // Given + val chatRoom = + ChatRoom().apply { + id = ChatRoomTest.TEST_ROOM_ID + addUsers(ChatRoomTest.TEST_MEMBERS) + } + val lock = Lock("chatRoom:${chatRoom.id}:lock", UUID.randomUUID().toString(), true) + + every { lockService.getLock(any()) } returns lock + every { lockService.releaseLock(any(), any()) } returns Unit + every { chatRoomRepository.findById(chatRoom.id!!) } returns Optional.of(chatRoom) + every { chatRoomRepository.save(any()) } returns chatRoom + every { chatRoomRepository.existsByIdAndMembersContaining(any(), CommonTest.TEST_USER_ID) } returns true + + // When + chatRoomService.inviteToChatRoomByUserIdWithLock(CommonTest.TEST_USER_ID, CommonTest.TEST_USER_ID, chatRoom.id!!) + + // Then + verify { lockService.getLock(any()) } + verify { chatRoomRepository.save(chatRoom) } + verify { lockService.releaseLock(any(), any()) } + chatRoom.members.shouldContain(CommonTest.TEST_USER_ID) + } + + test("inviteToChatRoomByUserId 는 사용자를 초대해야 한다") { + // Given + val chatRoom = + ChatRoom().apply { + id = ChatRoomTest.TEST_ROOM_ID + addUsers(ChatRoomTest.TEST_MEMBERS) + } + + every { chatRoomRepository.findById(chatRoom.id!!) } returns Optional.of(chatRoom) + every { chatRoomRepository.save(any()) } returns chatRoom + every { chatRoomRepository.existsByIdAndMembersContaining(any(), CommonTest.TEST_USER_ID) } returns true + + // When + chatRoomService.inviteToChatRoomByUserId(CommonTest.TEST_USER_ID, CommonTest.TEST_USER_ID, chatRoom.id!!) + + // Then + verify { chatRoomRepository.save(chatRoom) } + chatRoom.members.shouldContain(CommonTest.TEST_USER_ID) + } + + test("inviteToChatRoomByUserIdWithLock 는 잠금 획득 실패 시 예외를 발생해야 한다") { + // Given + val chatRoomId = ChatRoomTest.TEST_ROOM_ID + val userId = CommonTest.TEST_USER_ID + + every { lockService.getLock(any()) } throws GlobalException(ErrorCode.CONCURRENCY_CONFLICTION) + + // When + val exception = + shouldThrow { + chatRoomService.inviteToChatRoomByUserIdWithLock(userId, userId, chatRoomId) + } + + // Then + exception.getErrorCode() shouldBe ErrorCode.CONCURRENCY_CONFLICTION + } })