• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.volley.cronet;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.mockito.ArgumentMatchers.anyString;
21 import static org.mockito.Mockito.never;
22 import static org.mockito.Mockito.verify;
23 import static org.mockito.Mockito.when;
24 
25 import com.android.volley.Header;
26 import com.android.volley.cronet.CronetHttpStack.CurlCommandLogger;
27 import com.android.volley.mock.TestRequest;
28 import com.android.volley.toolbox.AsyncHttpStack.OnRequestComplete;
29 import com.android.volley.toolbox.UrlRewriter;
30 import com.google.common.collect.ImmutableMap;
31 import com.google.common.util.concurrent.MoreExecutors;
32 import java.io.UnsupportedEncodingException;
33 import java.util.ArrayList;
34 import java.util.HashMap;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.function.Consumer;
38 import org.chromium.net.CronetEngine;
39 import org.junit.Before;
40 import org.junit.Test;
41 import org.junit.runner.RunWith;
42 import org.mockito.Answers;
43 import org.mockito.ArgumentCaptor;
44 import org.mockito.Mock;
45 import org.mockito.MockitoAnnotations;
46 import org.robolectric.RobolectricTestRunner;
47 import org.robolectric.RuntimeEnvironment;
48 
49 @RunWith(RobolectricTestRunner.class)
50 public class CronetHttpStackTest {
51     @Mock private CurlCommandLogger mMockCurlCommandLogger;
52     @Mock private OnRequestComplete mMockOnRequestComplete;
53     @Mock private UrlRewriter mMockUrlRewriter;
54 
55     // A fake would be ideal here, but Cronet doesn't (yet) provide one, and at the moment we aren't
56     // exercising the full response flow.
57     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
58     private CronetEngine mMockCronetEngine;
59 
60     @Before
setUp()61     public void setUp() {
62         MockitoAnnotations.initMocks(this);
63     }
64 
65     @Test
curlLogging_disabled()66     public void curlLogging_disabled() {
67         CronetHttpStack stack =
68                 createStack(
69                         new Consumer<CronetHttpStack.Builder>() {
70                             @Override
71                             public void accept(CronetHttpStack.Builder builder) {
72                                 // Default parameters should not enable cURL logging.
73                             }
74                         });
75 
76         stack.executeRequest(
77                 new TestRequest.Get(), ImmutableMap.<String, String>of(), mMockOnRequestComplete);
78 
79         verify(mMockCurlCommandLogger, never()).logCurlCommand(anyString());
80     }
81 
82     @Test
curlLogging_simpleTextRequest()83     public void curlLogging_simpleTextRequest() {
84         CronetHttpStack stack =
85                 createStack(
86                         new Consumer<CronetHttpStack.Builder>() {
87                             @Override
88                             public void accept(CronetHttpStack.Builder builder) {
89                                 builder.setCurlLoggingEnabled(true);
90                             }
91                         });
92 
93         stack.executeRequest(
94                 new TestRequest.Get(), ImmutableMap.<String, String>of(), mMockOnRequestComplete);
95 
96         ArgumentCaptor<String> curlCommandCaptor = ArgumentCaptor.forClass(String.class);
97         verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture());
98         assertEquals("curl -X GET \"http://foo.com\"", curlCommandCaptor.getValue());
99     }
100 
101     @Test
curlLogging_rewrittenUrl()102     public void curlLogging_rewrittenUrl() {
103         CronetHttpStack stack =
104                 createStack(
105                         new Consumer<CronetHttpStack.Builder>() {
106                             @Override
107                             public void accept(CronetHttpStack.Builder builder) {
108                                 builder.setCurlLoggingEnabled(true)
109                                         .setUrlRewriter(mMockUrlRewriter);
110                             }
111                         });
112         when(mMockUrlRewriter.rewriteUrl("http://foo.com")).thenReturn("http://bar.com");
113 
114         stack.executeRequest(
115                 new TestRequest.Get(), ImmutableMap.<String, String>of(), mMockOnRequestComplete);
116 
117         ArgumentCaptor<String> curlCommandCaptor = ArgumentCaptor.forClass(String.class);
118         verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture());
119         assertEquals("curl -X GET \"http://bar.com\"", curlCommandCaptor.getValue());
120     }
121 
122     @Test
curlLogging_headers_withoutTokens()123     public void curlLogging_headers_withoutTokens() {
124         CronetHttpStack stack =
125                 createStack(
126                         new Consumer<CronetHttpStack.Builder>() {
127                             @Override
128                             public void accept(CronetHttpStack.Builder builder) {
129                                 builder.setCurlLoggingEnabled(true);
130                             }
131                         });
132 
133         stack.executeRequest(
134                 new TestRequest.Delete() {
135                     @Override
136                     public Map<String, String> getHeaders() {
137                         return ImmutableMap.of(
138                                 "SomeHeader", "SomeValue",
139                                 "Authorization", "SecretToken");
140                     }
141                 },
142                 ImmutableMap.of("SomeOtherHeader", "SomeValue"),
143                 mMockOnRequestComplete);
144 
145         ArgumentCaptor<String> curlCommandCaptor = ArgumentCaptor.forClass(String.class);
146         verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture());
147         // NOTE: Header order is stable because the implementation uses a TreeMap.
148         assertEquals(
149                 "curl -X DELETE --header \"Authorization: [REDACTED]\" "
150                         + "--header \"SomeHeader: SomeValue\" "
151                         + "--header \"SomeOtherHeader: SomeValue\" \"http://foo.com\"",
152                 curlCommandCaptor.getValue());
153     }
154 
155     @Test
curlLogging_headers_withTokens()156     public void curlLogging_headers_withTokens() {
157         CronetHttpStack stack =
158                 createStack(
159                         new Consumer<CronetHttpStack.Builder>() {
160                             @Override
161                             public void accept(CronetHttpStack.Builder builder) {
162                                 builder.setCurlLoggingEnabled(true)
163                                         .setLogAuthTokensInCurlCommands(true);
164                             }
165                         });
166 
167         stack.executeRequest(
168                 new TestRequest.Delete() {
169                     @Override
170                     public Map<String, String> getHeaders() {
171                         return ImmutableMap.of(
172                                 "SomeHeader", "SomeValue",
173                                 "Authorization", "SecretToken");
174                     }
175                 },
176                 ImmutableMap.of("SomeOtherHeader", "SomeValue"),
177                 mMockOnRequestComplete);
178 
179         ArgumentCaptor<String> curlCommandCaptor = ArgumentCaptor.forClass(String.class);
180         verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture());
181         // NOTE: Header order is stable because the implementation uses a TreeMap.
182         assertEquals(
183                 "curl -X DELETE --header \"Authorization: SecretToken\" "
184                         + "--header \"SomeHeader: SomeValue\" "
185                         + "--header \"SomeOtherHeader: SomeValue\" \"http://foo.com\"",
186                 curlCommandCaptor.getValue());
187     }
188 
189     @Test
curlLogging_textRequest()190     public void curlLogging_textRequest() {
191         CronetHttpStack stack =
192                 createStack(
193                         new Consumer<CronetHttpStack.Builder>() {
194                             @Override
195                             public void accept(CronetHttpStack.Builder builder) {
196                                 builder.setCurlLoggingEnabled(true);
197                             }
198                         });
199 
200         stack.executeRequest(
201                 new TestRequest.PostWithBody() {
202                     @Override
203                     public byte[] getBody() {
204                         try {
205                             return "hello".getBytes("UTF-8");
206                         } catch (UnsupportedEncodingException e) {
207                             throw new RuntimeException(e);
208                         }
209                     }
210 
211                     @Override
212                     public String getBodyContentType() {
213                         return "text/plain; charset=UTF-8";
214                     }
215                 },
216                 ImmutableMap.<String, String>of(),
217                 mMockOnRequestComplete);
218 
219         ArgumentCaptor<String> curlCommandCaptor = ArgumentCaptor.forClass(String.class);
220         verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture());
221         assertEquals(
222                 "curl -X POST "
223                         + "--header \"Content-Type: text/plain; charset=UTF-8\" \"http://foo.com\" "
224                         + "--data-ascii \"hello\"",
225                 curlCommandCaptor.getValue());
226     }
227 
228     @Test
curlLogging_gzipTextRequest()229     public void curlLogging_gzipTextRequest() {
230         CronetHttpStack stack =
231                 createStack(
232                         new Consumer<CronetHttpStack.Builder>() {
233                             @Override
234                             public void accept(CronetHttpStack.Builder builder) {
235                                 builder.setCurlLoggingEnabled(true);
236                             }
237                         });
238 
239         stack.executeRequest(
240                 new TestRequest.PostWithBody() {
241                     @Override
242                     public byte[] getBody() {
243                         return new byte[] {1, 2, 3, 4, 5};
244                     }
245 
246                     @Override
247                     public String getBodyContentType() {
248                         return "text/plain";
249                     }
250 
251                     @Override
252                     public Map<String, String> getHeaders() {
253                         return ImmutableMap.of("Content-Encoding", "gzip, identity");
254                     }
255                 },
256                 ImmutableMap.<String, String>of(),
257                 mMockOnRequestComplete);
258 
259         ArgumentCaptor<String> curlCommandCaptor = ArgumentCaptor.forClass(String.class);
260         verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture());
261         assertEquals(
262                 "echo 'AQIDBAU=' | base64 -d > /tmp/$$.bin; curl -X POST "
263                         + "--header \"Content-Encoding: gzip, identity\" "
264                         + "--header \"Content-Type: text/plain\" \"http://foo.com\" "
265                         + "--data-binary @/tmp/$$.bin",
266                 curlCommandCaptor.getValue());
267     }
268 
269     @Test
curlLogging_binaryRequest()270     public void curlLogging_binaryRequest() {
271         CronetHttpStack stack =
272                 createStack(
273                         new Consumer<CronetHttpStack.Builder>() {
274                             @Override
275                             public void accept(CronetHttpStack.Builder builder) {
276                                 builder.setCurlLoggingEnabled(true);
277                             }
278                         });
279 
280         stack.executeRequest(
281                 new TestRequest.PostWithBody() {
282                     @Override
283                     public byte[] getBody() {
284                         return new byte[] {1, 2, 3, 4, 5};
285                     }
286 
287                     @Override
288                     public String getBodyContentType() {
289                         return "application/octet-stream";
290                     }
291                 },
292                 ImmutableMap.<String, String>of(),
293                 mMockOnRequestComplete);
294 
295         ArgumentCaptor<String> curlCommandCaptor = ArgumentCaptor.forClass(String.class);
296         verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture());
297         assertEquals(
298                 "echo 'AQIDBAU=' | base64 -d > /tmp/$$.bin; curl -X POST "
299                         + "--header \"Content-Type: application/octet-stream\" \"http://foo.com\" "
300                         + "--data-binary @/tmp/$$.bin",
301                 curlCommandCaptor.getValue());
302     }
303 
304     @Test
curlLogging_largeRequest()305     public void curlLogging_largeRequest() {
306         CronetHttpStack stack =
307                 createStack(
308                         new Consumer<CronetHttpStack.Builder>() {
309                             @Override
310                             public void accept(CronetHttpStack.Builder builder) {
311                                 builder.setCurlLoggingEnabled(true);
312                             }
313                         });
314 
315         stack.executeRequest(
316                 new TestRequest.PostWithBody() {
317                     @Override
318                     public byte[] getBody() {
319                         return new byte[2048];
320                     }
321 
322                     @Override
323                     public String getBodyContentType() {
324                         return "application/octet-stream";
325                     }
326                 },
327                 ImmutableMap.<String, String>of(),
328                 mMockOnRequestComplete);
329 
330         ArgumentCaptor<String> curlCommandCaptor = ArgumentCaptor.forClass(String.class);
331         verify(mMockCurlCommandLogger).logCurlCommand(curlCommandCaptor.capture());
332         assertEquals(
333                 "curl -X POST "
334                         + "--header \"Content-Type: application/octet-stream\" \"http://foo.com\" "
335                         + "[REQUEST BODY TOO LARGE TO INCLUDE]",
336                 curlCommandCaptor.getValue());
337     }
338 
339     @Test
getHeadersEmptyTest()340     public void getHeadersEmptyTest() {
341         List<Map.Entry<String, String>> list = new ArrayList<>();
342         List<Header> actual = CronetHttpStack.getHeaders(list);
343         List<Header> expected = new ArrayList<>();
344         assertEquals(expected, actual);
345     }
346 
347     @Test
getHeadersNonEmptyTest()348     public void getHeadersNonEmptyTest() {
349         Map<String, String> headers = new HashMap<>();
350         for (int i = 1; i < 5; i++) {
351             headers.put("key" + i, "value" + i);
352         }
353         List<Map.Entry<String, String>> list = new ArrayList<>(headers.entrySet());
354         List<Header> actual = CronetHttpStack.getHeaders(list);
355         List<Header> expected = new ArrayList<>();
356         for (int i = 1; i < 5; i++) {
357             expected.add(new Header("key" + i, "value" + i));
358         }
359         assertHeaderListsEqual(expected, actual);
360     }
361 
assertHeaderListsEqual(List<Header> expected, List<Header> actual)362     private void assertHeaderListsEqual(List<Header> expected, List<Header> actual) {
363         assertEquals(expected.size(), actual.size());
364         for (int i = 0; i < expected.size(); i++) {
365             assertEquals(expected.get(i).getName(), actual.get(i).getName());
366             assertEquals(expected.get(i).getValue(), actual.get(i).getValue());
367         }
368     }
369 
createStack(Consumer<CronetHttpStack.Builder> stackEditor)370     private CronetHttpStack createStack(Consumer<CronetHttpStack.Builder> stackEditor) {
371         CronetHttpStack.Builder builder =
372                 new CronetHttpStack.Builder(RuntimeEnvironment.application)
373                         .setCronetEngine(mMockCronetEngine)
374                         .setCurlCommandLogger(mMockCurlCommandLogger);
375         stackEditor.accept(builder);
376         CronetHttpStack stack = builder.build();
377         stack.setBlockingExecutor(MoreExecutors.newDirectExecutorService());
378         stack.setNonBlockingExecutor(MoreExecutors.newDirectExecutorService());
379         return stack;
380     }
381 }
382