diff --git a/.editorconfig b/.editorconfig index efb002a..48d54cc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,4 +8,4 @@ max_line_length=150 trim_trailing_whitespace=true [*.{kt,kts}] -disabled_rules=import-ordering,no-wildcard-imports,indent +ktlint_disabled_rules=import-ordering,indent diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index be485aa..2c93009 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -10,6 +10,6 @@ val kotlinVersion = "1.7.20" dependencies { implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") // required by kotlin("jvm") implementation("org.jetbrains.kotlin:kotlin-allopen:$kotlinVersion") // required by kotlin("plugin.spring") - implementation("gradle.plugin.com.ekino.oss.plugin:kotlin-quality-plugin:3.2.0") // required by id("com.ekino.oss.plugin.kotlin-quality") + implementation("com.ekino.oss.plugin:kotlin-quality-plugin:4.1.0") // required by id("com.ekino.oss.plugin.kotlin-quality") implementation("org.jetbrains.dokka:dokka-gradle-plugin:$kotlinVersion") // required by id("org.jetbrains.dokka") } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5083229..37aef8d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/awsTest/kotlin/com/ekino/oss/errors/RestExceptionHandlerTest.kt b/src/awsTest/kotlin/com/ekino/oss/errors/RestExceptionHandlerTest.kt index 5633ef1..38cfbe7 100644 --- a/src/awsTest/kotlin/com/ekino/oss/errors/RestExceptionHandlerTest.kt +++ b/src/awsTest/kotlin/com/ekino/oss/errors/RestExceptionHandlerTest.kt @@ -21,8 +21,10 @@ class RestExceptionHandlerTest( fun `should get no such key exception`() { mockMvc.perform(get("$RESOLVED_ERROR_PATH/no-such-key-exception")) .andExpect(status().isNotFound) - .andExpect(content().string( - jsonMatcher(""" + .andExpect( + content().string( + jsonMatcher( + """ { "status": 404, "code": "error.not_found", @@ -34,7 +36,9 @@ class RestExceptionHandlerTest( "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } } diff --git a/src/dataRestTest/kotlin/com/ekino/oss/errors/RestExceptionHandlerTest.kt b/src/dataRestTest/kotlin/com/ekino/oss/errors/RestExceptionHandlerTest.kt index 4ea5a40..c8832a6 100644 --- a/src/dataRestTest/kotlin/com/ekino/oss/errors/RestExceptionHandlerTest.kt +++ b/src/dataRestTest/kotlin/com/ekino/oss/errors/RestExceptionHandlerTest.kt @@ -22,8 +22,10 @@ class RestExceptionHandlerTest( fun `should get not found error`() { mockMvc.perform(get("$RESOLVED_ERROR_PATH/notFound")) .andExpect(status().isNotFound) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 404, "code": "error.not_found", @@ -35,17 +37,23 @@ class RestExceptionHandlerTest( "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should get repository constraint validation error`() { - mockMvc.perform(get("$RESOLVED_ERROR_PATH/repository-constraint-validation") - .accept(MediaType.APPLICATION_JSON)) + mockMvc.perform( + get("$RESOLVED_ERROR_PATH/repository-constraint-validation") + .accept(MediaType.APPLICATION_JSON) + ) .andExpect(status().isBadRequest) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 400, "code": "error.invalid", @@ -69,7 +77,9 @@ class RestExceptionHandlerTest( "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } } diff --git a/src/main/kotlin/com/ekino/oss/errors/generator/ErrorBodyGenerator.kt b/src/main/kotlin/com/ekino/oss/errors/generator/ErrorBodyGenerator.kt index 8637aa4..b9e4400 100644 --- a/src/main/kotlin/com/ekino/oss/errors/generator/ErrorBodyGenerator.kt +++ b/src/main/kotlin/com/ekino/oss/errors/generator/ErrorBodyGenerator.kt @@ -7,14 +7,28 @@ import org.springframework.http.HttpStatus import java.time.Instant fun notFound(service: String, devMessage: String?, stacktrace: String): ErrorBody { - return toError(service, HttpStatus.NOT_FOUND, DefaultErrorCode.NOT_FOUND.value(), HttpStatus.NOT_FOUND.reasonPhrase, - devMessage, stacktrace, emptyList(), emptyList()) + return toError( + service, + HttpStatus.NOT_FOUND, + DefaultErrorCode.NOT_FOUND.value(), + HttpStatus.NOT_FOUND.reasonPhrase, + devMessage, + stacktrace, + emptyList(), + emptyList() + ) } fun unavailable(service: String, code: String, devMessage: String?, stacktrace: String): ErrorBody { return toError( - service, HttpStatus.SERVICE_UNAVAILABLE, code, HttpStatus.SERVICE_UNAVAILABLE.reasonPhrase, - devMessage, stacktrace, emptyList(), emptyList() + service, + HttpStatus.SERVICE_UNAVAILABLE, + code, + HttpStatus.SERVICE_UNAVAILABLE.reasonPhrase, + devMessage, + stacktrace, + emptyList(), + emptyList() ) } @@ -40,13 +54,28 @@ fun badRequest( fun methodNotAllowed(service: String, code: String, devMessage: String?, stacktrace: String): ErrorBody { return toError( - service, HttpStatus.METHOD_NOT_ALLOWED, code, HttpStatus.METHOD_NOT_ALLOWED.reasonPhrase, devMessage, stacktrace, emptyList(), emptyList() + service, + HttpStatus.METHOD_NOT_ALLOWED, + code, + HttpStatus.METHOD_NOT_ALLOWED.reasonPhrase, + devMessage, + stacktrace, + emptyList(), + emptyList() ) } fun conflict(service: String, devMessage: String?, stacktrace: String): ErrorBody { - return toError(service, HttpStatus.CONFLICT, DefaultErrorCode.CONFLICT.value(), HttpStatus.CONFLICT.reasonPhrase, - devMessage, stacktrace, emptyList(), emptyList()) + return toError( + service, + HttpStatus.CONFLICT, + DefaultErrorCode.CONFLICT.value(), + HttpStatus.CONFLICT.reasonPhrase, + devMessage, + stacktrace, + emptyList(), + emptyList() + ) } fun preconditionFailed(service: String, devMessage: String, stacktrace: String): ErrorBody { @@ -58,24 +87,46 @@ fun preconditionFailed(service: String, devMessage: String, stacktrace: String): devMessage, stacktrace, emptyList(), - emptyList()) + emptyList() + ) } fun conflict(service: String, devMessage: String, stacktrace: String, errors: List): ErrorBody { return toError( - service, HttpStatus.CONFLICT, DefaultErrorCode.CONFLICT.value(), HttpStatus.CONFLICT.reasonPhrase, devMessage, stacktrace, errors, emptyList() + service, + HttpStatus.CONFLICT, + DefaultErrorCode.CONFLICT.value(), + HttpStatus.CONFLICT.reasonPhrase, + devMessage, + stacktrace, + errors, + emptyList() ) } fun unprocessableEntity(service: String, code: String, devMessage: String, stacktrace: String, errors: List): ErrorBody { return toError( - service, HttpStatus.UNPROCESSABLE_ENTITY, code, HttpStatus.UNPROCESSABLE_ENTITY.reasonPhrase, devMessage, stacktrace, errors, emptyList() + service, + HttpStatus.UNPROCESSABLE_ENTITY, + code, + HttpStatus.UNPROCESSABLE_ENTITY.reasonPhrase, + devMessage, + stacktrace, + errors, + emptyList() ) } fun unsupportedMediaType(service: String, code: String, devMessage: String?, stacktrace: String): ErrorBody { return toError( - service, HttpStatus.UNSUPPORTED_MEDIA_TYPE, code, HttpStatus.UNSUPPORTED_MEDIA_TYPE.reasonPhrase, devMessage, stacktrace, emptyList(), emptyList() + service, + HttpStatus.UNSUPPORTED_MEDIA_TYPE, + code, + HttpStatus.UNSUPPORTED_MEDIA_TYPE.reasonPhrase, + devMessage, + stacktrace, + emptyList(), + emptyList() ) } diff --git a/src/main/kotlin/com/ekino/oss/errors/handler/CoreExceptionHandler.kt b/src/main/kotlin/com/ekino/oss/errors/handler/CoreExceptionHandler.kt index 342b177..a0ac655 100644 --- a/src/main/kotlin/com/ekino/oss/errors/handler/CoreExceptionHandler.kt +++ b/src/main/kotlin/com/ekino/oss/errors/handler/CoreExceptionHandler.kt @@ -46,7 +46,10 @@ abstract class CoreExceptionHandler( fun handleUnavailableServiceException(req: HttpServletRequest, e: Exception): ResponseEntity { log.error("Unavailable service : ", e) return unavailable( - req.toServiceName(applicationName), "error.unavailable", e.message, e.toStacktrace(properties.displayFullStacktrace) + req.toServiceName(applicationName), + "error.unavailable", + e.message, + e.toStacktrace(properties.displayFullStacktrace) ).toErrorResponse() } @@ -79,7 +82,11 @@ abstract class CoreExceptionHandler( val errors = e.constraintViolations?.map { it.toValidationErrorBody() } ?: emptyList() return badRequest( - req.toServiceName(applicationName), INVALID_ERROR_PREFIX, e.message, e.toStacktrace(properties.displayFullStacktrace), errors + req.toServiceName(applicationName), + INVALID_ERROR_PREFIX, + e.message, + e.toStacktrace(properties.displayFullStacktrace), + errors ).toErrorResponse() } @@ -91,11 +98,18 @@ abstract class CoreExceptionHandler( val validationErrorBody = listOf(cause.toEnumValidationErrorBody()) val message = "The value '${cause.value}' is not an accepted value" badRequest( - req.toServiceName(applicationName), "error.invalid.enum", message, e.toStacktrace(properties.displayFullStacktrace), validationErrorBody + req.toServiceName(applicationName), + "error.invalid.enum", + message, + e.toStacktrace(properties.displayFullStacktrace), + validationErrorBody ).toErrorResponse() } else { badRequest( - req.toServiceName(applicationName), "error.not_readable_json", e.message, e.toStacktrace(properties.displayFullStacktrace) + req.toServiceName(applicationName), + "error.not_readable_json", + e.message, + e.toStacktrace(properties.displayFullStacktrace) ).toErrorResponse() } } @@ -108,11 +122,18 @@ abstract class CoreExceptionHandler( val validationErrorBody = listOf(e.toEnumValidationErrorBody()) val message = "The value '${e.value}' is not an accepted value" badRequest( - req.toServiceName(applicationName), "error.invalid.enum", message, e.toStacktrace(properties.displayFullStacktrace), validationErrorBody + req.toServiceName(applicationName), + "error.invalid.enum", + message, + e.toStacktrace(properties.displayFullStacktrace), + validationErrorBody ).toErrorResponse() } else { badRequest( - req.toServiceName(applicationName), "error.argument_type_mismatch", e.message, e.toStacktrace(properties.displayFullStacktrace) + req.toServiceName(applicationName), + "error.argument_type_mismatch", + e.message, + e.toStacktrace(properties.displayFullStacktrace) ).toErrorResponse() } } @@ -121,7 +142,10 @@ abstract class CoreExceptionHandler( fun handleMissingServletRequestParameterException(req: HttpServletRequest, e: MissingServletRequestParameterException): ResponseEntity { log.debug("Missing parameter : ", e) return badRequest( - req.toServiceName(applicationName), "error.missing_parameter", e.message, e.toStacktrace(properties.displayFullStacktrace) + req.toServiceName(applicationName), + "error.missing_parameter", + e.message, + e.toStacktrace(properties.displayFullStacktrace) ).toErrorResponse() } @@ -129,7 +153,10 @@ abstract class CoreExceptionHandler( fun handleMethodNotSupportedException(req: HttpServletRequest, e: HttpRequestMethodNotSupportedException): ResponseEntity { log.debug("Method not supported : ", e) return methodNotAllowed( - req.toServiceName(applicationName), "error.method_not_allowed", e.message, e.toStacktrace(properties.displayFullStacktrace) + req.toServiceName(applicationName), + "error.method_not_allowed", + e.message, + e.toStacktrace(properties.displayFullStacktrace) ).toErrorResponse() } @@ -143,7 +170,10 @@ abstract class CoreExceptionHandler( fun handleHttpMediaTypeNotSupportedException(req: HttpServletRequest, e: HttpMediaTypeNotSupportedException): ResponseEntity { log.debug("Media type not supported : ", e) return unsupportedMediaType( - req.toServiceName(applicationName), "error.media_type_not_supported", e.message, e.toStacktrace(properties.displayFullStacktrace) + req.toServiceName(applicationName), + "error.media_type_not_supported", + e.message, + e.toStacktrace(properties.displayFullStacktrace) ).toErrorResponse() } @@ -159,8 +189,11 @@ abstract class CoreExceptionHandler( val message = responseStatus?.reason ?: e.toMessage() return defaultError( - req.toServiceName(applicationName), status, "error." + e.javaClass.simpleName.camelToSnakeCase(), - message, e.toStacktrace(properties.displayFullStacktrace) + req.toServiceName(applicationName), + status, + "error." + e.javaClass.simpleName.camelToSnakeCase(), + message, + e.toStacktrace(properties.displayFullStacktrace) ).toErrorResponse() } diff --git a/src/main/kotlin/com/ekino/oss/errors/handler/DataRestExceptionHandler.kt b/src/main/kotlin/com/ekino/oss/errors/handler/DataRestExceptionHandler.kt index b5972ef..b2dd7f3 100644 --- a/src/main/kotlin/com/ekino/oss/errors/handler/DataRestExceptionHandler.kt +++ b/src/main/kotlin/com/ekino/oss/errors/handler/DataRestExceptionHandler.kt @@ -38,14 +38,18 @@ abstract class DataRestExceptionHandler( e: RepositoryConstraintViolationException, req: HttpServletRequest ): ResponseEntity { - log.debug("Constraint violation errors : ", e) val errors = e.errors.fieldErrors.map { it.toValidationErrorBody() } val globalErrors = e.errors.globalErrors.map { it.toValidationErrorBody() } return badRequest( - req.toServiceName(applicationName), INVALID_ERROR_PREFIX, e.message, e.toStacktrace(properties.displayFullStacktrace), errors, globalErrors + req.toServiceName(applicationName), + INVALID_ERROR_PREFIX, + e.message, + e.toStacktrace(properties.displayFullStacktrace), + errors, + globalErrors ).toErrorResponse() } @@ -56,7 +60,9 @@ abstract class DataRestExceptionHandler( val cause = e.cause if (cause is JDBCException) { return conflict( - req.toServiceName(applicationName), cause.sqlException.message, e.toStacktrace(properties.displayFullStacktrace) + req.toServiceName(applicationName), + cause.sqlException.message, + e.toStacktrace(properties.displayFullStacktrace) ).toErrorResponse() } return conflict(req.toServiceName(applicationName), e.message, e.toStacktrace(properties.displayFullStacktrace)).toErrorResponse() diff --git a/src/main/kotlin/com/ekino/oss/errors/handler/SecurityExceptionHandler.kt b/src/main/kotlin/com/ekino/oss/errors/handler/SecurityExceptionHandler.kt index 0c40382..8979381 100644 --- a/src/main/kotlin/com/ekino/oss/errors/handler/SecurityExceptionHandler.kt +++ b/src/main/kotlin/com/ekino/oss/errors/handler/SecurityExceptionHandler.kt @@ -29,12 +29,19 @@ abstract class SecurityExceptionHandler( ) { private val log by logger() - @ExceptionHandler(AuthenticationCredentialsNotFoundException::class, InsufficientAuthenticationException::class, UsernameNotFoundException::class, - BadCredentialsException::class) + @ExceptionHandler( + AuthenticationCredentialsNotFoundException::class, + InsufficientAuthenticationException::class, + UsernameNotFoundException::class, + BadCredentialsException::class + ) fun handleAuthenticationException(req: HttpServletRequest, e: Exception): ResponseEntity { log.debug("Authentication failed : ", e) return unAuthorized( - req.toServiceName(applicationName), "error.unauthorized", e.message, e.toStacktrace(properties.displayFullStacktrace) + req.toServiceName(applicationName), + "error.unauthorized", + e.message, + e.toStacktrace(properties.displayFullStacktrace) ).toErrorResponse() } @@ -42,7 +49,10 @@ abstract class SecurityExceptionHandler( fun handleAccessDeniedException(req: HttpServletRequest, e: Exception): ResponseEntity { log.debug("Access denied", e) return forbidden( - req.toServiceName(applicationName), "error.access_denied", e.message, e.toStacktrace(properties.displayFullStacktrace) + req.toServiceName(applicationName), + "error.access_denied", + e.message, + e.toStacktrace(properties.displayFullStacktrace) ).toErrorResponse() } @@ -50,7 +60,10 @@ abstract class SecurityExceptionHandler( fun handleDisabledException(req: HttpServletRequest, e: Exception): ResponseEntity { log.debug("Disable account : ", e) return forbidden( - req.toServiceName(applicationName), "error.disabled_account", e.message, e.toStacktrace(properties.displayFullStacktrace) + req.toServiceName(applicationName), + "error.disabled_account", + e.message, + e.toStacktrace(properties.displayFullStacktrace) ).toErrorResponse() } @@ -58,7 +71,10 @@ abstract class SecurityExceptionHandler( fun handleFirewallException(req: HttpServletRequest, e: Exception): ResponseEntity { log.debug("Access denied", e) return forbidden( - req.toServiceName(applicationName), "error.request_rejected", e.message, e.toStacktrace(properties.displayFullStacktrace) + req.toServiceName(applicationName), + "error.request_rejected", + e.message, + e.toStacktrace(properties.displayFullStacktrace) ).toErrorResponse() } } diff --git a/src/main/kotlin/com/ekino/oss/errors/handler/TxExceptionHandler.kt b/src/main/kotlin/com/ekino/oss/errors/handler/TxExceptionHandler.kt index 236a090..b782d37 100644 --- a/src/main/kotlin/com/ekino/oss/errors/handler/TxExceptionHandler.kt +++ b/src/main/kotlin/com/ekino/oss/errors/handler/TxExceptionHandler.kt @@ -30,7 +30,9 @@ abstract class TxExceptionHandler( val cause = e.cause if (cause is JDBCException) { return conflict( - req.toServiceName(applicationName), cause.sqlException.message, e.toStacktrace(properties.displayFullStacktrace) + req.toServiceName(applicationName), + cause.sqlException.message, + e.toStacktrace(properties.displayFullStacktrace) ).toErrorResponse() } return conflict(req.toServiceName(applicationName), e.message, e.toStacktrace(properties.displayFullStacktrace)).toErrorResponse() diff --git a/src/securityTest/kotlin/com/ekino/oss/errors/SecurityExceptionHandlerTest.kt b/src/securityTest/kotlin/com/ekino/oss/errors/SecurityExceptionHandlerTest.kt index e18d885..fedf21e 100644 --- a/src/securityTest/kotlin/com/ekino/oss/errors/SecurityExceptionHandlerTest.kt +++ b/src/securityTest/kotlin/com/ekino/oss/errors/SecurityExceptionHandlerTest.kt @@ -23,8 +23,10 @@ class RestExceptionHandlerTest( fun `should get access denied error`() { mockMvc.perform(get("$RESOLVED_ERROR_PATH/accessDenied")) .andExpect(status().isForbidden) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 403, "code": "error.access_denied", @@ -36,16 +38,20 @@ class RestExceptionHandlerTest( "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should get credentials not found error`() { mockMvc.perform(get("$RESOLVED_ERROR_PATH/noCredentials")) .andExpect(status().isUnauthorized) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 401, "code": "error.unauthorized", @@ -57,16 +63,20 @@ class RestExceptionHandlerTest( "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should get insufficient credentials error`() { mockMvc.perform(get("$RESOLVED_ERROR_PATH/insufficientCredentials")) .andExpect(status().isUnauthorized) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 401, "code": "error.unauthorized", @@ -78,18 +88,24 @@ class RestExceptionHandlerTest( "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should get username not found error`() { - mockMvc.perform(post("$RESOLVED_ERROR_PATH/username-not-found-error") + mockMvc.perform( + post("$RESOLVED_ERROR_PATH/username-not-found-error") .contentType(MediaType.APPLICATION_JSON) - .content("{}")) + .content("{}") + ) .andExpect(status().isUnauthorized) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 401, "code": "error.unauthorized", @@ -101,16 +117,20 @@ class RestExceptionHandlerTest( "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should get disabled account error`() { mockMvc.perform(get("$RESOLVED_ERROR_PATH/disabledAccount")) .andExpect(status().isForbidden) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 403, "code": "error.disabled_account", @@ -122,16 +142,20 @@ class RestExceptionHandlerTest( "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should get request rejected error`() { mockMvc.perform(get("$RESOLVED_ERROR_PATH/requestRejected")) .andExpect(status().isForbidden) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 403, "code": "error.request_rejected", @@ -143,16 +167,20 @@ class RestExceptionHandlerTest( "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should get bad credentials error`() { mockMvc.perform(get("$RESOLVED_ERROR_PATH/badCredentials")) .andExpect(status().isUnauthorized) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 401, "code": "error.unauthorized", @@ -164,7 +192,9 @@ class RestExceptionHandlerTest( "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """) - )) + """ + ) + ) + ) } } diff --git a/src/test/kotlin/com/ekino/oss/errors/RestExceptionHandlerTest.kt b/src/test/kotlin/com/ekino/oss/errors/RestExceptionHandlerTest.kt index 9744b44..a9cdc4b 100644 --- a/src/test/kotlin/com/ekino/oss/errors/RestExceptionHandlerTest.kt +++ b/src/test/kotlin/com/ekino/oss/errors/RestExceptionHandlerTest.kt @@ -7,7 +7,10 @@ import org.springframework.boot.autoconfigure.ImportAutoConfiguration 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.request.MockMvcRequestBuilders.* +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete import org.springframework.test.web.servlet.result.MockMvcResultMatchers import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import java.util.UUID @@ -32,8 +35,10 @@ class RestExceptionHandlerTest { fun `should get missing parameter error`() { mockMvc.perform(get("$ROOT_PATH/ok")) .andExpect(status().isBadRequest) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 400, "code": "error.missing_parameter", @@ -45,18 +50,24 @@ class RestExceptionHandlerTest { "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should post with validation errors`() { - mockMvc.perform(post("$ROOT_PATH/ok") + mockMvc.perform( + post("$ROOT_PATH/ok") .contentType(MediaType.APPLICATION_JSON) - .content("""{"message":"a", "internalBody":{}}""")) + .content("""{"message":"a", "internalBody":{}}""") + ) .andExpect(status().isBadRequest) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 400, "code": "error.invalid.post_body", @@ -79,18 +90,24 @@ class RestExceptionHandlerTest { "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should get not readable error`() { - mockMvc.perform(post("$ROOT_PATH/ok") + mockMvc.perform( + post("$ROOT_PATH/ok") .contentType(MediaType.APPLICATION_JSON) - .content("{")) + .content("{") + ) .andExpect(status().isBadRequest) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 400, "code": "error.not_readable_json", @@ -102,8 +119,10 @@ class RestExceptionHandlerTest { "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test @@ -116,7 +135,10 @@ class RestExceptionHandlerTest { ) .andExpect(status().isBadRequest) // language=json - .andExpect(MockMvcResultMatchers.content().string(jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 400, "code": "error.not_readable_json", @@ -128,15 +150,20 @@ class RestExceptionHandlerTest { "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()))) + """.trimIndent() + ) + ) + ) } @Test fun `should get argument type mismatch error`() { mockMvc.perform(get("$ROOT_PATH/ok?id=1234")) .andExpect(status().isBadRequest) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 400, "code": "error.argument_type_mismatch", @@ -148,16 +175,20 @@ class RestExceptionHandlerTest { "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should get method not supported method error`() { mockMvc.perform(delete("$ROOT_PATH/ok")) .andExpect(status().isMethodNotAllowed) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 405, "code": "error.method_not_allowed", @@ -169,16 +200,20 @@ class RestExceptionHandlerTest { "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should get unexpected error`() { mockMvc.perform(get("$RESOLVED_ERROR_PATH/unexpected")) .andExpect(status().isInternalServerError) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 500, "code": "error.illegal_argument_exception", @@ -190,16 +225,20 @@ class RestExceptionHandlerTest { "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should get custom exception error`() { mockMvc.perform(get("$RESOLVED_ERROR_PATH/custom")) .andExpect(status().isGone) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 410, "code": "error.my_exception", @@ -211,16 +250,20 @@ class RestExceptionHandlerTest { "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should get unavailable error`() { mockMvc.perform(get("$RESOLVED_ERROR_PATH/unavailable")) .andExpect(status().isServiceUnavailable) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 503, "code": "error.unavailable", @@ -232,17 +275,23 @@ class RestExceptionHandlerTest { "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should get nested exception error`() { - mockMvc.perform(get("$RESOLVED_ERROR_PATH/nested-constraint-violation-error") - .accept(MediaType.APPLICATION_JSON)) + mockMvc.perform( + get("$RESOLVED_ERROR_PATH/nested-constraint-violation-error") + .accept(MediaType.APPLICATION_JSON) + ) .andExpect(status().isBadRequest) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 400, "code": "error.invalid", @@ -260,17 +309,23 @@ class RestExceptionHandlerTest { "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should get constraint exception error`() { - mockMvc.perform(get("$RESOLVED_ERROR_PATH/constraint-violation-error") - .accept(MediaType.APPLICATION_JSON)) + mockMvc.perform( + get("$RESOLVED_ERROR_PATH/constraint-violation-error") + .accept(MediaType.APPLICATION_JSON) + ) .andExpect(status().isBadRequest) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 400, "code": "error.invalid", @@ -288,16 +343,20 @@ class RestExceptionHandlerTest { "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should get not found error bis`() { mockMvc.perform(get("$RESOLVED_ERROR_PATH/no-handler-found-error")) .andExpect(status().isNotFound) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 404, "code": "error.not_found", @@ -309,17 +368,23 @@ class RestExceptionHandlerTest { "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should post with not supported media type`() { - mockMvc.perform(post("$ROOT_PATH/ok") - .contentType(MediaType.APPLICATION_PDF)) + mockMvc.perform( + post("$ROOT_PATH/ok") + .contentType(MediaType.APPLICATION_PDF) + ) .andExpect(status().isUnsupportedMediaType) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 415, "code": "error.media_type_not_supported", @@ -331,24 +396,32 @@ class RestExceptionHandlerTest { "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should post with unknown enum value in field`() { - mockMvc.perform(post("$ROOT_PATH/body-with-enum") + mockMvc.perform( + post("$ROOT_PATH/body-with-enum") .contentType(MediaType.APPLICATION_JSON_VALUE) // language=json - .content(""" + .content( + """ { "color": "DOG" } - """.trimIndent())) + """.trimIndent() + ) + ) .andExpect(status().isBadRequest) - .andExpect(MockMvcResultMatchers.content().string( + .andExpect( + MockMvcResultMatchers.content().string( // language=json - jsonMatcher(""" + jsonMatcher( + """ { "status": 400, "code": "error.invalid.enum", @@ -366,26 +439,34 @@ class RestExceptionHandlerTest { "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should post with unknown enum value in object`() { - mockMvc.perform(post("$ROOT_PATH/body-with-enum") + mockMvc.perform( + post("$ROOT_PATH/body-with-enum") .contentType(MediaType.APPLICATION_JSON_VALUE) // language=json - .content(""" + .content( + """ { "myObject": { "color": "DOG" } } - """.trimIndent())) + """.trimIndent() + ) + ) .andExpect(status().isBadRequest) - .andExpect(MockMvcResultMatchers.content().string( + .andExpect( + MockMvcResultMatchers.content().string( // language=json - jsonMatcher(""" + jsonMatcher( + """ { "status": 400, "code": "error.invalid.enum", @@ -403,24 +484,32 @@ class RestExceptionHandlerTest { "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should post with unknown enum value in array`() { - mockMvc.perform(post("$ROOT_PATH/body-with-enum") + mockMvc.perform( + post("$ROOT_PATH/body-with-enum") .contentType(MediaType.APPLICATION_JSON_VALUE) // language=json - .content(""" + .content( + """ { "array": ["BLUE", "DOG", "CAT"] } - """.trimIndent())) + """.trimIndent() + ) + ) .andExpect(status().isBadRequest) - .andExpect(MockMvcResultMatchers.content().string( + .andExpect( + MockMvcResultMatchers.content().string( // language=json - jsonMatcher(""" + jsonMatcher( + """ { "status": 400, "code": "error.invalid.enum", @@ -438,19 +527,25 @@ class RestExceptionHandlerTest { "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should get with unknown enum param`() { - mockMvc.perform(get("$ROOT_PATH/param-enum") + mockMvc.perform( + get("$ROOT_PATH/param-enum") .contentType(MediaType.APPLICATION_JSON_VALUE) - .param("color", "DOG")) + .param("color", "DOG") + ) .andExpect(status().isBadRequest) - .andExpect(MockMvcResultMatchers.content().string( + .andExpect( + MockMvcResultMatchers.content().string( // language=json - jsonMatcher(""" + jsonMatcher( + """ { "status": 400, "code": "error.invalid.enum", @@ -468,18 +563,24 @@ class RestExceptionHandlerTest { "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } @Test fun `should get with unknown enum path`() { - mockMvc.perform(get("$ROOT_PATH/colors/DOG") - .contentType(MediaType.APPLICATION_JSON_VALUE)) + mockMvc.perform( + get("$ROOT_PATH/colors/DOG") + .contentType(MediaType.APPLICATION_JSON_VALUE) + ) .andExpect(status().isBadRequest) - .andExpect(MockMvcResultMatchers.content().string( + .andExpect( + MockMvcResultMatchers.content().string( // language=json - jsonMatcher(""" + jsonMatcher( + """ { "status": 400, "code": "error.invalid.enum", @@ -497,7 +598,9 @@ class RestExceptionHandlerTest { "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } } diff --git a/src/test/kotlin/com/ekino/oss/errors/TestResource.kt b/src/test/kotlin/com/ekino/oss/errors/TestResource.kt index 2f642ba..bc87a24 100644 --- a/src/test/kotlin/com/ekino/oss/errors/TestResource.kt +++ b/src/test/kotlin/com/ekino/oss/errors/TestResource.kt @@ -36,10 +36,16 @@ class TestResource { fun getOk(@RequestParam id: UUID): ResponseEntity = ResponseEntity.ok("OK") @PostMapping("/ok", consumes = [APPLICATION_JSON_VALUE]) - fun postOk(@RequestBody @Valid body: PostBody): ResponseEntity = ResponseEntity.ok(body.message!!) + fun postOk( + @RequestBody @Valid +body: PostBody + ): ResponseEntity = ResponseEntity.ok(body.message!!) @PutMapping("/ok") - fun putOk(@RequestBody @Valid body: NonNullablePutBody) = ResponseEntity.ok(body.message) + fun putOk( + @RequestBody @Valid +body: NonNullablePutBody + ) = ResponseEntity.ok(body.message) @GetMapping("$ERROR_PATH/unexpected") fun unexpectedError(): ResponseEntity = throw IllegalArgumentException() diff --git a/src/txTest/kotlin/com/ekino/oss/errors/RestExceptionHandlerTest.kt b/src/txTest/kotlin/com/ekino/oss/errors/RestExceptionHandlerTest.kt index 82af23e..3424ceb 100644 --- a/src/txTest/kotlin/com/ekino/oss/errors/RestExceptionHandlerTest.kt +++ b/src/txTest/kotlin/com/ekino/oss/errors/RestExceptionHandlerTest.kt @@ -20,12 +20,16 @@ class RestExceptionHandlerTest( @Test fun `should get conflict error`() { - mockMvc.perform(post("$RESOLVED_ERROR_PATH/conflict") + mockMvc.perform( + post("$RESOLVED_ERROR_PATH/conflict") .contentType(MediaType.APPLICATION_JSON) - .content("{\"message\":\"a\", \"internalBody\":{}}")) + .content("{\"message\":\"a\", \"internalBody\":{}}") + ) .andExpect(status().isConflict) - .andExpect(MockMvcResultMatchers.content().string( - jsonMatcher(""" + .andExpect( + MockMvcResultMatchers.content().string( + jsonMatcher( + """ { "status": 409, "code": "error.conflict", @@ -37,7 +41,9 @@ class RestExceptionHandlerTest( "stacktrace": "", "timestamp": "{#date_time_format:iso_instant#}" } - """.trimIndent()) - )) + """.trimIndent() + ) + ) + ) } }