From 808c6675c1060d7f77b0b28633af1965e5f7b81d Mon Sep 17 00:00:00 2001 From: pricelees Date: Mon, 14 Jul 2025 12:41:18 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=83=88=EB=A1=9C=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=A0=20API=20/=20=EC=97=90=EB=9F=AC=20=ED=86=B5=ED=95=A9?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=20=EA=B0=9D=EC=B2=B4=20=EC=A0=95=EC=9D=98?= =?UTF-8?q?=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/RoomescapeApiResponseKT.kt | 33 ++++ .../common/exception/ErrorType.java | 13 ++ .../response/RoomescapeApiResponseKTTest.kt | 158 ++++++++++++++++++ 3 files changed, 204 insertions(+) create mode 100644 src/main/java/roomescape/common/dto/response/RoomescapeApiResponseKT.kt create mode 100644 src/test/java/roomescape/common/dto/response/RoomescapeApiResponseKTTest.kt diff --git a/src/main/java/roomescape/common/dto/response/RoomescapeApiResponseKT.kt b/src/main/java/roomescape/common/dto/response/RoomescapeApiResponseKT.kt new file mode 100644 index 00000000..e3e60f74 --- /dev/null +++ b/src/main/java/roomescape/common/dto/response/RoomescapeApiResponseKT.kt @@ -0,0 +1,33 @@ +package roomescape.common.dto.response + +import com.fasterxml.jackson.annotation.JsonInclude +import roomescape.common.exception.ErrorType + + +@JsonInclude(JsonInclude.Include.NON_NULL) +data class RoomescapeApiResponseKT( + val success: Boolean, + val data: T? = null, + val errorType: ErrorType? = null, + val message: String? = null, +) { + companion object { + + @JvmStatic + fun success(data: T? = null): RoomescapeApiResponseKT { + return RoomescapeApiResponseKT( + success = true, + data = data, + ) + } + + @JvmStatic + fun fail(errorType: ErrorType, message: String? = null): RoomescapeApiResponseKT { + return RoomescapeApiResponseKT( + success = false, + errorType = errorType, + message = message ?: errorType.description + ) + } + } +} diff --git a/src/main/java/roomescape/common/exception/ErrorType.java b/src/main/java/roomescape/common/exception/ErrorType.java index 499335ae..e81965a8 100644 --- a/src/main/java/roomescape/common/exception/ErrorType.java +++ b/src/main/java/roomescape/common/exception/ErrorType.java @@ -1,5 +1,10 @@ package roomescape.common.exception; +import java.util.Arrays; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + public enum ErrorType { // 400 Bad Request @@ -58,4 +63,12 @@ public enum ErrorType { public String getDescription() { return description; } + + @JsonCreator + public static ErrorType from(@JsonProperty("errorType") String errorType) { + return Arrays.stream(ErrorType.values()) + .filter(type -> type.name().equalsIgnoreCase(errorType)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Invalid error type: " + errorType)); + } } diff --git a/src/test/java/roomescape/common/dto/response/RoomescapeApiResponseKTTest.kt b/src/test/java/roomescape/common/dto/response/RoomescapeApiResponseKTTest.kt new file mode 100644 index 00000000..5ca55169 --- /dev/null +++ b/src/test/java/roomescape/common/dto/response/RoomescapeApiResponseKTTest.kt @@ -0,0 +1,158 @@ +package roomescape.common.dto.response + +import com.fasterxml.jackson.databind.ObjectMapper +import com.ninjasquad.springmockk.MockkBean +import com.ninjasquad.springmockk.SpykBean +import io.kotest.core.spec.style.BehaviorSpec +import org.hamcrest.CoreMatchers.equalTo +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.post +import org.springframework.web.bind.annotation.* +import roomescape.auth.infrastructure.jwt.JwtHandler +import roomescape.auth.web.support.AdminInterceptor +import roomescape.auth.web.support.LoginInterceptor +import roomescape.auth.web.support.MemberIdResolver +import roomescape.common.exception.ErrorType +import roomescape.member.business.MemberService +import roomescape.member.infrastructure.persistence.MemberRepository + +@WebMvcTest(ApiResponseTestController::class) +class RoomescapeApiResponseKTTest( + @Autowired private val mockMvc: MockMvc +) : BehaviorSpec() { + @Autowired + private lateinit var AdminInterceptor: AdminInterceptor + + @Autowired + private lateinit var loginInterceptor: LoginInterceptor + + @Autowired + private lateinit var memberIdResolver: MemberIdResolver + + @SpykBean + private lateinit var memberService: MemberService + + @MockkBean + private lateinit var memberRepository: MemberRepository + + @MockkBean + private lateinit var jwtHandler: JwtHandler + + init { + Given("성공 응답에") { + val endpoint = "/success" + When("객체 데이터를 담으면") { + val id: Long = 1L + val name = "name" + Then("success=true, data={객체} 형태로 응답한다.") { + mockMvc.post("$endpoint/$id/$name") { + contentType = MediaType.APPLICATION_JSON + }.andDo { + print() + }.andExpect { + status { isOk() } + jsonPath("$.success", equalTo(true)) + jsonPath("$.data.id", equalTo(id.toInt())) + jsonPath("$.data.name", equalTo(name)) + } + } + } + + When("문자열 데이터를 담으면") { + val message: String = "Hello, World!" + + Then("success=true, data={문자열} 형태로 응답한다.") { + mockMvc.get("/success/$message") { + contentType = MediaType.APPLICATION_JSON + }.andDo { + print() + }.andExpect { + status { isOk() } + jsonPath("$.success", equalTo(true)) + jsonPath("$.data", equalTo(message)) + } + } + } + } + + Given("실패 응답에") { + val endpoint = "/fail" + val objectMapper = ObjectMapper() + + When("errorType만 담으면") { + Then("success=false, errorType={errorType}, message={errorType.description} 형태로 응답한다.") { + mockMvc.post(endpoint) { + contentType = MediaType.APPLICATION_JSON + content = objectMapper.writeValueAsString(FailRequest(errorType = ErrorType.INTERNAL_SERVER_ERROR)) + }.andDo { + print() + }.andExpect { + status { isOk() } + jsonPath("$.success", equalTo(false)) + jsonPath("$.errorType", equalTo(ErrorType.INTERNAL_SERVER_ERROR.name)) + jsonPath("$.message", equalTo(ErrorType.INTERNAL_SERVER_ERROR.description)) + } + } + } + + When("errorType과 message를 담으면") { + val message: String = "An error occurred" + + Then("success=false, errorType={errorType}, message={message} 형태로 응답한다.") { + mockMvc.post(endpoint) { + contentType = MediaType.APPLICATION_JSON + content = objectMapper.writeValueAsString(FailRequest(errorType = ErrorType.INTERNAL_SERVER_ERROR, message = message)) + }.andDo { + print() + }.andExpect { + status { isOk() } + jsonPath("$.success", equalTo(false)) + jsonPath("$.errorType", equalTo(ErrorType.INTERNAL_SERVER_ERROR.name)) + jsonPath("$.message", equalTo(message)) + } + } + } + } + } +} + +data class SuccessResponse( + val id: Long, + val name: String +) + +data class FailRequest( + val errorType: ErrorType, + val message: String? = null +) + +@RestController +class ApiResponseTestController { + + @GetMapping("/success/{message}") + fun succeedToGet( + @PathVariable message: String, + ): RoomescapeApiResponseKT = + RoomescapeApiResponseKT.success(message) + + + @PostMapping("/success/{id}/{name}") + fun succeedToPost( + @PathVariable id: Long, + @PathVariable name: String, + ): RoomescapeApiResponseKT = + RoomescapeApiResponseKT.success(SuccessResponse(id, name)) + + + @PostMapping("/fail") + fun fail( + @RequestBody request: FailRequest + ): RoomescapeApiResponseKT = + request.message?.let { + RoomescapeApiResponseKT.fail(request.errorType, it) + } ?: RoomescapeApiResponseKT.fail(request.errorType) +}