Add custom-validation, etag-caching, restclient-basic-auth: Boot 4.1 API pass

Three companion modules verifying and rewriting the Boot 4.1.1 / Framework
7.0.9 story for three older articles: the javax->jakarta.validation namespace
fix plus Jakarta Validation 3.1 record-validation clarification, ETag/
conditional-request APIs re-verified unchanged plus the starter rename, and
RestTemplate Basic Auth rebuilt on RestClient with the exchange() trap called
out. 19 real passing tests generate every transcript quoted from the three
companion articles.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EQNA6DJ9VgCtW6zhCE8Xud
This commit is contained in:
Claude
2026-09-19 10:17:09 +00:00
parent 03bdf7ee87
commit e4b5636f7c
75 changed files with 2374 additions and 0 deletions
@@ -0,0 +1,64 @@
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\":\"[email protected]\",\"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\":\"[email protected]\",\"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");
}
}