Merge branch '3.5.x' into 4.0.x

Closes gh-50017
This commit is contained in:
Andy Wilkinson
2026-04-10 12:28:27 +01:00
4 changed files with 93 additions and 18 deletions
@@ -162,15 +162,40 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
.jsonPath("_links.length()")
.isEqualTo(3)
.jsonPath("_links.self.href")
.isNotEmpty()
.value(isLinkTo("/endpoints"))
.jsonPath("_links.self.templated")
.isEqualTo(false)
.jsonPath("_links.test.href")
.isNotEmpty()
.value(isLinkTo("/endpoints/test"))
.jsonPath("_links.test.templated")
.isEqualTo(false)
.jsonPath("_links.test-part.href")
.isNotEmpty()
.value(isLinkTo("/endpoints/test/{part}"))
.jsonPath("_links.test-part.templated")
.isEqualTo(true));
}
@Test
void whenRequestHasAQueryStringLinksToOtherEndpointsDoNotHaveAQueryString() {
load(TestEndpointConfiguration.class,
(client) -> client.get()
.uri("?a=alpha")
.exchange()
.expectStatus()
.isOk()
.expectBody()
.jsonPath("_links.length()")
.isEqualTo(3)
.jsonPath("_links.self.href")
.value(isLinkTo("/endpoints"))
.jsonPath("_links.self.templated")
.isEqualTo(false)
.jsonPath("_links.test.href")
.value(isLinkTo("/endpoints/test"))
.jsonPath("_links.test.templated")
.isEqualTo(false)
.jsonPath("_links.test-part.href")
.value(isLinkTo("/endpoints/test/{part}"))
.jsonPath("_links.test-part.templated")
.isEqualTo(true));
}
@@ -668,6 +693,10 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
(client) -> client.get().uri("/customstatus").exchange().expectStatus().isEqualTo(234));
}
private Consumer<Object> isLinkTo(String target) {
return (href) -> assertThat(href).asString().doesNotContain("?").endsWith(target);
}
protected abstract int getPort(T context);
protected void validateErrorBody(WebTestClient.BodyContentSpec body, HttpStatus status, String path,
@@ -50,6 +50,7 @@ import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
/**
* A custom {@link RequestMappingInfoHandlerMapping} that makes web endpoints available on
@@ -111,8 +112,9 @@ class CloudFoundryWebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointH
return new ResponseEntity<>(securityResponse.getStatus());
}
AccessLevel accessLevel = exchange.getAttribute(AccessLevel.REQUEST_ATTRIBUTE);
String requestUri = UriComponentsBuilder.fromUri(request.getURI()).replaceQuery(null).toUriString();
Map<String, Link> links = CloudFoundryWebFluxEndpointHandlerMapping.this.linksResolver
.resolveLinks(request.getURI().toString());
.resolveLinks(requestUri);
return new ResponseEntity<>(
Collections.singletonMap("_links", getAccessibleLinks(accessLevel, links)), HttpStatus.OK);
});
@@ -147,23 +147,23 @@ class CloudFoundryWebFluxEndpointIntegrationTests {
.jsonPath("_links.length()")
.isEqualTo(5)
.jsonPath("_links.self.href")
.isNotEmpty()
.value(isLinkTo("/cfApplication"))
.jsonPath("_links.self.templated")
.isEqualTo(false)
.jsonPath("_links.info.href")
.isNotEmpty()
.value(isLinkTo("/cfApplication/info"))
.jsonPath("_links.info.templated")
.isEqualTo(false)
.jsonPath("_links.env.href")
.isNotEmpty()
.value(isLinkTo("/cfApplication/env"))
.jsonPath("_links.env.templated")
.isEqualTo(false)
.jsonPath("_links.test.href")
.isNotEmpty()
.value(isLinkTo("/cfApplication/test"))
.jsonPath("_links.test.templated")
.isEqualTo(false)
.jsonPath("_links.test-part.href")
.isNotEmpty()
.value(isLinkTo("/cfApplication/test/{part}"))
.jsonPath("_links.test-part.templated")
.isEqualTo(true)));
}
@@ -197,11 +197,11 @@ class CloudFoundryWebFluxEndpointIntegrationTests {
.jsonPath("_links.length()")
.isEqualTo(2)
.jsonPath("_links.self.href")
.isNotEmpty()
.value(isLinkTo("/cfApplication"))
.jsonPath("_links.self.templated")
.isEqualTo(false)
.jsonPath("_links.info.href")
.isNotEmpty()
.value(isLinkTo("/cfApplication/info"))
.jsonPath("_links.info.templated")
.isEqualTo(false)
.jsonPath("_links.env")
@@ -212,12 +212,34 @@ class CloudFoundryWebFluxEndpointIntegrationTests {
.doesNotExist()));
}
@Test
void whenRequestHasAQueryStringLinksToOtherEndpointsDoNotHaveAQueryString() {
given(this.tokenValidator.validate(any())).willReturn(Mono.empty());
given(this.securityService.getAccessLevel(any(), eq("app-id"))).willReturn(Mono.just(AccessLevel.RESTRICTED));
this.contextRunner.run(withWebTestClient((client) -> client.get()
.uri("/cfApplication?x=1")
.accept(MediaType.APPLICATION_JSON)
.header("Authorization", "bearer " + mockAccessToken())
.exchange()
.expectStatus()
.isOk()
.expectBody()
.jsonPath("_links.self.href")
.value(isLinkTo("/cfApplication"))
.jsonPath("_links.info.href")
.value(isLinkTo("/cfApplication/info"))));
}
@Test
void unknownEndpointsAreForbidden() {
this.contextRunner.run(withWebTestClient(
(client) -> client.get().uri("/cfApplication/unknown").exchange().expectStatus().isForbidden()));
}
private Consumer<Object> isLinkTo(String target) {
return (href) -> assertThat(href).asString().doesNotContain("?").endsWith(target);
}
private ContextConsumer<AssertableReactiveWebApplicationContext> withWebTestClient(
Consumer<WebTestClient> clientConsumer) {
return (context) -> {
@@ -135,23 +135,23 @@ class CloudFoundryMvcWebEndpointIntegrationTests {
.jsonPath("_links.length()")
.isEqualTo(5)
.jsonPath("_links.self.href")
.isNotEmpty()
.value(isLinkTo("/cfApplication"))
.jsonPath("_links.self.templated")
.isEqualTo(false)
.jsonPath("_links.info.href")
.isNotEmpty()
.value(isLinkTo("/cfApplication/info"))
.jsonPath("_links.info.templated")
.isEqualTo(false)
.jsonPath("_links.env.href")
.isNotEmpty()
.value(isLinkTo("/cfApplication/env"))
.jsonPath("_links.env.templated")
.isEqualTo(false)
.jsonPath("_links.test.href")
.isNotEmpty()
.value(isLinkTo("/cfApplication/test"))
.jsonPath("_links.test.templated")
.isEqualTo(false)
.jsonPath("_links.test-part.href")
.isNotEmpty()
.value(isLinkTo("/cfApplication/test/{part}"))
.jsonPath("_links.test-part.templated")
.isEqualTo(true));
}
@@ -186,11 +186,11 @@ class CloudFoundryMvcWebEndpointIntegrationTests {
.jsonPath("_links.length()")
.isEqualTo(2)
.jsonPath("_links.self.href")
.isNotEmpty()
.value(isLinkTo("/cfApplication"))
.jsonPath("_links.self.templated")
.isEqualTo(false)
.jsonPath("_links.info.href")
.isNotEmpty()
.value(isLinkTo("/cfApplication/info"))
.jsonPath("_links.info.templated")
.isEqualTo(false)
.jsonPath("_links.env")
@@ -201,6 +201,24 @@ class CloudFoundryMvcWebEndpointIntegrationTests {
.doesNotExist());
}
@Test
void whenRequestHasAQueryStringLinksToOtherEndpointsDoNotHaveAQueryString() {
given(this.securityService.getAccessLevel(any(), eq("app-id"))).willReturn(AccessLevel.RESTRICTED);
load(TestEndpointConfiguration.class,
(client) -> client.get()
.uri("/cfApplication?x=1")
.accept(MediaType.APPLICATION_JSON)
.header("Authorization", "bearer " + mockAccessToken())
.exchange()
.expectStatus()
.isOk()
.expectBody()
.jsonPath("_links.self.href")
.value(isLinkTo("/cfApplication"))
.jsonPath("_links.info.href")
.value(isLinkTo("/cfApplication/info")));
}
@Test
void unknownEndpointsAreForbidden() {
load(TestEndpointConfiguration.class,
@@ -212,6 +230,10 @@ class CloudFoundryMvcWebEndpointIntegrationTests {
.isForbidden());
}
private Consumer<Object> isLinkTo(String target) {
return (href) -> assertThat(href).asString().doesNotContain("?").endsWith(target);
}
private void load(Class<?> configuration, Consumer<WebTestClient> clientConsumer) {
BiConsumer<ApplicationContext, WebTestClient> consumer = (context, client) -> clientConsumer.accept(client);
new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new)