Skip to content

Commit

Permalink
Merge pull request #186 from minahYu/feat/accept-friend-request
Browse files Browse the repository at this point in the history
[User] 친구 신청 수락 API 구현
  • Loading branch information
minahYu authored Jun 19, 2024
2 parents 08d5b31 + ced3a9b commit 668d7b5
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 2 deletions.
13 changes: 13 additions & 0 deletions user/src/main/kotlin/kpring/user/controller/FriendController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ class FriendController(
return ResponseEntity.ok(ApiResponse(data = response))
}

@PatchMapping("/user/{userId}/friend/{friendId}")
fun acceptFriendRequest(
@RequestHeader("Authorization") token: String,
@PathVariable userId: Long,
@PathVariable friendId: Long,
): ResponseEntity<ApiResponse<AddFriendResponse>> {
val validationResult = authClient.getTokenInfo(token)
val validatedUserId = authValidator.checkIfAccessTokenAndGetUserId(validationResult)
authValidator.checkIfUserIsSelf(userId.toString(), validatedUserId)
val response = friendService.acceptFriendRequest(userId, friendId)
return ResponseEntity.ok(ApiResponse(data = response))
}

@DeleteMapping("/user/{userId}/friend/{friendId}")
fun deleteFriend(
@RequestHeader("Authorization") token: String,
Expand Down
6 changes: 5 additions & 1 deletion user/src/main/kotlin/kpring/user/entity/Friend.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,8 @@ class Friend(
@Enumerated(EnumType.STRING)
@Column(nullable = false)
var requestStatus: FriendRequestStatus,
)
) {
fun updateRequestStatus(requestStatus: FriendRequestStatus) {
this.requestStatus = requestStatus
}
}
5 changes: 5 additions & 0 deletions user/src/main/kotlin/kpring/user/exception/UserErrorCode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +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",
"해당하는 친구신청이 없거나 이미 친구입니다.",
),
;

override fun message(): String = this.message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,10 @@ interface FriendRepository : JpaRepository<Friend, Long> {
userId: Long,
requestStatus: FriendRequestStatus,
): List<Friend>

fun findByUserIdAndFriendIdAndRequestStatus(
userId: Long,
friendId: Long,
requestStatus: FriendRequestStatus,
): Friend?
}
31 changes: 31 additions & 0 deletions user/src/main/kotlin/kpring/user/service/FriendService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,46 @@ import kpring.user.dto.response.GetFriendRequestsResponse
import kpring.user.dto.response.GetFriendsResponse

