From 9724956cb48e84aeeda15e15f5554ca1abb9335b Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Sat, 23 Dec 2023 03:26:25 -0700 Subject: [PATCH] Convert more tests from Java to Kotlin (#8155) --- .../mockwebserver/MockWebServerTest.java | 635 ---------- .../mockwebserver/MockWebServerTest.kt | 633 ++++++++++ .../junit4/MockWebServerRuleTest.kt | 2 +- .../dnsoverhttps/DnsOverHttpsTest.java | 265 ----- .../okhttp3/dnsoverhttps/DnsOverHttpsTest.kt | 254 ++++ .../dnsoverhttps/DnsRecordCodecTest.java | 83 -- .../dnsoverhttps/DnsRecordCodecTest.kt | 93 ++ .../okhttp3/dnsoverhttps/DohProviders.java | 122 -- .../java/okhttp3/dnsoverhttps/DohProviders.kt | 130 ++ .../okhttp3/dnsoverhttps/TestDohMain.java | 106 -- .../java/okhttp3/dnsoverhttps/TestDohMain.kt | 103 ++ okhttp-hpacktests/build.gradle.kts | 1 + .../http2/HpackDecodeInteropTest.java | 43 - .../internal/http2/HpackDecodeInteropTest.kt | 39 + .../internal/http2/HpackDecodeTestBase.java | 75 -- .../internal/http2/HpackDecodeTestBase.kt | 75 ++ .../internal/http2/HpackRoundTripTest.java | 61 - .../internal/http2/HpackRoundTripTest.kt | 60 + .../internal/http2/hpackjson/Case.java | 67 -- .../okhttp3/internal/http2/hpackjson/Case.kt | 41 + .../http2/hpackjson/HpackJsonUtil.java | 107 -- .../internal/http2/hpackjson/HpackJsonUtil.kt | 112 ++ .../internal/http2/hpackjson/Story.java | 71 -- .../okhttp3/internal/http2/hpackjson/Story.kt | 35 + .../logging/HttpLoggingInterceptorTest.java | 1051 ----------------- .../logging/HttpLoggingInterceptorTest.kt | 1016 ++++++++++++++++ .../logging/LoggingEventListenerTest.java | 283 ----- .../logging/LoggingEventListenerTest.kt | 239 ++++ .../test/java/okhttp3/sse/internal/Event.java | 52 - .../test/java/okhttp3/sse/internal/Event.kt | 19 +- .../sse/internal/EventSourceHttpTest.java | 236 ---- .../sse/internal/EventSourceHttpTest.kt | 262 ++++ .../sse/internal/EventSourceRecorder.java | 166 --- .../sse/internal/EventSourceRecorder.kt | 125 ++ .../sse/internal/EventSourcesHttpTest.java | 89 -- .../sse/internal/EventSourcesHttpTest.kt | 100 ++ .../internal/ServerSentEventIteratorTest.java | 226 ---- .../internal/ServerSentEventIteratorTest.kt | 305 +++++ .../java/okhttp3/tls/HeldCertificateTest.java | 506 -------- .../java/okhttp3/tls/HeldCertificateTest.kt | 522 ++++++++ okhttp/src/test/java/okhttp3/CallTest.kt | 4 +- .../test/java/okhttp3/CipherSuiteTest.java | 222 ---- .../src/test/java/okhttp3/CipherSuiteTest.kt | 220 ++++ okhttp/src/test/java/okhttp3/CookiesTest.java | 370 ------ okhttp/src/test/java/okhttp3/CookiesTest.kt | 353 ++++++ .../src/test/java/okhttp3/DispatcherTest.kt | 3 +- .../FallbackTestClientSocketFactory.java | 62 - .../FallbackTestClientSocketFactory.kt | 55 + .../src/test/java/okhttp3/FormBodyTest.java | 218 ---- okhttp/src/test/java/okhttp3/FormBodyTest.kt | 209 ++++ .../src/test/java/okhttp3/ProtocolTest.java | 52 - okhttp/src/test/java/okhttp3/ProtocolTest.kt | 53 + .../java/okhttp3/PublicInternalApiTest.java | 45 - .../java/okhttp3/PublicInternalApiTest.kt | 49 + .../test/java/okhttp3/RecordedResponse.java | 194 --- .../src/test/java/okhttp3/RecordedResponse.kt | 157 +++ .../test/java/okhttp3/RecordingCallback.java | 65 - .../test/java/okhttp3/RecordingCallback.kt | 67 ++ .../okhttp3/RecordingWebSocketListener.java | 45 - .../src/test/java/okhttp3/SocksProxyTest.java | 114 -- .../src/test/java/okhttp3/SocksProxyTest.kt | 106 ++ .../src/test/java/okhttp3/TestLogHandler.java | 101 -- .../src/test/java/okhttp3/TestLogHandler.kt | 93 ++ .../test/java/okhttp3/TestTls13Request.java | 106 -- .../src/test/java/okhttp3/TestTls13Request.kt | 101 ++ ...ddressDns.java => DoubleInetAddressDns.kt} | 18 +- .../internal/RecordingAuthenticator.java | 49 - .../internal/RecordingAuthenticator.kt | 41 + .../java/okhttp3/internal/SocketRecorder.java | 206 ---- .../okhttp3/internal/http/StatusLineTest.java | 118 -- .../okhttp3/internal/http/StatusLineTest.kt | 125 ++ .../internal/http/ThreadInterruptTest.java | 160 --- .../internal/http/ThreadInterruptTest.kt | 157 +++ .../okhttp3/internal/http2/HuffmanTest.java | 50 - .../okhttp3/internal/http2/HuffmanTest.kt | 52 + .../okhttp3/internal/http2/SettingsTest.java | 78 -- .../okhttp3/internal/http2/SettingsTest.kt | 71 ++ .../Jdk8WithJettyBootPlatformTest.java | 43 - .../platform/Jdk8WithJettyBootPlatformTest.kt | 42 + .../internal/platform/Jdk9PlatformTest.java | 66 -- .../internal/platform/Jdk9PlatformTest.kt | 63 + .../internal/platform/PlatformTest.java | 52 - .../okhttp3/internal/platform/PlatformTest.kt | 51 + .../internal/ws/WebSocketRecorder.java | 401 ------- .../okhttp3/internal/ws/WebSocketRecorder.kt | 239 ++++ .../src/test/java/okhttp3/osgi/OsgiTest.java | 161 --- okhttp/src/test/java/okhttp3/osgi/OsgiTest.kt | 160 +++ 87 files changed, 6628 insertions(+), 7249 deletions(-) delete mode 100644 mockwebserver-deprecated/src/test/java/okhttp3/mockwebserver/MockWebServerTest.java create mode 100644 mockwebserver-deprecated/src/test/java/okhttp3/mockwebserver/MockWebServerTest.kt delete mode 100644 okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.java create mode 100644 okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt delete mode 100644 okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsRecordCodecTest.java create mode 100644 okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsRecordCodecTest.kt delete mode 100644 okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DohProviders.java create mode 100644 okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DohProviders.kt delete mode 100644 okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/TestDohMain.java create mode 100644 okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/TestDohMain.kt delete mode 100644 okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackDecodeInteropTest.java create mode 100644 okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackDecodeInteropTest.kt delete mode 100644 okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackDecodeTestBase.java create mode 100644 okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackDecodeTestBase.kt delete mode 100644 okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackRoundTripTest.java create mode 100644 okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackRoundTripTest.kt delete mode 100644 okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/Case.java create mode 100644 okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/Case.kt delete mode 100644 okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/HpackJsonUtil.java create mode 100644 okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/HpackJsonUtil.kt delete mode 100644 okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/Story.java create mode 100644 okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/Story.kt delete mode 100644 okhttp-logging-interceptor/src/test/java/okhttp3/logging/HttpLoggingInterceptorTest.java create mode 100644 okhttp-logging-interceptor/src/test/java/okhttp3/logging/HttpLoggingInterceptorTest.kt delete mode 100644 okhttp-logging-interceptor/src/test/java/okhttp3/logging/LoggingEventListenerTest.java create mode 100644 okhttp-logging-interceptor/src/test/java/okhttp3/logging/LoggingEventListenerTest.kt delete mode 100644 okhttp-sse/src/test/java/okhttp3/sse/internal/Event.java rename okhttp-brotli/src/test/java/okhttp3/brotli/BrotliInterceptorJavaApiTest.java => okhttp-sse/src/test/java/okhttp3/sse/internal/Event.kt (66%) delete mode 100644 okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourceHttpTest.java create mode 100644 okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourceHttpTest.kt delete mode 100644 okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourceRecorder.java create mode 100644 okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourceRecorder.kt delete mode 100644 okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourcesHttpTest.java create mode 100644 okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourcesHttpTest.kt delete mode 100644 okhttp-sse/src/test/java/okhttp3/sse/internal/ServerSentEventIteratorTest.java create mode 100644 okhttp-sse/src/test/java/okhttp3/sse/internal/ServerSentEventIteratorTest.kt delete mode 100644 okhttp-tls/src/test/java/okhttp3/tls/HeldCertificateTest.java create mode 100644 okhttp-tls/src/test/java/okhttp3/tls/HeldCertificateTest.kt delete mode 100644 okhttp/src/test/java/okhttp3/CipherSuiteTest.java create mode 100644 okhttp/src/test/java/okhttp3/CipherSuiteTest.kt delete mode 100644 okhttp/src/test/java/okhttp3/CookiesTest.java create mode 100644 okhttp/src/test/java/okhttp3/CookiesTest.kt delete mode 100644 okhttp/src/test/java/okhttp3/FallbackTestClientSocketFactory.java create mode 100644 okhttp/src/test/java/okhttp3/FallbackTestClientSocketFactory.kt delete mode 100644 okhttp/src/test/java/okhttp3/FormBodyTest.java create mode 100644 okhttp/src/test/java/okhttp3/FormBodyTest.kt delete mode 100644 okhttp/src/test/java/okhttp3/ProtocolTest.java create mode 100644 okhttp/src/test/java/okhttp3/ProtocolTest.kt delete mode 100644 okhttp/src/test/java/okhttp3/PublicInternalApiTest.java create mode 100644 okhttp/src/test/java/okhttp3/PublicInternalApiTest.kt delete mode 100644 okhttp/src/test/java/okhttp3/RecordedResponse.java create mode 100644 okhttp/src/test/java/okhttp3/RecordedResponse.kt delete mode 100644 okhttp/src/test/java/okhttp3/RecordingCallback.java create mode 100644 okhttp/src/test/java/okhttp3/RecordingCallback.kt delete mode 100644 okhttp/src/test/java/okhttp3/RecordingWebSocketListener.java delete mode 100644 okhttp/src/test/java/okhttp3/SocksProxyTest.java create mode 100644 okhttp/src/test/java/okhttp3/SocksProxyTest.kt delete mode 100644 okhttp/src/test/java/okhttp3/TestLogHandler.java create mode 100644 okhttp/src/test/java/okhttp3/TestLogHandler.kt delete mode 100644 okhttp/src/test/java/okhttp3/TestTls13Request.java create mode 100644 okhttp/src/test/java/okhttp3/TestTls13Request.kt rename okhttp/src/test/java/okhttp3/internal/{DoubleInetAddressDns.java => DoubleInetAddressDns.kt} (64%) delete mode 100644 okhttp/src/test/java/okhttp3/internal/RecordingAuthenticator.java create mode 100644 okhttp/src/test/java/okhttp3/internal/RecordingAuthenticator.kt delete mode 100644 okhttp/src/test/java/okhttp3/internal/SocketRecorder.java delete mode 100644 okhttp/src/test/java/okhttp3/internal/http/StatusLineTest.java create mode 100644 okhttp/src/test/java/okhttp3/internal/http/StatusLineTest.kt delete mode 100644 okhttp/src/test/java/okhttp3/internal/http/ThreadInterruptTest.java create mode 100644 okhttp/src/test/java/okhttp3/internal/http/ThreadInterruptTest.kt delete mode 100644 okhttp/src/test/java/okhttp3/internal/http2/HuffmanTest.java create mode 100644 okhttp/src/test/java/okhttp3/internal/http2/HuffmanTest.kt delete mode 100644 okhttp/src/test/java/okhttp3/internal/http2/SettingsTest.java create mode 100644 okhttp/src/test/java/okhttp3/internal/http2/SettingsTest.kt delete mode 100644 okhttp/src/test/java/okhttp3/internal/platform/Jdk8WithJettyBootPlatformTest.java create mode 100644 okhttp/src/test/java/okhttp3/internal/platform/Jdk8WithJettyBootPlatformTest.kt delete mode 100644 okhttp/src/test/java/okhttp3/internal/platform/Jdk9PlatformTest.java create mode 100644 okhttp/src/test/java/okhttp3/internal/platform/Jdk9PlatformTest.kt delete mode 100644 okhttp/src/test/java/okhttp3/internal/platform/PlatformTest.java create mode 100644 okhttp/src/test/java/okhttp3/internal/platform/PlatformTest.kt delete mode 100644 okhttp/src/test/java/okhttp3/internal/ws/WebSocketRecorder.java create mode 100644 okhttp/src/test/java/okhttp3/internal/ws/WebSocketRecorder.kt delete mode 100644 okhttp/src/test/java/okhttp3/osgi/OsgiTest.java create mode 100644 okhttp/src/test/java/okhttp3/osgi/OsgiTest.kt diff --git a/mockwebserver-deprecated/src/test/java/okhttp3/mockwebserver/MockWebServerTest.java b/mockwebserver-deprecated/src/test/java/okhttp3/mockwebserver/MockWebServerTest.java deleted file mode 100644 index 30e7d85220fa..000000000000 --- a/mockwebserver-deprecated/src/test/java/okhttp3/mockwebserver/MockWebServerTest.java +++ /dev/null @@ -1,635 +0,0 @@ -/* - * Copyright (C) 2011 Google Inc. - * - * 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 - * - * 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 okhttp3.mockwebserver; - -import java.io.BufferedReader; -import java.io.Closeable; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.ConnectException; -import java.net.HttpURLConnection; -import java.net.ProtocolException; -import java.net.SocketTimeoutException; -import java.net.URL; -import java.net.URLConnection; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import javax.net.ssl.HttpsURLConnection; -import okhttp3.Handshake; -import okhttp3.Headers; -import okhttp3.HttpUrl; -import okhttp3.Protocol; -import okhttp3.RecordingHostnameVerifier; -import okhttp3.TestUtil; -import okhttp3.testing.PlatformRule; -import okhttp3.tls.HandshakeCertificates; -import okhttp3.tls.HeldCertificate; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Arrays.asList; -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.concurrent.TimeUnit.NANOSECONDS; -import static java.util.concurrent.TimeUnit.SECONDS; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.data.Offset.offset; -import static org.junit.jupiter.api.Assertions.fail; - -@SuppressWarnings({"ArraysAsListWithZeroOrOneArgument", "deprecation"}) -@Timeout(30) -@Tag("Slow") -public final class MockWebServerTest { - @RegisterExtension public PlatformRule platform = new PlatformRule(); - - private final MockWebServer server = new MockWebServer(); - - @BeforeEach public void setUp() throws IOException { - server.start(); - } - - @AfterEach - public void tearDown() throws Exception { - server.shutdown(); - } - - @Test public void defaultMockResponse() { - MockResponse response = new MockResponse(); - assertThat(headersToList(response)).containsExactly("Content-Length: 0"); - assertThat(response.getStatus()).isEqualTo("HTTP/1.1 200 OK"); - } - - @Test public void setResponseMockReason() { - String[] reasons = { - "Mock Response", - "Informational", - "OK", - "Redirection", - "Client Error", - "Server Error", - "Mock Response" - }; - for (int i = 0; i < 600; i++) { - MockResponse response = new MockResponse().setResponseCode(i); - String expectedReason = reasons[i / 100]; - assertThat(response.getStatus()).isEqualTo(("HTTP/1.1 " + i + " " + expectedReason)); - assertThat(headersToList(response)).containsExactly("Content-Length: 0"); - } - } - - @Test public void setStatusControlsWholeStatusLine() { - MockResponse response = new MockResponse().setStatus("HTTP/1.1 202 That'll do pig"); - assertThat(headersToList(response)).containsExactly("Content-Length: 0"); - assertThat(response.getStatus()).isEqualTo("HTTP/1.1 202 That'll do pig"); - } - - @Test public void setBodyAdjustsHeaders() throws IOException { - MockResponse response = new MockResponse().setBody("ABC"); - assertThat(headersToList(response)).containsExactly("Content-Length: 3"); - assertThat(response.getBody().readUtf8()).isEqualTo("ABC"); - } - - @Test public void mockResponseAddHeader() { - MockResponse response = new MockResponse() - .clearHeaders() - .addHeader("Cookie: s=square") - .addHeader("Cookie", "a=android"); - assertThat(headersToList(response)).containsExactly("Cookie: s=square", "Cookie: a=android"); - } - - @Test public void mockResponseSetHeader() { - MockResponse response = new MockResponse() - .clearHeaders() - .addHeader("Cookie: s=square") - .addHeader("Cookie: a=android") - .addHeader("Cookies: delicious"); - response.setHeader("cookie", "r=robot"); - assertThat(headersToList(response)).containsExactly("Cookies: delicious", "cookie: r=robot"); - } - - @Test public void mockResponseSetHeaders() { - MockResponse response = new MockResponse() - .clearHeaders() - .addHeader("Cookie: s=square") - .addHeader("Cookies: delicious"); - - response.setHeaders(new Headers.Builder().add("Cookie", "a=android").build()); - - assertThat(headersToList(response)).containsExactly("Cookie: a=android"); - } - - @Test public void regularResponse() throws Exception { - server.enqueue(new MockResponse().setBody("hello world")); - - URL url = server.url("/").url(); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestProperty("Accept-Language", "en-US"); - InputStream in = connection.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8)); - assertThat(connection.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK); - assertThat(reader.readLine()).isEqualTo("hello world"); - - RecordedRequest request = server.takeRequest(); - assertThat(request.getRequestLine()).isEqualTo("GET / HTTP/1.1"); - assertThat(request.getHeader("Accept-Language")).isEqualTo("en-US"); - - // Server has no more requests. - assertThat(server.takeRequest(100, MILLISECONDS)).isNull(); - } - - @Test public void redirect() throws Exception { - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) - .addHeader("Location: " + server.url("/new-path")) - .setBody("This page has moved!")); - server.enqueue(new MockResponse().setBody("This is the new location!")); - - URLConnection connection = server.url("/").url().openConnection(); - InputStream in = connection.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8)); - assertThat(reader.readLine()).isEqualTo("This is the new location!"); - - RecordedRequest first = server.takeRequest(); - assertThat(first.getRequestLine()).isEqualTo("GET / HTTP/1.1"); - RecordedRequest redirect = server.takeRequest(); - assertThat(redirect.getRequestLine()).isEqualTo("GET /new-path HTTP/1.1"); - } - - /** - * Test that MockWebServer blocks for a call to enqueue() if a request is made before a mock - * response is ready. - */ - @Test public void dispatchBlocksWaitingForEnqueue() throws Exception { - new Thread(() -> { - try { - Thread.sleep(1000); - } catch (InterruptedException ignored) { - } - server.enqueue(new MockResponse().setBody("enqueued in the background")); - }).start(); - - URLConnection connection = server.url("/").url().openConnection(); - InputStream in = connection.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8)); - assertThat(reader.readLine()).isEqualTo("enqueued in the background"); - } - - @Test public void nonHexadecimalChunkSize() throws Exception { - server.enqueue(new MockResponse() - .setBody("G\r\nxxxxxxxxxxxxxxxx\r\n0\r\n\r\n") - .clearHeaders() - .addHeader("Transfer-encoding: chunked")); - - URLConnection connection = server.url("/").url().openConnection(); - InputStream in = connection.getInputStream(); - try { - in.read(); - fail(); - } catch (IOException expected) { - } - } - - @Test public void responseTimeout() throws Exception { - server.enqueue(new MockResponse() - .setBody("ABC") - .clearHeaders() - .addHeader("Content-Length: 4")); - server.enqueue(new MockResponse().setBody("DEF")); - - URLConnection urlConnection = server.url("/").url().openConnection(); - urlConnection.setReadTimeout(1000); - InputStream in = urlConnection.getInputStream(); - assertThat(in.read()).isEqualTo('A'); - assertThat(in.read()).isEqualTo('B'); - assertThat(in.read()).isEqualTo('C'); - try { - in.read(); // if Content-Length was accurate, this would return -1 immediately - fail(); - } catch (SocketTimeoutException expected) { - } - - URLConnection urlConnection2 = server.url("/").url().openConnection(); - InputStream in2 = urlConnection2.getInputStream(); - assertThat(in2.read()).isEqualTo('D'); - assertThat(in2.read()).isEqualTo('E'); - assertThat(in2.read()).isEqualTo('F'); - assertThat(in2.read()).isEqualTo(-1); - - assertThat(server.takeRequest().getSequenceNumber()).isEqualTo(0); - assertThat(server.takeRequest().getSequenceNumber()).isEqualTo(0); - } - - @Disabled("Not actually failing where expected") - @Test public void disconnectAtStart() throws Exception { - server.enqueue(new MockResponse() - .setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); - server.enqueue(new MockResponse()); // The jdk's HttpUrlConnection is a bastard. - server.enqueue(new MockResponse()); - try { - server.url("/a").url().openConnection().getInputStream(); - fail(); - } catch (IOException expected) { - } - server.url("/b").url().openConnection().getInputStream(); // Should succeed. - } - - /** - * Throttle the request body by sleeping 500ms after every 3 bytes. With a 6-byte request, this - * should yield one sleep for a total delay of 500ms. - */ - @Test public void throttleRequest() throws Exception { - TestUtil.assumeNotWindows(); - - server.enqueue(new MockResponse() - .throttleBody(3, 500, TimeUnit.MILLISECONDS)); - - long startNanos = System.nanoTime(); - URLConnection connection = server.url("/").url().openConnection(); - connection.setDoOutput(true); - connection.getOutputStream().write("ABCDEF".getBytes(UTF_8)); - InputStream in = connection.getInputStream(); - assertThat(in.read()).isEqualTo(-1); - long elapsedNanos = System.nanoTime() - startNanos; - long elapsedMillis = NANOSECONDS.toMillis(elapsedNanos); - assertThat(elapsedMillis).isBetween(500L, 1000L); - } - - /** - * Throttle the response body by sleeping 500ms after every 3 bytes. With a 6-byte response, this - * should yield one sleep for a total delay of 500ms. - */ - @Test public void throttleResponse() throws Exception { - TestUtil.assumeNotWindows(); - - server.enqueue(new MockResponse() - .setBody("ABCDEF") - .throttleBody(3, 500, TimeUnit.MILLISECONDS)); - - long startNanos = System.nanoTime(); - URLConnection connection = server.url("/").url().openConnection(); - InputStream in = connection.getInputStream(); - assertThat(in.read()).isEqualTo('A'); - assertThat(in.read()).isEqualTo('B'); - assertThat(in.read()).isEqualTo('C'); - assertThat(in.read()).isEqualTo('D'); - assertThat(in.read()).isEqualTo('E'); - assertThat(in.read()).isEqualTo('F'); - assertThat(in.read()).isEqualTo(-1); - long elapsedNanos = System.nanoTime() - startNanos; - long elapsedMillis = NANOSECONDS.toMillis(elapsedNanos); - assertThat(elapsedMillis).isBetween(500L, 1000L); - } - - /** Delay the response body by sleeping 1s. */ - @Test public void delayResponse() throws IOException { - TestUtil.assumeNotWindows(); - - server.enqueue(new MockResponse() - .setBody("ABCDEF") - .setBodyDelay(1, SECONDS)); - - long startNanos = System.nanoTime(); - URLConnection connection = server.url("/").url().openConnection(); - InputStream in = connection.getInputStream(); - assertThat(in.read()).isEqualTo('A'); - long elapsedNanos = System.nanoTime() - startNanos; - long elapsedMillis = NANOSECONDS.toMillis(elapsedNanos); - assertThat(elapsedMillis).isGreaterThanOrEqualTo(1000L); - - in.close(); - } - - @Test public void disconnectRequestHalfway() throws Exception { - server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_REQUEST_BODY)); - // Limit the size of the request body that the server holds in memory to an arbitrary - // 3.5 MBytes so this test can pass on devices with little memory. - server.setBodyLimit(7 * 512 * 1024); - - HttpURLConnection connection = (HttpURLConnection) server.url("/").url().openConnection(); - connection.setRequestMethod("POST"); - connection.setDoOutput(true); - connection.setFixedLengthStreamingMode(1024 * 1024 * 1024); // 1 GB - connection.connect(); - OutputStream out = connection.getOutputStream(); - - byte[] data = new byte[1024 * 1024]; - int i; - for (i = 0; i < 1024; i++) { - try { - out.write(data); - out.flush(); - if (i == 513) { - // pause slightly after half way to make result more predictable - Thread.sleep(100); - } - } catch (IOException e) { - break; - } - } - // Halfway +/- 0.5% - assertThat((float) i).isCloseTo(512f, offset(5f)); - } - - @Test public void disconnectResponseHalfway() throws IOException { - server.enqueue(new MockResponse() - .setBody("ab") - .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY)); - - URLConnection connection = server.url("/").url().openConnection(); - assertThat(connection.getContentLength()).isEqualTo(2); - InputStream in = connection.getInputStream(); - assertThat(in.read()).isEqualTo('a'); - try { - int byteRead = in.read(); - // OpenJDK behavior: end of stream. - assertThat(byteRead).isEqualTo(-1); - } catch (ProtocolException e) { - // On Android, HttpURLConnection is implemented by OkHttp v2. OkHttp - // treats an incomplete response body as a ProtocolException. - } - } - - private List headersToList(MockResponse response) { - Headers headers = response.getHeaders(); - int size = headers.size(); - List headerList = new ArrayList<>(size); - for (int i = 0; i < size; i++) { - headerList.add(headers.name(i) + ": " + headers.value(i)); - } - return headerList; - } - - @Test public void shutdownWithoutStart() throws IOException { - MockWebServer server = new MockWebServer(); - server.shutdown(); - } - - @Test public void closeViaClosable() throws IOException { - Closeable server = new MockWebServer(); - server.close(); - } - - @Test public void shutdownWithoutEnqueue() throws IOException { - MockWebServer server = new MockWebServer(); - server.start(); - server.shutdown(); - } - - @Test public void portImplicitlyStarts() { - assertThat(server.getPort()).isGreaterThan(0); - } - - @Test public void hostnameImplicitlyStarts() { - assertThat(server.getHostName()).isNotNull(); - } - - @Test public void toProxyAddressImplicitlyStarts() { - assertThat(server.toProxyAddress()).isNotNull(); - } - - @Test public void differentInstancesGetDifferentPorts() throws IOException { - MockWebServer other = new MockWebServer(); - assertThat(other.getPort()).isNotEqualTo(server.getPort()); - other.shutdown(); - } - - @Test public void statementStartsAndStops() throws Throwable { - final AtomicBoolean called = new AtomicBoolean(); - Statement statement = server.apply(new Statement() { - @Override public void evaluate() throws Throwable { - called.set(true); - server.url("/").url().openConnection().connect(); - } - }, Description.EMPTY); - - statement.evaluate(); - - assertThat(called.get()).isTrue(); - try { - server.url("/").url().openConnection().connect(); - fail(); - } catch (ConnectException expected) { - } - } - - @Test public void shutdownWhileBlockedDispatching() throws Exception { - // Enqueue a request that'll cause MockWebServer to hang on QueueDispatcher.dispatch(). - HttpURLConnection connection = (HttpURLConnection) server.url("/").url().openConnection(); - connection.setReadTimeout(500); - try { - connection.getResponseCode(); - fail(); - } catch (SocketTimeoutException expected) { - } - - // Shutting down the server should unblock the dispatcher. - server.shutdown(); - } - - @Test public void requestUrlReconstructed() throws Exception { - server.enqueue(new MockResponse().setBody("hello world")); - - URL url = server.url("/a/deep/path?key=foo%20bar").url(); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - InputStream in = connection.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8)); - assertThat(connection.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK); - assertThat(reader.readLine()).isEqualTo("hello world"); - - RecordedRequest request = server.takeRequest(); - assertThat(request.getRequestLine()).isEqualTo( - "GET /a/deep/path?key=foo%20bar HTTP/1.1"); - - HttpUrl requestUrl = request.getRequestUrl(); - assertThat(requestUrl.scheme()).isEqualTo("http"); - assertThat(requestUrl.host()).isEqualTo(server.getHostName()); - assertThat(requestUrl.port()).isEqualTo(server.getPort()); - assertThat(requestUrl.encodedPath()).isEqualTo("/a/deep/path"); - assertThat(requestUrl.queryParameter("key")).isEqualTo("foo bar"); - } - - @Test public void shutdownServerAfterRequest() throws Exception { - server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.SHUTDOWN_SERVER_AFTER_RESPONSE)); - - URL url = server.url("/").url(); - - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - assertThat(connection.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK); - - HttpURLConnection refusedConnection = (HttpURLConnection) url.openConnection(); - - try { - refusedConnection.getResponseCode(); - fail("Second connection should be refused"); - } catch (ConnectException e) { - assertThat(e.getMessage()).contains("refused"); - } - } - - @Test public void http100Continue() throws Exception { - server.enqueue(new MockResponse().setBody("response")); - - URL url = server.url("/").url(); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setDoOutput(true); - connection.setRequestProperty("Expect", "100-Continue"); - connection.getOutputStream().write("request".getBytes(UTF_8)); - - InputStream in = connection.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8)); - assertThat(reader.readLine()).isEqualTo("response"); - - RecordedRequest request = server.takeRequest(); - assertThat(request.getBody().readUtf8()).isEqualTo("request"); - } - - @Test public void testH2PriorKnowledgeServerFallback() { - try { - server.setProtocols(asList(Protocol.H2_PRIOR_KNOWLEDGE, Protocol.HTTP_1_1)); - fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected.getMessage()).isEqualTo( - ("protocols containing h2_prior_knowledge cannot use other protocols: " - + "[h2_prior_knowledge, http/1.1]")); - } - } - - @Test public void testH2PriorKnowledgeServerDuplicates() { - try { - // Treating this use case as user error - server.setProtocols(asList(Protocol.H2_PRIOR_KNOWLEDGE, Protocol.H2_PRIOR_KNOWLEDGE)); - fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected.getMessage()).isEqualTo( - ("protocols containing h2_prior_knowledge cannot use other protocols: " - + "[h2_prior_knowledge, h2_prior_knowledge]")); - } - } - - @Test public void testMockWebServerH2PriorKnowledgeProtocol() { - server.setProtocols(asList(Protocol.H2_PRIOR_KNOWLEDGE)); - - assertThat(server.protocols().size()).isEqualTo(1); - assertThat(server.protocols().get(0)).isEqualTo(Protocol.H2_PRIOR_KNOWLEDGE); - } - - @Test public void https() throws Exception { - HandshakeCertificates handshakeCertificates = platform.localhostHandshakeCertificates(); - server.useHttps(handshakeCertificates.sslSocketFactory(), false); - server.enqueue(new MockResponse().setBody("abc")); - - HttpUrl url = server.url("/"); - HttpsURLConnection connection = (HttpsURLConnection) url.url().openConnection(); - connection.setSSLSocketFactory(handshakeCertificates.sslSocketFactory()); - connection.setHostnameVerifier(new RecordingHostnameVerifier()); - - assertThat(connection.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK); - BufferedReader reader = - new BufferedReader(new InputStreamReader(connection.getInputStream(), UTF_8)); - assertThat(reader.readLine()).isEqualTo("abc"); - - RecordedRequest request = server.takeRequest(); - assertThat(request.getRequestUrl().scheme()).isEqualTo("https"); - Handshake handshake = request.getHandshake(); - assertThat(handshake.tlsVersion()).isNotNull(); - assertThat(handshake.cipherSuite()).isNotNull(); - assertThat(handshake.localPrincipal()).isNotNull(); - assertThat(handshake.localCertificates().size()).isEqualTo(1); - assertThat(handshake.peerPrincipal()).isNull(); - assertThat(handshake.peerCertificates().size()).isEqualTo(0); - } - - @Test public void httpsWithClientAuth() throws Exception { - platform.assumeNotBouncyCastle(); - platform.assumeNotConscrypt(); - - HeldCertificate clientCa = new HeldCertificate.Builder() - .certificateAuthority(0) - .build(); - HeldCertificate serverCa = new HeldCertificate.Builder() - .certificateAuthority(0) - .build(); - HeldCertificate serverCertificate = new HeldCertificate.Builder() - .signedBy(serverCa) - .addSubjectAlternativeName(server.getHostName()) - .build(); - HandshakeCertificates serverHandshakeCertificates = new HandshakeCertificates.Builder() - .addTrustedCertificate(clientCa.certificate()) - .heldCertificate(serverCertificate) - .build(); - - server.useHttps(serverHandshakeCertificates.sslSocketFactory(), false); - server.enqueue(new MockResponse().setBody("abc")); - server.requestClientAuth(); - - HeldCertificate clientCertificate = new HeldCertificate.Builder() - .signedBy(clientCa) - .build(); - HandshakeCertificates clientHandshakeCertificates = new HandshakeCertificates.Builder() - .addTrustedCertificate(serverCa.certificate()) - .heldCertificate(clientCertificate) - .build(); - - HttpUrl url = server.url("/"); - HttpsURLConnection connection = (HttpsURLConnection) url.url().openConnection(); - connection.setSSLSocketFactory(clientHandshakeCertificates.sslSocketFactory()); - connection.setHostnameVerifier(new RecordingHostnameVerifier()); - - assertThat(connection.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK); - BufferedReader reader = - new BufferedReader(new InputStreamReader(connection.getInputStream(), UTF_8)); - assertThat(reader.readLine()).isEqualTo("abc"); - - RecordedRequest request = server.takeRequest(); - assertThat(request.getRequestUrl().scheme()).isEqualTo("https"); - Handshake handshake = request.getHandshake(); - assertThat(handshake.tlsVersion()).isNotNull(); - assertThat(handshake.cipherSuite()).isNotNull(); - assertThat(handshake.localPrincipal()).isNotNull(); - assertThat(handshake.localCertificates().size()).isEqualTo(1); - assertThat(handshake.peerPrincipal()).isNotNull(); - assertThat(handshake.peerCertificates().size()).isEqualTo(1); - } - - @Test - public void shutdownTwice() throws IOException { - MockWebServer server2 = new MockWebServer(); - - server2.start(); - server2.shutdown(); - try { - server2.start(); - fail(); - } catch (IllegalStateException expected) { - // expected - } - server2.shutdown(); - } - - public static String getPlatform() { - return System.getProperty("okhttp.platform", "jdk8"); - } -} diff --git a/mockwebserver-deprecated/src/test/java/okhttp3/mockwebserver/MockWebServerTest.kt b/mockwebserver-deprecated/src/test/java/okhttp3/mockwebserver/MockWebServerTest.kt new file mode 100644 index 000000000000..b2db3fde9e4a --- /dev/null +++ b/mockwebserver-deprecated/src/test/java/okhttp3/mockwebserver/MockWebServerTest.kt @@ -0,0 +1,633 @@ +/* + * Copyright (C) 2011 Google Inc. + * + * 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 + * + * 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 okhttp3.mockwebserver + +import java.io.BufferedReader +import java.io.Closeable +import java.io.IOException +import java.io.InputStreamReader +import java.net.ConnectException +import java.net.HttpURLConnection +import java.net.ProtocolException +import java.net.SocketTimeoutException +import java.nio.charset.StandardCharsets +import java.util.Arrays +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import javax.net.ssl.HttpsURLConnection +import okhttp3.Headers +import okhttp3.Protocol +import okhttp3.RecordingHostnameVerifier +import okhttp3.TestUtil.assumeNotWindows +import okhttp3.testing.PlatformRule +import okhttp3.tls.HandshakeCertificates +import okhttp3.tls.HeldCertificate +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.data.Offset +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.fail +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import org.junit.jupiter.api.extension.RegisterExtension +import org.junit.runner.Description +import org.junit.runners.model.Statement + +@Suppress("deprecation") +@Timeout(30) +@Tag("Slow") +class MockWebServerTest { + @RegisterExtension + var platform = PlatformRule() + private val server = MockWebServer() + + @BeforeEach + fun setUp() { + server.start() + } + + @AfterEach + fun tearDown() { + server.shutdown() + } + + @Test + fun defaultMockResponse() { + val response = MockResponse() + assertThat(headersToList(response)).containsExactly("Content-Length: 0") + assertThat(response.status).isEqualTo("HTTP/1.1 200 OK") + } + + @Test + fun setResponseMockReason() { + val reasons = arrayOf( + "Mock Response", + "Informational", + "OK", + "Redirection", + "Client Error", + "Server Error", + "Mock Response" + ) + for (i in 0..599) { + val response = MockResponse().setResponseCode(i) + val expectedReason = reasons[i / 100] + assertThat(response.status).isEqualTo("HTTP/1.1 $i $expectedReason") + assertThat(headersToList(response)).containsExactly("Content-Length: 0") + } + } + + @Test + fun setStatusControlsWholeStatusLine() { + val response = MockResponse().setStatus("HTTP/1.1 202 That'll do pig") + assertThat(headersToList(response)).containsExactly("Content-Length: 0") + assertThat(response.status).isEqualTo("HTTP/1.1 202 That'll do pig") + } + + @Test + fun setBodyAdjustsHeaders() { + val response = MockResponse().setBody("ABC") + assertThat(headersToList(response)).containsExactly("Content-Length: 3") + assertThat(response.getBody()!!.readUtf8()).isEqualTo("ABC") + } + + @Test + fun mockResponseAddHeader() { + val response = MockResponse() + .clearHeaders() + .addHeader("Cookie: s=square") + .addHeader("Cookie", "a=android") + assertThat(headersToList(response)) + .containsExactly("Cookie: s=square", "Cookie: a=android") + } + + @Test + fun mockResponseSetHeader() { + val response = MockResponse() + .clearHeaders() + .addHeader("Cookie: s=square") + .addHeader("Cookie: a=android") + .addHeader("Cookies: delicious") + response.setHeader("cookie", "r=robot") + assertThat(headersToList(response)) + .containsExactly("Cookies: delicious", "cookie: r=robot") + } + + @Test + fun mockResponseSetHeaders() { + val response = MockResponse() + .clearHeaders() + .addHeader("Cookie: s=square") + .addHeader("Cookies: delicious") + response.setHeaders(Headers.Builder().add("Cookie", "a=android").build()) + assertThat(headersToList(response)).containsExactly("Cookie: a=android") + } + + @Test + fun regularResponse() { + server.enqueue(MockResponse().setBody("hello world")) + val url = server.url("/").toUrl() + val connection = url.openConnection() as HttpURLConnection + connection.setRequestProperty("Accept-Language", "en-US") + val inputStream = connection.inputStream + val reader = BufferedReader(InputStreamReader(inputStream, StandardCharsets.UTF_8)) + assertThat(connection.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK) + assertThat(reader.readLine()).isEqualTo("hello world") + val request = server.takeRequest() + assertThat(request.requestLine).isEqualTo("GET / HTTP/1.1") + assertThat(request.getHeader("Accept-Language")).isEqualTo("en-US") + + // Server has no more requests. + assertThat(server.takeRequest(100, TimeUnit.MILLISECONDS)).isNull() + } + + @Test + fun redirect() { + server.enqueue( + MockResponse() + .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) + .addHeader("Location: " + server.url("/new-path")) + .setBody("This page has moved!") + ) + server.enqueue(MockResponse().setBody("This is the new location!")) + val connection = server.url("/").toUrl().openConnection() + val inputStream = connection.getInputStream() + val reader = BufferedReader(InputStreamReader(inputStream, StandardCharsets.UTF_8)) + assertThat(reader.readLine()).isEqualTo("This is the new location!") + val first = server.takeRequest() + assertThat(first.requestLine).isEqualTo("GET / HTTP/1.1") + val redirect = server.takeRequest() + assertThat(redirect.requestLine).isEqualTo("GET /new-path HTTP/1.1") + } + + /** + * Test that MockWebServer blocks for a call to enqueue() if a request is made before a mock + * response is ready. + */ + @Test + fun dispatchBlocksWaitingForEnqueue() { + Thread { + try { + Thread.sleep(1000) + } catch (ignored: InterruptedException) { + } + server.enqueue(MockResponse().setBody("enqueued in the background")) + }.start() + val connection = server.url("/").toUrl().openConnection() + val inputStream = connection.getInputStream() + val reader = BufferedReader(InputStreamReader(inputStream, StandardCharsets.UTF_8)) + assertThat(reader.readLine()).isEqualTo("enqueued in the background") + } + + @Test + fun nonHexadecimalChunkSize() { + server.enqueue( + MockResponse() + .setBody("G\r\nxxxxxxxxxxxxxxxx\r\n0\r\n\r\n") + .clearHeaders() + .addHeader("Transfer-encoding: chunked") + ) + val connection = server.url("/").toUrl().openConnection() + val inputStream = connection.getInputStream() + try { + inputStream.read() + fail() + } catch (expected: IOException) { + } + } + + @Test + fun responseTimeout() { + server.enqueue( + MockResponse() + .setBody("ABC") + .clearHeaders() + .addHeader("Content-Length: 4") + ) + server.enqueue(MockResponse().setBody("DEF")) + val urlConnection = server.url("/").toUrl().openConnection() + urlConnection.setReadTimeout(1000) + val inputStream = urlConnection.getInputStream() + assertThat(inputStream.read()).isEqualTo('A'.code) + assertThat(inputStream.read()).isEqualTo('B'.code) + assertThat(inputStream.read()).isEqualTo('C'.code) + try { + inputStream.read() // if Content-Length was accurate, this would return -1 immediately + fail() + } catch (expected: SocketTimeoutException) { + } + val urlConnection2 = server.url("/").toUrl().openConnection() + val in2 = urlConnection2.getInputStream() + assertThat(in2.read()).isEqualTo('D'.code) + assertThat(in2.read()).isEqualTo('E'.code) + assertThat(in2.read()).isEqualTo('F'.code) + assertThat(in2.read()).isEqualTo(-1) + assertThat(server.takeRequest().sequenceNumber).isEqualTo(0) + assertThat(server.takeRequest().sequenceNumber).isEqualTo(0) + } + + @Disabled("Not actually failing where expected") + @Test + fun disconnectAtStart() { + server.enqueue( + MockResponse() + .setSocketPolicy(SocketPolicy.DISCONNECT_AT_START) + ) + server.enqueue(MockResponse()) // The jdk's HttpUrlConnection is a bastard. + server.enqueue(MockResponse()) + try { + server.url("/a").toUrl().openConnection().getInputStream() + fail() + } catch (expected: IOException) { + } + server.url("/b").toUrl().openConnection().getInputStream() // Should succeed. + } + + /** + * Throttle the request body by sleeping 500ms after every 3 bytes. With a 6-byte request, this + * should yield one sleep for a total delay of 500ms. + */ + @Test + fun throttleRequest() { + assumeNotWindows() + server.enqueue( + MockResponse() + .throttleBody(3, 500, TimeUnit.MILLISECONDS) + ) + val startNanos = System.nanoTime() + val connection = server.url("/").toUrl().openConnection() + connection.setDoOutput(true) + connection.getOutputStream().write("ABCDEF".toByteArray(StandardCharsets.UTF_8)) + val inputStream = connection.getInputStream() + assertThat(inputStream.read()).isEqualTo(-1) + val elapsedNanos = System.nanoTime() - startNanos + val elapsedMillis = TimeUnit.NANOSECONDS.toMillis(elapsedNanos) + assertThat(elapsedMillis).isBetween(500L, 1000L) + } + + /** + * Throttle the response body by sleeping 500ms after every 3 bytes. With a 6-byte response, this + * should yield one sleep for a total delay of 500ms. + */ + @Test + fun throttleResponse() { + assumeNotWindows() + server.enqueue( + MockResponse() + .setBody("ABCDEF") + .throttleBody(3, 500, TimeUnit.MILLISECONDS) + ) + val startNanos = System.nanoTime() + val connection = server.url("/").toUrl().openConnection() + val inputStream = connection.getInputStream() + assertThat(inputStream.read()).isEqualTo('A'.code) + assertThat(inputStream.read()).isEqualTo('B'.code) + assertThat(inputStream.read()).isEqualTo('C'.code) + assertThat(inputStream.read()).isEqualTo('D'.code) + assertThat(inputStream.read()).isEqualTo('E'.code) + assertThat(inputStream.read()).isEqualTo('F'.code) + assertThat(inputStream.read()).isEqualTo(-1) + val elapsedNanos = System.nanoTime() - startNanos + val elapsedMillis = TimeUnit.NANOSECONDS.toMillis(elapsedNanos) + assertThat(elapsedMillis).isBetween(500L, 1000L) + } + + /** Delay the response body by sleeping 1s. */ + @Test + fun delayResponse() { + assumeNotWindows() + server.enqueue( + MockResponse() + .setBody("ABCDEF") + .setBodyDelay(1, TimeUnit.SECONDS) + ) + val startNanos = System.nanoTime() + val connection = server.url("/").toUrl().openConnection() + val inputStream = connection.getInputStream() + assertThat(inputStream.read()).isEqualTo('A'.code) + val elapsedNanos = System.nanoTime() - startNanos + val elapsedMillis = TimeUnit.NANOSECONDS.toMillis(elapsedNanos) + assertThat(elapsedMillis).isGreaterThanOrEqualTo(1000L) + inputStream.close() + } + + @Test + fun disconnectRequestHalfway() { + server.enqueue(MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_REQUEST_BODY)) + // Limit the size of the request body that the server holds in memory to an arbitrary + // 3.5 MBytes so this test can pass on devices with little memory. + server.bodyLimit = 7 * 512 * 1024 + val connection = server.url("/").toUrl().openConnection() as HttpURLConnection + connection.setRequestMethod("POST") + connection.setDoOutput(true) + connection.setFixedLengthStreamingMode(1024 * 1024 * 1024) // 1 GB + connection.connect() + val out = connection.outputStream + val data = ByteArray(1024 * 1024) + var i = 0 + while (i < 1024) { + try { + out.write(data) + out.flush() + if (i == 513) { + // pause slightly after halfway to make result more predictable + Thread.sleep(100) + } + } catch (e: IOException) { + break + } + i++ + } + // Halfway +/- 0.5% + assertThat(i.toFloat()).isCloseTo(512f, Offset.offset(5f)) + } + + @Test + fun disconnectResponseHalfway() { + server.enqueue( + MockResponse() + .setBody("ab") + .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY) + ) + val connection = server.url("/").toUrl().openConnection() + assertThat(connection.getContentLength()).isEqualTo(2) + val inputStream = connection.getInputStream() + assertThat(inputStream.read()).isEqualTo('a'.code) + try { + val byteRead = inputStream.read() + // OpenJDK behavior: end of stream. + assertThat(byteRead).isEqualTo(-1) + } catch (e: ProtocolException) { + // On Android, HttpURLConnection is implemented by OkHttp v2. OkHttp + // treats an incomplete response body as a ProtocolException. + } + } + + private fun headersToList(response: MockResponse): List { + val headers = response.headers + val size = headers.size + val headerList: MutableList = ArrayList(size) + for (i in 0 until size) { + headerList.add(headers.name(i) + ": " + headers.value(i)) + } + return headerList + } + + @Test + fun shutdownWithoutStart() { + val server = MockWebServer() + server.shutdown() + } + + @Test + fun closeViaClosable() { + val server: Closeable = MockWebServer() + server.close() + } + + @Test + fun shutdownWithoutEnqueue() { + val server = MockWebServer() + server.start() + server.shutdown() + } + + @Test + fun portImplicitlyStarts() { + assertThat(server.port).isGreaterThan(0) + } + + @Test + fun hostnameImplicitlyStarts() { + assertThat(server.hostName).isNotNull() + } + + @Test + fun toProxyAddressImplicitlyStarts() { + assertThat(server.toProxyAddress()).isNotNull() + } + + @Test + fun differentInstancesGetDifferentPorts() { + val other = MockWebServer() + assertThat(other.port).isNotEqualTo(server.port) + other.shutdown() + } + + @Test + fun statementStartsAndStops() { + val called = AtomicBoolean() + val statement = server.apply(object : Statement() { + override fun evaluate() { + called.set(true) + server.url("/").toUrl().openConnection().connect() + } + }, Description.EMPTY) + statement.evaluate() + assertThat(called.get()).isTrue() + try { + server.url("/").toUrl().openConnection().connect() + fail() + } catch (expected: ConnectException) { + } + } + + @Test + fun shutdownWhileBlockedDispatching() { + // Enqueue a request that'll cause MockWebServer to hang on QueueDispatcher.dispatch(). + val connection = server.url("/").toUrl().openConnection() as HttpURLConnection + connection.setReadTimeout(500) + try { + connection.getResponseCode() + fail() + } catch (expected: SocketTimeoutException) { + } + + // Shutting down the server should unblock the dispatcher. + server.shutdown() + } + + @Test + fun requestUrlReconstructed() { + server.enqueue(MockResponse().setBody("hello world")) + val url = server.url("/a/deep/path?key=foo%20bar").toUrl() + val connection = url.openConnection() as HttpURLConnection + val inputStream = connection.inputStream + val reader = BufferedReader(InputStreamReader(inputStream, StandardCharsets.UTF_8)) + assertThat(connection.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK) + assertThat(reader.readLine()).isEqualTo("hello world") + val request = server.takeRequest() + assertThat(request.requestLine).isEqualTo( + "GET /a/deep/path?key=foo%20bar HTTP/1.1" + ) + val requestUrl = request.requestUrl + assertThat(requestUrl!!.scheme).isEqualTo("http") + assertThat(requestUrl.host).isEqualTo(server.hostName) + assertThat(requestUrl.port).isEqualTo(server.port) + assertThat(requestUrl.encodedPath).isEqualTo("/a/deep/path") + assertThat(requestUrl.queryParameter("key")).isEqualTo("foo bar") + } + + @Test + fun shutdownServerAfterRequest() { + server.enqueue(MockResponse().setSocketPolicy(SocketPolicy.SHUTDOWN_SERVER_AFTER_RESPONSE)) + val url = server.url("/").toUrl() + val connection = url.openConnection() as HttpURLConnection + assertThat(connection.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK) + val refusedConnection = url.openConnection() as HttpURLConnection + try { + refusedConnection.getResponseCode() + fail("Second connection should be refused") + } catch (e: ConnectException) { + assertThat(e.message).contains("refused") + } + } + + @Test + fun http100Continue() { + server.enqueue(MockResponse().setBody("response")) + val url = server.url("/").toUrl() + val connection = url.openConnection() as HttpURLConnection + connection.setDoOutput(true) + connection.setRequestProperty("Expect", "100-Continue") + connection.outputStream.write("request".toByteArray(StandardCharsets.UTF_8)) + val inputStream = connection.inputStream + val reader = BufferedReader(InputStreamReader(inputStream, StandardCharsets.UTF_8)) + assertThat(reader.readLine()).isEqualTo("response") + val request = server.takeRequest() + assertThat(request.body.readUtf8()).isEqualTo("request") + } + + @Test + fun testH2PriorKnowledgeServerFallback() { + try { + server.protocols = Arrays.asList(Protocol.H2_PRIOR_KNOWLEDGE, Protocol.HTTP_1_1) + fail() + } catch (expected: IllegalArgumentException) { + assertThat(expected.message).isEqualTo( + "protocols containing h2_prior_knowledge cannot use other protocols: " + + "[h2_prior_knowledge, http/1.1]" + ) + } + } + + @Test + fun testH2PriorKnowledgeServerDuplicates() { + try { + // Treating this use case as user error + server.protocols = listOf(Protocol.H2_PRIOR_KNOWLEDGE, Protocol.H2_PRIOR_KNOWLEDGE) + fail() + } catch (expected: IllegalArgumentException) { + assertThat(expected.message).isEqualTo( + "protocols containing h2_prior_knowledge cannot use other protocols: " + + "[h2_prior_knowledge, h2_prior_knowledge]" + ) + } + } + + @Test + fun testMockWebServerH2PriorKnowledgeProtocol() { + server.protocols = Arrays.asList(Protocol.H2_PRIOR_KNOWLEDGE) + assertThat(server.protocols.size).isEqualTo(1) + assertThat(server.protocols[0]).isEqualTo(Protocol.H2_PRIOR_KNOWLEDGE) + } + + @Test + fun https() { + val handshakeCertificates = platform.localhostHandshakeCertificates() + server.useHttps(handshakeCertificates.sslSocketFactory(), false) + server.enqueue(MockResponse().setBody("abc")) + val url = server.url("/") + val connection = url.toUrl().openConnection() as HttpsURLConnection + connection.setSSLSocketFactory(handshakeCertificates.sslSocketFactory()) + connection.setHostnameVerifier(RecordingHostnameVerifier()) + assertThat(connection.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK) + val reader = BufferedReader(InputStreamReader(connection.inputStream, StandardCharsets.UTF_8)) + assertThat(reader.readLine()).isEqualTo("abc") + val request = server.takeRequest() + assertThat(request.requestUrl!!.scheme).isEqualTo("https") + val handshake = request.handshake + assertThat(handshake!!.tlsVersion).isNotNull() + assertThat(handshake.cipherSuite).isNotNull() + assertThat(handshake.localPrincipal).isNotNull() + assertThat(handshake.localCertificates.size).isEqualTo(1) + assertThat(handshake.peerPrincipal).isNull() + assertThat(handshake.peerCertificates.size).isEqualTo(0) + } + + @Test + fun httpsWithClientAuth() { + platform.assumeNotBouncyCastle() + platform.assumeNotConscrypt() + val clientCa = HeldCertificate.Builder() + .certificateAuthority(0) + .build() + val serverCa = HeldCertificate.Builder() + .certificateAuthority(0) + .build() + val serverCertificate = HeldCertificate.Builder() + .signedBy(serverCa) + .addSubjectAlternativeName(server.hostName) + .build() + val serverHandshakeCertificates = HandshakeCertificates.Builder() + .addTrustedCertificate(clientCa.certificate) + .heldCertificate(serverCertificate) + .build() + server.useHttps(serverHandshakeCertificates.sslSocketFactory(), false) + server.enqueue(MockResponse().setBody("abc")) + server.requestClientAuth() + val clientCertificate = HeldCertificate.Builder() + .signedBy(clientCa) + .build() + val clientHandshakeCertificates = HandshakeCertificates.Builder() + .addTrustedCertificate(serverCa.certificate) + .heldCertificate(clientCertificate) + .build() + val url = server.url("/") + val connection = url.toUrl().openConnection() as HttpsURLConnection + connection.setSSLSocketFactory(clientHandshakeCertificates.sslSocketFactory()) + connection.setHostnameVerifier(RecordingHostnameVerifier()) + assertThat(connection.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK) + val reader = + BufferedReader(InputStreamReader(connection.inputStream, StandardCharsets.UTF_8)) + assertThat(reader.readLine()).isEqualTo("abc") + val request = server.takeRequest() + assertThat(request.requestUrl!!.scheme).isEqualTo("https") + val handshake = request.handshake + assertThat(handshake!!.tlsVersion).isNotNull() + assertThat(handshake.cipherSuite).isNotNull() + assertThat(handshake.localPrincipal).isNotNull() + assertThat(handshake.localCertificates.size).isEqualTo(1) + assertThat(handshake.peerPrincipal).isNotNull() + assertThat(handshake.peerCertificates.size).isEqualTo(1) + } + + @Test + fun shutdownTwice() { + val server2 = MockWebServer() + server2.start() + server2.shutdown() + try { + server2.start() + fail() + } catch (expected: IllegalStateException) { + // expected + } + server2.shutdown() + } +} diff --git a/mockwebserver-junit4/src/test/java/mockwebserver3/junit4/MockWebServerRuleTest.kt b/mockwebserver-junit4/src/test/java/mockwebserver3/junit4/MockWebServerRuleTest.kt index 69a9a0a40969..a72065774771 100644 --- a/mockwebserver-junit4/src/test/java/mockwebserver3/junit4/MockWebServerRuleTest.kt +++ b/mockwebserver-junit4/src/test/java/mockwebserver3/junit4/MockWebServerRuleTest.kt @@ -29,7 +29,7 @@ class MockWebServerRuleTest { val rule = MockWebServerRule() val called = AtomicBoolean() val statement: Statement = rule.apply(object : Statement() { - @Throws(Throwable::class) override fun evaluate() { + override fun evaluate() { called.set(true) rule.server.url("/").toUrl().openConnection().connect() } diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.java b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.java deleted file mode 100644 index b5da35a5ee86..000000000000 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.java +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Copyright (C) 2018 Square, Inc. - * - * 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 - * - * 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 okhttp3.dnsoverhttps; - -import java.io.EOFException; -import java.io.File; -import java.io.IOException; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.TimeUnit; -import mockwebserver3.MockResponse; -import mockwebserver3.MockWebServer; -import mockwebserver3.RecordedRequest; -import mockwebserver3.junit5.internal.MockWebServerExtension; -import okhttp3.Cache; -import okhttp3.Dns; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Protocol; -import okhttp3.testing.PlatformRule; -import okio.Buffer; -import okio.ByteString; -import okio.FileSystem; -import okio.Path; -import okio.fakefilesystem.FakeFileSystem; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.RegisterExtension; - -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.fail; - -@ExtendWith(MockWebServerExtension.class) -@Tag("Slowish") -public class DnsOverHttpsTest { - @RegisterExtension public final PlatformRule platform = new PlatformRule(); - - private MockWebServer server; - private Dns dns; - private final FileSystem cacheFs = new FakeFileSystem(); - - private final OkHttpClient bootstrapClient = new OkHttpClient.Builder() - .protocols(asList(Protocol.HTTP_2, Protocol.HTTP_1_1)) - .build(); - - @BeforeEach public void setUp(MockWebServer server) { - this.server = server; - server.setProtocols(bootstrapClient.protocols()); - dns = buildLocalhost(bootstrapClient, false); - } - - @Test public void getOne() throws Exception { - server.enqueue(dnsResponse( - "0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c00050001" - + "00000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010" - + "0010000003b00049df00112")); - - List result = dns.lookup("google.com"); - - assertThat(result).isEqualTo(singletonList(address("157.240.1.18"))); - - RecordedRequest recordedRequest = server.takeRequest(); - assertThat(recordedRequest.getMethod()).isEqualTo("GET"); - assertThat(recordedRequest.getPath()).isEqualTo( - "/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ"); - } - - @Test public void getIpv6() throws Exception { - server.enqueue(dnsResponse( - "0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c00050001" - + "00000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010" - + "0010000003b00049df00112")); - server.enqueue(dnsResponse( - "0000818000010003000000000567726170680866616365626f6f6b03636f6d00001c0001c00c00050001" - + "00000a1b000603617069c012c0300005000100000b1f000c04737461720463313072c012c042001c0" - + "0010000003b00102a032880f0290011faceb00c00000002")); - - dns = buildLocalhost(bootstrapClient, true); - - List result = dns.lookup("google.com"); - - assertThat(result.size()).isEqualTo(2); - assertThat(result).contains(address("157.240.1.18")); - assertThat(result).contains(address("2a03:2880:f029:11:face:b00c:0:2")); - - RecordedRequest request1 = server.takeRequest(); - assertThat(request1.getMethod()).isEqualTo("GET"); - - RecordedRequest request2 = server.takeRequest(); - assertThat(request2.getMethod()).isEqualTo("GET"); - - assertThat(asList(request1.getPath(), request2.getPath())).containsExactlyInAnyOrder( - "/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ", - "/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AABwAAQ"); - } - - @Test public void failure() throws Exception { - server.enqueue(dnsResponse( - "0000818300010000000100000e7364666c6b686673646c6b6a64660265650000010001c01b00060001" - + "000007070038026e7303746c64c01b0a686f73746d61737465720d6565737469696e7465726e657" - + "4c01b5adb12c100000e10000003840012750000000e10")); - - try { - dns.lookup("google.com"); - fail(); - } catch (UnknownHostException uhe) { - uhe.printStackTrace(); - assertThat(uhe.getMessage()).isEqualTo("google.com: NXDOMAIN"); - } - - RecordedRequest recordedRequest = server.takeRequest(); - assertThat(recordedRequest.getMethod()).isEqualTo("GET"); - assertThat(recordedRequest.getPath()).isEqualTo( - "/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ"); - } - - @Test public void failOnExcessiveResponse() { - char[] array = new char[128 * 1024 + 2]; - Arrays.fill(array, '0'); - server.enqueue(dnsResponse(new String(array))); - - try { - dns.lookup("google.com"); - fail(); - } catch (IOException ioe) { - assertThat(ioe.getMessage()).isEqualTo("google.com"); - Throwable cause = ioe.getCause(); - assertThat(cause).isInstanceOf(IOException.class); - assertThat(cause).hasMessage("response size exceeds limit (65536 bytes): 65537 bytes"); - } - } - - @Test public void failOnBadResponse() { - server.enqueue(dnsResponse("00")); - - try { - dns.lookup("google.com"); - fail(); - } catch (IOException ioe) { - assertThat(ioe).hasMessage("google.com"); - assertThat(ioe.getCause()).isInstanceOf(EOFException.class); - } - } - - // TODO GET preferred order - with tests to confirm this - // 1. successful fresh cached GET response - // 2. unsuccessful (404, 500) fresh cached GET response - // 3. successful network response - // 4. successful stale cached GET response - // 5. unsuccessful response - - // TODO how closely to follow POST rules on caching? - - @Test public void usesCache() throws Exception { - Cache cache = new Cache(Path.get("cache"), 100 * 1024, cacheFs); - OkHttpClient cachedClient = bootstrapClient.newBuilder().cache(cache).build(); - DnsOverHttps cachedDns = buildLocalhost(cachedClient, false); - - server.enqueue(dnsResponse( - "0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c00050001" - + "00000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010" - + "0010000003b00049df00112") - .newBuilder() - .setHeader("cache-control", "private, max-age=298") - .build()); - - List result = cachedDns.lookup("google.com"); - - assertThat(result).containsExactly(address("157.240.1.18")); - - RecordedRequest recordedRequest = server.takeRequest(); - assertThat(recordedRequest.getMethod()).isEqualTo("GET"); - assertThat(recordedRequest.getPath()).isEqualTo( - "/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ"); - - result = cachedDns.lookup("google.com"); - assertThat(result).isEqualTo(singletonList(address("157.240.1.18"))); - } - - @Test public void usesCacheOnlyIfFresh() throws Exception { - Cache cache = new Cache(new File("./target/DnsOverHttpsTest.cache"), 100 * 1024); - OkHttpClient cachedClient = bootstrapClient.newBuilder().cache(cache).build(); - DnsOverHttps cachedDns = buildLocalhost(cachedClient, false); - - server.enqueue(dnsResponse( - "0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c00050001" - + "00000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010" - + "0010000003b00049df00112") - .newBuilder() - .setHeader("cache-control", "max-age=1") - .build()); - - List result = cachedDns.lookup("google.com"); - - assertThat(result).containsExactly(address("157.240.1.18")); - - RecordedRequest recordedRequest = server.takeRequest(0, TimeUnit.SECONDS); - assertThat(recordedRequest.getMethod()).isEqualTo("GET"); - assertThat(recordedRequest.getPath()).isEqualTo( - "/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ"); - - Thread.sleep(2000); - - server.enqueue(dnsResponse( - "0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c00050001" - + "00000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010" - + "0010000003b00049df00112") - .newBuilder() - .setHeader("cache-control", "max-age=1") - .build()); - - result = cachedDns.lookup("google.com"); - assertThat(result).isEqualTo(singletonList(address("157.240.1.18"))); - - recordedRequest = server.takeRequest(0, TimeUnit.SECONDS); - assertThat(recordedRequest.getMethod()).isEqualTo("GET"); - assertThat(recordedRequest.getPath()).isEqualTo( - "/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ"); - } - - private MockResponse dnsResponse(String s) { - return new MockResponse.Builder() - .body(new Buffer().write(ByteString.decodeHex(s))) - .addHeader("content-type", "application/dns-message") - .addHeader("content-length", s.length() / 2) - .build(); - } - - private DnsOverHttps buildLocalhost(OkHttpClient bootstrapClient, boolean includeIPv6) { - HttpUrl url = server.url("/lookup?ct"); - return new DnsOverHttps.Builder().client(bootstrapClient) - .includeIPv6(includeIPv6) - .resolvePrivateAddresses(true) - .url(url) - .build(); - } - - private static InetAddress address(String host) { - try { - return InetAddress.getByName(host); - } catch (UnknownHostException e) { - // impossible for IP addresses - throw new RuntimeException(e); - } - } -} diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt new file mode 100644 index 000000000000..7cc2f9cf88fe --- /dev/null +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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 + * + * 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 okhttp3.dnsoverhttps + +import java.io.EOFException +import java.io.File +import java.io.IOException +import java.net.InetAddress +import java.net.UnknownHostException +import java.util.Arrays +import java.util.concurrent.TimeUnit +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import mockwebserver3.junit5.internal.MockWebServerExtension +import okhttp3.Cache +import okhttp3.Dns +import okhttp3.OkHttpClient +import okhttp3.Protocol +import okhttp3.testing.PlatformRule +import okio.Buffer +import okio.ByteString.Companion.decodeHex +import okio.Path.Companion.toPath +import okio.fakefilesystem.FakeFileSystem +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.fail +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.RegisterExtension + +@ExtendWith(MockWebServerExtension::class) +@Tag("Slowish") +class DnsOverHttpsTest { + @RegisterExtension + val platform = PlatformRule() + private lateinit var server: MockWebServer + private lateinit var dns: Dns + private val cacheFs = FakeFileSystem() + private val bootstrapClient = OkHttpClient.Builder() + .protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1)) + .build() + + @BeforeEach + fun setUp(server: MockWebServer) { + this.server = server + server.protocols = bootstrapClient.protocols + dns = buildLocalhost(bootstrapClient, false) + } + + @Test + fun getOne() { + server.enqueue( + dnsResponse( + "0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c000500010" + + "0000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010001000" + + "0003b00049df00112" + ) + ) + val result = dns.lookup("google.com") + assertThat(result).isEqualTo(listOf(address("157.240.1.18"))) + val recordedRequest = server.takeRequest() + assertThat(recordedRequest.method).isEqualTo("GET") + assertThat(recordedRequest.path) + .isEqualTo("/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ") + } + + @Test + fun getIpv6() { + server.enqueue( + dnsResponse( + "0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c0005000" + + "100000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c0420001000" + + "10000003b00049df00112" + ) + ) + server.enqueue( + dnsResponse( + "0000818000010003000000000567726170680866616365626f6f6b03636f6d00001c0001c00c0005000" + + "100000a1b000603617069c012c0300005000100000b1f000c04737461720463313072c012c042001c000" + + "10000003b00102a032880f0290011faceb00c00000002" + ) + ) + dns = buildLocalhost(bootstrapClient, true) + val result = dns.lookup("google.com") + assertThat(result.size).isEqualTo(2) + assertThat(result).contains(address("157.240.1.18")) + assertThat(result).contains(address("2a03:2880:f029:11:face:b00c:0:2")) + val request1 = server.takeRequest() + assertThat(request1.method).isEqualTo("GET") + val request2 = server.takeRequest() + assertThat(request2.method).isEqualTo("GET") + assertThat(Arrays.asList(request1.path, request2.path)) + .containsExactlyInAnyOrder( + "/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ", + "/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AABwAAQ" + ) + } + + @Test + fun failure() { + server.enqueue( + dnsResponse( + "0000818300010000000100000e7364666c6b686673646c6b6a64660265650000010001c01b00060001000" + + "007070038026e7303746c64c01b0a686f73746d61737465720d6565737469696e7465726e6574c01b5adb1" + + "2c100000e10000003840012750000000e10" + ) + ) + try { + dns.lookup("google.com") + fail() + } catch (uhe: UnknownHostException) { + assertThat(uhe.message).isEqualTo("google.com: NXDOMAIN") + } + val recordedRequest = server.takeRequest() + assertThat(recordedRequest.method).isEqualTo("GET") + assertThat(recordedRequest.path) + .isEqualTo("/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ") + } + + @Test + fun failOnExcessiveResponse() { + val array = CharArray(128 * 1024 + 2) { '0' } + server.enqueue(dnsResponse(String(array))) + try { + dns.lookup("google.com") + fail() + } catch (ioe: IOException) { + assertThat(ioe.message).isEqualTo("google.com") + val cause = ioe.cause + assertThat(cause).isInstanceOf(IOException::class.java) + assertThat(cause).hasMessage("response size exceeds limit (65536 bytes): 65537 bytes") + } + } + + @Test + fun failOnBadResponse() { + server.enqueue(dnsResponse("00")) + try { + dns.lookup("google.com") + fail() + } catch (ioe: IOException) { + assertThat(ioe).hasMessage("google.com") + assertThat(ioe.cause).isInstanceOf(EOFException::class.java) + } + } + + // TODO GET preferred order - with tests to confirm this + // 1. successful fresh cached GET response + // 2. unsuccessful (404, 500) fresh cached GET response + // 3. successful network response + // 4. successful stale cached GET response + // 5. unsuccessful response + // TODO how closely to follow POST rules on caching? + @Test + fun usesCache() { + val cache = Cache("cache".toPath(), (100 * 1024).toLong(), cacheFs) + val cachedClient = bootstrapClient.newBuilder().cache(cache).build() + val cachedDns = buildLocalhost(cachedClient, false) + server.enqueue( + dnsResponse( + "0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c000500010" + + "0000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010001000" + + "0003b00049df00112" + ) + .newBuilder() + .setHeader("cache-control", "private, max-age=298") + .build() + ) + var result = cachedDns.lookup("google.com") + assertThat(result).containsExactly(address("157.240.1.18")) + val recordedRequest = server.takeRequest() + assertThat(recordedRequest.method).isEqualTo("GET") + assertThat(recordedRequest.path) + .isEqualTo("/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ") + result = cachedDns.lookup("google.com") + assertThat(result).isEqualTo(listOf(address("157.240.1.18"))) + } + + @Test + fun usesCacheOnlyIfFresh() { + val cache = Cache(File("./target/DnsOverHttpsTest.cache"), 100 * 1024L) + val cachedClient = bootstrapClient.newBuilder().cache(cache).build() + val cachedDns = buildLocalhost(cachedClient, false) + server.enqueue( + dnsResponse( + "0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c000500010" + + "0000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010001000" + + "0003b00049df00112" + ) + .newBuilder() + .setHeader("cache-control", "max-age=1") + .build() + ) + var result = cachedDns.lookup("google.com") + assertThat(result).containsExactly(address("157.240.1.18")) + var recordedRequest = server.takeRequest(0, TimeUnit.SECONDS) + assertThat(recordedRequest!!.method).isEqualTo("GET") + assertThat(recordedRequest.path).isEqualTo( + "/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ" + ) + Thread.sleep(2000) + server.enqueue( + dnsResponse( + "0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c000500010" + + "0000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010001000" + + "0003b00049df00112" + ) + .newBuilder() + .setHeader("cache-control", "max-age=1") + .build() + ) + result = cachedDns.lookup("google.com") + assertThat(result).isEqualTo(listOf(address("157.240.1.18"))) + recordedRequest = server.takeRequest(0, TimeUnit.SECONDS) + assertThat(recordedRequest!!.method).isEqualTo("GET") + assertThat(recordedRequest.path) + .isEqualTo("/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ") + } + + private fun dnsResponse(s: String): MockResponse { + return MockResponse.Builder() + .body(Buffer().write(s.decodeHex())) + .addHeader("content-type", "application/dns-message") + .addHeader("content-length", s.length / 2) + .build() + } + + private fun buildLocalhost(bootstrapClient: OkHttpClient, includeIPv6: Boolean): DnsOverHttps { + val url = server.url("/lookup?ct") + return DnsOverHttps.Builder().client(bootstrapClient) + .includeIPv6(includeIPv6) + .resolvePrivateAddresses(true) + .url(url) + .build() + } + + companion object { + private fun address(host: String) = InetAddress.getByName(host) + } +} diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsRecordCodecTest.java b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsRecordCodecTest.java deleted file mode 100644 index 3186522783ca..000000000000 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsRecordCodecTest.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (C) 2018 Square, Inc. - * - * 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 - * - * 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 okhttp3.dnsoverhttps; - -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.List; -import okio.ByteString; -import org.junit.jupiter.api.Test; - -import static okhttp3.dnsoverhttps.DnsRecordCodec.TYPE_A; -import static okhttp3.dnsoverhttps.DnsRecordCodec.TYPE_AAAA; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - -public class DnsRecordCodecTest { - @Test public void testGoogleDotComEncoding() { - String encoded = encodeQuery("google.com", TYPE_A); - - assertThat(encoded).isEqualTo("AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ"); - } - - private String encodeQuery(String host, int type) { - return DnsRecordCodec.INSTANCE.encodeQuery(host, type).base64Url().replace("=", ""); - } - - @Test public void testGoogleDotComEncodingWithIPv6() { - String encoded = encodeQuery("google.com", TYPE_AAAA); - - assertThat(encoded).isEqualTo("AAABAAABAAAAAAAABmdvb2dsZQNjb20AABwAAQ"); - } - - @Test public void testGoogleDotComDecodingFromCloudflare() throws Exception { - List encoded = DnsRecordCodec.INSTANCE.decodeAnswers("test.com", - ByteString.decodeHex("00008180000100010000000006676f6f676c6503636f6d0000010001c00c000100010" - + "00000430004d83ad54e")); - - assertThat(encoded).containsExactly(InetAddress.getByName("216.58.213.78")); - } - - @Test public void testGoogleDotComDecodingFromGoogle() throws Exception { - List decoded = DnsRecordCodec.INSTANCE.decodeAnswers("test.com", - ByteString.decodeHex("0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010" - + "001c00c0005000100000a6d000603617069c012c0300005000100000cde000c04737461720463313072c" - + "012c042000100010000003b00049df00112")); - - assertThat(decoded).containsExactly(InetAddress.getByName("157.240.1.18")); - } - - @Test public void testGoogleDotComDecodingFromGoogleIPv6() throws Exception { - List decoded = DnsRecordCodec.INSTANCE.decodeAnswers("test.com", - ByteString.decodeHex("0000818000010003000000000567726170680866616365626f6f6b03636f6d00001c0" - + "001c00c0005000100000a1b000603617069c012c0300005000100000b1f000c04737461720463313072c" - + "012c042001c00010000003b00102a032880f0290011faceb00c00000002")); - - assertThat(decoded).containsExactly(InetAddress.getByName("2a03:2880:f029:11:face:b00c:0:2")); - } - - @Test public void testGoogleDotComDecodingNxdomainFailure() throws Exception { - try { - DnsRecordCodec.INSTANCE.decodeAnswers("sdflkhfsdlkjdf.ee", ByteString.decodeHex("000081830001" - + "0000000100000e7364666c6b686673646c6b6a64660265650000010001c01b00060001000007070038026e" - + "7303746c64c01b0a686f73746d61737465720d6565737469696e7465726e6574c01b5adb12c100000e1000" - + "0003840012750000000e10")); - fail(""); - } catch (UnknownHostException uhe) { - assertThat(uhe.getMessage()).isEqualTo("sdflkhfsdlkjdf.ee: NXDOMAIN"); - } - } -} diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsRecordCodecTest.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsRecordCodecTest.kt new file mode 100644 index 000000000000..e89f845a84e4 --- /dev/null +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsRecordCodecTest.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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 + * + * 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. + */ +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +package okhttp3.dnsoverhttps + +import java.net.InetAddress +import java.net.UnknownHostException +import okhttp3.AsyncDns.Companion.TYPE_A +import okhttp3.AsyncDns.Companion.TYPE_AAAA +import okhttp3.dnsoverhttps.DnsRecordCodec.decodeAnswers +import okio.ByteString.Companion.decodeHex +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.fail +import org.junit.jupiter.api.Test + +class DnsRecordCodecTest { + @Test + fun testGoogleDotComEncoding() { + val encoded = encodeQuery("google.com", TYPE_A) + assertThat(encoded).isEqualTo("AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ") + } + + private fun encodeQuery(host: String, type: Int): String { + return DnsRecordCodec.encodeQuery(host, type).base64Url().replace("=", "") + } + + @Test + fun testGoogleDotComEncodingWithIPv6() { + val encoded = encodeQuery("google.com", TYPE_AAAA) + assertThat(encoded).isEqualTo("AAABAAABAAAAAAAABmdvb2dsZQNjb20AABwAAQ") + } + + @Test + fun testGoogleDotComDecodingFromCloudflare() { + val encoded = decodeAnswers( + hostname = "test.com", + byteString = ("00008180000100010000000006676f6f676c6503636f6d0000010001c00c0001000100000043" + + "0004d83ad54e").decodeHex() + ) + assertThat(encoded).containsExactly(InetAddress.getByName("216.58.213.78")) + } + + @Test + fun testGoogleDotComDecodingFromGoogle() { + val decoded = decodeAnswers( + hostname = "test.com", + byteString = ("0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c" + + "0005000100000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c0420001" + + "00010000003b00049df00112").decodeHex() + ) + assertThat(decoded).containsExactly(InetAddress.getByName("157.240.1.18")) + } + + @Test + fun testGoogleDotComDecodingFromGoogleIPv6() { + val decoded = decodeAnswers( + hostname = "test.com", + byteString = ("0000818000010003000000000567726170680866616365626f6f6b03636f6d00001c0001c00c" + + "0005000100000a1b000603617069c012c0300005000100000b1f000c04737461720463313072c012c042001c" + + "00010000003b00102a032880f0290011faceb00c00000002").decodeHex() + ) + assertThat(decoded) + .containsExactly(InetAddress.getByName("2a03:2880:f029:11:face:b00c:0:2")) + } + + @Test + fun testGoogleDotComDecodingNxdomainFailure() { + try { + decodeAnswers( + hostname = "sdflkhfsdlkjdf.ee", + byteString = ("0000818300010000000100000e7364666c6b686673646c6b6a64660265650000010001c01b" + + "00060001000007070038026e7303746c64c01b0a686f73746d61737465720d6565737469696e7465726e65" + + "74c01b5adb12c100000e10000003840012750000000e10").decodeHex() + ) + fail("") + } catch (uhe: UnknownHostException) { + assertThat(uhe.message).isEqualTo("sdflkhfsdlkjdf.ee: NXDOMAIN") + } + } +} diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DohProviders.java b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DohProviders.java deleted file mode 100644 index d11459061291..000000000000 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DohProviders.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (C) 2018 Square, Inc. - * - * 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 - * - * 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 okhttp3.dnsoverhttps; - -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.List; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; - -/** - * Temporary registry of known DNS over HTTPS providers. - * - * https://github.com/curl/curl/wiki/DNS-over-HTTPS - */ -public class DohProviders { - static DnsOverHttps buildGoogle(OkHttpClient bootstrapClient) { - return new DnsOverHttps.Builder().client(bootstrapClient) - .url(HttpUrl.get("https://dns.google/dns-query")) - .bootstrapDnsHosts(getByIp("8.8.4.4"), getByIp("8.8.8.8")) - .build(); - } - - static DnsOverHttps buildGooglePost(OkHttpClient bootstrapClient) { - return new DnsOverHttps.Builder().client(bootstrapClient) - .url(HttpUrl.get("https://dns.google/dns-query")) - .bootstrapDnsHosts(getByIp("8.8.4.4"), getByIp("8.8.8.8")) - .post(true) - .build(); - } - - static DnsOverHttps buildCloudflareIp(OkHttpClient bootstrapClient) { - return new DnsOverHttps.Builder().client(bootstrapClient) - .url(HttpUrl.get("https://1.1.1.1/dns-query")) - .includeIPv6(false) - .build(); - } - - static DnsOverHttps buildCloudflare(OkHttpClient bootstrapClient) { - return new DnsOverHttps.Builder().client(bootstrapClient) - .url(HttpUrl.get("https://1.1.1.1/dns-query")) - .bootstrapDnsHosts(getByIp("1.1.1.1"), getByIp("1.0.0.1")) - .includeIPv6(false) - .build(); - } - - static DnsOverHttps buildCloudflarePost(OkHttpClient bootstrapClient) { - return new DnsOverHttps.Builder().client(bootstrapClient) - .url(HttpUrl.get("https://cloudflare-dns.com/dns-query")) - .bootstrapDnsHosts(getByIp("1.1.1.1"), getByIp("1.0.0.1")) - .includeIPv6(false) - .post(true) - .build(); - } - - static DnsOverHttps buildCleanBrowsing(OkHttpClient bootstrapClient) { - return new DnsOverHttps.Builder().client(bootstrapClient) - .url(HttpUrl.get("https://doh.cleanbrowsing.org/doh/family-filter/")) - .includeIPv6(false) - .build(); - } - - static DnsOverHttps buildChantra(OkHttpClient bootstrapClient) { - return new DnsOverHttps.Builder().client(bootstrapClient) - .url(HttpUrl.get("https://dns.dnsoverhttps.net/dns-query")) - .includeIPv6(false) - .build(); - } - - static DnsOverHttps buildCryptoSx(OkHttpClient bootstrapClient) { - return new DnsOverHttps.Builder().client(bootstrapClient) - .url(HttpUrl.get("https://doh.crypto.sx/dns-query")) - .includeIPv6(false) - .build(); - } - - public static List providers(OkHttpClient client, boolean http2Only, - boolean workingOnly, boolean getOnly) { - - List result = new ArrayList<>(); - - result.add(buildGoogle(client)); - if (!getOnly) { - result.add(buildGooglePost(client)); - } - result.add(buildCloudflare(client)); - result.add(buildCloudflareIp(client)); - if (!getOnly) { - result.add(buildCloudflarePost(client)); - } - if (!workingOnly) { - //result.add(buildCleanBrowsing(client)); // timeouts - result.add(buildCryptoSx(client)); // 521 - server down - } - result.add(buildChantra(client)); - - return result; - } - - private static InetAddress getByIp(String host) { - try { - return InetAddress.getByName(host); - } catch (UnknownHostException e) { - // unlikely - throw new RuntimeException(e); - } - } -} diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DohProviders.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DohProviders.kt new file mode 100644 index 000000000000..6a8da59fb221 --- /dev/null +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DohProviders.kt @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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 + * + * 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 okhttp3.dnsoverhttps + +import java.net.InetAddress +import java.net.UnknownHostException +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient + +/** + * Temporary registry of known DNS over HTTPS providers. + * + * https://github.com/curl/curl/wiki/DNS-over-HTTPS + */ +object DohProviders { + private fun buildGoogle(bootstrapClient: OkHttpClient): DnsOverHttps { + return DnsOverHttps.Builder() + .client(bootstrapClient) + .url("https://dns.google/dns-query".toHttpUrl()) + .bootstrapDnsHosts(getByIp("8.8.4.4"), getByIp("8.8.8.8")) + .build() + } + + private fun buildGooglePost(bootstrapClient: OkHttpClient): DnsOverHttps { + return DnsOverHttps.Builder() + .client(bootstrapClient) + .url("https://dns.google/dns-query".toHttpUrl()) + .bootstrapDnsHosts(getByIp("8.8.4.4"), getByIp("8.8.8.8")) + .post(true) + .build() + } + + private fun buildCloudflareIp(bootstrapClient: OkHttpClient): DnsOverHttps { + return DnsOverHttps.Builder() + .client(bootstrapClient) + .url("https://1.1.1.1/dns-query".toHttpUrl()) + .includeIPv6(false) + .build() + } + + private fun buildCloudflare(bootstrapClient: OkHttpClient): DnsOverHttps { + return DnsOverHttps.Builder() + .client(bootstrapClient) + .url("https://1.1.1.1/dns-query".toHttpUrl()) + .bootstrapDnsHosts(getByIp("1.1.1.1"), getByIp("1.0.0.1")) + .includeIPv6(false) + .build() + } + + private fun buildCloudflarePost(bootstrapClient: OkHttpClient): DnsOverHttps { + return DnsOverHttps.Builder() + .client(bootstrapClient) + .url("https://cloudflare-dns.com/dns-query".toHttpUrl()) + .bootstrapDnsHosts(getByIp("1.1.1.1"), getByIp("1.0.0.1")) + .includeIPv6(false) + .post(true) + .build() + } + + fun buildCleanBrowsing(bootstrapClient: OkHttpClient): DnsOverHttps { + return DnsOverHttps.Builder() + .client(bootstrapClient) + .url("https://doh.cleanbrowsing.org/doh/family-filter/".toHttpUrl()) + .includeIPv6(false) + .build() + } + + private fun buildChantra(bootstrapClient: OkHttpClient): DnsOverHttps { + return DnsOverHttps.Builder() + .client(bootstrapClient) + .url("https://dns.dnsoverhttps.net/dns-query".toHttpUrl()) + .includeIPv6(false) + .build() + } + + private fun buildCryptoSx(bootstrapClient: OkHttpClient): DnsOverHttps { + return DnsOverHttps.Builder() + .client(bootstrapClient) + .url("https://doh.crypto.sx/dns-query".toHttpUrl()) + .includeIPv6(false) + .build() + } + + @JvmStatic + fun providers( + client: OkHttpClient, + http2Only: Boolean, + workingOnly: Boolean, + getOnly: Boolean, + ): List { + return buildList { + add(buildGoogle(client)) + if (!getOnly) { + add(buildGooglePost(client)) + } + add(buildCloudflare(client)) + add(buildCloudflareIp(client)) + if (!getOnly) { + add(buildCloudflarePost(client)) + } + if (!workingOnly) { + // result += buildCleanBrowsing(client); // timeouts + add(buildCryptoSx(client)) // 521 - server down + } + add(buildChantra(client)) + } + } + + private fun getByIp(host: String): InetAddress { + return try { + InetAddress.getByName(host) + } catch (e: UnknownHostException) { + // unlikely + throw RuntimeException(e) + } + } +} diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/TestDohMain.java b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/TestDohMain.java deleted file mode 100644 index 403adc3a3d78..000000000000 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/TestDohMain.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (C) 2018 Square, Inc. - * - * 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 - * - * 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 okhttp3.dnsoverhttps; - -import java.io.File; -import java.io.IOException; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.security.Security; -import java.util.Collections; -import java.util.List; -import okhttp3.Cache; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; - -import static java.util.Arrays.asList; - -public class TestDohMain { - public static void main(String[] args) throws IOException { - Security.insertProviderAt(new org.conscrypt.OpenSSLProvider(), 1); - - OkHttpClient bootstrapClient = new OkHttpClient.Builder().build(); - - List names = asList("google.com", "graph.facebook.com", "sdflkhfsdlkjdf.ee"); - - try { - System.out.println("uncached\n********\n"); - List dnsProviders = - DohProviders.providers(bootstrapClient, false, false, false); - runBatch(dnsProviders, names); - - Cache dnsCache = - new Cache(new File("./target/TestDohMain.cache." + System.currentTimeMillis()), - 10 * 1024 * 1024); - - System.out.println("Bad targets\n***********\n"); - - HttpUrl url = HttpUrl.get("https://dns.cloudflare.com/.not-so-well-known/run-dmc-query"); - List badProviders = Collections.singletonList( - new DnsOverHttps.Builder().client(bootstrapClient).url(url).post(true).build()); - runBatch(badProviders, names); - - System.out.println("cached first run\n****************\n"); - names = asList("google.com", "graph.facebook.com"); - bootstrapClient = bootstrapClient.newBuilder().cache(dnsCache).build(); - dnsProviders = DohProviders.providers(bootstrapClient, true, true, true); - runBatch(dnsProviders, names); - - System.out.println("cached second run\n*****************\n"); - dnsProviders = DohProviders.providers(bootstrapClient, true, true, true); - runBatch(dnsProviders, names); - } finally { - bootstrapClient.connectionPool().evictAll(); - bootstrapClient.dispatcher().executorService().shutdownNow(); - Cache cache = bootstrapClient.cache(); - if (cache != null) { - cache.close(); - } - } - } - - private static void runBatch(List dnsProviders, List names) { - long time = System.currentTimeMillis(); - - for (DnsOverHttps dns : dnsProviders) { - System.out.println("Testing " + dns.url()); - - for (String host : names) { - System.out.print(host + ": "); - System.out.flush(); - - try { - List results = dns.lookup(host); - System.out.println(results); - } catch (UnknownHostException uhe) { - Throwable e = uhe; - - while (e != null) { - System.out.println(e); - - e = e.getCause(); - } - } - } - - System.out.println(); - } - - time = System.currentTimeMillis() - time; - - System.out.println("Time: " + (((double) time) / 1000) + " seconds\n"); - } -} diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/TestDohMain.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/TestDohMain.kt new file mode 100644 index 000000000000..606ff202601e --- /dev/null +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/TestDohMain.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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 + * + * 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 okhttp3.dnsoverhttps + +import java.io.File +import java.net.UnknownHostException +import java.security.Security +import okhttp3.Cache +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.dnsoverhttps.DohProviders.providers +import org.conscrypt.OpenSSLProvider + +private fun runBatch(dnsProviders: List, names: List) { + var time = System.currentTimeMillis() + for (dns in dnsProviders) { + println("Testing ${dns.url}") + for (host in names) { + print("$host: ") + System.out.flush() + try { + val results = dns.lookup(host) + println(results) + } catch (uhe: UnknownHostException) { + var e: Throwable? = uhe + while (e != null) { + println(e) + e = e.cause + } + } + } + println() + } + time = System.currentTimeMillis() - time + println("Time: ${time.toDouble() / 1000} seconds\n") +} + +fun main() { + Security.insertProviderAt(OpenSSLProvider(), 1) + var bootstrapClient = OkHttpClient() + var names = listOf("google.com", "graph.facebook.com", "sdflkhfsdlkjdf.ee") + try { + println("uncached\n********\n") + var dnsProviders = providers( + client = bootstrapClient, + http2Only = false, + workingOnly = false, + getOnly = false, + ) + runBatch(dnsProviders, names) + val dnsCache = Cache( + directory = File("./target/TestDohMain.cache.${System.currentTimeMillis()}"), + maxSize = 10L * 1024 * 1024 + ) + println("Bad targets\n***********\n") + val url = "https://dns.cloudflare.com/.not-so-well-known/run-dmc-query".toHttpUrl() + val badProviders = listOf( + DnsOverHttps.Builder() + .client(bootstrapClient) + .url(url) + .post(true) + .build() + ) + runBatch(badProviders, names) + println("cached first run\n****************\n") + names = listOf("google.com", "graph.facebook.com") + bootstrapClient = bootstrapClient.newBuilder() + .cache(dnsCache) + .build() + dnsProviders = providers( + client = bootstrapClient, + http2Only = true, + workingOnly = true, + getOnly = true, + ) + runBatch(dnsProviders, names) + println("cached second run\n*****************\n") + dnsProviders = providers( + client = bootstrapClient, + http2Only = true, + workingOnly = true, + getOnly = true, + ) + runBatch(dnsProviders, names) + } finally { + bootstrapClient.connectionPool.evictAll() + bootstrapClient.dispatcher.executorService.shutdownNow() + bootstrapClient.cache?.close() + } +} diff --git a/okhttp-hpacktests/build.gradle.kts b/okhttp-hpacktests/build.gradle.kts index d9e9b8fb4fb5..6d5c2b271ed3 100644 --- a/okhttp-hpacktests/build.gradle.kts +++ b/okhttp-hpacktests/build.gradle.kts @@ -5,6 +5,7 @@ plugins { dependencies { testImplementation(libs.squareup.okio) testImplementation(libs.squareup.moshi) + testImplementation(libs.squareup.moshi.kotlin) testImplementation(projects.okhttp) testImplementation(projects.okhttpTestingSupport) testImplementation(projects.mockwebserver) diff --git a/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackDecodeInteropTest.java b/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackDecodeInteropTest.java deleted file mode 100644 index 60594cc07a32..000000000000 --- a/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackDecodeInteropTest.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 - * - * 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 okhttp3.internal.http2; - -import java.util.List; -import okhttp3.SimpleProvider; -import okhttp3.internal.http2.hpackjson.Story; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Assumptions; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ArgumentsSource; - -import static okhttp3.internal.http2.hpackjson.HpackJsonUtil.storiesForCurrentDraft; - -public class HpackDecodeInteropTest extends HpackDecodeTestBase { - - @ParameterizedTest - @ArgumentsSource(StoriesTestProvider.class) - public void testGoodDecoderInterop(Story story) throws Exception { - Assumptions.assumeFalse(story == Story.MISSING, "Test stories missing, checkout git submodule"); - - testDecoder(story); - } - - static class StoriesTestProvider extends SimpleProvider { - @NotNull @Override public List arguments() throws Exception { - return createStories(storiesForCurrentDraft()); - } - } -} diff --git a/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackDecodeInteropTest.kt b/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackDecodeInteropTest.kt new file mode 100644 index 000000000000..c7a267e77c7f --- /dev/null +++ b/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackDecodeInteropTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 + * + * 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 okhttp3.internal.http2 + +import okhttp3.SimpleProvider +import okhttp3.internal.http2.hpackjson.HpackJsonUtil +import okhttp3.internal.http2.hpackjson.Story +import org.junit.jupiter.api.Assumptions.assumeFalse +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ArgumentsSource + +class HpackDecodeInteropTest : HpackDecodeTestBase() { + @ParameterizedTest + @ArgumentsSource(StoriesTestProvider::class) + fun testGoodDecoderInterop(story: Story) { + assumeFalse( + story === Story.MISSING, + "Test stories missing, checkout git submodule" + ) + testDecoder(story) + } + + internal class StoriesTestProvider : SimpleProvider() { + override fun arguments(): List = createStories(HpackJsonUtil.storiesForCurrentDraft()) + } +} diff --git a/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackDecodeTestBase.java b/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackDecodeTestBase.java deleted file mode 100644 index a209ac01fba3..000000000000 --- a/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackDecodeTestBase.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 - * - * 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 okhttp3.internal.http2; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import okhttp3.internal.http2.hpackjson.Case; -import okhttp3.internal.http2.hpackjson.HpackJsonUtil; -import okhttp3.internal.http2.hpackjson.Story; -import okio.Buffer; - -import static okhttp3.internal.http2.hpackjson.Story.MISSING; -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests Hpack implementation using https://github.com/http2jp/hpack-test-case/ - */ -public class HpackDecodeTestBase { - - /** - * Reads all stories in the folders provided, asserts if no story found. - */ - protected static List createStories(String[] interopTests) - throws Exception { - if (interopTests.length == 0) { - return Collections.singletonList(MISSING); - } - - List result = new ArrayList<>(); - for (String interopTestName : interopTests) { - List stories = HpackJsonUtil.readStories(interopTestName); - result.addAll(stories); - } - return result; - } - - private final Buffer bytesIn = new Buffer(); - private final Hpack.Reader hpackReader = new Hpack.Reader(bytesIn, 4096); - - protected void testDecoder(Story story) throws Exception { - for (Case testCase : story.getCases()) { - bytesIn.write(testCase.getWire()); - hpackReader.readHeaders(); - assertSetEquals(String.format("seqno=%d", testCase.getSeqno()), testCase.getHeaders(), - hpackReader.getAndResetHeaderList()); - } - } - - /** - * Checks if {@code expected} and {@code observed} are equal when viewed as a set and headers are - * deduped. - * - * TODO: See if duped headers should be preserved on decode and verify. - */ - private static void assertSetEquals( - String message, List
expected, List
observed) { - assertThat(new LinkedHashSet<>(observed)).overridingErrorMessage(message).isEqualTo( - new LinkedHashSet<>(expected)); - } -} diff --git a/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackDecodeTestBase.kt b/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackDecodeTestBase.kt new file mode 100644 index 000000000000..3a3b4541dc71 --- /dev/null +++ b/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackDecodeTestBase.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 + * + * 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 okhttp3.internal.http2 + +import okhttp3.internal.http2.hpackjson.HpackJsonUtil +import okhttp3.internal.http2.hpackjson.Story +import okio.Buffer +import org.assertj.core.api.Assertions.assertThat + +/** + * Tests Hpack implementation using https://github.com/http2jp/hpack-test-case/ + */ +open class HpackDecodeTestBase { + private val bytesIn = Buffer() + private val hpackReader = Hpack.Reader(bytesIn, 4096) + + protected fun testDecoder(story: Story) { + for (testCase in story.cases) { + val encoded = testCase.wire ?: continue + bytesIn.write(encoded) + hpackReader.readHeaders() + assertSetEquals( + "seqno=$testCase.seqno", + testCase.headersList, + hpackReader.getAndResetHeaderList() + ) + } + } + + companion object { + /** + * Reads all stories in the folders provided, asserts if no story found. + */ + @JvmStatic + protected fun createStories(interopTests: Array): List { + if (interopTests.isEmpty()) return listOf(Story.MISSING) + + val result = mutableListOf() + for (interopTestName in interopTests) { + val stories = HpackJsonUtil.readStories(interopTestName) + result.addAll(stories) + } + return result + } + + /** + * Checks if `expected` and `observed` are equal when viewed as a set and headers are + * deduped. + * + * TODO: See if duped headers should be preserved on decode and verify. + */ + private fun assertSetEquals( + message: String, + expected: List
, + observed: List
, + ) { + assertThat(LinkedHashSet(observed)) + .overridingErrorMessage(message) + .isEqualTo(LinkedHashSet(expected)) + } + } +} diff --git a/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackRoundTripTest.java b/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackRoundTripTest.java deleted file mode 100644 index 341ddb49a462..000000000000 --- a/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackRoundTripTest.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 - * - * 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 okhttp3.internal.http2; - -import java.util.List; -import okhttp3.SimpleProvider; -import okhttp3.internal.http2.hpackjson.Case; -import okhttp3.internal.http2.hpackjson.Story; -import okio.Buffer; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Assumptions; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ArgumentsSource; - -/** - * Tests for round-tripping headers through hpack.. - */ -// TODO: update hpack-test-case with the output of our encoder. -// This test will hide complementary bugs in the encoder and decoder, -// We should test that the encoder is producing responses that are -public class HpackRoundTripTest extends HpackDecodeTestBase { - - private static final String[] RAW_DATA = {"raw-data"}; - - static class StoriesTestProvider extends SimpleProvider { - @NotNull @Override public List arguments() throws Exception { - return createStories(RAW_DATA); - } - } - - private final Buffer bytesOut = new Buffer(); - private final Hpack.Writer hpackWriter = new Hpack.Writer(bytesOut); - - @ParameterizedTest - @ArgumentsSource(StoriesTestProvider.class) - public void testRoundTrip(Story story) throws Exception { - Assumptions.assumeFalse(story == Story.MISSING, "Test stories missing, checkout git submodule"); - - Story story2 = story.clone(); - // Mutate cases in base class. - for (Case caze : story2.getCases()) { - hpackWriter.writeHeaders(caze.getHeaders()); - caze.setWire(bytesOut.readByteString()); - } - - testDecoder(story2); - } -} diff --git a/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackRoundTripTest.kt b/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackRoundTripTest.kt new file mode 100644 index 000000000000..c96f3d4118fc --- /dev/null +++ b/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackRoundTripTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 + * + * 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 okhttp3.internal.http2 + +import okhttp3.SimpleProvider +import okhttp3.internal.http2.hpackjson.Case +import okhttp3.internal.http2.hpackjson.Story +import okio.Buffer +import org.junit.jupiter.api.Assumptions.assumeFalse +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ArgumentsSource + +/** + * Tests for round-tripping headers through hpack. + */ +// TODO: update hpack-test-case with the output of our encoder. +// This test will hide complementary bugs in the encoder and decoder, +// We should test that the encoder is producing responses that are +class HpackRoundTripTest : HpackDecodeTestBase() { + internal class StoriesTestProvider : SimpleProvider() { + override fun arguments(): List = createStories(RAW_DATA) + } + + private val bytesOut = Buffer() + private val hpackWriter = Hpack.Writer(out = bytesOut) + + @ParameterizedTest + @ArgumentsSource(StoriesTestProvider::class) + fun testRoundTrip(story: Story) { + assumeFalse( + story === Story.MISSING, + "Test stories missing, checkout git submodule" + ) + + val newCases = mutableListOf() + for (case in story.cases) { + hpackWriter.writeHeaders(case.headersList) + newCases += case.copy(wire = bytesOut.readByteString()) + } + + testDecoder(story.copy(cases = newCases)) + } + + companion object { + private val RAW_DATA = arrayOf("raw-data") + } +} diff --git a/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/Case.java b/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/Case.java deleted file mode 100644 index 20a5ccac123c..000000000000 --- a/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/Case.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 - * - * 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 okhttp3.internal.http2.hpackjson; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import okhttp3.internal.http2.Header; -import okio.ByteString; - -/** - * Representation of an individual case (set of headers and wire format). There are many cases for a - * single story. This class is used reflectively with Moshi to parse stories. - */ -public class Case implements Cloneable { - - private int seqno; - private String wire; - private List> headers; - - public List
getHeaders() { - List
result = new ArrayList<>(); - for (Map inputHeader : headers) { - Map.Entry entry = inputHeader.entrySet().iterator().next(); - result.add(new Header(entry.getKey(), entry.getValue())); - } - return result; - } - - public ByteString getWire() { - return ByteString.decodeHex(wire); - } - - public int getSeqno() { - return seqno; - } - - public void setWire(ByteString wire) { - this.wire = wire.hex(); - } - - @Override - protected Case clone() throws CloneNotSupportedException { - Case result = new Case(); - result.seqno = seqno; - result.wire = wire; - result.headers = new ArrayList<>(); - for (Map header : headers) { - result.headers.add(new LinkedHashMap<>(header)); - } - return result; - } -} diff --git a/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/Case.kt b/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/Case.kt new file mode 100644 index 000000000000..86c11dc264d8 --- /dev/null +++ b/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/Case.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 + * + * 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 okhttp3.internal.http2.hpackjson + +import okhttp3.internal.http2.Header +import okio.ByteString + +/** + * Representation of an individual case (set of headers and wire format). There are many cases for a + * single story. This class is used reflectively with Moshi to parse stories. + */ +data class Case( + val seqno: Int = 0, + val wire: ByteString? = null, + val headers: List>, +) : Cloneable { + val headersList: List
+ get() { + val result = mutableListOf
() + for (inputHeader in headers) { + val (key, value) = inputHeader.entries.iterator().next() + result.add(Header(key, value)) + } + return result + } + + public override fun clone() = Case(seqno, this.wire, headers) +} diff --git a/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/HpackJsonUtil.java b/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/HpackJsonUtil.java deleted file mode 100644 index f40253716202..000000000000 --- a/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/HpackJsonUtil.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 - * - * 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 okhttp3.internal.http2.hpackjson; - -import com.squareup.moshi.JsonAdapter; -import com.squareup.moshi.Moshi; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.URISyntaxException; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; -import okio.Okio; - -import static java.util.Arrays.asList; -import static okhttp3.internal.http2.hpackjson.Story.MISSING; - -/** - * Utilities for reading HPACK tests. - */ -public final class HpackJsonUtil { - /** Earliest draft that is code-compatible with latest. */ - private static final int BASE_DRAFT = 9; - - private static final String STORY_RESOURCE_FORMAT = "/hpack-test-case/%s/story_%02d.json"; - - private static final Moshi MOSHI = new Moshi.Builder().build(); - private static final JsonAdapter STORY_JSON_ADAPTER = MOSHI.adapter(Story.class); - - private static Story readStory(InputStream jsonResource) throws IOException { - return STORY_JSON_ADAPTER.fromJson(Okio.buffer(Okio.source(jsonResource))); - } - - private static Story readStory(File file) throws IOException { - return STORY_JSON_ADAPTER.fromJson(Okio.buffer(Okio.source(file))); - } - - /** Iterate through the hpack-test-case resources, only picking stories for the current draft. */ - public static String[] storiesForCurrentDraft() throws URISyntaxException { - URL resource = HpackJsonUtil.class.getResource("/hpack-test-case"); - if (resource == null) { - return new String[0]; - } - File testCaseDirectory = new File(resource.toURI()); - List storyNames = new ArrayList<>(); - for (File path : testCaseDirectory.listFiles()) { - if (path.isDirectory() && asList(path.list()).contains("story_00.json")) { - try { - Story firstStory = readStory(new File(path, "story_00.json")); - if (firstStory.getDraft() >= BASE_DRAFT) { - storyNames.add(path.getName()); - } - } catch (IOException ignored) { - // Skip this path. - } - } - } - return storyNames.toArray(new String[0]); - } - - /** - * Reads stories named "story_xx.json" from the folder provided. - */ - public static List readStories(String testFolderName) throws Exception { - List result = new ArrayList<>(); - int i = 0; - while (true) { // break after last test. - String storyResourceName = String.format(STORY_RESOURCE_FORMAT, testFolderName, i); - InputStream storyInputStream = HpackJsonUtil.class.getResourceAsStream(storyResourceName); - if (storyInputStream == null) { - break; - } - try { - Story story = readStory(storyInputStream); - story.setFileName(storyResourceName); - result.add(story); - i++; - } finally { - storyInputStream.close(); - } - } - - if (result.isEmpty()) { - // missing files - result.add(MISSING); - } - - return result; - } - - private HpackJsonUtil() { - } // Utilities only. -} diff --git a/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/HpackJsonUtil.kt b/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/HpackJsonUtil.kt new file mode 100644 index 000000000000..fc16ea603758 --- /dev/null +++ b/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/HpackJsonUtil.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 + * + * 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 okhttp3.internal.http2.hpackjson + +import com.squareup.moshi.FromJson +import com.squareup.moshi.Moshi +import com.squareup.moshi.ToJson +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import java.io.File +import java.io.IOException +import okio.BufferedSource +import okio.ByteString +import okio.ByteString.Companion.decodeHex +import okio.FileSystem +import okio.Path +import okio.Path.Companion.toOkioPath +import okio.buffer +import okio.source + +/** + * Utilities for reading HPACK tests. + */ +object HpackJsonUtil { + @Suppress("unused") + private val MOSHI = Moshi.Builder() + .add(object : Any() { + @ToJson fun byteStringToJson(byteString: ByteString) = byteString.hex() + @FromJson fun byteStringFromJson(json: String) = json.decodeHex() + }) + .add(KotlinJsonAdapterFactory()) + .build() + private val STORY_JSON_ADAPTER = MOSHI.adapter(Story::class.java) + + private val fileSystem = FileSystem.SYSTEM + + private fun readStory(source: BufferedSource): Story { + return STORY_JSON_ADAPTER.fromJson(source)!! + } + + private fun readStory(file: Path): Story { + fileSystem.read(file) { + return readStory(this) + } + } + + /** Iterate through the hpack-test-case resources, only picking stories for the current draft. */ + fun storiesForCurrentDraft(): Array { + val resource = HpackJsonUtil::class.java.getResource("/hpack-test-case") + ?: return arrayOf() + + val testCaseDirectory = File(resource.toURI()).toOkioPath() + val result = mutableListOf() + for (path in fileSystem.list(testCaseDirectory)) { + val story00 = path / "story_00.json" + if (!fileSystem.exists(story00)) continue + try { + readStory(story00) + result.add(path.name) + } catch (ignored: IOException) { + // Skip this path. + } + } + return result.toTypedArray() + } + + /** + * Reads stories named "story_xx.json" from the folder provided. + */ + fun readStories(testFolderName: String): List { + val result = mutableListOf() + var i = 0 + while (true) { // break after last test. + val storyResourceName = String.format( + "/hpack-test-case/%s/story_%02d.json", + testFolderName, + i, + ) + val storyInputStream = HpackJsonUtil::class.java.getResourceAsStream(storyResourceName) + ?: break + try { + storyInputStream.use { + val story = readStory(storyInputStream.source().buffer()) + .copy(fileName = storyResourceName) + result.add(story) + i++ + } + } finally { + storyInputStream.close() + } + } + + if (result.isEmpty()) { + // missing files + result.add(Story.MISSING) + } + + return result + } +} diff --git a/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/Story.java b/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/Story.java deleted file mode 100644 index 25a63a073bde..000000000000 --- a/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/Story.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 - * - * 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 okhttp3.internal.http2.hpackjson; - -import java.util.ArrayList; -import java.util.List; - -/** - * Representation of one story, a set of request headers to encode or decode. This class is used - * reflectively with Moshi to parse stories from files. - */ -public class Story implements Cloneable { - public final static Story MISSING = new Story(); - - static { - MISSING.setFileName("missing"); - } - - private transient String fileName; - private List cases; - private int draft; - private String description; - - /** - * The filename is only used in the toString representation. - */ - void setFileName(String fileName) { - this.fileName = fileName; - } - - public List getCases() { - return cases; - } - - /** We only expect stories that match the draft we've implemented to pass. */ - public int getDraft() { - return draft; - } - - @Override - public Story clone() throws CloneNotSupportedException { - Story story = new Story(); - story.fileName = this.fileName; - story.cases = new ArrayList<>(); - for (Case caze : cases) { - story.cases.add(caze.clone()); - } - story.draft = draft; - story.description = description; - return story; - } - - @Override - public String toString() { - // Used as the test name. - return fileName; - } -} diff --git a/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/Story.kt b/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/Story.kt new file mode 100644 index 000000000000..bcab3fb7fd38 --- /dev/null +++ b/okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/Story.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 + * + * 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 okhttp3.internal.http2.hpackjson + +/** + * Representation of one story, a set of request headers to encode or decode. This class is used + * reflectively with Moshi to parse stories from files. + */ +data class Story( + val description: String? = null, + val cases: List, + val fileName: String? = null, +) { + + // Used as the test name. + override fun toString() = fileName ?: "?" + + companion object { + @JvmField + val MISSING = Story(description = "Missing", cases = listOf(), "missing") + } +} diff --git a/okhttp-logging-interceptor/src/test/java/okhttp3/logging/HttpLoggingInterceptorTest.java b/okhttp-logging-interceptor/src/test/java/okhttp3/logging/HttpLoggingInterceptorTest.java deleted file mode 100644 index cf6a590e374f..000000000000 --- a/okhttp-logging-interceptor/src/test/java/okhttp3/logging/HttpLoggingInterceptorTest.java +++ /dev/null @@ -1,1051 +0,0 @@ -/* - * Copyright (C) 2015 Square, Inc. - * - * 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 - * - * 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 okhttp3.logging; - -import java.io.IOException; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; -import javax.annotation.Nullable; -import javax.net.ssl.HostnameVerifier; -import mockwebserver3.MockResponse; -import mockwebserver3.MockWebServer; -import mockwebserver3.junit5.internal.MockWebServerExtension; -import okhttp3.HttpUrl; -import okhttp3.Interceptor; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Protocol; -import okhttp3.RecordingHostnameVerifier; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; -import okhttp3.logging.HttpLoggingInterceptor.Level; -import okhttp3.testing.PlatformRule; -import okhttp3.tls.HandshakeCertificates; -import okio.Buffer; -import okio.BufferedSink; -import okio.ByteString; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.RegisterExtension; -import static okhttp3.RequestBody.gzip; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.fail; -import static org.junit.jupiter.api.Assumptions.assumeTrue; - -@ExtendWith(MockWebServerExtension.class) -public final class HttpLoggingInterceptorTest { - private static final MediaType PLAIN = MediaType.get("text/plain; charset=utf-8"); - - @RegisterExtension public final PlatformRule platform = new PlatformRule(); - private MockWebServer server; - - private final HandshakeCertificates handshakeCertificates - = platform.localhostHandshakeCertificates(); - private final HostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); - private OkHttpClient client; - private String host; - private HttpUrl url; - - private final LogRecorder networkLogs = new LogRecorder(); - private final HttpLoggingInterceptor networkInterceptor = - new HttpLoggingInterceptor(networkLogs); - - private final LogRecorder applicationLogs = new LogRecorder(); - private final HttpLoggingInterceptor applicationInterceptor = - new HttpLoggingInterceptor(applicationLogs); - - private Interceptor extraNetworkInterceptor = null; - - private void setLevel(Level level) { - networkInterceptor.setLevel(level); - applicationInterceptor.setLevel(level); - } - - @BeforeEach public void setUp(MockWebServer server) { - this.server = server; - - client = new OkHttpClient.Builder() - .addNetworkInterceptor(chain -> extraNetworkInterceptor != null - ? extraNetworkInterceptor.intercept(chain) - : chain.proceed(chain.request())) - .addNetworkInterceptor(networkInterceptor) - .addInterceptor(applicationInterceptor) - .sslSocketFactory( - handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager()) - .hostnameVerifier(hostnameVerifier) - .build(); - - host = server.getHostName() + ":" + server.getPort(); - url = server.url("/"); - } - - @Test public void levelGetter() { - // The default is NONE. - assertThat(applicationInterceptor.getLevel()).isEqualTo(Level.NONE); - - for (Level level : Level.values()) { - applicationInterceptor.setLevel(level); - assertThat(applicationInterceptor.getLevel()).isEqualTo(level); - } - } - - @Test public void setLevelShouldPreventNullValue() { - try { - applicationInterceptor.setLevel(null); - fail(); - } catch (NullPointerException expected) { - } - } - - @Test public void setLevelShouldReturnSameInstanceOfInterceptor() { - for (Level level : Level.values()) { - assertThat(applicationInterceptor.setLevel(level)).isSameAs(applicationInterceptor); - } - } - - @Test public void none() throws IOException { - server.enqueue(new MockResponse()); - client.newCall(request().build()).execute(); - - applicationLogs.assertNoMoreLogs(); - networkLogs.assertNoMoreLogs(); - } - - @Test public void basicGet() throws IOException { - setLevel(Level.BASIC); - - server.enqueue(new MockResponse()); - client.newCall(request().build()).execute(); - - applicationLogs - .assertLogEqual("--> GET " + url) - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms, 0-byte body\\)") - .assertNoMoreLogs(); - - networkLogs - .assertLogEqual("--> GET " + url + " http/1.1") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms, 0-byte body\\)") - .assertNoMoreLogs(); - } - - @Test public void basicPost() throws IOException { - setLevel(Level.BASIC); - - server.enqueue(new MockResponse()); - client.newCall(request().post(RequestBody.create("Hi?", PLAIN)).build()).execute(); - - applicationLogs - .assertLogEqual("--> POST " + url + " (3-byte body)") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms, 0-byte body\\)") - .assertNoMoreLogs(); - - networkLogs - .assertLogEqual("--> POST " + url + " http/1.1 (3-byte body)") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms, 0-byte body\\)") - .assertNoMoreLogs(); - } - - @Test public void basicResponseBody() throws IOException { - setLevel(Level.BASIC); - - server.enqueue(new MockResponse.Builder() - .body("Hello!") - .setHeader("Content-Type", PLAIN) - .build()); - Response response = client.newCall(request().build()).execute(); - response.body().close(); - - applicationLogs - .assertLogEqual("--> GET " + url) - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms, 6-byte body\\)") - .assertNoMoreLogs(); - - networkLogs - .assertLogEqual("--> GET " + url + " http/1.1") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms, 6-byte body\\)") - .assertNoMoreLogs(); - } - - @Test public void basicChunkedResponseBody() throws IOException { - setLevel(Level.BASIC); - - server.enqueue(new MockResponse.Builder() - .chunkedBody("Hello!", 2) - .setHeader("Content-Type", PLAIN) - .build()); - Response response = client.newCall(request().build()).execute(); - response.body().close(); - - applicationLogs - .assertLogEqual("--> GET " + url) - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms, unknown-length body\\)") - .assertNoMoreLogs(); - - networkLogs - .assertLogEqual("--> GET " + url + " http/1.1") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms, unknown-length body\\)") - .assertNoMoreLogs(); - } - - @Test public void headersGet() throws IOException { - setLevel(Level.HEADERS); - - server.enqueue(new MockResponse()); - Response response = client.newCall(request().build()).execute(); - response.body().close(); - - applicationLogs - .assertLogEqual("--> GET " + url) - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 0") - .assertLogEqual("<-- END HTTP") - .assertNoMoreLogs(); - - networkLogs - .assertLogEqual("--> GET " + url + " http/1.1") - .assertLogEqual("Host: " + host) - .assertLogEqual("Connection: Keep-Alive") - .assertLogEqual("Accept-Encoding: gzip") - .assertLogMatch("User-Agent: okhttp/.+") - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 0") - .assertLogEqual("<-- END HTTP") - .assertNoMoreLogs(); - } - - @Test public void headersPost() throws IOException { - setLevel(Level.HEADERS); - - server.enqueue(new MockResponse()); - Request request = request().post(RequestBody.create("Hi?", PLAIN)).build(); - Response response = client.newCall(request).execute(); - response.body().close(); - - applicationLogs - .assertLogEqual("--> POST " + url) - .assertLogEqual("Content-Type: text/plain; charset=utf-8") - .assertLogEqual("Content-Length: 3") - .assertLogEqual("--> END POST") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 0") - .assertLogEqual("<-- END HTTP") - .assertNoMoreLogs(); - - networkLogs - .assertLogEqual("--> POST " + url + " http/1.1") - .assertLogEqual("Content-Type: text/plain; charset=utf-8") - .assertLogEqual("Content-Length: 3") - .assertLogEqual("Host: " + host) - .assertLogEqual("Connection: Keep-Alive") - .assertLogEqual("Accept-Encoding: gzip") - .assertLogMatch("User-Agent: okhttp/.+") - .assertLogEqual("--> END POST") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 0") - .assertLogEqual("<-- END HTTP") - .assertNoMoreLogs(); - } - - @Test public void headersPostNoContentType() throws IOException { - setLevel(Level.HEADERS); - - server.enqueue(new MockResponse()); - Request request = request().post(RequestBody.create("Hi?", null)).build(); - Response response = client.newCall(request).execute(); - response.body().close(); - - applicationLogs - .assertLogEqual("--> POST " + url) - .assertLogEqual("Content-Length: 3") - .assertLogEqual("--> END POST") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 0") - .assertLogEqual("<-- END HTTP") - .assertNoMoreLogs(); - - networkLogs - .assertLogEqual("--> POST " + url + " http/1.1") - .assertLogEqual("Content-Length: 3") - .assertLogEqual("Host: " + host) - .assertLogEqual("Connection: Keep-Alive") - .assertLogEqual("Accept-Encoding: gzip") - .assertLogMatch("User-Agent: okhttp/.+") - .assertLogEqual("--> END POST") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 0") - .assertLogEqual("<-- END HTTP") - .assertNoMoreLogs(); - } - - @Test public void headersPostNoLength() throws IOException { - setLevel(Level.HEADERS); - - server.enqueue(new MockResponse()); - RequestBody body = new RequestBody() { - @Override public MediaType contentType() { - return PLAIN; - } - - @Override public void writeTo(BufferedSink sink) throws IOException { - sink.writeUtf8("Hi!"); - } - }; - Response response = client.newCall(request().post(body).build()).execute(); - response.body().close(); - - applicationLogs - .assertLogEqual("--> POST " + url) - .assertLogEqual("Content-Type: text/plain; charset=utf-8") - .assertLogEqual("--> END POST") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 0") - .assertLogEqual("<-- END HTTP") - .assertNoMoreLogs(); - - networkLogs - .assertLogEqual("--> POST " + url + " http/1.1") - .assertLogEqual("Content-Type: text/plain; charset=utf-8") - .assertLogEqual("Transfer-Encoding: chunked") - .assertLogEqual("Host: " + host) - .assertLogEqual("Connection: Keep-Alive") - .assertLogEqual("Accept-Encoding: gzip") - .assertLogMatch("User-Agent: okhttp/.+") - .assertLogEqual("--> END POST") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 0") - .assertLogEqual("<-- END HTTP") - .assertNoMoreLogs(); - } - - @Test public void headersPostWithHeaderOverrides() throws IOException { - setLevel(Level.HEADERS); - - extraNetworkInterceptor = chain -> chain.proceed(chain.request() - .newBuilder() - .header("Content-Length", "2") - .header("Content-Type", "text/plain-ish") - .build()); - - server.enqueue(new MockResponse()); - client.newCall(request() - .post(RequestBody.create("Hi?", PLAIN)) - .build()).execute(); - - applicationLogs - .assertLogEqual("--> POST " + url) - .assertLogEqual("Content-Type: text/plain; charset=utf-8") - .assertLogEqual("Content-Length: 3") - .assertLogEqual("--> END POST") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 0") - .assertLogEqual("<-- END HTTP") - .assertNoMoreLogs(); - - networkLogs - .assertLogEqual("--> POST " + url + " http/1.1") - .assertLogEqual("Host: " + host) - .assertLogEqual("Connection: Keep-Alive") - .assertLogEqual("Accept-Encoding: gzip") - .assertLogMatch("User-Agent: okhttp/.+") - .assertLogEqual("Content-Length: 2") - .assertLogEqual("Content-Type: text/plain-ish") - .assertLogEqual("--> END POST") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 0") - .assertLogEqual("<-- END HTTP") - .assertNoMoreLogs(); - } - - @Test public void headersResponseBody() throws IOException { - setLevel(Level.HEADERS); - - server.enqueue(new MockResponse.Builder() - .body("Hello!") - .setHeader("Content-Type", PLAIN) - .build()); - Response response = client.newCall(request().build()).execute(); - response.body().close(); - - applicationLogs - .assertLogEqual("--> GET " + url) - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 6") - .assertLogEqual("Content-Type: text/plain; charset=utf-8") - .assertLogEqual("<-- END HTTP") - .assertNoMoreLogs(); - - networkLogs - .assertLogEqual("--> GET " + url + " http/1.1") - .assertLogEqual("Host: " + host) - .assertLogEqual("Connection: Keep-Alive") - .assertLogEqual("Accept-Encoding: gzip") - .assertLogMatch("User-Agent: okhttp/.+") - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 6") - .assertLogEqual("Content-Type: text/plain; charset=utf-8") - .assertLogEqual("<-- END HTTP") - .assertNoMoreLogs(); - } - - @Test public void bodyGet() throws IOException { - setLevel(Level.BODY); - - server.enqueue(new MockResponse()); - Response response = client.newCall(request().build()).execute(); - response.body().close(); - - applicationLogs - .assertLogEqual("--> GET " + url) - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 0") - .assertLogMatch("<-- END HTTP \\(\\d+ms, 0-byte body\\)") - .assertNoMoreLogs(); - - networkLogs - .assertLogEqual("--> GET " + url + " http/1.1") - .assertLogEqual("Host: " + host) - .assertLogEqual("Connection: Keep-Alive") - .assertLogEqual("Accept-Encoding: gzip") - .assertLogMatch("User-Agent: okhttp/.+") - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 0") - .assertLogMatch("<-- END HTTP \\(\\d+ms, 0-byte body\\)") - .assertNoMoreLogs(); - } - - @Test public void bodyGet204() throws IOException { - setLevel(Level.BODY); - bodyGetNoBody(204); - } - - @Test public void bodyGet205() throws IOException { - setLevel(Level.BODY); - bodyGetNoBody(205); - } - - private void bodyGetNoBody(int code) throws IOException { - server.enqueue(new MockResponse.Builder() - .status("HTTP/1.1 " + code + " No Content") - .build()); - Response response = client.newCall(request().build()).execute(); - response.body().close(); - - applicationLogs - .assertLogEqual("--> GET " + url) - .assertLogEqual("--> END GET") - .assertLogMatch("<-- " + code + " No Content " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 0") - .assertLogMatch("<-- END HTTP \\(\\d+ms, 0-byte body\\)") - .assertNoMoreLogs(); - - networkLogs - .assertLogEqual("--> GET " + url + " http/1.1") - .assertLogEqual("Host: " + host) - .assertLogEqual("Connection: Keep-Alive") - .assertLogEqual("Accept-Encoding: gzip") - .assertLogMatch("User-Agent: okhttp/.+") - .assertLogEqual("--> END GET") - .assertLogMatch("<-- " + code + " No Content " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 0") - .assertLogMatch("<-- END HTTP \\(\\d+ms, 0-byte body\\)") - .assertNoMoreLogs(); - } - - @Test public void bodyPost() throws IOException { - setLevel(Level.BODY); - - server.enqueue(new MockResponse()); - Request request = request().post(RequestBody.create("Hi?", PLAIN)).build(); - Response response = client.newCall(request).execute(); - response.body().close(); - - applicationLogs - .assertLogEqual("--> POST " + url) - .assertLogEqual("Content-Type: text/plain; charset=utf-8") - .assertLogEqual("Content-Length: 3") - .assertLogEqual("") - .assertLogEqual("Hi?") - .assertLogEqual("--> END POST (3-byte body)") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 0") - .assertLogMatch("<-- END HTTP \\(\\d+ms, 0-byte body\\)") - .assertNoMoreLogs(); - - networkLogs - .assertLogEqual("--> POST " + url + " http/1.1") - .assertLogEqual("Content-Type: text/plain; charset=utf-8") - .assertLogEqual("Content-Length: 3") - .assertLogEqual("Host: " + host) - .assertLogEqual("Connection: Keep-Alive") - .assertLogEqual("Accept-Encoding: gzip") - .assertLogMatch("User-Agent: okhttp/.+") - .assertLogEqual("") - .assertLogEqual("Hi?") - .assertLogEqual("--> END POST (3-byte body)") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 0") - .assertLogMatch("<-- END HTTP \\(\\d+ms, 0-byte body\\)") - .assertNoMoreLogs(); - } - - @Test public void bodyResponseBody() throws IOException { - setLevel(Level.BODY); - - server.enqueue(new MockResponse.Builder() - .body("Hello!") - .setHeader("Content-Type", PLAIN) - .build()); - Response response = client.newCall(request().build()).execute(); - response.body().close(); - - applicationLogs - .assertLogEqual("--> GET " + url) - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 6") - .assertLogEqual("Content-Type: text/plain; charset=utf-8") - .assertLogEqual("") - .assertLogEqual("Hello!") - .assertLogMatch("<-- END HTTP \\(\\d+ms, 6-byte body\\)") - .assertNoMoreLogs(); - - networkLogs - .assertLogEqual("--> GET " + url + " http/1.1") - .assertLogEqual("Host: " + host) - .assertLogEqual("Connection: Keep-Alive") - .assertLogEqual("Accept-Encoding: gzip") - .assertLogMatch("User-Agent: okhttp/.+") - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 6") - .assertLogEqual("Content-Type: text/plain; charset=utf-8") - .assertLogEqual("") - .assertLogEqual("Hello!") - .assertLogMatch("<-- END HTTP \\(\\d+ms, 6-byte body\\)") - .assertNoMoreLogs(); - } - - @Test public void bodyResponseBodyChunked() throws IOException { - setLevel(Level.BODY); - - server.enqueue(new MockResponse.Builder() - .chunkedBody("Hello!", 2) - .setHeader("Content-Type", PLAIN) - .build()); - Response response = client.newCall(request().build()).execute(); - response.body().close(); - - applicationLogs - .assertLogEqual("--> GET " + url) - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Transfer-encoding: chunked") - .assertLogEqual("Content-Type: text/plain; charset=utf-8") - .assertLogEqual("") - .assertLogEqual("Hello!") - .assertLogMatch("<-- END HTTP \\(\\d+ms, 6-byte body\\)") - .assertNoMoreLogs(); - - networkLogs - .assertLogEqual("--> GET " + url + " http/1.1") - .assertLogEqual("Host: " + host) - .assertLogEqual("Connection: Keep-Alive") - .assertLogEqual("Accept-Encoding: gzip") - .assertLogMatch("User-Agent: okhttp/.+") - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Transfer-encoding: chunked") - .assertLogEqual("Content-Type: text/plain; charset=utf-8") - .assertLogEqual("") - .assertLogEqual("Hello!") - .assertLogMatch("<-- END HTTP \\(\\d+ms, 6-byte body\\)") - .assertNoMoreLogs(); - } - - @Test public void bodyRequestGzipEncoded() throws IOException { - setLevel(Level.BODY); - - server.enqueue(new MockResponse.Builder() - .setHeader("Content-Type", PLAIN) - .body(new Buffer().writeUtf8("Uncompressed")) - .build()); - - Response response = client.newCall(request() - .addHeader("Content-Encoding", "gzip") - .post(gzip(RequestBody.create("Uncompressed", null))) - .build()).execute(); - - ResponseBody responseBody = response.body(); - assertThat(responseBody.string()).overridingErrorMessage( - "Expected response body to be valid").isEqualTo("Uncompressed"); - responseBody.close(); - - networkLogs - .assertLogEqual("--> POST " + url + " http/1.1") - .assertLogEqual("Content-Encoding: gzip") - .assertLogEqual("Transfer-Encoding: chunked") - .assertLogEqual("Host: " + host) - .assertLogEqual("Connection: Keep-Alive") - .assertLogEqual("Accept-Encoding: gzip") - .assertLogMatch("User-Agent: okhttp/.+") - .assertLogEqual("") - .assertLogEqual("--> END POST (12-byte, 32-gzipped-byte body)") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Type: text/plain; charset=utf-8") - .assertLogMatch("Content-Length: \\d+") - .assertLogEqual("") - .assertLogEqual("Uncompressed") - .assertLogMatch("<-- END HTTP \\(\\d+ms, 12-byte body\\)") - .assertNoMoreLogs(); - } - - @Test public void bodyResponseGzipEncoded() throws IOException { - setLevel(Level.BODY); - - server.enqueue(new MockResponse.Builder() - .setHeader("Content-Encoding", "gzip") - .setHeader("Content-Type", PLAIN) - .body(new Buffer().write(ByteString.decodeBase64( - "H4sIAAAAAAAAAPNIzcnJ11HwQKIAdyO+9hMAAAA="))) - .build()); - Response response = client.newCall(request().build()).execute(); - - ResponseBody responseBody = response.body(); - assertThat(responseBody.string()).overridingErrorMessage( - "Expected response body to be valid").isEqualTo("Hello, Hello, Hello"); - responseBody.close(); - - networkLogs - .assertLogEqual("--> GET " + url + " http/1.1") - .assertLogEqual("Host: " + host) - .assertLogEqual("Connection: Keep-Alive") - .assertLogEqual("Accept-Encoding: gzip") - .assertLogMatch("User-Agent: okhttp/.+") - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Encoding: gzip") - .assertLogEqual("Content-Type: text/plain; charset=utf-8") - .assertLogMatch("Content-Length: \\d+") - .assertLogEqual("") - .assertLogEqual("Hello, Hello, Hello") - .assertLogMatch("<-- END HTTP \\(\\d+ms, 19-byte, 29-gzipped-byte body\\)") - .assertNoMoreLogs(); - - applicationLogs - .assertLogEqual("--> GET " + url) - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Type: text/plain; charset=utf-8") - .assertLogEqual("") - .assertLogEqual("Hello, Hello, Hello") - .assertLogMatch("<-- END HTTP \\(\\d+ms, 19-byte body\\)") - .assertNoMoreLogs(); - } - - @Test public void bodyResponseUnknownEncoded() throws IOException { - setLevel(Level.BODY); - - server.enqueue(new MockResponse.Builder() - // It's invalid to return this if not requested, but the server might anyway - .setHeader("Content-Encoding", "br") - .setHeader("Content-Type", PLAIN) - .body(new Buffer().write(ByteString.decodeBase64( - "iwmASGVsbG8sIEhlbGxvLCBIZWxsbwoD"))) - .build()); - Response response = client.newCall(request().build()).execute(); - response.body().close(); - - networkLogs - .assertLogEqual("--> GET " + url + " http/1.1") - .assertLogEqual("Host: " + host) - .assertLogEqual("Connection: Keep-Alive") - .assertLogEqual("Accept-Encoding: gzip") - .assertLogMatch("User-Agent: okhttp/.+") - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Encoding: br") - .assertLogEqual("Content-Type: text/plain; charset=utf-8") - .assertLogMatch("Content-Length: \\d+") - .assertLogEqual("<-- END HTTP (encoded body omitted)") - .assertNoMoreLogs(); - - applicationLogs - .assertLogEqual("--> GET " + url) - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Encoding: br") - .assertLogEqual("Content-Type: text/plain; charset=utf-8") - .assertLogMatch("Content-Length: \\d+") - .assertLogEqual("<-- END HTTP (encoded body omitted)") - .assertNoMoreLogs(); - } - - @Test public void bodyResponseIsStreaming() throws IOException { - setLevel(Level.BODY); - - server.enqueue(new MockResponse.Builder() - .setHeader("Content-Type", "text/event-stream") - .chunkedBody("" - + "event: add\n" - + "data: 73857293\n" - + "\n" - + "event: remove\n" - + "data: 2153\n" - + "\n" - + "event: add\n" - + "data: 113411\n" - + "\n", 8) - .build() - ); - Response response = client.newCall(request().build()).execute(); - response.body().close(); - - networkLogs - .assertLogEqual("--> GET " + url + " http/1.1") - .assertLogEqual("Host: " + host) - .assertLogEqual("Connection: Keep-Alive") - .assertLogEqual("Accept-Encoding: gzip") - .assertLogMatch("User-Agent: okhttp/.+") - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Type: text/event-stream") - .assertLogMatch("Transfer-encoding: chunked") - .assertLogEqual("<-- END HTTP (streaming)") - .assertNoMoreLogs(); - - applicationLogs - .assertLogEqual("--> GET " + url) - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Type: text/event-stream") - .assertLogMatch("Transfer-encoding: chunked") - .assertLogEqual("<-- END HTTP (streaming)") - .assertNoMoreLogs(); - } - - @Test public void bodyGetMalformedCharset() throws IOException { - setLevel(Level.BODY); - - server.enqueue(new MockResponse.Builder() - .setHeader("Content-Type", "text/html; charset=0") - .body("Body with unknown charset") - .build()); - Response response = client.newCall(request().build()).execute(); - response.body().close(); - - networkLogs - .assertLogEqual("--> GET " + url + " http/1.1") - .assertLogEqual("Host: " + host) - .assertLogEqual("Connection: Keep-Alive") - .assertLogEqual("Accept-Encoding: gzip") - .assertLogMatch("User-Agent: okhttp/.+") - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Type: text/html; charset=0") - .assertLogMatch("Content-Length: \\d+") - .assertLogMatch("") - .assertLogEqual("Body with unknown charset") - .assertLogMatch("<-- END HTTP \\(\\d+ms, 25-byte body\\)") - .assertNoMoreLogs(); - - applicationLogs - .assertLogEqual("--> GET " + url) - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Type: text/html; charset=0") - .assertLogMatch("Content-Length: \\d+") - .assertLogEqual("") - .assertLogEqual("Body with unknown charset") - .assertLogMatch("<-- END HTTP \\(\\d+ms, 25-byte body\\)") - .assertNoMoreLogs(); - } - - @Test public void responseBodyIsBinary() throws IOException { - setLevel(Level.BODY); - Buffer buffer = new Buffer(); - buffer.writeUtf8CodePoint(0x89); - buffer.writeUtf8CodePoint(0x50); - buffer.writeUtf8CodePoint(0x4e); - buffer.writeUtf8CodePoint(0x47); - buffer.writeUtf8CodePoint(0x0d); - buffer.writeUtf8CodePoint(0x0a); - buffer.writeUtf8CodePoint(0x1a); - buffer.writeUtf8CodePoint(0x0a); - server.enqueue(new MockResponse.Builder() - .body(buffer) - .setHeader("Content-Type", "image/png; charset=utf-8") - .build()); - Response response = client.newCall(request().build()).execute(); - response.body().close(); - - applicationLogs - .assertLogEqual("--> GET " + url) - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 9") - .assertLogEqual("Content-Type: image/png; charset=utf-8") - .assertLogEqual("") - .assertLogMatch("<-- END HTTP \\(\\d+ms, binary 9-byte body omitted\\)") - .assertNoMoreLogs(); - - networkLogs - .assertLogEqual("--> GET " + url + " http/1.1") - .assertLogEqual("Host: " + host) - .assertLogEqual("Connection: Keep-Alive") - .assertLogEqual("Accept-Encoding: gzip") - .assertLogMatch("User-Agent: okhttp/.+") - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 9") - .assertLogEqual("Content-Type: image/png; charset=utf-8") - .assertLogEqual("") - .assertLogMatch("<-- END HTTP \\(\\d+ms, binary 9-byte body omitted\\)") - .assertNoMoreLogs(); - } - - @Test public void connectFail() throws IOException { - setLevel(Level.BASIC); - client = new OkHttpClient.Builder() - .dns(hostname -> { throw new UnknownHostException("reason"); }) - .addInterceptor(applicationInterceptor) - .build(); - - try { - client.newCall(request().build()).execute(); - fail(); - } catch (UnknownHostException expected) { - } - - applicationLogs - .assertLogEqual("--> GET " + url) - .assertLogEqual("<-- HTTP FAILED: java.net.UnknownHostException: reason") - .assertNoMoreLogs(); - } - - @Test public void http2() throws Exception { - server.useHttps(handshakeCertificates.sslSocketFactory()); - url = server.url("/"); - - setLevel(Level.BASIC); - - server.enqueue(new MockResponse()); - Response response = client.newCall(request().build()).execute(); - assumeTrue(response.protocol().equals(Protocol.HTTP_2)); - - applicationLogs - .assertLogEqual("--> GET " + url) - .assertLogMatch("<-- 200 " + url + " \\(\\d+ms, 0-byte body\\)") - .assertNoMoreLogs(); - - networkLogs - .assertLogEqual("--> GET " + url + " h2") - .assertLogMatch("<-- 200 " + url + " \\(\\d+ms, 0-byte body\\)") - .assertNoMoreLogs(); - } - - @Test - public void headersAreRedacted() throws Exception { - HttpLoggingInterceptor networkInterceptor = - new HttpLoggingInterceptor(networkLogs).setLevel(Level.HEADERS); - networkInterceptor.redactHeader("sEnSiTiVe"); - - HttpLoggingInterceptor applicationInterceptor = - new HttpLoggingInterceptor(applicationLogs).setLevel(Level.HEADERS); - applicationInterceptor.redactHeader("sEnSiTiVe"); - - client = - new OkHttpClient.Builder() - .addNetworkInterceptor(networkInterceptor) - .addInterceptor(applicationInterceptor) - .build(); - - server.enqueue(new MockResponse.Builder() - .addHeader("SeNsItIvE", "Value").addHeader("Not-Sensitive", "Value") - .build()); - Response response = - client - .newCall( - request() - .addHeader("SeNsItIvE", "Value") - .addHeader("Not-Sensitive", "Value") - .build()) - .execute(); - response.body().close(); - - applicationLogs - .assertLogEqual("--> GET " + url) - .assertLogEqual("SeNsItIvE: ██") - .assertLogEqual("Not-Sensitive: Value") - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 0") - .assertLogEqual("SeNsItIvE: ██") - .assertLogEqual("Not-Sensitive: Value") - .assertLogEqual("<-- END HTTP") - .assertNoMoreLogs(); - - networkLogs - .assertLogEqual("--> GET " + url + " http/1.1") - .assertLogEqual("SeNsItIvE: ██") - .assertLogEqual("Not-Sensitive: Value") - .assertLogEqual("Host: " + host) - .assertLogEqual("Connection: Keep-Alive") - .assertLogEqual("Accept-Encoding: gzip") - .assertLogMatch("User-Agent: okhttp/.+") - .assertLogEqual("--> END GET") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 0") - .assertLogEqual("SeNsItIvE: ██") - .assertLogEqual("Not-Sensitive: Value") - .assertLogEqual("<-- END HTTP") - .assertNoMoreLogs(); - } - - @Test public void duplexRequestsAreNotLogged() throws Exception { - platform.assumeHttp2Support(); - - server.useHttps(handshakeCertificates.sslSocketFactory()); // HTTP/2 - url = server.url("/"); - - setLevel(Level.BODY); - - server.enqueue(new MockResponse.Builder() - .body("Hello response!") - .build()); - - RequestBody asyncRequestBody = new RequestBody() { - @Override public @Nullable MediaType contentType() { - return null; - } - - @Override public void writeTo(BufferedSink sink) throws IOException { - sink.writeUtf8("Hello request!"); - sink.close(); - } - - @Override public boolean isDuplex() { - return true; - } - }; - - Request request = request() - .post(asyncRequestBody) - .build(); - Response response = client.newCall(request).execute(); - assumeTrue(response.protocol().equals(Protocol.HTTP_2)); - - assertThat(response.body().string()).isEqualTo("Hello response!"); - - applicationLogs - .assertLogEqual("--> POST " + url) - .assertLogEqual("--> END POST (duplex request body omitted)") - .assertLogMatch("<-- 200 " + url + " \\(\\d+ms\\)") - .assertLogEqual("content-length: 15") - .assertLogEqual("") - .assertLogEqual("Hello response!") - .assertLogMatch("<-- END HTTP \\(\\d+ms, 15-byte body\\)") - .assertNoMoreLogs(); - } - - @Test public void oneShotRequestsAreNotLogged() throws Exception { - url = server.url("/"); - - setLevel(Level.BODY); - - server.enqueue(new MockResponse.Builder() - .body("Hello response!") - .build()); - - RequestBody asyncRequestBody = new RequestBody() { - @Override public @Nullable MediaType contentType() { - return null; - } - - int counter = 0; - @Override public void writeTo(BufferedSink sink) throws IOException { - counter++; - assertThat(counter).isLessThanOrEqualTo(1); - - sink.writeUtf8("Hello request!"); - sink.close(); - } - - @Override public boolean isOneShot() { - return true; - } - }; - - Request request = request() - .post(asyncRequestBody) - .build(); - Response response = client.newCall(request).execute(); - - assertThat(response.body().string()).isEqualTo("Hello response!"); - - applicationLogs - .assertLogEqual("--> POST " + url) - .assertLogEqual("--> END POST (one-shot body omitted)") - .assertLogMatch("<-- 200 OK " + url + " \\(\\d+ms\\)") - .assertLogEqual("Content-Length: 15") - .assertLogEqual("") - .assertLogEqual("Hello response!") - .assertLogMatch("<-- END HTTP \\(\\d+ms, 15-byte body\\)") - .assertNoMoreLogs(); - } - - private Request.Builder request() { - return new Request.Builder().url(url); - } - - static class LogRecorder implements HttpLoggingInterceptor.Logger { - private final List logs = new ArrayList<>(); - private int index; - - LogRecorder assertLogEqual(String expected) { - assertThat(index).overridingErrorMessage("No more messages found").isLessThan(logs.size()); - String actual = logs.get(index++); - assertThat(actual).isEqualTo(expected); - return this; - } - - LogRecorder assertLogMatch(String pattern) { - assertThat(index).overridingErrorMessage("No more messages found").isLessThan(logs.size()); - String actual = logs.get(index++); - assertThat(actual).matches(Pattern.compile(pattern, Pattern.DOTALL)); - return this; - } - - void assertNoMoreLogs() { - assertThat(logs.size()).overridingErrorMessage( - "More messages remain: " + logs.subList(index, logs.size())).isEqualTo(index); - } - - @Override public void log(String message) { - logs.add(message); - } - } -} diff --git a/okhttp-logging-interceptor/src/test/java/okhttp3/logging/HttpLoggingInterceptorTest.kt b/okhttp-logging-interceptor/src/test/java/okhttp3/logging/HttpLoggingInterceptorTest.kt new file mode 100644 index 000000000000..11c7459ec125 --- /dev/null +++ b/okhttp-logging-interceptor/src/test/java/okhttp3/logging/HttpLoggingInterceptorTest.kt @@ -0,0 +1,1016 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * 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 + * + * 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 okhttp3.logging + +import java.net.UnknownHostException +import java.util.regex.Pattern +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import mockwebserver3.junit5.internal.MockWebServerExtension +import okhttp3.HttpUrl +import okhttp3.Interceptor +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Protocol +import okhttp3.RecordingHostnameVerifier +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.gzip +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.logging.HttpLoggingInterceptor.Level +import okhttp3.testing.PlatformRule +import okio.Buffer +import okio.BufferedSink +import okio.ByteString.Companion.decodeBase64 +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.fail +import org.junit.jupiter.api.Assumptions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.RegisterExtension + +@ExtendWith(MockWebServerExtension::class) +class HttpLoggingInterceptorTest { + @RegisterExtension + val platform = PlatformRule() + private lateinit var server: MockWebServer + private val handshakeCertificates = platform.localhostHandshakeCertificates() + private val hostnameVerifier = RecordingHostnameVerifier() + private lateinit var client: OkHttpClient + private lateinit var host: String + private lateinit var url: HttpUrl + private val networkLogs = LogRecorder() + private val networkInterceptor = HttpLoggingInterceptor(networkLogs) + private val applicationLogs = LogRecorder() + private val applicationInterceptor = HttpLoggingInterceptor(applicationLogs) + private var extraNetworkInterceptor: Interceptor? = null + + private fun setLevel(level: Level) { + networkInterceptor.setLevel(level) + applicationInterceptor.setLevel(level) + } + + @BeforeEach + fun setUp(server: MockWebServer) { + this.server = server + client = OkHttpClient.Builder() + .addNetworkInterceptor(Interceptor { chain -> + when { + extraNetworkInterceptor != null -> extraNetworkInterceptor!!.intercept(chain) + else -> chain.proceed(chain.request()) + } + }) + .addNetworkInterceptor(networkInterceptor) + .addInterceptor(applicationInterceptor) + .sslSocketFactory( + handshakeCertificates.sslSocketFactory(), + handshakeCertificates.trustManager, + ) + .hostnameVerifier(hostnameVerifier) + .build() + host = "${server.hostName}:${server.port}" + url = server.url("/") + } + + @Test + fun levelGetter() { + // The default is NONE. + assertThat(applicationInterceptor.level).isEqualTo(Level.NONE) + for (level in Level.entries) { + applicationInterceptor.setLevel(level) + assertThat(applicationInterceptor.level).isEqualTo(level) + } + } + + @Test + fun setLevelShouldReturnSameInstanceOfInterceptor() { + for (level in Level.entries) { + assertThat(applicationInterceptor.setLevel(level)).isSameAs(applicationInterceptor) + } + } + + @Test + fun none() { + server.enqueue(MockResponse()) + client.newCall(request().build()).execute() + applicationLogs.assertNoMoreLogs() + networkLogs.assertNoMoreLogs() + } + + @Test + fun basicGet() { + setLevel(Level.BASIC) + server.enqueue(MockResponse()) + client.newCall(request().build()).execute() + applicationLogs + .assertLogEqual("--> GET $url") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms, 0-byte body\)""")) + .assertNoMoreLogs() + networkLogs + .assertLogEqual("--> GET $url http/1.1") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms, 0-byte body\)""")) + .assertNoMoreLogs() + } + + @Test + fun basicPost() { + setLevel(Level.BASIC) + server.enqueue(MockResponse()) + client.newCall(request().post("Hi?".toRequestBody(PLAIN)).build()).execute() + applicationLogs + .assertLogEqual("--> POST $url (3-byte body)") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms, 0-byte body\)""")) + .assertNoMoreLogs() + networkLogs + .assertLogEqual("--> POST $url http/1.1 (3-byte body)") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms, 0-byte body\)""")) + .assertNoMoreLogs() + } + + @Test + fun basicResponseBody() { + setLevel(Level.BASIC) + server.enqueue( + MockResponse.Builder() + .body("Hello!") + .setHeader("Content-Type", PLAIN) + .build() + ) + val response = client.newCall(request().build()).execute() + response.body.close() + applicationLogs + .assertLogEqual("--> GET $url") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms, 6-byte body\)""")) + .assertNoMoreLogs() + networkLogs + .assertLogEqual("--> GET $url http/1.1") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms, 6-byte body\)""")) + .assertNoMoreLogs() + } + + @Test + fun basicChunkedResponseBody() { + setLevel(Level.BASIC) + server.enqueue( + MockResponse.Builder() + .chunkedBody("Hello!", 2) + .setHeader("Content-Type", PLAIN) + .build() + ) + val response = client.newCall(request().build()).execute() + response.body.close() + applicationLogs + .assertLogEqual("--> GET $url") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms, unknown-length body\)""")) + .assertNoMoreLogs() + networkLogs + .assertLogEqual("--> GET $url http/1.1") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms, unknown-length body\)""")) + .assertNoMoreLogs() + } + + @Test + fun headersGet() { + setLevel(Level.HEADERS) + server.enqueue(MockResponse()) + val response = client.newCall(request().build()).execute() + response.body.close() + applicationLogs + .assertLogEqual("--> GET $url") + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 0") + .assertLogEqual("<-- END HTTP") + .assertNoMoreLogs() + networkLogs + .assertLogEqual("--> GET $url http/1.1") + .assertLogEqual("Host: $host") + .assertLogEqual("Connection: Keep-Alive") + .assertLogEqual("Accept-Encoding: gzip") + .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 0") + .assertLogEqual("<-- END HTTP") + .assertNoMoreLogs() + } + + @Test + fun headersPost() { + setLevel(Level.HEADERS) + server.enqueue(MockResponse()) + val request = request().post("Hi?".toRequestBody(PLAIN)).build() + val response = client.newCall(request).execute() + response.body.close() + applicationLogs + .assertLogEqual("--> POST $url") + .assertLogEqual("Content-Type: text/plain; charset=utf-8") + .assertLogEqual("Content-Length: 3") + .assertLogEqual("--> END POST") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 0") + .assertLogEqual("<-- END HTTP") + .assertNoMoreLogs() + networkLogs + .assertLogEqual("--> POST $url http/1.1") + .assertLogEqual("Content-Type: text/plain; charset=utf-8") + .assertLogEqual("Content-Length: 3") + .assertLogEqual("Host: $host") + .assertLogEqual("Connection: Keep-Alive") + .assertLogEqual("Accept-Encoding: gzip") + .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) + .assertLogEqual("--> END POST") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 0") + .assertLogEqual("<-- END HTTP") + .assertNoMoreLogs() + } + + @Test + fun headersPostNoContentType() { + setLevel(Level.HEADERS) + server.enqueue(MockResponse()) + val request = request().post("Hi?".toRequestBody(null)).build() + val response = client.newCall(request).execute() + response.body.close() + applicationLogs + .assertLogEqual("--> POST $url") + .assertLogEqual("Content-Length: 3") + .assertLogEqual("--> END POST") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 0") + .assertLogEqual("<-- END HTTP") + .assertNoMoreLogs() + networkLogs + .assertLogEqual("--> POST $url http/1.1") + .assertLogEqual("Content-Length: 3") + .assertLogEqual("Host: $host") + .assertLogEqual("Connection: Keep-Alive") + .assertLogEqual("Accept-Encoding: gzip") + .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) + .assertLogEqual("--> END POST") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 0") + .assertLogEqual("<-- END HTTP") + .assertNoMoreLogs() + } + + @Test + fun headersPostNoLength() { + setLevel(Level.HEADERS) + server.enqueue(MockResponse()) + val body: RequestBody = object : RequestBody() { + override fun contentType() = PLAIN + + override fun writeTo(sink: BufferedSink) { + sink.writeUtf8("Hi!") + } + } + val response = client.newCall(request().post(body).build()).execute() + response.body.close() + applicationLogs + .assertLogEqual("--> POST $url") + .assertLogEqual("Content-Type: text/plain; charset=utf-8") + .assertLogEqual("--> END POST") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 0") + .assertLogEqual("<-- END HTTP") + .assertNoMoreLogs() + networkLogs + .assertLogEqual("--> POST $url http/1.1") + .assertLogEqual("Content-Type: text/plain; charset=utf-8") + .assertLogEqual("Transfer-Encoding: chunked") + .assertLogEqual("Host: $host") + .assertLogEqual("Connection: Keep-Alive") + .assertLogEqual("Accept-Encoding: gzip") + .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) + .assertLogEqual("--> END POST") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 0") + .assertLogEqual("<-- END HTTP") + .assertNoMoreLogs() + } + + @Test + fun headersPostWithHeaderOverrides() { + setLevel(Level.HEADERS) + extraNetworkInterceptor = Interceptor { chain: Interceptor.Chain -> + chain.proceed( + chain.request() + .newBuilder() + .header("Content-Length", "2") + .header("Content-Type", "text/plain-ish") + .build() + ) + } + server.enqueue(MockResponse()) + client.newCall( + request() + .post("Hi?".toRequestBody(PLAIN)) + .build() + ).execute() + applicationLogs + .assertLogEqual("--> POST $url") + .assertLogEqual("Content-Type: text/plain; charset=utf-8") + .assertLogEqual("Content-Length: 3") + .assertLogEqual("--> END POST") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 0") + .assertLogEqual("<-- END HTTP") + .assertNoMoreLogs() + networkLogs + .assertLogEqual("--> POST $url http/1.1") + .assertLogEqual("Host: $host") + .assertLogEqual("Connection: Keep-Alive") + .assertLogEqual("Accept-Encoding: gzip") + .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) + .assertLogEqual("Content-Length: 2") + .assertLogEqual("Content-Type: text/plain-ish") + .assertLogEqual("--> END POST") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 0") + .assertLogEqual("<-- END HTTP") + .assertNoMoreLogs() + } + + @Test + fun headersResponseBody() { + setLevel(Level.HEADERS) + server.enqueue( + MockResponse.Builder() + .body("Hello!") + .setHeader("Content-Type", PLAIN) + .build() + ) + val response = client.newCall(request().build()).execute() + response.body.close() + applicationLogs + .assertLogEqual("--> GET $url") + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 6") + .assertLogEqual("Content-Type: text/plain; charset=utf-8") + .assertLogEqual("<-- END HTTP") + .assertNoMoreLogs() + networkLogs + .assertLogEqual("--> GET $url http/1.1") + .assertLogEqual("Host: $host") + .assertLogEqual("Connection: Keep-Alive") + .assertLogEqual("Accept-Encoding: gzip") + .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 6") + .assertLogEqual("Content-Type: text/plain; charset=utf-8") + .assertLogEqual("<-- END HTTP") + .assertNoMoreLogs() + } + + @Test + fun bodyGet() { + setLevel(Level.BODY) + server.enqueue(MockResponse()) + val response = client.newCall(request().build()).execute() + response.body.close() + applicationLogs + .assertLogEqual("--> GET $url") + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 0") + .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 0-byte body\)""")) + .assertNoMoreLogs() + networkLogs + .assertLogEqual("--> GET $url http/1.1") + .assertLogEqual("Host: $host") + .assertLogEqual("Connection: Keep-Alive") + .assertLogEqual("Accept-Encoding: gzip") + .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 0") + .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 0-byte body\)""")) + .assertNoMoreLogs() + } + + @Test + fun bodyGet204() { + setLevel(Level.BODY) + bodyGetNoBody(204) + } + + @Test + fun bodyGet205() { + setLevel(Level.BODY) + bodyGetNoBody(205) + } + + private fun bodyGetNoBody(code: Int) { + server.enqueue( + MockResponse.Builder() + .status("HTTP/1.1 $code No Content") + .build() + ) + val response = client.newCall(request().build()).execute() + response.body.close() + applicationLogs + .assertLogEqual("--> GET $url") + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- $code No Content $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 0") + .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 0-byte body\)""")) + .assertNoMoreLogs() + networkLogs + .assertLogEqual("--> GET $url http/1.1") + .assertLogEqual("Host: $host") + .assertLogEqual("Connection: Keep-Alive") + .assertLogEqual("Accept-Encoding: gzip") + .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- $code No Content $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 0") + .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 0-byte body\)""")) + .assertNoMoreLogs() + } + + @Test + fun bodyPost() { + setLevel(Level.BODY) + server.enqueue(MockResponse()) + val request = request().post("Hi?".toRequestBody(PLAIN)).build() + val response = client.newCall(request).execute() + response.body.close() + applicationLogs + .assertLogEqual("--> POST $url") + .assertLogEqual("Content-Type: text/plain; charset=utf-8") + .assertLogEqual("Content-Length: 3") + .assertLogEqual("") + .assertLogEqual("Hi?") + .assertLogEqual("--> END POST (3-byte body)") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 0") + .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 0-byte body\)""")) + .assertNoMoreLogs() + networkLogs + .assertLogEqual("--> POST $url http/1.1") + .assertLogEqual("Content-Type: text/plain; charset=utf-8") + .assertLogEqual("Content-Length: 3") + .assertLogEqual("Host: $host") + .assertLogEqual("Connection: Keep-Alive") + .assertLogEqual("Accept-Encoding: gzip") + .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) + .assertLogEqual("") + .assertLogEqual("Hi?") + .assertLogEqual("--> END POST (3-byte body)") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 0") + .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 0-byte body\)""")) + .assertNoMoreLogs() + } + + @Test + fun bodyResponseBody() { + setLevel(Level.BODY) + server.enqueue( + MockResponse.Builder() + .body("Hello!") + .setHeader("Content-Type", PLAIN) + .build() + ) + val response = client.newCall(request().build()).execute() + response.body.close() + applicationLogs + .assertLogEqual("--> GET $url") + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 6") + .assertLogEqual("Content-Type: text/plain; charset=utf-8") + .assertLogEqual("") + .assertLogEqual("Hello!") + .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 6-byte body\)""")) + .assertNoMoreLogs() + networkLogs + .assertLogEqual("--> GET $url http/1.1") + .assertLogEqual("Host: $host") + .assertLogEqual("Connection: Keep-Alive") + .assertLogEqual("Accept-Encoding: gzip") + .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 6") + .assertLogEqual("Content-Type: text/plain; charset=utf-8") + .assertLogEqual("") + .assertLogEqual("Hello!") + .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 6-byte body\)""")) + .assertNoMoreLogs() + } + + @Test + fun bodyResponseBodyChunked() { + setLevel(Level.BODY) + server.enqueue( + MockResponse.Builder() + .chunkedBody("Hello!", 2) + .setHeader("Content-Type", PLAIN) + .build() + ) + val response = client.newCall(request().build()).execute() + response.body.close() + applicationLogs + .assertLogEqual("--> GET $url") + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Transfer-encoding: chunked") + .assertLogEqual("Content-Type: text/plain; charset=utf-8") + .assertLogEqual("") + .assertLogEqual("Hello!") + .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 6-byte body\)""")) + .assertNoMoreLogs() + networkLogs + .assertLogEqual("--> GET $url http/1.1") + .assertLogEqual("Host: $host") + .assertLogEqual("Connection: Keep-Alive") + .assertLogEqual("Accept-Encoding: gzip") + .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Transfer-encoding: chunked") + .assertLogEqual("Content-Type: text/plain; charset=utf-8") + .assertLogEqual("") + .assertLogEqual("Hello!") + .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 6-byte body\)""")) + .assertNoMoreLogs() + } + + @Test + fun bodyRequestGzipEncoded() { + setLevel(Level.BODY) + server.enqueue( + MockResponse.Builder() + .setHeader("Content-Type", PLAIN) + .body(Buffer().writeUtf8("Uncompressed")) + .build() + ) + val response = client.newCall( + request() + .addHeader("Content-Encoding", "gzip") + .post("Uncompressed".toRequestBody().gzip()) + .build() + ).execute() + val responseBody = response.body + assertThat(responseBody.string()).overridingErrorMessage( + "Expected response body to be valid" + ).isEqualTo("Uncompressed") + responseBody.close() + networkLogs + .assertLogEqual("--> POST $url http/1.1") + .assertLogEqual("Content-Encoding: gzip") + .assertLogEqual("Transfer-Encoding: chunked") + .assertLogEqual("Host: $host") + .assertLogEqual("Connection: Keep-Alive") + .assertLogEqual("Accept-Encoding: gzip") + .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) + .assertLogEqual("") + .assertLogEqual("--> END POST (12-byte, 32-gzipped-byte body)") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Type: text/plain; charset=utf-8") + .assertLogMatch(Regex("""Content-Length: \d+""")) + .assertLogEqual("") + .assertLogEqual("Uncompressed") + .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 12-byte body\)""")) + .assertNoMoreLogs() + } + + @Test + fun bodyResponseGzipEncoded() { + setLevel(Level.BODY) + server.enqueue( + MockResponse.Builder() + .setHeader("Content-Encoding", "gzip") + .setHeader("Content-Type", PLAIN) + .body(Buffer().write("H4sIAAAAAAAAAPNIzcnJ11HwQKIAdyO+9hMAAAA=".decodeBase64()!!)) + .build() + ) + val response = client.newCall(request().build()).execute() + val responseBody = response.body + assertThat(responseBody.string()).overridingErrorMessage( + "Expected response body to be valid" + ).isEqualTo("Hello, Hello, Hello") + responseBody.close() + networkLogs + .assertLogEqual("--> GET $url http/1.1") + .assertLogEqual("Host: $host") + .assertLogEqual("Connection: Keep-Alive") + .assertLogEqual("Accept-Encoding: gzip") + .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Encoding: gzip") + .assertLogEqual("Content-Type: text/plain; charset=utf-8") + .assertLogMatch(Regex("""Content-Length: \d+""")) + .assertLogEqual("") + .assertLogEqual("Hello, Hello, Hello") + .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 19-byte, 29-gzipped-byte body\)""")) + .assertNoMoreLogs() + applicationLogs + .assertLogEqual("--> GET $url") + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Type: text/plain; charset=utf-8") + .assertLogEqual("") + .assertLogEqual("Hello, Hello, Hello") + .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 19-byte body\)""")) + .assertNoMoreLogs() + } + + @Test + fun bodyResponseUnknownEncoded() { + setLevel(Level.BODY) + server.enqueue( + MockResponse.Builder() // It's invalid to return this if not requested, but the server might anyway + .setHeader("Content-Encoding", "br") + .setHeader("Content-Type", PLAIN) + .body(Buffer().write("iwmASGVsbG8sIEhlbGxvLCBIZWxsbwoD".decodeBase64()!!)) + .build() + ) + val response = client.newCall(request().build()).execute() + response.body.close() + networkLogs + .assertLogEqual("--> GET $url http/1.1") + .assertLogEqual("Host: $host") + .assertLogEqual("Connection: Keep-Alive") + .assertLogEqual("Accept-Encoding: gzip") + .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Encoding: br") + .assertLogEqual("Content-Type: text/plain; charset=utf-8") + .assertLogMatch(Regex("""Content-Length: \d+""")) + .assertLogEqual("<-- END HTTP (encoded body omitted)") + .assertNoMoreLogs() + applicationLogs + .assertLogEqual("--> GET $url") + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Encoding: br") + .assertLogEqual("Content-Type: text/plain; charset=utf-8") + .assertLogMatch(Regex("""Content-Length: \d+""")) + .assertLogEqual("<-- END HTTP (encoded body omitted)") + .assertNoMoreLogs() + } + + @Test + fun bodyResponseIsStreaming() { + setLevel(Level.BODY) + server.enqueue( + MockResponse.Builder() + .setHeader("Content-Type", "text/event-stream") + .chunkedBody( + """ + |event: add + |data: 73857293 + | + |event: remove + |data: 2153 + | + |event: add + |data: 113411 + | + | + """.trimMargin(), 8 + ) + .build() + ) + val response = client.newCall(request().build()).execute() + response.body.close() + networkLogs + .assertLogEqual("--> GET $url http/1.1") + .assertLogEqual("Host: $host") + .assertLogEqual("Connection: Keep-Alive") + .assertLogEqual("Accept-Encoding: gzip") + .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Type: text/event-stream") + .assertLogMatch(Regex("""Transfer-encoding: chunked""")) + .assertLogEqual("<-- END HTTP (streaming)") + .assertNoMoreLogs() + applicationLogs + .assertLogEqual("--> GET $url") + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Type: text/event-stream") + .assertLogMatch(Regex("""Transfer-encoding: chunked""")) + .assertLogEqual("<-- END HTTP (streaming)") + .assertNoMoreLogs() + } + + @Test + fun bodyGetMalformedCharset() { + setLevel(Level.BODY) + server.enqueue( + MockResponse.Builder() + .setHeader("Content-Type", "text/html; charset=0") + .body("Body with unknown charset") + .build() + ) + val response = client.newCall(request().build()).execute() + response.body.close() + networkLogs + .assertLogEqual("--> GET $url http/1.1") + .assertLogEqual("Host: $host") + .assertLogEqual("Connection: Keep-Alive") + .assertLogEqual("Accept-Encoding: gzip") + .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Type: text/html; charset=0") + .assertLogMatch(Regex("""Content-Length: \d+""")) + .assertLogMatch(Regex("")) + .assertLogEqual("Body with unknown charset") + .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 25-byte body\)""")) + .assertNoMoreLogs() + applicationLogs + .assertLogEqual("--> GET $url") + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Type: text/html; charset=0") + .assertLogMatch(Regex("""Content-Length: \d+""")) + .assertLogEqual("") + .assertLogEqual("Body with unknown charset") + .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 25-byte body\)""")) + .assertNoMoreLogs() + } + + @Test + fun responseBodyIsBinary() { + setLevel(Level.BODY) + val buffer = Buffer() + buffer.writeUtf8CodePoint(0x89) + buffer.writeUtf8CodePoint(0x50) + buffer.writeUtf8CodePoint(0x4e) + buffer.writeUtf8CodePoint(0x47) + buffer.writeUtf8CodePoint(0x0d) + buffer.writeUtf8CodePoint(0x0a) + buffer.writeUtf8CodePoint(0x1a) + buffer.writeUtf8CodePoint(0x0a) + server.enqueue( + MockResponse.Builder() + .body(buffer) + .setHeader("Content-Type", "image/png; charset=utf-8") + .build() + ) + val response = client.newCall(request().build()).execute() + response.body.close() + applicationLogs + .assertLogEqual("--> GET $url") + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 9") + .assertLogEqual("Content-Type: image/png; charset=utf-8") + .assertLogEqual("") + .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, binary 9-byte body omitted\)""")) + .assertNoMoreLogs() + networkLogs + .assertLogEqual("--> GET $url http/1.1") + .assertLogEqual("Host: $host") + .assertLogEqual("Connection: Keep-Alive") + .assertLogEqual("Accept-Encoding: gzip") + .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 9") + .assertLogEqual("Content-Type: image/png; charset=utf-8") + .assertLogEqual("") + .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, binary 9-byte body omitted\)""")) + .assertNoMoreLogs() + } + + @Test + fun connectFail() { + setLevel(Level.BASIC) + client = OkHttpClient.Builder() + .dns { hostname: String? -> throw UnknownHostException("reason") } + .addInterceptor(applicationInterceptor) + .build() + try { + client.newCall(request().build()).execute() + fail() + } catch (expected: UnknownHostException) { + } + applicationLogs + .assertLogEqual("--> GET $url") + .assertLogEqual("<-- HTTP FAILED: java.net.UnknownHostException: reason") + .assertNoMoreLogs() + } + + @Test + fun http2() { + server.useHttps(handshakeCertificates.sslSocketFactory()) + url = server.url("/") + setLevel(Level.BASIC) + server.enqueue(MockResponse()) + val response = client.newCall(request().build()).execute() + Assumptions.assumeTrue(response.protocol == Protocol.HTTP_2) + applicationLogs + .assertLogEqual("--> GET $url") + .assertLogMatch(Regex("""<-- 200 $url \(\d+ms, 0-byte body\)""")) + .assertNoMoreLogs() + networkLogs + .assertLogEqual("--> GET $url h2") + .assertLogMatch(Regex("""<-- 200 $url \(\d+ms, 0-byte body\)""")) + .assertNoMoreLogs() + } + + @Test + fun headersAreRedacted() { + val networkInterceptor = HttpLoggingInterceptor(networkLogs).setLevel( + Level.HEADERS + ) + networkInterceptor.redactHeader("sEnSiTiVe") + val applicationInterceptor = HttpLoggingInterceptor(applicationLogs).setLevel( + Level.HEADERS + ) + applicationInterceptor.redactHeader("sEnSiTiVe") + client = OkHttpClient.Builder() + .addNetworkInterceptor(networkInterceptor) + .addInterceptor(applicationInterceptor) + .build() + server.enqueue( + MockResponse.Builder() + .addHeader("SeNsItIvE", "Value").addHeader("Not-Sensitive", "Value") + .build() + ) + val response = client + .newCall( + request() + .addHeader("SeNsItIvE", "Value") + .addHeader("Not-Sensitive", "Value") + .build() + ) + .execute() + response.body.close() + applicationLogs + .assertLogEqual("--> GET $url") + .assertLogEqual("SeNsItIvE: ██") + .assertLogEqual("Not-Sensitive: Value") + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 0") + .assertLogEqual("SeNsItIvE: ██") + .assertLogEqual("Not-Sensitive: Value") + .assertLogEqual("<-- END HTTP") + .assertNoMoreLogs() + networkLogs + .assertLogEqual("--> GET $url http/1.1") + .assertLogEqual("SeNsItIvE: ██") + .assertLogEqual("Not-Sensitive: Value") + .assertLogEqual("Host: $host") + .assertLogEqual("Connection: Keep-Alive") + .assertLogEqual("Accept-Encoding: gzip") + .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("Content-Length: 0") + .assertLogEqual("SeNsItIvE: ██") + .assertLogEqual("Not-Sensitive: Value") + .assertLogEqual("<-- END HTTP") + .assertNoMoreLogs() + } + + @Test + fun duplexRequestsAreNotLogged() { + platform.assumeHttp2Support() + server.useHttps(handshakeCertificates.sslSocketFactory()) // HTTP/2 + url = server.url("/") + setLevel(Level.BODY) + server.enqueue( + MockResponse.Builder() + .body("Hello response!") + .build() + ) + val asyncRequestBody: RequestBody = object : RequestBody() { + override fun contentType(): MediaType? { + return null + } + + override fun writeTo(sink: BufferedSink) { + sink.writeUtf8("Hello request!") + sink.close() + } + + override fun isDuplex(): Boolean { + return true + } + } + val request = request() + .post(asyncRequestBody) + .build() + val response = client.newCall(request).execute() + Assumptions.assumeTrue(response.protocol == Protocol.HTTP_2) + assertThat(response.body.string()).isEqualTo("Hello response!") + applicationLogs + .assertLogEqual("--> POST $url") + .assertLogEqual("--> END POST (duplex request body omitted)") + .assertLogMatch(Regex("""<-- 200 $url \(\d+ms\)""")) + .assertLogEqual("content-length: 15") + .assertLogEqual("") + .assertLogEqual("Hello response!") + .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 15-byte body\)""")) + .assertNoMoreLogs() + } + + @Test + fun oneShotRequestsAreNotLogged() { + url = server.url("/") + setLevel(Level.BODY) + server.enqueue( + MockResponse.Builder() + .body("Hello response!") + .build() + ) + val asyncRequestBody: RequestBody = object : RequestBody() { + var counter = 0 + + override fun contentType() = null + + override fun writeTo(sink: BufferedSink) { + counter++ + assertThat(counter).isLessThanOrEqualTo(1) + sink.writeUtf8("Hello request!") + sink.close() + } + + override fun isOneShot() = true + } + val request = request() + .post(asyncRequestBody) + .build() + val response = client.newCall(request).execute() + assertThat(response.body.string()).isEqualTo("Hello response!") + applicationLogs + .assertLogEqual("""--> POST $url""") + .assertLogEqual("""--> END POST (one-shot body omitted)""") + .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) + .assertLogEqual("""Content-Length: 15""") + .assertLogEqual("") + .assertLogEqual("""Hello response!""") + .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 15-byte body\)""")) + .assertNoMoreLogs() + } + + private fun request(): Request.Builder { + return Request.Builder().url(url) + } + + internal class LogRecorder( + val prefix: Regex = Regex(""), + ) : HttpLoggingInterceptor.Logger { + private val logs = mutableListOf() + private var index = 0 + + fun assertLogEqual(expected: String) = apply { + assertThat(index) + .overridingErrorMessage("No more messages found") + .isLessThan(logs.size) + assertThat(logs[index++]).isEqualTo(expected) + return this + } + + fun assertLogMatch(regex: Regex) = apply { + assertThat(index) + .overridingErrorMessage("No more messages found") + .isLessThan(logs.size) + assertThat(logs[index++]) + .matches(Pattern.compile(prefix.pattern + regex.pattern, Pattern.DOTALL)) + } + + fun assertNoMoreLogs() { + assertThat(logs.size) + .overridingErrorMessage("More messages remain: ${logs.subList(index, logs.size)}") + .isEqualTo(index) + } + + override fun log(message: String) { + logs.add(message) + } + } + + companion object { + private val PLAIN = "text/plain; charset=utf-8".toMediaType() + } +} diff --git a/okhttp-logging-interceptor/src/test/java/okhttp3/logging/LoggingEventListenerTest.java b/okhttp-logging-interceptor/src/test/java/okhttp3/logging/LoggingEventListenerTest.java deleted file mode 100644 index 1b29dc64f86c..000000000000 --- a/okhttp-logging-interceptor/src/test/java/okhttp3/logging/LoggingEventListenerTest.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright (C) 2018 Square, Inc. - * - * 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 - * - * 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 okhttp3.logging; - -import java.io.IOException; -import java.net.UnknownHostException; -import mockwebserver3.MockResponse; -import mockwebserver3.MockWebServer; -import mockwebserver3.SocketPolicy; -import mockwebserver3.SocketPolicy.FailHandshake; -import mockwebserver3.junit5.internal.MockWebServerExtension; -import okhttp3.Call; -import okhttp3.EventListener; -import okhttp3.HttpUrl; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.OkHttpClientTestRule; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.TestUtil; -import okhttp3.testing.PlatformRule; -import okhttp3.tls.HandshakeCertificates; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.RegisterExtension; -import static java.util.Arrays.asList; -import static okhttp3.Protocol.HTTP_1_1; -import static okhttp3.Protocol.HTTP_2; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.fail; - -@ExtendWith(MockWebServerExtension.class) -public final class LoggingEventListenerTest { - private static final MediaType PLAIN = MediaType.get("text/plain"); - - @RegisterExtension public final PlatformRule platform = new PlatformRule(); - @RegisterExtension public final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule(); - private MockWebServer server; - - private final HandshakeCertificates handshakeCertificates - = platform.localhostHandshakeCertificates(); - private final LogRecorder logRecorder = new LogRecorder(); - private final LoggingEventListener.Factory loggingEventListenerFactory = - new LoggingEventListener.Factory(logRecorder); - private OkHttpClient client; - private HttpUrl url; - - @BeforeEach - public void setUp(MockWebServer server) { - this.server = server; - this.client = clientTestRule.newClientBuilder() - .eventListenerFactory(loggingEventListenerFactory) - .sslSocketFactory(handshakeCertificates.sslSocketFactory(), - handshakeCertificates.trustManager()) - .retryOnConnectionFailure(false) - .build(); - - url = server.url("/"); - } - - @Test - public void get() throws Exception { - TestUtil.assumeNotWindows(); - - server.enqueue(new MockResponse.Builder() - .body("Hello!") - .setHeader("Content-Type", PLAIN) - .build()); - Response response = client.newCall(request().build()).execute(); - assertThat(response.body()).isNotNull(); - response.body().bytes(); - - logRecorder - .assertLogMatch("callStart: Request\\{method=GET, url=" + url + "\\}") - .assertLogMatch("proxySelectStart: " + url) - .assertLogMatch("proxySelectEnd: \\[DIRECT\\]") - .assertLogMatch("dnsStart: " + url.host()) - .assertLogMatch("dnsEnd: \\[.+\\]") - .assertLogMatch("connectStart: " + url.host() + "/.+ DIRECT") - .assertLogMatch("connectEnd: http/1.1") - .assertLogMatch( - "connectionAcquired: Connection\\{" - + url.host() - + ":\\d+, proxy=DIRECT hostAddress=" - + url.host() - + "/.+ cipherSuite=none protocol=http/1\\.1\\}") - .assertLogMatch("requestHeadersStart") - .assertLogMatch("requestHeadersEnd") - .assertLogMatch("responseHeadersStart") - .assertLogMatch( - "responseHeadersEnd: Response\\{protocol=http/1\\.1, code=200, message=OK, url=" - + url - + "\\}") - .assertLogMatch("responseBodyStart") - .assertLogMatch("responseBodyEnd: byteCount=6") - .assertLogMatch("connectionReleased") - .assertLogMatch("callEnd") - .assertNoMoreLogs(); - } - - @Test - public void post() throws IOException { - TestUtil.assumeNotWindows(); - - server.enqueue(new MockResponse()); - client.newCall(request().post(RequestBody.create("Hello!", PLAIN)).build()).execute(); - - logRecorder - .assertLogMatch("callStart: Request\\{method=POST, url=" + url + "\\}") - .assertLogMatch("proxySelectStart: " + url) - .assertLogMatch("proxySelectEnd: \\[DIRECT\\]") - .assertLogMatch("dnsStart: " + url.host()) - .assertLogMatch("dnsEnd: \\[.+\\]") - .assertLogMatch("connectStart: " + url.host() + "/.+ DIRECT") - .assertLogMatch("connectEnd: http/1.1") - .assertLogMatch( - "connectionAcquired: Connection\\{" - + url.host() - + ":\\d+, proxy=DIRECT hostAddress=" - + url.host() - + "/.+ cipherSuite=none protocol=http/1\\.1\\}") - .assertLogMatch("requestHeadersStart") - .assertLogMatch("requestHeadersEnd") - .assertLogMatch("requestBodyStart") - .assertLogMatch("requestBodyEnd: byteCount=6") - .assertLogMatch("responseHeadersStart") - .assertLogMatch( - "responseHeadersEnd: Response\\{protocol=http/1\\.1, code=200, message=OK, url=" - + url - + "\\}") - .assertLogMatch("responseBodyStart") - .assertLogMatch("responseBodyEnd: byteCount=0") - .assertLogMatch("connectionReleased") - .assertLogMatch("callEnd") - .assertNoMoreLogs(); - } - - @Test - public void secureGet() throws Exception { - TestUtil.assumeNotWindows(); - - server.useHttps(handshakeCertificates.sslSocketFactory()); - url = server.url("/"); - - server.enqueue(new MockResponse()); - Response response = client.newCall(request().build()).execute(); - assertThat(response.body()).isNotNull(); - response.body().bytes(); - - platform.assumeHttp2Support(); - - logRecorder - .assertLogMatch("callStart: Request\\{method=GET, url=" + url + "\\}") - .assertLogMatch("proxySelectStart: " + url) - .assertLogMatch("proxySelectEnd: \\[DIRECT\\]") - .assertLogMatch("dnsStart: " + url.host()) - .assertLogMatch("dnsEnd: \\[.+\\]") - .assertLogMatch("connectStart: " + url.host() + "/.+ DIRECT") - .assertLogMatch("secureConnectStart") - .assertLogMatch("secureConnectEnd: Handshake\\{" - + "tlsVersion=TLS_1_[23] " - + "cipherSuite=TLS_.* " - + "peerCertificates=\\[CN=localhost\\] " - + "localCertificates=\\[\\]}") - .assertLogMatch("connectEnd: h2") - .assertLogMatch( - "connectionAcquired: Connection\\{" - + url.host() - + ":\\d+, proxy=DIRECT hostAddress=" - + url.host() - + "/.+ cipherSuite=.+ protocol=h2\\}") - .assertLogMatch("requestHeadersStart") - .assertLogMatch("requestHeadersEnd") - .assertLogMatch("responseHeadersStart") - .assertLogMatch( - "responseHeadersEnd: Response\\{protocol=h2, code=200, message=, url=" + url + "\\}") - .assertLogMatch("responseBodyStart") - .assertLogMatch("responseBodyEnd: byteCount=0") - .assertLogMatch("connectionReleased") - .assertLogMatch("callEnd") - .assertNoMoreLogs(); - } - - @Test - public void dnsFail() throws IOException { - client = new OkHttpClient.Builder() - .dns(hostname -> { throw new UnknownHostException("reason"); }) - .eventListenerFactory(loggingEventListenerFactory) - .build(); - - try { - client.newCall(request().build()).execute(); - fail(); - } catch (UnknownHostException expected) { - } - - logRecorder - .assertLogMatch("callStart: Request\\{method=GET, url=" + url + "\\}") - .assertLogMatch("proxySelectStart: " + url) - .assertLogMatch("proxySelectEnd: \\[DIRECT\\]") - .assertLogMatch("dnsStart: " + url.host()) - .assertLogMatch("callFailed: java.net.UnknownHostException: reason") - .assertNoMoreLogs(); - } - - @Test - public void connectFail() { - TestUtil.assumeNotWindows(); - - server.useHttps(handshakeCertificates.sslSocketFactory()); - server.setProtocols(asList(HTTP_2, HTTP_1_1)); - server.enqueue(new MockResponse.Builder() - .socketPolicy(FailHandshake.INSTANCE) - .build()); - url = server.url("/"); - - try { - client.newCall(request().build()).execute(); - fail(); - } catch (IOException expected) { - } - - logRecorder - .assertLogMatch("callStart: Request\\{method=GET, url=" + url + "\\}") - .assertLogMatch("proxySelectStart: " + url) - .assertLogMatch("proxySelectEnd: \\[DIRECT\\]") - .assertLogMatch("dnsStart: " + url.host()) - .assertLogMatch("dnsEnd: \\[.+\\]") - .assertLogMatch("connectStart: " + url.host() + "/.+ DIRECT") - .assertLogMatch("secureConnectStart") - .assertLogMatch( - "connectFailed: null \\S+(?:SSLProtocolException|SSLHandshakeException|TlsFatalAlert): (?:Unexpected handshake message: client_hello|Handshake message sequence violation, 1|Read error|Handshake failed|unexpected_message\\(10\\)).*") - .assertLogMatch( - "callFailed: \\S+(?:SSLProtocolException|SSLHandshakeException|TlsFatalAlert): (?:Unexpected handshake message: client_hello|Handshake message sequence violation, 1|Read error|Handshake failed|unexpected_message\\(10\\)).*") - .assertNoMoreLogs(); - } - - @Test - public void testCacheEvents() { - Request request = new Request.Builder().url(url).build(); - Call call = client.newCall(request); - Response response = new Response.Builder().request(request).code(200).message("").protocol(HTTP_2).build(); - - EventListener listener = loggingEventListenerFactory.create(call); - - listener.cacheConditionalHit(call, response); - listener.cacheHit(call, response); - listener.cacheMiss(call); - listener.satisfactionFailure(call, response); - - logRecorder - .assertLogMatch("cacheConditionalHit: Response\\{protocol=h2, code=200, message=, url=" + url + "\\}") - .assertLogMatch("cacheHit: Response\\{protocol=h2, code=200, message=, url=" + url + "\\}") - .assertLogMatch("cacheMiss") - .assertLogMatch("satisfactionFailure: Response\\{protocol=h2, code=200, message=, url=" + url + "\\}") - .assertNoMoreLogs(); - } - - private Request.Builder request() { - return new Request.Builder().url(url); - } - - private static class LogRecorder extends HttpLoggingInterceptorTest.LogRecorder { - @Override LogRecorder assertLogMatch(String pattern) { - return (LogRecorder) super.assertLogMatch("\\[\\d+ ms] " + pattern); - } - } -} diff --git a/okhttp-logging-interceptor/src/test/java/okhttp3/logging/LoggingEventListenerTest.kt b/okhttp-logging-interceptor/src/test/java/okhttp3/logging/LoggingEventListenerTest.kt new file mode 100644 index 000000000000..2a01101b29da --- /dev/null +++ b/okhttp-logging-interceptor/src/test/java/okhttp3/logging/LoggingEventListenerTest.kt @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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 + * + * 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 okhttp3.logging + +import java.io.IOException +import java.net.UnknownHostException +import java.util.Arrays +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import mockwebserver3.SocketPolicy.FailHandshake +import mockwebserver3.junit5.internal.MockWebServerExtension +import okhttp3.HttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.OkHttpClientTestRule +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import okhttp3.TestUtil.assumeNotWindows +import okhttp3.testing.PlatformRule +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.fail +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.RegisterExtension + +@ExtendWith(MockWebServerExtension::class) +class LoggingEventListenerTest { + @RegisterExtension + val platform = PlatformRule() + + @RegisterExtension + val clientTestRule = OkHttpClientTestRule() + private lateinit var server: MockWebServer + private val handshakeCertificates = platform.localhostHandshakeCertificates() + private val logRecorder = HttpLoggingInterceptorTest.LogRecorder( + prefix = Regex("""\[\d+ ms] """) + ) + private val loggingEventListenerFactory = LoggingEventListener.Factory(logRecorder) + private lateinit var client: OkHttpClient + private lateinit var url: HttpUrl + + @BeforeEach + fun setUp(server: MockWebServer) { + this.server = server + client = clientTestRule.newClientBuilder() + .eventListenerFactory(loggingEventListenerFactory) + .sslSocketFactory( + handshakeCertificates.sslSocketFactory(), + handshakeCertificates.trustManager + ) + .retryOnConnectionFailure(false) + .build() + url = server.url("/") + } + + @Test + fun get() { + assumeNotWindows() + server.enqueue( + MockResponse.Builder() + .body("Hello!") + .setHeader("Content-Type", PLAIN) + .build() + ) + val response = client.newCall(request().build()).execute() + assertThat(response.body).isNotNull() + response.body.bytes() + logRecorder + .assertLogMatch(Regex("""callStart: Request\{method=GET, url=$url}""")) + .assertLogMatch(Regex("""proxySelectStart: $url""")) + .assertLogMatch(Regex("""proxySelectEnd: \[DIRECT]""")) + .assertLogMatch(Regex("""dnsStart: ${url.host}""")) + .assertLogMatch(Regex("""dnsEnd: \[.+]""")) + .assertLogMatch(Regex("""connectStart: ${url.host}/.+ DIRECT""")) + .assertLogMatch(Regex("""connectEnd: http/1.1""")) + .assertLogMatch(Regex("""connectionAcquired: Connection\{${url.host}:\d+, proxy=DIRECT hostAddress=${url.host}/.+ cipherSuite=none protocol=http/1\.1}""")) + .assertLogMatch(Regex("""requestHeadersStart""")) + .assertLogMatch(Regex("""requestHeadersEnd""")) + .assertLogMatch(Regex("""responseHeadersStart""")) + .assertLogMatch(Regex("""responseHeadersEnd: Response\{protocol=http/1\.1, code=200, message=OK, url=$url}""")) + .assertLogMatch(Regex("""responseBodyStart""")) + .assertLogMatch(Regex("""responseBodyEnd: byteCount=6""")) + .assertLogMatch(Regex("""connectionReleased""")) + .assertLogMatch(Regex("""callEnd""")) + .assertNoMoreLogs() + } + + @Test + fun post() { + assumeNotWindows() + server.enqueue(MockResponse()) + client.newCall(request().post("Hello!".toRequestBody(PLAIN)).build()).execute() + logRecorder + .assertLogMatch(Regex("""callStart: Request\{method=POST, url=$url}""")) + .assertLogMatch(Regex("""proxySelectStart: $url""")) + .assertLogMatch(Regex("""proxySelectEnd: \[DIRECT]""")) + .assertLogMatch(Regex("""dnsStart: ${url.host}""")) + .assertLogMatch(Regex("""dnsEnd: \[.+]""")) + .assertLogMatch(Regex("""connectStart: ${url.host}/.+ DIRECT""")) + .assertLogMatch(Regex("""connectEnd: http/1.1""")) + .assertLogMatch(Regex("""connectionAcquired: Connection\{${url.host}:\d+, proxy=DIRECT hostAddress=${url.host}/.+ cipherSuite=none protocol=http/1\.1}""")) + .assertLogMatch(Regex("""requestHeadersStart""")) + .assertLogMatch(Regex("""requestHeadersEnd""")) + .assertLogMatch(Regex("""requestBodyStart""")) + .assertLogMatch(Regex("""requestBodyEnd: byteCount=6""")) + .assertLogMatch(Regex("""responseHeadersStart""")) + .assertLogMatch(Regex("""responseHeadersEnd: Response\{protocol=http/1\.1, code=200, message=OK, url=$url}""")) + .assertLogMatch(Regex("""responseBodyStart""")) + .assertLogMatch(Regex("""responseBodyEnd: byteCount=0""")) + .assertLogMatch(Regex("""connectionReleased""")) + .assertLogMatch(Regex("""callEnd""")) + .assertNoMoreLogs() + } + + @Test + fun secureGet() { + assumeNotWindows() + server.useHttps(handshakeCertificates.sslSocketFactory()) + url = server.url("/") + server.enqueue(MockResponse()) + val response = client.newCall(request().build()).execute() + assertThat(response.body).isNotNull() + response.body.bytes() + platform.assumeHttp2Support() + logRecorder + .assertLogMatch(Regex("""callStart: Request\{method=GET, url=$url}""")) + .assertLogMatch(Regex("""proxySelectStart: $url""")) + .assertLogMatch(Regex("""proxySelectEnd: \[DIRECT]""")) + .assertLogMatch(Regex("""dnsStart: ${url.host}""")) + .assertLogMatch(Regex("""dnsEnd: \[.+]""")) + .assertLogMatch(Regex("""connectStart: ${url.host}/.+ DIRECT""")) + .assertLogMatch(Regex("""secureConnectStart""")) + .assertLogMatch(Regex("""secureConnectEnd: Handshake\{tlsVersion=TLS_1_[23] cipherSuite=TLS_.* peerCertificates=\[CN=localhost] localCertificates=\[]}""")) + .assertLogMatch(Regex("""connectEnd: h2""")) + .assertLogMatch(Regex("""connectionAcquired: Connection\{${url.host}:\d+, proxy=DIRECT hostAddress=${url.host}/.+ cipherSuite=.+ protocol=h2}""")) + .assertLogMatch(Regex("""requestHeadersStart""")) + .assertLogMatch(Regex("""requestHeadersEnd""")) + .assertLogMatch(Regex("""responseHeadersStart""")) + .assertLogMatch(Regex("""responseHeadersEnd: Response\{protocol=h2, code=200, message=, url=$url}""")) + .assertLogMatch(Regex("""responseBodyStart""")) + .assertLogMatch(Regex("""responseBodyEnd: byteCount=0""")) + .assertLogMatch(Regex("""connectionReleased""")) + .assertLogMatch(Regex("""callEnd""")) + .assertNoMoreLogs() + } + + @Test + fun dnsFail() { + client = OkHttpClient.Builder() + .dns { _ -> throw UnknownHostException("reason") } + .eventListenerFactory(loggingEventListenerFactory) + .build() + try { + client.newCall(request().build()).execute() + fail() + } catch (expected: UnknownHostException) { + } + logRecorder + .assertLogMatch(Regex("""callStart: Request\{method=GET, url=$url}""")) + .assertLogMatch(Regex("""proxySelectStart: $url""")) + .assertLogMatch(Regex("""proxySelectEnd: \[DIRECT]""")) + .assertLogMatch(Regex("""dnsStart: ${url.host}""")) + .assertLogMatch(Regex("""callFailed: java.net.UnknownHostException: reason""")) + .assertNoMoreLogs() + } + + @Test + fun connectFail() { + assumeNotWindows() + server.useHttps(handshakeCertificates.sslSocketFactory()) + server.protocols = Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1) + server.enqueue( + MockResponse.Builder() + .socketPolicy(FailHandshake) + .build() + ) + url = server.url("/") + try { + client.newCall(request().build()).execute() + fail() + } catch (expected: IOException) { + } + logRecorder + .assertLogMatch(Regex("""callStart: Request\{method=GET, url=$url}""")) + .assertLogMatch(Regex("""proxySelectStart: $url""")) + .assertLogMatch(Regex("""proxySelectEnd: \[DIRECT]""")) + .assertLogMatch(Regex("""dnsStart: ${url.host}""")) + .assertLogMatch(Regex("""dnsEnd: \[.+]""")) + .assertLogMatch(Regex("""connectStart: ${url.host}/.+ DIRECT""")) + .assertLogMatch(Regex("""secureConnectStart""")) + .assertLogMatch(Regex("""connectFailed: null \S+(?:SSLProtocolException|SSLHandshakeException|TlsFatalAlert): (?:Unexpected handshake message: client_hello|Handshake message sequence violation, 1|Read error|Handshake failed|unexpected_message\(10\)).*""")) + .assertLogMatch(Regex("""callFailed: \S+(?:SSLProtocolException|SSLHandshakeException|TlsFatalAlert): (?:Unexpected handshake message: client_hello|Handshake message sequence violation, 1|Read error|Handshake failed|unexpected_message\(10\)).*""")) + .assertNoMoreLogs() + } + + @Test + fun testCacheEvents() { + val request = Request.Builder().url(url).build() + val call = client.newCall(request) + val response = + Response.Builder().request(request).code(200).message("").protocol(Protocol.HTTP_2) + .build() + val listener = loggingEventListenerFactory.create(call) + listener.cacheConditionalHit(call, response) + listener.cacheHit(call, response) + listener.cacheMiss(call) + listener.satisfactionFailure(call, response) + logRecorder + .assertLogMatch(Regex("""cacheConditionalHit: Response\{protocol=h2, code=200, message=, url=$url}""")) + .assertLogMatch(Regex("""cacheHit: Response\{protocol=h2, code=200, message=, url=$url}""")) + .assertLogMatch(Regex("""cacheMiss""")) + .assertLogMatch(Regex("""satisfactionFailure: Response\{protocol=h2, code=200, message=, url=$url}""")) + .assertNoMoreLogs() + } + + private fun request(): Request.Builder { + return Request.Builder().url(url) + } + + companion object { + private val PLAIN = "text/plain".toMediaType() + } +} diff --git a/okhttp-sse/src/test/java/okhttp3/sse/internal/Event.java b/okhttp-sse/src/test/java/okhttp3/sse/internal/Event.java deleted file mode 100644 index ac202dc95e47..000000000000 --- a/okhttp-sse/src/test/java/okhttp3/sse/internal/Event.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2018 Square, Inc. - * - * 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 - * - * 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 okhttp3.sse.internal; - -import java.util.Objects; -import javax.annotation.Nullable; - -final class Event { - final @Nullable String id; - final @Nullable String type; - final String data; - - Event(@Nullable String id, @Nullable String type, String data) { - if (data == null) throw new NullPointerException("data == null"); - this.id = id; - this.type = type; - this.data = data; - } - - @Override public String toString() { - return "Event{id='" + id + "', type='" + type + "', data='" + data + "'}"; - } - - @Override public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Event)) return false; - Event other = (Event) o; - return Objects.equals(id, other.id) - && Objects.equals(type, other.type) - && data.equals(other.data); - } - - @Override public int hashCode() { - int result = Objects.hashCode(id); - result = 31 * result + Objects.hashCode(type); - result = 31 * result + data.hashCode(); - return result; - } -} diff --git a/okhttp-brotli/src/test/java/okhttp3/brotli/BrotliInterceptorJavaApiTest.java b/okhttp-sse/src/test/java/okhttp3/sse/internal/Event.kt similarity index 66% rename from okhttp-brotli/src/test/java/okhttp3/brotli/BrotliInterceptorJavaApiTest.java rename to okhttp-sse/src/test/java/okhttp3/sse/internal/Event.kt index 63df99629e48..2d1aa6e1a3aa 100644 --- a/okhttp-brotli/src/test/java/okhttp3/brotli/BrotliInterceptorJavaApiTest.java +++ b/okhttp-sse/src/test/java/okhttp3/sse/internal/Event.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Square, Inc. + * Copyright (C) 2018 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,15 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package okhttp3.brotli; +package okhttp3.sse.internal -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -public class BrotliInterceptorJavaApiTest { - @Test - @Disabled("api only") - public void testApi() { - BrotliInterceptor.INSTANCE.intercept(null); - } -} +internal data class Event( + val id: String?, + val type: String?, + val data: String, +) diff --git a/okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourceHttpTest.java b/okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourceHttpTest.java deleted file mode 100644 index 418a595dfeae..000000000000 --- a/okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourceHttpTest.java +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright (C) 2018 Square, Inc. - * - * 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 - * - * 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 okhttp3.sse.internal; - -import java.io.IOException; -import java.util.concurrent.TimeUnit; -import javax.annotation.Nullable; -import mockwebserver3.MockResponse; -import mockwebserver3.MockWebServer; -import mockwebserver3.junit5.internal.MockWebServerExtension; -import okhttp3.OkHttpClient; -import okhttp3.OkHttpClientTestRule; -import okhttp3.RecordingEventListener; -import okhttp3.Request; -import okhttp3.sse.EventSource; -import okhttp3.sse.EventSources; -import okhttp3.testing.PlatformRule; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.junitpioneer.jupiter.RetryingTest; -import static org.assertj.core.api.Assertions.assertThat; - -@Tag("Slowish") -@ExtendWith(MockWebServerExtension.class) -public final class EventSourceHttpTest { - @RegisterExtension public final PlatformRule platform = new PlatformRule(); - - private MockWebServer server; - @RegisterExtension public final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule(); - - private final RecordingEventListener eventListener = new RecordingEventListener(); - - private final EventSourceRecorder listener = new EventSourceRecorder(); - private OkHttpClient client = clientTestRule.newClientBuilder() - .eventListenerFactory(clientTestRule.wrap(eventListener)) - .build(); - - @BeforeEach public void before(MockWebServer server) { - this.server = server; - } - - @AfterEach public void after() { - listener.assertExhausted(); - } - - @Test public void event() { - server.enqueue(new MockResponse.Builder() - .body("" - + "data: hey\n" - + "\n").setHeader("content-type", "text/event-stream") - .build()); - - EventSource source = newEventSource(); - - assertThat(source.request().url().encodedPath()).isEqualTo("/"); - - listener.assertOpen(); - listener.assertEvent(null, null, "hey"); - listener.assertClose(); - } - - @RetryingTest(5) - public void cancelInEventShortCircuits() throws IOException { - server.enqueue(new MockResponse.Builder() - .body("" - + "data: hey\n" - + "\n").setHeader("content-type", "text/event-stream") - .build()); - listener.enqueueCancel(); // Will cancel in onOpen(). - - newEventSource(); - listener.assertOpen(); - listener.assertFailure("canceled"); - } - - @Test public void badContentType() { - server.enqueue(new MockResponse.Builder() - .body("" - + "data: hey\n" - + "\n").setHeader("content-type", "text/plain") - .build()); - - newEventSource(); - listener.assertFailure("Invalid content-type: text/plain"); - } - - @Test public void badResponseCode() { - server.enqueue(new MockResponse.Builder() - .body("" - + "data: hey\n" - + "\n") - .setHeader("content-type", "text/event-stream") - .code(401) - .build()); - - newEventSource(); - listener.assertFailure(null); - } - - @Test public void fullCallTimeoutDoesNotApplyOnceConnected() throws Exception { - client = client.newBuilder() - .callTimeout(250, TimeUnit.MILLISECONDS) - .build(); - - server.enqueue(new MockResponse.Builder() - .bodyDelay(500, TimeUnit.MILLISECONDS) - .setHeader("content-type", "text/event-stream") - .body("data: hey\n\n") - .build()); - - EventSource source = newEventSource(); - - assertThat(source.request().url().encodedPath()).isEqualTo("/"); - - listener.assertOpen(); - listener.assertEvent(null, null, "hey"); - listener.assertClose(); - } - - @Test public void fullCallTimeoutAppliesToSetup() throws Exception { - client = client.newBuilder() - .callTimeout(250, TimeUnit.MILLISECONDS) - .build(); - - server.enqueue(new MockResponse.Builder() - .headersDelay(500, TimeUnit.MILLISECONDS) - .setHeader("content-type", "text/event-stream") - .body("data: hey\n\n") - .build()); - - newEventSource(); - listener.assertFailure("timeout"); - } - - @Test public void retainsAccept() throws InterruptedException { - server.enqueue(new MockResponse.Builder() - .body("" - + "data: hey\n" - + "\n").setHeader("content-type", "text/event-stream") - .build()); - - EventSource source = newEventSource("text/plain"); - - listener.assertOpen(); - listener.assertEvent(null, null, "hey"); - listener.assertClose(); - - assertThat(server.takeRequest().getHeaders().get("Accept")).isEqualTo("text/plain"); - } - - @Test public void setsMissingAccept() throws InterruptedException { - server.enqueue(new MockResponse.Builder() - .body("" - + "data: hey\n" - + "\n").setHeader("content-type", "text/event-stream") - .build()); - - EventSource source = newEventSource(); - - listener.assertOpen(); - listener.assertEvent(null, null, "hey"); - listener.assertClose(); - - assertThat(server.takeRequest().getHeaders().get("Accept")).isEqualTo("text/event-stream"); - } - - @Test public void eventListenerEvents() { - server.enqueue(new MockResponse.Builder() - .body("" - + "data: hey\n" - + "\n").setHeader("content-type", "text/event-stream") - .build()); - - EventSource source = newEventSource(); - - assertThat(source.request().url().encodedPath()).isEqualTo("/"); - - listener.assertOpen(); - listener.assertEvent(null, null, "hey"); - listener.assertClose(); - - assertThat(eventListener.recordedEventTypes()).containsExactly( - "CallStart", - "ProxySelectStart", - "ProxySelectEnd", - "DnsStart", - "DnsEnd", - "ConnectStart", - "ConnectEnd", - "ConnectionAcquired", - "RequestHeadersStart", - "RequestHeadersEnd", - "ResponseHeadersStart", - "ResponseHeadersEnd", - "ResponseBodyStart", - "ResponseBodyEnd", - "ConnectionReleased", - "CallEnd" - ); - } - - private EventSource newEventSource() { - return newEventSource(null); - } - - private EventSource newEventSource(@Nullable String accept) { - Request.Builder builder = new Request.Builder() - .url(server.url("/")); - - if (accept != null) { - builder.header("Accept", accept); - } - - Request request = builder - .build(); - EventSource.Factory factory = EventSources.createFactory(client); - return factory.newEventSource(request, listener); - } -} diff --git a/okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourceHttpTest.kt b/okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourceHttpTest.kt new file mode 100644 index 000000000000..b722a922091f --- /dev/null +++ b/okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourceHttpTest.kt @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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 + * + * 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 okhttp3.sse.internal + +import java.util.concurrent.TimeUnit +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import mockwebserver3.junit5.internal.MockWebServerExtension +import okhttp3.OkHttpClientTestRule +import okhttp3.RecordingEventListener +import okhttp3.Request +import okhttp3.sse.EventSource +import okhttp3.sse.EventSources.createFactory +import okhttp3.testing.PlatformRule +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.RegisterExtension +import org.junitpioneer.jupiter.RetryingTest + +@Tag("Slowish") +@ExtendWith(MockWebServerExtension::class) +class EventSourceHttpTest { + @RegisterExtension + val platform = PlatformRule() + private lateinit var server: MockWebServer + + @RegisterExtension + val clientTestRule = OkHttpClientTestRule() + private val eventListener = RecordingEventListener() + private val listener = EventSourceRecorder() + private var client = clientTestRule.newClientBuilder() + .eventListenerFactory(clientTestRule.wrap(eventListener)) + .build() + + @BeforeEach + fun before(server: MockWebServer) { + this.server = server + } + + @AfterEach + fun after() { + listener.assertExhausted() + } + + @Test + fun event() { + server.enqueue( + MockResponse.Builder() + .body( + """ + |data: hey + | + | + """.trimMargin() + ).setHeader("content-type", "text/event-stream") + .build() + ) + val source = newEventSource() + assertThat(source.request().url.encodedPath).isEqualTo("/") + listener.assertOpen() + listener.assertEvent(null, null, "hey") + listener.assertClose() + } + + @RetryingTest(5) + fun cancelInEventShortCircuits() { + server.enqueue( + MockResponse.Builder() + .body( + """ + |data: hey + | + | + """.trimMargin() + ).setHeader("content-type", "text/event-stream") + .build() + ) + listener.enqueueCancel() // Will cancel in onOpen(). + newEventSource() + listener.assertOpen() + listener.assertFailure("canceled") + } + + @Test + fun badContentType() { + server.enqueue( + MockResponse.Builder() + .body( + """ + |data: hey + | + | + """.trimMargin() + ).setHeader("content-type", "text/plain") + .build() + ) + newEventSource() + listener.assertFailure("Invalid content-type: text/plain") + } + + @Test + fun badResponseCode() { + server.enqueue( + MockResponse.Builder() + .body( + """ + |data: hey + | + | + """.trimMargin() + ) + .setHeader("content-type", "text/event-stream") + .code(401) + .build() + ) + newEventSource() + listener.assertFailure(null) + } + + @Test + fun fullCallTimeoutDoesNotApplyOnceConnected() { + client = client.newBuilder() + .callTimeout(250, TimeUnit.MILLISECONDS) + .build() + server.enqueue( + MockResponse.Builder() + .bodyDelay(500, TimeUnit.MILLISECONDS) + .setHeader("content-type", "text/event-stream") + .body("data: hey\n\n") + .build() + ) + val source = newEventSource() + assertThat(source.request().url.encodedPath).isEqualTo("/") + listener.assertOpen() + listener.assertEvent(null, null, "hey") + listener.assertClose() + } + + @Test + fun fullCallTimeoutAppliesToSetup() { + client = client.newBuilder() + .callTimeout(250, TimeUnit.MILLISECONDS) + .build() + server.enqueue( + MockResponse.Builder() + .headersDelay(500, TimeUnit.MILLISECONDS) + .setHeader("content-type", "text/event-stream") + .body("data: hey\n\n") + .build() + ) + newEventSource() + listener.assertFailure("timeout") + } + + @Test + fun retainsAccept() { + server.enqueue( + MockResponse.Builder() + .body( + """ + |data: hey + | + | + """.trimMargin() + ) + .setHeader("content-type", "text/event-stream") + .build() + ) + newEventSource("text/plain") + listener.assertOpen() + listener.assertEvent(null, null, "hey") + listener.assertClose() + assertThat(server.takeRequest().headers["Accept"]).isEqualTo("text/plain") + } + + @Test + fun setsMissingAccept() { + server.enqueue( + MockResponse.Builder() + .body( + """ + |data: hey + | + | + """.trimMargin() + ).setHeader("content-type", "text/event-stream") + .build() + ) + newEventSource() + listener.assertOpen() + listener.assertEvent(null, null, "hey") + listener.assertClose() + assertThat(server.takeRequest().headers["Accept"]) + .isEqualTo("text/event-stream") + } + + @Test + fun eventListenerEvents() { + server.enqueue( + MockResponse.Builder() + .body( + """ + |data: hey + | + | + """.trimMargin() + ).setHeader("content-type", "text/event-stream") + .build() + ) + val source = newEventSource() + assertThat(source.request().url.encodedPath).isEqualTo("/") + listener.assertOpen() + listener.assertEvent(null, null, "hey") + listener.assertClose() + assertThat(eventListener.recordedEventTypes()).containsExactly( + "CallStart", + "ProxySelectStart", + "ProxySelectEnd", + "DnsStart", + "DnsEnd", + "ConnectStart", + "ConnectEnd", + "ConnectionAcquired", + "RequestHeadersStart", + "RequestHeadersEnd", + "ResponseHeadersStart", + "ResponseHeadersEnd", + "ResponseBodyStart", + "ResponseBodyEnd", + "ConnectionReleased", + "CallEnd" + ) + } + + private fun newEventSource(accept: String? = null): EventSource { + val builder = Request.Builder() + .url(server.url("/")) + if (accept != null) { + builder.header("Accept", accept) + } + val request = builder.build() + val factory = createFactory(client) + return factory.newEventSource(request, listener) + } +} diff --git a/okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourceRecorder.java b/okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourceRecorder.java deleted file mode 100644 index 8de120169294..000000000000 --- a/okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourceRecorder.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (C) 2018 Square, Inc. - * - * 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 - * - * 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 okhttp3.sse.internal; - -import java.io.IOException; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingDeque; -import javax.annotation.Nullable; -import okhttp3.Response; -import okhttp3.internal.platform.Platform; -import okhttp3.sse.EventSource; -import okhttp3.sse.EventSourceListener; - -import static java.util.concurrent.TimeUnit.SECONDS; -import static org.assertj.core.api.Assertions.assertThat; - -public final class EventSourceRecorder extends EventSourceListener { - private final BlockingQueue events = new LinkedBlockingDeque<>(); - private boolean cancel = false; - - public void enqueueCancel() { - cancel = true; - } - - @Override public void onOpen(EventSource eventSource, Response response) { - Platform.get().log("[ES] onOpen", Platform.INFO, null); - events.add(new Open(eventSource, response)); - drainCancelQueue(eventSource); - } - - @Override public void onEvent(EventSource eventSource, @Nullable String id, @Nullable String type, - String data) { - Platform.get().log("[ES] onEvent", Platform.INFO, null); - events.add(new Event(id, type, data)); - drainCancelQueue(eventSource); - } - - @Override public void onClosed(EventSource eventSource) { - Platform.get().log("[ES] onClosed", Platform.INFO, null); - events.add(new Closed()); - drainCancelQueue(eventSource); - } - - @Override - public void onFailure(EventSource eventSource, @Nullable Throwable t, @Nullable Response response) { - Platform.get().log("[ES] onFailure", Platform.INFO, t); - events.add(new Failure(t, response)); - drainCancelQueue(eventSource); - } - - private void drainCancelQueue(EventSource eventSource) { - if (cancel) { - cancel = false; - eventSource.cancel(); - } - } - - private Object nextEvent() { - try { - Object event = events.poll(10, SECONDS); - if (event == null) { - throw new AssertionError("Timed out waiting for event."); - } - return event; - } catch (InterruptedException e) { - throw new AssertionError(e); - } - } - - public void assertExhausted() { - assertThat(events).isEmpty(); - } - - public void assertEvent(@Nullable String id, @Nullable String type, String data) { - Object actual = nextEvent(); - assertThat(actual).isEqualTo(new Event(id, type, data)); - } - - public EventSource assertOpen() { - Object event = nextEvent(); - if (!(event instanceof Open)) { - throw new AssertionError("Expected Open but was " + event); - } - return ((Open) event).eventSource; - } - - public void assertClose() { - Object event = nextEvent(); - if (!(event instanceof Closed)) { - throw new AssertionError("Expected Open but was " + event); - } - } - - public void assertFailure(@Nullable String message) { - Object event = nextEvent(); - if (!(event instanceof Failure)) { - throw new AssertionError("Expected Failure but was " + event); - } - if (message != null) { - assertThat(((Failure) event).t.getMessage()).isEqualTo(message); - } else { - assertThat(((Failure) event).t).isNull(); - } - } - - static final class Open { - final EventSource eventSource; - final Response response; - - Open(EventSource eventSource, Response response) { - this.eventSource = eventSource; - this.response = response; - } - - @Override public String toString() { - return "Open[" + response + ']'; - } - } - - static final class Failure { - final Throwable t; - final Response response; - final String responseBody; - - Failure(Throwable t, Response response) { - this.t = t; - this.response = response; - String responseBody = null; - if (response != null) { - try { - responseBody = response.body().string(); - } catch (IOException ignored) { - } catch (IllegalStateException ise) { - // Body was stripped (UnreadableResponseBody) - } - } - this.responseBody = responseBody; - } - - @Override public String toString() { - if (response == null) { - return "Failure[" + t + "]"; - } - return "Failure[" + response + "]"; - } - } - - static final class Closed { - @Override public String toString() { - return "Closed[]"; - } - } -} diff --git a/okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourceRecorder.kt b/okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourceRecorder.kt new file mode 100644 index 000000000000..d6662f0d9f38 --- /dev/null +++ b/okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourceRecorder.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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 + * + * 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 okhttp3.sse.internal + +import java.util.concurrent.LinkedBlockingDeque +import java.util.concurrent.TimeUnit +import okhttp3.Response +import okhttp3.internal.platform.Platform +import okhttp3.internal.platform.Platform.Companion.get +import okhttp3.sse.EventSource +import okhttp3.sse.EventSourceListener +import org.assertj.core.api.Assertions.assertThat + +class EventSourceRecorder : EventSourceListener() { + private val events = LinkedBlockingDeque() + private var cancel = false + + fun enqueueCancel() { + cancel = true + } + + override fun onOpen(eventSource: EventSource, response: Response) { + get().log("[ES] onOpen", Platform.INFO, null) + events.add(Open(eventSource, response)) + drainCancelQueue(eventSource) + } + + override fun onEvent( + eventSource: EventSource, + id: String?, + type: String?, + data: String, + ) { + get().log("[ES] onEvent", Platform.INFO, null) + events.add(Event(id, type, data)) + drainCancelQueue(eventSource) + } + + override fun onClosed( + eventSource: EventSource, + ) { + get().log("[ES] onClosed", Platform.INFO, null) + events.add(Closed) + drainCancelQueue(eventSource) + } + + override fun onFailure( + eventSource: EventSource, + t: Throwable?, + response: Response?, + ) { + get().log("[ES] onFailure", Platform.INFO, t) + events.add(Failure(t, response)) + drainCancelQueue(eventSource) + } + + private fun drainCancelQueue( + eventSource: EventSource, + ) { + if (cancel) { + cancel = false + eventSource.cancel() + } + } + + private fun nextEvent(): Any { + return events.poll(10, TimeUnit.SECONDS) + ?: throw AssertionError("Timed out waiting for event.") + } + + fun assertExhausted() { + assertThat(events).isEmpty() + } + + fun assertEvent( + id: String?, + type: String?, + data: String, + ) { + assertThat(nextEvent()).isEqualTo(Event(id, type, data)) + } + + fun assertOpen(): EventSource { + val event = nextEvent() as Open + return event.eventSource + } + + fun assertClose() { + nextEvent() as Closed + } + + fun assertFailure(message: String?) { + val event = nextEvent() as Failure + if (message != null) { + assertThat(event.t!!.message).isEqualTo(message) + } else { + assertThat(event.t).isNull() + } + } + + internal data class Open( + val eventSource: EventSource, + val response: Response, + ) + + internal data class Failure( + val t: Throwable?, + val response: Response?, + ) + + internal object Closed +} diff --git a/okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourcesHttpTest.java b/okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourcesHttpTest.java deleted file mode 100644 index 9383e2b7aaf7..000000000000 --- a/okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourcesHttpTest.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2018 Square, Inc. - * - * 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 - * - * 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 okhttp3.sse.internal; - -import java.io.IOException; -import mockwebserver3.MockResponse; -import mockwebserver3.MockWebServer; -import mockwebserver3.junit5.internal.MockWebServerExtension; -import okhttp3.OkHttpClient; -import okhttp3.OkHttpClientTestRule; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.sse.EventSources; -import okhttp3.testing.PlatformRule; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.RegisterExtension; - -@Tag("Slowish") -@ExtendWith(MockWebServerExtension.class) -public final class EventSourcesHttpTest { - @RegisterExtension public final PlatformRule platform = new PlatformRule(); - - private MockWebServer server; - @RegisterExtension public final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule(); - - private final EventSourceRecorder listener = new EventSourceRecorder(); - private OkHttpClient client = clientTestRule.newClient(); - - @BeforeEach public void before(MockWebServer server) { - this.server = server; - } - - @AfterEach public void after() { - listener.assertExhausted(); - } - - @Test public void processResponse() throws IOException { - server.enqueue(new MockResponse.Builder() - .body("" - + "data: hey\n" - + "\n").setHeader("content-type", "text/event-stream") - .build()); - - Request request = new Request.Builder() - .url(server.url("/")) - .build(); - Response response = client.newCall(request).execute(); - EventSources.processResponse(response, listener); - - listener.assertOpen(); - listener.assertEvent(null, null, "hey"); - listener.assertClose(); - } - - @Test public void cancelShortCircuits() throws IOException { - server.enqueue(new MockResponse.Builder() - .body("" - + "data: hey\n" - + "\n").setHeader("content-type", "text/event-stream") - .build()); - listener.enqueueCancel(); // Will cancel in onOpen(). - - Request request = new Request.Builder() - .url(server.url("/")) - .build(); - Response response = client.newCall(request).execute(); - EventSources.processResponse(response, listener); - - listener.assertOpen(); - listener.assertFailure("canceled"); - } -} diff --git a/okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourcesHttpTest.kt b/okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourcesHttpTest.kt new file mode 100644 index 000000000000..2628d410be70 --- /dev/null +++ b/okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourcesHttpTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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 + * + * 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 okhttp3.sse.internal + +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import mockwebserver3.junit5.internal.MockWebServerExtension +import okhttp3.OkHttpClientTestRule +import okhttp3.Request +import okhttp3.sse.EventSources.processResponse +import okhttp3.testing.PlatformRule +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.RegisterExtension + +@Tag("Slowish") +@ExtendWith(MockWebServerExtension::class) +class EventSourcesHttpTest { + @RegisterExtension + val platform = PlatformRule() + private lateinit var server: MockWebServer + + @RegisterExtension + val clientTestRule = OkHttpClientTestRule() + + private val listener = EventSourceRecorder() + private val client = clientTestRule.newClient() + + @BeforeEach + fun before(server: MockWebServer) { + this.server = server + } + + @AfterEach + fun after() { + listener.assertExhausted() + } + + @Test + fun processResponse() { + server.enqueue( + MockResponse.Builder() + .body( + """ + |data: hey + | + | + """.trimMargin() + ).setHeader("content-type", "text/event-stream") + .build() + ) + val request = Request.Builder() + .url(server.url("/")) + .build() + val response = client.newCall(request).execute() + processResponse(response, listener) + listener.assertOpen() + listener.assertEvent(null, null, "hey") + listener.assertClose() + } + + @Test + fun cancelShortCircuits() { + server.enqueue( + MockResponse.Builder() + .body( + """ + |data: hey + | + | + """.trimMargin() + ).setHeader("content-type", "text/event-stream") + .build() + ) + listener.enqueueCancel() // Will cancel in onOpen(). + val request = Request.Builder() + .url(server.url("/")) + .build() + val response = client.newCall(request).execute() + processResponse(response, listener) + listener.assertOpen() + listener.assertFailure("canceled") + } +} diff --git a/okhttp-sse/src/test/java/okhttp3/sse/internal/ServerSentEventIteratorTest.java b/okhttp-sse/src/test/java/okhttp3/sse/internal/ServerSentEventIteratorTest.java deleted file mode 100644 index bb5e0277a926..000000000000 --- a/okhttp-sse/src/test/java/okhttp3/sse/internal/ServerSentEventIteratorTest.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright (C) 2018 Square, Inc. - * - * 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 - * - * 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 okhttp3.sse.internal; - -import java.io.IOException; -import java.util.ArrayDeque; -import java.util.Deque; -import javax.annotation.Nullable; -import okio.Buffer; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -public final class ServerSentEventIteratorTest { - /** Either {@link Event} or {@link Long} items for events and retry changes, respectively. */ - private final Deque callbacks = new ArrayDeque<>(); - - @AfterEach public void after() { - assertThat(callbacks).isEmpty(); - } - - @Test public void multiline() throws IOException { - consumeEvents("" - + "data: YHOO\n" - + "data: +2\n" - + "data: 10\n" - + "\n"); - assertThat(callbacks.remove()).isEqualTo(new Event(null, null, "YHOO\n+2\n10")); - } - - @Test public void multilineCr() throws IOException { - consumeEvents("" - + "data: YHOO\r" - + "data: +2\r" - + "data: 10\r" - + "\r"); - assertThat(callbacks.remove()).isEqualTo(new Event(null, null, "YHOO\n+2\n10")); - } - - @Test public void multilineCrLf() throws IOException { - consumeEvents("" - + "data: YHOO\r\n" - + "data: +2\r\n" - + "data: 10\r\n" - + "\r\n"); - assertThat(callbacks.remove()).isEqualTo(new Event(null, null, "YHOO\n+2\n10")); - } - - @Test public void eventType() throws IOException { - consumeEvents("" - + "event: add\n" - + "data: 73857293\n" - + "\n" - + "event: remove\n" - + "data: 2153\n" - + "\n" - + "event: add\n" - + "data: 113411\n" - + "\n"); - assertThat(callbacks.remove()).isEqualTo(new Event(null, "add", "73857293")); - assertThat(callbacks.remove()).isEqualTo(new Event(null, "remove", "2153")); - assertThat(callbacks.remove()).isEqualTo(new Event(null, "add", "113411")); - } - - @Test public void commentsIgnored() throws IOException { - consumeEvents("" - + ": test stream\n" - + "\n" - + "data: first event\n" - + "id: 1\n" - + "\n"); - assertThat(callbacks.remove()).isEqualTo(new Event("1", null, "first event")); - } - - @Test public void idCleared() throws IOException { - consumeEvents("" - + "data: first event\n" - + "id: 1\n" - + "\n" - + "data: second event\n" - + "id\n" - + "\n" - + "data: third event\n" - + "\n"); - assertThat(callbacks.remove()).isEqualTo(new Event("1", null, "first event")); - assertThat(callbacks.remove()).isEqualTo(new Event(null, null, "second event")); - assertThat(callbacks.remove()).isEqualTo(new Event(null, null, "third event")); - } - - @Test public void nakedFieldNames() throws IOException { - consumeEvents("" - + "data\n" - + "\n" - + "data\n" - + "data\n" - + "\n" - + "data:\n"); - assertThat(callbacks.remove()).isEqualTo(new Event(null, null, "")); - assertThat(callbacks.remove()).isEqualTo(new Event(null, null, "\n")); - } - - @Test public void colonSpaceOptional() throws IOException { - consumeEvents("" - + "data:test\n" - + "\n" - + "data: test\n" - + "\n"); - assertThat(callbacks.remove()).isEqualTo(new Event(null, null, "test")); - assertThat(callbacks.remove()).isEqualTo(new Event(null, null, "test")); - } - - @Test public void leadingWhitespace() throws IOException { - consumeEvents("" - + "data: test\n" - + "\n"); - assertThat(callbacks.remove()).isEqualTo(new Event(null, null, " test")); - } - - @Test public void idReusedAcrossEvents() throws IOException { - consumeEvents("" - + "data: first event\n" - + "id: 1\n" - + "\n" - + "data: second event\n" - + "\n" - + "id: 2\n" - + "data: third event\n" - + "\n"); - assertThat(callbacks.remove()).isEqualTo(new Event("1", null, "first event")); - assertThat(callbacks.remove()).isEqualTo(new Event("1", null, "second event")); - assertThat(callbacks.remove()).isEqualTo(new Event("2", null, "third event")); - } - - @Test public void idIgnoredFromEmptyEvent() throws IOException { - consumeEvents("" - + "data: first event\n" - + "id: 1\n" - + "\n" - + "id: 2\n" - + "\n" - + "data: second event\n" - + "\n"); - assertThat(callbacks.remove()).isEqualTo(new Event("1", null, "first event")); - assertThat(callbacks.remove()).isEqualTo(new Event("1", null, "second event")); - } - - @Test public void retry() throws IOException { - consumeEvents("" - + "retry: 22\n" - + "\n" - + "data: first event\n" - + "id: 1\n" - + "\n"); - assertThat(callbacks.remove()).isEqualTo(22L); - assertThat(callbacks.remove()).isEqualTo(new Event("1", null, "first event")); - } - - @Test public void retryInvalidFormatIgnored() throws IOException { - consumeEvents("" - + "retry: 22\n" - + "\n" - + "retry: hey" - + "\n"); - assertThat(callbacks.remove()).isEqualTo(22L); - } - - @Test public void namePrefixIgnored() throws IOException { - consumeEvents("" - + "data: a\n" - + "eventually\n" - + "database\n" - + "identity\n" - + "retrying\n" - + "\n"); - assertThat(callbacks.remove()).isEqualTo(new Event(null, null, "a")); - } - - @Test public void nakedNameClearsIdAndTypeAppendsData() throws IOException { - consumeEvents("" - + "id: a\n" - + "event: b\n" - + "data: c\n" - + "id\n" - + "event\n" - + "data\n" - + "\n"); - assertThat(callbacks.remove()).isEqualTo(new Event(null, null, "c\n")); - } - - @Test public void nakedRetryIgnored() throws IOException { - consumeEvents("" - + "retry\n" - + "\n"); - assertThat(callbacks).isEmpty(); - } - - private void consumeEvents(String source) throws IOException { - ServerSentEventReader.Callback callback = new ServerSentEventReader.Callback() { - @Override public void onEvent(@Nullable String id, @Nullable String type, String data) { - callbacks.add(new Event(id, type, data)); - } - @Override public void onRetryChange(long timeMs) { - callbacks.add(timeMs); - } - }; - Buffer buffer = new Buffer().writeUtf8(source); - ServerSentEventReader reader = new ServerSentEventReader(buffer, callback); - while (reader.processNextEvent()); - assertThat(buffer.size()).overridingErrorMessage("Unconsumed buffer: " + buffer.readUtf8()) - .isEqualTo(0); - } -} diff --git a/okhttp-sse/src/test/java/okhttp3/sse/internal/ServerSentEventIteratorTest.kt b/okhttp-sse/src/test/java/okhttp3/sse/internal/ServerSentEventIteratorTest.kt new file mode 100644 index 000000000000..d08d2653dcea --- /dev/null +++ b/okhttp-sse/src/test/java/okhttp3/sse/internal/ServerSentEventIteratorTest.kt @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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 + * + * 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 okhttp3.sse.internal + +import java.util.ArrayDeque +import java.util.Deque +import okio.Buffer +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test + +class ServerSentEventIteratorTest { + /** Either [Event] or [Long] items for events and retry changes, respectively. */ + private val callbacks: Deque = ArrayDeque() + + @AfterEach + fun after() { + assertThat(callbacks).isEmpty() + } + + @Test + fun multiline() { + consumeEvents( + """ + |data: YHOO + |data: +2 + |data: 10 + | + | + """.trimMargin() + ) + assertThat(callbacks.remove()).isEqualTo(Event(null, null, "YHOO\n+2\n10")) + } + + @Test + fun multilineCr() { + consumeEvents( + """ + |data: YHOO + |data: +2 + |data: 10 + | + | + """.trimMargin().replace("\n", "\r") + ) + assertThat(callbacks.remove()).isEqualTo(Event(null, null, "YHOO\n+2\n10")) + } + + @Test + fun multilineCrLf() { + consumeEvents( + """ + |data: YHOO + |data: +2 + |data: 10 + | + | + """.trimMargin().replace("\n", "\r\n") + ) + assertThat(callbacks.remove()).isEqualTo(Event(null, null, "YHOO\n+2\n10")) + } + + @Test + fun eventType() { + consumeEvents( + """ + |event: add + |data: 73857293 + | + |event: remove + |data: 2153 + | + |event: add + |data: 113411 + | + | + """.trimMargin() + ) + assertThat(callbacks.remove()).isEqualTo(Event(null, "add", "73857293")) + assertThat(callbacks.remove()).isEqualTo(Event(null, "remove", "2153")) + assertThat(callbacks.remove()).isEqualTo(Event(null, "add", "113411")) + } + + @Test + fun commentsIgnored() { + consumeEvents( + """ + |: test stream + | + |data: first event + |id: 1 + | + | + """.trimMargin() + ) + assertThat(callbacks.remove()).isEqualTo(Event("1", null, "first event")) + } + + @Test + fun idCleared() { + consumeEvents( + """ + |data: first event + |id: 1 + | + |data: second event + |id + | + |data: third event + | + | + """.trimMargin() + ) + assertThat(callbacks.remove()).isEqualTo(Event("1", null, "first event")) + assertThat(callbacks.remove()).isEqualTo(Event(null, null, "second event")) + assertThat(callbacks.remove()).isEqualTo(Event(null, null, "third event")) + } + + @Test + fun nakedFieldNames() { + consumeEvents( + """ + |data + | + |data + |data + | + |data: + | + """.trimMargin() + ) + assertThat(callbacks.remove()).isEqualTo(Event(null, null, "")) + assertThat(callbacks.remove()).isEqualTo(Event(null, null, "\n")) + } + + @Test + fun colonSpaceOptional() { + consumeEvents( + """ + |data:test + | + |data: test + | + | + """.trimMargin() + ) + assertThat(callbacks.remove()).isEqualTo(Event(null, null, "test")) + assertThat(callbacks.remove()).isEqualTo(Event(null, null, "test")) + } + + @Test + fun leadingWhitespace() { + consumeEvents( + """ + |data: test + | + | + """.trimMargin() + ) + assertThat(callbacks.remove()).isEqualTo(Event(null, null, " test")) + } + + @Test + fun idReusedAcrossEvents() { + consumeEvents( + """ + |data: first event + |id: 1 + | + |data: second event + | + |id: 2 + |data: third event + | + | + """.trimMargin() + ) + assertThat(callbacks.remove()).isEqualTo(Event("1", null, "first event")) + assertThat(callbacks.remove()).isEqualTo(Event("1", null, "second event")) + assertThat(callbacks.remove()).isEqualTo(Event("2", null, "third event")) + } + + @Test + fun idIgnoredFromEmptyEvent() { + consumeEvents( + """ + |data: first event + |id: 1 + | + |id: 2 + | + |data: second event + | + | + """.trimMargin() + ) + assertThat(callbacks.remove()).isEqualTo(Event("1", null, "first event")) + assertThat(callbacks.remove()).isEqualTo(Event("1", null, "second event")) + } + + @Test + fun retry() { + consumeEvents( + """ + |retry: 22 + | + |data: first event + |id: 1 + | + | + """.trimMargin() + ) + assertThat(callbacks.remove()).isEqualTo(22L) + assertThat(callbacks.remove()).isEqualTo(Event("1", null, "first event")) + } + + @Test + fun retryInvalidFormatIgnored() { + consumeEvents( + """ + |retry: 22 + | + |retry: hey + | + """.trimMargin() + ) + assertThat(callbacks.remove()).isEqualTo(22L) + } + + @Test + fun namePrefixIgnored() { + consumeEvents( + """ + |data: a + |eventually + |database + |identity + |retrying + | + | + """.trimMargin() + ) + assertThat(callbacks.remove()).isEqualTo(Event(null, null, "a")) + } + + @Test + fun nakedNameClearsIdAndTypeAppendsData() { + consumeEvents( + """ + |id: a + |event: b + |data: c + |id + |event + |data + | + | + """.trimMargin() + ) + assertThat(callbacks.remove()).isEqualTo(Event(null, null, "c\n")) + } + + @Test + fun nakedRetryIgnored() { + consumeEvents( + """ + |retry + | + """.trimMargin() + ) + assertThat(callbacks).isEmpty() + } + + private fun consumeEvents(source: String) { + val callback: ServerSentEventReader.Callback = object : ServerSentEventReader.Callback { + override fun onEvent(id: String?, type: String?, data: String) { + callbacks.add(Event(id, type, data)) + } + + override fun onRetryChange(timeMs: Long) { + callbacks.add(timeMs) + } + } + val buffer = Buffer().writeUtf8(source) + val reader = ServerSentEventReader(buffer, callback) + while (reader.processNextEvent()) { + } + assertThat(buffer.size) + .overridingErrorMessage("Unconsumed buffer: ${buffer.readUtf8()}") + .isEqualTo(0) + } +} diff --git a/okhttp-tls/src/test/java/okhttp3/tls/HeldCertificateTest.java b/okhttp-tls/src/test/java/okhttp3/tls/HeldCertificateTest.java deleted file mode 100644 index df714a017279..000000000000 --- a/okhttp-tls/src/test/java/okhttp3/tls/HeldCertificateTest.java +++ /dev/null @@ -1,506 +0,0 @@ -/* - * Copyright (C) 2018 Square, Inc. - * - * 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 - * - * 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 okhttp3.tls; - -import java.math.BigInteger; -import java.security.KeyFactory; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.cert.CertificateParsingException; -import java.security.cert.X509Certificate; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; -import java.util.concurrent.TimeUnit; -import okhttp3.testing.PlatformRule; -import okio.ByteString; -import org.bouncycastle.asn1.x509.GeneralName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import static java.util.Arrays.asList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.data.Offset.offset; -import static org.junit.jupiter.api.Assertions.fail; - -public final class HeldCertificateTest { - @RegisterExtension public PlatformRule platform = new PlatformRule(); - - @Test public void defaultCertificate() throws CertificateParsingException { - long now = System.currentTimeMillis(); - HeldCertificate heldCertificate = new HeldCertificate.Builder().build(); - - X509Certificate certificate = heldCertificate.certificate(); - assertThat(certificate.getSubjectX500Principal().getName()).overridingErrorMessage( - "self-signed").isEqualTo(certificate.getIssuerX500Principal().getName()); - assertThat(certificate.getIssuerX500Principal().getName()).matches("CN=[0-9a-f-]{36}"); - assertThat(certificate.getSerialNumber()).isEqualTo(BigInteger.ONE); - assertThat(certificate.getSubjectAlternativeNames()).isNull(); - - double deltaMillis = 1000.0; - long durationMillis = TimeUnit.MINUTES.toMillis(60 * 24); - assertThat((double) certificate.getNotBefore().getTime()).isCloseTo( - (double) now, offset(deltaMillis)); - assertThat((double) certificate.getNotAfter().getTime()).isCloseTo( - (double) now + durationMillis, offset(deltaMillis)); - } - - @Test public void customInterval() { - // 5 seconds starting on 1970-01-01. - HeldCertificate heldCertificate = new HeldCertificate.Builder() - .validityInterval(5_000L, 10_000L) - .build(); - X509Certificate certificate = heldCertificate.certificate(); - assertThat(certificate.getNotBefore().getTime()).isEqualTo(5_000L); - assertThat(certificate.getNotAfter().getTime()).isEqualTo(10_000L); - } - - @Test public void customDuration() { - long now = System.currentTimeMillis(); - - HeldCertificate heldCertificate = new HeldCertificate.Builder() - .duration(5, TimeUnit.SECONDS) - .build(); - X509Certificate certificate = heldCertificate.certificate(); - - double deltaMillis = 1000.0; - long durationMillis = 5_000L; - assertThat((double) certificate.getNotBefore().getTime()).isCloseTo( - (double) now, offset(deltaMillis)); - assertThat((double) certificate.getNotAfter().getTime()).isCloseTo( - (double) now + durationMillis, offset(deltaMillis)); - } - - @Test public void subjectAlternativeNames() throws CertificateParsingException { - HeldCertificate heldCertificate = new HeldCertificate.Builder() - .addSubjectAlternativeName("1.1.1.1") - .addSubjectAlternativeName("cash.app") - .build(); - - X509Certificate certificate = heldCertificate.certificate(); - assertThat(certificate.getSubjectAlternativeNames()).containsExactly( - asList(GeneralName.iPAddress, "1.1.1.1"), - asList(GeneralName.dNSName, "cash.app")); - } - - @Test public void commonName() { - HeldCertificate heldCertificate = new HeldCertificate.Builder() - .commonName("cash.app") - .build(); - - X509Certificate certificate = heldCertificate.certificate(); - assertThat(certificate.getSubjectX500Principal().getName()).isEqualTo("CN=cash.app"); - } - - @Test public void organizationalUnit() { - HeldCertificate heldCertificate = new HeldCertificate.Builder() - .commonName("cash.app") - .organizationalUnit("cash") - .build(); - - X509Certificate certificate = heldCertificate.certificate(); - assertThat(certificate.getSubjectX500Principal().getName()).isEqualTo( - "CN=cash.app,OU=cash"); - } - - /** Confirm golden values of encoded PEMs. */ - @Test public void pems() throws Exception { - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - - ByteString publicKeyBytes = ByteString.decodeBase64("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCApF" - + "HhtrLan28q+oMolZuaTfWBA0V5aMIvq32BsloQu6LlvX1wJ4YEoUCjDlPOtpht7XLbUmBnbIzN89XK4UJVM6Sqp3" - + "K88Km8z7gMrdrfTom/274wL25fICR+yDEQ5fUVYBmJAKXZF1aoI0mIoEx0xFsQhIJ637v2MxJDupd61wIDAQAB"); - PublicKey publicKey = keyFactory.generatePublic( - new X509EncodedKeySpec(publicKeyBytes.toByteArray())); - - ByteString privateKeyBytes = ByteString.decodeBase64("MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbA" - + "gEAAoGBAICkUeG2stqfbyr6gyiVm5pN9YEDRXlowi+rfYGyWhC7ouW9fXAnhgShQKMOU862mG3tcttSYGdsjM3z1" - + "crhQlUzpKqncrzwqbzPuAyt2t9Oib/bvjAvbl8gJH7IMRDl9RVgGYkApdkXVqgjSYigTHTEWxCEgnrfu/YzEkO6l" - + "3rXAgMBAAECgYB99mhnB6piADOuddXv626NzUBTr4xbsYRTgSxHzwf50oFTTBSDuW+1IOBVyTWu94SSPyt0LllPb" - + "C8Di3sQSTnVGpSqAvEXknBMzIc0UO74Rn9p3gZjEenPt1l77fIBa2nK06/rdsJCoE/1P1JSfM9w7LU1RsTmseYML" - + "eJl5F79gQJBAO/BbAKqg1yzK7VijygvBoUrr+rt2lbmKgcUQ/rxu8IIQk0M/xgJqSkXDXuOnboGM7sQSKfJAZUtT" - + "7xozvLzV7ECQQCJW59w7NIM0qZ/gIX2gcNZr1B/V3zcGlolTDciRm+fnKGNt2EEDKnVL3swzbEfTCa48IT0QKgZJ" - + "qpXZERa26UHAkBLXmiP5f5pk8F3wcXzAeVw06z3k1IB41Tu6MX+CyPU+TeudRlz+wV8b0zDvK+EnRKCCbptVFj1B" - + "kt8lQ4JfcnhAkAk2Y3Gz+HySrkcT7Cg12M/NkdUQnZe3jr88pt/+IGNwomc6Wt/mJ4fcWONTkGMcfOZff1NQeNXD" - + "AZ6941XCsIVAkASOg02PlVHLidU7mIE65swMM5/RNhS4aFjez/MwxFNOHaxc9VgCwYPXCLOtdf7AVovdyG0XWgbU" - + "XH+NyxKwboE"); - PrivateKey privateKey = keyFactory.generatePrivate( - new PKCS8EncodedKeySpec(privateKeyBytes.toByteArray())); - - HeldCertificate heldCertificate = new HeldCertificate.Builder() - .keyPair(publicKey, privateKey) - .commonName("cash.app") - .validityInterval(0L, 1_000L) - .rsa2048() - .build(); - - assertThat("" - + "-----BEGIN CERTIFICATE-----\n" - + "MIIBmjCCAQOgAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhjYXNo\n" - + "LmFwcDAeFw03MDAxMDEwMDAwMDBaFw03MDAxMDEwMDAwMDFaMBMxETAPBgNVBAMM\n" - + "CGNhc2guYXBwMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCApFHhtrLan28q\n" - + "+oMolZuaTfWBA0V5aMIvq32BsloQu6LlvX1wJ4YEoUCjDlPOtpht7XLbUmBnbIzN\n" - + "89XK4UJVM6Sqp3K88Km8z7gMrdrfTom/274wL25fICR+yDEQ5fUVYBmJAKXZF1ao\n" - + "I0mIoEx0xFsQhIJ637v2MxJDupd61wIDAQABMA0GCSqGSIb3DQEBCwUAA4GBADHT\n" - + "vcjwl9Z4I5Cb2R1y7aaa860HkY2k3ThaDK5OJt6GYqJTA9P3LtX7VwQtL1TWqXGc\n" - + "+OEfl3zhm0PUqcbckMzhJtqIa7NkDSjNm71BKd843pIhGcEri69DcL/cR8T+eMex\n" - + "hadh7aGM9OjeL8gznLeq27Ly6Dj7Vkp5OmOrSKfn\n" - + "-----END CERTIFICATE-----\n").isEqualTo(heldCertificate.certificatePem()); - - assertThat(("" - + "-----BEGIN RSA PRIVATE KEY-----\n" - + "MIICWwIBAAKBgQCApFHhtrLan28q+oMolZuaTfWBA0V5aMIvq32BsloQu6LlvX1w\n" - + "J4YEoUCjDlPOtpht7XLbUmBnbIzN89XK4UJVM6Sqp3K88Km8z7gMrdrfTom/274w\n" - + "L25fICR+yDEQ5fUVYBmJAKXZF1aoI0mIoEx0xFsQhIJ637v2MxJDupd61wIDAQAB\n" - + "AoGAffZoZweqYgAzrnXV7+tujc1AU6+MW7GEU4EsR88H+dKBU0wUg7lvtSDgVck1\n" - + "rveEkj8rdC5ZT2wvA4t7EEk51RqUqgLxF5JwTMyHNFDu+EZ/ad4GYxHpz7dZe+3y\n" - + "AWtpytOv63bCQqBP9T9SUnzPcOy1NUbE5rHmDC3iZeRe/YECQQDvwWwCqoNcsyu1\n" - + "Yo8oLwaFK6/q7dpW5ioHFEP68bvCCEJNDP8YCakpFw17jp26BjO7EEinyQGVLU+8\n" - + "aM7y81exAkEAiVufcOzSDNKmf4CF9oHDWa9Qf1d83BpaJUw3IkZvn5yhjbdhBAyp\n" - + "1S97MM2xH0wmuPCE9ECoGSaqV2REWtulBwJAS15oj+X+aZPBd8HF8wHlcNOs95NS\n" - + "AeNU7ujF/gsj1Pk3rnUZc/sFfG9Mw7yvhJ0Sggm6bVRY9QZLfJUOCX3J4QJAJNmN\n" - + "xs/h8kq5HE+woNdjPzZHVEJ2Xt46/PKbf/iBjcKJnOlrf5ieH3FjjU5BjHHzmX39\n" - + "TUHjVwwGeveNVwrCFQJAEjoNNj5VRy4nVO5iBOubMDDOf0TYUuGhY3s/zMMRTTh2\n" - + "sXPVYAsGD1wizrXX+wFaL3chtF1oG1Fx/jcsSsG6BA==\n" - + "-----END RSA PRIVATE KEY-----\n")).isEqualTo(heldCertificate.privateKeyPkcs1Pem()); - - assertThat(("" - + "-----BEGIN PRIVATE KEY-----\n" - + "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAICkUeG2stqfbyr6\n" - + "gyiVm5pN9YEDRXlowi+rfYGyWhC7ouW9fXAnhgShQKMOU862mG3tcttSYGdsjM3z\n" - + "1crhQlUzpKqncrzwqbzPuAyt2t9Oib/bvjAvbl8gJH7IMRDl9RVgGYkApdkXVqgj\n" - + "SYigTHTEWxCEgnrfu/YzEkO6l3rXAgMBAAECgYB99mhnB6piADOuddXv626NzUBT\n" - + "r4xbsYRTgSxHzwf50oFTTBSDuW+1IOBVyTWu94SSPyt0LllPbC8Di3sQSTnVGpSq\n" - + "AvEXknBMzIc0UO74Rn9p3gZjEenPt1l77fIBa2nK06/rdsJCoE/1P1JSfM9w7LU1\n" - + "RsTmseYMLeJl5F79gQJBAO/BbAKqg1yzK7VijygvBoUrr+rt2lbmKgcUQ/rxu8II\n" - + "Qk0M/xgJqSkXDXuOnboGM7sQSKfJAZUtT7xozvLzV7ECQQCJW59w7NIM0qZ/gIX2\n" - + "gcNZr1B/V3zcGlolTDciRm+fnKGNt2EEDKnVL3swzbEfTCa48IT0QKgZJqpXZERa\n" - + "26UHAkBLXmiP5f5pk8F3wcXzAeVw06z3k1IB41Tu6MX+CyPU+TeudRlz+wV8b0zD\n" - + "vK+EnRKCCbptVFj1Bkt8lQ4JfcnhAkAk2Y3Gz+HySrkcT7Cg12M/NkdUQnZe3jr8\n" - + "8pt/+IGNwomc6Wt/mJ4fcWONTkGMcfOZff1NQeNXDAZ6941XCsIVAkASOg02PlVH\n" - + "LidU7mIE65swMM5/RNhS4aFjez/MwxFNOHaxc9VgCwYPXCLOtdf7AVovdyG0XWgb\n" - + "UXH+NyxKwboE\n" - + "-----END PRIVATE KEY-----\n")).isEqualTo(heldCertificate.privateKeyPkcs8Pem()); - } - - @Test public void ecdsaSignedByRsa() { - HeldCertificate root = new HeldCertificate.Builder() - .certificateAuthority(0) - .rsa2048() - .build(); - HeldCertificate leaf = new HeldCertificate.Builder() - .certificateAuthority(0) - .ecdsa256() - .signedBy(root) - .build(); - - assertThat(root.certificate().getSigAlgName()).isEqualToIgnoringCase("SHA256WITHRSA"); - assertThat(leaf.certificate().getSigAlgName()).isEqualToIgnoringCase("SHA256WITHRSA"); - } - - @Test public void rsaSignedByEcdsa() { - HeldCertificate root = new HeldCertificate.Builder() - .certificateAuthority(0) - .ecdsa256() - .build(); - HeldCertificate leaf = new HeldCertificate.Builder() - .certificateAuthority(0) - .rsa2048() - .signedBy(root) - .build(); - - assertThat(root.certificate().getSigAlgName()).isEqualToIgnoringCase("SHA256WITHECDSA"); - assertThat(leaf.certificate().getSigAlgName()).isEqualToIgnoringCase("SHA256WITHECDSA"); - } - - @Test public void decodeEcdsa256() throws Exception { - // The certificate + private key below was generated programmatically: - // - // HeldCertificate heldCertificate = new HeldCertificate.Builder() - // .validityInterval(5_000L, 10_000L) - // .addSubjectAlternativeName("1.1.1.1") - // .addSubjectAlternativeName("cash.app") - // .serialNumber(42L) - // .commonName("cash.app") - // .organizationalUnit("engineering") - // .ecdsa256() - // .build(); - - String certificatePem = "" - + "-----BEGIN CERTIFICATE-----\n" - + "MIIBYTCCAQegAwIBAgIBKjAKBggqhkjOPQQDAjApMRQwEgYDVQQLEwtlbmdpbmVl\n" - + "cmluZzERMA8GA1UEAxMIY2FzaC5hcHAwHhcNNzAwMTAxMDAwMDA1WhcNNzAwMTAx\n" - + "MDAwMDEwWjApMRQwEgYDVQQLEwtlbmdpbmVlcmluZzERMA8GA1UEAxMIY2FzaC5h\n" - + "cHAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASda8ChkQXxGELnrV/oBnIAx3dD\n" - + "ocUOJfdz4pOJTP6dVQB9U3UBiW5uSX/MoOD0LL5zG3bVyL3Y6pDwKuYvfLNhoyAw\n" - + "HjAcBgNVHREBAf8EEjAQhwQBAQEBgghjYXNoLmFwcDAKBggqhkjOPQQDAgNIADBF\n" - + "AiAyHHg1N6YDDQiY920+cnI5XSZwEGhAtb9PYWO8bLmkcQIhAI2CfEZf3V/obmdT\n" - + "yyaoEufLKVXhrTQhRfodTeigi4RX\n" - + "-----END CERTIFICATE-----\n"; - String pkcs8Pem = "" - + "-----BEGIN PRIVATE KEY-----\n" - + "MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCA7ODT0xhGSNn4ESj6J\n" - + "lu/GJQZoU9lDrCPeUcQ28tzOWw==\n" - + "-----END PRIVATE KEY-----\n"; - String bcPkcs8Pem = "" - + "-----BEGIN PRIVATE KEY-----\n" - + "ME0CAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEMzAxAgEBBCA7ODT0xhGSNn4ESj6J\n" - + "lu/GJQZoU9lDrCPeUcQ28tzOW6AKBggqhkjOPQMBBw==\n" - + "-----END PRIVATE KEY-----\n"; - - HeldCertificate heldCertificate = HeldCertificate.decode(certificatePem + pkcs8Pem); - assertThat(heldCertificate.certificatePem()).isEqualTo(certificatePem); - - // Slightly different encoding - if (platform.isBouncyCastle()) { - assertThat(heldCertificate.privateKeyPkcs8Pem()).isEqualTo(bcPkcs8Pem); - } else { - assertThat(heldCertificate.privateKeyPkcs8Pem()).isEqualTo(pkcs8Pem); - } - - X509Certificate certificate = heldCertificate.certificate(); - assertThat(certificate.getNotBefore().getTime()).isEqualTo(5_000L); - assertThat(certificate.getNotAfter().getTime()).isEqualTo(10_000L); - - assertThat(certificate.getSubjectAlternativeNames()).containsExactly( - asList(GeneralName.iPAddress, "1.1.1.1"), - asList(GeneralName.dNSName, "cash.app")); - - assertThat(certificate.getSubjectX500Principal().getName()) - .isEqualTo("CN=cash.app,OU=engineering"); - } - - @Test public void decodeRsa512() { - // The certificate + private key below was generated with OpenSSL. Never generate certificates - // with MD5 or 512-bit RSA; that's insecure! - // - // openssl req \ - // -x509 \ - // -md5 \ - // -nodes \ - // -days 1 \ - // -newkey rsa:512 \ - // -keyout privateKey.key \ - // -out certificate.crt - - String certificatePem = "" - + "-----BEGIN CERTIFICATE-----\n" - + "MIIBFzCBwgIJAIVAqagcVN7/MA0GCSqGSIb3DQEBBAUAMBMxETAPBgNVBAMMCGNh\n" - + "c2guYXBwMB4XDTE5MDkwNzAyMjg0NFoXDTE5MDkwODAyMjg0NFowEzERMA8GA1UE\n" - + "AwwIY2FzaC5hcHAwXDANBgkqhkiG9w0BAQEFAANLADBIAkEA8qAeoubm4mBTD9/J\n" - + "ujLQkfk/fuJt/T5pVQ1vUEqxfcMw0zYgszQ5C2MiIl7M6JkTRKU01q9hVFCR83wX\n" - + "zIdrLQIDAQABMA0GCSqGSIb3DQEBBAUAA0EAO1UpwhrkW3Ho1nZK/taoUQOoqz/n\n" - + "HFVMtyEkm5gBDgz8nJXwb3zbegclQyH+kVou02S8zC5WWzEtd0R8S0LsTA==\n" - + "-----END CERTIFICATE-----\n"; - String pkcs8Pem = "" - + "-----BEGIN PRIVATE KEY-----\n" - + "MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA8qAeoubm4mBTD9/J\n" - + "ujLQkfk/fuJt/T5pVQ1vUEqxfcMw0zYgszQ5C2MiIl7M6JkTRKU01q9hVFCR83wX\n" - + "zIdrLQIDAQABAkEA7dEA9o/5k77y68ZhRv9z7QEwucBcKzQ3rsSCbWMpYqg924F9\n" - + "L8Z76kzSedSO2PN8mg6y/OLL+qBuTeUK/yiowQIhAP0cknFMbqeNX6uvj/S+V7in\n" - + "bIhQkhcSdJjRw8fxMnJpAiEA9WTp9wzJpn+9etZo0jJ8wkM0+LTMNELo47Ctz7l1\n" - + "kiUCIQCi34vslD5wWyzBEcwUtZdFH5dbcF1Rs3KMFA9jzfWkYQIgHtiWiFV1K5a3\n" - + "DK/S8UkjYY/tIq4nVRJsD+LvlkLrwnkCIECcz4yF4HQgv+Tbzj/gGSBl1VIliTcB\n" - + "Rc5RUQ0mZJQF\n" - + "-----END PRIVATE KEY-----\n"; - - HeldCertificate heldCertificate = HeldCertificate.decode(pkcs8Pem + certificatePem); - assertThat(heldCertificate.certificatePem()).isEqualTo(certificatePem); - assertThat(heldCertificate.privateKeyPkcs8Pem()).isEqualTo(pkcs8Pem); - - X509Certificate certificate = heldCertificate.certificate(); - assertThat(certificate.getSubjectX500Principal().getName()) - .isEqualTo("CN=cash.app"); - } - - @Test public void decodeRsa2048() throws Exception { - // The certificate + private key below was generated programmatically: - // - // HeldCertificate heldCertificate = new HeldCertificate.Builder() - // .validityInterval(5_000L, 10_000L) - // .addSubjectAlternativeName("1.1.1.1") - // .addSubjectAlternativeName("cash.app") - // .serialNumber(42L) - // .commonName("cash.app") - // .organizationalUnit("engineering") - // .rsa2048() - // .build(); - - String certificatePem = "" - + "-----BEGIN CERTIFICATE-----\n" - + "MIIC7TCCAdWgAwIBAgIBKjANBgkqhkiG9w0BAQsFADApMRQwEgYDVQQLEwtlbmdp\n" - + "bmVlcmluZzERMA8GA1UEAxMIY2FzaC5hcHAwHhcNNzAwMTAxMDAwMDA1WhcNNzAw\n" - + "MTAxMDAwMDEwWjApMRQwEgYDVQQLEwtlbmdpbmVlcmluZzERMA8GA1UEAxMIY2Fz\n" - + "aC5hcHAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCaU+vrUPL0APGI\n" - + "SXIuRX4xRrigXmGKx+GRPnWDWvGJwOm23Vpq/eZxQx6PbSUB1+QZzAwge20RpNAp\n" - + "2lt5/qFtgUpEon2j06rd/0+ODqqVJX+6d3SpmF1fPfKUB6AOZbxEkaJpBSTavoTg\n" - + "G2M/NMdjZjrcB3quNQcLg54mmI3HJm1zOd/8i2fZjvoiyVY30Inn2SmQsAotXw1u\n" - + "aE/319bnR2sQlnkp6MJU0eLEtKyRif/IODvY+mtRYYdkFtoeT6qQPMIh+gF/H3to\n" - + "5tjs3g59QC8k2TJDop4EFYUOwdrtnb8wUiBnLyURD1szASE2IO2Ftk1zaNOPKtrv\n" - + "VeJuB/mpAgMBAAGjIDAeMBwGA1UdEQEB/wQSMBCHBAEBAQGCCGNhc2guYXBwMA0G\n" - + "CSqGSIb3DQEBCwUAA4IBAQAPm7vfk+rxSucxxbFiimmFKBw+ymieLY/kznNh0lHJ\n" - + "q15fsMYK7TTTt2FFqyfVXhhRZegLrkrGb3+4Dz1uNtcRrjT4qo+T/JOuZGxtBLbm\n" - + "4/hkFSYavtd2FW+/CK7EnQKUyabgLOblb21IHOlcPwpSe6KkJjpwq0TV/ozzfk/q\n" - + "kGRA7/Ubn5TMRYyHWnod2SS14+BkItcWN03Z7kvyMYrpNZpu6vQRYsqJJFMcmpGZ\n" - + "sZQW31gO2arPmfNotkQdFdNL12c9YZKkJGhyK6NcpffD2l6O9NS5SRD5RnkvBxQw\n" - + "fX5DamL8je/YKSLQ4wgUA/5iVKlCiJGQi6fYIJ0kxayO\n" - + "-----END CERTIFICATE-----\n"; - String pkcs8Pem = "" - + "-----BEGIN PRIVATE KEY-----\n" - + "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCaU+vrUPL0APGI\n" - + "SXIuRX4xRrigXmGKx+GRPnWDWvGJwOm23Vpq/eZxQx6PbSUB1+QZzAwge20RpNAp\n" - + "2lt5/qFtgUpEon2j06rd/0+ODqqVJX+6d3SpmF1fPfKUB6AOZbxEkaJpBSTavoTg\n" - + "G2M/NMdjZjrcB3quNQcLg54mmI3HJm1zOd/8i2fZjvoiyVY30Inn2SmQsAotXw1u\n" - + "aE/319bnR2sQlnkp6MJU0eLEtKyRif/IODvY+mtRYYdkFtoeT6qQPMIh+gF/H3to\n" - + "5tjs3g59QC8k2TJDop4EFYUOwdrtnb8wUiBnLyURD1szASE2IO2Ftk1zaNOPKtrv\n" - + "VeJuB/mpAgMBAAECggEAOlOXaYNZn1Cv+INRrR1EmVkSNEIXeX0bymohvbhka1zG\n" - + "t/8myiMVsh7c8PYeM3kl034j4y7ixPVWW0sUoaHT3vArYo9LDtzTyj1REu6GGAJp\n" - + "KM82/1X/jBx8jufm3SokIoIsMKbqC+ZPj+ep9dx7sxyTCE+nVSnjdL2Uyx+DDg3o\n" - + "na237HTScWIi+tMv5QGEwqLHS2q+NZYfjgnSxNY8BRw4XZCcIZRko9niuB5gUjj/\n" - + "y01HwvOCWuOMaSKZak1OdOaz3427/TkhYIqf6ft0ELF+ASRk3BLQA06pRt88H3u2\n" - + "3vsHJsWr2rkCN0h9uDp2o50ZQ5fvlxqG0QIZmvkIkQKBgQDOHeZKvXO5IxQ+S8ed\n" - + "09bC5SKiclCdW+Ry7N2x1MBfrxc4TTTTNaUN9Qdc6RXANG9bX2CJv0Dkh/0yH3z9\n" - + "Bdq6YcoP6DFCX46jwhCKvxMX9h9PFLvY7l2VSe7NfboGzvYLCy8ErsGuio8u9MHZ\n" - + "osX2ch6Gdhn1xUwLCw+T7rNwjQKBgQC/rWb0sWfgbKhEqV+u5oov+fFjooWmTQlQ\n" - + "jcj+lMWUOkitnPmX9TsH5JDa8I89Y0gJGu7Lfg8XSH+4FCCfX3mSLYwVH5vAIvmr\n" - + "TjMqRwSahQuTr/g+lx7alpcUHYv3z6b3WYIXFPPr3t7grWNJ14wMv9DnItWOg84H\n" - + "LlxAvXXsjQKBgQCRPPhdignVVyaYjwVl7TPTuWoiVbMAbxQW91lwSZ4UzmfqQF0M\n" - + "xyw7HYHGsmelPE2LcTWxWpb7cee0PgPwtwNdejLL6q1rO7JjKghF/EYUCFYff1iu\n" - + "j6hZ3fLr0cAXtBYjygmjnxDTUMd8KvO9y7j644cm8GlyiUgAMBcWAolmsQKBgQCT\n" - + "AJQTWfPGxM6QSi3d32VfwhsFROGnVzGrm/HofYTCV6jhraAmkKcDOKJ3p0LT286l\n" - + "XQiC/FzqiGmbbaRPVlPQbiofESzMQIamgMTwyaKYNy1XyP9kUVYSYqfff4GXPqRY\n" - + "00bYGPOxlC3utkuNmEgKhxnaCncqY5+hFkceR6+nCQKBgQC1Gonjhw0lYe43aHpp\n" - + "nDJKv3FnyN3wxjsR2c9sWpDzHA6CMVhSeLoXCB9ishmrSE/CygNlTU1TEy63xN22\n" - + "+dMHl5I/urMesjKKWiKZHdbWVIjJDv25r3jrN9VLr4q6AD9r1Su5G0o2j0N5ujVg\n" - + "SzpFHp+ZzhL/SANa8EqlcF6ItQ==\n" - + "-----END PRIVATE KEY-----\n"; - - HeldCertificate heldCertificate = HeldCertificate.decode(pkcs8Pem + certificatePem); - assertThat(heldCertificate.certificatePem()).isEqualTo(certificatePem); - assertThat(heldCertificate.privateKeyPkcs8Pem()).isEqualTo(pkcs8Pem); - - X509Certificate certificate = heldCertificate.certificate(); - assertThat(certificate.getNotBefore().getTime()).isEqualTo(5_000L); - assertThat(certificate.getNotAfter().getTime()).isEqualTo(10_000L); - - assertThat(certificate.getSubjectAlternativeNames()).containsExactly( - asList(GeneralName.iPAddress, "1.1.1.1"), - asList(GeneralName.dNSName, "cash.app")); - - assertThat(certificate.getSubjectX500Principal().getName()) - .isEqualTo("CN=cash.app,OU=engineering"); - } - - @Test public void decodeWrongNumber() { - String certificatePem = "" - + "-----BEGIN CERTIFICATE-----\n" - + "MIIBYTCCAQegAwIBAgIBKjAKBggqhkjOPQQDAjApMRQwEgYDVQQLEwtlbmdpbmVl\n" - + "cmluZzERMA8GA1UEAxMIY2FzaC5hcHAwHhcNNzAwMTAxMDAwMDA1WhcNNzAwMTAx\n" - + "MDAwMDEwWjApMRQwEgYDVQQLEwtlbmdpbmVlcmluZzERMA8GA1UEAxMIY2FzaC5h\n" - + "cHAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASda8ChkQXxGELnrV/oBnIAx3dD\n" - + "ocUOJfdz4pOJTP6dVQB9U3UBiW5uSX/MoOD0LL5zG3bVyL3Y6pDwKuYvfLNhoyAw\n" - + "HjAcBgNVHREBAf8EEjAQhwQBAQEBgghjYXNoLmFwcDAKBggqhkjOPQQDAgNIADBF\n" - + "AiAyHHg1N6YDDQiY920+cnI5XSZwEGhAtb9PYWO8bLmkcQIhAI2CfEZf3V/obmdT\n" - + "yyaoEufLKVXhrTQhRfodTeigi4RX\n" - + "-----END CERTIFICATE-----\n"; - String pkcs8Pem = "" - + "-----BEGIN PRIVATE KEY-----\n" - + "MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCA7ODT0xhGSNn4ESj6J\n" - + "lu/GJQZoU9lDrCPeUcQ28tzOWw==\n" - + "-----END PRIVATE KEY-----\n"; - - try { - HeldCertificate.decode(certificatePem); - fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected).hasMessage("string does not include a private key"); - } - - try { - HeldCertificate.decode(pkcs8Pem); - fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected).hasMessage("string does not include a certificate"); - } - - try { - HeldCertificate.decode(certificatePem + pkcs8Pem + certificatePem); - fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected).hasMessage("string includes multiple certificates"); - } - - try { - HeldCertificate.decode(pkcs8Pem + certificatePem + pkcs8Pem); - fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected).hasMessage("string includes multiple private keys"); - } - } - - @Test public void decodeWrongType() { - try { - HeldCertificate.decode("" - + "-----BEGIN CERTIFICATE-----\n" - + "MIIBmjCCAQOgAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhjYXNo\n" - + "-----END CERTIFICATE-----\n" - + "-----BEGIN RSA PRIVATE KEY-----\n" - + "sXPVYAsGD1wizrXX+wFaL3chtF1oG1Fx/jcsSsG6BA==\n" - + "-----END RSA PRIVATE KEY-----\n"); - fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected).hasMessage("unexpected type: RSA PRIVATE KEY"); - } - } - - @Test public void decodeMalformed() { - try { - HeldCertificate.decode("" - + "-----BEGIN CERTIFICATE-----\n" - + "MIIBYTCCAQegAwIBAgIBKjAKBggqhkjOPQQDAjApMRQwEgYDVQQLEwtlbmdpbmVl\n" - + "-----END CERTIFICATE-----\n" - + "-----BEGIN PRIVATE KEY-----\n" - + "MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCA7ODT0xhGSNn4ESj6J\n" - + "lu/GJQZoU9lDrCPeUcQ28tzOWw==\n" - + "-----END PRIVATE KEY-----\n"); - fail(); - } catch (IllegalArgumentException expected) { - if (!platform.isConscrypt()) { - assertThat(expected).hasMessage("failed to decode certificate"); - } - } - try { - HeldCertificate.decode("" - + "-----BEGIN CERTIFICATE-----\n" - + "MIIBYTCCAQegAwIBAgIBKjAKBggqhkjOPQQDAjApMRQwEgYDVQQLEwtlbmdpbmVl\n" - + "cmluZzERMA8GA1UEAxMIY2FzaC5hcHAwHhcNNzAwMTAxMDAwMDA1WhcNNzAwMTAx\n" - + "MDAwMDEwWjApMRQwEgYDVQQLEwtlbmdpbmVlcmluZzERMA8GA1UEAxMIY2FzaC5h\n" - + "cHAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASda8ChkQXxGELnrV/oBnIAx3dD\n" - + "ocUOJfdz4pOJTP6dVQB9U3UBiW5uSX/MoOD0LL5zG3bVyL3Y6pDwKuYvfLNhoyAw\n" - + "HjAcBgNVHREBAf8EEjAQhwQBAQEBgghjYXNoLmFwcDAKBggqhkjOPQQDAgNIADBF\n" - + "AiAyHHg1N6YDDQiY920+cnI5XSZwEGhAtb9PYWO8bLmkcQIhAI2CfEZf3V/obmdT\n" - + "yyaoEufLKVXhrTQhRfodTeigi4RX\n" - + "-----END CERTIFICATE-----\n" - + "-----BEGIN PRIVATE KEY-----\n" - + "MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCA7ODT0xhGSNn4ESj6J\n" - + "-----END PRIVATE KEY-----\n"); - fail(); - } catch (IllegalArgumentException expected) { - if (!platform.isConscrypt()) { - assertThat(expected).hasMessage("failed to decode private key"); - } - } - } -} diff --git a/okhttp-tls/src/test/java/okhttp3/tls/HeldCertificateTest.kt b/okhttp-tls/src/test/java/okhttp3/tls/HeldCertificateTest.kt new file mode 100644 index 000000000000..743bae841247 --- /dev/null +++ b/okhttp-tls/src/test/java/okhttp3/tls/HeldCertificateTest.kt @@ -0,0 +1,522 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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 + * + * 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 okhttp3.tls + +import java.math.BigInteger +import java.security.KeyFactory +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.X509EncodedKeySpec +import java.util.Arrays +import java.util.concurrent.TimeUnit +import okhttp3.testing.PlatformRule +import okhttp3.tls.HeldCertificate.Companion.decode +import okio.ByteString.Companion.decodeBase64 +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.data.Offset +import org.bouncycastle.asn1.x509.GeneralName +import org.junit.jupiter.api.Assertions.fail +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class HeldCertificateTest { + @RegisterExtension + var platform = PlatformRule() + + @Test + fun defaultCertificate() { + val now = System.currentTimeMillis() + val heldCertificate = HeldCertificate.Builder().build() + val certificate = heldCertificate.certificate + assertThat(certificate.getSubjectX500Principal().name).overridingErrorMessage( + "self-signed" + ).isEqualTo(certificate.getIssuerX500Principal().name) + assertThat(certificate.getIssuerX500Principal().name).matches("CN=[0-9a-f-]{36}") + assertThat(certificate.serialNumber).isEqualTo(BigInteger.ONE) + assertThat(certificate.subjectAlternativeNames).isNull() + val deltaMillis = 1000.0 + val durationMillis = TimeUnit.MINUTES.toMillis((60 * 24).toLong()) + assertThat(certificate.notBefore.time.toDouble()) + .isCloseTo(now.toDouble(), Offset.offset(deltaMillis)) + assertThat(certificate.notAfter.time.toDouble()).isCloseTo( + now.toDouble() + durationMillis, Offset.offset(deltaMillis) + ) + } + + @Test + fun customInterval() { + // 5 seconds starting on 1970-01-01. + val heldCertificate = HeldCertificate.Builder() + .validityInterval(5000L, 10000L) + .build() + val certificate = heldCertificate.certificate + assertThat(certificate.notBefore.time).isEqualTo(5000L) + assertThat(certificate.notAfter.time).isEqualTo(10000L) + } + + @Test + fun customDuration() { + val now = System.currentTimeMillis() + val heldCertificate = HeldCertificate.Builder() + .duration(5, TimeUnit.SECONDS) + .build() + val certificate = heldCertificate.certificate + val deltaMillis = 1000.0 + val durationMillis = 5000L + assertThat(certificate.notBefore.time.toDouble()) + .isCloseTo(now.toDouble(), Offset.offset(deltaMillis)) + assertThat(certificate.notAfter.time.toDouble()).isCloseTo( + now.toDouble() + durationMillis, Offset.offset(deltaMillis) + ) + } + + @Test + fun subjectAlternativeNames() { + val heldCertificate = HeldCertificate.Builder() + .addSubjectAlternativeName("1.1.1.1") + .addSubjectAlternativeName("cash.app") + .build() + val certificate = heldCertificate.certificate + assertThat(certificate.subjectAlternativeNames).containsExactly( + listOf(GeneralName.iPAddress, "1.1.1.1"), + listOf(GeneralName.dNSName, "cash.app") + ) + } + + @Test + fun commonName() { + val heldCertificate = HeldCertificate.Builder() + .commonName("cash.app") + .build() + val certificate = heldCertificate.certificate + assertThat(certificate.getSubjectX500Principal().name).isEqualTo("CN=cash.app") + } + + @Test + fun organizationalUnit() { + val heldCertificate = HeldCertificate.Builder() + .commonName("cash.app") + .organizationalUnit("cash") + .build() + val certificate = heldCertificate.certificate + assertThat(certificate.getSubjectX500Principal().name).isEqualTo( + "CN=cash.app,OU=cash" + ) + } + + /** Confirm golden values of encoded PEMs. */ + @Test + fun pems() { + val keyFactory = KeyFactory.getInstance("RSA") + val publicKeyBytes = ("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCApFHhtrLan28q+oMolZuaTfWBA0V5aM" + + "Ivq32BsloQu6LlvX1wJ4YEoUCjDlPOtpht7XLbUmBnbIzN89XK4UJVM6Sqp3K88Km8z7gMrdrfTom/274wL25fICR+" + + "yDEQ5fUVYBmJAKXZF1aoI0mIoEx0xFsQhIJ637v2MxJDupd61wIDAQAB") + .decodeBase64()!! + val publicKey = keyFactory.generatePublic( + X509EncodedKeySpec(publicKeyBytes.toByteArray()) + ) + val privateKeyBytes = ("MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAICkUeG2stqfbyr6gyiVm" + + "5pN9YEDRXlowi+rfYGyWhC7ouW9fXAnhgShQKMOU862mG3tcttSYGdsjM3z1crhQlUzpKqncrzwqbzPuAyt2t9Oib/" + + "bvjAvbl8gJH7IMRDl9RVgGYkApdkXVqgjSYigTHTEWxCEgnrfu/YzEkO6l3rXAgMBAAECgYB99mhnB6piADOuddXv6" + + "26NzUBTr4xbsYRTgSxHzwf50oFTTBSDuW+1IOBVyTWu94SSPyt0LllPbC8Di3sQSTnVGpSqAvEXknBMzIc0UO74Rn9" + + "p3gZjEenPt1l77fIBa2nK06/rdsJCoE/1P1JSfM9w7LU1RsTmseYMLeJl5F79gQJBAO/BbAKqg1yzK7VijygvBoUrr" + + "+rt2lbmKgcUQ/rxu8IIQk0M/xgJqSkXDXuOnboGM7sQSKfJAZUtT7xozvLzV7ECQQCJW59w7NIM0qZ/gIX2gcNZr1B" + + "/V3zcGlolTDciRm+fnKGNt2EEDKnVL3swzbEfTCa48IT0QKgZJqpXZERa26UHAkBLXmiP5f5pk8F3wcXzAeVw06z3k" + + "1IB41Tu6MX+CyPU+TeudRlz+wV8b0zDvK+EnRKCCbptVFj1Bkt8lQ4JfcnhAkAk2Y3Gz+HySrkcT7Cg12M/NkdUQnZ" + + "e3jr88pt/+IGNwomc6Wt/mJ4fcWONTkGMcfOZff1NQeNXDAZ6941XCsIVAkASOg02PlVHLidU7mIE65swMM5/RNhS4" + + "aFjez/MwxFNOHaxc9VgCwYPXCLOtdf7AVovdyG0XWgbUXH+NyxKwboE").decodeBase64()!! + val privateKey = keyFactory.generatePrivate( + PKCS8EncodedKeySpec(privateKeyBytes.toByteArray()) + ) + val heldCertificate = HeldCertificate.Builder() + .keyPair(publicKey, privateKey) + .commonName("cash.app") + .validityInterval(0L, 1000L) + .rsa2048() + .build() + assertThat( + """ + |-----BEGIN CERTIFICATE----- + |MIIBmjCCAQOgAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhjYXNo + |LmFwcDAeFw03MDAxMDEwMDAwMDBaFw03MDAxMDEwMDAwMDFaMBMxETAPBgNVBAMM + |CGNhc2guYXBwMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCApFHhtrLan28q + |+oMolZuaTfWBA0V5aMIvq32BsloQu6LlvX1wJ4YEoUCjDlPOtpht7XLbUmBnbIzN + |89XK4UJVM6Sqp3K88Km8z7gMrdrfTom/274wL25fICR+yDEQ5fUVYBmJAKXZF1ao + |I0mIoEx0xFsQhIJ637v2MxJDupd61wIDAQABMA0GCSqGSIb3DQEBCwUAA4GBADHT + |vcjwl9Z4I5Cb2R1y7aaa860HkY2k3ThaDK5OJt6GYqJTA9P3LtX7VwQtL1TWqXGc + |+OEfl3zhm0PUqcbckMzhJtqIa7NkDSjNm71BKd843pIhGcEri69DcL/cR8T+eMex + |hadh7aGM9OjeL8gznLeq27Ly6Dj7Vkp5OmOrSKfn + |-----END CERTIFICATE----- + |""".trimMargin() + ).isEqualTo(heldCertificate.certificatePem()) + assertThat( + """ + |-----BEGIN RSA PRIVATE KEY----- + |MIICWwIBAAKBgQCApFHhtrLan28q+oMolZuaTfWBA0V5aMIvq32BsloQu6LlvX1w + |J4YEoUCjDlPOtpht7XLbUmBnbIzN89XK4UJVM6Sqp3K88Km8z7gMrdrfTom/274w + |L25fICR+yDEQ5fUVYBmJAKXZF1aoI0mIoEx0xFsQhIJ637v2MxJDupd61wIDAQAB + |AoGAffZoZweqYgAzrnXV7+tujc1AU6+MW7GEU4EsR88H+dKBU0wUg7lvtSDgVck1 + |rveEkj8rdC5ZT2wvA4t7EEk51RqUqgLxF5JwTMyHNFDu+EZ/ad4GYxHpz7dZe+3y + |AWtpytOv63bCQqBP9T9SUnzPcOy1NUbE5rHmDC3iZeRe/YECQQDvwWwCqoNcsyu1 + |Yo8oLwaFK6/q7dpW5ioHFEP68bvCCEJNDP8YCakpFw17jp26BjO7EEinyQGVLU+8 + |aM7y81exAkEAiVufcOzSDNKmf4CF9oHDWa9Qf1d83BpaJUw3IkZvn5yhjbdhBAyp + |1S97MM2xH0wmuPCE9ECoGSaqV2REWtulBwJAS15oj+X+aZPBd8HF8wHlcNOs95NS + |AeNU7ujF/gsj1Pk3rnUZc/sFfG9Mw7yvhJ0Sggm6bVRY9QZLfJUOCX3J4QJAJNmN + |xs/h8kq5HE+woNdjPzZHVEJ2Xt46/PKbf/iBjcKJnOlrf5ieH3FjjU5BjHHzmX39 + |TUHjVwwGeveNVwrCFQJAEjoNNj5VRy4nVO5iBOubMDDOf0TYUuGhY3s/zMMRTTh2 + |sXPVYAsGD1wizrXX+wFaL3chtF1oG1Fx/jcsSsG6BA== + |-----END RSA PRIVATE KEY----- + |""".trimMargin() + ).isEqualTo(heldCertificate.privateKeyPkcs1Pem()) + assertThat( + """ + |-----BEGIN PRIVATE KEY----- + |MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAICkUeG2stqfbyr6 + |gyiVm5pN9YEDRXlowi+rfYGyWhC7ouW9fXAnhgShQKMOU862mG3tcttSYGdsjM3z + |1crhQlUzpKqncrzwqbzPuAyt2t9Oib/bvjAvbl8gJH7IMRDl9RVgGYkApdkXVqgj + |SYigTHTEWxCEgnrfu/YzEkO6l3rXAgMBAAECgYB99mhnB6piADOuddXv626NzUBT + |r4xbsYRTgSxHzwf50oFTTBSDuW+1IOBVyTWu94SSPyt0LllPbC8Di3sQSTnVGpSq + |AvEXknBMzIc0UO74Rn9p3gZjEenPt1l77fIBa2nK06/rdsJCoE/1P1JSfM9w7LU1 + |RsTmseYMLeJl5F79gQJBAO/BbAKqg1yzK7VijygvBoUrr+rt2lbmKgcUQ/rxu8II + |Qk0M/xgJqSkXDXuOnboGM7sQSKfJAZUtT7xozvLzV7ECQQCJW59w7NIM0qZ/gIX2 + |gcNZr1B/V3zcGlolTDciRm+fnKGNt2EEDKnVL3swzbEfTCa48IT0QKgZJqpXZERa + |26UHAkBLXmiP5f5pk8F3wcXzAeVw06z3k1IB41Tu6MX+CyPU+TeudRlz+wV8b0zD + |vK+EnRKCCbptVFj1Bkt8lQ4JfcnhAkAk2Y3Gz+HySrkcT7Cg12M/NkdUQnZe3jr8 + |8pt/+IGNwomc6Wt/mJ4fcWONTkGMcfOZff1NQeNXDAZ6941XCsIVAkASOg02PlVH + |LidU7mIE65swMM5/RNhS4aFjez/MwxFNOHaxc9VgCwYPXCLOtdf7AVovdyG0XWgb + |UXH+NyxKwboE + |-----END PRIVATE KEY----- + |""".trimMargin() + ).isEqualTo(heldCertificate.privateKeyPkcs8Pem()) + } + + @Test + fun ecdsaSignedByRsa() { + val root = HeldCertificate.Builder() + .certificateAuthority(0) + .rsa2048() + .build() + val leaf = HeldCertificate.Builder() + .certificateAuthority(0) + .ecdsa256() + .signedBy(root) + .build() + assertThat(root.certificate.sigAlgName).isEqualToIgnoringCase("SHA256WITHRSA") + assertThat(leaf.certificate.sigAlgName).isEqualToIgnoringCase("SHA256WITHRSA") + } + + @Test + fun rsaSignedByEcdsa() { + val root = HeldCertificate.Builder() + .certificateAuthority(0) + .ecdsa256() + .build() + val leaf = HeldCertificate.Builder() + .certificateAuthority(0) + .rsa2048() + .signedBy(root) + .build() + assertThat(root.certificate.sigAlgName).isEqualToIgnoringCase("SHA256WITHECDSA") + assertThat(leaf.certificate.sigAlgName).isEqualToIgnoringCase("SHA256WITHECDSA") + } + + @Test + fun decodeEcdsa256() { + // The certificate + private key below was generated programmatically: + // + // HeldCertificate heldCertificate = new HeldCertificate.Builder() + // .validityInterval(5_000L, 10_000L) + // .addSubjectAlternativeName("1.1.1.1") + // .addSubjectAlternativeName("cash.app") + // .serialNumber(42L) + // .commonName("cash.app") + // .organizationalUnit("engineering") + // .ecdsa256() + // .build(); + val certificatePem = """ + |-----BEGIN CERTIFICATE----- + |MIIBYTCCAQegAwIBAgIBKjAKBggqhkjOPQQDAjApMRQwEgYDVQQLEwtlbmdpbmVl + |cmluZzERMA8GA1UEAxMIY2FzaC5hcHAwHhcNNzAwMTAxMDAwMDA1WhcNNzAwMTAx + |MDAwMDEwWjApMRQwEgYDVQQLEwtlbmdpbmVlcmluZzERMA8GA1UEAxMIY2FzaC5h + |cHAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASda8ChkQXxGELnrV/oBnIAx3dD + |ocUOJfdz4pOJTP6dVQB9U3UBiW5uSX/MoOD0LL5zG3bVyL3Y6pDwKuYvfLNhoyAw + |HjAcBgNVHREBAf8EEjAQhwQBAQEBgghjYXNoLmFwcDAKBggqhkjOPQQDAgNIADBF + |AiAyHHg1N6YDDQiY920+cnI5XSZwEGhAtb9PYWO8bLmkcQIhAI2CfEZf3V/obmdT + |yyaoEufLKVXhrTQhRfodTeigi4RX + |-----END CERTIFICATE----- + |""".trimMargin() + val pkcs8Pem = """ + |-----BEGIN PRIVATE KEY----- + |MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCA7ODT0xhGSNn4ESj6J + |lu/GJQZoU9lDrCPeUcQ28tzOWw== + |-----END PRIVATE KEY----- + |""".trimMargin() + val bcPkcs8Pem = """ + |-----BEGIN PRIVATE KEY----- + |ME0CAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEMzAxAgEBBCA7ODT0xhGSNn4ESj6J + |lu/GJQZoU9lDrCPeUcQ28tzOW6AKBggqhkjOPQMBBw== + |-----END PRIVATE KEY----- + |""".trimMargin() + val heldCertificate = decode(certificatePem + pkcs8Pem) + assertThat(heldCertificate.certificatePem()).isEqualTo(certificatePem) + + // Slightly different encoding + if (platform.isBouncyCastle()) { + assertThat(heldCertificate.privateKeyPkcs8Pem()).isEqualTo(bcPkcs8Pem) + } else { + assertThat(heldCertificate.privateKeyPkcs8Pem()).isEqualTo(pkcs8Pem) + } + val certificate = heldCertificate.certificate + assertThat(certificate.notBefore.time).isEqualTo(5000L) + assertThat(certificate.notAfter.time).isEqualTo(10000L) + assertThat(certificate.subjectAlternativeNames).containsExactly( + listOf(GeneralName.iPAddress, "1.1.1.1"), + listOf(GeneralName.dNSName, "cash.app") + ) + assertThat(certificate.getSubjectX500Principal().name) + .isEqualTo("CN=cash.app,OU=engineering") + } + + @Test + fun decodeRsa512() { + // The certificate + private key below was generated with OpenSSL. Never generate certificates + // with MD5 or 512-bit RSA; that's insecure! + // + // openssl req \ + // -x509 \ + // -md5 \ + // -nodes \ + // -days 1 \ + // -newkey rsa:512 \ + // -keyout privateKey.key \ + // -out certificate.crt + val certificatePem = """ + |-----BEGIN CERTIFICATE----- + |MIIBFzCBwgIJAIVAqagcVN7/MA0GCSqGSIb3DQEBBAUAMBMxETAPBgNVBAMMCGNh + |c2guYXBwMB4XDTE5MDkwNzAyMjg0NFoXDTE5MDkwODAyMjg0NFowEzERMA8GA1UE + |AwwIY2FzaC5hcHAwXDANBgkqhkiG9w0BAQEFAANLADBIAkEA8qAeoubm4mBTD9/J + |ujLQkfk/fuJt/T5pVQ1vUEqxfcMw0zYgszQ5C2MiIl7M6JkTRKU01q9hVFCR83wX + |zIdrLQIDAQABMA0GCSqGSIb3DQEBBAUAA0EAO1UpwhrkW3Ho1nZK/taoUQOoqz/n + |HFVMtyEkm5gBDgz8nJXwb3zbegclQyH+kVou02S8zC5WWzEtd0R8S0LsTA== + |-----END CERTIFICATE----- + |""".trimMargin() + val pkcs8Pem = """ + |-----BEGIN PRIVATE KEY----- + |MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA8qAeoubm4mBTD9/J + |ujLQkfk/fuJt/T5pVQ1vUEqxfcMw0zYgszQ5C2MiIl7M6JkTRKU01q9hVFCR83wX + |zIdrLQIDAQABAkEA7dEA9o/5k77y68ZhRv9z7QEwucBcKzQ3rsSCbWMpYqg924F9 + |L8Z76kzSedSO2PN8mg6y/OLL+qBuTeUK/yiowQIhAP0cknFMbqeNX6uvj/S+V7in + |bIhQkhcSdJjRw8fxMnJpAiEA9WTp9wzJpn+9etZo0jJ8wkM0+LTMNELo47Ctz7l1 + |kiUCIQCi34vslD5wWyzBEcwUtZdFH5dbcF1Rs3KMFA9jzfWkYQIgHtiWiFV1K5a3 + |DK/S8UkjYY/tIq4nVRJsD+LvlkLrwnkCIECcz4yF4HQgv+Tbzj/gGSBl1VIliTcB + |Rc5RUQ0mZJQF + |-----END PRIVATE KEY----- + |""".trimMargin() + val heldCertificate = decode(pkcs8Pem + certificatePem) + assertThat(heldCertificate.certificatePem()).isEqualTo(certificatePem) + assertThat(heldCertificate.privateKeyPkcs8Pem()).isEqualTo(pkcs8Pem) + val certificate = heldCertificate.certificate + assertThat(certificate.getSubjectX500Principal().name) + .isEqualTo("CN=cash.app") + } + + @Test + fun decodeRsa2048() { + // The certificate + private key below was generated programmatically: + // + // HeldCertificate heldCertificate = new HeldCertificate.Builder() + // .validityInterval(5_000L, 10_000L) + // .addSubjectAlternativeName("1.1.1.1") + // .addSubjectAlternativeName("cash.app") + // .serialNumber(42L) + // .commonName("cash.app") + // .organizationalUnit("engineering") + // .rsa2048() + // .build(); + val certificatePem = """ + |-----BEGIN CERTIFICATE----- + |MIIC7TCCAdWgAwIBAgIBKjANBgkqhkiG9w0BAQsFADApMRQwEgYDVQQLEwtlbmdp + |bmVlcmluZzERMA8GA1UEAxMIY2FzaC5hcHAwHhcNNzAwMTAxMDAwMDA1WhcNNzAw + |MTAxMDAwMDEwWjApMRQwEgYDVQQLEwtlbmdpbmVlcmluZzERMA8GA1UEAxMIY2Fz + |aC5hcHAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCaU+vrUPL0APGI + |SXIuRX4xRrigXmGKx+GRPnWDWvGJwOm23Vpq/eZxQx6PbSUB1+QZzAwge20RpNAp + |2lt5/qFtgUpEon2j06rd/0+ODqqVJX+6d3SpmF1fPfKUB6AOZbxEkaJpBSTavoTg + |G2M/NMdjZjrcB3quNQcLg54mmI3HJm1zOd/8i2fZjvoiyVY30Inn2SmQsAotXw1u + |aE/319bnR2sQlnkp6MJU0eLEtKyRif/IODvY+mtRYYdkFtoeT6qQPMIh+gF/H3to + |5tjs3g59QC8k2TJDop4EFYUOwdrtnb8wUiBnLyURD1szASE2IO2Ftk1zaNOPKtrv + |VeJuB/mpAgMBAAGjIDAeMBwGA1UdEQEB/wQSMBCHBAEBAQGCCGNhc2guYXBwMA0G + |CSqGSIb3DQEBCwUAA4IBAQAPm7vfk+rxSucxxbFiimmFKBw+ymieLY/kznNh0lHJ + |q15fsMYK7TTTt2FFqyfVXhhRZegLrkrGb3+4Dz1uNtcRrjT4qo+T/JOuZGxtBLbm + |4/hkFSYavtd2FW+/CK7EnQKUyabgLOblb21IHOlcPwpSe6KkJjpwq0TV/ozzfk/q + |kGRA7/Ubn5TMRYyHWnod2SS14+BkItcWN03Z7kvyMYrpNZpu6vQRYsqJJFMcmpGZ + |sZQW31gO2arPmfNotkQdFdNL12c9YZKkJGhyK6NcpffD2l6O9NS5SRD5RnkvBxQw + |fX5DamL8je/YKSLQ4wgUA/5iVKlCiJGQi6fYIJ0kxayO + |-----END CERTIFICATE----- + |""".trimMargin() + val pkcs8Pem = """ + |-----BEGIN PRIVATE KEY----- + |MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCaU+vrUPL0APGI + |SXIuRX4xRrigXmGKx+GRPnWDWvGJwOm23Vpq/eZxQx6PbSUB1+QZzAwge20RpNAp + |2lt5/qFtgUpEon2j06rd/0+ODqqVJX+6d3SpmF1fPfKUB6AOZbxEkaJpBSTavoTg + |G2M/NMdjZjrcB3quNQcLg54mmI3HJm1zOd/8i2fZjvoiyVY30Inn2SmQsAotXw1u + |aE/319bnR2sQlnkp6MJU0eLEtKyRif/IODvY+mtRYYdkFtoeT6qQPMIh+gF/H3to + |5tjs3g59QC8k2TJDop4EFYUOwdrtnb8wUiBnLyURD1szASE2IO2Ftk1zaNOPKtrv + |VeJuB/mpAgMBAAECggEAOlOXaYNZn1Cv+INRrR1EmVkSNEIXeX0bymohvbhka1zG + |t/8myiMVsh7c8PYeM3kl034j4y7ixPVWW0sUoaHT3vArYo9LDtzTyj1REu6GGAJp + |KM82/1X/jBx8jufm3SokIoIsMKbqC+ZPj+ep9dx7sxyTCE+nVSnjdL2Uyx+DDg3o + |na237HTScWIi+tMv5QGEwqLHS2q+NZYfjgnSxNY8BRw4XZCcIZRko9niuB5gUjj/ + |y01HwvOCWuOMaSKZak1OdOaz3427/TkhYIqf6ft0ELF+ASRk3BLQA06pRt88H3u2 + |3vsHJsWr2rkCN0h9uDp2o50ZQ5fvlxqG0QIZmvkIkQKBgQDOHeZKvXO5IxQ+S8ed + |09bC5SKiclCdW+Ry7N2x1MBfrxc4TTTTNaUN9Qdc6RXANG9bX2CJv0Dkh/0yH3z9 + |Bdq6YcoP6DFCX46jwhCKvxMX9h9PFLvY7l2VSe7NfboGzvYLCy8ErsGuio8u9MHZ + |osX2ch6Gdhn1xUwLCw+T7rNwjQKBgQC/rWb0sWfgbKhEqV+u5oov+fFjooWmTQlQ + |jcj+lMWUOkitnPmX9TsH5JDa8I89Y0gJGu7Lfg8XSH+4FCCfX3mSLYwVH5vAIvmr + |TjMqRwSahQuTr/g+lx7alpcUHYv3z6b3WYIXFPPr3t7grWNJ14wMv9DnItWOg84H + |LlxAvXXsjQKBgQCRPPhdignVVyaYjwVl7TPTuWoiVbMAbxQW91lwSZ4UzmfqQF0M + |xyw7HYHGsmelPE2LcTWxWpb7cee0PgPwtwNdejLL6q1rO7JjKghF/EYUCFYff1iu + |j6hZ3fLr0cAXtBYjygmjnxDTUMd8KvO9y7j644cm8GlyiUgAMBcWAolmsQKBgQCT + |AJQTWfPGxM6QSi3d32VfwhsFROGnVzGrm/HofYTCV6jhraAmkKcDOKJ3p0LT286l + |XQiC/FzqiGmbbaRPVlPQbiofESzMQIamgMTwyaKYNy1XyP9kUVYSYqfff4GXPqRY + |00bYGPOxlC3utkuNmEgKhxnaCncqY5+hFkceR6+nCQKBgQC1Gonjhw0lYe43aHpp + |nDJKv3FnyN3wxjsR2c9sWpDzHA6CMVhSeLoXCB9ishmrSE/CygNlTU1TEy63xN22 + |+dMHl5I/urMesjKKWiKZHdbWVIjJDv25r3jrN9VLr4q6AD9r1Su5G0o2j0N5ujVg + |SzpFHp+ZzhL/SANa8EqlcF6ItQ== + |-----END PRIVATE KEY----- + |""".trimMargin() + val heldCertificate = decode(pkcs8Pem + certificatePem) + assertThat(heldCertificate.certificatePem()).isEqualTo(certificatePem) + assertThat(heldCertificate.privateKeyPkcs8Pem()).isEqualTo(pkcs8Pem) + val certificate = heldCertificate.certificate + assertThat(certificate.notBefore.time).isEqualTo(5000L) + assertThat(certificate.notAfter.time).isEqualTo(10000L) + assertThat(certificate.subjectAlternativeNames).containsExactly( + Arrays.asList(GeneralName.iPAddress, "1.1.1.1"), + Arrays.asList(GeneralName.dNSName, "cash.app") + ) + assertThat(certificate.getSubjectX500Principal().name) + .isEqualTo("CN=cash.app,OU=engineering") + } + + @Test + fun decodeWrongNumber() { + val certificatePem = """ + |-----BEGIN CERTIFICATE----- + |MIIBYTCCAQegAwIBAgIBKjAKBggqhkjOPQQDAjApMRQwEgYDVQQLEwtlbmdpbmVl + |cmluZzERMA8GA1UEAxMIY2FzaC5hcHAwHhcNNzAwMTAxMDAwMDA1WhcNNzAwMTAx + |MDAwMDEwWjApMRQwEgYDVQQLEwtlbmdpbmVlcmluZzERMA8GA1UEAxMIY2FzaC5h + |cHAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASda8ChkQXxGELnrV/oBnIAx3dD + |ocUOJfdz4pOJTP6dVQB9U3UBiW5uSX/MoOD0LL5zG3bVyL3Y6pDwKuYvfLNhoyAw + |HjAcBgNVHREBAf8EEjAQhwQBAQEBgghjYXNoLmFwcDAKBggqhkjOPQQDAgNIADBF + |AiAyHHg1N6YDDQiY920+cnI5XSZwEGhAtb9PYWO8bLmkcQIhAI2CfEZf3V/obmdT + |yyaoEufLKVXhrTQhRfodTeigi4RX + |-----END CERTIFICATE----- + |""".trimMargin() + val pkcs8Pem = """ + |-----BEGIN PRIVATE KEY----- + |MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCA7ODT0xhGSNn4ESj6J + |lu/GJQZoU9lDrCPeUcQ28tzOWw== + |-----END PRIVATE KEY----- + |""".trimMargin() + try { + decode(certificatePem) + fail() + } catch (expected: IllegalArgumentException) { + assertThat(expected).hasMessage("string does not include a private key") + } + try { + decode(pkcs8Pem) + fail() + } catch (expected: IllegalArgumentException) { + assertThat(expected).hasMessage("string does not include a certificate") + } + try { + decode(certificatePem + pkcs8Pem + certificatePem) + fail() + } catch (expected: IllegalArgumentException) { + assertThat(expected).hasMessage("string includes multiple certificates") + } + try { + decode(pkcs8Pem + certificatePem + pkcs8Pem) + fail() + } catch (expected: IllegalArgumentException) { + assertThat(expected).hasMessage("string includes multiple private keys") + } + } + + @Test + fun decodeWrongType() { + try { + decode( + """ + |-----BEGIN CERTIFICATE----- + |MIIBmjCCAQOgAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhjYXNo + |-----END CERTIFICATE----- + |-----BEGIN RSA PRIVATE KEY----- + |sXPVYAsGD1wizrXX+wFaL3chtF1oG1Fx/jcsSsG6BA== + |-----END RSA PRIVATE KEY----- + |""".trimMargin() + ) + fail() + } catch (expected: IllegalArgumentException) { + assertThat(expected).hasMessage("unexpected type: RSA PRIVATE KEY") + } + } + + @Test + fun decodeMalformed() { + try { + decode( + """ + |-----BEGIN CERTIFICATE----- + |MIIBYTCCAQegAwIBAgIBKjAKBggqhkjOPQQDAjApMRQwEgYDVQQLEwtlbmdpbmVl + |-----END CERTIFICATE----- + |-----BEGIN PRIVATE KEY----- + |MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCA7ODT0xhGSNn4ESj6J + |lu/GJQZoU9lDrCPeUcQ28tzOWw== + |-----END PRIVATE KEY----- + |""".trimMargin() + ) + fail() + } catch (expected: IllegalArgumentException) { + if (!platform.isConscrypt()) { + assertThat(expected).hasMessage("failed to decode certificate") + } + } + try { + decode( + """ + |-----BEGIN CERTIFICATE----- + |MIIBYTCCAQegAwIBAgIBKjAKBggqhkjOPQQDAjApMRQwEgYDVQQLEwtlbmdpbmVl + |cmluZzERMA8GA1UEAxMIY2FzaC5hcHAwHhcNNzAwMTAxMDAwMDA1WhcNNzAwMTAx + |MDAwMDEwWjApMRQwEgYDVQQLEwtlbmdpbmVlcmluZzERMA8GA1UEAxMIY2FzaC5h + |cHAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASda8ChkQXxGELnrV/oBnIAx3dD + |ocUOJfdz4pOJTP6dVQB9U3UBiW5uSX/MoOD0LL5zG3bVyL3Y6pDwKuYvfLNhoyAw + |HjAcBgNVHREBAf8EEjAQhwQBAQEBgghjYXNoLmFwcDAKBggqhkjOPQQDAgNIADBF + |AiAyHHg1N6YDDQiY920+cnI5XSZwEGhAtb9PYWO8bLmkcQIhAI2CfEZf3V/obmdT + |yyaoEufLKVXhrTQhRfodTeigi4RX + |-----END CERTIFICATE----- + |-----BEGIN PRIVATE KEY----- + |MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCA7ODT0xhGSNn4ESj6J + |-----END PRIVATE KEY----- + |""".trimMargin() + ) + fail() + } catch (expected: IllegalArgumentException) { + if (!platform.isConscrypt()) { + assertThat(expected).hasMessage("failed to decode private key") + } + } + } +} diff --git a/okhttp/src/test/java/okhttp3/CallTest.kt b/okhttp/src/test/java/okhttp3/CallTest.kt index 01cdad6a700b..c0fe00c1d8be 100644 --- a/okhttp/src/test/java/okhttp3/CallTest.kt +++ b/okhttp/src/test/java/okhttp3/CallTest.kt @@ -750,8 +750,8 @@ open class CallTest { val firstResponse = callback.await(request.url).assertSuccessful() val secondResponse = callback.await(request.url).assertSuccessful() val bodies: MutableSet = LinkedHashSet() - bodies.add(firstResponse.getBody()) - bodies.add(secondResponse.getBody()) + bodies.add(firstResponse.body) + bodies.add(secondResponse.body) assertThat(bodies).contains("abc") assertThat(bodies).contains("def") } diff --git a/okhttp/src/test/java/okhttp3/CipherSuiteTest.java b/okhttp/src/test/java/okhttp3/CipherSuiteTest.java deleted file mode 100644 index 4a57285ae43c..000000000000 --- a/okhttp/src/test/java/okhttp3/CipherSuiteTest.java +++ /dev/null @@ -1,222 +0,0 @@ -/* - * Copyright (C) 2016 Google Inc. - * - * 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 - * - * 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 okhttp3; - -import org.junit.jupiter.api.Test; - -import static okhttp3.CipherSuite.TLS_KRB5_WITH_DES_CBC_MD5; -import static okhttp3.CipherSuite.TLS_RSA_EXPORT_WITH_RC4_40_MD5; -import static okhttp3.CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA256; -import static okhttp3.CipherSuite.forJavaName; -import static okhttp3.internal.Internal.applyConnectionSpec; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.fail; - -public class CipherSuiteTest { - @Test public void nullCipherName() { - try { - forJavaName(null); - fail("Should have thrown"); - } catch (NullPointerException expected) { - } - } - - @Test public void hashCode_usesIdentityHashCode_legacyCase() { - CipherSuite cs = TLS_RSA_EXPORT_WITH_RC4_40_MD5; // This one's javaName starts with "SSL_". - assertThat(cs.hashCode()).overridingErrorMessage(cs.toString()).isEqualTo( - System.identityHashCode(cs)); - } - - @Test public void hashCode_usesIdentityHashCode_regularCase() { - CipherSuite cs = TLS_RSA_WITH_AES_128_CBC_SHA256; // This one's javaName matches the identifier. - assertThat(cs.hashCode()).overridingErrorMessage(cs.toString()).isEqualTo( - System.identityHashCode(cs)); - } - - @Test public void instancesAreInterned() { - assertThat(forJavaName("TestCipherSuite")).isSameAs(forJavaName("TestCipherSuite")); - assertThat(forJavaName(TLS_KRB5_WITH_DES_CBC_MD5.javaName())) - .isSameAs(TLS_KRB5_WITH_DES_CBC_MD5); - } - - /** - * Tests that interned CipherSuite instances remain the case across garbage collections, even if - * the String used to construct them is no longer strongly referenced outside of the CipherSuite. - */ - @SuppressWarnings("RedundantStringConstructorCall") - @Test public void instancesAreInterned_survivesGarbageCollection() { - // We're not holding onto a reference to this String instance outside of the CipherSuite... - CipherSuite cs = forJavaName(new String("FakeCipherSuite_instancesAreInterned")); - System.gc(); // Unless cs references the String instance, it may now be garbage collected. - assertThat(forJavaName(new String(cs.javaName()))).isSameAs(cs); - } - - @Test public void equals() { - assertThat(forJavaName("cipher")).isEqualTo(forJavaName("cipher")); - assertThat(forJavaName("cipherB")).isNotEqualTo(forJavaName("cipherA")); - assertThat(TLS_RSA_EXPORT_WITH_RC4_40_MD5).isEqualTo( - forJavaName("SSL_RSA_EXPORT_WITH_RC4_40_MD5")); - assertThat(TLS_RSA_WITH_AES_128_CBC_SHA256).isNotEqualTo( - TLS_RSA_EXPORT_WITH_RC4_40_MD5); - } - - @Test public void forJavaName_acceptsArbitraryStrings() { - // Shouldn't throw. - forJavaName("example CipherSuite name that is not in the whitelist"); - } - - @Test public void javaName_examples() { - assertThat(TLS_RSA_EXPORT_WITH_RC4_40_MD5.javaName()).isEqualTo( - "SSL_RSA_EXPORT_WITH_RC4_40_MD5"); - assertThat(TLS_RSA_WITH_AES_128_CBC_SHA256.javaName()).isEqualTo( - "TLS_RSA_WITH_AES_128_CBC_SHA256"); - assertThat(forJavaName("TestCipherSuite").javaName()).isEqualTo("TestCipherSuite"); - } - - @Test public void javaName_equalsToString() { - assertThat(TLS_RSA_EXPORT_WITH_RC4_40_MD5.toString()).isEqualTo( - TLS_RSA_EXPORT_WITH_RC4_40_MD5.javaName()); - assertThat(TLS_RSA_WITH_AES_128_CBC_SHA256.toString()).isEqualTo( - TLS_RSA_WITH_AES_128_CBC_SHA256.javaName()); - } - - /** - * On the Oracle JVM some older cipher suites have the "SSL_" prefix and others have the "TLS_" - * prefix. On the IBM JVM all cipher suites have the "SSL_" prefix. - * - *

Prior to OkHttp 3.3.1 we accepted either form and consider them equivalent. And since OkHttp - * 3.7.0 this is also true. But OkHttp 3.3.1 through 3.6.0 treated these as different. - */ - @Test public void forJavaName_fromLegacyEnumName() { - // These would have been considered equal in OkHttp 3.3.1, but now aren't. - assertThat(forJavaName("SSL_RSA_EXPORT_WITH_RC4_40_MD5")).isEqualTo( - forJavaName("TLS_RSA_EXPORT_WITH_RC4_40_MD5")); - assertThat(forJavaName("SSL_DH_RSA_EXPORT_WITH_DES40_CBC_SHA")).isEqualTo( - forJavaName("TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA")); - assertThat(forJavaName("SSL_FAKE_NEW_CIPHER")).isEqualTo( - forJavaName("TLS_FAKE_NEW_CIPHER")); - } - - @Test public void applyIntersectionRetainsTlsPrefixes() throws Exception { - FakeSslSocket socket = new FakeSslSocket(); - socket.setEnabledProtocols(new String[] { "TLSv1" }); - socket.setSupportedCipherSuites(new String[] { "SSL_A", "SSL_B", "SSL_C", "SSL_D", "SSL_E" }); - socket.setEnabledCipherSuites(new String[] { "SSL_A", "SSL_B", "SSL_C" }); - - ConnectionSpec connectionSpec = new ConnectionSpec.Builder(true) - .tlsVersions(TlsVersion.TLS_1_0) - .cipherSuites("TLS_A", "TLS_C", "TLS_E") - .build(); - applyConnectionSpec(connectionSpec, socket, false); - - assertArrayEquals(new String[] { "TLS_A", "TLS_C" }, socket.enabledCipherSuites); - } - - @Test public void applyIntersectionRetainsSslPrefixes() throws Exception { - FakeSslSocket socket = new FakeSslSocket(); - socket.setEnabledProtocols(new String[] { "TLSv1" }); - socket.setSupportedCipherSuites(new String[] { "TLS_A", "TLS_B", "TLS_C", "TLS_D", "TLS_E" }); - socket.setEnabledCipherSuites(new String[] { "TLS_A", "TLS_B", "TLS_C" }); - - ConnectionSpec connectionSpec = new ConnectionSpec.Builder(true) - .tlsVersions(TlsVersion.TLS_1_0) - .cipherSuites("SSL_A", "SSL_C", "SSL_E") - .build(); - applyConnectionSpec(connectionSpec, socket, false); - - assertArrayEquals(new String[] { "SSL_A", "SSL_C" }, socket.enabledCipherSuites); - } - - @Test public void applyIntersectionAddsSslScsvForFallback() throws Exception { - FakeSslSocket socket = new FakeSslSocket(); - socket.setEnabledProtocols(new String[] { "TLSv1" }); - socket.setSupportedCipherSuites(new String[] { "SSL_A", "SSL_FALLBACK_SCSV" }); - socket.setEnabledCipherSuites(new String[] { "SSL_A" }); - - ConnectionSpec connectionSpec = new ConnectionSpec.Builder(true) - .tlsVersions(TlsVersion.TLS_1_0) - .cipherSuites("SSL_A") - .build(); - applyConnectionSpec(connectionSpec, socket, true); - - assertArrayEquals(new String[] { "SSL_A", "SSL_FALLBACK_SCSV" }, socket.enabledCipherSuites); - } - - @Test public void applyIntersectionAddsTlsScsvForFallback() throws Exception { - FakeSslSocket socket = new FakeSslSocket(); - socket.setEnabledProtocols(new String[] { "TLSv1" }); - socket.setSupportedCipherSuites(new String[] { "TLS_A", "TLS_FALLBACK_SCSV" }); - socket.setEnabledCipherSuites(new String[] { "TLS_A" }); - - ConnectionSpec connectionSpec = new ConnectionSpec.Builder(true) - .tlsVersions(TlsVersion.TLS_1_0) - .cipherSuites("TLS_A") - .build(); - applyConnectionSpec(connectionSpec, socket, true); - - assertArrayEquals(new String[] { "TLS_A", "TLS_FALLBACK_SCSV" }, socket.enabledCipherSuites); - } - - @Test public void applyIntersectionToProtocolVersion() throws Exception { - FakeSslSocket socket = new FakeSslSocket(); - socket.setEnabledProtocols(new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" }); - socket.setSupportedCipherSuites(new String[] { "TLS_A" }); - socket.setEnabledCipherSuites(new String[] { "TLS_A" }); - - ConnectionSpec connectionSpec = new ConnectionSpec.Builder(true) - .tlsVersions(TlsVersion.TLS_1_1, TlsVersion.TLS_1_2, TlsVersion.TLS_1_3) - .cipherSuites("TLS_A") - .build(); - applyConnectionSpec(connectionSpec, socket, false); - - assertArrayEquals(new String[] { "TLSv1.1", "TLSv1.2" }, socket.enabledProtocols); - } - - static final class FakeSslSocket extends DelegatingSSLSocket { - private String[] enabledProtocols; - private String[] supportedCipherSuites; - private String[] enabledCipherSuites; - - FakeSslSocket() { - super(null); - } - - @Override public String[] getEnabledProtocols() { - return enabledProtocols; - } - - @Override public void setEnabledProtocols(String[] enabledProtocols) { - this.enabledProtocols = enabledProtocols; - } - - @Override public String[] getSupportedCipherSuites() { - return supportedCipherSuites; - } - - public void setSupportedCipherSuites(String[] supportedCipherSuites) { - this.supportedCipherSuites = supportedCipherSuites; - } - - @Override public String[] getEnabledCipherSuites() { - return enabledCipherSuites; - } - - @Override public void setEnabledCipherSuites(String[] enabledCipherSuites) { - this.enabledCipherSuites = enabledCipherSuites; - } - } -} diff --git a/okhttp/src/test/java/okhttp3/CipherSuiteTest.kt b/okhttp/src/test/java/okhttp3/CipherSuiteTest.kt new file mode 100644 index 000000000000..bb7dcdf971e3 --- /dev/null +++ b/okhttp/src/test/java/okhttp3/CipherSuiteTest.kt @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * 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 + * + * 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 okhttp3 + +import okhttp3.CipherSuite.Companion.forJavaName +import okhttp3.internal.applyConnectionSpec +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Test + +class CipherSuiteTest { + @Test + fun hashCode_usesIdentityHashCode_legacyCase() { + val cs = CipherSuite.TLS_RSA_EXPORT_WITH_RC4_40_MD5 // This one's javaName starts with "SSL_". + assertThat(cs.hashCode()) + .overridingErrorMessage(cs.toString()) + .isEqualTo(System.identityHashCode(cs)) + } + + @Test + fun hashCode_usesIdentityHashCode_regularCase() { + // This one's javaName matches the identifier. + val cs = CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA256 + assertThat(cs.hashCode()) + .overridingErrorMessage(cs.toString()) + .isEqualTo(System.identityHashCode(cs)) + } + + @Test + fun instancesAreInterned() { + assertThat(forJavaName("TestCipherSuite")) + .isSameAs(forJavaName("TestCipherSuite")) + assertThat(forJavaName(CipherSuite.TLS_KRB5_WITH_DES_CBC_MD5.javaName)) + .isSameAs(CipherSuite.TLS_KRB5_WITH_DES_CBC_MD5) + } + + /** + * Tests that interned CipherSuite instances remain the case across garbage collections, even if + * the String used to construct them is no longer strongly referenced outside of the CipherSuite. + */ + @Test + fun instancesAreInterned_survivesGarbageCollection() { + // We're not holding onto a reference to this String instance outside of the CipherSuite... + val cs = forJavaName("FakeCipherSuite_instancesAreInterned") + System.gc() // Unless cs references the String instance, it may now be garbage collected. + assertThat(forJavaName(java.lang.String(cs.javaName) as String)) + .isSameAs(cs) + } + + @Test + fun equals() { + assertThat(forJavaName("cipher")).isEqualTo(forJavaName("cipher")) + assertThat(forJavaName("cipherB")).isNotEqualTo(forJavaName("cipherA")) + assertThat(CipherSuite.TLS_RSA_EXPORT_WITH_RC4_40_MD5) + .isEqualTo(forJavaName("SSL_RSA_EXPORT_WITH_RC4_40_MD5")) + assertThat(CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA256) + .isNotEqualTo(CipherSuite.TLS_RSA_EXPORT_WITH_RC4_40_MD5) + } + + @Test + fun forJavaName_acceptsArbitraryStrings() { + // Shouldn't throw. + forJavaName("example CipherSuite name that is not in the whitelist") + } + + @Test + fun javaName_examples() { + assertThat(CipherSuite.TLS_RSA_EXPORT_WITH_RC4_40_MD5.javaName) + .isEqualTo("SSL_RSA_EXPORT_WITH_RC4_40_MD5") + assertThat(CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA256.javaName) + .isEqualTo("TLS_RSA_WITH_AES_128_CBC_SHA256") + assertThat(forJavaName("TestCipherSuite").javaName) + .isEqualTo("TestCipherSuite") + } + + @Test + fun javaName_equalsToString() { + assertThat(CipherSuite.TLS_RSA_EXPORT_WITH_RC4_40_MD5.toString()) + .isEqualTo(CipherSuite.TLS_RSA_EXPORT_WITH_RC4_40_MD5.javaName) + assertThat(CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA256.toString()) + .isEqualTo(CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA256.javaName) + } + + /** + * On the Oracle JVM some older cipher suites have the "SSL_" prefix and others have the "TLS_" + * prefix. On the IBM JVM all cipher suites have the "SSL_" prefix. + * + * Prior to OkHttp 3.3.1 we accepted either form and consider them equivalent. And since OkHttp + * 3.7.0 this is also true. But OkHttp 3.3.1 through 3.6.0 treated these as different. + */ + @Test + fun forJavaName_fromLegacyEnumName() { + // These would have been considered equal in OkHttp 3.3.1, but now aren't. + assertThat(forJavaName("SSL_RSA_EXPORT_WITH_RC4_40_MD5")) + .isEqualTo(forJavaName("TLS_RSA_EXPORT_WITH_RC4_40_MD5")) + assertThat(forJavaName("SSL_DH_RSA_EXPORT_WITH_DES40_CBC_SHA")) + .isEqualTo(forJavaName("TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA")) + assertThat(forJavaName("SSL_FAKE_NEW_CIPHER")) + .isEqualTo(forJavaName("TLS_FAKE_NEW_CIPHER")) + } + + @Test + fun applyIntersectionRetainsTlsPrefixes() { + val socket = FakeSslSocket() + socket.enabledProtocols = arrayOf("TLSv1") + socket.supportedCipherSuites = arrayOf("SSL_A", "SSL_B", "SSL_C", "SSL_D", "SSL_E") + socket.enabledCipherSuites = arrayOf("SSL_A", "SSL_B", "SSL_C") + val connectionSpec = ConnectionSpec.Builder(true) + .tlsVersions(TlsVersion.TLS_1_0) + .cipherSuites("TLS_A", "TLS_C", "TLS_E") + .build() + applyConnectionSpec(connectionSpec, socket, false) + assertArrayEquals(arrayOf("TLS_A", "TLS_C"), socket.enabledCipherSuites) + } + + @Test + fun applyIntersectionRetainsSslPrefixes() { + val socket = FakeSslSocket() + socket.enabledProtocols = arrayOf("TLSv1") + socket.supportedCipherSuites = + arrayOf("TLS_A", "TLS_B", "TLS_C", "TLS_D", "TLS_E") + socket.enabledCipherSuites = arrayOf("TLS_A", "TLS_B", "TLS_C") + val connectionSpec = ConnectionSpec.Builder(true) + .tlsVersions(TlsVersion.TLS_1_0) + .cipherSuites("SSL_A", "SSL_C", "SSL_E") + .build() + applyConnectionSpec(connectionSpec, socket, false) + assertArrayEquals(arrayOf("SSL_A", "SSL_C"), socket.enabledCipherSuites) + } + + @Test + fun applyIntersectionAddsSslScsvForFallback() { + val socket = FakeSslSocket() + socket.enabledProtocols = arrayOf("TLSv1") + socket.supportedCipherSuites = arrayOf("SSL_A", "SSL_FALLBACK_SCSV") + socket.enabledCipherSuites = arrayOf("SSL_A") + val connectionSpec = ConnectionSpec.Builder(true) + .tlsVersions(TlsVersion.TLS_1_0) + .cipherSuites("SSL_A") + .build() + applyConnectionSpec(connectionSpec, socket, true) + assertArrayEquals( + arrayOf("SSL_A", "SSL_FALLBACK_SCSV"), + socket.enabledCipherSuites + ) + } + + @Test + fun applyIntersectionAddsTlsScsvForFallback() { + val socket = FakeSslSocket() + socket.enabledProtocols = arrayOf("TLSv1") + socket.supportedCipherSuites = arrayOf("TLS_A", "TLS_FALLBACK_SCSV") + socket.enabledCipherSuites = arrayOf("TLS_A") + val connectionSpec = ConnectionSpec.Builder(true) + .tlsVersions(TlsVersion.TLS_1_0) + .cipherSuites("TLS_A") + .build() + applyConnectionSpec(connectionSpec, socket, true) + assertArrayEquals( + arrayOf("TLS_A", "TLS_FALLBACK_SCSV"), + socket.enabledCipherSuites + ) + } + + @Test + fun applyIntersectionToProtocolVersion() { + val socket = FakeSslSocket() + socket.enabledProtocols = arrayOf("TLSv1", "TLSv1.1", "TLSv1.2") + socket.supportedCipherSuites = arrayOf("TLS_A") + socket.enabledCipherSuites = arrayOf("TLS_A") + val connectionSpec = ConnectionSpec.Builder(true) + .tlsVersions(TlsVersion.TLS_1_1, TlsVersion.TLS_1_2, TlsVersion.TLS_1_3) + .cipherSuites("TLS_A") + .build() + applyConnectionSpec(connectionSpec, socket, false) + assertArrayEquals(arrayOf("TLSv1.1", "TLSv1.2"), socket.enabledProtocols) + } + + internal class FakeSslSocket : DelegatingSSLSocket(null) { + private lateinit var enabledProtocols: Array + private lateinit var supportedCipherSuites: Array + private lateinit var enabledCipherSuites: Array + override fun getEnabledProtocols(): Array { + return enabledProtocols + } + + override fun setEnabledProtocols(protocols: Array) { + this.enabledProtocols = protocols + } + + override fun getSupportedCipherSuites(): Array { + return supportedCipherSuites + } + + fun setSupportedCipherSuites(supportedCipherSuites: Array) { + this.supportedCipherSuites = supportedCipherSuites + } + + override fun getEnabledCipherSuites(): Array { + return enabledCipherSuites + } + + override fun setEnabledCipherSuites(suites: Array) { + this.enabledCipherSuites = suites + } + } +} diff --git a/okhttp/src/test/java/okhttp3/CookiesTest.java b/okhttp/src/test/java/okhttp3/CookiesTest.java deleted file mode 100644 index 5a3b621d55ef..000000000000 --- a/okhttp/src/test/java/okhttp3/CookiesTest.java +++ /dev/null @@ -1,370 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * 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 - * - * 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 okhttp3; - -import java.io.IOException; -import java.net.CookieHandler; -import java.net.CookieManager; -import java.net.HttpCookie; -import java.net.HttpURLConnection; -import java.net.InetAddress; -import java.net.URI; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import mockwebserver3.MockResponse; -import mockwebserver3.MockWebServer; -import mockwebserver3.RecordedRequest; -import okhttp3.java.net.cookiejar.JavaNetCookieJar; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; -import org.junit.jupiter.api.extension.RegisterExtension; - -import static java.net.CookiePolicy.ACCEPT_ORIGINAL_SERVER; -import static java.util.Arrays.asList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.data.Offset.offset; -import static org.junit.jupiter.api.Assertions.fail; - -/** Derived from Android's CookiesTest. */ -@Timeout(30) -public class CookiesTest { - @RegisterExtension public final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule(); - - private MockWebServer server; - private OkHttpClient client = clientTestRule.newClient(); - - @BeforeEach - public void setUp(MockWebServer server) throws Exception { - this.server = server; - } - - @Test - public void testNetscapeResponse() throws Exception { - CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER); - client = client.newBuilder() - .cookieJar(new JavaNetCookieJar(cookieManager)) - .build(); - - HttpUrl urlWithIpAddress = urlWithIpAddress(server, "/path/foo"); - server.enqueue(new MockResponse.Builder() - .addHeader("Set-Cookie: a=android; " - + "expires=Fri, 31-Dec-9999 23:59:59 GMT; " - + "path=/path; " - + "domain=" + urlWithIpAddress.host() + "; " - + "secure") - .build()); - get(urlWithIpAddress); - - List cookies = cookieManager.getCookieStore().getCookies(); - assertThat(cookies.size()).isEqualTo(1); - HttpCookie cookie = cookies.get(0); - assertThat(cookie.getName()).isEqualTo("a"); - assertThat(cookie.getValue()).isEqualTo("android"); - assertThat(cookie.getComment()).isNull(); - assertThat(cookie.getCommentURL()).isNull(); - assertThat(cookie.getDiscard()).isFalse(); - assertThat(cookie.getMaxAge()).isGreaterThan(100000000000L); - assertThat(cookie.getPath()).isEqualTo("/path"); - assertThat(cookie.getSecure()).isTrue(); - assertThat(cookie.getVersion()).isEqualTo(0); - } - - @Test public void testRfc2109Response() throws Exception { - CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER); - client = client.newBuilder() - .cookieJar(new JavaNetCookieJar(cookieManager)) - .build(); - - HttpUrl urlWithIpAddress = urlWithIpAddress(server, "/path/foo"); - server.enqueue(new MockResponse.Builder() - .addHeader("Set-Cookie: a=android; " - + "Comment=this cookie is delicious; " - + "Domain=" + urlWithIpAddress.host() + "; " - + "Max-Age=60; " - + "Path=/path; " - + "Secure; " - + "Version=1") - .build()); - get(urlWithIpAddress); - - List cookies = cookieManager.getCookieStore().getCookies(); - assertThat(cookies.size()).isEqualTo(1); - HttpCookie cookie = cookies.get(0); - assertThat(cookie.getName()).isEqualTo("a"); - assertThat(cookie.getValue()).isEqualTo("android"); - assertThat(cookie.getCommentURL()).isNull(); - assertThat(cookie.getDiscard()).isFalse(); - // Converting to a fixed date can cause rounding! - assertThat((double) cookie.getMaxAge()).isCloseTo(60.0, offset(5.0)); - assertThat(cookie.getPath()).isEqualTo("/path"); - assertThat(cookie.getSecure()).isTrue(); - } - - @Test public void testQuotedAttributeValues() throws Exception { - CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER); - client = client.newBuilder() - .cookieJar(new JavaNetCookieJar(cookieManager)) - .build(); - - HttpUrl urlWithIpAddress = urlWithIpAddress(server, "/path/foo"); - server.enqueue(new MockResponse.Builder() - .addHeader("Set-Cookie: a=\"android\"; " - + "Comment=\"this cookie is delicious\"; " - + "CommentURL=\"http://google.com/\"; " - + "Discard; " - + "Domain=" + urlWithIpAddress.host() + "; " - + "Max-Age=60; " - + "Path=\"/path\"; " - + "Port=\"80,443," + server.getPort() + "\"; " - + "Secure; " - + "Version=\"1\"") - .build()); - get(urlWithIpAddress); - - List cookies = cookieManager.getCookieStore().getCookies(); - assertThat(cookies.size()).isEqualTo(1); - HttpCookie cookie = cookies.get(0); - assertThat(cookie.getName()).isEqualTo("a"); - assertThat(cookie.getValue()).isEqualTo("android"); - // Converting to a fixed date can cause rounding! - assertThat((double) cookie.getMaxAge()).isCloseTo(60.0, offset(1.0)); - assertThat(cookie.getPath()).isEqualTo("/path"); - assertThat(cookie.getSecure()).isTrue(); - } - - @Test public void testSendingCookiesFromStore() throws Exception { - server.enqueue(new MockResponse()); - HttpUrl serverUrl = urlWithIpAddress(server, "/"); - - CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER); - HttpCookie cookieA = new HttpCookie("a", "android"); - cookieA.setDomain(serverUrl.host()); - cookieA.setPath("/"); - cookieManager.getCookieStore().add(serverUrl.uri(), cookieA); - HttpCookie cookieB = new HttpCookie("b", "banana"); - cookieB.setDomain(serverUrl.host()); - cookieB.setPath("/"); - cookieManager.getCookieStore().add(serverUrl.uri(), cookieB); - client = client.newBuilder() - .cookieJar(new JavaNetCookieJar(cookieManager)) - .build(); - - get(serverUrl); - RecordedRequest request = server.takeRequest(); - - assertThat(request.getHeaders().get("Cookie")).isEqualTo("a=android; b=banana"); - } - - @Test public void cookieHandlerLikeAndroid() throws Exception { - server.enqueue(new MockResponse()); - final HttpUrl serverUrl = urlWithIpAddress(server, "/"); - - CookieHandler androidCookieHandler = new CookieHandler() { - @Override public Map> get(URI uri, Map> map) { - return Collections.singletonMap("Cookie", Collections.singletonList("$Version=\"1\"; " - + "a=\"android\";$Path=\"/\";$Domain=\"" + serverUrl.host() + "\"; " - + "b=\"banana\";$Path=\"/\";$Domain=\"" + serverUrl.host() + "\"")); - } - - @Override public void put(URI uri, Map> map) throws IOException { - } - }; - - client = client.newBuilder() - .cookieJar(new JavaNetCookieJar(androidCookieHandler)) - .build(); - - get(serverUrl); - RecordedRequest request = server.takeRequest(); - - assertThat(request.getHeaders().get("Cookie")).isEqualTo("a=android; b=banana"); - } - - @Test public void receiveAndSendMultipleCookies() throws Exception { - server.enqueue(new MockResponse.Builder() - .addHeader("Set-Cookie", "a=android") - .addHeader("Set-Cookie", "b=banana") - .build()); - server.enqueue(new MockResponse()); - - CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER); - client = client.newBuilder() - .cookieJar(new JavaNetCookieJar(cookieManager)) - .build(); - - get(urlWithIpAddress(server, "/")); - RecordedRequest request1 = server.takeRequest(); - assertThat(request1.getHeaders().get("Cookie")).isNull(); - - get(urlWithIpAddress(server, "/")); - RecordedRequest request2 = server.takeRequest(); - assertThat(request2.getHeaders().get("Cookie")).isEqualTo("a=android; b=banana"); - } - - @Test public void testRedirectsDoNotIncludeTooManyCookies() throws Exception { - MockWebServer redirectTarget = new MockWebServer(); - redirectTarget.enqueue(new MockResponse.Builder().body("A").build()); - redirectTarget.start(); - HttpUrl redirectTargetUrl = urlWithIpAddress(redirectTarget, "/"); - - MockWebServer redirectSource = new MockWebServer(); - redirectSource.enqueue(new MockResponse.Builder() - .code(HttpURLConnection.HTTP_MOVED_TEMP) - .addHeader("Location: " + redirectTargetUrl) - .build()); - redirectSource.start(); - HttpUrl redirectSourceUrl = urlWithIpAddress(redirectSource, "/"); - - CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER); - HttpCookie cookie = new HttpCookie("c", "cookie"); - cookie.setDomain(redirectSourceUrl.host()); - cookie.setPath("/"); - String portList = Integer.toString(redirectSource.getPort()); - cookie.setPortlist(portList); - cookieManager.getCookieStore().add(redirectSourceUrl.uri(), cookie); - client = client.newBuilder() - .cookieJar(new JavaNetCookieJar(cookieManager)) - .build(); - - get(redirectSourceUrl); - RecordedRequest request = redirectSource.takeRequest(); - - assertThat(request.getHeaders().get("Cookie")).isEqualTo("c=cookie"); - - for (String header : redirectTarget.takeRequest().getHeaders().names()) { - if (header.startsWith("Cookie")) { - fail(header); - } - } - } - - @Test public void testCookiesSentIgnoresCase() throws Exception { - client = client.newBuilder() - .cookieJar(new JavaNetCookieJar(new CookieManager() { - @Override public Map> get(URI uri, - Map> requestHeaders) { - Map> result = new LinkedHashMap<>(); - result.put("COOKIE", Collections.singletonList("Bar=bar")); - result.put("cooKIE2", Collections.singletonList("Baz=baz")); - return result; - } - })) - .build(); - - server.enqueue(new MockResponse()); - - get(server.url("/")); - - RecordedRequest request = server.takeRequest(); - assertThat(request.getHeaders().get("Cookie")).isEqualTo("Bar=bar; Baz=baz"); - assertThat(request.getHeaders().get("Cookie2")).isNull(); - assertThat(request.getHeaders().get("Quux")).isNull(); - } - - @Test public void acceptOriginalServerMatchesSubdomain() throws Exception { - CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER); - JavaNetCookieJar cookieJar = new JavaNetCookieJar(cookieManager); - - HttpUrl url = HttpUrl.get("https://www.squareup.com/"); - cookieJar.saveFromResponse(url, asList( - Cookie.parse(url, "a=android; Domain=squareup.com"))); - List actualCookies = cookieJar.loadForRequest(url); - assertThat(actualCookies.size()).isEqualTo(1); - assertThat(actualCookies.get(0).name()).isEqualTo("a"); - assertThat(actualCookies.get(0).value()).isEqualTo("android"); - } - - @Test public void acceptOriginalServerMatchesRfc2965Dot() throws Exception { - CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER); - JavaNetCookieJar cookieJar = new JavaNetCookieJar(cookieManager); - - HttpUrl url = HttpUrl.get("https://www.squareup.com/"); - cookieJar.saveFromResponse(url, asList( - Cookie.parse(url, "a=android; Domain=.squareup.com"))); - List actualCookies = cookieJar.loadForRequest(url); - assertThat(actualCookies.size()).isEqualTo(1); - assertThat(actualCookies.get(0).name()).isEqualTo("a"); - assertThat(actualCookies.get(0).value()).isEqualTo("android"); - } - - @Test public void acceptOriginalServerMatchesExactly() throws Exception { - CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER); - JavaNetCookieJar cookieJar = new JavaNetCookieJar(cookieManager); - - HttpUrl url = HttpUrl.get("https://squareup.com/"); - cookieJar.saveFromResponse(url, asList( - Cookie.parse(url, "a=android; Domain=squareup.com"))); - List actualCookies = cookieJar.loadForRequest(url); - assertThat(actualCookies.size()).isEqualTo(1); - assertThat(actualCookies.get(0).name()).isEqualTo("a"); - assertThat(actualCookies.get(0).value()).isEqualTo("android"); - } - - @Test public void acceptOriginalServerDoesNotMatchDifferentServer() throws Exception { - CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER); - JavaNetCookieJar cookieJar = new JavaNetCookieJar(cookieManager); - - HttpUrl url1 = HttpUrl.get("https://api.squareup.com/"); - cookieJar.saveFromResponse(url1, asList( - Cookie.parse(url1, "a=android; Domain=api.squareup.com"))); - - HttpUrl url2 = HttpUrl.get("https://www.squareup.com/"); - List actualCookies = cookieJar.loadForRequest(url2); - assertThat(actualCookies).isEmpty(); - } - - @Test public void testQuoteStripping() throws Exception { - client = client.newBuilder() - .cookieJar(new JavaNetCookieJar(new CookieManager() { - @Override public Map> get(URI uri, - Map> requestHeaders) { - Map> result = new LinkedHashMap<>(); - result.put("COOKIE", Collections.singletonList("Bar=\"")); - result.put("cooKIE2", Collections.singletonList("Baz=\"baz\"")); - return result; - } - })) - .build(); - - server.enqueue(new MockResponse()); - - get(server.url("/")); - - RecordedRequest request = server.takeRequest(); - assertThat(request.getHeaders().get("Cookie")).isEqualTo("Bar=\"; Baz=baz"); - assertThat(request.getHeaders().get("Cookie2")).isNull(); - assertThat(request.getHeaders().get("Quux")).isNull(); - } - - private HttpUrl urlWithIpAddress(MockWebServer server, String path) throws Exception { - return server.url(path) - .newBuilder() - .host(InetAddress.getByName(server.getHostName()).getHostAddress()) - .build(); - } - - private void get(HttpUrl url) throws Exception { - Call call = client.newCall(new Request.Builder() - .url(url) - .build()); - Response response = call.execute(); - response.body().close(); - } -} diff --git a/okhttp/src/test/java/okhttp3/CookiesTest.kt b/okhttp/src/test/java/okhttp3/CookiesTest.kt new file mode 100644 index 000000000000..52a83f7cb932 --- /dev/null +++ b/okhttp/src/test/java/okhttp3/CookiesTest.kt @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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 + * + * 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 okhttp3 + +import java.net.CookieHandler +import java.net.CookieManager +import java.net.CookiePolicy +import java.net.HttpCookie +import java.net.HttpURLConnection +import java.net.InetAddress +import java.net.URI +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import okhttp3.Cookie.Companion.parse +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.java.net.cookiejar.JavaNetCookieJar +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.data.Offset +import org.junit.jupiter.api.Assertions.fail +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import org.junit.jupiter.api.extension.RegisterExtension + +/** Derived from Android's CookiesTest. */ +@Timeout(30) +class CookiesTest { + @RegisterExtension + val clientTestRule = OkHttpClientTestRule() + private lateinit var server: MockWebServer + private var client = clientTestRule.newClient() + + @BeforeEach + fun setUp(server: MockWebServer) { + this.server = server + } + + @Test + fun testNetscapeResponse() { + val cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER) + client = client.newBuilder() + .cookieJar(JavaNetCookieJar(cookieManager)) + .build() + val urlWithIpAddress = urlWithIpAddress(server, "/path/foo") + server.enqueue( + MockResponse.Builder() + .addHeader( + "Set-Cookie: a=android; " + + "expires=Fri, 31-Dec-9999 23:59:59 GMT; " + + "path=/path; " + + "domain=${urlWithIpAddress.host}; " + + "secure" + ) + .build() + ) + get(urlWithIpAddress) + val cookies = cookieManager.cookieStore.cookies + assertThat(cookies.size).isEqualTo(1) + val cookie = cookies[0] + assertThat(cookie.name).isEqualTo("a") + assertThat(cookie.value).isEqualTo("android") + assertThat(cookie.comment).isNull() + assertThat(cookie.commentURL).isNull() + assertThat(cookie.discard).isFalse() + assertThat(cookie.maxAge).isGreaterThan(100000000000L) + assertThat(cookie.path).isEqualTo("/path") + assertThat(cookie.secure).isTrue() + assertThat(cookie.version).isEqualTo(0) + } + + @Test + fun testRfc2109Response() { + val cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER) + client = client.newBuilder() + .cookieJar(JavaNetCookieJar(cookieManager)) + .build() + val urlWithIpAddress = urlWithIpAddress(server, "/path/foo") + server.enqueue( + MockResponse.Builder() + .addHeader( + "Set-Cookie: a=android; " + + "Comment=this cookie is delicious; " + + "Domain=${urlWithIpAddress.host}; " + + "Max-Age=60; " + + "Path=/path; " + + "Secure; " + + "Version=1" + ) + .build() + ) + get(urlWithIpAddress) + val cookies = cookieManager.cookieStore.cookies + assertThat(cookies.size).isEqualTo(1) + val cookie = cookies[0] + assertThat(cookie.name).isEqualTo("a") + assertThat(cookie.value).isEqualTo("android") + assertThat(cookie.commentURL).isNull() + assertThat(cookie.discard).isFalse() + // Converting to a fixed date can cause rounding! + assertThat(cookie.maxAge.toDouble()).isCloseTo(60.0, Offset.offset(5.0)) + assertThat(cookie.path).isEqualTo("/path") + assertThat(cookie.secure).isTrue() + } + + @Test + fun testQuotedAttributeValues() { + val cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER) + client = client.newBuilder() + .cookieJar(JavaNetCookieJar(cookieManager)) + .build() + val urlWithIpAddress = urlWithIpAddress(server, "/path/foo") + server.enqueue( + MockResponse.Builder() + .addHeader( + "Set-Cookie: a=\"android\"; " + + "Comment=\"this cookie is delicious\"; " + + "CommentURL=\"http://google.com/\"; " + + "Discard; " + + "Domain=${urlWithIpAddress.host}; " + + "Max-Age=60; " + + "Path=\"/path\"; " + + "Port=\"80,443,${server.port}\"; " + + "Secure; " + + "Version=\"1\"" + ) + .build() + ) + get(urlWithIpAddress) + val cookies = cookieManager.cookieStore.cookies + assertThat(cookies.size).isEqualTo(1) + val cookie = cookies[0] + assertThat(cookie.name).isEqualTo("a") + assertThat(cookie.value).isEqualTo("android") + // Converting to a fixed date can cause rounding! + assertThat(cookie.maxAge.toDouble()).isCloseTo(60.0, Offset.offset(1.0)) + assertThat(cookie.path).isEqualTo("/path") + assertThat(cookie.secure).isTrue() + } + + @Test + fun testSendingCookiesFromStore() { + server.enqueue(MockResponse()) + val serverUrl = urlWithIpAddress(server, "/") + val cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER) + val cookieA = HttpCookie("a", "android") + cookieA.domain = serverUrl.host + cookieA.path = "/" + cookieManager.cookieStore.add(serverUrl.toUri(), cookieA) + val cookieB = HttpCookie("b", "banana") + cookieB.domain = serverUrl.host + cookieB.path = "/" + cookieManager.cookieStore.add(serverUrl.toUri(), cookieB) + client = client.newBuilder() + .cookieJar(JavaNetCookieJar(cookieManager)) + .build() + get(serverUrl) + val request = server.takeRequest() + assertThat(request.headers["Cookie"]).isEqualTo("a=android; b=banana") + } + + @Test + fun cookieHandlerLikeAndroid() { + server.enqueue(MockResponse()) + val serverUrl = urlWithIpAddress(server, "/") + val androidCookieHandler: CookieHandler = object : CookieHandler() { + override fun get(uri: URI, map: Map>) = mapOf( + "Cookie" to listOf( + "\$Version=\"1\"; " + + "a=\"android\";\$Path=\"/\";\$Domain=\"${serverUrl.host}\"; " + + "b=\"banana\";\$Path=\"/\";\$Domain=\"${serverUrl.host}\"" + ) + ) + + override fun put(uri: URI, map: Map>) { + } + } + client = client.newBuilder() + .cookieJar(JavaNetCookieJar(androidCookieHandler)) + .build() + get(serverUrl) + val request = server.takeRequest() + assertThat(request.headers["Cookie"]).isEqualTo("a=android; b=banana") + } + + @Test + fun receiveAndSendMultipleCookies() { + server.enqueue( + MockResponse.Builder() + .addHeader("Set-Cookie", "a=android") + .addHeader("Set-Cookie", "b=banana") + .build() + ) + server.enqueue(MockResponse()) + val cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER) + client = client.newBuilder() + .cookieJar(JavaNetCookieJar(cookieManager)) + .build() + get(urlWithIpAddress(server, "/")) + val request1 = server.takeRequest() + assertThat(request1.headers["Cookie"]).isNull() + get(urlWithIpAddress(server, "/")) + val request2 = server.takeRequest() + assertThat(request2.headers["Cookie"]).isEqualTo("a=android; b=banana") + } + + @Test + fun testRedirectsDoNotIncludeTooManyCookies() { + val redirectTarget = MockWebServer() + redirectTarget.enqueue(MockResponse.Builder().body("A").build()) + redirectTarget.start() + val redirectTargetUrl = urlWithIpAddress(redirectTarget, "/") + val redirectSource = MockWebServer() + redirectSource.enqueue( + MockResponse.Builder() + .code(HttpURLConnection.HTTP_MOVED_TEMP) + .addHeader("Location: $redirectTargetUrl") + .build() + ) + redirectSource.start() + val redirectSourceUrl = urlWithIpAddress(redirectSource, "/") + val cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER) + val cookie = HttpCookie("c", "cookie") + cookie.domain = redirectSourceUrl.host + cookie.path = "/" + val portList = redirectSource.port.toString() + cookie.portlist = portList + cookieManager.cookieStore.add(redirectSourceUrl.toUri(), cookie) + client = client.newBuilder() + .cookieJar(JavaNetCookieJar(cookieManager)) + .build() + get(redirectSourceUrl) + val request = redirectSource.takeRequest() + assertThat(request.headers["Cookie"]).isEqualTo("c=cookie") + for (header in redirectTarget.takeRequest().headers.names()) { + if (header.startsWith("Cookie")) { + fail(header) + } + } + } + + @Test + fun testCookiesSentIgnoresCase() { + client = client.newBuilder() + .cookieJar(JavaNetCookieJar(object : CookieManager() { + override fun get(uri: URI, requestHeaders: Map>) = mapOf( + "COOKIE" to listOf("Bar=bar"), + "cooKIE2" to listOf("Baz=baz"), + ) + })) + .build() + server.enqueue(MockResponse()) + get(server.url("/")) + val request = server.takeRequest() + assertThat(request.headers["Cookie"]).isEqualTo("Bar=bar; Baz=baz") + assertThat(request.headers["Cookie2"]).isNull() + assertThat(request.headers["Quux"]).isNull() + } + + @Test + fun acceptOriginalServerMatchesSubdomain() { + val cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER) + val cookieJar = JavaNetCookieJar(cookieManager) + val url = "https://www.squareup.com/".toHttpUrl() + cookieJar.saveFromResponse(url, listOf(parse(url, "a=android; Domain=squareup.com")!!)) + val actualCookies = cookieJar.loadForRequest(url) + assertThat(actualCookies.size).isEqualTo(1) + assertThat(actualCookies[0].name).isEqualTo("a") + assertThat(actualCookies[0].value).isEqualTo("android") + } + + @Test + fun acceptOriginalServerMatchesRfc2965Dot() { + val cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER) + val cookieJar = JavaNetCookieJar(cookieManager) + val url = "https://www.squareup.com/".toHttpUrl() + cookieJar.saveFromResponse(url, listOf(parse(url, "a=android; Domain=.squareup.com")!!)) + val actualCookies = cookieJar.loadForRequest(url) + assertThat(actualCookies.size).isEqualTo(1) + assertThat(actualCookies[0].name).isEqualTo("a") + assertThat(actualCookies[0].value).isEqualTo("android") + } + + @Test + fun acceptOriginalServerMatchesExactly() { + val cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER) + val cookieJar = JavaNetCookieJar(cookieManager) + val url = "https://squareup.com/".toHttpUrl() + cookieJar.saveFromResponse(url, listOf(parse(url, "a=android; Domain=squareup.com")!!)) + val actualCookies = cookieJar.loadForRequest(url) + assertThat(actualCookies.size).isEqualTo(1) + assertThat(actualCookies[0].name).isEqualTo("a") + assertThat(actualCookies[0].value).isEqualTo("android") + } + + @Test + fun acceptOriginalServerDoesNotMatchDifferentServer() { + val cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER) + val cookieJar = JavaNetCookieJar(cookieManager) + val url1 = "https://api.squareup.com/".toHttpUrl() + cookieJar.saveFromResponse(url1, listOf(parse(url1, "a=android; Domain=api.squareup.com")!!)) + val url2 = "https://www.squareup.com/".toHttpUrl() + val actualCookies = cookieJar.loadForRequest(url2) + assertThat(actualCookies).isEmpty() + } + + @Test + fun testQuoteStripping() { + client = client.newBuilder() + .cookieJar(JavaNetCookieJar(object : CookieManager() { + override fun get(uri: URI, requestHeaders: Map>) = mapOf( + "COOKIE" to listOf("Bar=\""), + "cooKIE2" to listOf("Baz=\"baz\""), + ) + })) + .build() + server.enqueue(MockResponse()) + get(server.url("/")) + val request = server.takeRequest() + assertThat(request.headers["Cookie"]).isEqualTo("Bar=\"; Baz=baz") + assertThat(request.headers["Cookie2"]).isNull() + assertThat(request.headers["Quux"]).isNull() + } + + private fun urlWithIpAddress(server: MockWebServer, path: String): HttpUrl { + return server.url(path) + .newBuilder() + .host(InetAddress.getByName(server.hostName).hostAddress) + .build() + } + + private operator fun get(url: HttpUrl) { + val call = client.newCall( + Request.Builder() + .url(url) + .build() + ) + val response = call.execute() + response.body.close() + } +} diff --git a/okhttp/src/test/java/okhttp3/DispatcherTest.kt b/okhttp/src/test/java/okhttp3/DispatcherTest.kt index 3144f874f0b8..9226710bbdcd 100644 --- a/okhttp/src/test/java/okhttp3/DispatcherTest.kt +++ b/okhttp/src/test/java/okhttp3/DispatcherTest.kt @@ -19,7 +19,8 @@ class DispatcherTest { val clientTestRule = OkHttpClientTestRule() private val executor = RecordingExecutor(this) val callback = RecordingCallback() - val webSocketListener = RecordingWebSocketListener() + val webSocketListener = object : WebSocketListener() { + } val dispatcher = Dispatcher(executor) val listener = RecordingEventListener() var client = clientTestRule.newClientBuilder() diff --git a/okhttp/src/test/java/okhttp3/FallbackTestClientSocketFactory.java b/okhttp/src/test/java/okhttp3/FallbackTestClientSocketFactory.java deleted file mode 100644 index 49404d0abcf1..000000000000 --- a/okhttp/src/test/java/okhttp3/FallbackTestClientSocketFactory.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2014 Square Inc. - * - * 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 - * - * 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 okhttp3; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; - -/** - * An SSLSocketFactory that delegates calls. Sockets created by the delegate are wrapped with ones - * that will not accept the {@link #TLS_FALLBACK_SCSV} cipher, thus bypassing server-side fallback - * checks on platforms that support it. Unfortunately this wrapping will disable any - * reflection-based calls to SSLSocket from Platform. - */ -public class FallbackTestClientSocketFactory extends DelegatingSSLSocketFactory { - /** - * The cipher suite used during TLS connection fallback to indicate a fallback. See - * https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00 - */ - public static final String TLS_FALLBACK_SCSV = "TLS_FALLBACK_SCSV"; - - public FallbackTestClientSocketFactory(SSLSocketFactory delegate) { - super(delegate); - } - - @Override protected SSLSocket configureSocket(SSLSocket sslSocket) throws IOException { - return new TlsFallbackScsvDisabledSSLSocket(sslSocket); - } - - private static class TlsFallbackScsvDisabledSSLSocket extends DelegatingSSLSocket { - - public TlsFallbackScsvDisabledSSLSocket(SSLSocket socket) { - super(socket); - } - - @Override public void setEnabledCipherSuites(String[] suites) { - List enabledCipherSuites = new ArrayList<>(suites.length); - for (String suite : suites) { - if (!suite.equals(TLS_FALLBACK_SCSV)) { - enabledCipherSuites.add(suite); - } - } - getDelegate().setEnabledCipherSuites( - enabledCipherSuites.toArray(new String[enabledCipherSuites.size()])); - } - } -} diff --git a/okhttp/src/test/java/okhttp3/FallbackTestClientSocketFactory.kt b/okhttp/src/test/java/okhttp3/FallbackTestClientSocketFactory.kt new file mode 100644 index 000000000000..065295a42854 --- /dev/null +++ b/okhttp/src/test/java/okhttp3/FallbackTestClientSocketFactory.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2014 Square Inc. + * + * 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 + * + * 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 okhttp3 + +import javax.net.ssl.SSLSocket +import javax.net.ssl.SSLSocketFactory +import okhttp3.FallbackTestClientSocketFactory.Companion.TLS_FALLBACK_SCSV + +/** + * An SSLSocketFactory that delegates calls. Sockets created by the delegate are wrapped with ones + * that will not accept the [TLS_FALLBACK_SCSV] cipher, thus bypassing server-side fallback + * checks on platforms that support it. Unfortunately this wrapping will disable any + * reflection-based calls to SSLSocket from Platform. + */ +class FallbackTestClientSocketFactory( + delegate: SSLSocketFactory, +) : DelegatingSSLSocketFactory(delegate) { + override fun configureSocket(sslSocket: SSLSocket): SSLSocket = + TlsFallbackScsvDisabledSSLSocket(sslSocket) + + private class TlsFallbackScsvDisabledSSLSocket( + socket: SSLSocket + ) : DelegatingSSLSocket(socket) { + override fun setEnabledCipherSuites(suites: Array) { + val enabledCipherSuites = mutableListOf() + for (suite in suites) { + if (suite != TLS_FALLBACK_SCSV) { + enabledCipherSuites.add(suite) + } + } + delegate!!.enabledCipherSuites = enabledCipherSuites.toTypedArray() + } + } + + companion object { + /** + * The cipher suite used during TLS connection fallback to indicate a fallback. See + * https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00 + */ + const val TLS_FALLBACK_SCSV = "TLS_FALLBACK_SCSV" + } +} diff --git a/okhttp/src/test/java/okhttp3/FormBodyTest.java b/okhttp/src/test/java/okhttp3/FormBodyTest.java deleted file mode 100644 index 335d069453ab..000000000000 --- a/okhttp/src/test/java/okhttp3/FormBodyTest.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 - * - * 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 okhttp3; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import okio.Buffer; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -public final class FormBodyTest { - @Test public void urlEncoding() throws Exception { - FormBody body = new FormBody.Builder() - .add("a+=& b", "c+=& d") - .add("space, the", "final frontier") - .add("%25", "%25") - .build(); - - assertThat(body.size()).isEqualTo(3); - - assertThat(body.encodedName(0)).isEqualTo("a%2B%3D%26+b"); - assertThat(body.encodedName(1)).isEqualTo("space%2C+the"); - assertThat(body.encodedName(2)).isEqualTo("%2525"); - - assertThat(body.name(0)).isEqualTo("a+=& b"); - assertThat(body.name(1)).isEqualTo("space, the"); - assertThat(body.name(2)).isEqualTo("%25"); - - assertThat(body.encodedValue(0)).isEqualTo("c%2B%3D%26+d"); - assertThat(body.encodedValue(1)).isEqualTo("final+frontier"); - assertThat(body.encodedValue(2)).isEqualTo("%2525"); - - assertThat(body.value(0)).isEqualTo("c+=& d"); - assertThat(body.value(1)).isEqualTo("final frontier"); - assertThat(body.value(2)).isEqualTo("%25"); - - assertThat(body.contentType().toString()).isEqualTo( - "application/x-www-form-urlencoded"); - - String expected = "a%2B%3D%26+b=c%2B%3D%26+d&space%2C+the=final+frontier&%2525=%2525"; - assertThat(body.contentLength()).isEqualTo(expected.length()); - - Buffer out = new Buffer(); - body.writeTo(out); - assertThat(out.readUtf8()).isEqualTo(expected); - } - - @Test public void addEncoded() throws Exception { - FormBody body = new FormBody.Builder() - .addEncoded("a+=& b", "c+=& d") - .addEncoded("e+=& f", "g+=& h") - .addEncoded("%25", "%25") - .build(); - - String expected = "a+%3D%26+b=c+%3D%26+d&e+%3D%26+f=g+%3D%26+h&%25=%25"; - Buffer out = new Buffer(); - body.writeTo(out); - assertThat(out.readUtf8()).isEqualTo(expected); - } - - @Test public void encodedPair() throws Exception { - FormBody body = new FormBody.Builder() - .add("sim", "ple") - .build(); - - String expected = "sim=ple"; - assertThat(body.contentLength()).isEqualTo(expected.length()); - - Buffer buffer = new Buffer(); - body.writeTo(buffer); - assertThat(buffer.readUtf8()).isEqualTo(expected); - } - - @Test public void encodeMultiplePairs() throws Exception { - FormBody body = new FormBody.Builder() - .add("sim", "ple") - .add("hey", "there") - .add("help", "me") - .build(); - - String expected = "sim=ple&hey=there&help=me"; - assertThat(body.contentLength()).isEqualTo(expected.length()); - - Buffer buffer = new Buffer(); - body.writeTo(buffer); - assertThat(buffer.readUtf8()).isEqualTo(expected); - } - - @Test public void buildEmptyForm() throws Exception { - FormBody body = new FormBody.Builder().build(); - - String expected = ""; - assertThat(body.contentLength()).isEqualTo(expected.length()); - - Buffer buffer = new Buffer(); - body.writeTo(buffer); - assertThat(buffer.readUtf8()).isEqualTo(expected); - } - - @Test public void characterEncoding() throws Exception { - // Browsers convert '\u0000' to '%EF%BF%BD'. - assertThat(formEncode(0)).isEqualTo("%00"); - assertThat(formEncode(1)).isEqualTo("%01"); - assertThat(formEncode(2)).isEqualTo("%02"); - assertThat(formEncode(3)).isEqualTo("%03"); - assertThat(formEncode(4)).isEqualTo("%04"); - assertThat(formEncode(5)).isEqualTo("%05"); - assertThat(formEncode(6)).isEqualTo("%06"); - assertThat(formEncode(7)).isEqualTo("%07"); - assertThat(formEncode(8)).isEqualTo("%08"); - assertThat(formEncode(9)).isEqualTo("%09"); - // Browsers convert '\n' to '\r\n' - assertThat(formEncode(10)).isEqualTo("%0A"); - assertThat(formEncode(11)).isEqualTo("%0B"); - assertThat(formEncode(12)).isEqualTo("%0C"); - // Browsers convert '\r' to '\r\n' - assertThat(formEncode(13)).isEqualTo("%0D"); - assertThat(formEncode(14)).isEqualTo("%0E"); - assertThat(formEncode(15)).isEqualTo("%0F"); - assertThat(formEncode(16)).isEqualTo("%10"); - assertThat(formEncode(17)).isEqualTo("%11"); - assertThat(formEncode(18)).isEqualTo("%12"); - assertThat(formEncode(19)).isEqualTo("%13"); - assertThat(formEncode(20)).isEqualTo("%14"); - assertThat(formEncode(21)).isEqualTo("%15"); - assertThat(formEncode(22)).isEqualTo("%16"); - assertThat(formEncode(23)).isEqualTo("%17"); - assertThat(formEncode(24)).isEqualTo("%18"); - assertThat(formEncode(25)).isEqualTo("%19"); - assertThat(formEncode(26)).isEqualTo("%1A"); - assertThat(formEncode(27)).isEqualTo("%1B"); - assertThat(formEncode(28)).isEqualTo("%1C"); - assertThat(formEncode(29)).isEqualTo("%1D"); - assertThat(formEncode(30)).isEqualTo("%1E"); - assertThat(formEncode(31)).isEqualTo("%1F"); - // Browsers use '+' for space. - assertThat(formEncode(32)).isEqualTo("+"); - assertThat(formEncode(33)).isEqualTo("%21"); - assertThat(formEncode(34)).isEqualTo("%22"); - assertThat(formEncode(35)).isEqualTo("%23"); - assertThat(formEncode(36)).isEqualTo("%24"); - assertThat(formEncode(37)).isEqualTo("%25"); - assertThat(formEncode(38)).isEqualTo("%26"); - assertThat(formEncode(39)).isEqualTo("%27"); - assertThat(formEncode(40)).isEqualTo("%28"); - assertThat(formEncode(41)).isEqualTo("%29"); - assertThat(formEncode(42)).isEqualTo("*"); - assertThat(formEncode(43)).isEqualTo("%2B"); - assertThat(formEncode(44)).isEqualTo("%2C"); - assertThat(formEncode(45)).isEqualTo("-"); - assertThat(formEncode(46)).isEqualTo("."); - assertThat(formEncode(47)).isEqualTo("%2F"); - assertThat(formEncode(48)).isEqualTo("0"); - assertThat(formEncode(57)).isEqualTo("9"); - assertThat(formEncode(58)).isEqualTo("%3A"); - assertThat(formEncode(59)).isEqualTo("%3B"); - assertThat(formEncode(60)).isEqualTo("%3C"); - assertThat(formEncode(61)).isEqualTo("%3D"); - assertThat(formEncode(62)).isEqualTo("%3E"); - assertThat(formEncode(63)).isEqualTo("%3F"); - assertThat(formEncode(64)).isEqualTo("%40"); - assertThat(formEncode(65)).isEqualTo("A"); - assertThat(formEncode(90)).isEqualTo("Z"); - assertThat(formEncode(91)).isEqualTo("%5B"); - assertThat(formEncode(92)).isEqualTo("%5C"); - assertThat(formEncode(93)).isEqualTo("%5D"); - assertThat(formEncode(94)).isEqualTo("%5E"); - assertThat(formEncode(95)).isEqualTo("_"); - assertThat(formEncode(96)).isEqualTo("%60"); - assertThat(formEncode(97)).isEqualTo("a"); - assertThat(formEncode(122)).isEqualTo("z"); - assertThat(formEncode(123)).isEqualTo("%7B"); - assertThat(formEncode(124)).isEqualTo("%7C"); - assertThat(formEncode(125)).isEqualTo("%7D"); - assertThat(formEncode(126)).isEqualTo("%7E"); - assertThat(formEncode(127)).isEqualTo("%7F"); - assertThat(formEncode(128)).isEqualTo("%C2%80"); - assertThat(formEncode(255)).isEqualTo("%C3%BF"); - } - - private String formEncode(int codePoint) throws IOException { - // Wrap the codepoint with regular printable characters to prevent trimming. - FormBody body = new FormBody.Builder() - .add("a", new String(new int[] {'b', codePoint, 'c'}, 0, 3)) - .build(); - Buffer buffer = new Buffer(); - body.writeTo(buffer); - buffer.skip(3); // Skip "a=b" prefix. - return buffer.readUtf8(buffer.size() - 1); // Skip the "c" suffix. - } - - @Test public void manualCharset() throws Exception { - FormBody body = new FormBody.Builder(StandardCharsets.ISO_8859_1) - .add("name", "Nicolás") - .build(); - - String expected = "name=Nicol%E1s"; - assertThat(body.contentLength()).isEqualTo(expected.length()); - - Buffer out = new Buffer(); - body.writeTo(out); - assertThat(out.readUtf8()).isEqualTo(expected); - } -} diff --git a/okhttp/src/test/java/okhttp3/FormBodyTest.kt b/okhttp/src/test/java/okhttp3/FormBodyTest.kt new file mode 100644 index 000000000000..6e8f8a09720d --- /dev/null +++ b/okhttp/src/test/java/okhttp3/FormBodyTest.kt @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 + * + * 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 okhttp3 + +import java.io.IOException +import java.nio.charset.StandardCharsets +import okio.Buffer +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class FormBodyTest { + @Test + fun urlEncoding() { + val body = FormBody.Builder() + .add("a+=& b", "c+=& d") + .add("space, the", "final frontier") + .add("%25", "%25") + .build() + assertThat(body.size).isEqualTo(3) + assertThat(body.encodedName(0)).isEqualTo("a%2B%3D%26+b") + assertThat(body.encodedName(1)).isEqualTo("space%2C+the") + assertThat(body.encodedName(2)).isEqualTo("%2525") + assertThat(body.name(0)).isEqualTo("a+=& b") + assertThat(body.name(1)).isEqualTo("space, the") + assertThat(body.name(2)).isEqualTo("%25") + assertThat(body.encodedValue(0)).isEqualTo("c%2B%3D%26+d") + assertThat(body.encodedValue(1)).isEqualTo("final+frontier") + assertThat(body.encodedValue(2)).isEqualTo("%2525") + assertThat(body.value(0)).isEqualTo("c+=& d") + assertThat(body.value(1)).isEqualTo("final frontier") + assertThat(body.value(2)).isEqualTo("%25") + assertThat(body.contentType().toString()).isEqualTo( + "application/x-www-form-urlencoded" + ) + val expected = "a%2B%3D%26+b=c%2B%3D%26+d&space%2C+the=final+frontier&%2525=%2525" + assertThat(body.contentLength()).isEqualTo(expected.length.toLong()) + val out = Buffer() + body.writeTo(out) + assertThat(out.readUtf8()).isEqualTo(expected) + } + + @Test + fun addEncoded() { + val body = FormBody.Builder() + .addEncoded("a+=& b", "c+=& d") + .addEncoded("e+=& f", "g+=& h") + .addEncoded("%25", "%25") + .build() + val expected = "a+%3D%26+b=c+%3D%26+d&e+%3D%26+f=g+%3D%26+h&%25=%25" + val out = Buffer() + body.writeTo(out) + assertThat(out.readUtf8()).isEqualTo(expected) + } + + @Test + fun encodedPair() { + val body = FormBody.Builder() + .add("sim", "ple") + .build() + val expected = "sim=ple" + assertThat(body.contentLength()).isEqualTo(expected.length.toLong()) + val buffer = Buffer() + body.writeTo(buffer) + assertThat(buffer.readUtf8()).isEqualTo(expected) + } + + @Test + fun encodeMultiplePairs() { + val body = FormBody.Builder() + .add("sim", "ple") + .add("hey", "there") + .add("help", "me") + .build() + val expected = "sim=ple&hey=there&help=me" + assertThat(body.contentLength()).isEqualTo(expected.length.toLong()) + val buffer = Buffer() + body.writeTo(buffer) + assertThat(buffer.readUtf8()).isEqualTo(expected) + } + + @Test + fun buildEmptyForm() { + val body = FormBody.Builder().build() + val expected = "" + assertThat(body.contentLength()).isEqualTo(expected.length.toLong()) + val buffer = Buffer() + body.writeTo(buffer) + assertThat(buffer.readUtf8()).isEqualTo(expected) + } + + @Test + fun characterEncoding() { + // Browsers convert '\u0000' to '%EF%BF%BD'. + assertThat(formEncode(0)).isEqualTo("%00") + assertThat(formEncode(1)).isEqualTo("%01") + assertThat(formEncode(2)).isEqualTo("%02") + assertThat(formEncode(3)).isEqualTo("%03") + assertThat(formEncode(4)).isEqualTo("%04") + assertThat(formEncode(5)).isEqualTo("%05") + assertThat(formEncode(6)).isEqualTo("%06") + assertThat(formEncode(7)).isEqualTo("%07") + assertThat(formEncode(8)).isEqualTo("%08") + assertThat(formEncode(9)).isEqualTo("%09") + // Browsers convert '\n' to '\r\n' + assertThat(formEncode(10)).isEqualTo("%0A") + assertThat(formEncode(11)).isEqualTo("%0B") + assertThat(formEncode(12)).isEqualTo("%0C") + // Browsers convert '\r' to '\r\n' + assertThat(formEncode(13)).isEqualTo("%0D") + assertThat(formEncode(14)).isEqualTo("%0E") + assertThat(formEncode(15)).isEqualTo("%0F") + assertThat(formEncode(16)).isEqualTo("%10") + assertThat(formEncode(17)).isEqualTo("%11") + assertThat(formEncode(18)).isEqualTo("%12") + assertThat(formEncode(19)).isEqualTo("%13") + assertThat(formEncode(20)).isEqualTo("%14") + assertThat(formEncode(21)).isEqualTo("%15") + assertThat(formEncode(22)).isEqualTo("%16") + assertThat(formEncode(23)).isEqualTo("%17") + assertThat(formEncode(24)).isEqualTo("%18") + assertThat(formEncode(25)).isEqualTo("%19") + assertThat(formEncode(26)).isEqualTo("%1A") + assertThat(formEncode(27)).isEqualTo("%1B") + assertThat(formEncode(28)).isEqualTo("%1C") + assertThat(formEncode(29)).isEqualTo("%1D") + assertThat(formEncode(30)).isEqualTo("%1E") + assertThat(formEncode(31)).isEqualTo("%1F") + // Browsers use '+' for space. + assertThat(formEncode(32)).isEqualTo("+") + assertThat(formEncode(33)).isEqualTo("%21") + assertThat(formEncode(34)).isEqualTo("%22") + assertThat(formEncode(35)).isEqualTo("%23") + assertThat(formEncode(36)).isEqualTo("%24") + assertThat(formEncode(37)).isEqualTo("%25") + assertThat(formEncode(38)).isEqualTo("%26") + assertThat(formEncode(39)).isEqualTo("%27") + assertThat(formEncode(40)).isEqualTo("%28") + assertThat(formEncode(41)).isEqualTo("%29") + assertThat(formEncode(42)).isEqualTo("*") + assertThat(formEncode(43)).isEqualTo("%2B") + assertThat(formEncode(44)).isEqualTo("%2C") + assertThat(formEncode(45)).isEqualTo("-") + assertThat(formEncode(46)).isEqualTo(".") + assertThat(formEncode(47)).isEqualTo("%2F") + assertThat(formEncode(48)).isEqualTo("0") + assertThat(formEncode(57)).isEqualTo("9") + assertThat(formEncode(58)).isEqualTo("%3A") + assertThat(formEncode(59)).isEqualTo("%3B") + assertThat(formEncode(60)).isEqualTo("%3C") + assertThat(formEncode(61)).isEqualTo("%3D") + assertThat(formEncode(62)).isEqualTo("%3E") + assertThat(formEncode(63)).isEqualTo("%3F") + assertThat(formEncode(64)).isEqualTo("%40") + assertThat(formEncode(65)).isEqualTo("A") + assertThat(formEncode(90)).isEqualTo("Z") + assertThat(formEncode(91)).isEqualTo("%5B") + assertThat(formEncode(92)).isEqualTo("%5C") + assertThat(formEncode(93)).isEqualTo("%5D") + assertThat(formEncode(94)).isEqualTo("%5E") + assertThat(formEncode(95)).isEqualTo("_") + assertThat(formEncode(96)).isEqualTo("%60") + assertThat(formEncode(97)).isEqualTo("a") + assertThat(formEncode(122)).isEqualTo("z") + assertThat(formEncode(123)).isEqualTo("%7B") + assertThat(formEncode(124)).isEqualTo("%7C") + assertThat(formEncode(125)).isEqualTo("%7D") + assertThat(formEncode(126)).isEqualTo("%7E") + assertThat(formEncode(127)).isEqualTo("%7F") + assertThat(formEncode(128)).isEqualTo("%C2%80") + assertThat(formEncode(255)).isEqualTo("%C3%BF") + } + + @Throws(IOException::class) + private fun formEncode(codePoint: Int): String { + // Wrap the codepoint with regular printable characters to prevent trimming. + val body = FormBody.Builder() + .add("a", String(intArrayOf('b'.code, codePoint, 'c'.code), 0, 3)) + .build() + val buffer = Buffer() + body.writeTo(buffer) + buffer.skip(3) // Skip "a=b" prefix. + return buffer.readUtf8(buffer.size - 1) // Skip the "c" suffix. + } + + @Test + fun manualCharset() { + val body = FormBody.Builder(StandardCharsets.ISO_8859_1) + .add("name", "Nicolás") + .build() + val expected = "name=Nicol%E1s" + assertThat(body.contentLength()).isEqualTo(expected.length.toLong()) + val out = Buffer() + body.writeTo(out) + assertThat(out.readUtf8()).isEqualTo(expected) + } +} diff --git a/okhttp/src/test/java/okhttp3/ProtocolTest.java b/okhttp/src/test/java/okhttp3/ProtocolTest.java deleted file mode 100644 index 2384d4d2738a..000000000000 --- a/okhttp/src/test/java/okhttp3/ProtocolTest.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2018 Square, Inc. - * - * 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 - * - * 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 okhttp3; - -import java.io.IOException; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class ProtocolTest { - @Test - public void testGetKnown() throws IOException { - assertThat(Protocol.get("http/1.0")).isEqualTo(Protocol.HTTP_1_0); - assertThat(Protocol.get("http/1.1")).isEqualTo(Protocol.HTTP_1_1); - assertThat(Protocol.get("spdy/3.1")).isEqualTo(Protocol.SPDY_3); - assertThat(Protocol.get("h2")).isEqualTo(Protocol.HTTP_2); - assertThat(Protocol.get("h2_prior_knowledge")).isEqualTo(Protocol.H2_PRIOR_KNOWLEDGE); - assertThat(Protocol.get("quic")).isEqualTo(Protocol.QUIC); - assertThat(Protocol.get("h3")).isEqualTo(Protocol.HTTP_3); - assertThat(Protocol.get("h3-29")).isEqualTo(Protocol.HTTP_3); - } - - @Test - public void testGetUnknown() { - assertThrows(IOException.class, () -> Protocol.get("tcp")); - } - - @Test - public void testToString() { - assertThat(Protocol.HTTP_1_0.toString()).isEqualTo("http/1.0"); - assertThat(Protocol.HTTP_1_1.toString()).isEqualTo("http/1.1"); - assertThat(Protocol.SPDY_3.toString()).isEqualTo("spdy/3.1"); - assertThat(Protocol.HTTP_2.toString()).isEqualTo("h2"); - assertThat(Protocol.H2_PRIOR_KNOWLEDGE.toString()).isEqualTo("h2_prior_knowledge"); - assertThat(Protocol.QUIC.toString()).isEqualTo("quic"); - assertThat(Protocol.HTTP_3.toString()).isEqualTo("h3"); - } -} diff --git a/okhttp/src/test/java/okhttp3/ProtocolTest.kt b/okhttp/src/test/java/okhttp3/ProtocolTest.kt new file mode 100644 index 000000000000..408dbc75b33c --- /dev/null +++ b/okhttp/src/test/java/okhttp3/ProtocolTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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 + * + * 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 okhttp3 + +import java.io.IOException +import okhttp3.Protocol.Companion.get +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test + +class ProtocolTest { + @Test + fun testGetKnown() { + assertThat(get("http/1.0")).isEqualTo(Protocol.HTTP_1_0) + assertThat(get("http/1.1")).isEqualTo(Protocol.HTTP_1_1) + assertThat(get("spdy/3.1")).isEqualTo(Protocol.SPDY_3) + assertThat(get("h2")).isEqualTo(Protocol.HTTP_2) + assertThat(get("h2_prior_knowledge")).isEqualTo(Protocol.H2_PRIOR_KNOWLEDGE) + assertThat(get("quic")).isEqualTo(Protocol.QUIC) + assertThat(get("h3")).isEqualTo(Protocol.HTTP_3) + assertThat(get("h3-29")).isEqualTo(Protocol.HTTP_3) + } + + @Test + fun testGetUnknown() { + assertThrows(IOException::class.java) { get("tcp") } + } + + @Test + fun testToString() { + assertThat(Protocol.HTTP_1_0.toString()).isEqualTo("http/1.0") + assertThat(Protocol.HTTP_1_1.toString()).isEqualTo("http/1.1") + assertThat(Protocol.SPDY_3.toString()).isEqualTo("spdy/3.1") + assertThat(Protocol.HTTP_2.toString()).isEqualTo("h2") + assertThat(Protocol.H2_PRIOR_KNOWLEDGE.toString()) + .isEqualTo("h2_prior_knowledge") + assertThat(Protocol.QUIC.toString()).isEqualTo("quic") + assertThat(Protocol.HTTP_3.toString()).isEqualTo("h3") + } +} diff --git a/okhttp/src/test/java/okhttp3/PublicInternalApiTest.java b/okhttp/src/test/java/okhttp3/PublicInternalApiTest.java deleted file mode 100644 index 44d73d35a843..000000000000 --- a/okhttp/src/test/java/okhttp3/PublicInternalApiTest.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2019 Square, Inc. - * - * 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 - * - * 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 okhttp3; - -import okhttp3.internal.http.HttpHeaders; -import okhttp3.internal.http.HttpMethod; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@SuppressWarnings("ALL") public class PublicInternalApiTest { - @Test public void permitsRequestBody() { - assertTrue(HttpMethod.permitsRequestBody("POST")); - assertFalse(HttpMethod.permitsRequestBody("GET")); - } - - @Test public void requiresRequestBody() { - assertTrue(HttpMethod.requiresRequestBody("PUT")); - assertFalse(HttpMethod.requiresRequestBody("GET")); - } - - @Test public void hasBody() { - Request request = new Request.Builder().url("http://example.com").build(); - Response response = new Response.Builder().code(200) - .message("OK") - .request(request) - .protocol(Protocol.HTTP_2) - .build(); - assertTrue(HttpHeaders.hasBody(response)); - } -} diff --git a/okhttp/src/test/java/okhttp3/PublicInternalApiTest.kt b/okhttp/src/test/java/okhttp3/PublicInternalApiTest.kt new file mode 100644 index 000000000000..29acba8738c5 --- /dev/null +++ b/okhttp/src/test/java/okhttp3/PublicInternalApiTest.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * 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 + * + * 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 okhttp3 + +import okhttp3.internal.http.HttpMethod.permitsRequestBody +import okhttp3.internal.http.HttpMethod.requiresRequestBody +import okhttp3.internal.http.hasBody +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +@Suppress("DEPRECATION_ERROR") +class PublicInternalApiTest { + @Test + fun permitsRequestBody() { + assertTrue(permitsRequestBody("POST")) + assertFalse(permitsRequestBody("GET")) + } + + @Test + fun requiresRequestBody() { + assertTrue(requiresRequestBody("PUT")) + assertFalse(requiresRequestBody("GET")) + } + + @Test + fun hasBody() { + val request = Request.Builder().url("http://example.com").build() + val response = Response.Builder().code(200) + .message("OK") + .request(request) + .protocol(Protocol.HTTP_2) + .build() + assertTrue(hasBody(response)) + } +} diff --git a/okhttp/src/test/java/okhttp3/RecordedResponse.java b/okhttp/src/test/java/okhttp3/RecordedResponse.java deleted file mode 100644 index e27422ee5f8a..000000000000 --- a/okhttp/src/test/java/okhttp3/RecordedResponse.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright (C) 2013 Square, Inc. - * - * 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 - * - * 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 okhttp3; - -import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.Date; -import javax.annotation.Nullable; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * A received response or failure recorded by the response recorder. - */ -public final class RecordedResponse { - public final Request request; - public final @Nullable Response response; - public final @Nullable WebSocket webSocket; - public final @Nullable String body; - public final @Nullable IOException failure; - - public RecordedResponse(Request request, @Nullable Response response, - @Nullable WebSocket webSocket, @Nullable String body, @Nullable IOException failure) { - this.request = request; - this.response = response; - this.webSocket = webSocket; - this.body = body; - this.failure = failure; - } - - public RecordedResponse assertRequestUrl(HttpUrl url) { - assertThat(request.url()).isEqualTo(url); - return this; - } - - public RecordedResponse assertRequestMethod(String method) { - assertThat(request.method()).isEqualTo(method); - return this; - } - - public RecordedResponse assertRequestHeader(String name, String... values) { - assertThat(request.headers(name)).containsExactly(values); - return this; - } - - public RecordedResponse assertCode(int expectedCode) { - assertThat(response == null ? null : response.code()).isEqualTo(expectedCode); - return this; - } - - public RecordedResponse assertSuccessful() { - assertThat(failure).isNull(); - assertThat(response.isSuccessful()).isTrue(); - return this; - } - - public RecordedResponse assertNotSuccessful() { - assertThat(response.isSuccessful()).isFalse(); - return this; - } - - public RecordedResponse assertHeader(String name, String... values) { - assertThat(response.headers(name)).containsExactly(values); - return this; - } - - public RecordedResponse assertHeaders(Headers headers) { - assertThat(response.headers()).isEqualTo(headers); - return this; - } - - public RecordedResponse assertBody(String expectedBody) { - assertThat(body).isEqualTo(expectedBody); - return this; - } - - public RecordedResponse assertHandshake() { - Handshake handshake = response.handshake(); - assertThat(handshake.tlsVersion()).isNotNull(); - assertThat(handshake.cipherSuite()).isNotNull(); - assertThat(handshake.peerPrincipal()).isNotNull(); - assertThat(handshake.peerCertificates().size()).isEqualTo(1); - assertThat(handshake.localPrincipal()).isNull(); - assertThat(handshake.localCertificates().size()).isEqualTo(0); - return this; - } - - /** - * Asserts that the current response was redirected and returns the prior response. - */ - public RecordedResponse priorResponse() { - Response priorResponse = response.priorResponse(); - assertThat(priorResponse).isNotNull(); - return new RecordedResponse(priorResponse.request(), priorResponse, null, null, null); - } - - /** - * Asserts that the current response used the network and returns the network response. - */ - public RecordedResponse networkResponse() { - Response networkResponse = response.networkResponse(); - assertThat(networkResponse).isNotNull(); - return new RecordedResponse(networkResponse.request(), networkResponse, null, null, null); - } - - /** Asserts that the current response didn't use the network. */ - public RecordedResponse assertNoNetworkResponse() { - assertThat(response.networkResponse()).isNull(); - return this; - } - - /** Asserts that the current response didn't use the cache. */ - public RecordedResponse assertNoCacheResponse() { - assertThat(response.cacheResponse()).isNull(); - return this; - } - - /** - * Asserts that the current response used the cache and returns the cache response. - */ - public RecordedResponse cacheResponse() { - Response cacheResponse = response.cacheResponse(); - assertThat(cacheResponse).isNotNull(); - return new RecordedResponse(cacheResponse.request(), cacheResponse, null, null, null); - } - - public RecordedResponse assertFailure(Class... allowedExceptionTypes) { - boolean found = false; - for (Class expectedClass : allowedExceptionTypes) { - if (expectedClass.isInstance(failure)) { - found = true; - break; - } - } - assertThat(found) - .overridingErrorMessage("Expected exception type among " - + Arrays.toString(allowedExceptionTypes) + ", got " + failure) - .isTrue(); - return this; - } - - public RecordedResponse assertFailure(String... messages) { - assertThat(failure).overridingErrorMessage("No failure found").isNotNull(); - assertThat(messages).contains(failure.getMessage()); - return this; - } - - public RecordedResponse assertFailureMatches(String... patterns) { - assertThat(failure).isNotNull(); - for (String pattern : patterns) { - if (failure.getMessage().matches(pattern)) return this; - } - throw new AssertionError(failure.getMessage()); - } - - public RecordedResponse assertSentRequestAtMillis(long minimum, long maximum) { - assertDateInRange(minimum, response.sentRequestAtMillis(), maximum); - return this; - } - - public RecordedResponse assertReceivedResponseAtMillis(long minimum, long maximum) { - assertDateInRange(minimum, response.receivedResponseAtMillis(), maximum); - return this; - } - - private void assertDateInRange(long minimum, long actual, long maximum) { - assertThat(actual) - .overridingErrorMessage("%s <= %s <= %s", format(minimum), format(actual), format(maximum)) - .isBetween(minimum, maximum); - - } - - private String format(long time) { - return new SimpleDateFormat("HH:mm:ss.SSS").format(new Date(time)); - } - - public String getBody() { - return body; - } -} diff --git a/okhttp/src/test/java/okhttp3/RecordedResponse.kt b/okhttp/src/test/java/okhttp3/RecordedResponse.kt new file mode 100644 index 000000000000..90ad85c8fbf6 --- /dev/null +++ b/okhttp/src/test/java/okhttp3/RecordedResponse.kt @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * 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 + * + * 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 okhttp3 + +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.Date +import org.assertj.core.api.Assertions.assertThat + +/** + * A received response or failure recorded by the response recorder. + */ +class RecordedResponse( + @JvmField val request: Request, + val response: Response?, + val webSocket: WebSocket?, + val body: String?, + val failure: IOException? +) { + fun assertRequestUrl(url: HttpUrl) = apply { + assertThat(request.url).isEqualTo(url) + } + + fun assertRequestMethod(method: String) = apply { + assertThat(request.method).isEqualTo(method) + } + + fun assertRequestHeader(name: String, vararg values: String) = apply { + assertThat(request.headers(name)).containsExactly(*values) + } + + fun assertCode(expectedCode: Int) = apply { + assertThat(response!!.code).isEqualTo(expectedCode) + } + + fun assertSuccessful() = apply { + assertThat(failure).isNull() + assertThat(response!!.isSuccessful).isTrue() + } + + fun assertNotSuccessful() = apply { + assertThat(response!!.isSuccessful).isFalse() + } + + fun assertHeader(name: String, vararg values: String?) = apply { + assertThat(response!!.headers(name)).containsExactly(*values) + } + + fun assertHeaders(headers: Headers) = apply { + assertThat(response!!.headers).isEqualTo(headers) + } + + fun assertBody(expectedBody: String) = apply { + assertThat(body).isEqualTo(expectedBody) + } + + fun assertHandshake() = apply { + val handshake = response!!.handshake!! + assertThat(handshake.tlsVersion).isNotNull() + assertThat(handshake.cipherSuite).isNotNull() + assertThat(handshake.peerPrincipal).isNotNull() + assertThat(handshake.peerCertificates.size).isEqualTo(1) + assertThat(handshake.localPrincipal).isNull() + assertThat(handshake.localCertificates.size).isEqualTo(0) + } + + /** + * Asserts that the current response was redirected and returns the prior response. + */ + fun priorResponse(): RecordedResponse { + val priorResponse = response!!.priorResponse!! + return RecordedResponse(priorResponse.request, priorResponse, null, null, null) + } + + /** + * Asserts that the current response used the network and returns the network response. + */ + fun networkResponse(): RecordedResponse { + val networkResponse = response!!.networkResponse!! + return RecordedResponse(networkResponse.request, networkResponse, null, null, null) + } + + /** Asserts that the current response didn't use the network. */ + fun assertNoNetworkResponse() = apply { + assertThat(response!!.networkResponse).isNull() + } + + /** Asserts that the current response didn't use the cache. */ + fun assertNoCacheResponse() = apply { + assertThat(response!!.cacheResponse).isNull() + } + + /** + * Asserts that the current response used the cache and returns the cache response. + */ + fun cacheResponse(): RecordedResponse { + val cacheResponse = response!!.cacheResponse!! + return RecordedResponse(cacheResponse.request, cacheResponse, null, null, null) + } + + fun assertFailure(vararg allowedExceptionTypes: Class<*>) = apply { + var found = false + for (expectedClass in allowedExceptionTypes) { + if (expectedClass.isInstance(failure)) { + found = true + break + } + } + assertThat(found) + .overridingErrorMessage( + "Expected exception type among " + + allowedExceptionTypes.contentToString() + ", got " + failure + ) + .isTrue() + } + + fun assertFailure(vararg messages: String) = apply { + assertThat(failure).overridingErrorMessage("No failure found").isNotNull() + assertThat(messages).contains(failure!!.message) + } + + fun assertFailureMatches(vararg patterns: String) = apply { + val message = failure!!.message!! + assertThat(patterns.firstOrNull { pattern -> + message.matches(pattern.toRegex()) + }).isNotNull() + } + + fun assertSentRequestAtMillis(minimum: Long, maximum: Long) = apply { + assertDateInRange(minimum, response!!.sentRequestAtMillis, maximum) + } + + fun assertReceivedResponseAtMillis(minimum: Long, maximum: Long) = apply { + assertDateInRange(minimum, response!!.receivedResponseAtMillis, maximum) + } + + private fun assertDateInRange(minimum: Long, actual: Long, maximum: Long) { + assertThat(actual) + .overridingErrorMessage("${format(minimum)} <= ${format(actual)} <= ${format(maximum)}") + .isBetween(minimum, maximum) + } + + private fun format(time: Long) = SimpleDateFormat("HH:mm:ss.SSS").format(Date(time)) +} diff --git a/okhttp/src/test/java/okhttp3/RecordingCallback.java b/okhttp/src/test/java/okhttp3/RecordingCallback.java deleted file mode 100644 index 56a1d7e0036f..000000000000 --- a/okhttp/src/test/java/okhttp3/RecordingCallback.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2013 Square, Inc. - * - * 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 - * - * 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 okhttp3; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.TimeUnit; - -/** - * Records received HTTP responses so they can be later retrieved by tests. - */ -public class RecordingCallback implements Callback { - public static final long TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10); - - private final List responses = new ArrayList<>(); - - @Override public synchronized void onFailure(Call call, IOException e) { - responses.add(new RecordedResponse(call.request(), null, null, null, e)); - notifyAll(); - } - - @Override public synchronized void onResponse(Call call, Response response) throws IOException { - String body = response.body().string(); - responses.add(new RecordedResponse(call.request(), response, null, body, null)); - notifyAll(); - } - - /** - * Returns the recorded response triggered by {@code request}. Throws if the response isn't - * enqueued before the timeout. - */ - public synchronized RecordedResponse await(HttpUrl url) throws Exception { - long timeoutMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) + TIMEOUT_MILLIS; - while (true) { - for (Iterator i = responses.iterator(); i.hasNext(); ) { - RecordedResponse recordedResponse = i.next(); - if (recordedResponse.request.url().equals(url)) { - i.remove(); - return recordedResponse; - } - } - - long nowMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()); - if (nowMillis >= timeoutMillis) break; - wait(timeoutMillis - nowMillis); - } - - throw new AssertionError("Timed out waiting for response to " + url); - } -} diff --git a/okhttp/src/test/java/okhttp3/RecordingCallback.kt b/okhttp/src/test/java/okhttp3/RecordingCallback.kt new file mode 100644 index 000000000000..e60e46205391 --- /dev/null +++ b/okhttp/src/test/java/okhttp3/RecordingCallback.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * 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 + * + * 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 okhttp3 + +import java.io.IOException +import java.util.concurrent.TimeUnit + +/** + * Records received HTTP responses so they can be later retrieved by tests. + */ +class RecordingCallback : Callback { + private val responses = mutableListOf() + + @Synchronized + override fun onFailure(call: Call, e: IOException) { + responses.add(RecordedResponse(call.request(), null, null, null, e)) + (this as Object).notifyAll() + } + + @Synchronized + override fun onResponse(call: Call, response: Response) { + val body = response.body.string() + responses.add(RecordedResponse(call.request(), response, null, body, null)) + (this as Object).notifyAll() + } + + /** + * Returns the recorded response triggered by `request`. Throws if the response isn't + * enqueued before the timeout. + */ + @Synchronized + fun await(url: HttpUrl): RecordedResponse { + val timeoutMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) + TIMEOUT_MILLIS + while (true) { + val i = responses.iterator() + while (i.hasNext()) { + val recordedResponse = i.next() + if (recordedResponse.request.url.equals(url)) { + i.remove() + return recordedResponse + } + } + val nowMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) + if (nowMillis >= timeoutMillis) break + (this as Object).wait(timeoutMillis - nowMillis) + } + + throw AssertionError("Timed out waiting for response to $url") + } + + companion object { + val TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10) + } +} diff --git a/okhttp/src/test/java/okhttp3/RecordingWebSocketListener.java b/okhttp/src/test/java/okhttp3/RecordingWebSocketListener.java deleted file mode 100644 index 45a13c47864e..000000000000 --- a/okhttp/src/test/java/okhttp3/RecordingWebSocketListener.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2018 Square, Inc. - * - * 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 - * - * 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 okhttp3; - -import javax.annotation.Nullable; -import okio.ByteString; - -public final class RecordingWebSocketListener extends WebSocketListener { - @Override public void onOpen(WebSocket webSocket, Response response) { - // TODO - } - - @Override public void onMessage(WebSocket webSocket, String text) { - // TODO - } - - @Override public void onMessage(WebSocket webSocket, ByteString bytes) { - // TODO - } - - @Override public void onClosing(WebSocket webSocket, int code, String reason) { - // TODO - } - - @Override public void onClosed(WebSocket webSocket, int code, String reason) { - // TODO - } - - @Override public void onFailure(WebSocket webSocket, Throwable t, @Nullable Response response) { - // TODO - } -} diff --git a/okhttp/src/test/java/okhttp3/SocksProxyTest.java b/okhttp/src/test/java/okhttp3/SocksProxyTest.java deleted file mode 100644 index ff99fb55dd47..000000000000 --- a/okhttp/src/test/java/okhttp3/SocksProxyTest.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 - * - * 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 okhttp3; - -import java.io.IOException; -import java.net.Proxy; -import java.net.ProxySelector; -import java.net.SocketAddress; -import java.net.URI; -import java.util.Collections; -import java.util.List; -import mockwebserver3.MockResponse; -import mockwebserver3.MockWebServer; -import okhttp3.testing.PlatformRule; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import static org.assertj.core.api.Assertions.assertThat; - -public final class SocksProxyTest { - @RegisterExtension public final PlatformRule platform = new PlatformRule(); - @RegisterExtension public final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule(); - - private MockWebServer server; - private final SocksProxy socksProxy = new SocksProxy(); - - @BeforeEach public void setUp(MockWebServer server) throws Exception { - this.server = server; - socksProxy.play(); - } - - @AfterEach public void tearDown() throws Exception { - socksProxy.shutdown(); - } - - @Test public void proxy() throws Exception { - server.enqueue(new MockResponse.Builder().body("abc").build()); - server.enqueue(new MockResponse.Builder().body("def").build()); - - OkHttpClient client = clientTestRule.newClientBuilder() - .proxy(socksProxy.proxy()) - .build(); - - Request request1 = new Request.Builder().url(server.url("/")).build(); - Response response1 = client.newCall(request1).execute(); - assertThat(response1.body().string()).isEqualTo("abc"); - - Request request2 = new Request.Builder().url(server.url("/")).build(); - Response response2 = client.newCall(request2).execute(); - assertThat(response2.body().string()).isEqualTo("def"); - - // The HTTP calls should share a single connection. - assertThat(socksProxy.connectionCount()).isEqualTo(1); - } - - @Test public void proxySelector() throws Exception { - server.enqueue(new MockResponse.Builder().body("abc").build()); - - ProxySelector proxySelector = new ProxySelector() { - @Override public List select(URI uri) { - return Collections.singletonList(socksProxy.proxy()); - } - - @Override public void connectFailed(URI uri, SocketAddress socketAddress, IOException e) { - throw new AssertionError(); - } - }; - - OkHttpClient client = clientTestRule.newClientBuilder() - .proxySelector(proxySelector) - .build(); - - Request request = new Request.Builder().url(server.url("/")).build(); - Response response = client.newCall(request).execute(); - assertThat(response.body().string()).isEqualTo("abc"); - - assertThat(socksProxy.connectionCount()).isEqualTo(1); - } - - @Test public void checkRemoteDNSResolve() throws Exception { - // This testcase will fail if the target is resolved locally instead of through the proxy. - server.enqueue(new MockResponse.Builder().body("abc").build()); - - OkHttpClient client = clientTestRule.newClientBuilder() - .proxy(socksProxy.proxy()) - .build(); - - HttpUrl url = server.url("/") - .newBuilder() - .host(SocksProxy.HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS) - .build(); - - Request request = new Request.Builder().url(url).build(); - Response response1 = client.newCall(request).execute(); - assertThat(response1.body().string()).isEqualTo("abc"); - - assertThat(socksProxy.connectionCount()).isEqualTo(1); - } -} diff --git a/okhttp/src/test/java/okhttp3/SocksProxyTest.kt b/okhttp/src/test/java/okhttp3/SocksProxyTest.kt new file mode 100644 index 000000000000..8d76986a7f4c --- /dev/null +++ b/okhttp/src/test/java/okhttp3/SocksProxyTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 + * + * 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 okhttp3 + +import java.io.IOException +import java.net.ProxySelector +import java.net.SocketAddress +import java.net.URI +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import okhttp3.testing.PlatformRule +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class SocksProxyTest { + @RegisterExtension + val platform = PlatformRule() + + @RegisterExtension + val clientTestRule = OkHttpClientTestRule() + private lateinit var server: MockWebServer + private val socksProxy = SocksProxy() + + @BeforeEach + fun setUp(server: MockWebServer) { + this.server = server + socksProxy.play() + } + + @AfterEach + fun tearDown() { + socksProxy.shutdown() + } + + @Test + fun proxy() { + server.enqueue(MockResponse.Builder().body("abc").build()) + server.enqueue(MockResponse.Builder().body("def").build()) + val client = clientTestRule.newClientBuilder() + .proxy(socksProxy.proxy()) + .build() + val request1 = Request.Builder().url(server.url("/")).build() + val response1 = client.newCall(request1).execute() + assertThat(response1.body.string()).isEqualTo("abc") + val request2 = Request.Builder().url(server.url("/")).build() + val response2 = client.newCall(request2).execute() + assertThat(response2.body.string()).isEqualTo("def") + + // The HTTP calls should share a single connection. + assertThat(socksProxy.connectionCount()).isEqualTo(1) + } + + @Test + fun proxySelector() { + server.enqueue(MockResponse.Builder().body("abc").build()) + val proxySelector: ProxySelector = object : ProxySelector() { + override fun select(uri: URI) = listOf(socksProxy.proxy()) + + override fun connectFailed( + uri: URI, + socketAddress: SocketAddress, + e: IOException, + ) = error("unexpected call") + } + val client = clientTestRule.newClientBuilder() + .proxySelector(proxySelector) + .build() + val request = Request.Builder().url(server.url("/")).build() + val response = client.newCall(request).execute() + assertThat(response.body.string()).isEqualTo("abc") + assertThat(socksProxy.connectionCount()).isEqualTo(1) + } + + @Test + fun checkRemoteDNSResolve() { + // This testcase will fail if the target is resolved locally instead of through the proxy. + server.enqueue(MockResponse.Builder().body("abc").build()) + val client = clientTestRule.newClientBuilder() + .proxy(socksProxy.proxy()) + .build() + val url = server.url("/") + .newBuilder() + .host(SocksProxy.HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS) + .build() + val request = Request.Builder().url(url).build() + val response1 = client.newCall(request).execute() + assertThat(response1.body.string()).isEqualTo("abc") + assertThat(socksProxy.connectionCount()).isEqualTo(1) + } +} diff --git a/okhttp/src/test/java/okhttp3/TestLogHandler.java b/okhttp/src/test/java/okhttp3/TestLogHandler.java deleted file mode 100644 index 8115903426c8..000000000000 --- a/okhttp/src/test/java/okhttp3/TestLogHandler.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 - * - * 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 okhttp3; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.logging.Handler; -import java.util.logging.Level; -import java.util.logging.LogRecord; -import java.util.logging.Logger; -import org.junit.jupiter.api.extension.AfterEachCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; - -/** - * A log handler that records which log messages were published so that a calling test can make - * assertions about them. - */ -public final class TestLogHandler implements TestRule, BeforeEachCallback, AfterEachCallback { - private final Logger logger; - private final BlockingQueue logs = new LinkedBlockingQueue<>(); - - private final Handler handler = new Handler() { - @Override public void publish(LogRecord logRecord) { - logs.add(logRecord.getLevel() + ": " + logRecord.getMessage()); - } - - @Override public void flush() { - } - - @Override public void close() throws SecurityException { - } - }; - - private Level previousLevel; - - public TestLogHandler(Class loggerName) { - logger = Logger.getLogger(loggerName.getName()); - } - - public TestLogHandler(Logger logger) { - this.logger = logger; - } - - @Override public void beforeEach(ExtensionContext context) { - previousLevel = logger.getLevel(); - logger.addHandler(handler); - logger.setLevel(Level.FINEST); - } - - @Override public void afterEach(ExtensionContext context) { - logger.setLevel(previousLevel); - logger.removeHandler(handler); - } - - @Override public Statement apply(Statement base, Description description) { - return new Statement() { - @Override public void evaluate() throws Throwable { - beforeEach(null); - try { - base.evaluate(); - } finally { - afterEach(null); - } - } - }; - } - - public List takeAll() { - List list = new ArrayList<>(); - logs.drainTo(list); - return list; - } - - public String take() throws Exception { - String message = logs.poll(10, TimeUnit.SECONDS); - if (message == null) { - throw new AssertionError("Timed out waiting for log message."); - } - return message; - } -} diff --git a/okhttp/src/test/java/okhttp3/TestLogHandler.kt b/okhttp/src/test/java/okhttp3/TestLogHandler.kt new file mode 100644 index 000000000000..9e0d8efa8b35 --- /dev/null +++ b/okhttp/src/test/java/okhttp3/TestLogHandler.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 + * + * 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 okhttp3 + +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit +import java.util.logging.Handler +import java.util.logging.Level +import java.util.logging.LogRecord +import java.util.logging.Logger +import org.junit.jupiter.api.extension.AfterEachCallback +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * A log handler that records which log messages were published so that a calling test can make + * assertions about them. + */ +class TestLogHandler( + private val logger: Logger, +) : TestRule, BeforeEachCallback, AfterEachCallback { + constructor(loggerName: Class<*>) : this(Logger.getLogger(loggerName.getName())) + + private val logs = LinkedBlockingQueue() + + private val handler = object : Handler() { + override fun publish(logRecord: LogRecord) { + logs += "${logRecord.level}: ${logRecord.message}" + } + + override fun flush() { + } + + override fun close() { + } + } + + private var previousLevel: Level? = null + + override fun beforeEach(context: ExtensionContext?) { + previousLevel = logger.level + logger.addHandler(handler) + logger.setLevel(Level.FINEST) + } + + override fun afterEach(context: ExtensionContext?) { + logger.setLevel(previousLevel) + logger.removeHandler(handler) + } + + override fun apply( + base: Statement, + description: Description, + ): Statement { + return object : Statement() { + override fun evaluate() { + beforeEach(null) + try { + base.evaluate() + } finally { + afterEach(null) + } + } + } + } + + fun takeAll(): List { + val list = mutableListOf() + logs.drainTo(list) + return list + } + + fun take(): String { + return logs.poll(10, TimeUnit.SECONDS) + ?: throw AssertionError("Timed out waiting for log message.") + } +} diff --git a/okhttp/src/test/java/okhttp3/TestTls13Request.java b/okhttp/src/test/java/okhttp3/TestTls13Request.java deleted file mode 100644 index cfd1693b8e40..000000000000 --- a/okhttp/src/test/java/okhttp3/TestTls13Request.java +++ /dev/null @@ -1,106 +0,0 @@ -package okhttp3; - -import java.io.IOException; -import java.security.Security; -import java.util.List; -import okhttp3.internal.platform.Platform; -import org.conscrypt.Conscrypt; - -import static java.util.Arrays.asList; - -public class TestTls13Request { - - // TLS 1.3 - private static final CipherSuite[] TLS13_CIPHER_SUITES = new CipherSuite[] { - CipherSuite.TLS_AES_128_GCM_SHA256, - CipherSuite.TLS_AES_256_GCM_SHA384, - CipherSuite.TLS_CHACHA20_POLY1305_SHA256, - CipherSuite.TLS_AES_128_CCM_SHA256, - CipherSuite.TLS_AES_128_CCM_8_SHA256 - }; - - /** - * A TLS 1.3 only Connection Spec. This will be eventually be exposed - * as part of MODERN_TLS or folded into the default OkHttp client once published and - * available in JDK11 or Conscrypt. - */ - private static final ConnectionSpec TLS_13 = new ConnectionSpec.Builder(true) - .cipherSuites(TLS13_CIPHER_SUITES) - .tlsVersions(TlsVersion.TLS_1_3) - .build(); - - - private static final ConnectionSpec TLS_12 = - new ConnectionSpec.Builder(ConnectionSpec.RESTRICTED_TLS).tlsVersions(TlsVersion.TLS_1_2) - .build(); - - private TestTls13Request() { - } - - public static void main(String[] args) { - //System.setProperty("javax.net.debug", "ssl:handshake:verbose"); - Security.insertProviderAt(Conscrypt.newProviderBuilder().provideTrustManager().build(), 1); - - System.out.println( - "Running tests using " + Platform.get() + " " + System.getProperty("java.vm.version")); - - // https://github.com/tlswg/tls13-spec/wiki/Implementations - List urls = - asList("https://enabled.tls13.com", "https://www.howsmyssl.com/a/check", - "https://tls13.cloudflare.com", "https://www.allizom.org/robots.txt", - "https://tls13.crypto.mozilla.org/", "https://tls.ctf.network/robots.txt", - "https://rustls.jbp.io/", "https://h2o.examp1e.net", "https://mew.org/", - "https://tls13.baishancloud.com/", "https://tls13.akamai.io/", "https://swifttls.org/", - "https://www.googleapis.com/robots.txt", "https://graph.facebook.com/robots.txt", - "https://api.twitter.com/robots.txt", "https://connect.squareup.com/robots.txt"); - - System.out.println("TLS1.3+TLS1.2"); - testClient(urls, buildClient(ConnectionSpec.RESTRICTED_TLS)); - - System.out.println("\nTLS1.3 only"); - testClient(urls, buildClient(TLS_13)); - - System.out.println("\nTLS1.3 then fallback"); - testClient(urls, buildClient(TLS_13, TLS_12)); - } - - private static void testClient(List urls, OkHttpClient client) { - try { - for (String url : urls) { - sendRequest(client, url); - } - } finally { - client.dispatcher().executorService().shutdownNow(); - client.connectionPool().evictAll(); - } - } - - private static OkHttpClient buildClient(ConnectionSpec... specs) { - return new OkHttpClient.Builder().connectionSpecs(asList(specs)).build(); - } - - private static void sendRequest(OkHttpClient client, String url) { - System.out.printf("%-40s ", url); - System.out.flush(); - - System.out.println(Platform.get()); - - Request request = new Request.Builder().url(url).build(); - - try (Response response = client.newCall(request).execute()) { - Handshake handshake = response.handshake(); - System.out.println(handshake.tlsVersion() - + " " - + handshake.cipherSuite() - + " " - + response.protocol() - + " " - + response.code() - + " " - + response.body().bytes().length - + "b"); - } catch (IOException ioe) { - System.out.println(ioe); - } - } -} diff --git a/okhttp/src/test/java/okhttp3/TestTls13Request.kt b/okhttp/src/test/java/okhttp3/TestTls13Request.kt new file mode 100644 index 000000000000..a5d1862799b2 --- /dev/null +++ b/okhttp/src/test/java/okhttp3/TestTls13Request.kt @@ -0,0 +1,101 @@ +package okhttp3 + +import java.io.IOException +import java.security.Security +import okhttp3.internal.platform.Platform +import org.conscrypt.Conscrypt + +// TLS 1.3 +private val TLS13_CIPHER_SUITES = listOf( + CipherSuite.TLS_AES_128_GCM_SHA256, + CipherSuite.TLS_AES_256_GCM_SHA384, + CipherSuite.TLS_CHACHA20_POLY1305_SHA256, + CipherSuite.TLS_AES_128_CCM_SHA256, + CipherSuite.TLS_AES_128_CCM_8_SHA256 +) + +/** + * A TLS 1.3 only Connection Spec. This will be eventually be exposed + * as part of MODERN_TLS or folded into the default OkHttp client once published and + * available in JDK11 or Conscrypt. + */ +private val TLS_13 = ConnectionSpec.Builder(true) + .cipherSuites(*TLS13_CIPHER_SUITES.toTypedArray()) + .tlsVersions(TlsVersion.TLS_1_3) + .build() + +private val TLS_12 = ConnectionSpec.Builder(ConnectionSpec.RESTRICTED_TLS) + .tlsVersions(TlsVersion.TLS_1_2) + .build() + +private fun testClient(urls: List, client: OkHttpClient) { + try { + for (url in urls) { + sendRequest(client, url) + } + } finally { + client.dispatcher.executorService.shutdownNow() + client.connectionPool.evictAll() + } +} + +private fun buildClient(vararg specs: ConnectionSpec): OkHttpClient { + return OkHttpClient.Builder() + .connectionSpecs(listOf(*specs)) + .build() +} + +private fun sendRequest(client: OkHttpClient, url: String) { + System.out.printf("%-40s ", url) + System.out.flush() + println(Platform.get()) + val request = Request.Builder() + .url(url) + .build() + try { + client.newCall(request).execute().use { response -> + val handshake = response.handshake + println( + "${handshake!!.tlsVersion} ${handshake.cipherSuite} ${response.protocol} " + + "${response.code} ${response.body.bytes().size}b" + ) + } + } catch (ioe: IOException) { + println(ioe) + } +} + +fun main(vararg args: String) { + // System.setProperty("javax.net.debug", "ssl:handshake:verbose"); + Security.insertProviderAt(Conscrypt.newProviderBuilder().provideTrustManager().build(), 1) + println("Running tests using ${Platform.get()} ${System.getProperty("java.vm.version")}") + + // https://github.com/tlswg/tls13-spec/wiki/Implementations + val urls = listOf( + "https://enabled.tls13.com", + "https://www.howsmyssl.com/a/check", + "https://tls13.cloudflare.com", + "https://www.allizom.org/robots.txt", + "https://tls13.crypto.mozilla.org/", + "https://tls.ctf.network/robots.txt", + "https://rustls.jbp.io/", + "https://h2o.examp1e.net", + "https://mew.org/", + "https://tls13.baishancloud.com/", + "https://tls13.akamai.io/", + "https://swifttls.org/", + "https://www.googleapis.com/robots.txt", + "https://graph.facebook.com/robots.txt", + "https://api.twitter.com/robots.txt", + "https://connect.squareup.com/robots.txt", + ) + + println("TLS1.3+TLS1.2") + testClient(urls, buildClient(ConnectionSpec.RESTRICTED_TLS)) + + println("\nTLS1.3 only") + testClient(urls, buildClient(TLS_13)) + + println("\nTLS1.3 then fallback") + testClient(urls, buildClient(TLS_13, TLS_12)) +} diff --git a/okhttp/src/test/java/okhttp3/internal/DoubleInetAddressDns.java b/okhttp/src/test/java/okhttp3/internal/DoubleInetAddressDns.kt similarity index 64% rename from okhttp/src/test/java/okhttp3/internal/DoubleInetAddressDns.java rename to okhttp/src/test/java/okhttp3/internal/DoubleInetAddressDns.kt index ac42c15c7f1c..4ed9153a9152 100644 --- a/okhttp/src/test/java/okhttp3/internal/DoubleInetAddressDns.java +++ b/okhttp/src/test/java/okhttp3/internal/DoubleInetAddressDns.kt @@ -13,22 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package okhttp3.internal; +package okhttp3.internal -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.List; -import okhttp3.Dns; - -import static java.util.Arrays.asList; +import java.net.InetAddress +import okhttp3.Dns /** * A network that always resolves two IP addresses per host. Use this when testing route selection * fallbacks to guarantee that a fallback address is available. */ -public class DoubleInetAddressDns implements Dns { - @Override public List lookup(String hostname) throws UnknownHostException { - List addresses = Dns.SYSTEM.lookup(hostname); - return asList(addresses.get(0), addresses.get(0)); +class DoubleInetAddressDns : Dns { + override fun lookup(hostname: String): List { + val addresses = Dns.SYSTEM.lookup(hostname) + return listOf(addresses[0], addresses[0]) } } diff --git a/okhttp/src/test/java/okhttp3/internal/RecordingAuthenticator.java b/okhttp/src/test/java/okhttp3/internal/RecordingAuthenticator.java deleted file mode 100644 index aeb34f742f21..000000000000 --- a/okhttp/src/test/java/okhttp3/internal/RecordingAuthenticator.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * 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 - * - * 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 okhttp3.internal; - -import java.net.Authenticator; -import java.net.PasswordAuthentication; -import java.util.ArrayList; -import java.util.List; - -public final class RecordingAuthenticator extends Authenticator { - /** base64("username:password") */ - public static final String BASE_64_CREDENTIALS = "dXNlcm5hbWU6cGFzc3dvcmQ="; - - public final List calls = new ArrayList<>(); - public final PasswordAuthentication authentication; - - public RecordingAuthenticator(PasswordAuthentication authentication) { - this.authentication = authentication; - } - - public RecordingAuthenticator() { - this(new PasswordAuthentication("username", "password".toCharArray())); - } - - @Override protected PasswordAuthentication getPasswordAuthentication() { - this.calls.add("host=" + getRequestingHost() - + " port=" + getRequestingPort() - + " site=" + getRequestingSite().getHostName() - + " url=" + getRequestingURL() - + " type=" + getRequestorType() - + " prompt=" + getRequestingPrompt() - + " protocol=" + getRequestingProtocol() - + " scheme=" + getRequestingScheme()); - return authentication; - } -} diff --git a/okhttp/src/test/java/okhttp3/internal/RecordingAuthenticator.kt b/okhttp/src/test/java/okhttp3/internal/RecordingAuthenticator.kt new file mode 100644 index 000000000000..e13ec5beca39 --- /dev/null +++ b/okhttp/src/test/java/okhttp3/internal/RecordingAuthenticator.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * 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 + * + * 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 okhttp3.internal + +import java.net.Authenticator +import java.net.PasswordAuthentication + +class RecordingAuthenticator( + private val authentication: PasswordAuthentication? = PasswordAuthentication( + "username", + "password".toCharArray() + ) +) : Authenticator() { + val calls = mutableListOf() + + override fun getPasswordAuthentication(): PasswordAuthentication? { + calls.add("host=$requestingHost port=$requestingPort site=${requestingSite.hostName} " + + "url=$requestingURL type=$requestorType prompt=$requestingPrompt " + + "protocol=$requestingProtocol scheme=$requestingScheme" + ) + return authentication + } + + companion object { + /** base64("username:password") */ + const val BASE_64_CREDENTIALS = "dXNlcm5hbWU6cGFzc3dvcmQ=" + } +} diff --git a/okhttp/src/test/java/okhttp3/internal/SocketRecorder.java b/okhttp/src/test/java/okhttp3/internal/SocketRecorder.java deleted file mode 100644 index 7bf6ac4b42cd..000000000000 --- a/okhttp/src/test/java/okhttp3/internal/SocketRecorder.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright (C) 2016 Square, Inc. - * - * 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 - * - * 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 okhttp3.internal; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.Socket; -import java.util.Deque; -import java.util.concurrent.LinkedBlockingDeque; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; -import okhttp3.DelegatingSSLSocket; -import okhttp3.DelegatingSSLSocketFactory; -import okio.Buffer; -import okio.ByteString; - -/** Records all bytes written and read from a socket and makes them available for inspection. */ -public final class SocketRecorder { - private final Deque recordedSockets = new LinkedBlockingDeque<>(); - - /** Returns an SSLSocketFactory whose sockets will record all transmitted bytes. */ - public SSLSocketFactory sslSocketFactory(SSLSocketFactory delegate) { - return new DelegatingSSLSocketFactory(delegate) { - @Override protected SSLSocket configureSocket(SSLSocket sslSocket) throws IOException { - RecordedSocket recordedSocket = new RecordedSocket(); - recordedSockets.add(recordedSocket); - return new RecordingSSLSocket(sslSocket, recordedSocket); - } - }; - } - - public RecordedSocket takeSocket() { - return recordedSockets.remove(); - } - - /** A bidirectional transfer of unadulterated bytes over a socket. */ - public static final class RecordedSocket { - private final Buffer bytesWritten = new Buffer(); - private final Buffer bytesRead = new Buffer(); - - synchronized void byteWritten(int b) { - bytesWritten.writeByte(b); - } - - synchronized void byteRead(int b) { - bytesRead.writeByte(b); - } - - synchronized void bytesWritten(byte[] bytes, int offset, int length) { - bytesWritten.write(bytes, offset, length); - } - - synchronized void bytesRead(byte[] bytes, int offset, int length) { - bytesRead.write(bytes, offset, length); - } - - /** Returns all bytes that have been written to this socket. */ - public synchronized ByteString bytesWritten() { - return bytesWritten.readByteString(); - } - - /** Returns all bytes that have been read from this socket. */ - public synchronized ByteString bytesRead() { - return bytesRead.readByteString(); - } - } - - static final class RecordingInputStream extends InputStream { - private final Socket socket; - private final RecordedSocket recordedSocket; - - RecordingInputStream(Socket socket, RecordedSocket recordedSocket) { - this.socket = socket; - this.recordedSocket = recordedSocket; - } - - @Override public int read() throws IOException { - int b = socket.getInputStream().read(); - if (b == -1) return -1; - recordedSocket.byteRead(b); - return b; - } - - @Override public int read(byte[] b, int off, int len) throws IOException { - int read = socket.getInputStream().read(b, off, len); - if (read == -1) return -1; - recordedSocket.bytesRead(b, off, read); - return read; - } - - @Override public void close() throws IOException { - socket.getInputStream().close(); - } - } - - static final class RecordingOutputStream extends OutputStream { - private final Socket socket; - private final RecordedSocket recordedSocket; - - RecordingOutputStream(Socket socket, RecordedSocket recordedSocket) { - this.socket = socket; - this.recordedSocket = recordedSocket; - } - - @Override public void write(int b) throws IOException { - socket.getOutputStream().write(b); - recordedSocket.byteWritten(b); - } - - @Override public void write(byte[] b, int off, int len) throws IOException { - socket.getOutputStream().write(b, off, len); - recordedSocket.bytesWritten(b, off, len); - } - - @Override public void close() throws IOException { - socket.getOutputStream().close(); - } - - @Override public void flush() throws IOException { - socket.getOutputStream().flush(); - } - } - - static final class RecordingSSLSocket extends DelegatingSSLSocket { - private final InputStream inputStream; - private final OutputStream outputStream; - - RecordingSSLSocket(SSLSocket delegate, RecordedSocket recordedSocket) { - super(delegate); - inputStream = new RecordingInputStream(delegate, recordedSocket); - outputStream = new RecordingOutputStream(delegate, recordedSocket); - } - - @Override public void startHandshake() throws IOException { - // Intercept the handshake to properly configure TLS extensions with Jetty ALPN. Jetty ALPN - // expects the real SSLSocket to be placed in the global map. Because we are wrapping the real - // SSLSocket, it confuses Jetty ALPN. This patches that up so things work as expected. - Class alpn = null; - Class provider = null; - try { - alpn = Class.forName("org.eclipse.jetty.alpn.ALPN"); - provider = Class.forName("org.eclipse.jetty.alpn.ALPN$Provider"); - } catch (ClassNotFoundException ignored) { - } - - if (alpn == null || provider == null) { - // No Jetty, so nothing to worry about. - super.startHandshake(); - return; - } - - Object providerInstance = null; - Method putMethod = null; - try { - Method getMethod = alpn.getMethod("get", SSLSocket.class); - putMethod = alpn.getMethod("put", SSLSocket.class, provider); - providerInstance = getMethod.invoke(null, this); - if (providerInstance == null) { - // Jetty's on the classpath but TLS extensions weren't used. - super.startHandshake(); - return; - } - - // TLS extensions were used; replace with the real SSLSocket to make Jetty ALPN happy. - putMethod.invoke(null, getDelegate(), providerInstance); - super.startHandshake(); - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - throw new AssertionError(); - } finally { - // If we replaced the SSLSocket in the global map, we must put the original back for - // everything to work inside OkHttp. - if (providerInstance != null) { - try { - putMethod.invoke(null, this, providerInstance); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new AssertionError(); - } - } - } - } - - @Override public InputStream getInputStream() { - return inputStream; - } - - @Override public OutputStream getOutputStream() { - return outputStream; - } - } -} diff --git a/okhttp/src/test/java/okhttp3/internal/http/StatusLineTest.java b/okhttp/src/test/java/okhttp3/internal/http/StatusLineTest.java deleted file mode 100644 index 7eec760b2d32..000000000000 --- a/okhttp/src/test/java/okhttp3/internal/http/StatusLineTest.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (C) 2012 Square, Inc. - * - * 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 - * - * 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 okhttp3.internal.http; - -import java.io.IOException; -import java.net.ProtocolException; -import okhttp3.Protocol; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.fail; - -public final class StatusLineTest { - @Test public void parse() throws IOException { - String message = "Temporary Redirect"; - int version = 1; - int code = 200; - StatusLine statusLine = StatusLine.Companion.parse( - "HTTP/1." + version + " " + code + " " + message); - assertThat(statusLine.message).isEqualTo(message); - assertThat(statusLine.protocol).isEqualTo(Protocol.HTTP_1_1); - assertThat(statusLine.code).isEqualTo(code); - } - - @Test public void emptyMessage() throws IOException { - int version = 1; - int code = 503; - StatusLine statusLine = StatusLine.Companion.parse("HTTP/1." + version + " " + code + " "); - assertThat(statusLine.message).isEqualTo(""); - assertThat(statusLine.protocol).isEqualTo(Protocol.HTTP_1_1); - assertThat(statusLine.code).isEqualTo(code); - } - - /** - * This is not defined in the protocol but some servers won't add the leading empty space when the - * message is empty. http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1 - */ - @Test public void emptyMessageAndNoLeadingSpace() throws IOException { - int version = 1; - int code = 503; - StatusLine statusLine = StatusLine.Companion.parse("HTTP/1." + version + " " + code); - assertThat(statusLine.message).isEqualTo(""); - assertThat(statusLine.protocol).isEqualTo(Protocol.HTTP_1_1); - assertThat(statusLine.code).isEqualTo(code); - } - - // https://github.com/square/okhttp/issues/386 - @Test public void shoutcast() throws IOException { - StatusLine statusLine = StatusLine.Companion.parse("ICY 200 OK"); - assertThat(statusLine.message).isEqualTo("OK"); - assertThat(statusLine.protocol).isEqualTo(Protocol.HTTP_1_0); - assertThat(statusLine.code).isEqualTo(200); - } - - @Test public void missingProtocol() throws IOException { - assertInvalid(""); - assertInvalid(" "); - assertInvalid("200 OK"); - assertInvalid(" 200 OK"); - } - - @Test public void protocolVersions() throws IOException { - assertInvalid("HTTP/2.0 200 OK"); - assertInvalid("HTTP/2.1 200 OK"); - assertInvalid("HTTP/-.1 200 OK"); - assertInvalid("HTTP/1.- 200 OK"); - assertInvalid("HTTP/0.1 200 OK"); - assertInvalid("HTTP/101 200 OK"); - assertInvalid("HTTP/1.1_200 OK"); - } - - @Test public void nonThreeDigitCode() throws IOException { - assertInvalid("HTTP/1.1 OK"); - assertInvalid("HTTP/1.1 2 OK"); - assertInvalid("HTTP/1.1 20 OK"); - assertInvalid("HTTP/1.1 2000 OK"); - assertInvalid("HTTP/1.1 two OK"); - assertInvalid("HTTP/1.1 2"); - assertInvalid("HTTP/1.1 2000"); - assertInvalid("HTTP/1.1 two"); - } - - @Test public void truncated() throws IOException { - assertInvalid(""); - assertInvalid("H"); - assertInvalid("HTTP/1"); - assertInvalid("HTTP/1."); - assertInvalid("HTTP/1.1"); - assertInvalid("HTTP/1.1 "); - assertInvalid("HTTP/1.1 2"); - assertInvalid("HTTP/1.1 20"); - } - - @Test public void wrongMessageDelimiter() throws IOException { - assertInvalid("HTTP/1.1 200_"); - } - - private void assertInvalid(String statusLine) throws IOException { - try { - StatusLine.Companion.parse(statusLine); - fail(""); - } catch (ProtocolException expected) { - } - } -} diff --git a/okhttp/src/test/java/okhttp3/internal/http/StatusLineTest.kt b/okhttp/src/test/java/okhttp3/internal/http/StatusLineTest.kt new file mode 100644 index 000000000000..4e5064af96aa --- /dev/null +++ b/okhttp/src/test/java/okhttp3/internal/http/StatusLineTest.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2012 Square, Inc. + * + * 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 + * + * 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 okhttp3.internal.http + +import java.net.ProtocolException +import okhttp3.Protocol +import okhttp3.internal.http.StatusLine.Companion.parse +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.fail +import org.junit.jupiter.api.Test + +class StatusLineTest { + @Test + fun parse() { + val message = "Temporary Redirect" + val version = 1 + val code = 200 + val statusLine = parse("HTTP/1.$version $code $message") + assertThat(statusLine.message).isEqualTo(message) + assertThat(statusLine.protocol).isEqualTo(Protocol.HTTP_1_1) + assertThat(statusLine.code).isEqualTo(code) + } + + @Test + fun emptyMessage() { + val version = 1 + val code = 503 + val statusLine = parse("HTTP/1.$version $code ") + assertThat(statusLine.message).isEqualTo("") + assertThat(statusLine.protocol).isEqualTo(Protocol.HTTP_1_1) + assertThat(statusLine.code).isEqualTo(code) + } + + /** + * This is not defined in the protocol but some servers won't add the leading empty space when the + * message is empty. http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1 + */ + @Test + fun emptyMessageAndNoLeadingSpace() { + val version = 1 + val code = 503 + val statusLine = parse("HTTP/1.$version $code") + assertThat(statusLine.message).isEqualTo("") + assertThat(statusLine.protocol).isEqualTo(Protocol.HTTP_1_1) + assertThat(statusLine.code).isEqualTo(code) + } + + // https://github.com/square/okhttp/issues/386 + @Test + fun shoutcast() { + val statusLine = parse("ICY 200 OK") + assertThat(statusLine.message).isEqualTo("OK") + assertThat(statusLine.protocol).isEqualTo(Protocol.HTTP_1_0) + assertThat(statusLine.code).isEqualTo(200) + } + + @Test + fun missingProtocol() { + assertInvalid("") + assertInvalid(" ") + assertInvalid("200 OK") + assertInvalid(" 200 OK") + } + + @Test + fun protocolVersions() { + assertInvalid("HTTP/2.0 200 OK") + assertInvalid("HTTP/2.1 200 OK") + assertInvalid("HTTP/-.1 200 OK") + assertInvalid("HTTP/1.- 200 OK") + assertInvalid("HTTP/0.1 200 OK") + assertInvalid("HTTP/101 200 OK") + assertInvalid("HTTP/1.1_200 OK") + } + + @Test + fun nonThreeDigitCode() { + assertInvalid("HTTP/1.1 OK") + assertInvalid("HTTP/1.1 2 OK") + assertInvalid("HTTP/1.1 20 OK") + assertInvalid("HTTP/1.1 2000 OK") + assertInvalid("HTTP/1.1 two OK") + assertInvalid("HTTP/1.1 2") + assertInvalid("HTTP/1.1 2000") + assertInvalid("HTTP/1.1 two") + } + + @Test + fun truncated() { + assertInvalid("") + assertInvalid("H") + assertInvalid("HTTP/1") + assertInvalid("HTTP/1.") + assertInvalid("HTTP/1.1") + assertInvalid("HTTP/1.1 ") + assertInvalid("HTTP/1.1 2") + assertInvalid("HTTP/1.1 20") + } + + @Test + fun wrongMessageDelimiter() { + assertInvalid("HTTP/1.1 200_") + } + + private fun assertInvalid(statusLine: String) { + try { + parse(statusLine) + fail("") + } catch (expected: ProtocolException) { + } + } +} diff --git a/okhttp/src/test/java/okhttp3/internal/http/ThreadInterruptTest.java b/okhttp/src/test/java/okhttp3/internal/http/ThreadInterruptTest.java deleted file mode 100644 index 1b854b65651f..000000000000 --- a/okhttp/src/test/java/okhttp3/internal/http/ThreadInterruptTest.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 - * - * 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 okhttp3.internal.http; - -import java.io.IOException; -import java.io.InputStream; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketException; -import java.util.concurrent.TimeUnit; -import javax.annotation.Nullable; -import javax.net.ServerSocketFactory; -import javax.net.SocketFactory; -import okhttp3.Call; -import okhttp3.DelegatingServerSocketFactory; -import okhttp3.DelegatingSocketFactory; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.OkHttpClientTestRule; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.testing.PlatformRule; -import okio.Buffer; -import okio.BufferedSink; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import static org.junit.jupiter.api.Assertions.fail; - -@Tag("Slowish") -public final class ThreadInterruptTest { - @RegisterExtension public final PlatformRule platform = new PlatformRule(); - @RegisterExtension public final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule(); - - // The size of the socket buffers in bytes. - private static final int SOCKET_BUFFER_SIZE = 256 * 1024; - - private MockWebServer server; - private OkHttpClient client; - - @BeforeEach public void setUp() throws Exception { - // Sockets on some platforms can have large buffers that mean writes do not block when - // required. These socket factories explicitly set the buffer sizes on sockets created. - server = new MockWebServer(); - server.setServerSocketFactory( - new DelegatingServerSocketFactory(ServerSocketFactory.getDefault()) { - @Override - protected ServerSocket configureServerSocket(ServerSocket serverSocket) throws SocketException { - serverSocket.setReceiveBufferSize(SOCKET_BUFFER_SIZE); - return serverSocket; - } - }); - client = clientTestRule.newClientBuilder() - .socketFactory(new DelegatingSocketFactory(SocketFactory.getDefault()) { - @Override - protected Socket configureSocket(Socket socket) throws IOException { - socket.setSendBufferSize(SOCKET_BUFFER_SIZE); - socket.setReceiveBufferSize(SOCKET_BUFFER_SIZE); - return socket; - } - }) - .build(); - } - - @AfterEach public void tearDown() throws Exception { - Thread.interrupted(); // Clear interrupted state. - } - - @Test public void interruptWritingRequestBody() throws Exception { - server.enqueue(new MockResponse()); - server.start(); - - Call call = client.newCall(new Request.Builder() - .url(server.url("/")) - .post(new RequestBody() { - @Override public @Nullable MediaType contentType() { - return null; - } - - @Override public void writeTo(BufferedSink sink) throws IOException { - for (int i = 0; i < 10; i++) { - sink.writeByte(0); - sink.flush(); - sleep(100); - } - fail("Expected connection to be closed"); - } - }) - .build()); - - interruptLater(500); - try { - call.execute(); - fail(""); - } catch (IOException expected) { - } - } - - @Test public void interruptReadingResponseBody() throws Exception { - int responseBodySize = 8 * 1024 * 1024; // 8 MiB. - - server.enqueue(new MockResponse() - .setBody(new Buffer().write(new byte[responseBodySize])) - .throttleBody(64 * 1024, 125, TimeUnit.MILLISECONDS)); // 500 Kbps - server.start(); - - Call call = client.newCall(new Request.Builder() - .url(server.url("/")) - .build()); - - Response response = call.execute(); - interruptLater(500); - InputStream responseBody = response.body().byteStream(); - byte[] buffer = new byte[1024]; - try { - while (responseBody.read(buffer) != -1) { - } - fail("Expected connection to be interrupted"); - } catch (IOException expected) { - } - - responseBody.close(); - } - - private void sleep(int delayMillis) { - try { - Thread.sleep(delayMillis); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - private void interruptLater(int delayMillis) { - Thread toInterrupt = Thread.currentThread(); - Thread interruptingCow = new Thread(() -> { - sleep(delayMillis); - toInterrupt.interrupt(); - }); - interruptingCow.start(); - } -} diff --git a/okhttp/src/test/java/okhttp3/internal/http/ThreadInterruptTest.kt b/okhttp/src/test/java/okhttp3/internal/http/ThreadInterruptTest.kt new file mode 100644 index 000000000000..2871253f3c63 --- /dev/null +++ b/okhttp/src/test/java/okhttp3/internal/http/ThreadInterruptTest.kt @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 + * + * 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 okhttp3.internal.http + +import java.io.IOException +import java.net.ServerSocket +import java.net.Socket +import java.net.SocketException +import java.util.concurrent.TimeUnit +import okhttp3.DelegatingServerSocketFactory +import okhttp3.DelegatingSocketFactory +import okhttp3.OkHttpClient +import okhttp3.OkHttpClientTestRule +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.testing.PlatformRule +import okio.Buffer +import okio.BufferedSink +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.fail +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +@Tag("Slowish") +class ThreadInterruptTest { + @RegisterExtension + val platform = PlatformRule() + + @RegisterExtension + val clientTestRule = OkHttpClientTestRule() + private lateinit var server: MockWebServer + private lateinit var client: OkHttpClient + + @BeforeEach + fun setUp() { + // Sockets on some platforms can have large buffers that mean writes do not block when + // required. These socket factories explicitly set the buffer sizes on sockets created. + server = MockWebServer() + server.serverSocketFactory = object : DelegatingServerSocketFactory(getDefault()) { + @Throws(SocketException::class) + override fun configureServerSocket(serverSocket: ServerSocket): ServerSocket { + serverSocket.setReceiveBufferSize(SOCKET_BUFFER_SIZE) + return serverSocket + } + } + client = clientTestRule.newClientBuilder() + .socketFactory(object : DelegatingSocketFactory(getDefault()) { + @Throws(IOException::class) + override fun configureSocket(socket: Socket): Socket { + socket.setSendBufferSize(SOCKET_BUFFER_SIZE) + socket.setReceiveBufferSize(SOCKET_BUFFER_SIZE) + return socket + } + }) + .build() + } + + @AfterEach + fun tearDown() { + Thread.interrupted() // Clear interrupted state. + } + + @Test + fun interruptWritingRequestBody() { + server.enqueue(MockResponse()) + server.start() + val call = client.newCall( + Request.Builder() + .url(server.url("/")) + .post(object : RequestBody() { + override fun contentType() = null + + override fun writeTo(sink: BufferedSink) { + for (i in 0..9) { + sink.writeByte(0) + sink.flush() + sleep(100) + } + fail("Expected connection to be closed") + } + }) + .build() + ) + interruptLater(500) + try { + call.execute() + fail("") + } catch (expected: IOException) { + } + } + + @Test + fun interruptReadingResponseBody() { + val responseBodySize = 8 * 1024 * 1024 // 8 MiB. + server.enqueue( + MockResponse() + .setBody(Buffer().write(ByteArray(responseBodySize))) + .throttleBody((64 * 1024).toLong(), 125, TimeUnit.MILLISECONDS) + ) // 500 Kbps + server.start() + val call = client.newCall( + Request.Builder() + .url(server.url("/")) + .build() + ) + val response = call.execute() + interruptLater(500) + val responseBody = response.body.byteStream() + val buffer = ByteArray(1024) + try { + while (responseBody.read(buffer) != -1) { + } + fail("Expected connection to be interrupted") + } catch (expected: IOException) { + } + responseBody.close() + } + + private fun sleep(delayMillis: Int) { + try { + Thread.sleep(delayMillis.toLong()) + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() + } + } + + private fun interruptLater(delayMillis: Int) { + val toInterrupt = Thread.currentThread() + val interruptingCow = Thread { + sleep(delayMillis) + toInterrupt.interrupt() + } + interruptingCow.start() + } + + companion object { + // The size of the socket buffers in bytes. + private const val SOCKET_BUFFER_SIZE = 256 * 1024 + } +} diff --git a/okhttp/src/test/java/okhttp3/internal/http2/HuffmanTest.java b/okhttp/src/test/java/okhttp3/internal/http2/HuffmanTest.java deleted file mode 100644 index 21677553881c..000000000000 --- a/okhttp/src/test/java/okhttp3/internal/http2/HuffmanTest.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2013 Twitter, Inc. - * - * 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 - * - * 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 okhttp3.internal.http2; - -import java.io.IOException; -import java.util.Random; -import okio.Buffer; -import okio.ByteString; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; - -/** Original version of this class was lifted from {@code com.twitter.hpack.HuffmanTest}. */ -public final class HuffmanTest { - @Test public void roundTripForRequestAndResponse() throws IOException { - String s = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - for (int i = 0; i < s.length(); i++) { - assertRoundTrip(ByteString.encodeUtf8(s.substring(0, i))); - } - - Random random = new Random(123456789L); - byte[] buf = new byte[4096]; - random.nextBytes(buf); - assertRoundTrip(ByteString.of(buf)); - } - - private void assertRoundTrip(ByteString data) throws IOException { - Buffer encodeBuffer = new Buffer(); - Huffman.INSTANCE.encode(data, encodeBuffer); - assertThat(Huffman.INSTANCE.encodedLength(data)).isEqualTo(encodeBuffer.size()); - - Buffer decodeBuffer = new Buffer(); - Huffman.INSTANCE.decode(encodeBuffer, encodeBuffer.size(), decodeBuffer); - assertEquals(data, decodeBuffer.readByteString()); - } -} diff --git a/okhttp/src/test/java/okhttp3/internal/http2/HuffmanTest.kt b/okhttp/src/test/java/okhttp3/internal/http2/HuffmanTest.kt new file mode 100644 index 000000000000..1278ad9b0d19 --- /dev/null +++ b/okhttp/src/test/java/okhttp3/internal/http2/HuffmanTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2013 Twitter, Inc. + * + * 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 + * + * 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 okhttp3.internal.http2 + +import java.util.Random +import okhttp3.internal.http2.Huffman.decode +import okhttp3.internal.http2.Huffman.encode +import okhttp3.internal.http2.Huffman.encodedLength +import okio.Buffer +import okio.ByteString +import okio.ByteString.Companion.encodeUtf8 +import okio.ByteString.Companion.toByteString +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +/** Original version of this class was lifted from `com.twitter.hpack.HuffmanTest`. */ +class HuffmanTest { + @Test + fun roundTripForRequestAndResponse() { + val s = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + for (i in s.indices) { + assertRoundTrip(s.substring(0, i).encodeUtf8()) + } + val random = Random(123456789L) + val buf = ByteArray(4096) + random.nextBytes(buf) + assertRoundTrip(buf.toByteString()) + } + + private fun assertRoundTrip(data: ByteString) { + val encodeBuffer = Buffer() + encode(data, encodeBuffer) + assertThat(encodedLength(data)).isEqualTo(encodeBuffer.size) + val decodeBuffer = Buffer() + decode(encodeBuffer, encodeBuffer.size, decodeBuffer) + assertEquals(data, decodeBuffer.readByteString()) + } +} diff --git a/okhttp/src/test/java/okhttp3/internal/http2/SettingsTest.java b/okhttp/src/test/java/okhttp3/internal/http2/SettingsTest.java deleted file mode 100644 index 93274c297108..000000000000 --- a/okhttp/src/test/java/okhttp3/internal/http2/SettingsTest.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2012 Square, Inc. - * - * 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 - * - * 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 okhttp3.internal.http2; - -import org.junit.jupiter.api.Test; - -import static okhttp3.internal.http2.Settings.DEFAULT_INITIAL_WINDOW_SIZE; -import static okhttp3.internal.http2.Settings.MAX_CONCURRENT_STREAMS; -import static org.assertj.core.api.Assertions.assertThat; - -public final class SettingsTest { - @Test public void unsetField() { - Settings settings = new Settings(); - assertThat(settings.isSet(MAX_CONCURRENT_STREAMS)).isFalse(); - assertThat(settings.getMaxConcurrentStreams()).isEqualTo(Integer.MAX_VALUE); - } - - @Test public void setFields() { - Settings settings = new Settings(); - - settings.set(Settings.HEADER_TABLE_SIZE, 8096); - assertThat(settings.getHeaderTableSize()).isEqualTo(8096); - - assertThat(settings.getEnablePush(true)).isTrue(); - settings.set(Settings.ENABLE_PUSH, 1); - assertThat(settings.getEnablePush(false)).isTrue(); - settings.clear(); - - assertThat(settings.getMaxConcurrentStreams()).isEqualTo(Integer.MAX_VALUE); - settings.set(MAX_CONCURRENT_STREAMS, 75); - assertThat(settings.getMaxConcurrentStreams()).isEqualTo(75); - - settings.clear(); - assertThat(settings.getMaxFrameSize(16384)).isEqualTo(16384); - settings.set(Settings.MAX_FRAME_SIZE, 16777215); - assertThat(settings.getMaxFrameSize(16384)).isEqualTo(16777215); - - assertThat(settings.getMaxHeaderListSize(-1)).isEqualTo(-1); - settings.set(Settings.MAX_HEADER_LIST_SIZE, 16777215); - assertThat(settings.getMaxHeaderListSize(-1)).isEqualTo(16777215); - - assertThat(settings.getInitialWindowSize()).isEqualTo( - DEFAULT_INITIAL_WINDOW_SIZE); - settings.set(Settings.INITIAL_WINDOW_SIZE, 108); - assertThat(settings.getInitialWindowSize()).isEqualTo(108); - } - - @Test public void merge() { - Settings a = new Settings(); - a.set(Settings.HEADER_TABLE_SIZE, 10000); - a.set(Settings.MAX_HEADER_LIST_SIZE, 20000); - a.set(Settings.INITIAL_WINDOW_SIZE, 30000); - - Settings b = new Settings(); - b.set(Settings.MAX_HEADER_LIST_SIZE, 40000); - b.set(Settings.INITIAL_WINDOW_SIZE, 50000); - b.set(Settings.MAX_CONCURRENT_STREAMS, 60000); - - a.merge(b); - assertThat(a.getHeaderTableSize()).isEqualTo(10000); - assertThat(a.getMaxHeaderListSize(-1)).isEqualTo(40000); - assertThat(a.getInitialWindowSize()).isEqualTo(50000); - assertThat(a.getMaxConcurrentStreams()).isEqualTo(60000); - } -} diff --git a/okhttp/src/test/java/okhttp3/internal/http2/SettingsTest.kt b/okhttp/src/test/java/okhttp3/internal/http2/SettingsTest.kt new file mode 100644 index 000000000000..4b508ade893c --- /dev/null +++ b/okhttp/src/test/java/okhttp3/internal/http2/SettingsTest.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2012 Square, Inc. + * + * 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 + * + * 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 okhttp3.internal.http2 + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class SettingsTest { + @Test + fun unsetField() { + val settings = Settings() + assertThat(settings.isSet(Settings.MAX_CONCURRENT_STREAMS)).isFalse() + assertThat(settings.getMaxConcurrentStreams()).isEqualTo(Int.MAX_VALUE) + } + + @Test + fun setFields() { + val settings = Settings() + settings[Settings.HEADER_TABLE_SIZE] = 8096 + assertThat(settings.headerTableSize).isEqualTo(8096) + assertThat(settings.getEnablePush(true)).isTrue() + settings[Settings.ENABLE_PUSH] = 1 + assertThat(settings.getEnablePush(false)).isTrue() + settings.clear() + assertThat(settings.getMaxConcurrentStreams()).isEqualTo(Int.MAX_VALUE) + settings[Settings.MAX_CONCURRENT_STREAMS] = 75 + assertThat(settings.getMaxConcurrentStreams()).isEqualTo(75) + settings.clear() + assertThat(settings.getMaxFrameSize(16384)).isEqualTo(16384) + settings[Settings.MAX_FRAME_SIZE] = 16777215 + assertThat(settings.getMaxFrameSize(16384)).isEqualTo(16777215) + assertThat(settings.getMaxHeaderListSize(-1)).isEqualTo(-1) + settings[Settings.MAX_HEADER_LIST_SIZE] = 16777215 + assertThat(settings.getMaxHeaderListSize(-1)).isEqualTo(16777215) + assertThat(settings.initialWindowSize).isEqualTo( + Settings.DEFAULT_INITIAL_WINDOW_SIZE + ) + settings[Settings.INITIAL_WINDOW_SIZE] = 108 + assertThat(settings.initialWindowSize).isEqualTo(108) + } + + @Test + fun merge() { + val a = Settings() + a[Settings.HEADER_TABLE_SIZE] = 10000 + a[Settings.MAX_HEADER_LIST_SIZE] = 20000 + a[Settings.INITIAL_WINDOW_SIZE] = 30000 + val b = Settings() + b[Settings.MAX_HEADER_LIST_SIZE] = 40000 + b[Settings.INITIAL_WINDOW_SIZE] = 50000 + b[Settings.MAX_CONCURRENT_STREAMS] = 60000 + a.merge(b) + assertThat(a.headerTableSize).isEqualTo(10000) + assertThat(a.getMaxHeaderListSize(-1)).isEqualTo(40000) + assertThat(a.initialWindowSize).isEqualTo(50000) + assertThat(a.getMaxConcurrentStreams()).isEqualTo(60000) + } +} diff --git a/okhttp/src/test/java/okhttp3/internal/platform/Jdk8WithJettyBootPlatformTest.java b/okhttp/src/test/java/okhttp3/internal/platform/Jdk8WithJettyBootPlatformTest.java deleted file mode 100644 index cb9d7ca25add..000000000000 --- a/okhttp/src/test/java/okhttp3/internal/platform/Jdk8WithJettyBootPlatformTest.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2016 Square, Inc. - * - * 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 - * - * 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 okhttp3.internal.platform; - -import okhttp3.testing.PlatformRule; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import static org.assertj.core.api.Java6Assertions.assertThat; -import static org.junit.jupiter.api.Assumptions.assumeFalse; -import static org.junit.jupiter.api.Assumptions.assumeTrue; - -public class Jdk8WithJettyBootPlatformTest { - @RegisterExtension public final PlatformRule platform = new PlatformRule(); - - @Test - public void testBuildsWithJettyBoot() { - assumeTrue(System.getProperty("java.specification.version").equals("1.8")); - platform.assumeJettyBootEnabled(); - - assertThat(Jdk8WithJettyBootPlatform.Companion.buildIfSupported()).isNotNull(); - } - - @Test - public void testNotBuildWithOther() { - assumeFalse(System.getProperty("java.specification.version").equals("1.8")); - - assertThat(Jdk8WithJettyBootPlatform.Companion.buildIfSupported()).isNull(); - } -} diff --git a/okhttp/src/test/java/okhttp3/internal/platform/Jdk8WithJettyBootPlatformTest.kt b/okhttp/src/test/java/okhttp3/internal/platform/Jdk8WithJettyBootPlatformTest.kt new file mode 100644 index 000000000000..f7364c043453 --- /dev/null +++ b/okhttp/src/test/java/okhttp3/internal/platform/Jdk8WithJettyBootPlatformTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2016 Square, Inc. + * + * 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 + * + * 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 okhttp3.internal.platform + +import okhttp3.internal.platform.Jdk8WithJettyBootPlatform.Companion.buildIfSupported +import okhttp3.testing.PlatformRule +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assumptions.assumeFalse +import org.junit.jupiter.api.Assumptions.assumeTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class Jdk8WithJettyBootPlatformTest { + @RegisterExtension + val platform = PlatformRule() + + @Test + fun testBuildsWithJettyBoot() { + assumeTrue(System.getProperty("java.specification.version") == "1.8") + platform.assumeJettyBootEnabled() + assertThat(buildIfSupported()).isNotNull() + } + + @Test + fun testNotBuildWithOther() { + assumeFalse(System.getProperty("java.specification.version") == "1.8") + assertThat(buildIfSupported()).isNull() + } +} diff --git a/okhttp/src/test/java/okhttp3/internal/platform/Jdk9PlatformTest.java b/okhttp/src/test/java/okhttp3/internal/platform/Jdk9PlatformTest.java deleted file mode 100644 index 545f55fdb3b3..000000000000 --- a/okhttp/src/test/java/okhttp3/internal/platform/Jdk9PlatformTest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2016 Square, Inc. - * - * 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 - * - * 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 okhttp3.internal.platform; - -import javax.net.ssl.SSLSocket; -import okhttp3.DelegatingSSLSocket; -import okhttp3.testing.PlatformRule; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import static org.assertj.core.api.Assertions.assertThat; - -public class Jdk9PlatformTest { - @RegisterExtension public final PlatformRule platform = new PlatformRule(); - - @Test - public void buildsWhenJdk9() { - platform.assumeJdk9(); - assertThat(Jdk9Platform.Companion.buildIfSupported()).isNotNull(); - } - - @Test - public void buildsWhenJdk8() { - platform.assumeJdk8(); - - try { - SSLSocket.class.getMethod("getApplicationProtocol"); - - // also present on JDK8 after build 252. - assertThat(Jdk9Platform.Companion.buildIfSupported()).isNotNull(); - } catch (NoSuchMethodException nsme) { - assertThat(Jdk9Platform.Companion.buildIfSupported()).isNull(); - } - } - - @Test - public void testToStringIsClassname() { - assertThat(new Jdk9Platform().toString()).isEqualTo("Jdk9Platform"); - } - - @Test - public void selectedProtocolIsNullWhenSslSocketThrowsExceptionForApplicationProtocol() { - platform.assumeJdk9(); - - DelegatingSSLSocket applicationProtocolUnsupported = new DelegatingSSLSocket(null) { - @Override public String getApplicationProtocol() { - throw new UnsupportedOperationException("Mock exception"); - } - }; - - assertThat(new Jdk9Platform().getSelectedProtocol(applicationProtocolUnsupported)).isNull(); - } -} diff --git a/okhttp/src/test/java/okhttp3/internal/platform/Jdk9PlatformTest.kt b/okhttp/src/test/java/okhttp3/internal/platform/Jdk9PlatformTest.kt new file mode 100644 index 000000000000..f4a9e535f405 --- /dev/null +++ b/okhttp/src/test/java/okhttp3/internal/platform/Jdk9PlatformTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2016 Square, Inc. + * + * 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 + * + * 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 okhttp3.internal.platform + +import javax.net.ssl.SSLSocket +import okhttp3.DelegatingSSLSocket +import okhttp3.internal.platform.Jdk9Platform.Companion.buildIfSupported +import okhttp3.testing.PlatformRule +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class Jdk9PlatformTest { + @RegisterExtension + val platform = PlatformRule() + + @Test + fun buildsWhenJdk9() { + platform.assumeJdk9() + assertThat(buildIfSupported()).isNotNull() + } + + @Test + fun buildsWhenJdk8() { + platform.assumeJdk8() + try { + SSLSocket::class.java.getMethod("getApplicationProtocol") + // also present on JDK8 after build 252. + assertThat(buildIfSupported()).isNotNull() + } catch (nsme: NoSuchMethodException) { + assertThat(buildIfSupported()).isNull() + } + } + + @Test + fun testToStringIsClassname() { + assertThat(Jdk9Platform().toString()).isEqualTo("Jdk9Platform") + } + + @Test + fun selectedProtocolIsNullWhenSslSocketThrowsExceptionForApplicationProtocol() { + platform.assumeJdk9() + val applicationProtocolUnsupported = object : DelegatingSSLSocket(null) { + override fun getApplicationProtocol(): String { + throw UnsupportedOperationException("Mock exception") + } + } + assertThat(Jdk9Platform().getSelectedProtocol(applicationProtocolUnsupported)).isNull() + } +} diff --git a/okhttp/src/test/java/okhttp3/internal/platform/PlatformTest.java b/okhttp/src/test/java/okhttp3/internal/platform/PlatformTest.java deleted file mode 100644 index f888612881c9..000000000000 --- a/okhttp/src/test/java/okhttp3/internal/platform/PlatformTest.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2016 Square, Inc. - * - * 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 - * - * 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 okhttp3.internal.platform; - -import okhttp3.testing.PlatformRule; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import static org.assertj.core.api.Assertions.assertThat; - -public class PlatformTest { - @RegisterExtension public PlatformRule platform = new PlatformRule(); - - @Test public void alwaysBuilds() { - new Platform(); - } - - /** Guard against the default value changing by accident. */ - @Test public void defaultPrefix() { - assertThat(new Platform().getPrefix()).isEqualTo("OkHttp"); - } - - public static String getJvmSpecVersion() { - return System.getProperty("java.specification.version", "unknown"); - } - - @Test - public void testToStringIsClassname() { - assertThat(new Platform().toString()).isEqualTo("Platform"); - } - - @Test - public void testNotAndroid() { - platform.assumeNotAndroid(); - - // This is tautological so just confirms that it runs. - assertThat(Platform.Companion.isAndroid()).isEqualTo(false); - } -} diff --git a/okhttp/src/test/java/okhttp3/internal/platform/PlatformTest.kt b/okhttp/src/test/java/okhttp3/internal/platform/PlatformTest.kt new file mode 100644 index 000000000000..14081a824911 --- /dev/null +++ b/okhttp/src/test/java/okhttp3/internal/platform/PlatformTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2016 Square, Inc. + * + * 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 + * + * 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 okhttp3.internal.platform + +import okhttp3.internal.platform.Platform.Companion.isAndroid +import okhttp3.testing.PlatformRule +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class PlatformTest { + @RegisterExtension + var platform = PlatformRule() + + @Test + fun alwaysBuilds() { + Platform() + } + + /** Guard against the default value changing by accident. */ + @Test + fun defaultPrefix() { + assertThat(Platform().getPrefix()).isEqualTo("OkHttp") + } + + @Test + fun testToStringIsClassname() { + assertThat(Platform().toString()).isEqualTo("Platform") + } + + @Test + fun testNotAndroid() { + platform.assumeNotAndroid() + + // This is tautological so just confirms that it runs. + assertThat(isAndroid).isEqualTo(false) + } +} diff --git a/okhttp/src/test/java/okhttp3/internal/ws/WebSocketRecorder.java b/okhttp/src/test/java/okhttp3/internal/ws/WebSocketRecorder.java deleted file mode 100644 index fe4ab80c38cc..000000000000 --- a/okhttp/src/test/java/okhttp3/internal/ws/WebSocketRecorder.java +++ /dev/null @@ -1,401 +0,0 @@ -/* - * Copyright (C) 2016 Square, Inc. - * - * 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 - * - * 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 okhttp3.internal.ws; - -import java.io.IOException; -import java.util.Objects; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import javax.annotation.Nullable; -import okhttp3.Response; -import okhttp3.WebSocket; -import okhttp3.WebSocketListener; -import okhttp3.internal.platform.Platform; -import okio.ByteString; -import static org.assertj.core.api.Assertions.assertThat; - -public final class WebSocketRecorder extends WebSocketListener { - private final String name; - private final BlockingQueue events = new LinkedBlockingQueue<>(); - private WebSocketListener delegate; - - public WebSocketRecorder(String name) { - this.name = name; - } - - /** Sets a delegate for handling the next callback to this listener. Cleared after invoked. */ - public void setNextEventDelegate(WebSocketListener delegate) { - this.delegate = delegate; - } - - @Override public void onOpen(WebSocket webSocket, Response response) { - Platform.get().log("[WS " + name + "] onOpen", Platform.INFO, null); - - WebSocketListener delegate = this.delegate; - if (delegate != null) { - this.delegate = null; - delegate.onOpen(webSocket, response); - } else { - events.add(new Open(webSocket, response)); - } - } - - @Override public void onMessage(WebSocket webSocket, ByteString bytes) { - Platform.get().log("[WS " + name + "] onMessage", Platform.INFO, null); - - WebSocketListener delegate = this.delegate; - if (delegate != null) { - this.delegate = null; - delegate.onMessage(webSocket, bytes); - } else { - Message event = new Message(bytes); - events.add(event); - } - } - - @Override public void onMessage(WebSocket webSocket, String text) { - Platform.get().log("[WS " + name + "] onMessage", Platform.INFO, null); - - WebSocketListener delegate = this.delegate; - if (delegate != null) { - this.delegate = null; - delegate.onMessage(webSocket, text); - } else { - Message event = new Message(text); - events.add(event); - } - } - - @Override public void onClosing(WebSocket webSocket, int code, String reason) { - Platform.get().log("[WS " + name + "] onClosing " + code, Platform.INFO, null); - - WebSocketListener delegate = this.delegate; - if (delegate != null) { - this.delegate = null; - delegate.onClosing(webSocket, code, reason); - } else { - events.add(new Closing(code, reason)); - } - } - - @Override public void onClosed(WebSocket webSocket, int code, String reason) { - Platform.get().log("[WS " + name + "] onClosed " + code, Platform.INFO, null); - - WebSocketListener delegate = this.delegate; - if (delegate != null) { - this.delegate = null; - delegate.onClosed(webSocket, code, reason); - } else { - events.add(new Closed(code, reason)); - } - } - - @Override public void onFailure(WebSocket webSocket, Throwable t, @Nullable Response response) { - Platform.get().log("[WS " + name + "] onFailure", Platform.INFO, t); - - WebSocketListener delegate = this.delegate; - if (delegate != null) { - this.delegate = null; - delegate.onFailure(webSocket, t, response); - } else { - events.add(new Failure(t, response)); - } - } - - private Object nextEvent() { - try { - Object event = events.poll(10, TimeUnit.SECONDS); - if (event == null) { - throw new AssertionError("Timed out waiting for event."); - } - return event; - } catch (InterruptedException e) { - throw new AssertionError(e); - } - } - - public void assertTextMessage(String payload) { - Object actual = nextEvent(); - assertThat(actual).isEqualTo(new Message(payload)); - } - - public void assertBinaryMessage(ByteString payload) { - Object actual = nextEvent(); - assertThat(actual).isEqualTo(new Message(payload)); - } - - public void assertPing(ByteString payload) { - Object actual = nextEvent(); - assertThat(actual).isEqualTo(new Ping(payload)); - } - - public void assertPong(ByteString payload) { - Object actual = nextEvent(); - assertThat(actual).isEqualTo(new Pong(payload)); - } - - public void assertClosing(int code, String reason) { - Object actual = nextEvent(); - assertThat(actual).isEqualTo(new Closing(code, reason)); - } - - public void assertClosed(int code, String reason) { - Object actual = nextEvent(); - assertThat(actual).isEqualTo(new Closed(code, reason)); - } - - public void assertExhausted() { - assertThat(events).isEmpty(); - } - - public WebSocket assertOpen() { - Object event = nextEvent(); - if (!(event instanceof Open)) { - throw new AssertionError("Expected Open but was " + event); - } - return ((Open) event).webSocket; - } - - public void assertFailure(Throwable t) { - Object event = nextEvent(); - if (!(event instanceof Failure)) { - throw new AssertionError("Expected Failure but was " + event); - } - Failure failure = (Failure) event; - assertThat(failure.response).isNull(); - assertThat(failure.t).isSameAs(t); - } - - public void assertFailure(Class cls, String... messages) { - Object event = nextEvent(); - if (!(event instanceof Failure)) { - throw new AssertionError("Expected Failure but was " + event); - } - Failure failure = (Failure) event; - assertThat(failure.response).isNull(); - assertThat(failure.t.getClass()).isEqualTo(cls); - if (messages.length > 0) { - assertThat(messages).contains(failure.t.getMessage()); - } - } - - public void assertFailure() { - Object event = nextEvent(); - if (!(event instanceof Failure)) { - throw new AssertionError("Expected Failure but was " + event); - } - } - - public void assertFailure(int code, String body, Class cls, String message) - throws IOException { - Object event = nextEvent(); - if (!(event instanceof Failure)) { - throw new AssertionError("Expected Failure but was " + event); - } - Failure failure = (Failure) event; - assertThat(failure.response.code()).isEqualTo(code); - if (body != null) { - assertThat(failure.responseBody).isEqualTo(body); - } - assertThat(failure.t.getClass()).isEqualTo(cls); - assertThat(failure.t.getMessage()).isEqualTo(message); - } - - /** Expose this recorder as a frame callback and shim in "ping" events. */ - public WebSocketReader.FrameCallback asFrameCallback() { - return new WebSocketReader.FrameCallback() { - @Override public void onReadMessage(String text) throws IOException { - onMessage(null, text); - } - - @Override public void onReadMessage(ByteString bytes) throws IOException { - onMessage(null, bytes); - } - - @Override public void onReadPing(ByteString payload) { - events.add(new Ping(payload)); - } - - @Override public void onReadPong(ByteString payload) { - events.add(new Pong(payload)); - } - - @Override public void onReadClose(int code, String reason) { - onClosing(null, code, reason); - } - }; - } - - static final class Open { - final WebSocket webSocket; - final Response response; - - Open(WebSocket webSocket, Response response) { - this.webSocket = webSocket; - this.response = response; - } - - @Override public String toString() { - return "Open[" + response + "]"; - } - } - - static final class Failure { - final Throwable t; - final Response response; - final String responseBody; - - Failure(Throwable t, Response response) { - this.t = t; - this.response = response; - String responseBody = null; - if (response != null && response.code() != 101) { - try { - responseBody = response.body().string(); - } catch (IOException ignored) { - } - } - this.responseBody = responseBody; - } - - @Override public String toString() { - if (response == null) { - return "Failure[" + t + "]"; - } - return "Failure[" + response + "]"; - } - } - - static final class Message { - public final ByteString bytes; - public final String string; - - public Message(ByteString bytes) { - this.bytes = bytes; - this.string = null; - } - - public Message(String string) { - this.bytes = null; - this.string = string; - } - - @Override public String toString() { - return "Message[" + (bytes != null ? bytes : string) + "]"; - } - - @Override public int hashCode() { - return (bytes != null ? bytes : string).hashCode(); - } - - @Override public boolean equals(Object other) { - return other instanceof Message - && Objects.equals(((Message) other).bytes, bytes) - && Objects.equals(((Message) other).string, string); - } - } - - static final class Ping { - public final ByteString payload; - - public Ping(ByteString payload) { - this.payload = payload; - } - - @Override public String toString() { - return "Ping[" + payload + "]"; - } - - @Override public int hashCode() { - return payload.hashCode(); - } - - @Override public boolean equals(Object other) { - return other instanceof Ping - && ((Ping) other).payload.equals(payload); - } - } - - static final class Pong { - public final ByteString payload; - - public Pong(ByteString payload) { - this.payload = payload; - } - - @Override public String toString() { - return "Pong[" + payload + "]"; - } - - @Override public int hashCode() { - return payload.hashCode(); - } - - @Override public boolean equals(Object other) { - return other instanceof Pong - && ((Pong) other).payload.equals(payload); - } - } - - static final class Closing { - public final int code; - public final String reason; - - Closing(int code, String reason) { - this.code = code; - this.reason = reason; - } - - @Override public String toString() { - return "Closing[" + code + " " + reason + "]"; - } - - @Override public int hashCode() { - return code * 37 + reason.hashCode(); - } - - @Override public boolean equals(Object other) { - return other instanceof Closing - && ((Closing) other).code == code - && ((Closing) other).reason.equals(reason); - } - } - - static final class Closed { - public final int code; - public final String reason; - - Closed(int code, String reason) { - this.code = code; - this.reason = reason; - } - - @Override public String toString() { - return "Closed[" + code + " " + reason + "]"; - } - - @Override public int hashCode() { - return code * 37 + reason.hashCode(); - } - - @Override public boolean equals(Object other) { - return other instanceof Closed - && ((Closed) other).code == code - && ((Closed) other).reason.equals(reason); - } - } -} diff --git a/okhttp/src/test/java/okhttp3/internal/ws/WebSocketRecorder.kt b/okhttp/src/test/java/okhttp3/internal/ws/WebSocketRecorder.kt new file mode 100644 index 000000000000..4aba0c594521 --- /dev/null +++ b/okhttp/src/test/java/okhttp3/internal/ws/WebSocketRecorder.kt @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2016 Square, Inc. + * + * 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 + * + * 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 okhttp3.internal.ws + +import java.io.IOException +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit +import okhttp3.Response +import okhttp3.WebSocket +import okhttp3.WebSocketListener +import okhttp3.internal.platform.Platform +import okio.ByteString +import org.assertj.core.api.Assertions.assertThat + +class WebSocketRecorder( + private val name: String, +) : WebSocketListener() { + private val events = LinkedBlockingQueue() + private var delegate: WebSocketListener? = null + + /** Sets a delegate for handling the next callback to this listener. Cleared after invoked. */ + fun setNextEventDelegate(delegate: WebSocketListener?) { + this.delegate = delegate + } + + override fun onOpen(webSocket: WebSocket, response: Response) { + Platform.get().log("[WS $name] onOpen", Platform.INFO, null) + val delegate = delegate + if (delegate != null) { + this.delegate = null + delegate.onOpen(webSocket, response) + } else { + events.add(Open(webSocket, response)) + } + } + + override fun onMessage(webSocket: WebSocket, bytes: ByteString) { + Platform.get().log("[WS $name] onMessage", Platform.INFO, null) + val delegate = delegate + if (delegate != null) { + this.delegate = null + delegate.onMessage(webSocket, bytes) + } else { + events.add(Message(bytes = bytes)) + } + } + + override fun onMessage(webSocket: WebSocket, text: String) { + Platform.get().log("[WS $name] onMessage", Platform.INFO, null) + val delegate = delegate + if (delegate != null) { + this.delegate = null + delegate.onMessage(webSocket, text) + } else { + events.add(Message(string = text)) + } + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + Platform.get().log("[WS $name] onClosing $code", Platform.INFO, null) + val delegate = delegate + if (delegate != null) { + this.delegate = null + delegate.onClosing(webSocket, code, reason) + } else { + events.add(Closing(code, reason)) + } + } + + override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { + Platform.get().log("[WS $name] onClosed $code", Platform.INFO, null) + val delegate = delegate + if (delegate != null) { + this.delegate = null + delegate.onClosed(webSocket, code, reason) + } else { + events.add(Closed(code, reason)) + } + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + Platform.get().log("[WS $name] onFailure", Platform.INFO, t) + val delegate = delegate + if (delegate != null) { + this.delegate = null + delegate.onFailure(webSocket, t, response) + } else { + events.add(Failure(t, response)) + } + } + + private fun nextEvent(): Any { + return events.poll(10, TimeUnit.SECONDS) + ?: throw AssertionError("Timed out waiting for event.") + } + + fun assertTextMessage(payload: String?) { + assertThat(nextEvent()).isEqualTo(Message(string = payload)) + } + + fun assertBinaryMessage(payload: ByteString?) { + assertThat(nextEvent()).isEqualTo(Message(payload)) + } + + fun assertPing(payload: ByteString) { + assertThat(nextEvent()).isEqualTo(Ping(payload)) + } + + fun assertPong(payload: ByteString) { + assertThat(nextEvent()).isEqualTo(Pong(payload)) + } + + fun assertClosing(code: Int, reason: String) { + assertThat(nextEvent()).isEqualTo(Closing(code, reason)) + } + + fun assertClosed(code: Int, reason: String) { + assertThat(nextEvent()).isEqualTo(Closed(code, reason)) + } + + fun assertExhausted() { + assertThat(events).isEmpty() + } + + fun assertOpen(): WebSocket { + val event = nextEvent() as Open + return event.webSocket + } + + fun assertFailure(t: Throwable?) { + val event = nextEvent() as Failure + assertThat(event.response).isNull() + assertThat(event.t).isSameAs(t) + } + + fun assertFailure(cls: Class?, vararg messages: String) { + val event = nextEvent() as Failure + assertThat(event.response).isNull() + assertThat(event.t.javaClass).isEqualTo(cls) + if (messages.isNotEmpty()) { + assertThat(messages).contains(event.t.message) + } + } + + fun assertFailure() { + nextEvent() as Failure + } + + fun assertFailure(code: Int, body: String?, cls: Class?, message: String?) { + val event = nextEvent() as Failure + assertThat(event.response!!.code).isEqualTo(code) + if (body != null) { + assertThat(event.responseBody).isEqualTo(body) + } + assertThat(event.t.javaClass).isEqualTo(cls) + assertThat(event.t.message).isEqualTo(message) + } + + /** Expose this recorder as a frame callback and shim in "ping" events. */ + fun asFrameCallback() = object : WebSocketReader.FrameCallback { + override fun onReadMessage(text: String) { + events.add(Message(string = text)) + } + + override fun onReadMessage(bytes: ByteString) { + events.add(Message(bytes = bytes)) + } + + override fun onReadPing(payload: ByteString) { + events.add(Ping(payload)) + } + + override fun onReadPong(payload: ByteString) { + events.add(Pong(payload)) + } + + override fun onReadClose(code: Int, reason: String) { + events.add(Closing(code, reason)) + } + } + + internal class Open( + val webSocket: WebSocket, + val response: Response, + ) + + internal class Failure( + val t: Throwable, + val response: Response?, + ) { + val responseBody: String? = when { + response != null && response.code != 101 -> response.body.string() + else -> null + } + + override fun toString(): String { + return when (response) { + null -> "Failure[$t]" + else -> "Failure[$response]" + } + } + } + + internal data class Message( + val bytes: ByteString? = null, + val string: String? = null, + ) + + internal data class Ping( + val payload: ByteString, + ) + + internal data class Pong( + val payload: ByteString, + ) + + internal data class Closing( + val code: Int, + val reason: String, + ) + + internal data class Closed( + val code: Int, + val reason: String, + ) +} diff --git a/okhttp/src/test/java/okhttp3/osgi/OsgiTest.java b/okhttp/src/test/java/okhttp3/osgi/OsgiTest.java deleted file mode 100644 index 60bb0925918e..000000000000 --- a/okhttp/src/test/java/okhttp3/osgi/OsgiTest.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (C) 2020 Square, Inc. - * - * 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 - * - * 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 okhttp3.osgi; - -import aQute.bnd.build.Project; -import aQute.bnd.build.Workspace; -import aQute.bnd.build.model.BndEditModel; -import aQute.bnd.deployer.repository.LocalIndexedRepo; -import aQute.bnd.osgi.Constants; -import aQute.bnd.service.RepositoryPlugin; -import biz.aQute.resolve.Bndrun; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; -import okio.BufferedSource; -import okio.Okio; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; - -@Tag("Slow") -public final class OsgiTest { - /** Each is the Bundle-SymbolicName of an OkHttp module's OSGi configuration. */ - private static final List REQUIRED_BUNDLES = Arrays.asList( - "com.squareup.okhttp3", - "com.squareup.okhttp3.brotli", - "com.squareup.okhttp3.dnsoverhttps", - "com.squareup.okhttp3.logging", - "com.squareup.okhttp3.sse", - "com.squareup.okhttp3.tls", - "com.squareup.okhttp3.urlconnection" - ); - - /** Equinox must also be on the testing classpath. */ - private static final String RESOLVE_OSGI_FRAMEWORK = "org.eclipse.osgi"; - private static final String RESOLVE_JAVA_VERSION = "JavaSE-1.8"; - private static final String REPO_NAME = "OsgiTest"; - - private File testResourceDir; - private File workspaceDir; - - @BeforeEach - public void setUp() throws Exception { - testResourceDir = new File("./build/resources/test/okhttp3/osgi"); - workspaceDir = new File(testResourceDir, "workspace"); - - // Ensure we start from scratch. - deleteDirectory(workspaceDir); - workspaceDir.mkdirs(); - } - - /** - * Resolve the OSGi metadata of the all okhttp3 modules. If required modules do not have OSGi - * metadata this will fail with an exception. - */ - @Test - public void testMainModuleWithSiblings() throws Exception { - try (Workspace workspace = createWorkspace(); - Bndrun bndRun = createBndRun(workspace)) { - bndRun.resolve(false, false); - } - } - - private Workspace createWorkspace() throws Exception { - File bndDir = new File(workspaceDir, "cnf"); - File repoDir = new File(bndDir, "repo"); - repoDir.mkdirs(); - - Workspace workspace = new Workspace(workspaceDir, bndDir.getName()); - workspace.setProperty(Constants.PLUGIN + "." + REPO_NAME, "" - + LocalIndexedRepo.class.getName() - + "; " + LocalIndexedRepo.PROP_NAME + " = '" + REPO_NAME + "'" - + "; " + LocalIndexedRepo.PROP_LOCAL_DIR + " = '" + repoDir + "'"); - workspace.refresh(); - prepareWorkspace(workspace); - return workspace; - } - - private void prepareWorkspace(Workspace workspace) throws Exception { - RepositoryPlugin repositoryPlugin = workspace.getRepository(REPO_NAME); - - // Deploy the bundles in the deployments test directory. - deployDirectory(repositoryPlugin, new File(testResourceDir, "deployments")); - deployClassPath(repositoryPlugin); - } - - private Bndrun createBndRun(Workspace workspace) throws Exception { - // Creating the run require string. It will always use the latest version of each bundle - // available in the repository. - String runRequireString = REQUIRED_BUNDLES.stream() - .map(s -> "osgi.identity;filter:='(osgi.identity=" + s + ")'") - .collect(Collectors.joining(",")); - - BndEditModel bndEditModel = new BndEditModel(workspace); - // Temporary project to satisfy bnd API. - bndEditModel.setProject(new Project(workspace, workspaceDir)); - - Bndrun result = new Bndrun(bndEditModel); - result.setRunfw(RESOLVE_OSGI_FRAMEWORK); - result.setRunee(RESOLVE_JAVA_VERSION); - result.setRunRequires(runRequireString); - return result; - } - - private void deployDirectory(RepositoryPlugin repository, File directory) throws Exception { - File[] files = directory.listFiles(); - if (files == null) return; - - for (File file : files) { - deployFile(repository, file); - } - } - - private void deployClassPath(RepositoryPlugin repositoryPlugin) throws Exception { - String classpath = System.getProperty("java.class.path"); - for (String classPathEntry : classpath.split(File.pathSeparator)) { - deployFile(repositoryPlugin, new File(classPathEntry)); - } - } - - private void deployFile(RepositoryPlugin repositoryPlugin, File file) throws Exception { - if (!file.exists() || file.isDirectory()) return; - - try (BufferedSource source = Okio.buffer(Okio.source(file))) { - repositoryPlugin.put(source.inputStream(), new RepositoryPlugin.PutOptions()); - System.out.println("Deployed " + file.getName()); - } catch (IllegalArgumentException e) { - if (e.getMessage().contains("Jar does not have a symbolic name")) { - System.out.println("Skipped non-OSGi dependency: " + file.getName()); - return; - } - throw e; - } - } - - private static void deleteDirectory(File dir) throws IOException { - if (!dir.exists()) return; - - Files.walk(dir.toPath()) - .filter(Files::isRegularFile) - .map(Path::toFile) - .forEach(File::delete); - } -} diff --git a/okhttp/src/test/java/okhttp3/osgi/OsgiTest.kt b/okhttp/src/test/java/okhttp3/osgi/OsgiTest.kt new file mode 100644 index 000000000000..55c8d792e3e5 --- /dev/null +++ b/okhttp/src/test/java/okhttp3/osgi/OsgiTest.kt @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 + * + * 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 okhttp3.osgi + +import aQute.bnd.build.Project +import aQute.bnd.build.Workspace +import aQute.bnd.build.model.BndEditModel +import aQute.bnd.deployer.repository.LocalIndexedRepo +import aQute.bnd.osgi.Constants +import aQute.bnd.service.RepositoryPlugin +import biz.aQute.resolve.Bndrun +import java.io.File +import okio.FileSystem +import okio.Path +import okio.Path.Companion.toPath +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test + +@Tag("Slow") +class OsgiTest { + private lateinit var testResourceDir: Path + private lateinit var workspaceDir: Path + + @BeforeEach + fun setUp() { + testResourceDir = "./build/resources/test/okhttp3/osgi".toPath() + workspaceDir = testResourceDir / "workspace" + + // Ensure we start from scratch. + fileSystem.deleteRecursively(workspaceDir) + fileSystem.createDirectories(workspaceDir) + } + + /** + * Resolve the OSGi metadata of the all okhttp3 modules. If required modules do not have OSGi + * metadata this will fail with an exception. + */ + @Test + fun testMainModuleWithSiblings() { + createWorkspace().use { workspace -> + createBndRun(workspace).use { bndRun -> + bndRun.resolve( + false, + false + ) + } + } + } + + private fun createWorkspace(): Workspace { + val bndDir = workspaceDir / "cnf" + val repoDir = bndDir / "repo" + fileSystem.createDirectories(repoDir) + return Workspace(workspaceDir.toFile(), bndDir.name) + .apply { + setProperty( + "${Constants.PLUGIN}.$REPO_NAME", + LocalIndexedRepo::class.java.getName() + + "; ${LocalIndexedRepo.PROP_NAME} = '$REPO_NAME'" + + "; ${LocalIndexedRepo.PROP_LOCAL_DIR} = '$repoDir'" + ) + refresh() + prepareWorkspace() + } + } + + private fun Workspace.prepareWorkspace() { + val repositoryPlugin = getRepository(REPO_NAME) + + // Deploy the bundles in the deployments test directory. + repositoryPlugin.deployDirectory(testResourceDir / "deployments") + repositoryPlugin.deployClassPath() + } + + private fun createBndRun(workspace: Workspace): Bndrun { + // Creating the run require string. It will always use the latest version of each bundle + // available in the repository. + val runRequireString = REQUIRED_BUNDLES.joinToString(separator = ",") { + "osgi.identity;filter:='(osgi.identity=$it)'" + } + + val bndEditModel = BndEditModel(workspace).apply { + // Temporary project to satisfy bnd API. + project = Project(workspace, workspaceDir.toFile()) + } + + return Bndrun(bndEditModel).apply { + setRunfw(RESOLVE_OSGI_FRAMEWORK) + runee = RESOLVE_JAVA_VERSION + setRunRequires(runRequireString) + } + } + + private fun RepositoryPlugin.deployDirectory(directory: Path) { + for (path in fileSystem.list(directory)) { + deployFile(path) + } + } + + private fun RepositoryPlugin.deployClassPath() { + val classpath = System.getProperty("java.class.path") + val entries = classpath.split(File.pathSeparator.toRegex()) + .dropLastWhile { it.isEmpty() } + .toTypedArray() + for (classPathEntry in entries) { + deployFile(classPathEntry.toPath()) + } + } + + private fun RepositoryPlugin.deployFile(file: Path) { + if (fileSystem.metadataOrNull(file)?.isRegularFile != true) return + try { + fileSystem.read(file) { + put(inputStream(), RepositoryPlugin.PutOptions()) + println("Deployed ${file.name}") + } + } catch (e: IllegalArgumentException) { + if ("Jar does not have a symbolic name" in e.message!!) { + println("Skipped non-OSGi dependency: ${file.name}") + return + } + throw e + } + } + + companion object { + val fileSystem = FileSystem.SYSTEM + + /** Each is the Bundle-SymbolicName of an OkHttp module's OSGi configuration. */ + private val REQUIRED_BUNDLES: List = mutableListOf( + "com.squareup.okhttp3", + "com.squareup.okhttp3.brotli", + "com.squareup.okhttp3.dnsoverhttps", + "com.squareup.okhttp3.logging", + "com.squareup.okhttp3.sse", + "com.squareup.okhttp3.tls", + "com.squareup.okhttp3.urlconnection" + ) + + /** Equinox must also be on the testing classpath. */ + private const val RESOLVE_OSGI_FRAMEWORK = "org.eclipse.osgi" + private const val RESOLVE_JAVA_VERSION = "JavaSE-1.8" + private const val REPO_NAME = "OsgiTest" + + } +}