1 /* 2 * Copyright (C) 2023 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.adservices.service.common.httpclient; 18 19 import static android.adservices.exceptions.RetryableAdServicesNetworkException.DEFAULT_RETRY_AFTER_VALUE; 20 21 import static com.android.adservices.service.common.httpclient.AdServicesHttpUtil.EMPTY_BODY; 22 23 import static com.google.common.truth.Truth.assertThat; 24 import static com.google.common.truth.Truth.assertWithMessage; 25 26 import static org.junit.Assert.assertThrows; 27 import static org.mockito.Mockito.any; 28 import static org.mockito.Mockito.doAnswer; 29 import static org.mockito.Mockito.doReturn; 30 import static org.mockito.Mockito.timeout; 31 import static org.mockito.Mockito.verify; 32 import static org.mockito.Mockito.when; 33 34 import android.adservices.exceptions.AdServicesNetworkException; 35 import android.adservices.exceptions.RetryableAdServicesNetworkException; 36 import android.adservices.http.MockWebServerRule; 37 import android.net.Uri; 38 39 import androidx.room.Room; 40 41 import com.android.adservices.MockWebServerRuleFactory; 42 import com.android.adservices.common.AdServicesMockitoTestCase; 43 import com.android.adservices.concurrency.AdServicesExecutors; 44 import com.android.adservices.service.Flags; 45 import com.android.adservices.service.common.WebAddresses; 46 import com.android.adservices.service.common.cache.CacheDatabase; 47 import com.android.adservices.service.common.cache.CacheEntryDao; 48 import com.android.adservices.service.common.cache.CacheProviderFactory; 49 import com.android.adservices.service.common.cache.FledgeHttpCache; 50 import com.android.adservices.service.common.cache.HttpCache; 51 import com.android.adservices.service.devapi.DevContext; 52 import com.android.adservices.service.stats.AdServicesLogger; 53 import com.android.adservices.service.stats.AdServicesLoggerImpl; 54 import com.android.adservices.service.stats.SelectAdsFromOutcomesApiCalledStats; 55 import com.android.adservices.service.stats.SelectAdsFromOutcomesExecutionLogger; 56 import com.android.adservices.service.stats.SelectAdsFromOutcomesExecutionLoggerImpl; 57 import com.android.adservices.shared.util.Clock; 58 59 import com.google.common.collect.ImmutableList; 60 import com.google.common.collect.ImmutableMap; 61 import com.google.common.collect.ImmutableSet; 62 import com.google.common.io.BaseEncoding; 63 import com.google.common.util.concurrent.ListenableFuture; 64 import com.google.common.util.concurrent.MoreExecutors; 65 import com.google.mockwebserver.Dispatcher; 66 import com.google.mockwebserver.MockResponse; 67 import com.google.mockwebserver.MockWebServer; 68 import com.google.mockwebserver.RecordedRequest; 69 70 import org.json.JSONObject; 71 import org.junit.Before; 72 import org.junit.Rule; 73 import org.junit.Test; 74 import org.mockito.ArgumentCaptor; 75 import org.mockito.Mock; 76 import org.mockito.Mockito; 77 import org.mockito.internal.stubbing.answers.AnswersWithDelay; 78 import org.mockito.internal.stubbing.answers.Returns; 79 80 import java.io.IOException; 81 import java.io.InputStream; 82 import java.net.URL; 83 import java.nio.charset.StandardCharsets; 84 import java.time.Duration; 85 import java.util.List; 86 import java.util.Map; 87 import java.util.concurrent.ExecutionException; 88 import java.util.concurrent.ExecutorService; 89 import java.util.concurrent.TimeUnit; 90 91 import javax.net.ssl.HttpsURLConnection; 92 93 public final class AdServicesHttpsClientTest extends AdServicesMockitoTestCase { 94 private static final String CACHE_HEADER = "Cache-Control: max-age=60"; 95 private static final String NO_CACHE_HEADER = "Cache-Control: no-cache"; 96 private static final String RESPONSE_HEADER_KEY = "fake_response_header_key"; 97 private static final String REQUEST_PROPERTY_KEY = "X_REQUEST_KEY"; 98 private static final String REQUEST_PROPERTY_VALUE = "Fake_Value"; 99 private static final long MAX_AGE_SECONDS = 120; 100 private static final long MAX_ENTRIES = 20; 101 private static final DevContext DEV_CONTEXT_DISABLED = DevContext.createForDevOptionsDisabled(); 102 private static final DevContext DEV_CONTEXT_ENABLED = 103 DevContext.builder(sPackageName).setDeviceDevOptionsEnabled(true).build(); 104 105 private final ExecutorService mExecutorService = MoreExecutors.newDirectExecutorService(); 106 private final String mJsScript = "function test() { return \"hello world\"; }"; 107 private final String mReportingPath = "/reporting/"; 108 private final String mFetchPayloadPath = "/fetchPayload/"; 109 private final String mFakeUrl = "https://fakeprivacysandboxdomain.never/this/is/a/fake"; 110 111 @Rule public MockWebServerRule mMockWebServerRule = MockWebServerRuleFactory.createForHttps(); 112 private AdServicesHttpsClient mClient; 113 @Mock private AdServicesHttpsClient.UriConverter mUriConverterMock; 114 @Mock private URL mUrlMock; 115 @Mock private HttpsURLConnection mURLConnectionMock; 116 @Mock private InputStream mInputStreamMock; 117 @Mock private Clock mMockClock; 118 private HttpCache mCache; 119 private String mData; 120 private AdServicesLogger mAdServicesLoggerSpy; 121 private final long mStartDownloadTimestamp = 98L; 122 private final long mEndDownloadTimestamp = 199L; 123 124 @Before setup()125 public void setup() throws Exception { 126 CacheEntryDao cacheEntryDao = 127 Room.inMemoryDatabaseBuilder(mContext, CacheDatabase.class) 128 .build() 129 .getCacheEntryDao(); 130 131 mCache = new FledgeHttpCache(cacheEntryDao, MAX_AGE_SECONDS, MAX_ENTRIES); 132 mClient = new AdServicesHttpsClient(mExecutorService, mCache); 133 mData = new JSONObject().put("key", "value").toString(); 134 } 135 136 @Test testGetAndReadNothingSuccessfulResponse()137 public void testGetAndReadNothingSuccessfulResponse() throws Exception { 138 MockWebServer server = 139 mMockWebServerRule.startMockWebServer(ImmutableList.of(new MockResponse())); 140 URL url = server.getUrl(mReportingPath); 141 142 assertThat(getAndReadNothing(Uri.parse(url.toString()), DEV_CONTEXT_DISABLED)).isNull(); 143 } 144 145 @Test testGetAndReadNothingSuccessfulResponse_DevOptionsEnabled()146 public void testGetAndReadNothingSuccessfulResponse_DevOptionsEnabled() throws Exception { 147 MockWebServer server = 148 mMockWebServerRule.startMockWebServer(ImmutableList.of(new MockResponse())); 149 URL url = server.getUrl(mReportingPath); 150 151 assertThat(getAndReadNothing(Uri.parse(url.toString()), DEV_CONTEXT_ENABLED)).isNull(); 152 } 153 154 @Test testGetAndReadNothingCorrectPath()155 public void testGetAndReadNothingCorrectPath() throws Exception { 156 MockWebServer server = 157 mMockWebServerRule.startMockWebServer(ImmutableList.of(new MockResponse())); 158 URL url = server.getUrl(mReportingPath); 159 getAndReadNothing(Uri.parse(url.toString()), DEV_CONTEXT_DISABLED); 160 161 RecordedRequest request1 = server.takeRequest(); 162 expect.that(request1.getPath()).isEqualTo(mReportingPath); 163 expect.that(request1.getMethod()).isEqualTo("GET"); 164 } 165 166 @Test testGetAndReadNothingFailedResponse()167 public void testGetAndReadNothingFailedResponse() throws Exception { 168 MockWebServer server = 169 mMockWebServerRule.startMockWebServer( 170 ImmutableList.of(new MockResponse().setResponseCode(305))); 171 URL url = server.getUrl(mReportingPath); 172 173 Exception exception = 174 assertThrows( 175 ExecutionException.class, 176 () -> getAndReadNothing(Uri.parse(url.toString()), DEV_CONTEXT_DISABLED)); 177 assertThat(exception.getCause()).isInstanceOf(AdServicesNetworkException.class); 178 } 179 180 @Test testGetAndReadNothingDomainDoesNotExist()181 public void testGetAndReadNothingDomainDoesNotExist() throws Exception { 182 mMockWebServerRule.startMockWebServer(ImmutableList.of(new MockResponse())); 183 184 Exception exception = 185 assertThrows( 186 ExecutionException.class, 187 () -> getAndReadNothing(Uri.parse(mFakeUrl), DEV_CONTEXT_DISABLED)); 188 assertThat(exception.getCause()).isInstanceOf(IOException.class); 189 } 190 191 @Test testGetAndReadNothingThrowsExceptionIfUsingPlainTextHttp()192 public void testGetAndReadNothingThrowsExceptionIfUsingPlainTextHttp() { 193 ExecutionException wrapperExecutionException = 194 assertThrows( 195 ExecutionException.class, 196 () -> 197 getAndReadNothing( 198 Uri.parse("http://google.com"), DEV_CONTEXT_DISABLED)); 199 200 assertThat(wrapperExecutionException.getCause()) 201 .isInstanceOf(IllegalArgumentException.class); 202 } 203 204 @Test testFetchPayloadSuccessfulResponse()205 public void testFetchPayloadSuccessfulResponse() throws Exception { 206 MockWebServer server = 207 mMockWebServerRule.startMockWebServer( 208 ImmutableList.of(new MockResponse().setBody(mJsScript))); 209 URL url = server.getUrl(mFetchPayloadPath); 210 211 AdServicesHttpClientResponse result = 212 fetchPayload(Uri.parse(url.toString()), DEV_CONTEXT_DISABLED); 213 expect.that(result.getResponseBody()).isEqualTo(mJsScript); 214 } 215 216 @Test testFetchPayloadSuccessfulResponse_DevOptionsEnabled()217 public void testFetchPayloadSuccessfulResponse_DevOptionsEnabled() throws Exception { 218 MockWebServer server = 219 mMockWebServerRule.startMockWebServer( 220 ImmutableList.of(new MockResponse().setBody(mJsScript))); 221 URL url = server.getUrl(mFetchPayloadPath); 222 223 AdServicesHttpClientResponse result = 224 fetchPayload(Uri.parse(url.toString()), DEV_CONTEXT_ENABLED); 225 expect.that(result.getResponseBody()).isEqualTo(mJsScript); 226 } 227 228 @Test testFetchPayloadCorrectPath()229 public void testFetchPayloadCorrectPath() throws Exception { 230 MockWebServer server = 231 mMockWebServerRule.startMockWebServer( 232 ImmutableList.of(new MockResponse().setBody(mJsScript))); 233 URL url = server.getUrl(mFetchPayloadPath); 234 fetchPayload(Uri.parse(url.toString()), DEV_CONTEXT_DISABLED); 235 236 RecordedRequest request1 = server.takeRequest(); 237 expect.that(request1.getPath()).isEqualTo(mFetchPayloadPath); 238 expect.that(request1.getMethod()).isEqualTo("GET"); 239 } 240 241 @Test testFetchPayloadFailedResponse()242 public void testFetchPayloadFailedResponse() throws Exception { 243 MockWebServer server = 244 mMockWebServerRule.startMockWebServer( 245 ImmutableList.of(new MockResponse().setResponseCode(305))); 246 URL url = server.getUrl(mFetchPayloadPath); 247 248 Exception exception = 249 assertThrows( 250 ExecutionException.class, 251 () -> fetchPayload(Uri.parse(url.toString()), DEV_CONTEXT_DISABLED)); 252 assertThat(exception.getCause()).isInstanceOf(AdServicesNetworkException.class); 253 } 254 255 @Test testFetchPayloadDomainDoesNotExist()256 public void testFetchPayloadDomainDoesNotExist() throws Exception { 257 mMockWebServerRule.startMockWebServer(ImmutableList.of(new MockResponse())); 258 259 Exception exception = 260 assertThrows( 261 ExecutionException.class, 262 () -> fetchPayload(Uri.parse(mFakeUrl), DEV_CONTEXT_DISABLED)); 263 assertThat(exception.getCause()).isInstanceOf(IOException.class); 264 } 265 266 @Test testThrowsIOExceptionWhenConnectionTimesOut()267 public void testThrowsIOExceptionWhenConnectionTimesOut() throws Exception { 268 int timeoutDeltaMs = 1000; 269 int bytesPerPeriod = 1; 270 MockWebServer server = 271 mMockWebServerRule.startMockWebServer( 272 ImmutableList.of( 273 new MockResponse() 274 .setBody(mJsScript) 275 .throttleBody( 276 bytesPerPeriod, 277 mClient.getConnectTimeoutMs() 278 + mClient.getReadTimeoutMs() 279 + timeoutDeltaMs, 280 TimeUnit.MILLISECONDS))); 281 URL url = server.getUrl(mFetchPayloadPath); 282 283 Exception exception = 284 assertThrows( 285 ExecutionException.class, 286 () -> fetchPayload(Uri.parse(url.toString()), DEV_CONTEXT_DISABLED)); 287 assertThat(exception.getCause()).isInstanceOf(IOException.class); 288 } 289 290 @Test testFetchPayloadThrowsExceptionIfUsingPlainTextHttp()291 public void testFetchPayloadThrowsExceptionIfUsingPlainTextHttp() { 292 Exception wrapperExecutionException = 293 assertThrows( 294 ExecutionException.class, 295 () -> fetchPayload(Uri.parse("http://google.com"), DEV_CONTEXT_DISABLED)); 296 297 assertThat(wrapperExecutionException.getCause()) 298 .isInstanceOf(IllegalArgumentException.class); 299 } 300 301 @Test testInputStreamToStringThrowsExceptionWhenExceedingMaxSize()302 public void testInputStreamToStringThrowsExceptionWhenExceedingMaxSize() throws Exception { 303 // Creating a client with a max byte size of 5; 304 int defaultTimeoutMs = 5000; 305 mClient = 306 new AdServicesHttpsClient(mExecutorService, defaultTimeoutMs, defaultTimeoutMs, 5); 307 308 // Setting a response of size 6 309 MockWebServer server = 310 mMockWebServerRule.startMockWebServer( 311 ImmutableList.of(new MockResponse().setBody("123456"))); 312 URL url = server.getUrl(mFetchPayloadPath); 313 314 Exception exception = 315 assertThrows( 316 ExecutionException.class, 317 () -> fetchPayload(Uri.parse(url.toString()), DEV_CONTEXT_DISABLED)); 318 assertThat(exception.getCause()).isInstanceOf(IOException.class); 319 } 320 321 @Test testHttpsClientFreesResourcesWhenCancelled()322 public void testHttpsClientFreesResourcesWhenCancelled() throws Exception { 323 // Creating a client with large default limits 324 int defaultTimeoutMs = 5000; 325 int defaultMaxSizeBytes = 5000; 326 int delayMs = 4000; 327 long waitForEventualCompletionMs = delayMs * 4L; 328 mClient = 329 new AdServicesHttpsClient( 330 AdServicesExecutors.getBackgroundExecutor(), 331 defaultTimeoutMs, 332 defaultTimeoutMs, 333 defaultMaxSizeBytes, 334 mUriConverterMock, 335 mCache); 336 337 doReturn(mUrlMock).when(mUriConverterMock).toUrl(any(Uri.class)); 338 doReturn(mURLConnectionMock).when(mUrlMock).openConnection(); 339 doReturn(mInputStreamMock).when(mURLConnectionMock).getInputStream(); 340 doAnswer(new AnswersWithDelay(delayMs, new Returns(202))) 341 .when(mURLConnectionMock) 342 .getResponseCode(); 343 344 ListenableFuture<AdServicesHttpClientResponse> futureResponse = 345 mClient.fetchPayload(Uri.parse((mFakeUrl)), DEV_CONTEXT_DISABLED); 346 347 // There could be some lag between fetch call and connection opening 348 verify(mUrlMock, timeout(delayMs)).openConnection(); 349 // We cancel the future while the request is going on 350 assertWithMessage("The request should have been ongoing, until being force-cancelled now") 351 .that(futureResponse.cancel(true)) 352 .isTrue(); 353 // Given the resources are set to be eventually closed, we add a timeout 354 verify(mURLConnectionMock, timeout(waitForEventualCompletionMs).atLeast(1)).disconnect(); 355 verify(mInputStreamMock, timeout(waitForEventualCompletionMs).atLeast(1)).close(); 356 } 357 358 @Test testHttpsClientFreesResourcesInNormalFlow()359 public void testHttpsClientFreesResourcesInNormalFlow() throws Exception { 360 // Creating a client with large default limits 361 int defaultTimeoutMs = 5000; 362 int defaultMaxSizeBytes = 5000; 363 int delayMs = 2000; 364 long waitForEventualCompletionMs = delayMs * 4L; 365 mClient = 366 new AdServicesHttpsClient( 367 AdServicesExecutors.getBackgroundExecutor(), 368 defaultTimeoutMs, 369 defaultTimeoutMs, 370 defaultMaxSizeBytes, 371 mUriConverterMock, 372 mCache); 373 374 doReturn(mUrlMock).when(mUriConverterMock).toUrl(any(Uri.class)); 375 doReturn(mURLConnectionMock).when(mUrlMock).openConnection(); 376 doReturn(mInputStreamMock).when(mURLConnectionMock).getInputStream(); 377 doReturn(202).when(mURLConnectionMock).getResponseCode(); 378 379 ListenableFuture<AdServicesHttpClientResponse> futureResponse = 380 mClient.fetchPayload(Uri.parse((mFakeUrl)), DEV_CONTEXT_DISABLED); 381 382 // There could be some lag between fetch call and connection opening 383 verify(mUrlMock, timeout(delayMs)).openConnection(); 384 // Given the resources are set to be eventually closed, we add a timeout 385 verify(mInputStreamMock, timeout(waitForEventualCompletionMs).atLeast(1)).close(); 386 verify(mURLConnectionMock, timeout(waitForEventualCompletionMs).atLeast(1)).disconnect(); 387 assertWithMessage("The future response for fetchPayload should have been completed") 388 .that(futureResponse.isDone()) 389 .isTrue(); 390 } 391 392 @Test testFetchPayloadResponsesSkipsHeaderIfAbsent()393 public void testFetchPayloadResponsesSkipsHeaderIfAbsent() throws Exception { 394 MockWebServer server = 395 mMockWebServerRule.startMockWebServer( 396 new Dispatcher() { 397 @Override 398 public MockResponse dispatch(RecordedRequest request) { 399 return new MockResponse().setBody(mJsScript); 400 } 401 }); 402 URL url = server.getUrl(mFetchPayloadPath); 403 AdServicesHttpClientResponse response = 404 mClient.fetchPayload( 405 AdServicesHttpClientRequest.builder() 406 .setUri(Uri.parse(url.toString())) 407 .setUseCache(false) 408 .setResponseHeaderKeys(ImmutableSet.of(RESPONSE_HEADER_KEY)) 409 .setDevContext(DEV_CONTEXT_DISABLED) 410 .build()) 411 .get(); 412 expect.that(response.getResponseBody()).isEqualTo(mJsScript); 413 expect.withMessage("No header should have been returned") 414 .that(response.getResponseHeaders()) 415 .hasSize(0); 416 } 417 418 @Test testFetchPayloadContainsRequestProperties()419 public void testFetchPayloadContainsRequestProperties() throws Exception { 420 MockWebServer server = 421 mMockWebServerRule.startMockWebServer( 422 new Dispatcher() { 423 @Override 424 public MockResponse dispatch(RecordedRequest request) { 425 assertWithMessage("Request header mismatch") 426 .that(request.getHeader(REQUEST_PROPERTY_KEY)) 427 .isEqualTo(REQUEST_PROPERTY_VALUE); 428 return new MockResponse().setBody(mJsScript); 429 } 430 }); 431 URL url = server.getUrl(mFetchPayloadPath); 432 mClient.fetchPayload( 433 AdServicesHttpClientRequest.builder() 434 .setUri(Uri.parse(url.toString())) 435 .setUseCache(false) 436 .setRequestProperties( 437 ImmutableMap.of( 438 REQUEST_PROPERTY_KEY, REQUEST_PROPERTY_VALUE)) 439 .setDevContext(DEV_CONTEXT_DISABLED) 440 .build()) 441 .get(); 442 } 443 444 @Test testAdServiceRequestResponseDefault_Empty()445 public void testAdServiceRequestResponseDefault_Empty() { 446 AdServicesHttpClientRequest request = 447 AdServicesHttpClientRequest.builder() 448 .setUri(Uri.EMPTY) 449 .setDevContext(DEV_CONTEXT_DISABLED) 450 .build(); 451 452 expect.that(request.getRequestProperties()).isEmpty(); 453 expect.that(request.getResponseHeaderKeys()).isEmpty(); 454 expect.that(request.getUseCache()).isFalse(); 455 456 AdServicesHttpClientResponse response = 457 AdServicesHttpClientResponse.builder().setResponseBody("").build(); 458 459 expect.that(response.getResponseHeaders()).isEmpty(); 460 } 461 462 @Test testCreateAdServicesRequestResponse_Success()463 public void testCreateAdServicesRequestResponse_Success() { 464 Uri uri = Uri.parse("www.google.com"); 465 ImmutableMap<String, String> requestProperties = ImmutableMap.of("key", "value"); 466 ImmutableSet<String> responseHeaderKeys = ImmutableSet.of("entry1", "entry2"); 467 468 AdServicesHttpClientRequest request = 469 AdServicesHttpClientRequest.create( 470 uri, 471 requestProperties, 472 responseHeaderKeys, 473 false, 474 DEV_CONTEXT_DISABLED, 475 AdServicesHttpUtil.HttpMethodType.GET, 476 EMPTY_BODY); 477 478 expect.that(request.getUri()).isEqualTo(uri); 479 expect.that(request.getRequestProperties()).isEqualTo(requestProperties); 480 expect.that(request.getResponseHeaderKeys()).isEqualTo(responseHeaderKeys); 481 expect.that(request.getUseCache()).isFalse(); 482 483 String body = "Fake response body"; 484 ImmutableMap<String, List<String>> responseHeaders = 485 ImmutableMap.of("key", List.of("value1", "value2")); 486 AdServicesHttpClientResponse response = 487 AdServicesHttpClientResponse.create(body, responseHeaders); 488 489 expect.that(response.getResponseBody()).isEqualTo(body); 490 expect.that(response.getResponseHeaders()).isEqualTo(responseHeaders); 491 } 492 493 @Test testCreateAdServicesRequestResponse_Success_DevOptionsEnabled()494 public void testCreateAdServicesRequestResponse_Success_DevOptionsEnabled() { 495 Uri uri = Uri.parse("www.google.com"); 496 ImmutableMap<String, String> requestProperties = ImmutableMap.of("key", "value"); 497 ImmutableSet<String> responseHeaderKeys = ImmutableSet.of("entry1", "entry2"); 498 499 AdServicesHttpClientRequest request = 500 AdServicesHttpClientRequest.create( 501 uri, 502 requestProperties, 503 responseHeaderKeys, 504 false, 505 DEV_CONTEXT_ENABLED, 506 AdServicesHttpUtil.HttpMethodType.GET, 507 EMPTY_BODY); 508 509 expect.that(request.getUri()).isEqualTo(uri); 510 expect.that(request.getRequestProperties()).isEqualTo(requestProperties); 511 expect.that(request.getResponseHeaderKeys()).isEqualTo(responseHeaderKeys); 512 expect.that(request.getUseCache()).isFalse(); 513 514 String body = "Fake response body"; 515 ImmutableMap<String, List<String>> responseHeaders = 516 ImmutableMap.of("key", List.of("value1", "value2")); 517 AdServicesHttpClientResponse response = 518 AdServicesHttpClientResponse.create(body, responseHeaders); 519 520 expect.that(response.getResponseBody()).isEqualTo(body); 521 expect.that(response.getResponseHeaders()).isEqualTo(responseHeaders); 522 } 523 524 @Test testFetchPayloadResponsesDefaultSkipsCache()525 public void testFetchPayloadResponsesDefaultSkipsCache() throws Exception { 526 MockWebServer server = 527 mMockWebServerRule.startMockWebServer( 528 new Dispatcher() { 529 @Override 530 public MockResponse dispatch(RecordedRequest request) { 531 return new MockResponse() 532 .setBody(mJsScript) 533 .addHeader(CACHE_HEADER); 534 } 535 }); 536 URL url = server.getUrl(mFetchPayloadPath); 537 538 mClient.fetchPayload(Uri.parse(url.toString()), DEV_CONTEXT_DISABLED); 539 540 RecordedRequest request1 = server.takeRequest(); 541 expect.that(request1.getPath()).isEqualTo(mFetchPayloadPath); 542 expect.that(request1.getMethod()).isEqualTo("GET"); 543 expect.that(server.getRequestCount()).isEqualTo(1); 544 545 AdServicesHttpClientResponse response = 546 fetchPayload(Uri.parse(url.toString()), DEV_CONTEXT_DISABLED); 547 expect.that(response.getResponseBody()).isEqualTo(mJsScript); 548 expect.withMessage("This call should not have been cached") 549 .that(server.getRequestCount()) 550 .isEqualTo(2); 551 } 552 553 @Test testFetchPayloadResponsesNoCacheHeaderSkipsCache()554 public void testFetchPayloadResponsesNoCacheHeaderSkipsCache() throws Exception { 555 MockWebServer server = 556 mMockWebServerRule.startMockWebServer( 557 new Dispatcher() { 558 @Override 559 public MockResponse dispatch(RecordedRequest request) { 560 return new MockResponse() 561 .setBody(mJsScript) 562 .addHeader(NO_CACHE_HEADER); 563 } 564 }); 565 URL url = server.getUrl(mFetchPayloadPath); 566 567 mClient.fetchPayload(Uri.parse(url.toString()), DEV_CONTEXT_DISABLED); 568 569 RecordedRequest request1 = server.takeRequest(); 570 expect.that(request1.getPath()).isEqualTo(mFetchPayloadPath); 571 expect.that(request1.getMethod()).isEqualTo("GET"); 572 expect.that(server.getRequestCount()).isEqualTo(1); 573 574 AdServicesHttpClientResponse response = 575 fetchPayload(Uri.parse(url.toString()), DEV_CONTEXT_DISABLED); 576 expect.that(response.getResponseBody()).isEqualTo(mJsScript); 577 expect.withMessage("This call should not have been cached") 578 .that(server.getRequestCount()) 579 .isEqualTo(2); 580 } 581 582 @Test testFetchPayloadCacheDisabledSkipsCache()583 public void testFetchPayloadCacheDisabledSkipsCache() throws Exception { 584 MockWebServer server = 585 mMockWebServerRule.startMockWebServer( 586 new Dispatcher() { 587 @Override 588 public MockResponse dispatch(RecordedRequest request) { 589 return new MockResponse() 590 .setBody(mJsScript) 591 .addHeader(CACHE_HEADER); 592 } 593 }); 594 URL url = server.getUrl(mFetchPayloadPath); 595 596 Flags disableCacheFlags = 597 new Flags() { 598 @Override 599 public boolean getFledgeHttpCachingEnabled() { 600 return false; 601 } 602 }; 603 HttpCache cache = CacheProviderFactory.create(mContext, disableCacheFlags); 604 AdServicesHttpsClient client = new AdServicesHttpsClient(mExecutorService, cache); 605 606 client.fetchPayload( 607 AdServicesHttpClientRequest.builder() 608 .setUri(Uri.parse(url.toString())) 609 .setUseCache(true) 610 .setDevContext(DEV_CONTEXT_DISABLED) 611 .build()); 612 613 RecordedRequest request1 = server.takeRequest(); 614 expect.that(request1.getPath()).isEqualTo(mFetchPayloadPath); 615 expect.that(request1.getMethod()).isEqualTo("GET"); 616 expect.that(server.getRequestCount()).isEqualTo(1); 617 618 AdServicesHttpClientResponse response = 619 client.fetchPayload( 620 AdServicesHttpClientRequest.builder() 621 .setUri(Uri.parse(url.toString())) 622 .setUseCache(true) 623 .setDevContext(DEV_CONTEXT_DISABLED) 624 .build()) 625 .get(); 626 expect.that(response.getResponseBody()).isEqualTo(mJsScript); 627 expect.withMessage("This call should not have been cached") 628 .that(server.getRequestCount()) 629 .isEqualTo(2); 630 } 631 632 @Test testPostJsonSuccessfulResponse()633 public void testPostJsonSuccessfulResponse() throws Exception { 634 MockWebServer server = 635 mMockWebServerRule.startMockWebServer(ImmutableList.of(new MockResponse())); 636 URL url = server.getUrl(mReportingPath); 637 assertThat(postJson(Uri.parse(url.toString()), mData, DEV_CONTEXT_DISABLED)).isNull(); 638 } 639 640 @Test testPostJsonCorrectPath()641 public void testPostJsonCorrectPath() throws Exception { 642 MockWebServer server = 643 mMockWebServerRule.startMockWebServer(ImmutableList.of(new MockResponse())); 644 URL url = server.getUrl(mReportingPath); 645 postJson(Uri.parse(url.toString()), mData, DEV_CONTEXT_DISABLED); 646 647 RecordedRequest request1 = server.takeRequest(); 648 expect.that(request1.getPath()).isEqualTo(mReportingPath); 649 expect.that(request1.getMethod()).isEqualTo("POST"); 650 } 651 652 @Test testPostJsonCorrectPath_DevOptionsEnabled()653 public void testPostJsonCorrectPath_DevOptionsEnabled() throws Exception { 654 MockWebServer server = 655 mMockWebServerRule.startMockWebServer(ImmutableList.of(new MockResponse())); 656 URL url = server.getUrl(mReportingPath); 657 postJson(Uri.parse(url.toString()), mData, DEV_CONTEXT_ENABLED); 658 659 RecordedRequest request1 = server.takeRequest(); 660 expect.that(request1.getPath()).isEqualTo(mReportingPath); 661 expect.that(request1.getMethod()).isEqualTo("POST"); 662 } 663 664 @Test testPostJsonCorrectData()665 public void testPostJsonCorrectData() throws Exception { 666 MockWebServer server = 667 mMockWebServerRule.startMockWebServer(ImmutableList.of(new MockResponse())); 668 URL url = server.getUrl(mReportingPath); 669 postJson(Uri.parse(url.toString()), mData, DEV_CONTEXT_DISABLED); 670 671 RecordedRequest request1 = server.takeRequest(); 672 expect.that(request1.getMethod()).isEqualTo("POST"); 673 expect.that(request1.getUtf8Body()).isEqualTo(mData); 674 } 675 676 @Test testPostJsonFailedResponse()677 public void testPostJsonFailedResponse() throws Exception { 678 MockWebServer server = 679 mMockWebServerRule.startMockWebServer( 680 ImmutableList.of(new MockResponse().setResponseCode(305))); 681 URL url = server.getUrl(mReportingPath); 682 683 Exception exception = 684 assertThrows( 685 ExecutionException.class, 686 () -> postJson(Uri.parse(url.toString()), mData, DEV_CONTEXT_DISABLED)); 687 assertThat(exception.getCause()).isInstanceOf(AdServicesNetworkException.class); 688 } 689 690 @Test testPostJsonDomainDoesNotExist()691 public void testPostJsonDomainDoesNotExist() throws Exception { 692 mMockWebServerRule.startMockWebServer(ImmutableList.of(new MockResponse())); 693 694 Exception exception = 695 assertThrows( 696 ExecutionException.class, 697 () -> postJson(Uri.parse(mFakeUrl), mData, DEV_CONTEXT_DISABLED)); 698 assertThat(exception.getCause()).isInstanceOf(IOException.class); 699 } 700 701 @Test testPostJsonThrowsExceptionIfUsingPlainTextHttp()702 public void testPostJsonThrowsExceptionIfUsingPlainTextHttp() { 703 ExecutionException wrapperExecutionException = 704 assertThrows( 705 ExecutionException.class, 706 () -> 707 postJson( 708 Uri.parse("http://google.com"), 709 mData, 710 DEV_CONTEXT_DISABLED)); 711 712 assertThat(wrapperExecutionException.getCause()) 713 .isInstanceOf(IllegalArgumentException.class); 714 } 715 716 @Test testFailedResponseWithStatusCode()717 public void testFailedResponseWithStatusCode() throws Exception { 718 MockResponse response = new MockResponse().setResponseCode(429); 719 MockWebServer server = mMockWebServerRule.startMockWebServer(ImmutableList.of(response)); 720 URL url = server.getUrl(mFetchPayloadPath); 721 722 // Assert future chain throws an AdServicesNetworkException. 723 Exception wrapperException = 724 assertThrows( 725 ExecutionException.class, 726 () -> fetchPayload(Uri.parse(url.toString()), DEV_CONTEXT_DISABLED)); 727 assertThat(wrapperException.getCause()).isInstanceOf(AdServicesNetworkException.class); 728 729 // Assert the expected AdServicesNetworkException is thrown. 730 AdServicesNetworkException exception = 731 (AdServicesNetworkException) wrapperException.getCause(); 732 assertThat(exception.getErrorCode()) 733 .isEqualTo(AdServicesNetworkException.ERROR_TOO_MANY_REQUESTS); 734 } 735 736 @Test testFailedResponseWithStatusCodeAndRetryAfter()737 public void testFailedResponseWithStatusCodeAndRetryAfter() throws Exception { 738 MockResponse response = 739 new MockResponse().setResponseCode(429).setHeader("Retry-After", 1000); 740 MockWebServer server = mMockWebServerRule.startMockWebServer(ImmutableList.of(response)); 741 URL url = server.getUrl(mFetchPayloadPath); 742 743 // Assert future chain throws an AdServicesNetworkException. 744 Exception wrapperException = 745 assertThrows( 746 ExecutionException.class, 747 () -> fetchPayload(Uri.parse(url.toString()), DEV_CONTEXT_DISABLED)); 748 assertThat(wrapperException.getCause()) 749 .isInstanceOf(RetryableAdServicesNetworkException.class); 750 751 // Assert the expected RetryableAdServicesNetworkException is thrown. 752 RetryableAdServicesNetworkException exception = 753 (RetryableAdServicesNetworkException) wrapperException.getCause(); 754 assertThat(exception.getErrorCode()) 755 .isEqualTo(AdServicesNetworkException.ERROR_TOO_MANY_REQUESTS); 756 assertThat(exception.getRetryAfter()).isEqualTo(Duration.ofMillis(1000)); 757 } 758 759 @Test testFailedResponseWithStatusCodeAndRetryAfterWithNoRetryHeader()760 public void testFailedResponseWithStatusCodeAndRetryAfterWithNoRetryHeader() throws Exception { 761 MockResponse response = new MockResponse().setResponseCode(429); 762 MockWebServer server = mMockWebServerRule.startMockWebServer(ImmutableList.of(response)); 763 URL url = server.getUrl(mFetchPayloadPath); 764 765 // Assert future chain throws an AdServicesNetworkException. 766 Exception wrapperException = 767 assertThrows( 768 ExecutionException.class, 769 () -> fetchPayload(Uri.parse(url.toString()), DEV_CONTEXT_DISABLED)); 770 assertThat(wrapperException.getCause()) 771 .isInstanceOf(RetryableAdServicesNetworkException.class); 772 773 // Assert the expected RetryableAdServicesNetworkException is thrown. 774 RetryableAdServicesNetworkException exception = 775 (RetryableAdServicesNetworkException) wrapperException.getCause(); 776 assertThat(exception.getErrorCode()) 777 .isEqualTo(AdServicesNetworkException.ERROR_TOO_MANY_REQUESTS); 778 assertThat(exception.getRetryAfter()).isEqualTo(DEFAULT_RETRY_AFTER_VALUE); 779 } 780 781 @Test testFetchPayloadDomainIsLocalhost_DevOptionsDisabled()782 public void testFetchPayloadDomainIsLocalhost_DevOptionsDisabled() throws Exception { 783 MockWebServer server = 784 mMockWebServerRule.startMockWebServer( 785 ImmutableList.of(new MockResponse().setResponseCode(305))); 786 URL url = server.getUrl(mFetchPayloadPath); 787 788 Exception exception = 789 assertThrows( 790 ExecutionException.class, 791 () -> fetchPayload(Uri.parse(url.toString()), DEV_CONTEXT_DISABLED)); 792 // Verify we are pinging a local domain. 793 assertThat(WebAddresses.isLocalhost(Uri.parse(url.toString()))).isTrue(); 794 assertThat(exception.getCause()).isInstanceOf(AdServicesNetworkException.class); 795 } 796 797 @Test testFetchPayloadDomainIsLocalhost_DevOptionsEnabled()798 public void testFetchPayloadDomainIsLocalhost_DevOptionsEnabled() throws Exception { 799 MockWebServer server = 800 mMockWebServerRule.startMockWebServer( 801 new Dispatcher() { 802 @Override 803 public MockResponse dispatch(RecordedRequest request) { 804 return new MockResponse() 805 .setBody(mJsScript) 806 .addHeader(NO_CACHE_HEADER); 807 } 808 }); 809 URL url = server.getUrl(mFetchPayloadPath); 810 811 AdServicesHttpClientResponse response = 812 mClient.fetchPayload(Uri.parse(url.toString()), DEV_CONTEXT_ENABLED).get(); 813 814 // Verify we are pinging a local domain. 815 assertThat(WebAddresses.isLocalhost(Uri.parse(url.toString()))).isTrue(); 816 expect.that(response.getResponseBody()).isEqualTo(mJsScript); 817 } 818 819 @Test testperformRequestAndGetResponseInBytes_postsCorrectData()820 public void testperformRequestAndGetResponseInBytes_postsCorrectData() throws Exception { 821 MockWebServer server = 822 mMockWebServerRule.startMockWebServer(ImmutableList.of(new MockResponse())); 823 URL url = server.getUrl(mReportingPath); 824 byte[] postedBody = {1, 2, 3}; 825 AdServicesHttpClientRequest request = 826 AdServicesHttpClientRequest.builder() 827 .setRequestProperties( 828 AdServicesHttpUtil.REQUEST_PROPERTIES_PROTOBUF_CONTENT_TYPE) 829 .setUri(Uri.parse(url.toString())) 830 .setDevContext(DEV_CONTEXT_DISABLED) 831 .setBodyInBytes(postedBody) 832 .setHttpMethodType(AdServicesHttpUtil.HttpMethodType.POST) 833 .build(); 834 835 mClient.performRequestGetResponseInBase64String(request).get(); 836 837 RecordedRequest recordedRequest1 = server.takeRequest(); 838 assertThat(recordedRequest1.getMethod()) 839 .isEqualTo(AdServicesHttpUtil.HttpMethodType.POST.name()); 840 assertThat(recordedRequest1.getBody()).isEqualTo(postedBody); 841 } 842 843 @Test performRequestGetResponseBytes_getRequestNonEmptyBody_requestBodyShouldBeEmpty()844 public void performRequestGetResponseBytes_getRequestNonEmptyBody_requestBodyShouldBeEmpty() 845 throws Exception { 846 MockWebServer server = 847 mMockWebServerRule.startMockWebServer(ImmutableList.of(new MockResponse())); 848 URL url = server.getUrl(mReportingPath); 849 byte[] postedBody = {1, 2, 3}; 850 AdServicesHttpClientRequest request = 851 AdServicesHttpClientRequest.builder() 852 .setRequestProperties( 853 AdServicesHttpUtil.REQUEST_PROPERTIES_PROTOBUF_CONTENT_TYPE) 854 .setUri(Uri.parse(url.toString())) 855 .setDevContext(DEV_CONTEXT_DISABLED) 856 .setBodyInBytes(postedBody) 857 .setHttpMethodType(AdServicesHttpUtil.HttpMethodType.GET) 858 .build(); 859 860 mClient.performRequestGetResponseInBase64String(request).get(); 861 862 RecordedRequest recordedRequest = server.takeRequest(); 863 assertThat(recordedRequest.getMethod()) 864 .isEqualTo(AdServicesHttpUtil.HttpMethodType.GET.name()); 865 assertThat(recordedRequest.getBody()).isEqualTo(EMPTY_BODY); 866 } 867 868 @Test testperformRequestAndGetResponseInBytes_shouldReturnResponseInBytes()869 public void testperformRequestAndGetResponseInBytes_shouldReturnResponseInBytes() 870 throws Exception { 871 byte[] byteResponse = {1, 2, 3, 54}; 872 MockWebServer server = 873 mMockWebServerRule.startMockWebServer( 874 ImmutableList.of(new MockResponse().setBody(byteResponse))); 875 URL url = server.getUrl(mReportingPath); 876 byte[] postedBodyInBytes = {1, 2, 3}; 877 AdServicesHttpClientRequest request = 878 AdServicesHttpClientRequest.builder() 879 .setRequestProperties( 880 AdServicesHttpUtil.REQUEST_PROPERTIES_PROTOBUF_CONTENT_TYPE) 881 .setUri(Uri.parse(url.toString())) 882 .setDevContext(DEV_CONTEXT_DISABLED) 883 .setBodyInBytes(postedBodyInBytes) 884 .setHttpMethodType(AdServicesHttpUtil.HttpMethodType.GET) 885 .build(); 886 887 AdServicesHttpClientResponse response = 888 mClient.performRequestGetResponseInBase64String(request).get(); 889 890 String expectedResponseString = BaseEncoding.base64().encode(byteResponse); 891 assertThat(response.getResponseBody()).isEqualTo(expectedResponseString); 892 } 893 894 @Test performRequestGetResponseBytes_failedStatusCode_shouldThrowErrorWithCorrectCode()895 public void performRequestGetResponseBytes_failedStatusCode_shouldThrowErrorWithCorrectCode() 896 throws Exception { 897 MockWebServer server = 898 mMockWebServerRule.startMockWebServer( 899 ImmutableList.of(new MockResponse().setResponseCode(429))); 900 URL url = server.getUrl(mReportingPath); 901 byte[] postedBody = {1, 2, 3}; 902 AdServicesHttpClientRequest request = 903 AdServicesHttpClientRequest.builder() 904 .setRequestProperties( 905 AdServicesHttpUtil.REQUEST_PROPERTIES_PROTOBUF_CONTENT_TYPE) 906 .setUri(Uri.parse(url.toString())) 907 .setDevContext(DEV_CONTEXT_DISABLED) 908 .setBodyInBytes(postedBody) 909 .setHttpMethodType(AdServicesHttpUtil.HttpMethodType.GET) 910 .build(); 911 912 // Assert future chain throws an AdServicesNetworkException. 913 Exception wrapperException = 914 assertThrows( 915 ExecutionException.class, 916 () -> mClient.performRequestGetResponseInBase64String(request).get()); 917 assertThat(wrapperException.getCause()).isInstanceOf(AdServicesNetworkException.class); 918 919 // Assert the expected AdServicesNetworkException is thrown. 920 AdServicesNetworkException exception = 921 (AdServicesNetworkException) wrapperException.getCause(); 922 assertThat(exception.getErrorCode()) 923 .isEqualTo(AdServicesNetworkException.ERROR_TOO_MANY_REQUESTS); 924 } 925 926 @Test testPerformRequestAndGetResponseInString_shouldReturnResponseString()927 public void testPerformRequestAndGetResponseInString_shouldReturnResponseString() 928 throws Exception { 929 String stringResponse = "This is a plain String response which could also be a JSON String"; 930 byte[] postedBodyInBytes = "{[1,2,3]}".getBytes(StandardCharsets.UTF_8); 931 932 Dispatcher dispatcher = 933 new Dispatcher() { 934 @Override 935 public MockResponse dispatch(RecordedRequest request) { 936 assertThat(new String(postedBodyInBytes)) 937 .isEqualTo(new String(request.getBody())); 938 return new MockResponse().setBody(stringResponse); 939 } 940 }; 941 MockWebServer server = mMockWebServerRule.startMockWebServer(dispatcher); 942 URL url = server.getUrl(mReportingPath); 943 944 ImmutableMap<String, String> requestProperties = 945 ImmutableMap.of( 946 "Content-Type", "application/json", 947 "Accept", "application/json"); 948 949 AdServicesHttpClientRequest request = 950 AdServicesHttpClientRequest.builder() 951 .setRequestProperties(requestProperties) 952 .setUri(Uri.parse(url.toString())) 953 .setDevContext(DEV_CONTEXT_DISABLED) 954 .setBodyInBytes(postedBodyInBytes) 955 .setHttpMethodType(AdServicesHttpUtil.HttpMethodType.POST) 956 .build(); 957 958 AdServicesHttpClientResponse response = 959 mClient.performRequestGetResponseInPlainString(request).get(); 960 961 expect.that(server.getRequestCount()).isEqualTo(1); 962 assertThat(response.getResponseBody()).isEqualTo(stringResponse); 963 } 964 965 @Test testFetchPayloadSuccessfulResponseWithSelectAdsFromOutcomesLogging()966 public void testFetchPayloadSuccessfulResponseWithSelectAdsFromOutcomesLogging() 967 throws Exception { 968 SelectAdsFromOutcomesExecutionLogger executionLogger = 969 setupSelectAdsFromOutcomesApiCalledStatsLogging(); 970 971 MockWebServer server = 972 mMockWebServerRule.startMockWebServer( 973 ImmutableList.of(new MockResponse().setBody(mJsScript))); 974 URL url = server.getUrl(mFetchPayloadPath); 975 976 AdServicesHttpClientResponse result = 977 mClient.fetchPayloadWithLogging( 978 Uri.parse(url.toString()), DEV_CONTEXT_DISABLED, executionLogger) 979 .get(); 980 expect.that(result.getResponseBody()).isEqualTo(mJsScript); 981 982 // Verify the logging of SelectAdsFromOutcomesApiCalledStats 983 verifySelectAdsFromOutcomesApiCalledStatsLogging(executionLogger, 200); 984 } 985 986 @Test testPickRequiredHeaderFields()987 public void testPickRequiredHeaderFields() { 988 ImmutableMap<String, List<String>> allHeaders = 989 ImmutableMap.of( 990 "key1", ImmutableList.of("value1"), "key2", ImmutableList.of("value2")); 991 ImmutableSet<String> requiredHeaderKeys = ImmutableSet.of("key1"); 992 993 Map<String, List<String>> result = 994 mClient.pickRequiredHeaderFields(allHeaders, requiredHeaderKeys); 995 assertThat(result).isEqualTo(ImmutableMap.of("key1", ImmutableList.of("value1"))); 996 } 997 998 @Test testPickRequiredHeaderFieldsCaseInsensitive()999 public void testPickRequiredHeaderFieldsCaseInsensitive() { 1000 ImmutableMap<String, List<String>> allHeaders = 1001 ImmutableMap.of( 1002 "KEY1", ImmutableList.of("value1"), "KEY2", ImmutableList.of("value2")); 1003 ImmutableSet<String> requiredHeaderKeys = ImmutableSet.of("key1", "key2"); 1004 1005 Map<String, List<String>> result = 1006 mClient.pickRequiredHeaderFields(allHeaders, requiredHeaderKeys); 1007 assertThat(result) 1008 .isEqualTo( 1009 ImmutableMap.of( 1010 "key1", 1011 ImmutableList.of("value1"), 1012 "key2", 1013 ImmutableList.of("value2"))); 1014 } 1015 fetchPayload(Uri uri, DevContext devContext)1016 private AdServicesHttpClientResponse fetchPayload(Uri uri, DevContext devContext) 1017 throws Exception { 1018 return mClient.fetchPayload(uri, devContext).get(); 1019 } 1020 getAndReadNothing(Uri uri, DevContext devContext)1021 private Void getAndReadNothing(Uri uri, DevContext devContext) throws Exception { 1022 return mClient.getAndReadNothing(uri, devContext).get(); 1023 } 1024 postJson(Uri uri, String data, DevContext devContext)1025 private Void postJson(Uri uri, String data, DevContext devContext) throws Exception { 1026 return mClient.postPlainText(uri, data, devContext).get(); 1027 } 1028 setupSelectAdsFromOutcomesApiCalledStatsLogging()1029 private SelectAdsFromOutcomesExecutionLogger setupSelectAdsFromOutcomesApiCalledStatsLogging() { 1030 mAdServicesLoggerSpy = Mockito.spy(AdServicesLoggerImpl.getInstance()); 1031 1032 when(mMockClock.elapsedRealtime()) 1033 .thenReturn(mStartDownloadTimestamp, mEndDownloadTimestamp); 1034 return new SelectAdsFromOutcomesExecutionLoggerImpl(mMockClock, mAdServicesLoggerSpy); 1035 } 1036 verifySelectAdsFromOutcomesApiCalledStatsLogging( SelectAdsFromOutcomesExecutionLogger executionLogger, int statusCode)1037 private void verifySelectAdsFromOutcomesApiCalledStatsLogging( 1038 SelectAdsFromOutcomesExecutionLogger executionLogger, int statusCode) { 1039 ArgumentCaptor<SelectAdsFromOutcomesApiCalledStats> argumentCaptor = 1040 ArgumentCaptor.forClass(SelectAdsFromOutcomesApiCalledStats.class); 1041 executionLogger.logSelectAdsFromOutcomesApiCalledStats(); 1042 verify(mAdServicesLoggerSpy) 1043 .logSelectAdsFromOutcomesApiCalledStats(argumentCaptor.capture()); 1044 SelectAdsFromOutcomesApiCalledStats stats = argumentCaptor.getValue(); 1045 1046 int downloadLatency = (int) (mEndDownloadTimestamp - mStartDownloadTimestamp); 1047 assertThat(stats.getDownloadLatencyMillis()).isEqualTo(downloadLatency); 1048 assertThat(stats.getDownloadResultCode()).isEqualTo(statusCode); 1049 } 1050 } 1051