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