diff --git a/java/src/org/openqa/selenium/grid/web/RoutableHttpClientFactory.java b/java/src/org/openqa/selenium/grid/web/RoutableHttpClientFactory.java index d3576bd9f922e..be5342137d944 100644 --- a/java/src/org/openqa/selenium/grid/web/RoutableHttpClientFactory.java +++ b/java/src/org/openqa/selenium/grid/web/RoutableHttpClientFactory.java @@ -75,6 +75,21 @@ public HttpResponse execute(HttpRequest request) throws UncheckedIOException { public WebSocket openSocket(HttpRequest request, WebSocket.Listener listener) { throw new UnsupportedOperationException("openSocket"); } + + @Override + public + java.util.concurrent.CompletableFuture> sendAsyncNative( + java.net.http.HttpRequest request, + java.net.http.HttpResponse.BodyHandler handler) { + throw new UnsupportedOperationException("sendAsyncNative is not supported"); + } + + @Override + public java.net.http.HttpResponse sendNative( + java.net.http.HttpRequest request, java.net.http.HttpResponse.BodyHandler handler) + throws java.io.IOException, InterruptedException { + throw new UnsupportedOperationException("sendNative is not supported"); + } }; } diff --git a/java/src/org/openqa/selenium/remote/RemoteWebDriverBuilder.java b/java/src/org/openqa/selenium/remote/RemoteWebDriverBuilder.java index e310b95ee573e..2e6b708268955 100644 --- a/java/src/org/openqa/selenium/remote/RemoteWebDriverBuilder.java +++ b/java/src/org/openqa/selenium/remote/RemoteWebDriverBuilder.java @@ -297,6 +297,23 @@ public WebSocket openSocket(HttpRequest request, WebSocket.Listener listener) { public HttpResponse execute(HttpRequest req) throws UncheckedIOException { return handler.execute(req); } + + @Override + public + java.util.concurrent.CompletableFuture> + sendAsyncNative( + java.net.http.HttpRequest request, + java.net.http.HttpResponse.BodyHandler handler) { + throw new UnsupportedOperationException("sendAsyncNative is not supported"); + } + + @Override + public java.net.http.HttpResponse sendNative( + java.net.http.HttpRequest request, + java.net.http.HttpResponse.BodyHandler handler) + throws java.io.IOException, InterruptedException { + throw new UnsupportedOperationException("sendNative is not supported"); + } }; } }; diff --git a/java/src/org/openqa/selenium/remote/http/HttpClient.java b/java/src/org/openqa/selenium/remote/http/HttpClient.java index 6cc86a6418a2f..458909441724b 100644 --- a/java/src/org/openqa/selenium/remote/http/HttpClient.java +++ b/java/src/org/openqa/selenium/remote/http/HttpClient.java @@ -39,6 +39,31 @@ default CompletableFuture executeAsync(HttpRequest req) { default void close() {} + /** + * Sends an HTTP request using java.net.http.HttpClient and allows specifying the BodyHandler. + * + * @param the response body type + * @param request the HTTP request to send + * @param handler the BodyHandler that determines how to handle the response body + * @return a CompletableFuture containing the HTTP response + */ + CompletableFuture> sendAsyncNative( + java.net.http.HttpRequest request, java.net.http.HttpResponse.BodyHandler handler); + + /** + * Sends an HTTP request using java.net.http.HttpClient and allows specifying the BodyHandler. + * + * @param the response body type + * @param request the HTTP request to send + * @param handler the BodyHandler that determines how to handle the response body + * @return the HTTP response + * @throws java.io.IOException if an I/O error occurs + * @throws InterruptedException if the operation is interrupted + */ + java.net.http.HttpResponse sendNative( + java.net.http.HttpRequest request, java.net.http.HttpResponse.BodyHandler handler) + throws java.io.IOException, InterruptedException; + interface Factory { /** diff --git a/java/src/org/openqa/selenium/remote/http/jdk/JdkHttpClient.java b/java/src/org/openqa/selenium/remote/http/jdk/JdkHttpClient.java index f2d844a21b152..931f90c79462c 100644 --- a/java/src/org/openqa/selenium/remote/http/jdk/JdkHttpClient.java +++ b/java/src/org/openqa/selenium/remote/http/jdk/JdkHttpClient.java @@ -509,6 +509,19 @@ private HttpResponse execute0(HttpRequest req) throws UncheckedIOException { } } + @Override + public CompletableFuture> sendAsyncNative( + java.net.http.HttpRequest request, java.net.http.HttpResponse.BodyHandler handler) { + return client.sendAsync(request, handler); + } + + @Override + public java.net.http.HttpResponse sendNative( + java.net.http.HttpRequest request, java.net.http.HttpResponse.BodyHandler handler) + throws IOException, InterruptedException { + return client.send(request, handler); + } + @Override public void close() { if (this.client == null) { diff --git a/java/src/org/openqa/selenium/remote/tracing/TracedHttpClient.java b/java/src/org/openqa/selenium/remote/tracing/TracedHttpClient.java index 3d4fc11e885a4..7c09d045f0d9b 100644 --- a/java/src/org/openqa/selenium/remote/tracing/TracedHttpClient.java +++ b/java/src/org/openqa/selenium/remote/tracing/TracedHttpClient.java @@ -57,6 +57,19 @@ public HttpResponse execute(HttpRequest req) { } } + @Override + public java.util.concurrent.CompletableFuture> sendAsyncNative( + java.net.http.HttpRequest request, java.net.http.HttpResponse.BodyHandler handler) { + return delegate.sendAsyncNative(request, handler); + } + + @Override + public java.net.http.HttpResponse sendNative( + java.net.http.HttpRequest request, java.net.http.HttpResponse.BodyHandler handler) + throws java.io.IOException, InterruptedException { + return delegate.sendNative(request, handler); + } + @Override public void close() { delegate.close(); diff --git a/java/test/org/openqa/selenium/grid/testing/PassthroughHttpClient.java b/java/test/org/openqa/selenium/grid/testing/PassthroughHttpClient.java index f465dff3acdee..d77faab75b4ca 100644 --- a/java/test/org/openqa/selenium/grid/testing/PassthroughHttpClient.java +++ b/java/test/org/openqa/selenium/grid/testing/PassthroughHttpClient.java @@ -48,6 +48,19 @@ public WebSocket openSocket(HttpRequest request, WebSocket.Listener listener) { throw new UnsupportedOperationException("openSocket"); } + @Override + public java.util.concurrent.CompletableFuture> sendAsyncNative( + java.net.http.HttpRequest request, java.net.http.HttpResponse.BodyHandler handler) { + throw new UnsupportedOperationException("sendAsyncNative"); + } + + @Override + public java.net.http.HttpResponse sendNative( + java.net.http.HttpRequest request, java.net.http.HttpResponse.BodyHandler handler) + throws java.io.IOException, InterruptedException { + throw new UnsupportedOperationException("sendNative"); + } + public static class Factory implements HttpClient.Factory { private final Routable handler; diff --git a/java/test/org/openqa/selenium/remote/ProtocolHandshakeTest.java b/java/test/org/openqa/selenium/remote/ProtocolHandshakeTest.java index d04e845a76a39..784b9fe0056ca 100644 --- a/java/test/org/openqa/selenium/remote/ProtocolHandshakeTest.java +++ b/java/test/org/openqa/selenium/remote/ProtocolHandshakeTest.java @@ -194,5 +194,19 @@ String getRequestPayload() { public WebSocket openSocket(HttpRequest request, WebSocket.Listener listener) { throw new UnsupportedOperationException("openSocket"); } + + @Override + public + java.util.concurrent.CompletableFuture> sendAsyncNative( + java.net.http.HttpRequest request, java.net.http.HttpResponse.BodyHandler handler) { + throw new UnsupportedOperationException("sendAsyncNative"); + } + + @Override + public java.net.http.HttpResponse sendNative( + java.net.http.HttpRequest request, java.net.http.HttpResponse.BodyHandler handler) + throws java.io.IOException, InterruptedException { + throw new UnsupportedOperationException("sendNative"); + } } } diff --git a/java/test/org/openqa/selenium/remote/http/NativeHttpClientMethodsTest.java b/java/test/org/openqa/selenium/remote/http/NativeHttpClientMethodsTest.java new file mode 100644 index 0000000000000..bce11145c9fb5 --- /dev/null +++ b/java/test/org/openqa/selenium/remote/http/NativeHttpClientMethodsTest.java @@ -0,0 +1,303 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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 +// +// http://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 org.openqa.selenium.remote.http; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the new native Java 11 HTTP methods in HttpClient interface: - sendAsyncNative() - + * sendNative() + */ +@Tag("UnitTests") +class NativeHttpClientMethodsTest { + + private TestHttpClient httpClient; + private HttpRequest testRequest; + + @BeforeEach + void setUp() { + // Initialize test client and create sample HTTP request for testing + httpClient = new TestHttpClient(); + + // Create a sample HTTP request for testing + testRequest = HttpRequest.newBuilder().uri(URI.create("https://httpbin.org/get")).GET().build(); + } + + /** + * Tests that sendAsyncNative() method executes successfully and returns a CompletableFuture with + * the expected HTTP response. Verifies that: - The method returns a non-null CompletableFuture - + * The response can be retrieved within timeout - The response contains expected status code and + * body + */ + @Test + void testSendAsyncNative_successful() + throws ExecutionException, InterruptedException, TimeoutException { + // Act - Execute asynchronous HTTP request using native Java 11 API + CompletableFuture> result = + httpClient.sendAsyncNative(testRequest, HttpResponse.BodyHandlers.ofString()); + + // Assert - Verify the asynchronous response is correct + assertNotNull(result, "Future should not be null"); + HttpResponse response = result.get(5, TimeUnit.SECONDS); + assertNotNull(response, "Response should not be null"); + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.body()).isEqualTo("Test response body"); + } + + /** + * Tests that sendNative() method executes successfully in a synchronous manner. Verifies that: - + * The method returns a valid HTTP response - The response contains the expected status code and + * body content - The synchronous call completes without throwing exceptions + */ + @Test + void testSendNative_successful() throws IOException, InterruptedException { + // Act - Execute synchronous HTTP request using native Java 11 API + HttpResponse result = + httpClient.sendNative(testRequest, HttpResponse.BodyHandlers.ofString()); + + // Assert - Verify the synchronous response is correct + assertNotNull(result, "Response should not be null"); + assertThat(result.statusCode()).isEqualTo(200); + assertThat(result.body()).isEqualTo("Test response body"); + } + + /** + * Tests that sendNative() method properly handles IOException when network errors occur. Verifies + * that: - IOException is thrown when the client is configured to fail - The exception propagates + * correctly to the caller - Error handling behavior is consistent with Java 11 HttpClient + * expectations + */ + @Test + void testSendNative_handlesIOException() { + // Arrange - Create a client configured to simulate network failure + TestHttpClient failingClient = new TestHttpClient(true); + + // Act & Assert - Verify that IOException is thrown when network failure occurs + assertThrows( + IOException.class, + () -> failingClient.sendNative(testRequest, HttpResponse.BodyHandlers.ofString())); + } + + /** + * Tests that HTTP request parameters are properly validated and constructed. Verifies that: - GET + * requests are created with correct method and URI - POST requests are created with correct + * method, headers, and body - Request headers are properly set and accessible - Different HTTP + * methods can be distinguished correctly + */ + @Test + void testRequestParameters_validation() { + // Test GET request creation and validation + HttpRequest getRequest = + HttpRequest.newBuilder().uri(URI.create("https://httpbin.org/get")).GET().build(); + + // Assert GET request properties + assertNotNull(getRequest); + assertThat(getRequest.method()).isEqualTo("GET"); + + // Test POST request creation with body and headers + HttpRequest postRequest = + HttpRequest.newBuilder() + .uri(URI.create("https://httpbin.org/post")) + .POST(HttpRequest.BodyPublishers.ofString("{\"test\": \"data\"}")) + .header("Content-Type", "application/json") + .build(); + + // Assert POST request properties including headers + assertNotNull(postRequest); + assertThat(postRequest.method()).isEqualTo("POST"); + assertTrue(postRequest.headers().firstValue("Content-Type").isPresent()); + } + + /** + * Tests that different types of BodyHandlers are properly supported and instantiated. Verifies + * that: - String BodyHandler for text responses works correctly - Discarding BodyHandler for HEAD + * requests or when body is not needed - Lines BodyHandler for streaming large text responses line + * by line - All BodyHandler types can be created without errors + */ + @Test + void testBodyHandlers_variations() { + // Test String handler for regular text responses + HttpResponse.BodyHandler stringHandler = HttpResponse.BodyHandlers.ofString(); + assertNotNull(stringHandler); + + // Test discarding handler (useful for HEAD requests or when body is not needed) + HttpResponse.BodyHandler discardingHandler = HttpResponse.BodyHandlers.discarding(); + assertNotNull(discardingHandler); + + // Test lines handler for streaming large responses line by line + HttpResponse.BodyHandler> linesHandler = + HttpResponse.BodyHandlers.ofLines(); + assertNotNull(linesHandler); + } + + /** + * Simple test implementation of HttpClient for testing purposes. Provides mock implementations of + * all HttpClient methods with: - Configurable failure simulation for error testing - Mock HTTP + * responses with predictable data - UnsupportedOperationException for methods not relevant to + * native API testing + */ + private static class TestHttpClient implements HttpClient { + private final boolean shouldFail; + + public TestHttpClient() { + this(false); + } + + public TestHttpClient(boolean shouldFail) { + this.shouldFail = shouldFail; + } + + @Override + public WebSocket openSocket( + org.openqa.selenium.remote.http.HttpRequest request, WebSocket.Listener listener) { + throw new UnsupportedOperationException("openSocket not implemented in test"); + } + + @Override + public org.openqa.selenium.remote.http.HttpResponse execute( + org.openqa.selenium.remote.http.HttpRequest request) { + throw new UnsupportedOperationException("execute not implemented in test"); + } + + @Override + public CompletableFuture> sendAsyncNative( + java.net.http.HttpRequest request, java.net.http.HttpResponse.BodyHandler handler) { + + // Create a mock response for testing asynchronous behavior + java.net.http.HttpResponse mockResponse = + new java.net.http.HttpResponse<>() { + @Override + public int statusCode() { + return 200; + } + + @Override + public java.net.http.HttpRequest request() { + return request; + } + + @Override + public java.util.Optional> previousResponse() { + return java.util.Optional.empty(); + } + + @Override + public java.net.http.HttpHeaders headers() { + return java.net.http.HttpHeaders.of(java.util.Map.of(), (a, b) -> true); + } + + @Override + public T body() { + // This is a simplified mock that returns a string for any type T + @SuppressWarnings("unchecked") + T result = (T) "Test response body"; + return result; + } + + @Override + public java.util.Optional sslSession() { + return java.util.Optional.empty(); + } + + @Override + public java.net.URI uri() { + return request.uri(); + } + + @Override + public java.net.http.HttpClient.Version version() { + return java.net.http.HttpClient.Version.HTTP_1_1; + } + }; + + return CompletableFuture.completedFuture(mockResponse); + } + + @Override + public java.net.http.HttpResponse sendNative( + java.net.http.HttpRequest request, java.net.http.HttpResponse.BodyHandler handler) + throws IOException, InterruptedException { + + // Simulate network failure if configured to do so + if (shouldFail) { + throw new IOException("Simulated network error"); + } + + // Create a mock response for testing synchronous behavior + return new java.net.http.HttpResponse<>() { + @Override + public int statusCode() { + return 200; + } + + @Override + public java.net.http.HttpRequest request() { + return request; + } + + @Override + public java.util.Optional> previousResponse() { + return java.util.Optional.empty(); + } + + @Override + public java.net.http.HttpHeaders headers() { + return java.net.http.HttpHeaders.of(java.util.Map.of(), (a, b) -> true); + } + + @Override + public T body() { + // This is a simplified mock that returns a string for any type T + @SuppressWarnings("unchecked") + T result = (T) "Test response body"; + return result; + } + + @Override + public java.util.Optional sslSession() { + return java.util.Optional.empty(); + } + + @Override + public java.net.URI uri() { + return request.uri(); + } + + @Override + public java.net.http.HttpClient.Version version() { + return java.net.http.HttpClient.Version.HTTP_1_1; + } + }; + } + } +}