1 // Copyright 2016 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.net; 6 7 import static com.google.common.truth.Truth.assertThat; 8 9 import static org.chromium.net.truth.UrlResponseInfoSubject.assertThat; 10 11 import androidx.test.ext.junit.runners.AndroidJUnit4; 12 import androidx.test.filters.SmallTest; 13 14 import org.json.JSONObject; 15 import org.junit.After; 16 import org.junit.Before; 17 import org.junit.Rule; 18 import org.junit.Test; 19 import org.junit.runner.RunWith; 20 21 import org.chromium.base.test.util.Batch; 22 import org.chromium.net.CronetTestRule.CronetImplementation; 23 import org.chromium.net.CronetTestRule.IgnoreFor; 24 25 import java.nio.ByteBuffer; 26 import java.util.Date; 27 28 /** Tests functionality of BidirectionalStream's QUIC implementation. */ 29 @RunWith(AndroidJUnit4.class) 30 @Batch(Batch.UNIT_TESTS) 31 @IgnoreFor( 32 implementations = {CronetImplementation.FALLBACK, CronetImplementation.AOSP_PLATFORM}, 33 reason = 34 "The fallback implementation doesn't support bidirectional streaming. " 35 + "crbug.com/1494870: Enable for AOSP_PLATFORM once fixed") 36 public class BidirectionalStreamQuicTest { 37 @Rule public final CronetTestRule mTestRule = CronetTestRule.withManualEngineStartup(); 38 39 private ExperimentalCronetEngine mCronetEngine; 40 41 @Before setUp()42 public void setUp() throws Exception { 43 mTestRule 44 .getTestFramework() 45 .applyEngineBuilderPatch( 46 (builder) -> { 47 QuicTestServer.startQuicTestServer( 48 mTestRule.getTestFramework().getContext()); 49 50 JSONObject quicParams = new JSONObject(); 51 JSONObject hostResolverParams = 52 CronetTestUtil.generateHostResolverRules(); 53 JSONObject experimentalOptions = 54 new JSONObject() 55 .put("QUIC", quicParams) 56 .put("HostResolverRules", hostResolverParams); 57 builder.setExperimentalOptions(experimentalOptions.toString()) 58 .addQuicHint( 59 QuicTestServer.getServerHost(), 60 QuicTestServer.getServerPort(), 61 QuicTestServer.getServerPort()); 62 63 CronetTestUtil.setMockCertVerifierForTesting( 64 builder, QuicTestServer.createMockCertVerifier()); 65 }); 66 67 mCronetEngine = mTestRule.getTestFramework().startEngine(); 68 } 69 70 @After tearDown()71 public void tearDown() throws Exception { 72 QuicTestServer.shutdownQuicTestServer(); 73 } 74 75 @Test 76 @SmallTest 77 // Test that QUIC is negotiated. testSimpleGet()78 public void testSimpleGet() throws Exception { 79 String path = "/simple.txt"; 80 String quicURL = QuicTestServer.getServerURL() + path; 81 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); 82 BidirectionalStream stream = 83 mCronetEngine 84 .newBidirectionalStreamBuilder(quicURL, callback, callback.getExecutor()) 85 .setHttpMethod("GET") 86 .build(); 87 stream.start(); 88 callback.blockForDone(); 89 assertThat(stream.isDone()).isTrue(); 90 assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200); 91 assertThat(callback.mResponseAsString) 92 .isEqualTo("This is a simple text file served by QUIC.\n"); 93 assertThat(callback.getResponseInfoWithChecks()) 94 .hasNegotiatedProtocolThat() 95 .isEqualTo("quic/1+spdy/3"); 96 } 97 98 @Test 99 @SmallTest testSimplePost()100 public void testSimplePost() throws Exception { 101 String path = "/simple.txt"; 102 String quicURL = QuicTestServer.getServerURL() + path; 103 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); 104 // Although we have no way to verify data sent at this point, this test 105 // can make sure that onWriteCompleted is invoked appropriately. 106 callback.addWriteData("Test String".getBytes()); 107 callback.addWriteData("1234567890".getBytes()); 108 callback.addWriteData("woot!".getBytes()); 109 TestRequestFinishedListener requestFinishedListener = new TestRequestFinishedListener(); 110 mCronetEngine.addRequestFinishedListener(requestFinishedListener); 111 BidirectionalStream stream = 112 mCronetEngine 113 .newBidirectionalStreamBuilder(quicURL, callback, callback.getExecutor()) 114 .addHeader("foo", "bar") 115 .addHeader("empty", "") 116 .addHeader("Content-Type", "zebra") 117 .addRequestAnnotation("request annotation") 118 .addRequestAnnotation(this) 119 .build(); 120 Date startTime = new Date(); 121 stream.start(); 122 callback.blockForDone(); 123 assertThat(stream.isDone()).isTrue(); 124 requestFinishedListener.blockUntilDone(); 125 Date endTime = new Date(); 126 RequestFinishedInfo finishedInfo = requestFinishedListener.getRequestInfo(); 127 MetricsTestUtil.checkRequestFinishedInfo(finishedInfo, quicURL, startTime, endTime); 128 assertThat(finishedInfo.getFinishedReason()).isEqualTo(RequestFinishedInfo.SUCCEEDED); 129 MetricsTestUtil.checkHasConnectTiming(finishedInfo.getMetrics(), startTime, endTime, true); 130 assertThat(finishedInfo.getAnnotations()).containsExactly("request annotation", this); 131 assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200); 132 assertThat(callback.mResponseAsString) 133 .isEqualTo("This is a simple text file served by QUIC.\n"); 134 assertThat(callback.getResponseInfoWithChecks()) 135 .hasNegotiatedProtocolThat() 136 .isEqualTo("quic/1+spdy/3"); 137 } 138 139 @Test 140 @SmallTest testSimplePostWithFlush()141 public void testSimplePostWithFlush() throws Exception { 142 // TODO(xunjieli): Use ParameterizedTest instead of the loop. 143 for (int i = 0; i < 2; i++) { 144 String path = "/simple.txt"; 145 String quicURL = QuicTestServer.getServerURL() + path; 146 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); 147 // Although we have no way to verify data sent at this point, this test 148 // can make sure that onWriteCompleted is invoked appropriately. 149 callback.addWriteData("Test String".getBytes(), false); 150 callback.addWriteData("1234567890".getBytes(), false); 151 callback.addWriteData("woot!".getBytes(), true); 152 BidirectionalStream stream = 153 mCronetEngine 154 .newBidirectionalStreamBuilder( 155 quicURL, callback, callback.getExecutor()) 156 .delayRequestHeadersUntilFirstFlush(i == 0) 157 .addHeader("foo", "bar") 158 .addHeader("empty", "") 159 .addHeader("Content-Type", "zebra") 160 .build(); 161 stream.start(); 162 callback.blockForDone(); 163 assertThat(stream.isDone()).isTrue(); 164 assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200); 165 assertThat(callback.mResponseAsString) 166 .isEqualTo("This is a simple text file served by QUIC.\n"); 167 assertThat(callback.getResponseInfoWithChecks()) 168 .hasNegotiatedProtocolThat() 169 .isEqualTo("quic/1+spdy/3"); 170 } 171 } 172 173 @Test 174 @SmallTest testSimplePostWithFlushTwice()175 public void testSimplePostWithFlushTwice() throws Exception { 176 // TODO(xunjieli): Use ParameterizedTest instead of the loop. 177 for (int i = 0; i < 2; i++) { 178 String path = "/simple.txt"; 179 String quicURL = QuicTestServer.getServerURL() + path; 180 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); 181 // Although we have no way to verify data sent at this point, this test 182 // can make sure that onWriteCompleted is invoked appropriately. 183 callback.addWriteData("Test String".getBytes(), false); 184 callback.addWriteData("1234567890".getBytes(), false); 185 callback.addWriteData("woot!".getBytes(), true); 186 callback.addWriteData("Test String".getBytes(), false); 187 callback.addWriteData("1234567890".getBytes(), false); 188 callback.addWriteData("woot!".getBytes(), true); 189 BidirectionalStream stream = 190 mCronetEngine 191 .newBidirectionalStreamBuilder( 192 quicURL, callback, callback.getExecutor()) 193 .delayRequestHeadersUntilFirstFlush(i == 0) 194 .addHeader("foo", "bar") 195 .addHeader("empty", "") 196 .addHeader("Content-Type", "zebra") 197 .build(); 198 stream.start(); 199 callback.blockForDone(); 200 assertThat(stream.isDone()).isTrue(); 201 assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200); 202 assertThat(callback.mResponseAsString) 203 .isEqualTo("This is a simple text file served by QUIC.\n"); 204 assertThat(callback.getResponseInfoWithChecks()) 205 .hasNegotiatedProtocolThat() 206 .isEqualTo("quic/1+spdy/3"); 207 } 208 } 209 210 @Test 211 @SmallTest testSimpleGetWithFlush()212 public void testSimpleGetWithFlush() throws Exception { 213 // TODO(xunjieli): Use ParameterizedTest instead of the loop. 214 for (int i = 0; i < 2; i++) { 215 String path = "/simple.txt"; 216 String url = QuicTestServer.getServerURL() + path; 217 218 TestBidirectionalStreamCallback callback = 219 new TestBidirectionalStreamCallback() { 220 @Override 221 public void onStreamReady(BidirectionalStream stream) { 222 // This flush should send the delayed headers. 223 stream.flush(); 224 super.onStreamReady(stream); 225 } 226 }; 227 BidirectionalStream stream = 228 mCronetEngine 229 .newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) 230 .setHttpMethod("GET") 231 .delayRequestHeadersUntilFirstFlush(i == 0) 232 .addHeader("foo", "bar") 233 .addHeader("empty", "") 234 .build(); 235 // Flush before stream is started should not crash. 236 stream.flush(); 237 238 stream.start(); 239 callback.blockForDone(); 240 assertThat(stream.isDone()).isTrue(); 241 242 // Flush after stream is completed is no-op. It shouldn't call into the destroyed 243 // adapter. 244 stream.flush(); 245 246 assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200); 247 assertThat(callback.mResponseAsString) 248 .isEqualTo("This is a simple text file served by QUIC.\n"); 249 assertThat(callback.getResponseInfoWithChecks()) 250 .hasNegotiatedProtocolThat() 251 .isEqualTo("quic/1+spdy/3"); 252 } 253 } 254 255 @Test 256 @SmallTest testSimplePostWithFlushAfterOneWrite()257 public void testSimplePostWithFlushAfterOneWrite() throws Exception { 258 // TODO(xunjieli): Use ParameterizedTest instead of the loop. 259 for (int i = 0; i < 2; i++) { 260 String path = "/simple.txt"; 261 String url = QuicTestServer.getServerURL() + path; 262 263 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); 264 callback.addWriteData("Test String".getBytes(), true); 265 BidirectionalStream stream = 266 mCronetEngine 267 .newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) 268 .delayRequestHeadersUntilFirstFlush(i == 0) 269 .addHeader("foo", "bar") 270 .addHeader("empty", "") 271 .addHeader("Content-Type", "zebra") 272 .build(); 273 stream.start(); 274 callback.blockForDone(); 275 assertThat(stream.isDone()).isTrue(); 276 277 assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200); 278 assertThat(callback.mResponseAsString) 279 .isEqualTo("This is a simple text file served by QUIC.\n"); 280 assertThat(callback.getResponseInfoWithChecks()) 281 .hasNegotiatedProtocolThat() 282 .isEqualTo("quic/1+spdy/3"); 283 } 284 } 285 286 @Test 287 @SmallTest 288 // Tests that if the stream failed between the time when we issue a Write() 289 // and when the Write() is executed in the native stack, there is no crash. 290 // This test is racy, but it should catch a crash (if there is any) most of 291 // the time. testStreamFailBeforeWriteIsExecutedOnNetworkThread()292 public void testStreamFailBeforeWriteIsExecutedOnNetworkThread() throws Exception { 293 String path = "/simple.txt"; 294 String quicURL = QuicTestServer.getServerURL() + path; 295 296 TestBidirectionalStreamCallback callback = 297 new TestBidirectionalStreamCallback() { 298 @Override 299 public void onWriteCompleted( 300 BidirectionalStream stream, 301 UrlResponseInfo info, 302 ByteBuffer buffer, 303 boolean endOfStream) { 304 // Super class will write the next piece of data. 305 super.onWriteCompleted(stream, info, buffer, endOfStream); 306 // Shut down the server, and the stream should error out. 307 // The second call to shutdownQuicTestServer is no-op. 308 QuicTestServer.shutdownQuicTestServer(); 309 } 310 }; 311 312 callback.addWriteData("Test String".getBytes()); 313 callback.addWriteData("1234567890".getBytes()); 314 callback.addWriteData("woot!".getBytes()); 315 316 BidirectionalStream stream = 317 mCronetEngine 318 .newBidirectionalStreamBuilder(quicURL, callback, callback.getExecutor()) 319 .addHeader("foo", "bar") 320 .addHeader("empty", "") 321 .addHeader("Content-Type", "zebra") 322 .build(); 323 stream.start(); 324 callback.blockForDone(); 325 assertThat(stream.isDone()).isTrue(); 326 // Server terminated on us, so the stream must fail. 327 // QUIC reports this as ERR_QUIC_PROTOCOL_ERROR. Sometimes we get ERR_CONNECTION_REFUSED. 328 assertThat(callback.mError).isInstanceOf(NetworkException.class); 329 NetworkException networkError = (NetworkException) callback.mError; 330 assertThat(networkError.getCronetInternalErrorCode()) 331 .isAnyOf(NetError.ERR_QUIC_PROTOCOL_ERROR, NetError.ERR_CONNECTION_REFUSED); 332 if (NetError.ERR_CONNECTION_REFUSED == networkError.getCronetInternalErrorCode()) return; 333 assertThat(callback.mError).isInstanceOf(QuicException.class); 334 } 335 336 @Test 337 @SmallTest testStreamFailWithQuicDetailedErrorCode()338 public void testStreamFailWithQuicDetailedErrorCode() throws Exception { 339 String path = "/simple.txt"; 340 String quicURL = QuicTestServer.getServerURL() + path; 341 TestBidirectionalStreamCallback callback = 342 new TestBidirectionalStreamCallback() { 343 @Override 344 public void onStreamReady(BidirectionalStream stream) { 345 // Shut down the server, and the stream should error out. 346 // The second call to shutdownQuicTestServer is no-op. 347 QuicTestServer.shutdownQuicTestServer(); 348 } 349 }; 350 BidirectionalStream stream = 351 mCronetEngine 352 .newBidirectionalStreamBuilder(quicURL, callback, callback.getExecutor()) 353 .setHttpMethod("GET") 354 .delayRequestHeadersUntilFirstFlush(true) 355 .addHeader("Content-Type", "zebra") 356 .build(); 357 stream.start(); 358 callback.blockForDone(); 359 assertThat(stream.isDone()).isTrue(); 360 assertThat(callback.mError).isNotNull(); 361 if (callback.mError instanceof QuicException) { 362 QuicException quicException = (QuicException) callback.mError; 363 // Checks that detailed quic error code is not QUIC_NO_ERROR == 0. 364 assertThat(quicException.getQuicDetailedErrorCode()).isGreaterThan(0); 365 } 366 } 367 } 368