Skip to content

Commit

Permalink
Mergup 3.10.x into 4.0.x (micronaut-projects#9619)
Browse files Browse the repository at this point in the history
  • Loading branch information
sdelamo committed Jul 26, 2023
1 parent 9c73f45 commit bbfd262
Show file tree
Hide file tree
Showing 4 changed files with 330 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package io.micronaut.http.server.netty.cors

import io.micronaut.context.annotation.Property
import io.micronaut.context.annotation.Requires
import io.micronaut.core.type.Argument
import io.micronaut.core.util.StringUtils
import io.micronaut.core.version.annotation.Version
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.BlockingHttpClient
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import jakarta.inject.Inject
import spock.lang.Specification

@Property(name = "spec.name", value = "CorsVersionSpec")
@Property(name = "micronaut.server.cors.enabled", value = StringUtils.TRUE)
@Property(name = "micronaut.router.versioning.enabled", value = StringUtils.TRUE)
@Property(name = "micronaut.router.versioning.default-version", value = "1")
@Property(name = "micronaut.router.versioning.header.enabled", value = StringUtils.TRUE)
@Property(name = "micronaut.router.versioning.header.names", value = "x-api-version")
@MicronautTest
class CorsVersionSpec extends Specification {
@Inject
@Client("/")
HttpClient httpClient

void "verify versioned routes behaved as expected"() {
given:
BlockingHttpClient client = httpClient.toBlocking()
HttpRequest<?> request = createRequest("/common", "1")

when:
HttpResponse<Map<String, String>> rsp = client.exchange(request, Argument.mapOf(String.class, String.class))

then:
assertResponse(rsp, "common from v1")

when:
request = createRequest("/common", null)
rsp = client.exchange(request, Argument.mapOf(String.class, String.class))

then:
assertResponse(rsp, "common from v1")

when:
request = createRequest("/common", "2")
rsp = client.exchange(request, Argument.mapOf(String.class, String.class))
then:
assertResponse(rsp, "common from v2")

when:
request = createRequest("/new", "2")
rsp = client.exchange(request, Argument.mapOf(String.class, String.class))

then:
assertResponse(rsp, "new from v2")
}

void "preflight for version routed which does not have a matching default version"() {
given:
BlockingHttpClient client = httpClient.toBlocking()

when:
MutableHttpRequest<?> request = HttpRequest.OPTIONS("/new")
preflightHeaders("x-api-version").each { k, v -> request.header(k, v)}
client.exchange(request)

then:
noExceptionThrown()
}

void "preflight for version routed without version header"() {
given:
BlockingHttpClient client = httpClient.toBlocking()

when:
MutableHttpRequest<?> request = HttpRequest.OPTIONS("/common")
preflightHeaders(null).each { k, v -> request.header(k, v)}
client.exchange(request)

then:
noExceptionThrown()

when:
request = HttpRequest.OPTIONS("/new")
preflightHeaders(null).each { k, v -> request.header(k, v)}
client.exchange(request)

then:
def ex = thrown(HttpClientResponseException)
ex.status == HttpStatus.NOT_FOUND
}

static Map<String, String> preflightHeaders(String accessControlRequestHeaders) {
Map<String, String> headers = new HashMap<>();
headers.put("Host", "localhost:8080")
headers.put("Sec-Fetch-Site", "same-site")
headers.put("Accept-Encoding", "gzip, deflate")
headers.put("Access-Control-Request-Method", "GET")
headers.put("Sec-Fetch-Mode", "cors")
headers.put("Accept-Language", "en-GB,en;q=0.9")
headers.put("Origin", "http://localhost:8000")
if (accessControlRequestHeaders) {
headers.put("Access-Control-Request-Headers", accessControlRequestHeaders)
}
headers.put("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.15")
headers.put("Referer", "http://localhost:8000/")
headers.put("Content-Length", "0")
headers.put("Connection", "keep-alive")
headers.put("Sec-Fetch-Dest", "empty")
headers.put("Accept", "*/*")
headers
}

HttpRequest<?> createRequest(String path, String apiVersion) {
return apiVersion == null ?
HttpRequest.GET(path) :
HttpRequest.GET(path).header("X-API-VERSION", apiVersion);
}

void assertResponse(HttpResponse<Map<String, String>> rsp, String expected) {
assert HttpStatus.OK == rsp.getStatus()
Map<String, String> body = rsp.body()
assert body
assert 1 == body.keySet().size()
assert expected == body.get("message")
}

@Requires(property = "spec.name", value = "CorsVersionSpec")
@Controller
static class TestController {
public static final String MESSAGE = "message";

@Get( "/common")
@Version("1")
Map<String, String> commonEndpointV1() {
return Collections.singletonMap(MESSAGE, "common from v1");
}

@Get( "/common")
@Version("2")
Map<String, String> commonEndpointV2() {
return Collections.singletonMap(MESSAGE, "common from v2");
}

@Get( "/new")
@Version("2")
Map<String, String> newEndpointV2() {
return Collections.singletonMap(MESSAGE, "new from v2");
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import io.micronaut.context.annotation.Replaces;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.core.version.annotation.Version;
import io.micronaut.http.HttpHeaders;
import io.micronaut.http.HttpMethod;
import io.micronaut.http.HttpRequest;
Expand All @@ -43,6 +45,7 @@

import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
import java.util.function.BiConsumer;

Expand Down Expand Up @@ -307,6 +310,88 @@ void accessControlMaxAgeValueIsSet() throws IOException {
.build()));
}

@Test
void versionedPreflightBehavesAsExpectedWithDefaultVersion() {
Map<String, Object> config = versionedRoutesConfig();
assertAll(
() -> {
// V1 version/common
config.put("micronaut.router.versioning.default-version", 1);
asserts(SPECNAME, config,
preflight(UriBuilder.of("/version").path("common"), "https://foo.com", HttpMethod.GET),
(server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder()
.status(HttpStatus.OK)
.assertResponse(response -> assertCorsHeaders(response, "https://foo.com", HttpMethod.GET, false))
.build()));
},
() -> {
// V2 version/common
config.put("micronaut.router.versioning.default-version", 2);
asserts(SPECNAME, config,
preflight(UriBuilder.of("/version").path("common"), "https://foo.com", HttpMethod.GET),
(server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder()
.status(HttpStatus.OK)
.assertResponse(response -> assertCorsHeaders(response, "https://foo.com", HttpMethod.GET, false))
.build()));
},
() -> {
// V2 version/new
config.put("micronaut.router.versioning.default-version", 2);
asserts(SPECNAME, config,
preflight(UriBuilder.of("/version").path("new"), "https://foo.com", HttpMethod.GET),
(server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder()
.status(HttpStatus.OK)
.assertResponse(response -> assertCorsHeaders(response, "https://foo.com", HttpMethod.GET, false))
.build()));
}
);
}

@Test
void versionedPreflightWithHeaderNoDefaultVersion() throws IOException {
Map<String, Object> config = versionedRoutesConfig();
asserts(SPECNAME, config,
preflight(UriBuilder.of("/version").path("new"), "https://foo.com", HttpMethod.GET)
.header("Access-Control-Request-Headers", "x-api-version"),
(server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder()
.status(HttpStatus.OK)
.assertResponse(response -> assertCorsHeaders(response, "https://foo.com", HttpMethod.GET, false))
.build()));
}

@Test
void versionedPreflightWhenDefaultVersionNotMatchHasHeader() throws IOException {
Map<String, Object> config = versionedRoutesConfig();
config.put("micronaut.router.versioning.default-version", 1);
asserts(SPECNAME, config,
preflight(UriBuilder.of("/version").path("new"), "https://foo.com", HttpMethod.GET)
.header("Access-Control-Request-Headers", "x-api-version"),
(server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder()
.status(HttpStatus.OK)
.assertResponse(response -> assertCorsHeaders(response, "https://foo.com", HttpMethod.GET, false))
.build()));
}

@Test
void versionedPreflightFailsWhenDefaultVersionNotMatchAndNoHeader() throws IOException {
Map<String, Object> config = versionedRoutesConfig();
config.put("micronaut.router.versioning.default-version", 1);
asserts(SPECNAME, config,
preflight(UriBuilder.of("/version").path("new"), "https://foo.com", HttpMethod.GET),
(server, request) -> AssertionUtils.assertThrows(server, request, HttpResponseAssertion.builder()
.status(HttpStatus.NOT_FOUND)
.assertResponse(CorsUtils::assertCorsHeadersNotPresent)
.build()));
}

private static Map<String, Object> versionedRoutesConfig() {
return CollectionUtils.mapOf(
"micronaut.router.versioning.enabled", StringUtils.TRUE,
"micronaut.router.versioning.header.enabled", StringUtils.TRUE,
"micronaut.router.versioning.header.names", Collections.singletonList("x-api-version")
);
}

private static MutableHttpRequest<?> preflight(UriBuilder uriBuilder, String originValue, HttpMethod method) {
return preflight(uriBuilder.build(), originValue, method);
}
Expand Down Expand Up @@ -477,6 +562,32 @@ String bar() {
}
}

@Requires(property = "spec.name", value = SPECNAME)
@CrossOrigin("https://foo.com")
@Controller("/version")
static class ApiVersionController {
@Version("1")
@Produces(MediaType.TEXT_PLAIN)
@Get(value = "common")
public String commonV1() {
return "This endpoint exists both in V1 and V2";
}

@Version("2")
@Produces(MediaType.TEXT_PLAIN)
@Get(value = "common")
public String commonV2() {
return "This endpoint exists both in V1 and V2";
}

@Version("2")
@Produces(MediaType.TEXT_PLAIN)
@Get(value = "new")
public String newV2() {
return "This is a new endpoint in V2 of the API";
}
}

@Requires(property = "spec.name", value = SPECNAME)
@Replaces(HttpHostResolver.class)
@Singleton
Expand All @@ -486,4 +597,5 @@ public String resolve(@Nullable HttpRequest request) {
return "https://micronautexample.com";
}
}

}
Loading

0 comments on commit bbfd262

Please sign in to comment.