package com.ankurm.customvalidation; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.test.web.servlet.MockMvc; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * What the default in {@link ValidationScenariosTest#recordBasedSpamMessageRejectedButWithNoErrorBodyByDefault} * does not do: emit a structured error body for a validation failure that has no BindingResult to * carry it. That structured body exists, but it is opt-in behind * {@code spring.mvc.problemdetails.enabled=true} -- unrelated to Bean Validation 3.1 itself, and * a property that already existed in Boot 3, but worth verifying directly rather than assuming it * changes the earlier empty-body result, because it does not touch content negotiation, only * whether a body is produced at all. */ @SpringBootTest(properties = "spring.mvc.problemdetails.enabled=true") @AutoConfigureMockMvc class ProblemDetailsEnabledTest { @Autowired MockMvc mockMvc; @Test void recordValidationFailureNowGetsAProblemDetailBody() throws Exception { var result = mockMvc.perform(post("/contact-record") .contentType("application/json") .content("{\"email\":\"spammy@email.com\",\"message\":\"This is spam.\"}")) .andExpect(status().isBadRequest()) .andReturn(); String body = result.getResponse().getContentAsString(); // A real body now exists -- progress over the empty one -- but it is a GENERIC // ProblemDetail with no mention of "spam" or which field failed. Spring's default // MethodArgumentNotValidException -> ProblemDetail mapping does not populate per-field // messages for you; that needs a custom @ExceptionHandler (or ResponseEntityExceptionHandler // override) that reads bindingResult.getFieldErrors() into the ProblemDetail's properties. assertThat(body).isNotEmpty(); assertThat(body).doesNotContain("spam"); assertThat(body).contains("\"status\":400"); Transcript.write("07-problemdetails-enabled-record-rejected.txt", "# application.yaml: spring.mvc.problemdetails.enabled: true (opt-in; unrelated to Bean\n" + "# Validation 3.1 itself -- this property already existed in Boot 3)\n\n" + "$ curl -s -X POST localhost:8080/contact-record \\\n" + " -H 'Content-Type: application/json' \\\n" + " -d '{\"email\":\"spammy@email.com\",\"message\":\"This is spam.\"}'\n\n" + "HTTP status: " + result.getResponse().getStatus() + "\n" + "Content-Type: " + result.getResponse().getContentType() + "\n" + "Body: " + body + "\n" + "\n# Progress over the empty body, but notice what is MISSING: no mention of \"spam\", no field\n" + "# name. Boot's default MethodArgumentNotValidException -> ProblemDetail mapping fills in\n" + "# only the generic RFC 9457 fields (title, status, detail=\"Invalid request content.\").\n" + "# Per-field messages -- what the class-based /contact endpoint hand-rolls from\n" + "# bindingResult.getFieldErrors() -- need a custom @ExceptionHandler that does the same\n" + "# thing into the ProblemDetail's own \"properties\" map. Turning the property on is not,\n" + "# by itself, a drop-in replacement for BindingResult-based error reporting.\n"); } }