1 /* 2 * Copyright (C) 2022 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.adselection; 18 19 import static android.adservices.adselection.AdSelectionFromOutcomesConfigFixture.SAMPLE_SELLER; 20 import static android.adservices.common.AdServicesStatusUtils.STATUS_INVALID_ARGUMENT; 21 import static android.adservices.common.AdServicesStatusUtils.STATUS_TIMEOUT; 22 import static android.adservices.common.AdServicesStatusUtils.STATUS_USER_CONSENT_REVOKED; 23 24 import static com.android.adservices.service.PhFlagsFixture.EXTENDED_FLEDGE_AD_SELECTION_FROM_OUTCOMES_OVERALL_TIMEOUT_MS; 25 import static com.android.adservices.service.PhFlagsFixture.EXTENDED_FLEDGE_AD_SELECTION_SELECTING_OUTCOME_TIMEOUT_MS; 26 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN; 27 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; 28 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; 29 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doThrow; 30 import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq; 31 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; 32 33 import static org.junit.Assert.assertEquals; 34 import static org.junit.Assert.assertFalse; 35 import static org.junit.Assert.assertNotNull; 36 import static org.junit.Assert.assertNull; 37 import static org.junit.Assert.assertTrue; 38 import static org.mockito.ArgumentMatchers.any; 39 import static org.mockito.ArgumentMatchers.anyInt; 40 import static org.mockito.ArgumentMatchers.argThat; 41 import static org.mockito.Mockito.never; 42 43 import android.adservices.adselection.AdSelectionCallback; 44 import android.adservices.adselection.AdSelectionFromOutcomesConfig; 45 import android.adservices.adselection.AdSelectionFromOutcomesConfigFixture; 46 import android.adservices.adselection.AdSelectionFromOutcomesInput; 47 import android.adservices.adselection.AdSelectionResponse; 48 import android.adservices.adselection.CustomAudienceSignalsFixture; 49 import android.adservices.common.CommonFixture; 50 import android.adservices.common.FledgeErrorResponse; 51 import android.annotation.NonNull; 52 import android.content.Context; 53 import android.net.Uri; 54 import android.os.Process; 55 import android.os.RemoteException; 56 57 import androidx.room.Room; 58 import androidx.test.core.app.ApplicationProvider; 59 60 import com.android.adservices.concurrency.AdServicesExecutors; 61 import com.android.adservices.data.adselection.AdSelectionDatabase; 62 import com.android.adservices.data.adselection.AdSelectionEntryDao; 63 import com.android.adservices.data.adselection.CustomAudienceSignals; 64 import com.android.adservices.data.adselection.DBAdSelection; 65 import com.android.adservices.service.Flags; 66 import com.android.adservices.service.common.AdSelectionServiceFilter; 67 import com.android.adservices.service.common.Throttler; 68 import com.android.adservices.service.consent.ConsentManager; 69 import com.android.adservices.service.exception.FilterException; 70 import com.android.adservices.service.stats.AdServicesLogger; 71 import com.android.adservices.service.stats.AdServicesLoggerImpl; 72 import com.android.adservices.service.stats.AdServicesStatsLog; 73 import com.android.dx.mockito.inline.extended.ExtendedMockito; 74 75 import com.google.common.util.concurrent.ListenableFuture; 76 import com.google.common.util.concurrent.ListeningExecutorService; 77 78 import org.junit.After; 79 import org.junit.Before; 80 import org.junit.Test; 81 import org.mockito.ArgumentMatcher; 82 import org.mockito.Mock; 83 import org.mockito.Mockito; 84 import org.mockito.MockitoSession; 85 import org.mockito.quality.Strictness; 86 87 import java.time.Instant; 88 import java.util.HashSet; 89 import java.util.List; 90 import java.util.concurrent.CountDownLatch; 91 import java.util.stream.Collectors; 92 93 public class OutcomeSelectionRunnerTest { 94 private static final int CALLER_UID = Process.myUid(); 95 private static final String MY_APP_PACKAGE_NAME = CommonFixture.TEST_PACKAGE_NAME; 96 private static final String ANOTHER_CALLER_PACKAGE_NAME = "another.caller.package"; 97 private static final Uri RENDER_URI_1 = Uri.parse("https://www.domain.com/advert1/"); 98 private static final Uri RENDER_URI_2 = Uri.parse("https://www.domain.com/advert2/"); 99 private static final Uri RENDER_URI_3 = Uri.parse("https://www.domain.com/advert3/"); 100 private static final long AD_SELECTION_ID_1 = 1; 101 private static final long AD_SELECTION_ID_2 = 2; 102 private static final long AD_SELECTION_ID_3 = 3; 103 private static final double BID_1 = 10.0; 104 private static final double BID_2 = 20.0; 105 private static final double BID_3 = 30.0; 106 private static final AdSelectionIdWithBidAndRenderUri AD_SELECTION_WITH_BID_1 = 107 AdSelectionIdWithBidAndRenderUri.builder() 108 .setAdSelectionId(AD_SELECTION_ID_1) 109 .setBid(BID_1) 110 .setRenderUri(RENDER_URI_1) 111 .build(); 112 private static final AdSelectionIdWithBidAndRenderUri AD_SELECTION_WITH_BID_2 = 113 AdSelectionIdWithBidAndRenderUri.builder() 114 .setAdSelectionId(AD_SELECTION_ID_2) 115 .setBid(BID_2) 116 .setRenderUri(RENDER_URI_2) 117 .build(); 118 private static final AdSelectionIdWithBidAndRenderUri AD_SELECTION_WITH_BID_3 = 119 AdSelectionIdWithBidAndRenderUri.builder() 120 .setAdSelectionId(AD_SELECTION_ID_3) 121 .setBid(BID_3) 122 .setRenderUri(RENDER_URI_3) 123 .build(); 124 125 private final Context mContext = ApplicationProvider.getApplicationContext(); 126 127 private AdSelectionEntryDao mAdSelectionEntryDao; 128 @Mock private AdOutcomeSelector mAdOutcomeSelectorMock; 129 private OutcomeSelectionRunner mOutcomeSelectionRunner; 130 private Flags mFlags = new OutcomeSelectionRunnerTestFlags(); 131 private final AdServicesLogger mAdServicesLoggerMock = 132 ExtendedMockito.mock(AdServicesLoggerImpl.class); 133 private MockitoSession mStaticMockSession = null; 134 private ListeningExecutorService mBlockingExecutorService; 135 136 @Mock private AdSelectionServiceFilter mAdSelectionServiceFilter; 137 138 @Before setup()139 public void setup() { 140 mBlockingExecutorService = AdServicesExecutors.getBlockingExecutor(); 141 mStaticMockSession = 142 ExtendedMockito.mockitoSession() 143 // .spyStatic(JSScriptEngine.class) 144 // mAdServicesLoggerMock is not referenced in many tests 145 .strictness(Strictness.LENIENT) 146 .initMocks(this) 147 .startMocking(); 148 149 mAdSelectionEntryDao = 150 Room.inMemoryDatabaseBuilder( 151 ApplicationProvider.getApplicationContext(), 152 AdSelectionDatabase.class) 153 .build() 154 .adSelectionEntryDao(); 155 156 mOutcomeSelectionRunner = 157 new OutcomeSelectionRunner( 158 CALLER_UID, 159 mAdOutcomeSelectorMock, 160 mAdSelectionEntryDao, 161 mBlockingExecutorService, 162 AdServicesExecutors.getLightWeightExecutor(), 163 AdServicesExecutors.getScheduler(), 164 mAdServicesLoggerMock, 165 mContext, 166 mFlags, 167 mAdSelectionServiceFilter); 168 169 doNothing() 170 .when(mAdSelectionServiceFilter) 171 .filterRequest( 172 SAMPLE_SELLER, 173 MY_APP_PACKAGE_NAME, 174 true, 175 true, 176 CALLER_UID, 177 AdServicesStatsLog.AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN, 178 Throttler.ApiKey.FLEDGE_API_SELECT_ADS); 179 } 180 181 @After tearDown()182 public void tearDown() { 183 if (mStaticMockSession != null) { 184 mStaticMockSession.finishMocking(); 185 } 186 } 187 188 @Test testRunOutcomeSelectionInvalidAdSelectionConfigFromOutcomes()189 public void testRunOutcomeSelectionInvalidAdSelectionConfigFromOutcomes() { 190 List<AdSelectionIdWithBidAndRenderUri> AdSelectionIdWithBidAndRenderUris = 191 List.of(AD_SELECTION_WITH_BID_1, AD_SELECTION_WITH_BID_2, AD_SELECTION_WITH_BID_3); 192 persistAdSelectionEntry(AdSelectionIdWithBidAndRenderUris.get(0), MY_APP_PACKAGE_NAME); 193 // Not persisting index 1 194 // Persisting index 2 with a different package name 195 persistAdSelectionEntry( 196 AdSelectionIdWithBidAndRenderUris.get(2), ANOTHER_CALLER_PACKAGE_NAME); 197 198 List<Long> adOutcomesConfigParam = 199 AdSelectionIdWithBidAndRenderUris.stream() 200 .map(AdSelectionIdWithBidAndRenderUri::getAdSelectionId) 201 .collect(Collectors.toList()); 202 203 AdSelectionFromOutcomesConfig config = 204 AdSelectionFromOutcomesConfigFixture.anAdSelectionFromOutcomesConfig( 205 adOutcomesConfigParam); 206 207 AdSelectionTestCallback resultsCallback = 208 invokeRunAdSelectionFromOutcomes( 209 mOutcomeSelectionRunner, config, MY_APP_PACKAGE_NAME); 210 211 verify(mAdOutcomeSelectorMock, never()).runAdOutcomeSelector(any(), any()); 212 assertFalse(resultsCallback.mIsSuccess); 213 assertEquals(STATUS_INVALID_ARGUMENT, resultsCallback.mFledgeErrorResponse.getStatusCode()); 214 verify(mAdServicesLoggerMock) 215 .logFledgeApiCallStats( 216 eq(AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN), 217 eq(STATUS_INVALID_ARGUMENT), 218 anyInt()); 219 } 220 221 @Test testRunOutcomeSelectionRevokedUserConsentEmptyResult()222 public void testRunOutcomeSelectionRevokedUserConsentEmptyResult() { 223 doThrow(new FilterException(new ConsentManager.RevokedConsentException())) 224 .when(mAdSelectionServiceFilter) 225 .filterRequest( 226 SAMPLE_SELLER, 227 MY_APP_PACKAGE_NAME, 228 true, 229 true, 230 CALLER_UID, 231 AdServicesStatsLog.AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN, 232 Throttler.ApiKey.FLEDGE_API_SELECT_ADS); 233 234 List<AdSelectionIdWithBidAndRenderUri> adSelectionIdWithBidAndRenderUris = 235 List.of(AD_SELECTION_WITH_BID_1, AD_SELECTION_WITH_BID_2, AD_SELECTION_WITH_BID_3); 236 for (AdSelectionIdWithBidAndRenderUri idWithBid : adSelectionIdWithBidAndRenderUris) { 237 persistAdSelectionEntry(idWithBid, MY_APP_PACKAGE_NAME); 238 } 239 240 List<Long> adOutcomesConfigParam = 241 adSelectionIdWithBidAndRenderUris.stream() 242 .map(AdSelectionIdWithBidAndRenderUri::getAdSelectionId) 243 .collect(Collectors.toList()); 244 245 AdSelectionFromOutcomesConfig config = 246 AdSelectionFromOutcomesConfigFixture.anAdSelectionFromOutcomesConfig( 247 adOutcomesConfigParam); 248 249 AdSelectionTestCallback resultsCallback = 250 invokeRunAdSelectionFromOutcomes( 251 mOutcomeSelectionRunner, config, MY_APP_PACKAGE_NAME); 252 253 verify(mAdOutcomeSelectorMock, never()).runAdOutcomeSelector(any(), any()); 254 assertTrue(resultsCallback.mIsSuccess); 255 assertNull(resultsCallback.mAdSelectionResponse); 256 257 // Confirm a duplicate log entry does not exist. 258 // AdSelectionServiceFilter ensures the failing assertion is logged internally. 259 verify(mAdServicesLoggerMock, never()) 260 .logFledgeApiCallStats( 261 eq(AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN), 262 eq(STATUS_USER_CONSENT_REVOKED), 263 anyInt()); 264 } 265 266 @Test testRunOutcomeSelectionOrchestrationTimeoutFailure()267 public void testRunOutcomeSelectionOrchestrationTimeoutFailure() { 268 mFlags = 269 new Flags() { 270 @Override 271 public long getAdSelectionSelectingOutcomeTimeoutMs() { 272 return 300; 273 } 274 275 @Override 276 public boolean getDisableFledgeEnrollmentCheck() { 277 return true; 278 } 279 280 @Override 281 public long getAdSelectionFromOutcomesOverallTimeoutMs() { 282 return 100; 283 } 284 }; 285 286 List<AdSelectionIdWithBidAndRenderUri> adSelectionIdWithBidAndRenderUris = 287 List.of(AD_SELECTION_WITH_BID_1, AD_SELECTION_WITH_BID_2, AD_SELECTION_WITH_BID_3); 288 for (AdSelectionIdWithBidAndRenderUri idWithBid : adSelectionIdWithBidAndRenderUris) { 289 persistAdSelectionEntry(idWithBid, MY_APP_PACKAGE_NAME); 290 } 291 292 List<Long> adOutcomesConfigParam = 293 adSelectionIdWithBidAndRenderUris.stream() 294 .map(AdSelectionIdWithBidAndRenderUri::getAdSelectionId) 295 .collect(Collectors.toList()); 296 297 AdSelectionFromOutcomesConfig config = 298 AdSelectionFromOutcomesConfigFixture.anAdSelectionFromOutcomesConfig( 299 adOutcomesConfigParam); 300 301 GenericListMatcher matcher = new GenericListMatcher(adSelectionIdWithBidAndRenderUris); 302 doAnswer((ignored) -> getSelectedOutcomeWithDelay(AD_SELECTION_ID_1, mFlags)) 303 .when(mAdOutcomeSelectorMock) 304 .runAdOutcomeSelector(argThat(matcher), eq(config)); 305 306 OutcomeSelectionRunner outcomeSelectionRunner = 307 new OutcomeSelectionRunner( 308 CALLER_UID, 309 mAdOutcomeSelectorMock, 310 mAdSelectionEntryDao, 311 mBlockingExecutorService, 312 AdServicesExecutors.getLightWeightExecutor(), 313 AdServicesExecutors.getScheduler(), 314 mAdServicesLoggerMock, 315 mContext, 316 mFlags, 317 mAdSelectionServiceFilter); 318 319 AdSelectionTestCallback resultsCallback = 320 invokeRunAdSelectionFromOutcomes( 321 outcomeSelectionRunner, config, MY_APP_PACKAGE_NAME); 322 323 verify(mAdOutcomeSelectorMock, Mockito.times(1)).runAdOutcomeSelector(any(), any()); 324 assertFalse(resultsCallback.mIsSuccess); 325 assertNotNull(resultsCallback.mFledgeErrorResponse); 326 assertEquals(STATUS_TIMEOUT, resultsCallback.mFledgeErrorResponse.getStatusCode()); 327 verify(mAdServicesLoggerMock) 328 .logFledgeApiCallStats( 329 eq(AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN), 330 eq(STATUS_TIMEOUT), 331 anyInt()); 332 } 333 persistAdSelectionEntry( AdSelectionIdWithBidAndRenderUri idWithBidAndRenderUri, String callerPackageName)334 private void persistAdSelectionEntry( 335 AdSelectionIdWithBidAndRenderUri idWithBidAndRenderUri, String callerPackageName) { 336 final Uri biddingLogicUri1 = Uri.parse("https://www.domain.com/logic/1"); 337 final Instant activationTime = Instant.now(); 338 final String contextualSignals = "contextual_signals"; 339 final CustomAudienceSignals customAudienceSignals = 340 CustomAudienceSignalsFixture.aCustomAudienceSignals(); 341 342 final DBAdSelection dbAdSelectionEntry = 343 new DBAdSelection.Builder() 344 .setAdSelectionId(idWithBidAndRenderUri.getAdSelectionId()) 345 .setCustomAudienceSignals(customAudienceSignals) 346 .setContextualSignals(contextualSignals) 347 .setBiddingLogicUri(biddingLogicUri1) 348 .setWinningAdRenderUri(idWithBidAndRenderUri.getRenderUri()) 349 .setWinningAdBid(idWithBidAndRenderUri.getBid()) 350 .setCreationTimestamp(activationTime) 351 .setCallerPackageName(callerPackageName) 352 .build(); 353 mAdSelectionEntryDao.persistAdSelection(dbAdSelectionEntry); 354 } 355 invokeRunAdSelectionFromOutcomes( OutcomeSelectionRunner outcomeSelectionRunner, AdSelectionFromOutcomesConfig config, String callerPackageName)356 private OutcomeSelectionRunnerTest.AdSelectionTestCallback invokeRunAdSelectionFromOutcomes( 357 OutcomeSelectionRunner outcomeSelectionRunner, 358 AdSelectionFromOutcomesConfig config, 359 String callerPackageName) { 360 361 // Counted down in the callback 362 CountDownLatch countDownLatch = new CountDownLatch(1); 363 OutcomeSelectionRunnerTest.AdSelectionTestCallback adSelectionTestCallback = 364 new OutcomeSelectionRunnerTest.AdSelectionTestCallback(countDownLatch); 365 366 AdSelectionFromOutcomesInput input = 367 new AdSelectionFromOutcomesInput.Builder() 368 .setAdSelectionFromOutcomesConfig(config) 369 .setCallerPackageName(callerPackageName) 370 .build(); 371 372 outcomeSelectionRunner.runOutcomeSelection(input, adSelectionTestCallback); 373 try { 374 adSelectionTestCallback.mCountDownLatch.await(); 375 } catch (InterruptedException e) { 376 e.printStackTrace(); 377 } 378 return adSelectionTestCallback; 379 } 380 getSelectedOutcomeWithDelay( Long outcomeId, @NonNull Flags flags)381 private ListenableFuture<Long> getSelectedOutcomeWithDelay( 382 Long outcomeId, @NonNull Flags flags) { 383 return mBlockingExecutorService.submit( 384 () -> { 385 Thread.sleep(2 * flags.getAdSelectionFromOutcomesOverallTimeoutMs()); 386 return outcomeId; 387 }); 388 } 389 390 static class AdSelectionTestCallback extends AdSelectionCallback.Stub { 391 392 final CountDownLatch mCountDownLatch; 393 boolean mIsSuccess = false; 394 AdSelectionResponse mAdSelectionResponse; 395 FledgeErrorResponse mFledgeErrorResponse; 396 AdSelectionTestCallback(CountDownLatch countDownLatch)397 AdSelectionTestCallback(CountDownLatch countDownLatch) { 398 mCountDownLatch = countDownLatch; 399 mAdSelectionResponse = null; 400 mFledgeErrorResponse = null; 401 } 402 403 @Override onSuccess(AdSelectionResponse adSelectionResponse)404 public void onSuccess(AdSelectionResponse adSelectionResponse) throws RemoteException { 405 mIsSuccess = true; 406 mAdSelectionResponse = adSelectionResponse; 407 mCountDownLatch.countDown(); 408 } 409 410 @Override onFailure(FledgeErrorResponse fledgeErrorResponse)411 public void onFailure(FledgeErrorResponse fledgeErrorResponse) throws RemoteException { 412 mIsSuccess = false; 413 mFledgeErrorResponse = fledgeErrorResponse; 414 mCountDownLatch.countDown(); 415 } 416 } 417 418 static class GenericListMatcher 419 implements ArgumentMatcher<List<AdSelectionIdWithBidAndRenderUri>> { 420 private final List<AdSelectionIdWithBidAndRenderUri> mTruth; 421 GenericListMatcher(List<AdSelectionIdWithBidAndRenderUri> truth)422 GenericListMatcher(List<AdSelectionIdWithBidAndRenderUri> truth) { 423 this.mTruth = truth; 424 } 425 426 @Override matches(List<AdSelectionIdWithBidAndRenderUri> argument)427 public boolean matches(List<AdSelectionIdWithBidAndRenderUri> argument) { 428 return mTruth.size() == argument.size() 429 && new HashSet<>(mTruth).equals(new HashSet<>(argument)); 430 } 431 } 432 433 private static class OutcomeSelectionRunnerTestFlags implements Flags { 434 @Override getAdSelectionSelectingOutcomeTimeoutMs()435 public long getAdSelectionSelectingOutcomeTimeoutMs() { 436 return EXTENDED_FLEDGE_AD_SELECTION_SELECTING_OUTCOME_TIMEOUT_MS; 437 } 438 439 @Override getAdSelectionFromOutcomesOverallTimeoutMs()440 public long getAdSelectionFromOutcomesOverallTimeoutMs() { 441 return EXTENDED_FLEDGE_AD_SELECTION_FROM_OUTCOMES_OVERALL_TIMEOUT_MS; 442 } 443 } 444 } 445