interface FriendService {
/**
* 로그인한 사용자에게 친구신청을 한 사람들의 목록을 조회하는 메서드
*
* @param userId : 로그인한 사용자의 ID.
* @return 로그인한 사용자 ID, 해당 사용자에게 친구신청을 보낸 사람들의 리스트를 GetFriendRequestsResponse 리턴
*/
fun getFriendRequests(userId: Long): GetFriendRequestsResponse

fun getFriends(userId: Long): GetFriendsResponse

/**
* 로그인한 사용자가 friendId를 가진 사용자에게 친구 신청을 하는 메서드
*
* @param userId : 친구 요청을 하고자 하는 사용자의 ID.
* @param friendId : 친구 요청을 받는 사용자의 ID.
* @return 로그인한 사용자가 친구 신청을 보낸 사용자의 ID를 담고 있는 AddFriendResponse 리턴
* @throws NOT_SELF_FOLLOW
* : 로그인한 사용자가 스스로에게 친구신청을 시도할 때 발생하는 예외
* @throws ALREADY_FRIEND
* : 친구신청을 받은 사용자와 이미 친구일 때 발생하는 예외
*/
fun addFriend(
userId: Long,
friendId: Long,
): AddFriendResponse

/**
* 사용자가 친구 신청을 수락해, 두 사용자 간의 상태를 ACCEPTED 로 변경하는 메서드
*
* @param userId : 친구 요청을 수락하는 사용자의 ID.
* @param friendId : 친구 요청을 보낸 사용자의 ID.
* @return 새로 친구가 된 사용자의 ID를 담고 있는 AddFriendResponse 리턴
* @throws FRIENDSHIP_ALREADY_EXISTS_OR_NOT_FOUND
* : friendId가 보낸 친구 신청이 없거나, 해당 사용자와 이미 친구일 때 발생하는 예외
*/
fun acceptFriendRequest(
userId: Long,
friendId: Long,
): AddFriendResponse

fun deleteFriend(
userId: Long,
friendId: Long,
Expand Down
23 changes: 23 additions & 0 deletions user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ class FriendServiceImpl(
return AddFriendResponse(friend.id!!)
}

override fun acceptFriendRequest(
userId: Long,
friendId: Long,
): AddFriendResponse {
val receivedFriend = getFriendshipWithStatus(userId, friendId, FriendRequestStatus.RECEIVED)
val requestedFriend = getFriendshipWithStatus(friendId, userId, FriendRequestStatus.REQUESTED)

receivedFriend.updateRequestStatus(FriendRequestStatus.ACCEPTED)
requestedFriend.updateRequestStatus(FriendRequestStatus.ACCEPTED)

return AddFriendResponse(friendId)
}

override fun deleteFriend(
userId: Long,
friendId: Long,
Expand All @@ -73,4 +86,14 @@ class FriendServiceImpl(
throw ServiceException(UserErrorCode.ALREADY_FRIEND)
}
}

private fun getFriendshipWithStatus(
userId: Long,
friendId: Long,
requestStatus: FriendRequestStatus,
): Friend {
return friendRepository
.findByUserIdAndFriendIdAndRequestStatus(userId, friendId, requestStatus)
?: throw ServiceException(UserErrorCode.FRIENDSHIP_ALREADY_EXISTS_OR_NOT_FOUND)
}
}
149 changes: 149 additions & 0 deletions user/src/test/kotlin/kpring/user/controller/FriendControllerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -344,4 +344,153 @@ internal class FriendControllerTest(
}
}
}
describe("친구신청 수락 API") {
it("친구신청 수락 성공") {
// given
val data =
AddFriendResponse(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.acceptFriendRequest(
CommonTest.TEST_USER_ID,
CommonTest.TEST_FRIEND_ID,
)
} returns data

// when
val result =
webTestClient.patch()
.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 = "acceptFriendRequest200",
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.patch()
.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 = "acceptFriendRequest403",
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.patch()
.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 = "acceptFriendRequest500",
description = "친구신청 수락 API",
) {
request {
path {
"userId" mean "사용자 아이디"
"friendId" mean "친구신청을 받은 사용자의 아이디"
}
header {
"Authorization" mean "Bearer token"
}
}
response {
body {
"message" type Strings mean "에러 메시지"
}
}
}
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ internal class FriendServiceImplTest : FunSpec({
val friendList = listOf(mockk<Friend>(relaxed = true))

every {
friendRepository.findAllByUserIdAndRequestStatus(CommonTest.TEST_USER_ID, FriendRequestStatus.RECEIVED)
friendRepository.findAllByUserIdAndRequestStatus(
CommonTest.TEST_USER_ID,
FriendRequestStatus.RECEIVED,
)
} returns friendList

val response = friendService.getFriendRequests(CommonTest.TEST_USER_ID)
Expand All @@ -116,4 +119,75 @@ internal class FriendServiceImplTest : FunSpec({
request.username shouldBe friend.username
}
}

test("친구신청수락_성공") {
val receivedFriend = mockk<Friend>(relaxed = true)
val requestedFriend = mockk<Friend>(relaxed = true)

every {
friendRepository.findByUserIdAndFriendIdAndRequestStatus(
CommonTest.TEST_USER_ID,
CommonTest.TEST_FRIEND_ID,
FriendRequestStatus.RECEIVED,
)
} returns receivedFriend
every {
friendRepository.findByUserIdAndFriendIdAndRequestStatus(
CommonTest.TEST_FRIEND_ID,
CommonTest.TEST_USER_ID,
FriendRequestStatus.REQUESTED,
)
} returns requestedFriend

every { receivedFriend.updateRequestStatus(any()) } just Runs
every { requestedFriend.updateRequestStatus(any()) } just Runs

val response =
friendService.acceptFriendRequest(CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID)
response.friendId shouldBe CommonTest.TEST_FRIEND_ID

verify(exactly = 2) {
friendRepository.findByUserIdAndFriendIdAndRequestStatus(any(), any(), any())
}
}

test("친구신청수락_실패_해당하는 친구신청이 없는 케이스") {
every {
friendRepository.findByUserIdAndFriendIdAndRequestStatus(
CommonTest.TEST_USER_ID,
CommonTest.TEST_FRIEND_ID,
FriendRequestStatus.RECEIVED,
)
} throws ServiceException(UserErrorCode.FRIENDSHIP_ALREADY_EXISTS_OR_NOT_FOUND)

val exception =
shouldThrow<ServiceException> {
friendService.acceptFriendRequest(CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID)
}
exception.errorCode.message() shouldBe "해당하는 친구신청이 없거나 이미 친구입니다."
}

test("친구신청수락_실패_이미 친구인 케이스") {

every {
friendRepository.findByUserIdAndFriendIdAndRequestStatus(
CommonTest.TEST_USER_ID,
CommonTest.TEST_FRIEND_ID,
FriendRequestStatus.RECEIVED,
)
} throws ServiceException(UserErrorCode.FRIENDSHIP_ALREADY_EXISTS_OR_NOT_FOUND)
every {
friendRepository.findByUserIdAndFriendIdAndRequestStatus(
CommonTest.TEST_FRIEND_ID,
CommonTest.TEST_USER_ID,
FriendRequestStatus.REQUESTED,
)
} throws ServiceException(UserErrorCode.FRIENDSHIP_ALREADY_EXISTS_OR_NOT_FOUND)

val exception =
shouldThrow<ServiceException> {
friendService.acceptFriendRequest(CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID)
}
exception.errorCode.message() shouldBe "해당하는 친구신청이 없거나 이미 친구입니다."
}
})

0 comments on commit 668d7b5

Please sign in to comment.