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