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,107 @@
package com.ankurm.restclientbasicauth;
import com.ankurm.restclientbasicauth.client.ApiClient;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatusCode;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestClient;
import static org.assertj.core.api.Assertions.assertThat;
/**
* A real embedded Tomcat on a random port, a real Spring Security filter chain, and a real
* RestClient making real HTTP calls over loopback -- no MockMvc here, because the point is to
* prove the client actually authenticates over the wire, the same way the original article's
* CommandLineRunner did against RestTemplate.
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class RestClientBasicAuthTest {
@LocalServerPort
int port;
@Autowired
ApiClient apiClient;
@Autowired
RestClient.Builder builder;
private String baseUrl() {
return "http://localhost:" + port;
}
@Test
void defaultHeadersBasicAuthSucceeds() {
String response = apiClient.callSecuredEndpointViaDefaultHeaders(baseUrl());
assertThat(response).isEqualTo("Hello, you have accessed a secured endpoint!");
Transcript.write("01-restclient-basic-auth-default-headers.txt",
"// RestClient.Builder builder = ...;\n"
+ "// RestClient client = builder.defaultHeaders(h -> h.setBasicAuth(\"admin\", \"password123\")).build();\n"
+ "// client.get().uri(baseUrl + \"/api/hello\").retrieve().body(String.class);\n\n"
+ "Response: " + response + "\n");
}
@Test
void interceptorBasicAuthSucceeds() {
String response = apiClient.callSecuredEndpointViaInterceptor(baseUrl());
assertThat(response).isEqualTo("Hello, you have accessed a secured endpoint!");
Transcript.write("02-restclient-basic-auth-interceptor.txt",
"// RestClient.Builder builder = ...;\n"
+ "// RestClient client = builder.requestInterceptor(\n"
+ "// new BasicAuthenticationInterceptor(\"admin\", \"password123\")).build();\n"
+ "// client.get().uri(baseUrl + \"/api/hello\").retrieve().body(String.class);\n\n"
+ "Response: " + response + "\n");
}
@Test
void noCredentialsGets401() {
RestClient noAuthClient = builder.build(); // the auto-configured builder, no basic auth added
HttpClientErrorException ex = catchHttpClientErrorException(() ->
noAuthClient.get().uri(baseUrl() + "/api/hello").retrieve().body(String.class));
assertThat(ex.getStatusCode()).isEqualTo(org.springframework.http.HttpStatus.UNAUTHORIZED);
Transcript.write("03-restclient-no-credentials-401.txt",
"// RestClient client = builder.build(); // no basic auth\n"
+ "// client.get().uri(baseUrl + \"/api/hello\").retrieve().body(String.class);\n\n"
+ "Thrown: " + ex.getClass().getName() + "\n"
+ "Status: " + ex.getStatusCode() + "\n"
+ "\n# retrieve() throws HttpClientErrorException on 4xx by default -- same default as\n"
+ "# RestTemplate, unlike RestClient's own exchange() method, which disables that default.\n");
}
@Test
void wrongPasswordAlsoGets401() {
RestClient wrongPasswordClient = builder
.defaultHeaders(h -> h.setBasicAuth("admin", "not-the-password"))
.build();
HttpClientErrorException ex = catchHttpClientErrorException(() ->
wrongPasswordClient.get().uri(baseUrl() + "/api/hello").retrieve().body(String.class));
assertThat(ex.getStatusCode()).isEqualTo(org.springframework.http.HttpStatus.UNAUTHORIZED);
Transcript.write("04-restclient-wrong-password-401.txt",
"Thrown: " + ex.getClass().getName() + "\n"
+ "Status: " + ex.getStatusCode() + "\n");
}
private interface ThrowingRunnable {
void run();
}
private HttpClientErrorException catchHttpClientErrorException(ThrowingRunnable runnable) {
try {
runnable.run();
} catch (HttpClientErrorException e) {
return e;
}
throw new AssertionError("Expected HttpClientErrorException but none was thrown");
}
}
@@ -0,0 +1,21 @@
package com.ankurm.restclientbasicauth;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
final class Transcript {
private Transcript() {
}
static void write(String fileName, String content) {
try {
Path out = Paths.get("docs", "output", fileName);
Files.createDirectories(out.getParent());
Files.writeString(out, content);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}