Skip to content

#94 - ExceptionView를 함께 반환한다. #99

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

Merged
merged 7 commits into from
Aug 24, 2024
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
113 changes: 49 additions & 64 deletions app/src/main/kotlin/com/wespot/common/GlobalExceptionHandler.kt
Original file line number Diff line number Diff line change
@@ -1,124 +1,111 @@
package com.wespot.common

import com.wespot.ReasonPhraseUtil
import com.wespot.common.`in`.ErrorNotificationUseCase
import com.wespot.exception.CustomException
import com.wespot.exception.ExceptionResponse
import com.wespot.exception.ExceptionView
import feign.FeignException
import jakarta.servlet.http.HttpServletRequest
import org.slf4j.LoggerFactory
import org.springframework.core.env.Environment
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.HttpStatusCode
import org.springframework.http.ProblemDetail
import org.springframework.http.ResponseEntity
import org.springframework.web.HttpRequestMethodNotSupportedException
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.context.request.WebRequest
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
import java.net.URI
import java.time.LocalDateTime

@ControllerAdvice
class GlobalExceptionHandler(
private val errorNotificationUseCase: ErrorNotificationUseCase,
private val environment: Environment
) {
) : ResponseEntityExceptionHandler() {

private val logger = LoggerFactory.getLogger(GlobalExceptionHandler::class.java)

@ExceptionHandler(IllegalArgumentException::class)
fun handleIllegalArgumentException(
exception: IllegalArgumentException,
@ExceptionHandler(CustomException::class)
fun handleCustomException(
exception: CustomException,
request: HttpServletRequest
): ResponseEntity<ProblemDetail> {
): ResponseEntity<ExceptionResponse> {
notifyException(false, request, exception)
logger.error("요청된 정보가 잘못되었습니다.", exception)
logger.warn("예외가 발생했습니다.", exception)

val problemDetail = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST,
exception.status,
exception.message
).apply {
type = URI.create("/errors/illegal-argument")
type = ReasonPhraseUtil.createErrorTypeInProblemDetail("/error", exception.status)
instance = URI.create(request.requestURI)
}

return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(problemDetail)
return ResponseEntity.status(exception.status)
.body(ExceptionResponse(exception.view, problemDetail))
}

@ExceptionHandler(IllegalStateException::class)
fun handleIllegalStateException(
exception: IllegalStateException,
@ExceptionHandler(Exception::class)
fun handleException(
exception: Exception,
request: HttpServletRequest
): ResponseEntity<ProblemDetail> {
notifyException(false, request, exception)
logger.error("잘못된 상태입니다", exception)
notifyException(true, request, exception)
val internalErrorMessage = "서버에서 알 수 없는 에러가 발생했습니다."
logger.error(internalErrorMessage, exception)

val problemDetail = ProblemDetail.forStatusAndDetail(
HttpStatus.CONFLICT,
exception.message
HttpStatus.INTERNAL_SERVER_ERROR,
internalErrorMessage
).apply {
type = URI.create("/errors/conflict")
type = ReasonPhraseUtil.createErrorTypeInProblemDetail("/error", HttpStatus.INTERNAL_SERVER_ERROR)
instance = URI.create(request.requestURI)
}

return ResponseEntity.status(HttpStatus.CONFLICT)
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(problemDetail)
}

@ExceptionHandler(HttpRequestMethodNotSupportedException::class)
fun handleHttpRequestMethodNotSupported(
exception: HttpRequestMethodNotSupportedException,
@ExceptionHandler(FeignException::class)
fun handleFeignException(
exception: FeignException,
request: HttpServletRequest
): ResponseEntity<ProblemDetail> {
): ResponseEntity<ExceptionResponse> {
notifyException(false, request, exception)
logger.error("지원하지 않는 HTTP 메소드입니다.", exception)
logger.warn("외부 API 호출 중 예외가 발생했습니다.", exception)

val problemDetail = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST,
exception.message
).apply {
type = URI.create("/errors/method-not-supported")
type = ReasonPhraseUtil.createErrorTypeInProblemDetail("/error", HttpStatus.BAD_REQUEST)
instance = URI.create(request.requestURI)
}

return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(problemDetail)
.body(ExceptionResponse(ExceptionView.TOAST, problemDetail))
}

@ExceptionHandler(NoSuchElementException::class)
fun handleNoSuchElementException(
exception: NoSuchElementException,
request: HttpServletRequest
): ResponseEntity<ProblemDetail> {
notifyException(false, request, exception)
logger.error("자원을 찾을 수 없습니다.", exception)
override fun handleMethodArgumentNotValid(
exception: MethodArgumentNotValidException,
headers: HttpHeaders,
status: HttpStatusCode,
request: WebRequest
): ResponseEntity<Any> {
logger.warn("잘못된 요청입니다.", exception)

val problemDetail = ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND,
HttpStatus.BAD_REQUEST,
exception.message
).apply {
type = URI.create("/errors/no-such-element")
instance = URI.create(request.requestURI)
}

return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(problemDetail)
}

@ExceptionHandler(Exception::class)
fun handleException(
exception: Exception,
request: HttpServletRequest
): ResponseEntity<ProblemDetail> {
notifyException(true, request, exception)
logger.error("서버에서 알 수 없는 에러가 발생했습니다.", exception)

val problemDetail = ProblemDetail.forStatusAndDetail(
HttpStatus.INTERNAL_SERVER_ERROR,
"서버에서 알 수 없는 에러가 발생했습니다."
).apply {
type = URI.create("/errors/internal-server-error")
instance = URI.create(request.requestURI)
type = ReasonPhraseUtil.createErrorTypeInProblemDetail("/error", HttpStatus.BAD_REQUEST)
instance = URI.create(request.getDescription(false))
}

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(problemDetail)
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ExceptionResponse(ExceptionView.TOAST, problemDetail))
}

private fun notifyException(isError: Boolean, request: HttpServletRequest, exception: Exception) {
Expand All @@ -136,7 +123,6 @@ class GlobalExceptionHandler(

val fullStackTrace = exception.stackTraceToString().take(3000)

// 예외 발생 알림을 디스코드에 전송
errorNotificationUseCase.notifyError(
isError,
"### 🕖 발생 시간\n" +
Expand Down Expand Up @@ -169,7 +155,6 @@ class GlobalExceptionHandler(
)
}


private fun extractExceptionSource(exception: Exception): String {
val stackTrace = exception.stackTrace
if (stackTrace.isNotEmpty()) {
Expand Down
6 changes: 5 additions & 1 deletion app/src/main/kotlin/com/wespot/config/FirebaseConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ package com.wespot.config
import com.google.auth.oauth2.GoogleCredentials
import com.google.firebase.FirebaseApp
import com.google.firebase.FirebaseOptions
import com.wespot.config.security.CustomUrlFilter
import com.wespot.exception.CustomException
import com.wespot.exception.ExceptionView
import jakarta.annotation.PostConstruct
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpStatus

@Configuration
class FirebaseConfig {
Expand All @@ -21,7 +25,7 @@ class FirebaseConfig {
FirebaseApp.initializeApp(options)
}
} catch (e: Exception) {
throw IllegalArgumentException("Firebase APP 연결에 실패했습니다.", e)
throw CustomException(HttpStatus.INTERNAL_SERVER_ERROR, ExceptionView.DIALOG, "Firebase APP 연결에 실패했습니다.")
}
}

Expand Down
35 changes: 24 additions & 11 deletions app/src/test/kotlin/com/wespot/auth/service/AuthServiceTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import com.wespot.auth.fixture.AuthFixture
import com.wespot.auth.port.out.AuthDataPort
import com.wespot.auth.port.out.RefreshTokenPort
import com.wespot.auth.service.jwt.JwtTokenProvider
import com.wespot.exception.CustomException
import com.wespot.exception.ExceptionView
import com.wespot.school.port.out.SchoolPort
import com.wespot.user.SocialType
import com.wespot.user.event.CreatedVoteEvent
Expand All @@ -31,6 +33,7 @@ import io.mockk.just
import io.mockk.mockk
import io.mockk.spyk
import org.springframework.context.ApplicationEventPublisher
import org.springframework.http.HttpStatus
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.core.Authentication
import org.springframework.security.crypto.password.PasswordEncoder
Expand Down Expand Up @@ -167,10 +170,14 @@ class AuthServiceTest : BehaviorSpec({
}

`when`("잘못된 socialType으로 loginAccess를 호출할 때") {
every { authService.fetchSocialEmail(authLoginRequest) } throws NoSuchElementException("잘못된 socialType 입니다")
every { authService.fetchSocialEmail(authLoginRequest) } throws CustomException(
HttpStatus.BAD_REQUEST,
ExceptionView.TOAST,
"잘못된 socialType 입니다"
)

then("NoSuchElementException이 발생해야 한다") {
shouldThrow<NoSuchElementException> {
then("CustomException 400이 발생해야 한다") {
shouldThrow<CustomException> {
authService.socialAccess(authLoginRequest)
}
}
Expand Down Expand Up @@ -235,10 +242,14 @@ class AuthServiceTest : BehaviorSpec({
}

`when`("잘못된 signUpToken으로 signUp을 호출할 때") {
every { authService.checkSignUpToken(signUpRequest.signUpToken) } throws NoSuchElementException("잘못된 signUpToken 입니다")
every { authService.checkSignUpToken(signUpRequest.signUpToken) } throws CustomException(
HttpStatus.BAD_REQUEST,
ExceptionView.TOAST,
"잘못된 signUpToken 입니다"
)

then("NoSuchElementException이 발생해야 한다") {
shouldThrow<NoSuchElementException> {
then("CustomException 400이 발생해야 한다") {
shouldThrow<CustomException> {
authService.signUp(signUpRequest)
}
}
Expand Down Expand Up @@ -334,12 +345,14 @@ class AuthServiceTest : BehaviorSpec({
}

`when`("잘못된 refreshToken으로 reIssueToken을 호출할 때") {
every { authenticationService.getAuthentication(refreshTokenRequest.refreshToken) } throws NoSuchElementException(
every { authenticationService.getAuthentication(refreshTokenRequest.refreshToken) } throws CustomException(
HttpStatus.BAD_REQUEST,
ExceptionView.TOAST,
"잘못된 refreshToken 입니다"
)

then("NoSuchElementException이 발생해야 한다") {
shouldThrow<NoSuchElementException> {
then("CustomException이 Bad Request가 발생해야 한다") {
shouldThrow<CustomException> {
authService.reIssueToken(refreshTokenRequest)
}
}
Expand Down Expand Up @@ -387,8 +400,8 @@ class AuthServiceTest : BehaviorSpec({
`when`("잘못된 사용자 ID로 revoke를 호출할 때") {
every { userPort.findById(user.id) } returns null

then("NoSuchElementException이 발생해야 한다") {
shouldThrow<NoSuchElementException> {
then("CustomException Not Found가 발생해야 한다") {
shouldThrow<CustomException> {
authService.revoke()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.wespot.common.domain

import com.wespot.common.ProfanityChecker
import com.wespot.exception.CustomException
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe
Expand All @@ -23,10 +24,10 @@ class ProfanityCheckerTest : BehaviorSpec({
}
}
`when`("욕설이 존재하면") {
val shouldThrow1 = shouldThrow<IllegalArgumentException> { ProfanityChecker.validateContent(badWords[0]) }
val shouldThrow2 = shouldThrow<IllegalArgumentException> { ProfanityChecker.validateContent(badWords[1]) }
val shouldThrow3 = shouldThrow<IllegalArgumentException> { ProfanityChecker.validateContent(badWords[2]) }
val shouldThrow4 = shouldThrow<IllegalArgumentException> { ProfanityChecker.validateContent(badWords[3]) }
val shouldThrow1 = shouldThrow<CustomException> { ProfanityChecker.validateContent(badWords[0]) }
val shouldThrow2 = shouldThrow<CustomException> { ProfanityChecker.validateContent(badWords[1]) }
val shouldThrow3 = shouldThrow<CustomException> { ProfanityChecker.validateContent(badWords[2]) }
val shouldThrow4 = shouldThrow<CustomException> { ProfanityChecker.validateContent(badWords[3]) }
then("에외를 발생시킨다.") {
shouldThrow1 shouldHaveMessage "비속어가 포함되어 있습니다."
shouldThrow2 shouldHaveMessage "비속어가 포함되어 있습니다."
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.wespot.common.service

import com.wespot.common.dto.CheckProfanityRequest
import com.wespot.exception.CustomException
import io.kotest.assertions.throwables.shouldNotThrow
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.throwable.shouldHaveMessage
Expand All @@ -18,7 +19,7 @@ class CheckProfanityServiceTest @Autowired constructor(
val checkProfanityRequest = CheckProfanityRequest(validWord)

// when then
shouldNotThrow<IllegalArgumentException> { checkProfanityService.checkProfanity(checkProfanityRequest) }
shouldNotThrow<CustomException> { checkProfanityService.checkProfanity(checkProfanityRequest) }
}

@Test
Expand All @@ -29,7 +30,7 @@ class CheckProfanityServiceTest @Autowired constructor(

// when
val shouldThrow =
shouldThrow<IllegalArgumentException> { checkProfanityService.checkProfanity(checkProfanityRequest) }
shouldThrow<CustomException> { checkProfanityService.checkProfanity(checkProfanityRequest) }

// then
shouldThrow shouldHaveMessage "비속어가 포함되어 있습니다."
Expand Down
25 changes: 25 additions & 0 deletions app/src/test/kotlin/com/wespot/common/util/ReasonPhraseUtilTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.wespot.common.util

import com.wespot.ReasonPhraseUtil
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test
import org.springframework.http.HttpStatus
import java.net.URI

class ReasonPhraseUtilTest {

@Test
fun `HttpStatus내의 ReasonPhrase를 ProblemDetail내의 형식으로 변경한다`() {
// given
val prefixUrl = "/error"
val httpStatus = HttpStatus.BAD_REQUEST
val expected = URI.create("/error/bad-request")

// when
val actual = ReasonPhraseUtil.createErrorTypeInProblemDetail(prefixUrl, httpStatus)

// then
actual shouldBe expected
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.wespot.message.domain

import com.wespot.exception.CustomException
import com.wespot.message.MessageContent
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.BehaviorSpec
Expand All @@ -13,13 +14,13 @@ class MessageContentTest : BehaviorSpec({
val emptyContent = ""
val validContent = "헬로우"
`when`("욕설이 포함되어 있는 경우") {
val shouldThrow = shouldThrow<IllegalArgumentException> { MessageContent.from(badWordsContent) }
val shouldThrow = shouldThrow<CustomException> { MessageContent.from(badWordsContent) }
then("예외가 발생한다.") {
shouldThrow shouldHaveMessage "메시지의 내용에 비속어가 포함되어 있습니다."
}
}
`when`("아무런 내용이 없는 경우") {
val shouldThrow = shouldThrow<IllegalArgumentException> { MessageContent.from(emptyContent) }
val shouldThrow = shouldThrow<CustomException> { MessageContent.from(emptyContent) }
then("예외가 발생한다.") {
shouldThrow shouldHaveMessage "메시지의 내용은 필수로 존재해야합니다."
}
Expand Down
Loading
Loading