Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add HTTP Server TCK #8499

Merged
merged 13 commits into from Dec 20, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Expand Up @@ -401,6 +401,7 @@ jsr107 = { module = "org.jsr107.ri:cache-ri-impl", version.ref = "jsr107" }

junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "managed-junit5" }
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "managed-junit5" }
junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "managed-junit5" }
junit-vintage = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "managed-junit5" }

jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" }
Expand Down
28 changes: 28 additions & 0 deletions http-server-tck/build.gradle.kts
@@ -0,0 +1,28 @@
plugins {
id("io.micronaut.build.internal.convention-library")
}
repositories {
mavenCentral()
}

dependencies {
annotationProcessor(projects.injectJava)
annotationProcessor(projects.validation)
implementation(projects.validation)
implementation(projects.runtime)
implementation(projects.inject)
api(projects.httpServer)
api(libs.junit.jupiter.api)
api(libs.junit.jupiter.params)
api(libs.managed.reactor)
}

java {
sourceCompatibility = JavaVersion.toVersion("1.8")
targetCompatibility = JavaVersion.toVersion("1.8")
}
micronautBuild {
binaryCompatibility {
enabled.set(false)
}
}
@@ -0,0 +1,117 @@
/*
* Copyright 2017-2022 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.http.server.tck;

import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.http.HttpHeaders;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.function.Executable;
import org.junit.jupiter.api.function.ThrowingSupplier;

import java.util.Map;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Utility class used to perform assertions.
* @author Sergio del Amo
* @since 3.8.0
*/
public final class AssertionUtils {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module should be internal

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, other modules may want to use it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but not outside of Micronaut org


private AssertionUtils() {

}

public static void assertThrows(@NonNull ServerUnderTest server,
@NonNull HttpRequest<?> request,
@NonNull HttpResponseAssertion assertion) {
assertThrows(server, request, assertion.getHttpStatus(), assertion.getBody(), assertion.getHeaders());
}

public static void assertThrows(@NonNull ServerUnderTest server,
@NonNull HttpRequest<?> request,
@NonNull HttpStatus expectedStatus,
@Nullable String expectedBody,
@Nullable Map<String, String> expectedHeaders) {
Executable e = expectedBody != null ?
() -> server.exchange(request, String.class) :
() -> server.exchange(request);
HttpClientResponseException thrown = Assertions.assertThrows(HttpClientResponseException.class, e);
HttpResponse<?> response = thrown.getResponse();
assertEquals(expectedStatus, response.getStatus());
assertHeaders(response, expectedHeaders);
assertBody(response, expectedBody);
}

public static <T> void assertDoesNotThrow(@NonNull ServerUnderTest server,
@NonNull HttpRequest<T> request,
@NonNull HttpResponseAssertion assertion) {
assertDoesNotThrow(server, request, assertion.getHttpStatus(), assertion.getBody(), assertion.getHeaders());
}

public static <T> void assertDoesNotThrow(@NonNull ServerUnderTest server,
@NonNull HttpRequest<T> request,
@NonNull HttpStatus expectedStatus,
@Nullable String expectedBody,
@Nullable Map<String, String> expectedHeaders) {
ThrowingSupplier<HttpResponse<?>> executable = expectedBody != null ?
() -> server.exchange(request, String.class) :
() -> server.exchange(request);
HttpResponse<?> response = Assertions.assertDoesNotThrow(executable);
assertEquals(expectedStatus, response.getStatus());
assertHeaders(response, expectedHeaders);
assertBody(response, expectedBody);
}

private static void assertBody(@NonNull HttpResponse<?> response, @Nullable String expectedBody) {
if (expectedBody != null) {
Optional<String> bodyOptional = response.getBody(String.class);
assertTrue(bodyOptional.isPresent());
assertTrue(bodyOptional.get().contains(expectedBody));
}
}

private static void assertHeaders(@NonNull HttpResponse<?> response, @Nullable Map<String, String> expectedHeaders) {

if (expectedHeaders != null) {
for (String headerName : expectedHeaders.keySet()) {
Optional<String> headerOptional = response.getHeaders().getFirst(headerName);
assertTrue(headerOptional.isPresent(), () -> "Header " + headerName + " not present");
String headerValue = headerOptional.get();
String expectedValue = expectedHeaders.get(headerName);
if (headerName.equals(HttpHeaders.CONTENT_TYPE)) {
if (headerValue.contains(";charset=")) {
assertTrue(headerValue.startsWith(expectedValue), () -> "header value " + headerValue + " does not start with " + expectedValue);
} else {
assertEquals(expectedValue, headerOptional.get());
}
} else {
assertEquals(expectedValue, headerOptional.get());
}

}
}
}

}
@@ -0,0 +1,87 @@
/*
* Copyright 2017-2022 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.http.server.tck;

import io.micronaut.context.ApplicationContext;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.client.BlockingHttpClient;
import io.micronaut.http.client.HttpClient;
import io.micronaut.runtime.server.EmbeddedServer;

import java.io.IOException;
import java.util.Map;
import java.util.Optional;

/**
* {@link ServerUnderTest} implementation for {@link EmbeddedServer}.
* @author Sergio del Amo
* @since 3.0.0
*/
public class EmbeddedServerUnderTest implements ServerUnderTest {

private EmbeddedServer embeddedServer;
private HttpClient httpClient;
private BlockingHttpClient client;

public EmbeddedServerUnderTest(@NonNull Map<String, Object> properties) {
this.embeddedServer = ApplicationContext.run(EmbeddedServer.class, properties);
}

@Override
public <I, O> HttpResponse<O> exchange(HttpRequest<I> request, Argument<O> bodyType) {
return getBlockingHttpClient().exchange(request, bodyType);
}

@Override
public ApplicationContext getApplicationContext() {
return embeddedServer.getApplicationContext();
}

@Override
public void close() throws IOException {
if (httpClient != null) {
httpClient.close();
}
if (embeddedServer != null) {
embeddedServer.close();
}
}

@Override
@NonNull
public Optional<Integer> getPort() {
return Optional.ofNullable(embeddedServer).map(EmbeddedServer::getPort);
}

@NonNull
private HttpClient getHttpClient() {
if (httpClient == null) {
this.httpClient = getApplicationContext().createBean(HttpClient.class, embeddedServer.getURL());
}
return httpClient;
}

@NonNull
private BlockingHttpClient getBlockingHttpClient() {
if (client == null) {
this.client = getHttpClient().toBlocking();
}
return client;
}
}
@@ -0,0 +1,33 @@
/*
* Copyright 2017-2022 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.http.server.tck;

import io.micronaut.core.annotation.NonNull;

import java.util.Map;

/**
* {@link ServerUnderTestProvider} implemntation which returns an instance of {@link EmbeddedServerUnderTest}.
* @author Sergio del Amo
* @since 3.8.0
*/
public class EmbeddedServerUnderTestProvider implements ServerUnderTestProvider {
@Override
@NonNull
public ServerUnderTest getServer(@NonNull Map<String, Object> properties) {
return new EmbeddedServerUnderTest(properties);
}
}
@@ -0,0 +1,118 @@
/*
* Copyright 2017-2022 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.http.server.tck;

import io.micronaut.http.HttpStatus;

import java.util.Map;
import java.util.Objects;

/**
* Utility class to verify assertions given an HTTP Response.
* @author Sergio del Amo
* @since 3.8.0
*/
public final class HttpResponseAssertion {

private final HttpStatus httpStatus;
private final Map<String, String> headers;
private final String body;

private HttpResponseAssertion(HttpStatus httpStatus, Map<String, String> headers, String body) {
this.httpStatus = httpStatus;
this.headers = headers;
this.body = body;
}

/**
*
* @return Expected HTTP Response Status
*/
public HttpStatus getHttpStatus() {
return httpStatus;
}

/**
*
* @return Expected HTTP Response Headers
*/
public Map<String, String> getHeaders() {
return headers;
}

/**
*
* @return Expected HTTP Response body
*/
public String getBody() {
return body;
}

/**
*
* @return Creates an instance of {@link HttpResponseAssertion.Builder}.
*/
public static HttpResponseAssertion.Builder builder() {
return new HttpResponseAssertion.Builder();
}

/**
* HTTP Response Assertion Builder.
*/
public static class Builder {
private HttpStatus httpStatus;
private Map<String, String> headers;
private String body;

/**
*
* @param headers HTTP Headers
* @return HTTP Response Assertion Builder
*/
public Builder headers(Map<String, String> headers) {
this.headers = headers;
return this;
}

/**
*
* @param body Response Body
* @return HTTP Response Assertion Builder
*/
public Builder body(String body) {
this.body = body;
return this;
}

/**
*
* @param httpStatus Response's HTTP Status
* @return HTTP Response Assertion Builder
*/
public Builder status(HttpStatus httpStatus) {
this.httpStatus = httpStatus;
return this;
}

/**
*
* @return HTTP Response Assertion
*/
public HttpResponseAssertion build() {
return new HttpResponseAssertion(Objects.requireNonNull(httpStatus), headers, body);
}
}
}