• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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