diff --git a/src/intTest/kotlin/mastodon/account/AccountApiTest.kt b/src/intTest/kotlin/mastodon/account/AccountApiTest.kt index a70400fb..77f785c9 100644 --- a/src/intTest/kotlin/mastodon/account/AccountApiTest.kt +++ b/src/intTest/kotlin/mastodon/account/AccountApiTest.kt @@ -141,10 +141,11 @@ class AccountApiTest { mockMvc .post("/api/v1/accounts") { contentType = MediaType.APPLICATION_FORM_URLENCODED - param("username", "api-test-user-3") + param("password", "api-test-user-3") with(csrf()) } - .andExpect { status { isBadRequest() } } + .andDo { print() } + .andExpect { status { isUnprocessableEntity() } } } @Test @@ -156,7 +157,7 @@ class AccountApiTest { param("username", "api-test-user-4") with(csrf()) } - .andExpect { status { isBadRequest() } } + .andExpect { status { isUnprocessableEntity() } } } @Test diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/springweb/MastodonApiControllerAdvice.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/springweb/MastodonApiControllerAdvice.kt index e27f1a27..d2ca2c6a 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/springweb/MastodonApiControllerAdvice.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/springweb/MastodonApiControllerAdvice.kt @@ -32,6 +32,7 @@ import org.slf4j.LoggerFactory import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.validation.BindException +import org.springframework.validation.FieldError import org.springframework.web.bind.annotation.ControllerAdvice import org.springframework.web.bind.annotation.ExceptionHandler @@ -52,23 +53,43 @@ class MastodonApiControllerAdvice { @ExceptionHandler(BindException::class) fun handleException(ex: BindException): ResponseEntity { logger.debug("Failed bind entity.", ex) - val error = ex.bindingResult.fieldErrors - val message = error.map { - "${it.field} ${it.defaultMessage}" - }.joinToString(prefix = "Validation failed: ") - val details = error.associate { - it.field to UnprocessableEntityResponseDetails( - when (it.code) { - "Email" -> "ERR_INVALID" - "Pattern" -> "ERR_INVALID" - else -> "ERR_INVALID" - }, - it.defaultMessage - ) + val details = mutableMapOf>() + + ex.allErrors.forEach { + val defaultMessage = it.defaultMessage + when { + it is FieldError -> { + val code = when (it.code) { + "Email" -> "ERR_INVALID" + "Pattern" -> "ERR_INVALID" + else -> "ERR_INVALID" + } + details.getOrPut(it.field) { + mutableListOf() + }.add(UnprocessableEntityResponseDetails(code, defaultMessage.orEmpty())) + } + + defaultMessage?.startsWith("Parameter specified as non-null is null:") == true -> { + val parameter = defaultMessage.substringAfterLast("parameter ") + + details.getOrPut(parameter) { + mutableListOf() + }.add(UnprocessableEntityResponseDetails("ERR_BLANK", "can't be blank")) + } + + else -> { + logger.warn("Unknown validation error", ex) + } + } } - return ResponseEntity.unprocessableEntity().body(UnprocessableEntityResponse(message, details)) + val message = details.map { + it.key + " " + it.value.joinToString { it.description } + }.joinToString() + + return ResponseEntity.unprocessableEntity() + .body(UnprocessableEntityResponse(message, details)) } @ExceptionHandler(StatusNotFoundException::class) diff --git a/src/main/resources/openapi/mastodon.yaml b/src/main/resources/openapi/mastodon.yaml index 66fb6704..e104eff7 100644 --- a/src/main/resources/openapi/mastodon.yaml +++ b/src/main/resources/openapi/mastodon.yaml @@ -1455,6 +1455,8 @@ components: type: object properties: username: + + nullable: false type: string minLength: 1 maxLength: 300 @@ -2841,9 +2843,11 @@ components: error: type: string details: - type: object + type: array additionalProperties: - $ref: "#/components/schemas/UnprocessableEntityResponseDetails" + type: array + items: + $ref: "#/components/schemas/UnprocessableEntityResponseDetails" UnprocessableEntityResponseDetails: type: object @@ -2852,6 +2856,10 @@ components: type: string description: type: string + nullable: false + required: + - error + - description UnauthorizedResponse: type: object diff --git a/templates/beanValidationModel.mustache b/templates/beanValidationModel.mustache index 99635314..0d4b99fa 100644 --- a/templates/beanValidationModel.mustache +++ b/templates/beanValidationModel.mustache @@ -35,4 +35,5 @@ isLong set Not Integer, not Long => we have a decimal value! }}{{^isInteger}}{{^isLong}}{{#minimum}} @get:DecimalMin("{{.}}"){{/minimum}}{{#maximum}} - @get:DecimalMax("{{.}}"){{/maximum}}{{/isLong}}{{/isInteger}} \ No newline at end of file + @get:DecimalMax("{{.}}"){{/maximum}}{{/isLong}}{{/isInteger}} + diff --git a/templates/dataClassReqVar.mustache b/templates/dataClassReqVar.mustache index 1812c1cf..9ae7d3a9 100644 --- a/templates/dataClassReqVar.mustache +++ b/templates/dataClassReqVar.mustache @@ -1,4 +1,4 @@ {{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}} @Schema({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}} - @ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}} + @ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}}{{^isNullable}}@get:NotNull{{/isNullable}} @get:JsonProperty("{{{baseName}}}", required = true){{#isInherited}} override{{/isInherited}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInCamelCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}}?{{/isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}