diff --git a/bom/application/pom.xml b/bom/application/pom.xml index f1ee28fd90596..a9a511de263eb 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -4151,6 +4151,11 @@ mockito-core ${mockito.version} + + org.mockito + mockito-inline + ${mockito.version} + org.mockito mockito-junit-jupiter diff --git a/extensions/google-cloud-functions-http/runtime/pom.xml b/extensions/google-cloud-functions-http/runtime/pom.xml index afaf187dcc707..5dd813408f4ea 100644 --- a/extensions/google-cloud-functions-http/runtime/pom.xml +++ b/extensions/google-cloud-functions-http/runtime/pom.xml @@ -32,6 +32,26 @@ functions-framework-api provided + + org.junit.jupiter + junit-jupiter + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-inline + test + diff --git a/extensions/google-cloud-functions-http/runtime/src/main/java/io/quarkus/gcp/functions/http/QuarkusHttpFunction.java b/extensions/google-cloud-functions-http/runtime/src/main/java/io/quarkus/gcp/functions/http/QuarkusHttpFunction.java index e761ea71cb49e..cc491f6b2da64 100644 --- a/extensions/google-cloud-functions-http/runtime/src/main/java/io/quarkus/gcp/functions/http/QuarkusHttpFunction.java +++ b/extensions/google-cloud-functions-http/runtime/src/main/java/io/quarkus/gcp/functions/http/QuarkusHttpFunction.java @@ -92,7 +92,7 @@ private void nettyDispatch(HttpRequest request, HttpResponse response) String path = request.getPath(); Optional host = request.getFirstHeader("Host"); DefaultHttpRequest nettyRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, - HttpMethod.valueOf(request.getMethod()), path); + HttpMethod.valueOf(request.getMethod()), request.getQuery().map(q -> path + "?" + q).orElse(path)); if (host.isPresent()) { nettyRequest.headers().set("Host", host.get()); } @@ -142,6 +142,7 @@ public void handleMessage(Object msg) { try { if (msg instanceof io.netty.handler.codec.http.HttpResponse) { io.netty.handler.codec.http.HttpResponse res = (io.netty.handler.codec.http.HttpResponse) msg; + response.setStatusCode(res.status().code(), res.status().reasonPhrase()); for (Map.Entry entry : res.headers()) { response.appendHeader(entry.getKey(), entry.getValue()); } diff --git a/extensions/google-cloud-functions-http/runtime/src/test/java/io/quarkus/gcp/functions/http/QuarkusHttpFunctionTest.java b/extensions/google-cloud-functions-http/runtime/src/test/java/io/quarkus/gcp/functions/http/QuarkusHttpFunctionTest.java new file mode 100644 index 0000000000000..3e9b02ba7b1f9 --- /dev/null +++ b/extensions/google-cloud-functions-http/runtime/src/test/java/io/quarkus/gcp/functions/http/QuarkusHttpFunctionTest.java @@ -0,0 +1,109 @@ +package io.quarkus.gcp.functions.http; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import com.google.cloud.functions.HttpRequest; +import com.google.cloud.functions.HttpResponse; + +import io.netty.handler.codec.http.DefaultHttpRequest; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.quarkus.netty.runtime.virtual.VirtualClientConnection; +import io.quarkus.netty.runtime.virtual.VirtualResponseHandler; +import io.quarkus.runtime.Application; + +public class QuarkusHttpFunctionTest { + private static final long PROCESSING_TIMEOUT = TimeUnit.SECONDS.toMillis(1); + + private static final String PATH = "test/path"; + private static final String QUERY = "testParam1=testValue1&testParam2=testValue2"; + private static final String HOST_HEADER = "Host"; + private static final String HOST = "localhost"; + private static final String METHOD = "GET"; + + private final Application application = mock(Application.class); + private final HttpRequest request = mock(HttpRequest.class); + private final HttpResponse response = mock(HttpResponse.class); + private final VirtualClientConnection connection = mock(VirtualClientConnection.class); + + @BeforeEach + public void mockSetup() { + when(request.getPath()).thenReturn(PATH); + when(request.getFirstHeader(eq(HOST_HEADER))).thenReturn(Optional.of(HOST)); + when(request.getHeaders()).thenReturn(Collections.singletonMap(HOST_HEADER, Collections.singletonList(HOST))); + when(request.getMethod()).thenReturn(METHOD); + } + + @SuppressWarnings({ "OptionalUsedAsFieldOrParameterType", "rawtypes", "unused" }) + private void mockHttpFunction(Optional query, HttpResponseStatus status) { + when(request.getQuery()).thenReturn(query); + try (MockedStatic applicationMock = Mockito.mockStatic(Application.class)) { + applicationMock.when(Application::currentApplication).thenReturn(application); + QuarkusHttpFunction function = new QuarkusHttpFunction(); + CompletableFuture requestFuture = CompletableFuture.supplyAsync(() -> { + try (MockedStatic connectionMock = Mockito.mockStatic(VirtualClientConnection.class)) { + connectionMock.when(() -> VirtualClientConnection.connect(any(), any())).thenAnswer(i -> { + VirtualResponseHandler handler = i.getArgument(0); + CompletableFuture responseFuture = CompletableFuture.supplyAsync(() -> { + handler.handleMessage(new DefaultHttpResponse(HttpVersion.HTTP_1_1, status)); + return null; + }); + return connection; + }); + function.service(request, response); + } catch (IOException ignore) { + } + return null; + }); + } + } + + public static Iterable queries() { + return Arrays.asList(new Object[] { Optional.of(QUERY), PATH + "?" + QUERY }, new Object[] { Optional.empty(), PATH }); + } + + @ParameterizedTest + @MethodSource("queries") + @SuppressWarnings({ "OptionalUsedAsFieldOrParameterType" }) + public void verifyQueryParametersBypass(Optional query, String expected) { + mockHttpFunction(query, HttpResponseStatus.OK); + ArgumentCaptor captor = ArgumentCaptor.forClass(Object.class); + verify(connection, timeout(PROCESSING_TIMEOUT).times(2)).sendMessage(captor.capture()); + DefaultHttpRequest rq = (DefaultHttpRequest) captor.getAllValues().get(0); + assertEquals(expected, rq.uri()); + } + + public static Iterable responses() { + return Arrays.asList(new Object[] { HttpResponseStatus.CREATED }, new Object[] { HttpResponseStatus.OK }, + new Object[] { HttpResponseStatus.BAD_REQUEST }); + } + + @ParameterizedTest + @MethodSource("responses") + public void verifyResponseStatusBypass(final HttpResponseStatus status) { + mockHttpFunction(Optional.empty(), status); + verify(connection, timeout(PROCESSING_TIMEOUT).times(2)).sendMessage(any()); + verify(response).setStatusCode(eq(status.code()), eq(status.reasonPhrase())); + } +}