diff --git a/user/src/main/java/kpring/user/dto/response/DeleteFriendResponse.java b/user/src/main/java/kpring/user/dto/response/DeleteFriendResponse.java deleted file mode 100644 index dc7f9a83..00000000 --- a/user/src/main/java/kpring/user/dto/response/DeleteFriendResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package kpring.user.dto.response; - -public record DeleteFriendResponse() { -} diff --git a/user/src/main/kotlin/kpring/user/dto/response/DeleteFriendResponse.kt b/user/src/main/kotlin/kpring/user/dto/response/DeleteFriendResponse.kt new file mode 100644 index 00000000..6c1f1ed9 --- /dev/null +++ b/user/src/main/kotlin/kpring/user/dto/response/DeleteFriendResponse.kt @@ -0,0 +1,5 @@ +package kpring.user.dto.response + +data class DeleteFriendResponse( + val friendId: Long, +) diff --git a/user/src/main/kotlin/kpring/user/entity/User.kt b/user/src/main/kotlin/kpring/user/entity/User.kt index 02f71211..4f97a7ff 100644 --- a/user/src/main/kotlin/kpring/user/entity/User.kt +++ b/user/src/main/kotlin/kpring/user/entity/User.kt @@ -16,7 +16,12 @@ class User( @Column(nullable = false) var password: String, var file: String?, - @OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = [CascadeType.ALL]) + @OneToMany( + fetch = FetchType.LAZY, + mappedBy = "user", + cascade = [CascadeType.ALL], + orphanRemoval = true, + ) val friends: MutableSet = mutableSetOf(), // Other fields and methods... ) { @@ -40,6 +45,11 @@ class User( friends.add(friendRelation) } + fun removeFriendRelation(friendRelation: Friend) { + friends.remove(friendRelation) + friendRelation.friend.friends.removeIf { it.friend == this } + } + fun updateInfo( request: UpdateUserProfileRequest, newPassword: String?, diff --git a/user/src/main/kotlin/kpring/user/exception/UserErrorCode.kt b/user/src/main/kotlin/kpring/user/exception/UserErrorCode.kt index 8b8f15ae..a26fa30f 100644 --- a/user/src/main/kotlin/kpring/user/exception/UserErrorCode.kt +++ b/user/src/main/kotlin/kpring/user/exception/UserErrorCode.kt @@ -19,11 +19,11 @@ enum class UserErrorCode( ALREADY_FRIEND(HttpStatus.BAD_REQUEST, "4030", "이미 친구입니다."), NOT_SELF_FOLLOW(HttpStatus.BAD_REQUEST, "4031", "자기자신에게 친구요청을 보낼 수 없습니다"), - FRIENDSHIP_ALREADY_EXISTS_OR_NOT_FOUND( - HttpStatus.BAD_REQUEST, - "4032", - "해당하는 친구신청이 없거나 이미 친구입니다.", - ), + + FRIENDSHIP_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "4032", "이미 친구입니다."), + FRIENDSHIP_NOT_FOUND(HttpStatus.NOT_FOUND, "4033", "해당하는 친구신청이 없습니다."), + + FRIEND_NOT_FOUND(HttpStatus.NOT_FOUND, "4034", "해당하는 친구가 없습니다."), ; override fun message(): String = this.message diff --git a/user/src/main/kotlin/kpring/user/repository/FriendRepository.kt b/user/src/main/kotlin/kpring/user/repository/FriendRepository.kt index eadac275..66e8ceb7 100644 --- a/user/src/main/kotlin/kpring/user/repository/FriendRepository.kt +++ b/user/src/main/kotlin/kpring/user/repository/FriendRepository.kt @@ -20,4 +20,9 @@ interface FriendRepository : JpaRepository { friendId: Long, requestStatus: FriendRequestStatus, ): Friend? + + fun findByUserIdAndFriendId( + userId: Long, + friendId: Long, + ): Friend? } diff --git a/user/src/main/kotlin/kpring/user/service/FriendService.kt b/user/src/main/kotlin/kpring/user/service/FriendService.kt index 6fd10a30..6f4214e7 100644 --- a/user/src/main/kotlin/kpring/user/service/FriendService.kt +++ b/user/src/main/kotlin/kpring/user/service/FriendService.kt @@ -46,6 +46,14 @@ interface FriendService { friendId: Long, ): AddFriendResponse + /*** + * 사용자가 친구와의 관계를 끊을 때 친구 상태를 삭제하는 메서드 + * + * @param userId : 로그인한 사용자 ID. + * @param friendId : 기존에 친구였지만 친구관계를 삭제하고자 하는 사용자 ID. + * @return 전에 친구였던 사용자의 ID를 담고 있는 DeleteFriendResponse 리턴 + * @throws FRIEND_NOT_FOUND : 친구 중에 friendId 를 가진 사용자가 없을 경우 발생하는 예외 + */ fun deleteFriend( userId: Long, friendId: Long, diff --git a/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt b/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt index 896ecefb..27f16cf4 100644 --- a/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt +++ b/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt @@ -53,8 +53,8 @@ class FriendServiceImpl( userId: Long, friendId: Long, ): AddFriendResponse { - val receivedFriend = getFriendshipWithStatus(userId, friendId, FriendRequestStatus.RECEIVED) - val requestedFriend = getFriendshipWithStatus(friendId, userId, FriendRequestStatus.REQUESTED) + val receivedFriend = getPendingFriendship(userId, friendId) + val requestedFriend = getPendingFriendship(friendId, userId) receivedFriend.updateRequestStatus(FriendRequestStatus.ACCEPTED) requestedFriend.updateRequestStatus(FriendRequestStatus.ACCEPTED) @@ -66,7 +66,13 @@ class FriendServiceImpl( userId: Long, friendId: Long, ): DeleteFriendResponse { - TODO("Not yet implemented") + val user = userServiceImpl.getUser(userId) + userServiceImpl.getUser(friendId) + + val userFriendRelation = findAcceptedFriendship(userId, friendId) + user.removeFriendRelation(userFriendRelation) + + return DeleteFriendResponse(friendId) } fun checkSelfFriend( @@ -87,13 +93,37 @@ class FriendServiceImpl( } } - private fun getFriendshipWithStatus( + private fun findFriendship( + userId: Long, + friendId: Long, + ): Friend { + val friendship = + friendRepository.findByUserIdAndFriendId(userId, friendId) + ?: throw ServiceException(UserErrorCode.FRIENDSHIP_NOT_FOUND) + return friendship + } + + private fun checkNotAcceptedFriendship(friendship: Friend) { + if (friendship.requestStatus == FriendRequestStatus.ACCEPTED) { + throw ServiceException(UserErrorCode.FRIENDSHIP_ALREADY_EXISTS) + } + } + + private fun getPendingFriendship( + userId: Long, + friendId: Long, + ): Friend { + val friendship = findFriendship(userId, friendId) + checkNotAcceptedFriendship(friendship) + return friendship + } + + private fun findAcceptedFriendship( userId: Long, friendId: Long, - requestStatus: FriendRequestStatus, ): Friend { return friendRepository - .findByUserIdAndFriendIdAndRequestStatus(userId, friendId, requestStatus) - ?: throw ServiceException(UserErrorCode.FRIENDSHIP_ALREADY_EXISTS_OR_NOT_FOUND) + .findByUserIdAndFriendIdAndRequestStatus(userId, friendId, FriendRequestStatus.ACCEPTED) + ?: throw ServiceException(UserErrorCode.FRIEND_NOT_FOUND) } } diff --git a/user/src/test/kotlin/kpring/user/controller/FriendControllerTest.kt b/user/src/test/kotlin/kpring/user/controller/FriendControllerTest.kt index 53867dc6..84e07818 100644 --- a/user/src/test/kotlin/kpring/user/controller/FriendControllerTest.kt +++ b/user/src/test/kotlin/kpring/user/controller/FriendControllerTest.kt @@ -13,10 +13,7 @@ import kpring.core.global.exception.ServiceException import kpring.test.restdoc.dsl.restDoc import kpring.test.restdoc.json.JsonDataType import kpring.test.restdoc.json.JsonDataType.Strings -import kpring.user.dto.response.AddFriendResponse -import kpring.user.dto.response.FailMessageResponse -import kpring.user.dto.response.GetFriendRequestResponse -import kpring.user.dto.response.GetFriendRequestsResponse +import kpring.user.dto.response.* import kpring.user.exception.UserErrorCode import kpring.user.global.AuthValidator import kpring.user.global.CommonTest @@ -492,5 +489,154 @@ internal class FriendControllerTest( } } } + + describe("친구삭제 API") { + it("친구삭제 성공") { + // given + val data = DeleteFriendResponse(friendId = CommonTest.TEST_FRIEND_ID) + + val response = ApiResponse(data = data) + every { authClient.getTokenInfo(any()) }.returns( + ApiResponse(data = TokenInfo(TokenType.ACCESS, CommonTest.TEST_USER_ID.toString())), + ) + every { authValidator.checkIfAccessTokenAndGetUserId(any()) } returns CommonTest.TEST_USER_ID.toString() + every { authValidator.checkIfUserIsSelf(any(), any()) } returns Unit + every { + friendService.deleteFriend( + CommonTest.TEST_USER_ID, + CommonTest.TEST_FRIEND_ID, + ) + } returns data + + // when + val result = + webTestClient.delete() + .uri( + "/api/v1/user/{userId}/friend/{friendId}", + CommonTest.TEST_USER_ID, + CommonTest.TEST_FRIEND_ID, + ) + .header("Authorization", CommonTest.TEST_TOKEN) + .exchange() + + // then + val docsRoot = + result + .expectStatus().isOk + .expectBody().json(objectMapper.writeValueAsString(response)) + + // docs + docsRoot + .restDoc( + identifier = "deleteFriend200", + description = "친구삭제 API", + ) { + request { + path { + "userId" mean "사용자 아이디" + "friendId" mean "친구신청을 받은 사용자의 아이디" + } + header { + "Authorization" mean "Bearer token" + } + } + response { + body { + "data.friendId" type Strings mean "친구신청을 받은 사용자의 아이디" + } + } + } + } + it("친구삭제 실패 : 권한이 없는 토큰") { + // given + val response = + FailMessageResponse.builder().message(UserErrorCode.NOT_ALLOWED.message()).build() + every { authClient.getTokenInfo(any()) } throws ServiceException(UserErrorCode.NOT_ALLOWED) + + // when + val result = + webTestClient.delete() + .uri( + "/api/v1/user/{userId}/friend/{friendId}", + CommonTest.TEST_USER_ID, + CommonTest.TEST_FRIEND_ID, + ) + .header("Authorization", CommonTest.TEST_TOKEN) + .exchange() + + // then + val docsRoot = + result + .expectStatus().isForbidden + .expectBody().json(objectMapper.writeValueAsString(response)) + + // docs + docsRoot + .restDoc( + identifier = "deleteFriend403", + description = "친구삭제 API", + ) { + request { + path { + "userId" mean "사용자 아이디" + "friendId" mean "친구신청을 받은 사용자의 아이디" + } + header { + "Authorization" mean "Bearer token" + } + } + response { + body { + "message" type Strings mean "에러 메시지" + } + } + } + } + it("친구삭제 실패 : 서버 내부 오류") { + // given + val response = + FailMessageResponse.serverError + every { authClient.getTokenInfo(any()) } throws RuntimeException("서버 내부 오류") + + // when + val result = + webTestClient.delete() + .uri( + "/api/v1/user/{userId}/friend/{friendId}", + CommonTest.TEST_USER_ID, + CommonTest.TEST_FRIEND_ID, + ) + .header("Authorization", CommonTest.TEST_TOKEN) + .exchange() + + // then + val docsRoot = + result + .expectStatus().isEqualTo(500) + .expectBody().json(objectMapper.writeValueAsString(response)) + + // docs + docsRoot + .restDoc( + identifier = "deleteFriend500", + description = "친구삭제 API", + ) { + request { + path { + "userId" mean "사용자 아이디" + "friendId" mean "친구신청을 받은 사용자의 아이디" + } + header { + "Authorization" mean "Bearer token" + } + } + response { + body { + "message" type Strings mean "에러 메시지" + } + } + } + } + } } }) diff --git a/user/src/test/kotlin/kpring/user/service/FriendServiceImplTest.kt b/user/src/test/kotlin/kpring/user/service/FriendServiceImplTest.kt index 4b174825..cf195e4f 100644 --- a/user/src/test/kotlin/kpring/user/service/FriendServiceImplTest.kt +++ b/user/src/test/kotlin/kpring/user/service/FriendServiceImplTest.kt @@ -34,8 +34,8 @@ internal class FriendServiceImplTest : FunSpec({ ) test("친구신청_성공") { - val user = mockk(relaxed = true) - val friend = mockk(relaxed = true) + val user = mockk { every { id } returns CommonTest.TEST_USER_ID } + val friend = mockk { every { id } returns CommonTest.TEST_FRIEND_ID } every { userService.getUser(CommonTest.TEST_USER_ID) } returns user every { userService.getUser(CommonTest.TEST_FRIEND_ID) } returns friend @@ -47,7 +47,7 @@ internal class FriendServiceImplTest : FunSpec({ every { friend.receiveFriendRequest(user) } just Runs val response = friendService.addFriend(CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID) - response.friendId shouldBe friend.id + response.friendId shouldBe CommonTest.TEST_FRIEND_ID verify { friendService.checkSelfFriend(user, friend) } verify { @@ -56,7 +56,7 @@ internal class FriendServiceImplTest : FunSpec({ } test("친구신청_실패_자기자신을 친구로 추가하는 케이스") { - val user = mockk(relaxed = true) + val user = mockk() every { userService.getUser(CommonTest.TEST_USER_ID) } returns user every { @@ -77,8 +77,8 @@ internal class FriendServiceImplTest : FunSpec({ } test("친구신청_실패_이미 친구인 케이스") { - val user = mockk(relaxed = true) - val friend = mockk(relaxed = true) + val user = mockk() + val friend = mockk() every { userService.getUser(CommonTest.TEST_USER_ID) } returns user every { userService.getUser(CommonTest.TEST_FRIEND_ID) } returns friend @@ -103,9 +103,17 @@ internal class FriendServiceImplTest : FunSpec({ } test("친구신청조회_성공") { - val friend = mockk(relaxed = true) - val friendList = listOf(mockk(relaxed = true)) + val friendInfo = + mockk { + every { id } returns CommonTest.TEST_FRIEND_ID + every { username } returns CommonTest.TEST_FRIEND_USERNAME + } + val friend = + mockk { + every { friend } returns friendInfo + } + val friendList = listOf(friend) every { friendRepository.findAllByUserIdAndRequestStatus( CommonTest.TEST_USER_ID, @@ -114,28 +122,33 @@ internal class FriendServiceImplTest : FunSpec({ } returns friendList val response = friendService.getFriendRequests(CommonTest.TEST_USER_ID) + response.userId shouldBe CommonTest.TEST_USER_ID for (request in response.friendRequests) { - request.friendId shouldBe friend.id - request.username shouldBe friend.username + request.friendId shouldBe CommonTest.TEST_FRIEND_ID + request.username shouldBe CommonTest.TEST_FRIEND_USERNAME } } test("친구신청수락_성공") { - val receivedFriend = mockk(relaxed = true) - val requestedFriend = mockk(relaxed = true) + val receivedFriend = + mockk { + every { requestStatus } returns FriendRequestStatus.RECEIVED + } + val requestedFriend = + mockk { + every { requestStatus } returns FriendRequestStatus.REQUESTED + } every { - friendRepository.findByUserIdAndFriendIdAndRequestStatus( + friendRepository.findByUserIdAndFriendId( CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID, - FriendRequestStatus.RECEIVED, ) } returns receivedFriend every { - friendRepository.findByUserIdAndFriendIdAndRequestStatus( + friendRepository.findByUserIdAndFriendId( CommonTest.TEST_FRIEND_ID, CommonTest.TEST_USER_ID, - FriendRequestStatus.REQUESTED, ) } returns requestedFriend @@ -147,47 +160,88 @@ internal class FriendServiceImplTest : FunSpec({ response.friendId shouldBe CommonTest.TEST_FRIEND_ID verify(exactly = 2) { - friendRepository.findByUserIdAndFriendIdAndRequestStatus(any(), any(), any()) + friendRepository.findByUserIdAndFriendId(any(), any()) } } test("친구신청수락_실패_해당하는 친구신청이 없는 케이스") { every { - friendRepository.findByUserIdAndFriendIdAndRequestStatus( + friendRepository.findByUserIdAndFriendId( CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID, - FriendRequestStatus.RECEIVED, ) - } throws ServiceException(UserErrorCode.FRIENDSHIP_ALREADY_EXISTS_OR_NOT_FOUND) + } throws ServiceException(UserErrorCode.FRIENDSHIP_NOT_FOUND) val exception = shouldThrow { friendService.acceptFriendRequest(CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID) } - exception.errorCode.message() shouldBe "해당하는 친구신청이 없거나 이미 친구입니다." + exception.errorCode.message() shouldBe "해당하는 친구신청이 없습니다." } test("친구신청수락_실패_이미 친구인 케이스") { every { - friendRepository.findByUserIdAndFriendIdAndRequestStatus( + friendRepository.findByUserIdAndFriendId( CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID, - FriendRequestStatus.RECEIVED, ) - } throws ServiceException(UserErrorCode.FRIENDSHIP_ALREADY_EXISTS_OR_NOT_FOUND) + } throws ServiceException(UserErrorCode.FRIENDSHIP_ALREADY_EXISTS) every { - friendRepository.findByUserIdAndFriendIdAndRequestStatus( + friendRepository.findByUserIdAndFriendId( CommonTest.TEST_FRIEND_ID, CommonTest.TEST_USER_ID, - FriendRequestStatus.REQUESTED, ) - } throws ServiceException(UserErrorCode.FRIENDSHIP_ALREADY_EXISTS_OR_NOT_FOUND) + } throws ServiceException(UserErrorCode.FRIENDSHIP_ALREADY_EXISTS) val exception = shouldThrow { friendService.acceptFriendRequest(CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID) } - exception.errorCode.message() shouldBe "해당하는 친구신청이 없거나 이미 친구입니다." + exception.errorCode.message() shouldBe "이미 친구입니다." + } + + test("친구삭제_성공") { + val user = mockk() + val friend = mockk() + val userFriendRelation = mockk() + + every { userService.getUser(CommonTest.TEST_USER_ID) } returns user + every { userService.getUser(CommonTest.TEST_FRIEND_ID) } returns friend + + every { + friendRepository.findByUserIdAndFriendIdAndRequestStatus( + CommonTest.TEST_USER_ID, + CommonTest.TEST_FRIEND_ID, + FriendRequestStatus.ACCEPTED, + ) + } returns userFriendRelation + + every { user.removeFriendRelation(userFriendRelation) } just Runs + + val response = + friendService.deleteFriend(CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID) + response.friendId shouldBe CommonTest.TEST_FRIEND_ID + } + + test("친구삭제_실패_해당하는 친구가 없는 케이스") { + val user = mockk() + val friend = mockk() + + every { userService.getUser(CommonTest.TEST_USER_ID) } returns user + every { userService.getUser(CommonTest.TEST_FRIEND_ID) } returns friend + every { + friendRepository.findByUserIdAndFriendIdAndRequestStatus( + CommonTest.TEST_USER_ID, + CommonTest.TEST_FRIEND_ID, + FriendRequestStatus.ACCEPTED, + ) + } throws ServiceException(UserErrorCode.FRIEND_NOT_FOUND) + + val exception = + shouldThrow { + friendService.deleteFriend(CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID) + } + exception.errorCode.message() shouldBe "해당하는 친구가 없습니다." } })