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

[User] 친구 신청 수락 API 구현 #186

Merged
merged 10 commits into from
Jun 19, 2024
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 @@ -3,6 +3,7 @@ package kpring.user.repository
import kpring.user.entity.Friend
import kpring.user.entity.FriendRequestStatus
import org.springframework.data.jpa.repository.JpaRepository
import java.util.*

interface FriendRepository : JpaRepository<Friend, Long> {
fun existsByUserIdAndFriendId(
Expand All @@ -14,4 +15,10 @@ interface FriendRepository : JpaRepository<Friend, Long> {
userId: Long,
requestStatus: FriendRequestStatus,
): List<Friend>

fun findByUserIdAndFriendIdAndRequestStatus(
userId: Long,
friendId: Long,
requestStatus: FriendRequestStatus,
): Optional<Friend>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional을 사용하는 이유가 있나요? Friend? 타입을 사용하지 않고 Optional을 사용하신 이유가 궁금해요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수락하고자 하는 친구관계를 가져오는 과정에서 null이 반환될 경우, exception처리를 하기에 Optional을 사용하는 것이 더 깔끔한 것 같다고 판단하여 사용했습니다! Optional 사용을 지양하는 게 좋을까요,,?

}
5 changes: 5 additions & 0 deletions user/src/main/kotlin/kpring/user/service/FriendService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ interface FriendService {
friendId: Long,
): AddFriendResponse

fun acceptFriendRequest(
userId: Long,
friendId: Long,
): AddFriendResponse

Comment on lines +44 to +48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

내부 구현을 보지 않고도 이 함수가 제공하는 기능이 무엇인지 주석을 달아주면 좋을 것 같아요.

기대하는 동작은 무엇인지 그리고 잘못된 요청을 한 케이스에 어떤 예외가 발생하는지와 같은 정보가 있으면 유지보수할 때, 좋을 것 같아요.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 서비스단 메서드에 주석문 추가하도록 하겠습니다!!

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)
.orElseThrow { 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 @@ -13,6 +13,7 @@ import kpring.user.global.CommonTest
import kpring.user.repository.FriendRepository
import kpring.user.repository.UserRepository
import org.springframework.security.crypto.password.PasswordEncoder
import java.util.*

internal class FriendServiceImplTest : FunSpec({
val userRepository: UserRepository = mockk()
Expand Down Expand Up @@ -107,7 +108,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 +120,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 Optional.of(receivedFriend)
every {
friendRepository.findByUserIdAndFriendIdAndRequestStatus(
CommonTest.TEST_FRIEND_ID,
CommonTest.TEST_USER_ID,
FriendRequestStatus.REQUESTED,
)
} returns Optional.of(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,
)
} returns Optional.empty()
every {
friendRepository.findByUserIdAndFriendIdAndRequestStatus(
CommonTest.TEST_FRIEND_ID,
CommonTest.TEST_USER_ID,
FriendRequestStatus.REQUESTED,
)
} returns Optional.empty()

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