generated from pricelees/issue-pr-template
add application code with E2E tests
This commit is contained in:
parent
55856659dd
commit
68a0724e16
48
build.gradle
48
build.gradle
@ -1,48 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id 'org.jetbrains.kotlin.jvm' version '1.9.25'
|
|
||||||
id 'org.jetbrains.kotlin.plugin.spring' version '1.9.25'
|
|
||||||
id 'org.springframework.boot' version '3.5.3'
|
|
||||||
id 'io.spring.dependency-management' version '1.1.7'
|
|
||||||
}
|
|
||||||
|
|
||||||
group = 'com.sangdol'
|
|
||||||
version = '0.0.1-SNAPSHOT'
|
|
||||||
|
|
||||||
java {
|
|
||||||
toolchain {
|
|
||||||
languageVersion = JavaLanguageVersion.of(17)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
configurations {
|
|
||||||
compileOnly {
|
|
||||||
extendsFrom annotationProcessor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
|
||||||
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin'
|
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-reflect'
|
|
||||||
implementation 'io.github.oshai:kotlin-logging-jvm:7.0.3'
|
|
||||||
compileOnly 'org.projectlombok:lombok'
|
|
||||||
annotationProcessor 'org.projectlombok:lombok'
|
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
|
||||||
testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5'
|
|
||||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlin {
|
|
||||||
compilerOptions {
|
|
||||||
freeCompilerArgs.addAll '-Xjsr305=strict'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named('test') {
|
|
||||||
useJUnitPlatform()
|
|
||||||
}
|
|
||||||
57
build.gradle.kts
Normal file
57
build.gradle.kts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
plugins {
|
||||||
|
val springBootVersion = "3.5.3"
|
||||||
|
val springDependencyVersion = "1.1.7"
|
||||||
|
val kotlinVersion = "2.2.0"
|
||||||
|
|
||||||
|
id("org.springframework.boot") version springBootVersion
|
||||||
|
id("io.spring.dependency-management") version springDependencyVersion
|
||||||
|
|
||||||
|
//kotlin plugins
|
||||||
|
kotlin("jvm") version kotlinVersion
|
||||||
|
kotlin("plugin.spring") version kotlinVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "com.sangdol"
|
||||||
|
version = "0.0.1-SNAPSHOT"
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(17)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||||
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||||
|
implementation("io.github.oshai:kotlin-logging-jvm:7.0.3")
|
||||||
|
|
||||||
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
compileKotlin {
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs.add("-Xjsr305=strict")
|
||||||
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||||
|
freeCompilerArgs.set(listOf("-Xannotation-default-target=param-property"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileTestKotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||||
|
freeCompilerArgs.set(listOf("-Xannotation-default-target=param-property"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/main/kotlin/com/sangdol/validation/DemoController.kt
Normal file
40
src/main/kotlin/com/sangdol/validation/DemoController.kt
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package com.sangdol.validation
|
||||||
|
|
||||||
|
import jakarta.validation.Valid
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
class DemoController(
|
||||||
|
private val demoService: DemoService
|
||||||
|
) {
|
||||||
|
|
||||||
|
@PostMapping("/solution")
|
||||||
|
fun solution(
|
||||||
|
@RequestBody @Valid request: SolutionForPrimitive
|
||||||
|
): ResponseEntity<Void> {
|
||||||
|
demoService.doSomething(request)
|
||||||
|
|
||||||
|
return ResponseEntity.ok().build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/wrapper")
|
||||||
|
fun wrapper(
|
||||||
|
@RequestBody @Valid request: WrapperRequest
|
||||||
|
): ResponseEntity<Void> {
|
||||||
|
demoService.doSomething(request)
|
||||||
|
|
||||||
|
return ResponseEntity.ok().build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/primitive")
|
||||||
|
fun primitive(
|
||||||
|
@RequestBody @Valid request: PrimitiveRequest
|
||||||
|
): ResponseEntity<Void> {
|
||||||
|
demoService.doSomething(request)
|
||||||
|
|
||||||
|
return ResponseEntity.ok().build()
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/main/kotlin/com/sangdol/validation/DemoDTO.kt
Normal file
28
src/main/kotlin/com/sangdol/validation/DemoDTO.kt
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package com.sangdol.validation
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull
|
||||||
|
|
||||||
|
|
||||||
|
data class WrapperRequest(
|
||||||
|
@NotNull
|
||||||
|
val withAnnotation: String,
|
||||||
|
|
||||||
|
val withoutAnnotation: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PrimitiveRequest(
|
||||||
|
@NotNull
|
||||||
|
val withAnnotation: Int,
|
||||||
|
|
||||||
|
val withoutAnnotation: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SolutionForPrimitive(
|
||||||
|
@NotNull
|
||||||
|
val value: Int?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ErrorResponse(
|
||||||
|
val type: String,
|
||||||
|
val message: String
|
||||||
|
)
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
package com.sangdol.validation
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.http.converter.HttpMessageNotReadableException
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
class DemoExceptionHandler(
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
) {
|
||||||
|
|
||||||
|
@ExceptionHandler(value = [HttpMessageNotReadableException::class, MethodArgumentNotValidException::class])
|
||||||
|
fun handleHttpMessageNotReadable(e: Exception): ResponseEntity<ErrorResponse> {
|
||||||
|
log.warn { "${e.javaClass} 발생!" }
|
||||||
|
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(
|
||||||
|
ErrorResponse(
|
||||||
|
e.javaClass.toString(),
|
||||||
|
e.message ?: "Empty Messages"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception::class)
|
||||||
|
fun handleException(e: Exception): ResponseEntity<ErrorResponse> {
|
||||||
|
log.error { "${e.javaClass} 발생!" }
|
||||||
|
|
||||||
|
return ResponseEntity.internalServerError().body(
|
||||||
|
ErrorResponse(
|
||||||
|
e.javaClass.toString(),
|
||||||
|
"Internal Server Error"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/main/kotlin/com/sangdol/validation/DemoService.kt
Normal file
26
src/main/kotlin/com/sangdol/validation/DemoService.kt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package com.sangdol.validation
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class DemoService(
|
||||||
|
private val log: KLogger = KotlinLogging.logger {},
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun doSomething(request: SolutionForPrimitive) {
|
||||||
|
log.info { "solution for primitive requests: $request" }
|
||||||
|
log.info { "value: ${request.value!!}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doSomething(request: WrapperRequest) {
|
||||||
|
log.info { "wrapper requests: $request." }
|
||||||
|
log.info { "withAnnotation: ${request.withAnnotation}, withoutAnnotation: ${request.withoutAnnotation}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doSomething(request: PrimitiveRequest) {
|
||||||
|
log.info { "primitive requests: $request." }
|
||||||
|
log.info { "withAnnotation: ${request.withAnnotation}, withoutAnnotation: ${request.withoutAnnotation}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.sangdol.validation
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||||
|
import org.springframework.boot.runApplication
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
class ValidationApplication
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
runApplication<ValidationApplication>(*args)
|
||||||
|
}
|
||||||
1
src/main/resources/application.properties
Normal file
1
src/main/resources/application.properties
Normal file
@ -0,0 +1 @@
|
|||||||
|
spring.application.name=validation
|
||||||
@ -1,3 +0,0 @@
|
|||||||
GET http://localhost:8080/numbers
|
|
||||||
Accept: application/json
|
|
||||||
|
|
||||||
100
src/test/kotlin/com/sangdol/validation/DemoControllerTest.kt
Normal file
100
src/test/kotlin/com/sangdol/validation/DemoControllerTest.kt
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package com.sangdol.validation
|
||||||
|
|
||||||
|
import org.hamcrest.CoreMatchers.containsString
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
|
import org.junit.jupiter.params.provider.CsvSource
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
||||||
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
|
import org.springframework.test.web.servlet.ResultActions
|
||||||
|
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
|
||||||
|
import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print
|
||||||
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
|
||||||
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
|
||||||
|
|
||||||
|
@WebMvcTest(controllers = [DemoController::class])
|
||||||
|
@MockitoBean(types = [DemoService::class])
|
||||||
|
class DemoControllerTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private val mockMvc: MockMvc? = null
|
||||||
|
|
||||||
|
@DisplayName("Wrapper 타입인 경우 값이 입력되지 않으면 HttpMessageNotReadableException이 발생한다.")
|
||||||
|
@ParameterizedTest(name = "{0}")
|
||||||
|
@CsvSource(
|
||||||
|
value = [
|
||||||
|
"@NotNull 필드의 값만 입력되는 경우 ; withAnnotation",
|
||||||
|
"@NotNull이 아닌 필드의 값만 입력되는 경우 ; withoutAnnotation"
|
||||||
|
],
|
||||||
|
delimiter = ';'
|
||||||
|
)
|
||||||
|
fun except_wrapper_field(testName: String, field: String) {
|
||||||
|
val endpoint = "/wrapper"
|
||||||
|
val value = "message"
|
||||||
|
val body = "{\"$field\": \"$value\"}"
|
||||||
|
|
||||||
|
runException(endpoint, body, HttpStatus.BAD_REQUEST.value(), "HttpMessageNotReadableException")
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("Primitive 타입인 경우 @NotNull 지정과 무관하게 값이 입력되지 않아도 기본값이 들어간다.")
|
||||||
|
@ParameterizedTest(name = "{0}")
|
||||||
|
@CsvSource(
|
||||||
|
value = [
|
||||||
|
"@NotNull 필드의 값만 입력되는 경우 ; withAnnotation",
|
||||||
|
"@NotNull이 아닌 필드의 값만 입력되는 경우 ; withoutAnnotation"
|
||||||
|
],
|
||||||
|
delimiter = ';'
|
||||||
|
)
|
||||||
|
fun except_primitive_field(testName: String, field: String) {
|
||||||
|
val endpoint = "/primitive"
|
||||||
|
val value = 10
|
||||||
|
val body = "{\"$field\": $value}"
|
||||||
|
|
||||||
|
run(endpoint, body, HttpStatus.OK.value())
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("Primitive 타입인 경우 Bean Validation을 위해서는 @NotNull + nullable 필드 지정이 필요하다.")
|
||||||
|
@ParameterizedTest(name = "{0}")
|
||||||
|
@CsvSource(
|
||||||
|
value = [
|
||||||
|
"""필드가 없는 경우 ; {"notExistField": 10} ; 400""",
|
||||||
|
"""필드가 Null인 경우 ; {"value": null} ; 400""",
|
||||||
|
"""필드의 값이 정상인 경우 ; {"value": 10} ; 200"""
|
||||||
|
], delimiter = ';'
|
||||||
|
)
|
||||||
|
fun primitive_validation_with_nullable_field(testName: String, body: String, expectedStatus: Int) {
|
||||||
|
val endpoint = "/solution"
|
||||||
|
|
||||||
|
if (expectedStatus == HttpStatus.OK.value()) {
|
||||||
|
run(endpoint, body, expectedStatus)
|
||||||
|
} else {
|
||||||
|
runException(endpoint, body, expectedStatus, "MethodArgumentNotValidException")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun run(endpoint: String, body: String, expectedStatus: Int): ResultActions = mockMvc!!
|
||||||
|
.perform(
|
||||||
|
post(endpoint)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
.content(body)
|
||||||
|
)
|
||||||
|
.andExpect(status().`is`(expectedStatus))
|
||||||
|
.andDo(print())
|
||||||
|
|
||||||
|
|
||||||
|
private fun runException(
|
||||||
|
endpoint: String,
|
||||||
|
body: String,
|
||||||
|
expectedStatus: Int,
|
||||||
|
expectedExceptionType: String
|
||||||
|
): ResultActions = run(endpoint, body, expectedStatus)
|
||||||
|
.andExpect(
|
||||||
|
jsonPath(
|
||||||
|
"$.type", containsString(expectedExceptionType)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user