summaryrefslogtreecommitdiff
path: root/adservices/tests/unittest/service-core/src/com/android/adservices/service/adselection/OnDeviceAdSelectionRunnerTest.java
diff options
context:
space:
mode:
Diffstat (limited to 'adservices/tests/unittest/service-core/src/com/android/adservices/service/adselection/OnDeviceAdSelectionRunnerTest.java')
-rw-r--r--adservices/tests/unittest/service-core/src/com/android/adservices/service/adselection/OnDeviceAdSelectionRunnerTest.java1840
1 files changed, 1840 insertions, 0 deletions
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/adselection/OnDeviceAdSelectionRunnerTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/adselection/OnDeviceAdSelectionRunnerTest.java
new file mode 100644
index 0000000000..6a596c463d
--- /dev/null
+++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/adselection/OnDeviceAdSelectionRunnerTest.java
@@ -0,0 +1,1840 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.adservices.service.adselection;
+
+import static android.adservices.common.AdServicesStatusUtils.STATUS_INTERNAL_ERROR;
+
+import static com.android.adservices.service.adselection.AdSelectionRunner.AD_SELECTION_THROTTLED;
+import static com.android.adservices.service.adselection.AdSelectionRunner.AD_SELECTION_TIMED_OUT;
+import static com.android.adservices.service.adselection.AdSelectionRunner.ERROR_AD_SELECTION_FAILURE;
+import static com.android.adservices.service.adselection.AdSelectionRunner.ERROR_NO_CA_AVAILABLE;
+import static com.android.adservices.service.adselection.AdSelectionRunner.ERROR_NO_VALID_BIDS_FOR_SCORING;
+import static com.android.adservices.service.adselection.AdSelectionRunner.ERROR_NO_WINNING_AD_FOUND;
+import static com.android.adservices.service.adselection.AdSelectionRunner.JS_SANDBOX_IS_NOT_AVAILABLE;
+import static com.android.adservices.service.common.Throttler.ApiKey.FLEDGE_API_SELECT_ADS;
+import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyString;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doThrow;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyZeroInteractions;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.adservices.adselection.AdBiddingOutcomeFixture;
+import android.adservices.adselection.AdSelectionCallback;
+import android.adservices.adselection.AdSelectionConfig;
+import android.adservices.adselection.AdSelectionConfigFixture;
+import android.adservices.adselection.AdSelectionInput;
+import android.adservices.adselection.AdSelectionResponse;
+import android.adservices.common.AdSelectionSignals;
+import android.adservices.common.AdServicesStatusUtils;
+import android.adservices.common.AdTechIdentifier;
+import android.adservices.common.CommonFixture;
+import android.adservices.common.FledgeErrorResponse;
+import android.adservices.customaudience.CustomAudienceFixture;
+import android.adservices.exceptions.AdServicesException;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Process;
+import android.os.RemoteException;
+import android.webkit.WebView;
+
+import androidx.room.Room;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.adservices.concurrency.AdServicesExecutors;
+import com.android.adservices.customaudience.DBCustomAudienceFixture;
+import com.android.adservices.data.DbTestUtil;
+import com.android.adservices.data.adselection.AdSelectionDatabase;
+import com.android.adservices.data.adselection.AdSelectionEntryDao;
+import com.android.adservices.data.adselection.DBAdSelection;
+import com.android.adservices.data.adselection.DBAdSelectionEntry;
+import com.android.adservices.data.customaudience.CustomAudienceDao;
+import com.android.adservices.data.customaudience.CustomAudienceDatabase;
+import com.android.adservices.data.customaudience.DBCustomAudience;
+import com.android.adservices.data.enrollment.EnrollmentDao;
+import com.android.adservices.service.Flags;
+import com.android.adservices.service.FlagsFactory;
+import com.android.adservices.service.common.AdServicesHttpsClient;
+import com.android.adservices.service.common.AppImportanceFilter;
+import com.android.adservices.service.common.FledgeAllowListsFilter;
+import com.android.adservices.service.common.FledgeAuthorizationFilter;
+import com.android.adservices.service.common.Throttler;
+import com.android.adservices.service.consent.AdServicesApiConsent;
+import com.android.adservices.service.consent.ConsentManager;
+import com.android.adservices.service.devapi.DevContext;
+import com.android.adservices.service.stats.AdServicesLogger;
+import com.android.adservices.service.stats.AdServicesLoggerImpl;
+import com.android.adservices.service.stats.ApiServiceLatencyCalculator;
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.FluentFuture;
+import com.google.common.util.concurrent.Futures;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.function.ThrowingRunnable;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.Spy;
+import org.mockito.internal.stubbing.answers.AnswersWithDelay;
+import org.mockito.internal.stubbing.answers.Returns;
+import org.mockito.quality.Strictness;
+import org.mockito.stubbing.Answer;
+
+import java.time.Clock;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+/**
+ * This test covers strictly the unit of {@link AdSelectionRunner} The dependencies in this test are
+ * mocked and provide expected mock responses when invoked with desired input
+ */
+public class OnDeviceAdSelectionRunnerTest {
+ private static final String TAG = OnDeviceAdSelectionRunnerTest.class.getName();
+
+ private static final AdTechIdentifier BUYER_1 = AdSelectionConfigFixture.BUYER_1;
+ private static final AdTechIdentifier BUYER_2 = AdSelectionConfigFixture.BUYER_2;
+ private static final Long AD_SELECTION_ID = 1234L;
+ private static final String ERROR_INVALID_JSON = "Invalid Json Exception";
+ private static final int CALLER_UID = Process.myUid();
+ private static final String MY_APP_PACKAGE_NAME = CommonFixture.TEST_PACKAGE_NAME;
+
+ private static final AdTechIdentifier SELLER_VALID =
+ AdTechIdentifier.fromString("developer.android.com");
+ private static final Uri DECISION_LOGIC_URI =
+ Uri.parse("https://developer.android.com/test/decisions_logic_uris");
+ private static final Uri TRUSTED_SIGNALS_URI =
+ Uri.parse("https://developer.android.com/test/trusted_signals_uri");
+ private static final int RUN_AD_SELECTION_OVERALL_LATENCY_MS = 200;
+
+ private MockitoSession mStaticMockSession = null;
+ @Mock private AdsScoreGenerator mMockAdsScoreGenerator;
+ @Mock private AdBidGenerator mMockAdBidGenerator;
+ @Mock private AdSelectionIdGenerator mMockAdSelectionIdGenerator;
+ @Mock private AppImportanceFilter mAppImportanceFilter;
+ @Spy private Clock mClock = Clock.systemUTC();
+ @Mock private ConsentManager mConsentManagerMock;
+ @Mock private Throttler mMockThrottler;
+ @Mock private ApiServiceLatencyCalculator mMockApiServiceLatencyCalculator;
+
+ private Flags mFlags =
+ new Flags() {
+ @Override
+ public long getAdSelectionOverallTimeoutMs() {
+ return 300;
+ }
+
+ @Override
+ public boolean getDisableFledgeEnrollmentCheck() {
+ return true;
+ }
+ };
+ private Context mContext = ApplicationProvider.getApplicationContext();
+ private AdServicesHttpsClient mAdServicesHttpsClient;
+ private ExecutorService mLightweightExecutorService;
+ private ExecutorService mBackgroundExecutorService;
+ private ScheduledThreadPoolExecutor mScheduledExecutor;
+ private CustomAudienceDao mCustomAudienceDao;
+ private AdSelectionEntryDao mAdSelectionEntryDao;
+ private Supplier<Throttler> mThrottlerSupplier = () -> mMockThrottler;
+ private AdServicesLogger mAdServicesLoggerMock =
+ ExtendedMockito.mock(AdServicesLoggerImpl.class);
+ private final FledgeAuthorizationFilter mFledgeAuthorizationFilter =
+ new FledgeAuthorizationFilter(
+ mContext.getPackageManager(),
+ new EnrollmentDao(mContext, DbTestUtil.getDbHelperForTest()),
+ mAdServicesLoggerMock);
+ private final FledgeAllowListsFilter mFledgeAllowListsFilter =
+ new FledgeAllowListsFilter(mFlags, mAdServicesLoggerMock);
+
+ private AdSelectionConfig.Builder mAdSelectionConfigBuilder;
+
+ private DBCustomAudience mDBCustomAudienceForBuyer1;
+ private DBCustomAudience mDBCustomAudienceForBuyer2;
+ private List<DBCustomAudience> mBuyerCustomAudienceList;
+
+ private AdBiddingOutcome mAdBiddingOutcomeForBuyer1;
+ private AdBiddingOutcome mAdBiddingOutcomeForBuyer2;
+ private List<AdBiddingOutcome> mAdBiddingOutcomeList;
+
+ private AdScoringOutcome mAdScoringOutcomeForBuyer1;
+ private AdScoringOutcome mAdScoringOutcomeForBuyer2;
+ private List<AdScoringOutcome> mAdScoringOutcomeList;
+
+ private AdSelectionRunner mAdSelectionRunner;
+
+ @Before
+ public void setUp() {
+ // Test applications don't have the required permissions to read config P/H flags, and
+ // injecting mocked flags everywhere is annoying and non-trivial for static methods
+ mStaticMockSession =
+ ExtendedMockito.mockitoSession()
+ .spyStatic(FlagsFactory.class)
+ .spyStatic(WebView.class)
+ .strictness(Strictness.LENIENT)
+ .initMocks(this)
+ .startMocking();
+
+ mContext = ApplicationProvider.getApplicationContext();
+ mLightweightExecutorService = AdServicesExecutors.getLightWeightExecutor();
+ mBackgroundExecutorService = AdServicesExecutors.getBackgroundExecutor();
+ mScheduledExecutor = AdServicesExecutors.getScheduler();
+ mAdServicesHttpsClient =
+ new AdServicesHttpsClient(AdServicesExecutors.getBlockingExecutor());
+ mCustomAudienceDao =
+ Room.inMemoryDatabaseBuilder(mContext, CustomAudienceDatabase.class)
+ .build()
+ .customAudienceDao();
+
+ mAdSelectionEntryDao =
+ Room.inMemoryDatabaseBuilder(mContext, AdSelectionDatabase.class)
+ .build()
+ .adSelectionEntryDao();
+
+ mAdSelectionConfigBuilder =
+ AdSelectionConfigFixture.anAdSelectionConfigBuilder()
+ .setSeller(SELLER_VALID)
+ .setCustomAudienceBuyers(Arrays.asList(BUYER_1, BUYER_2))
+ .setDecisionLogicUri(DECISION_LOGIC_URI)
+ .setTrustedScoringSignalsUri(TRUSTED_SIGNALS_URI);
+
+ mDBCustomAudienceForBuyer1 = createDBCustomAudience(BUYER_1);
+ mDBCustomAudienceForBuyer2 = createDBCustomAudience(BUYER_2);
+ mBuyerCustomAudienceList =
+ Arrays.asList(mDBCustomAudienceForBuyer1, mDBCustomAudienceForBuyer2);
+
+ mAdBiddingOutcomeForBuyer1 =
+ AdBiddingOutcomeFixture.anAdBiddingOutcomeBuilder(BUYER_1, 1.0).build();
+ mAdBiddingOutcomeForBuyer2 =
+ AdBiddingOutcomeFixture.anAdBiddingOutcomeBuilder(BUYER_2, 2.0).build();
+ mAdBiddingOutcomeList =
+ Arrays.asList(mAdBiddingOutcomeForBuyer1, mAdBiddingOutcomeForBuyer2);
+
+ mAdScoringOutcomeForBuyer1 =
+ AdScoringOutcomeFixture.anAdScoringBuilder(BUYER_1, 2.0).build();
+ mAdScoringOutcomeForBuyer2 =
+ AdScoringOutcomeFixture.anAdScoringBuilder(BUYER_2, 3.0).build();
+ mAdScoringOutcomeList =
+ Arrays.asList(mAdScoringOutcomeForBuyer1, mAdScoringOutcomeForBuyer2);
+ when(mMockThrottler.tryAcquire(eq(FLEDGE_API_SELECT_ADS), anyString())).thenReturn(true);
+ }
+
+ private DBCustomAudience createDBCustomAudience(final AdTechIdentifier buyer) {
+ return DBCustomAudienceFixture.getValidBuilderByBuyer(buyer)
+ .setOwner(buyer.toString() + CustomAudienceFixture.VALID_OWNER)
+ .setName(buyer + CustomAudienceFixture.VALID_NAME)
+ .setCreationTime(CommonFixture.FIXED_NOW_TRUNCATED_TO_MILLI)
+ .setLastAdsAndBiddingDataUpdatedTime(CommonFixture.FIXED_NOW_TRUNCATED_TO_MILLI)
+ .build();
+ }
+
+ @Test
+ public void testRunAdSelectionSuccess() throws AdServicesException {
+ when(mClock.instant()).thenReturn(Clock.systemUTC().instant());
+ doReturn(mFlags).when(FlagsFactory::getFlags);
+ doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent();
+ AdSelectionConfig adSelectionConfig = mAdSelectionConfigBuilder.build();
+
+ // Populating the Custom Audience DB
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer1,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_1));
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer2,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_2));
+
+ // Getting BiddingOutcome-forBuyerX corresponding to each CA-forBuyerX
+ doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer1)))
+ .when(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer1,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer1.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer2)))
+ .when(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer2,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer2.getBuyer()),
+ AdSelectionSignals.EMPTY);
+
+ // Getting ScoringOutcome-ForBuyerX corresponding to each BiddingOutcome-forBuyerX
+ when(mMockAdsScoreGenerator.runAdScoring(mAdBiddingOutcomeList, adSelectionConfig))
+ .thenReturn((FluentFuture.from(Futures.immediateFuture(mAdScoringOutcomeList))));
+
+ Instant adSelectionCreationTs = Clock.systemUTC().instant().truncatedTo(ChronoUnit.MILLIS);
+ when(mClock.instant()).thenReturn(adSelectionCreationTs);
+
+ DBAdSelectionEntry expectedAdSelectionResult =
+ new DBAdSelectionEntry.Builder()
+ .setAdSelectionId(AD_SELECTION_ID)
+ .setWinningAdBid(
+ mAdScoringOutcomeForBuyer2.getAdWithScore().getAdWithBid().getBid())
+ .setCustomAudienceSignals(
+ mAdScoringOutcomeForBuyer2
+ .getCustomAudienceBiddingInfo()
+ .getCustomAudienceSignals())
+ .setWinningAdRenderUri(
+ mAdScoringOutcomeForBuyer2
+ .getAdWithScore()
+ .getAdWithBid()
+ .getAdData()
+ .getRenderUri())
+ .setBuyerDecisionLogicJs(
+ mAdBiddingOutcomeForBuyer1
+ .getCustomAudienceBiddingInfo()
+ .getBuyerDecisionLogicJs())
+ // TODO(b/230569187) add contextual signals once supported in the main logic
+ .setContextualSignals("{}")
+ .setCreationTimestamp(adSelectionCreationTs)
+ .build();
+
+ when(mMockAdSelectionIdGenerator.generateId()).thenReturn(AD_SELECTION_ID);
+ when(mMockApiServiceLatencyCalculator.getApiServiceOverallLatencyMs())
+ .thenReturn(RUN_AD_SELECTION_OVERALL_LATENCY_MS);
+ mAdSelectionRunner =
+ new OnDeviceAdSelectionRunner(
+ mContext,
+ mCustomAudienceDao,
+ mAdSelectionEntryDao,
+ mAdServicesHttpsClient,
+ mLightweightExecutorService,
+ mBackgroundExecutorService,
+ mScheduledExecutor,
+ mConsentManagerMock,
+ mMockAdsScoreGenerator,
+ mMockAdBidGenerator,
+ mMockAdSelectionIdGenerator,
+ mClock,
+ mAdServicesLoggerMock,
+ mAppImportanceFilter,
+ mFlags,
+ mThrottlerSupplier,
+ CALLER_UID,
+ mFledgeAuthorizationFilter,
+ mFledgeAllowListsFilter,
+ mMockApiServiceLatencyCalculator);
+ assertFalse(mAdSelectionEntryDao.doesAdSelectionIdExist(AD_SELECTION_ID));
+
+ AdSelectionTestCallback resultsCallback =
+ invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
+
+ verify(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer1,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer1.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ verify(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer2,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer2.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ verify(mMockAdsScoreGenerator).runAdScoring(mAdBiddingOutcomeList, adSelectionConfig);
+
+ assertTrue(resultsCallback.mIsSuccess);
+ assertEquals(
+ expectedAdSelectionResult.getAdSelectionId(),
+ resultsCallback.mAdSelectionResponse.getAdSelectionId());
+ assertEquals(
+ expectedAdSelectionResult.getWinningAdRenderUri(),
+ resultsCallback.mAdSelectionResponse.getRenderUri());
+ assertTrue(mAdSelectionEntryDao.doesAdSelectionIdExist(AD_SELECTION_ID));
+ assertEquals(
+ expectedAdSelectionResult,
+ mAdSelectionEntryDao.getAdSelectionEntityById(AD_SELECTION_ID));
+
+ verify(mMockApiServiceLatencyCalculator).getApiServiceOverallLatencyMs();
+ verify(mAdServicesLoggerMock)
+ .logFledgeApiCallStats(
+ eq(AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS),
+ eq(AdServicesStatusUtils.STATUS_SUCCESS),
+ eq(RUN_AD_SELECTION_OVERALL_LATENCY_MS));
+ }
+
+ @Test
+ public void testRunAdSelectionWithRevokedUserConsentSuccess() throws AdServicesException {
+ doReturn(mFlags).when(FlagsFactory::getFlags);
+ doReturn(AdServicesApiConsent.REVOKED).when(mConsentManagerMock).getConsent();
+
+ // Creating ad selection config for happy case with all the buyers in place
+ AdSelectionConfig adSelectionConfig = mAdSelectionConfigBuilder.build();
+
+ // Populating the Custom Audience DB
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer1,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_1));
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer2,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_2));
+
+ when(mMockAdSelectionIdGenerator.generateId()).thenReturn(AD_SELECTION_ID);
+ when(mMockApiServiceLatencyCalculator.getApiServiceOverallLatencyMs())
+ .thenReturn(RUN_AD_SELECTION_OVERALL_LATENCY_MS);
+ mAdSelectionRunner =
+ new OnDeviceAdSelectionRunner(
+ mContext,
+ mCustomAudienceDao,
+ mAdSelectionEntryDao,
+ mAdServicesHttpsClient,
+ mLightweightExecutorService,
+ mBackgroundExecutorService,
+ mScheduledExecutor,
+ mConsentManagerMock,
+ mMockAdsScoreGenerator,
+ mMockAdBidGenerator,
+ mMockAdSelectionIdGenerator,
+ mClock,
+ mAdServicesLoggerMock,
+ mAppImportanceFilter,
+ mFlags,
+ mThrottlerSupplier,
+ CALLER_UID,
+ mFledgeAuthorizationFilter,
+ mFledgeAllowListsFilter,
+ mMockApiServiceLatencyCalculator);
+
+ assertFalse(mAdSelectionEntryDao.doesAdSelectionIdExist(AD_SELECTION_ID));
+
+ AdSelectionTestCallback resultsCallback =
+ invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
+
+ verify(mMockAdBidGenerator, never()).runAdBiddingPerCA(any(), any(), any(), any());
+ verify(mMockAdsScoreGenerator, never()).runAdScoring(any(), any());
+
+ assertTrue(resultsCallback.mIsSuccess);
+ assertFalse(
+ mAdSelectionEntryDao.doesAdSelectionIdExist(
+ resultsCallback.mAdSelectionResponse.getAdSelectionId()));
+ assertEquals(Uri.EMPTY, resultsCallback.mAdSelectionResponse.getRenderUri());
+ assertFalse(mAdSelectionEntryDao.doesAdSelectionIdExist(AD_SELECTION_ID));
+
+ verify(mMockApiServiceLatencyCalculator).getApiServiceOverallLatencyMs();
+ verify(mAdServicesLoggerMock)
+ .logFledgeApiCallStats(
+ eq(AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS),
+ eq(AdServicesStatusUtils.STATUS_USER_CONSENT_REVOKED),
+ eq(RUN_AD_SELECTION_OVERALL_LATENCY_MS));
+ }
+
+ @Test
+ public void testRunAdSelectionMissingBuyerSignals() throws AdServicesException {
+ when(mClock.instant()).thenReturn(Clock.systemUTC().instant());
+ doReturn(mFlags).when(FlagsFactory::getFlags);
+ doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent();
+
+ // Creating ad selection config with missing Buyer signals to test the fallback
+ AdSelectionConfig adSelectionConfig =
+ mAdSelectionConfigBuilder.setPerBuyerSignals(Collections.EMPTY_MAP).build();
+
+ // Populating the Custom Audience DB
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer1,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_1));
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer2,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_2));
+
+ // Getting BiddingOutcome-forBuyerX corresponding to each CA-forBuyerX
+ doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer1)))
+ .when(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer1,
+ adSelectionConfig.getAdSelectionSignals(),
+ AdSelectionSignals.EMPTY,
+ AdSelectionSignals.EMPTY);
+ doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer2)))
+ .when(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer2,
+ adSelectionConfig.getAdSelectionSignals(),
+ AdSelectionSignals.EMPTY,
+ AdSelectionSignals.EMPTY);
+
+ // Getting ScoringOutcome-ForBuyerX corresponding to each BiddingOutcome-forBuyerX
+ when(mMockAdsScoreGenerator.runAdScoring(mAdBiddingOutcomeList, adSelectionConfig))
+ .thenReturn((FluentFuture.from(Futures.immediateFuture(mAdScoringOutcomeList))));
+
+ DBAdSelection expectedDBAdSelectionResult =
+ new DBAdSelection.Builder()
+ .setAdSelectionId(AD_SELECTION_ID)
+ .setCreationTimestamp(Calendar.getInstance().toInstant())
+ .setWinningAdBid(
+ mAdScoringOutcomeForBuyer2.getAdWithScore().getAdWithBid().getBid())
+ .setCustomAudienceSignals(
+ mAdScoringOutcomeForBuyer2
+ .getCustomAudienceBiddingInfo()
+ .getCustomAudienceSignals())
+ .setWinningAdRenderUri(
+ mAdScoringOutcomeForBuyer2
+ .getAdWithScore()
+ .getAdWithBid()
+ .getAdData()
+ .getRenderUri())
+ .setBiddingLogicUri(
+ mAdScoringOutcomeForBuyer2
+ .getCustomAudienceBiddingInfo()
+ .getBiddingLogicUri())
+ .setContextualSignals("{}")
+ .setCallerPackageName(MY_APP_PACKAGE_NAME)
+ .build();
+
+ when(mMockAdSelectionIdGenerator.generateId()).thenReturn(AD_SELECTION_ID);
+ when(mMockApiServiceLatencyCalculator.getApiServiceOverallLatencyMs())
+ .thenReturn(RUN_AD_SELECTION_OVERALL_LATENCY_MS);
+ mAdSelectionRunner =
+ new OnDeviceAdSelectionRunner(
+ mContext,
+ mCustomAudienceDao,
+ mAdSelectionEntryDao,
+ mAdServicesHttpsClient,
+ mLightweightExecutorService,
+ mBackgroundExecutorService,
+ mScheduledExecutor,
+ mConsentManagerMock,
+ mMockAdsScoreGenerator,
+ mMockAdBidGenerator,
+ mMockAdSelectionIdGenerator,
+ mClock,
+ mAdServicesLoggerMock,
+ mAppImportanceFilter,
+ mFlags,
+ mThrottlerSupplier,
+ CALLER_UID,
+ mFledgeAuthorizationFilter,
+ mFledgeAllowListsFilter,
+ mMockApiServiceLatencyCalculator);
+
+ assertFalse(mAdSelectionEntryDao.doesAdSelectionIdExist(AD_SELECTION_ID));
+
+ AdSelectionTestCallback resultsCallback =
+ invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
+
+ verify(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer1,
+ adSelectionConfig.getAdSelectionSignals(),
+ AdSelectionSignals.EMPTY,
+ AdSelectionSignals.EMPTY);
+ verify(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer2,
+ adSelectionConfig.getAdSelectionSignals(),
+ AdSelectionSignals.EMPTY,
+ AdSelectionSignals.EMPTY);
+ verify(mMockAdsScoreGenerator).runAdScoring(mAdBiddingOutcomeList, adSelectionConfig);
+
+ assertTrue(resultsCallback.mIsSuccess);
+ assertEquals(
+ expectedDBAdSelectionResult.getAdSelectionId(),
+ resultsCallback.mAdSelectionResponse.getAdSelectionId());
+ assertEquals(
+ expectedDBAdSelectionResult.getWinningAdRenderUri(),
+ resultsCallback.mAdSelectionResponse.getRenderUri());
+ assertTrue(mAdSelectionEntryDao.doesAdSelectionIdExist(AD_SELECTION_ID));
+ verify(mMockApiServiceLatencyCalculator).getApiServiceOverallLatencyMs();
+ verify(mAdServicesLoggerMock)
+ .logFledgeApiCallStats(
+ eq(AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS),
+ eq(AdServicesStatusUtils.STATUS_SUCCESS),
+ eq(RUN_AD_SELECTION_OVERALL_LATENCY_MS));
+ }
+
+ @Test
+ public void testRunAdSelectionNoCAs() {
+ when(mClock.instant()).thenReturn(Clock.systemUTC().instant());
+ doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent();
+
+ // Creating ad selection config for happy case with all the buyers in place
+ AdSelectionConfig adSelectionConfig = mAdSelectionConfigBuilder.build();
+
+ // Do not populate CustomAudience DAO
+
+ // If there are no corresponding CAs we should not even attempt bidding
+ verifyZeroInteractions(mMockAdBidGenerator);
+ // If there was no bidding then we should not even attempt to run scoring
+ verifyZeroInteractions(mMockAdsScoreGenerator);
+ when(mMockApiServiceLatencyCalculator.getApiServiceOverallLatencyMs())
+ .thenReturn(RUN_AD_SELECTION_OVERALL_LATENCY_MS);
+ mAdSelectionRunner =
+ new OnDeviceAdSelectionRunner(
+ mContext,
+ mCustomAudienceDao,
+ mAdSelectionEntryDao,
+ mAdServicesHttpsClient,
+ mLightweightExecutorService,
+ mBackgroundExecutorService,
+ mScheduledExecutor,
+ mConsentManagerMock,
+ mMockAdsScoreGenerator,
+ mMockAdBidGenerator,
+ mMockAdSelectionIdGenerator,
+ mClock,
+ mAdServicesLoggerMock,
+ mAppImportanceFilter,
+ mFlags,
+ mThrottlerSupplier,
+ CALLER_UID,
+ mFledgeAuthorizationFilter,
+ mFledgeAllowListsFilter,
+ mMockApiServiceLatencyCalculator);
+ AdSelectionTestCallback resultsCallback =
+ invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
+
+ assertFalse(resultsCallback.mIsSuccess);
+ verifyErrorMessageIsCorrect(
+ resultsCallback.mFledgeErrorResponse.getErrorMessage(), ERROR_NO_CA_AVAILABLE);
+ verify(mMockApiServiceLatencyCalculator).getApiServiceOverallLatencyMs();
+ verify(mAdServicesLoggerMock)
+ .logFledgeApiCallStats(
+ eq(AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS),
+ eq(STATUS_INTERNAL_ERROR),
+ eq(RUN_AD_SELECTION_OVERALL_LATENCY_MS));
+ }
+
+ @Test
+ public void testRunAdSelectionCallerNotInForeground_fails() {
+ doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent();
+
+ // Creating ad selection config for happy case with all the buyers in place
+ AdSelectionConfig adSelectionConfig = mAdSelectionConfigBuilder.build();
+
+ String validationFailure = "Failed app status validation";
+ doThrow(new IllegalStateException(validationFailure))
+ .when(mAppImportanceFilter)
+ .assertCallerIsInForeground(
+ CALLER_UID, AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS, null);
+
+ mAdSelectionRunner =
+ new OnDeviceAdSelectionRunner(
+ mContext,
+ mCustomAudienceDao,
+ mAdSelectionEntryDao,
+ mAdServicesHttpsClient,
+ mLightweightExecutorService,
+ mBackgroundExecutorService,
+ mScheduledExecutor,
+ mConsentManagerMock,
+ mMockAdsScoreGenerator,
+ mMockAdBidGenerator,
+ mMockAdSelectionIdGenerator,
+ mClock,
+ mAdServicesLoggerMock,
+ mAppImportanceFilter,
+ mFlags,
+ mThrottlerSupplier,
+ CALLER_UID,
+ mFledgeAuthorizationFilter,
+ mFledgeAllowListsFilter,
+ mMockApiServiceLatencyCalculator);
+ AdSelectionTestCallback resultsCallback =
+ invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
+
+ assertFalse(resultsCallback.mIsSuccess);
+ verifyErrorMessageIsCorrect(
+ resultsCallback.mFledgeErrorResponse.getErrorMessage(), validationFailure);
+ }
+
+ @Test
+ public void testRunAdSelectionCallerNotInForegroundFlagDisabled_doesNotFailValidation() {
+ doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent();
+ Flags flags =
+ new Flags() {
+ @Override
+ public boolean getEnforceForegroundStatusForFledgeRunAdSelection() {
+ return false;
+ }
+
+ @Override
+ public boolean getDisableFledgeEnrollmentCheck() {
+ return true;
+ }
+ };
+
+ // Creating ad selection config for happy case with all the buyers in place
+ AdSelectionConfig adSelectionConfig = mAdSelectionConfigBuilder.build();
+ when(mMockApiServiceLatencyCalculator.getApiServiceOverallLatencyMs())
+ .thenReturn(RUN_AD_SELECTION_OVERALL_LATENCY_MS);
+ mAdSelectionRunner =
+ new OnDeviceAdSelectionRunner(
+ mContext,
+ mCustomAudienceDao,
+ mAdSelectionEntryDao,
+ mAdServicesHttpsClient,
+ mLightweightExecutorService,
+ mBackgroundExecutorService,
+ mScheduledExecutor,
+ mConsentManagerMock,
+ mMockAdsScoreGenerator,
+ mMockAdBidGenerator,
+ mMockAdSelectionIdGenerator,
+ mClock,
+ mAdServicesLoggerMock,
+ mAppImportanceFilter,
+ flags,
+ mThrottlerSupplier,
+ CALLER_UID,
+ mFledgeAuthorizationFilter,
+ mFledgeAllowListsFilter,
+ mMockApiServiceLatencyCalculator);
+ AdSelectionTestCallback resultsCallback =
+ invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
+
+ verifyZeroInteractions(mAppImportanceFilter);
+
+ // This ad selection fails because there are no CAs but the foreground status validation
+ // is not blocking the rest of the process
+ assertFalse(resultsCallback.mIsSuccess);
+ verifyErrorMessageIsCorrect(
+ resultsCallback.mFledgeErrorResponse.getErrorMessage(), ERROR_NO_CA_AVAILABLE);
+ }
+
+ @Test
+ public void testRunAdSelectionPartialBidding() throws AdServicesException {
+ when(mClock.instant()).thenReturn(Clock.systemUTC().instant());
+ doReturn(mFlags).when(FlagsFactory::getFlags);
+ doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent();
+
+ // Creating ad selection config for happy case with all the buyers in place
+ AdSelectionConfig adSelectionConfig = mAdSelectionConfigBuilder.build();
+
+ // Populating the Custom Audience DB
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer1,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_1));
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer2,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_2));
+
+ // Getting BiddingOutcome-forBuyerX corresponding to each CA-forBuyerX
+ // In this case assuming bidding fails for one of ads and return partial result
+ doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer1)))
+ .when(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer1,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer1.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ doReturn(FluentFuture.from(Futures.immediateFuture(null)))
+ .when(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer2,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer2.getBuyer()),
+ AdSelectionSignals.EMPTY);
+
+ // Getting ScoringOutcome-ForBuyerX corresponding to each BiddingOutcome-forBuyerX
+ // In this case we are only expected to get score for the first bidding,
+ // as second one is null
+ List<AdBiddingOutcome> partialBiddingOutcome = Arrays.asList(mAdBiddingOutcomeForBuyer1);
+ when(mMockAdsScoreGenerator.runAdScoring(partialBiddingOutcome, adSelectionConfig))
+ .thenReturn(
+ (FluentFuture.from(
+ Futures.immediateFuture(mAdScoringOutcomeList.subList(0, 1)))));
+
+ when(mMockAdSelectionIdGenerator.generateId()).thenReturn(AD_SELECTION_ID);
+ when(mMockApiServiceLatencyCalculator.getApiServiceOverallLatencyMs())
+ .thenReturn(RUN_AD_SELECTION_OVERALL_LATENCY_MS);
+ mAdSelectionRunner =
+ new OnDeviceAdSelectionRunner(
+ mContext,
+ mCustomAudienceDao,
+ mAdSelectionEntryDao,
+ mAdServicesHttpsClient,
+ mLightweightExecutorService,
+ mBackgroundExecutorService,
+ mScheduledExecutor,
+ mConsentManagerMock,
+ mMockAdsScoreGenerator,
+ mMockAdBidGenerator,
+ mMockAdSelectionIdGenerator,
+ mClock,
+ mAdServicesLoggerMock,
+ mAppImportanceFilter,
+ mFlags,
+ mThrottlerSupplier,
+ CALLER_UID,
+ mFledgeAuthorizationFilter,
+ mFledgeAllowListsFilter,
+ mMockApiServiceLatencyCalculator);
+
+ assertFalse(mAdSelectionEntryDao.doesAdSelectionIdExist(AD_SELECTION_ID));
+
+ AdSelectionTestCallback resultsCallback =
+ invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
+
+ DBAdSelection expectedDBAdSelectionResult =
+ new DBAdSelection.Builder()
+ .setAdSelectionId(AD_SELECTION_ID)
+ .setCreationTimestamp(Calendar.getInstance().toInstant())
+ .setWinningAdBid(
+ mAdScoringOutcomeForBuyer1.getAdWithScore().getAdWithBid().getBid())
+ .setCustomAudienceSignals(
+ mAdScoringOutcomeForBuyer1
+ .getCustomAudienceBiddingInfo()
+ .getCustomAudienceSignals())
+ .setWinningAdRenderUri(
+ mAdScoringOutcomeForBuyer1
+ .getAdWithScore()
+ .getAdWithBid()
+ .getAdData()
+ .getRenderUri())
+ .setBiddingLogicUri(
+ mAdScoringOutcomeForBuyer1
+ .getCustomAudienceBiddingInfo()
+ .getBiddingLogicUri())
+ .setContextualSignals("{}")
+ .setCallerPackageName(MY_APP_PACKAGE_NAME)
+ .build();
+
+ verify(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer1,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer1.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ verify(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer2,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer2.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ verify(mMockAdsScoreGenerator).runAdScoring(partialBiddingOutcome, adSelectionConfig);
+
+ assertTrue(resultsCallback.mIsSuccess);
+ assertEquals(
+ expectedDBAdSelectionResult.getAdSelectionId(),
+ resultsCallback.mAdSelectionResponse.getAdSelectionId());
+ assertEquals(
+ expectedDBAdSelectionResult.getWinningAdRenderUri(),
+ resultsCallback.mAdSelectionResponse.getRenderUri());
+ assertTrue(mAdSelectionEntryDao.doesAdSelectionIdExist(AD_SELECTION_ID));
+ verify(mMockApiServiceLatencyCalculator).getApiServiceOverallLatencyMs();
+ verify(mAdServicesLoggerMock)
+ .logFledgeApiCallStats(
+ eq(AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS),
+ eq(AdServicesStatusUtils.STATUS_SUCCESS),
+ eq(RUN_AD_SELECTION_OVERALL_LATENCY_MS));
+ }
+
+ @Test
+ public void testRunAdSelectionBiddingFailure() {
+ when(mClock.instant()).thenReturn(Clock.systemUTC().instant());
+ doReturn(mFlags).when(FlagsFactory::getFlags);
+ doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent();
+
+ // Creating ad selection config for happy case with all the buyers in place
+ AdSelectionConfig adSelectionConfig = mAdSelectionConfigBuilder.build();
+
+ // Populating the Custom Audience DB
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer1,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_1));
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer2,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_2));
+
+ // Getting BiddingOutcome-forBuyerX corresponding to each CA-forBuyerX
+ // In this case assuming bidding fails and returns null
+ doReturn(FluentFuture.from(Futures.immediateFuture(null)))
+ .when(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer1,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer1.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ doReturn(FluentFuture.from(Futures.immediateFuture(null)))
+ .when(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer2,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer2.getBuyer()),
+ AdSelectionSignals.EMPTY);
+
+ // If the result of bidding is empty, then we should not even attempt to run scoring
+ verifyZeroInteractions(mMockAdsScoreGenerator);
+ when(mMockApiServiceLatencyCalculator.getApiServiceOverallLatencyMs())
+ .thenReturn(RUN_AD_SELECTION_OVERALL_LATENCY_MS);
+ mAdSelectionRunner =
+ new OnDeviceAdSelectionRunner(
+ mContext,
+ mCustomAudienceDao,
+ mAdSelectionEntryDao,
+ mAdServicesHttpsClient,
+ mLightweightExecutorService,
+ mBackgroundExecutorService,
+ mScheduledExecutor,
+ mConsentManagerMock,
+ mMockAdsScoreGenerator,
+ mMockAdBidGenerator,
+ mMockAdSelectionIdGenerator,
+ mClock,
+ mAdServicesLoggerMock,
+ mAppImportanceFilter,
+ mFlags,
+ mThrottlerSupplier,
+ CALLER_UID,
+ mFledgeAuthorizationFilter,
+ mFledgeAllowListsFilter,
+ mMockApiServiceLatencyCalculator);
+
+ AdSelectionTestCallback resultsCallback =
+ invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
+
+ verify(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer1,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer1.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ verify(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer2,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer2.getBuyer()),
+ AdSelectionSignals.EMPTY);
+
+ assertFalse(resultsCallback.mIsSuccess);
+ verifyErrorMessageIsCorrect(
+ resultsCallback.mFledgeErrorResponse.getErrorMessage(),
+ ERROR_NO_VALID_BIDS_FOR_SCORING);
+ verify(mMockApiServiceLatencyCalculator).getApiServiceOverallLatencyMs();
+ verify(mAdServicesLoggerMock)
+ .logFledgeApiCallStats(
+ eq(AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS),
+ eq(STATUS_INTERNAL_ERROR),
+ eq(RUN_AD_SELECTION_OVERALL_LATENCY_MS));
+ }
+
+ @Test
+ public void testRunAdSelectionScoringFailure() throws AdServicesException {
+ when(mClock.instant()).thenReturn(Clock.systemUTC().instant());
+ doReturn(mFlags).when(FlagsFactory::getFlags);
+ doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent();
+
+ // Creating ad selection config for happy case with all the buyers in place
+ AdSelectionConfig adSelectionConfig = mAdSelectionConfigBuilder.build();
+
+ // Populating the Custom Audience DB
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer1,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_1));
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer2,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_2));
+
+ // Getting BiddingOutcome-forBuyerX corresponding to each CA-forBuyerX
+ doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer1)))
+ .when(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer1,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer1.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer2)))
+ .when(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer2,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer2.getBuyer()),
+ AdSelectionSignals.EMPTY);
+
+ // Getting ScoringOutcome-ForBuyerX corresponding to each BiddingOutcome-forBuyerX
+ // In this case assuming we get an empty result
+ when(mMockAdsScoreGenerator.runAdScoring(mAdBiddingOutcomeList, adSelectionConfig))
+ .thenReturn((FluentFuture.from(Futures.immediateFuture(Collections.EMPTY_LIST))));
+ when(mMockApiServiceLatencyCalculator.getApiServiceOverallLatencyMs())
+ .thenReturn(RUN_AD_SELECTION_OVERALL_LATENCY_MS);
+ mAdSelectionRunner =
+ new OnDeviceAdSelectionRunner(
+ mContext,
+ mCustomAudienceDao,
+ mAdSelectionEntryDao,
+ mAdServicesHttpsClient,
+ mLightweightExecutorService,
+ mBackgroundExecutorService,
+ mScheduledExecutor,
+ mConsentManagerMock,
+ mMockAdsScoreGenerator,
+ mMockAdBidGenerator,
+ mMockAdSelectionIdGenerator,
+ mClock,
+ mAdServicesLoggerMock,
+ mAppImportanceFilter,
+ mFlags,
+ mThrottlerSupplier,
+ CALLER_UID,
+ mFledgeAuthorizationFilter,
+ mFledgeAllowListsFilter,
+ mMockApiServiceLatencyCalculator);
+
+ AdSelectionTestCallback resultsCallback =
+ invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
+
+ verify(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer1,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer1.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ verify(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer2,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer2.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ verify(mMockAdsScoreGenerator).runAdScoring(mAdBiddingOutcomeList, adSelectionConfig);
+
+ assertFalse(resultsCallback.mIsSuccess);
+ verifyErrorMessageIsCorrect(
+ resultsCallback.mFledgeErrorResponse.getErrorMessage(), ERROR_NO_WINNING_AD_FOUND);
+ verify(mMockApiServiceLatencyCalculator).getApiServiceOverallLatencyMs();
+ verify(mAdServicesLoggerMock)
+ .logFledgeApiCallStats(
+ eq(AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS),
+ eq(STATUS_INTERNAL_ERROR),
+ eq(RUN_AD_SELECTION_OVERALL_LATENCY_MS));
+ }
+
+ @Test
+ public void testRunAdSelectionNegativeScoring() throws AdServicesException {
+ when(mClock.instant()).thenReturn(Clock.systemUTC().instant());
+ doReturn(mFlags).when(FlagsFactory::getFlags);
+ doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent();
+
+ // Creating ad selection config for happy case with all the buyers in place
+ AdSelectionConfig adSelectionConfig = mAdSelectionConfigBuilder.build();
+
+ // Populating the Custom Audience DB
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer1,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_1));
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer2,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_2));
+
+ // Getting BiddingOutcome-forBuyerX corresponding to each CA-forBuyerX
+ doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer1)))
+ .when(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer1,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer1.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer2)))
+ .when(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer2,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer2.getBuyer()),
+ AdSelectionSignals.EMPTY);
+
+ AdScoringOutcome adScoringNegativeOutcomeForBuyer1 =
+ AdScoringOutcomeFixture.anAdScoringBuilder(BUYER_1, -2.0).build();
+ AdScoringOutcome adScoringNegativeOutcomeForBuyer2 =
+ AdScoringOutcomeFixture.anAdScoringBuilder(BUYER_2, -3.0).build();
+ List<AdScoringOutcome> negativeScoreOutcome =
+ Arrays.asList(adScoringNegativeOutcomeForBuyer1, adScoringNegativeOutcomeForBuyer2);
+
+ // Getting ScoringOutcome-ForBuyerX corresponding to each BiddingOutcome-forBuyerX
+ // In this case assuming we get a result with negative scores
+ when(mMockAdsScoreGenerator.runAdScoring(mAdBiddingOutcomeList, adSelectionConfig))
+ .thenReturn((FluentFuture.from(Futures.immediateFuture(negativeScoreOutcome))));
+ when(mMockApiServiceLatencyCalculator.getApiServiceOverallLatencyMs())
+ .thenReturn(RUN_AD_SELECTION_OVERALL_LATENCY_MS);
+ mAdSelectionRunner =
+ new OnDeviceAdSelectionRunner(
+ mContext,
+ mCustomAudienceDao,
+ mAdSelectionEntryDao,
+ mAdServicesHttpsClient,
+ mLightweightExecutorService,
+ mBackgroundExecutorService,
+ mScheduledExecutor,
+ mConsentManagerMock,
+ mMockAdsScoreGenerator,
+ mMockAdBidGenerator,
+ mMockAdSelectionIdGenerator,
+ mClock,
+ mAdServicesLoggerMock,
+ mAppImportanceFilter,
+ mFlags,
+ mThrottlerSupplier,
+ CALLER_UID,
+ mFledgeAuthorizationFilter,
+ mFledgeAllowListsFilter,
+ mMockApiServiceLatencyCalculator);
+
+ AdSelectionTestCallback resultsCallback =
+ invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
+
+ verify(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer1,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer1.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ verify(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer2,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer2.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ verify(mMockAdsScoreGenerator).runAdScoring(mAdBiddingOutcomeList, adSelectionConfig);
+
+ assertFalse(resultsCallback.mIsSuccess);
+ verifyErrorMessageIsCorrect(
+ resultsCallback.mFledgeErrorResponse.getErrorMessage(), ERROR_NO_WINNING_AD_FOUND);
+ verify(mMockApiServiceLatencyCalculator).getApiServiceOverallLatencyMs();
+ verify(mAdServicesLoggerMock)
+ .logFledgeApiCallStats(
+ eq(AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS),
+ eq(STATUS_INTERNAL_ERROR),
+ eq(RUN_AD_SELECTION_OVERALL_LATENCY_MS));
+ }
+
+ @Test
+ public void testRunAdSelectionPartialNegativeScoring() throws AdServicesException {
+ when(mClock.instant()).thenReturn(Clock.systemUTC().instant());
+ doReturn(mFlags).when(FlagsFactory::getFlags);
+ doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent();
+
+ // Creating ad selection config for happy case with all the buyers in place
+ AdSelectionConfig adSelectionConfig = mAdSelectionConfigBuilder.build();
+
+ // Populating the Custom Audience DB
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer1,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_1));
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer2,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_2));
+
+ // Getting BiddingOutcome-forBuyerX corresponding to each CA-forBuyerX
+ doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer1)))
+ .when(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer1,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer1.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer2)))
+ .when(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer2,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer2.getBuyer()),
+ AdSelectionSignals.EMPTY);
+
+ AdScoringOutcome adScoringNegativeOutcomeForBuyer1 =
+ AdScoringOutcomeFixture.anAdScoringBuilder(BUYER_1, 2.0).build();
+ AdScoringOutcome adScoringNegativeOutcomeForBuyer2 =
+ AdScoringOutcomeFixture.anAdScoringBuilder(BUYER_2, -3.0).build();
+ List<AdScoringOutcome> negativeScoreOutcome =
+ Arrays.asList(adScoringNegativeOutcomeForBuyer1, adScoringNegativeOutcomeForBuyer2);
+
+ // Getting ScoringOutcome-ForBuyerX corresponding to each BiddingOutcome-forBuyerX
+ // In this case assuming we get a result with partially negative scores
+ when(mMockAdsScoreGenerator.runAdScoring(mAdBiddingOutcomeList, adSelectionConfig))
+ .thenReturn((FluentFuture.from(Futures.immediateFuture(negativeScoreOutcome))));
+
+ when(mMockAdSelectionIdGenerator.generateId()).thenReturn(AD_SELECTION_ID);
+ when(mMockApiServiceLatencyCalculator.getApiServiceOverallLatencyMs())
+ .thenReturn(RUN_AD_SELECTION_OVERALL_LATENCY_MS);
+ mAdSelectionRunner =
+ new OnDeviceAdSelectionRunner(
+ mContext,
+ mCustomAudienceDao,
+ mAdSelectionEntryDao,
+ mAdServicesHttpsClient,
+ mLightweightExecutorService,
+ mBackgroundExecutorService,
+ mScheduledExecutor,
+ mConsentManagerMock,
+ mMockAdsScoreGenerator,
+ mMockAdBidGenerator,
+ mMockAdSelectionIdGenerator,
+ mClock,
+ mAdServicesLoggerMock,
+ mAppImportanceFilter,
+ mFlags,
+ mThrottlerSupplier,
+ CALLER_UID,
+ mFledgeAuthorizationFilter,
+ mFledgeAllowListsFilter,
+ mMockApiServiceLatencyCalculator);
+
+ assertFalse(mAdSelectionEntryDao.doesAdSelectionIdExist(AD_SELECTION_ID));
+
+ AdSelectionTestCallback resultsCallback =
+ invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
+
+ verify(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer1,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer1.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ verify(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer2,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer2.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ verify(mMockAdsScoreGenerator).runAdScoring(mAdBiddingOutcomeList, adSelectionConfig);
+
+ DBAdSelection expectedDBAdSelectionResult =
+ new DBAdSelection.Builder()
+ .setAdSelectionId(AD_SELECTION_ID)
+ .setCreationTimestamp(Calendar.getInstance().toInstant())
+ .setWinningAdBid(
+ mAdScoringOutcomeForBuyer1.getAdWithScore().getAdWithBid().getBid())
+ .setCustomAudienceSignals(
+ mAdScoringOutcomeForBuyer1
+ .getCustomAudienceBiddingInfo()
+ .getCustomAudienceSignals())
+ .setWinningAdRenderUri(
+ mAdScoringOutcomeForBuyer1
+ .getAdWithScore()
+ .getAdWithBid()
+ .getAdData()
+ .getRenderUri())
+ .setBiddingLogicUri(
+ mAdScoringOutcomeForBuyer1
+ .getCustomAudienceBiddingInfo()
+ .getBiddingLogicUri())
+ .setContextualSignals("{}")
+ .setCallerPackageName(MY_APP_PACKAGE_NAME)
+ .build();
+
+ assertTrue(resultsCallback.mIsSuccess);
+ assertEquals(
+ expectedDBAdSelectionResult.getAdSelectionId(),
+ resultsCallback.mAdSelectionResponse.getAdSelectionId());
+ assertEquals(
+ expectedDBAdSelectionResult.getWinningAdRenderUri(),
+ resultsCallback.mAdSelectionResponse.getRenderUri());
+ assertTrue(mAdSelectionEntryDao.doesAdSelectionIdExist(AD_SELECTION_ID));
+ verify(mMockApiServiceLatencyCalculator).getApiServiceOverallLatencyMs();
+ verify(mAdServicesLoggerMock)
+ .logFledgeApiCallStats(
+ eq(AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS),
+ eq(AdServicesStatusUtils.STATUS_SUCCESS),
+ eq(RUN_AD_SELECTION_OVERALL_LATENCY_MS));
+ }
+
+ @Test
+ public void testRunAdSelectionScoringException() throws AdServicesException {
+ when(mClock.instant()).thenReturn(Clock.systemUTC().instant());
+ doReturn(mFlags).when(FlagsFactory::getFlags);
+ doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent();
+
+ // Creating ad selection config for happy case with all the buyers in place
+ AdSelectionConfig adSelectionConfig =
+ mAdSelectionConfigBuilder
+ .setAdSelectionSignals(AdSelectionSignals.fromString("{/}"))
+ .build();
+
+ // Populating the Custom Audience DB
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer1,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_1));
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer2,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_2));
+
+ // Getting BiddingOutcome-forBuyerX corresponding to each CA-forBuyerX
+ doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer1)))
+ .when(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer1,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer1.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer2)))
+ .when(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer2,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer2.getBuyer()),
+ AdSelectionSignals.EMPTY);
+
+ // Getting ScoringOutcome-ForBuyerX corresponding to each BiddingOutcome-forBuyerX
+ // In this case we expect a JSON validation exception
+ when(mMockAdsScoreGenerator.runAdScoring(mAdBiddingOutcomeList, adSelectionConfig))
+ .thenThrow(new AdServicesException(ERROR_INVALID_JSON));
+ when(mMockApiServiceLatencyCalculator.getApiServiceOverallLatencyMs())
+ .thenReturn(RUN_AD_SELECTION_OVERALL_LATENCY_MS);
+ mAdSelectionRunner =
+ new OnDeviceAdSelectionRunner(
+ mContext,
+ mCustomAudienceDao,
+ mAdSelectionEntryDao,
+ mAdServicesHttpsClient,
+ mLightweightExecutorService,
+ mBackgroundExecutorService,
+ mScheduledExecutor,
+ mConsentManagerMock,
+ mMockAdsScoreGenerator,
+ mMockAdBidGenerator,
+ mMockAdSelectionIdGenerator,
+ mClock,
+ mAdServicesLoggerMock,
+ mAppImportanceFilter,
+ mFlags,
+ mThrottlerSupplier,
+ CALLER_UID,
+ mFledgeAuthorizationFilter,
+ mFledgeAllowListsFilter,
+ mMockApiServiceLatencyCalculator);
+
+ AdSelectionTestCallback resultsCallback =
+ invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
+
+ verify(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer1,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer1.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ verify(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer2,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer2.getBuyer()),
+ AdSelectionSignals.EMPTY);
+
+ assertFalse(resultsCallback.mIsSuccess);
+ verifyErrorMessageIsCorrect(
+ resultsCallback.mFledgeErrorResponse.getErrorMessage(), ERROR_INVALID_JSON);
+ verify(mMockApiServiceLatencyCalculator).getApiServiceOverallLatencyMs();
+ verify(mAdServicesLoggerMock)
+ .logFledgeApiCallStats(
+ eq(AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS),
+ eq(STATUS_INTERNAL_ERROR),
+ eq(RUN_AD_SELECTION_OVERALL_LATENCY_MS));
+ }
+
+ @Test
+ public void testRunAdSelectionOrchestrationTimesOut() throws AdServicesException {
+ when(mClock.instant()).thenReturn(Clock.systemUTC().instant());
+ doReturn(mFlags).when(FlagsFactory::getFlags);
+ doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent();
+
+ Flags flagsWithSmallerLimits =
+ new Flags() {
+ @Override
+ public long getAdSelectionOverallTimeoutMs() {
+ return 100;
+ }
+
+ @Override
+ public boolean getDisableFledgeEnrollmentCheck() {
+ return true;
+ }
+ };
+
+ // Creating ad selection config for happy case with all the buyers in place
+ AdSelectionConfig adSelectionConfig = mAdSelectionConfigBuilder.build();
+
+ // Populating the Custom Audience DB
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer1,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_1));
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer2,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_2));
+
+ // Getting BiddingOutcome-forBuyerX corresponding to each CA-forBuyerX
+ doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer1)))
+ .when(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer1,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer1.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer2)))
+ .when(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer2,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer2.getBuyer()),
+ AdSelectionSignals.EMPTY);
+
+ // Getting ScoringOutcome-ForBuyerX corresponding to each BiddingOutcome-forBuyerX
+ when(mMockAdsScoreGenerator.runAdScoring(mAdBiddingOutcomeList, adSelectionConfig))
+ .thenReturn((FluentFuture.from(Futures.immediateFuture(mAdScoringOutcomeList))));
+
+ Instant adSelectionCreationTs = Clock.systemUTC().instant().truncatedTo(ChronoUnit.MILLIS);
+ when(mClock.instant()).thenReturn(adSelectionCreationTs);
+
+ when(mMockAdSelectionIdGenerator.generateId())
+ .thenAnswer(
+ new AnswersWithDelay(
+ 2 * mFlags.getAdSelectionOverallTimeoutMs(),
+ new Returns(AD_SELECTION_ID)));
+ when(mMockApiServiceLatencyCalculator.getApiServiceOverallLatencyMs())
+ .thenReturn(RUN_AD_SELECTION_OVERALL_LATENCY_MS);
+ mAdSelectionRunner =
+ new OnDeviceAdSelectionRunner(
+ mContext,
+ mCustomAudienceDao,
+ mAdSelectionEntryDao,
+ mAdServicesHttpsClient,
+ mLightweightExecutorService,
+ mBackgroundExecutorService,
+ mScheduledExecutor,
+ mConsentManagerMock,
+ mMockAdsScoreGenerator,
+ mMockAdBidGenerator,
+ mMockAdSelectionIdGenerator,
+ mClock,
+ mAdServicesLoggerMock,
+ mAppImportanceFilter,
+ flagsWithSmallerLimits,
+ mThrottlerSupplier,
+ CALLER_UID,
+ mFledgeAuthorizationFilter,
+ mFledgeAllowListsFilter,
+ mMockApiServiceLatencyCalculator);
+
+ assertFalse(mAdSelectionEntryDao.doesAdSelectionIdExist(AD_SELECTION_ID));
+
+ AdSelectionTestCallback resultsCallback =
+ invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
+
+ assertFalse(resultsCallback.mIsSuccess);
+ FledgeErrorResponse response = resultsCallback.mFledgeErrorResponse;
+ verifyErrorMessageIsCorrect(response.getErrorMessage(), AD_SELECTION_TIMED_OUT);
+ Assert.assertEquals(
+ "Error response code mismatch",
+ AdServicesStatusUtils.STATUS_TIMEOUT,
+ response.getStatusCode());
+ }
+
+ @Test
+ public void testRunAdSelectionPerBuyerTimeout() throws AdServicesException {
+ Flags flagsWithSmallPerBuyerTimeout =
+ new Flags() {
+ @Override
+ public long getAdSelectionOverallTimeoutMs() {
+ return 5000;
+ }
+
+ @Override
+ public long getAdSelectionBiddingTimeoutPerBuyerMs() {
+ return 100;
+ }
+
+ @Override
+ public boolean getDisableFledgeEnrollmentCheck() {
+ return true;
+ }
+ };
+ when(mClock.instant()).thenReturn(Clock.systemUTC().instant());
+ doReturn(flagsWithSmallPerBuyerTimeout).when(FlagsFactory::getFlags);
+ doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent();
+ AdSelectionConfig adSelectionConfig = mAdSelectionConfigBuilder.build();
+
+ // Populating the Custom Audience DB
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer1,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_1));
+ mCustomAudienceDao.insertOrOverwriteCustomAudience(
+ mDBCustomAudienceForBuyer2,
+ CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER_2));
+
+ Callable<AdBiddingOutcome> delayedBiddingOutcomeForBuyer1 =
+ () -> {
+ TimeUnit.MILLISECONDS.sleep(
+ 10
+ * flagsWithSmallPerBuyerTimeout
+ .getAdSelectionBiddingTimeoutPerBuyerMs());
+ return mAdBiddingOutcomeForBuyer1;
+ };
+
+ doReturn(mLightweightExecutorService.submit(delayedBiddingOutcomeForBuyer1))
+ .when(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer1,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer1.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer2)))
+ .when(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer2,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer2.getBuyer()),
+ AdSelectionSignals.EMPTY);
+
+ // bidding Outcome List should only have one bidding outcome
+ List<AdBiddingOutcome> adBiddingOutcomeList = ImmutableList.of(mAdBiddingOutcomeForBuyer2);
+
+ // Getting ScoringOutcome-ForBuyerX corresponding to each BiddingOutcome-forBuyerX
+ when(mMockAdsScoreGenerator.runAdScoring(adBiddingOutcomeList, adSelectionConfig))
+ .thenReturn(
+ (FluentFuture.from(
+ Futures.immediateFuture(
+ ImmutableList.of(mAdScoringOutcomeForBuyer2)))));
+
+ Instant adSelectionCreationTs = Clock.systemUTC().instant().truncatedTo(ChronoUnit.MILLIS);
+ when(mClock.instant()).thenReturn(adSelectionCreationTs);
+
+ DBAdSelectionEntry expectedAdSelectionResult =
+ new DBAdSelectionEntry.Builder()
+ .setAdSelectionId(AD_SELECTION_ID)
+ .setWinningAdBid(
+ mAdScoringOutcomeForBuyer2.getAdWithScore().getAdWithBid().getBid())
+ .setCustomAudienceSignals(
+ mAdScoringOutcomeForBuyer2
+ .getCustomAudienceBiddingInfo()
+ .getCustomAudienceSignals())
+ .setWinningAdRenderUri(
+ mAdScoringOutcomeForBuyer2
+ .getAdWithScore()
+ .getAdWithBid()
+ .getAdData()
+ .getRenderUri())
+ .setBuyerDecisionLogicJs(
+ mAdBiddingOutcomeForBuyer1
+ .getCustomAudienceBiddingInfo()
+ .getBuyerDecisionLogicJs())
+ // TODO(b/230569187) add contextual signals once supported in the main logic
+ .setContextualSignals("{}")
+ .setCreationTimestamp(adSelectionCreationTs)
+ .build();
+
+ when(mMockAdSelectionIdGenerator.generateId()).thenReturn(AD_SELECTION_ID);
+
+ mAdSelectionRunner =
+ new OnDeviceAdSelectionRunner(
+ mContext,
+ mCustomAudienceDao,
+ mAdSelectionEntryDao,
+ mAdServicesHttpsClient,
+ mLightweightExecutorService,
+ mBackgroundExecutorService,
+ mScheduledExecutor,
+ mConsentManagerMock,
+ mMockAdsScoreGenerator,
+ mMockAdBidGenerator,
+ mMockAdSelectionIdGenerator,
+ mClock,
+ mAdServicesLoggerMock,
+ mAppImportanceFilter,
+ flagsWithSmallPerBuyerTimeout,
+ mThrottlerSupplier,
+ CALLER_UID,
+ mFledgeAuthorizationFilter,
+ mFledgeAllowListsFilter,
+ mMockApiServiceLatencyCalculator);
+
+ assertFalse(mAdSelectionEntryDao.doesAdSelectionIdExist(AD_SELECTION_ID));
+
+ AdSelectionTestCallback resultsCallback =
+ invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
+
+ verify(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer1,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer1.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ verify(mMockAdBidGenerator)
+ .runAdBiddingPerCA(
+ mDBCustomAudienceForBuyer2,
+ adSelectionConfig.getAdSelectionSignals(),
+ adSelectionConfig
+ .getPerBuyerSignals()
+ .get(mDBCustomAudienceForBuyer2.getBuyer()),
+ AdSelectionSignals.EMPTY);
+ verify(mMockAdsScoreGenerator).runAdScoring(adBiddingOutcomeList, adSelectionConfig);
+
+ assertTrue(resultsCallback.mIsSuccess);
+ assertEquals(
+ expectedAdSelectionResult.getAdSelectionId(),
+ resultsCallback.mAdSelectionResponse.getAdSelectionId());
+ assertEquals(
+ expectedAdSelectionResult.getWinningAdRenderUri(),
+ resultsCallback.mAdSelectionResponse.getRenderUri());
+ assertTrue(mAdSelectionEntryDao.doesAdSelectionIdExist(AD_SELECTION_ID));
+ assertEquals(
+ expectedAdSelectionResult,
+ mAdSelectionEntryDao.getAdSelectionEntityById(AD_SELECTION_ID));
+
+ verify(mAdServicesLoggerMock)
+ .logFledgeApiCallStats(
+ eq(AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS),
+ eq(AdServicesStatusUtils.STATUS_SUCCESS),
+ anyInt());
+ }
+
+ @Test
+ public void testRunAdSelectionThrottledFailure() throws AdServicesException {
+ when(mClock.instant()).thenReturn(Clock.systemUTC().instant());
+ doReturn(mFlags).when(FlagsFactory::getFlags);
+ doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent();
+ // Creating ad selection config for happy case with all the buyers in place
+ AdSelectionConfig adSelectionConfig = mAdSelectionConfigBuilder.build();
+
+ // Throttle Ad Selection request
+ when(mMockThrottler.tryAcquire(eq(FLEDGE_API_SELECT_ADS), anyString())).thenReturn(false);
+ when(mMockApiServiceLatencyCalculator.getApiServiceOverallLatencyMs())
+ .thenReturn(RUN_AD_SELECTION_OVERALL_LATENCY_MS);
+ mAdSelectionRunner =
+ new OnDeviceAdSelectionRunner(
+ mContext,
+ mCustomAudienceDao,
+ mAdSelectionEntryDao,
+ mAdServicesHttpsClient,
+ mLightweightExecutorService,
+ mBackgroundExecutorService,
+ mScheduledExecutor,
+ mConsentManagerMock,
+ mMockAdsScoreGenerator,
+ mMockAdBidGenerator,
+ mMockAdSelectionIdGenerator,
+ mClock,
+ mAdServicesLoggerMock,
+ mAppImportanceFilter,
+ mFlags,
+ mThrottlerSupplier,
+ CALLER_UID,
+ mFledgeAuthorizationFilter,
+ mFledgeAllowListsFilter,
+ mMockApiServiceLatencyCalculator);
+
+ AdSelectionTestCallback resultsCallback =
+ invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
+
+ assertFalse(resultsCallback.mIsSuccess);
+ FledgeErrorResponse response = resultsCallback.mFledgeErrorResponse;
+ verifyErrorMessageIsCorrect(response.getErrorMessage(), AD_SELECTION_THROTTLED);
+ Assert.assertEquals(
+ "Error response code mismatch",
+ AdServicesStatusUtils.STATUS_RATE_LIMIT_REACHED,
+ response.getStatusCode());
+ }
+
+ @Test
+ public void testAdSelectionRunnerInstanceNotCreatedIfJSSandboxNotInWebView() {
+ doReturn(null).when(WebView::getCurrentWebViewPackage);
+
+ ThrowingRunnable initializeAdSelectionRunner =
+ () ->
+ new OnDeviceAdSelectionRunner(
+ mContext,
+ mCustomAudienceDao,
+ mAdSelectionEntryDao,
+ mAdServicesHttpsClient,
+ mLightweightExecutorService,
+ mBackgroundExecutorService,
+ mScheduledExecutor,
+ mConsentManagerMock,
+ mAdServicesLoggerMock,
+ DevContext.createForDevOptionsDisabled(),
+ mAppImportanceFilter,
+ mFlags,
+ mThrottlerSupplier,
+ CALLER_UID,
+ mFledgeAuthorizationFilter,
+ mFledgeAllowListsFilter,
+ mMockApiServiceLatencyCalculator);
+
+ Throwable throwable =
+ assertThrows(IllegalArgumentException.class, initializeAdSelectionRunner);
+ verifyErrorMessageIsCorrect(throwable.getMessage(), JS_SANDBOX_IS_NOT_AVAILABLE);
+ }
+
+ private void verifyErrorMessageIsCorrect(
+ final String actualErrorMassage, final String expectedErrorReason) {
+ Assert.assertTrue(
+ String.format(
+ "Actual error [%s] does not begin with [%s]",
+ actualErrorMassage, ERROR_AD_SELECTION_FAILURE),
+ actualErrorMassage.startsWith(ERROR_AD_SELECTION_FAILURE));
+ Assert.assertTrue(
+ String.format(
+ "Actual error [%s] does not contain expected message: [%s]",
+ actualErrorMassage, expectedErrorReason),
+ actualErrorMassage.contains(expectedErrorReason));
+ }
+
+ private AdSelectionTestCallback invokeRunAdSelection(
+ AdSelectionRunner adSelectionRunner,
+ AdSelectionConfig adSelectionConfig,
+ String callerPackageName) {
+
+ // Counted down in 1) callback and 2) logApiCall
+ CountDownLatch countDownLatch = new CountDownLatch(2);
+ AdSelectionTestCallback adSelectionTestCallback =
+ new AdSelectionTestCallback(countDownLatch);
+
+ // Wait for the logging call, which happens after the callback
+ Answer<Void> countDownAnswer =
+ unused -> {
+ countDownLatch.countDown();
+ return null;
+ };
+ doAnswer(countDownAnswer)
+ .when(mAdServicesLoggerMock)
+ .logFledgeApiCallStats(anyInt(), anyInt(), anyInt());
+
+ AdSelectionInput input =
+ new AdSelectionInput.Builder()
+ .setAdSelectionConfig(adSelectionConfig)
+ .setCallerPackageName(callerPackageName)
+ .build();
+
+ adSelectionRunner.runAdSelection(input, adSelectionTestCallback);
+ try {
+ adSelectionTestCallback.mCountDownLatch.await();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ return adSelectionTestCallback;
+ }
+
+ @After
+ public void tearDown() {
+ mAdSelectionEntryDao.removeAdSelectionEntriesByIds(Arrays.asList(AD_SELECTION_ID));
+ if (mStaticMockSession != null) {
+ mStaticMockSession.finishMocking();
+ }
+ }
+
+ static class AdSelectionTestCallback extends AdSelectionCallback.Stub {
+
+ final CountDownLatch mCountDownLatch;
+ boolean mIsSuccess = false;
+ AdSelectionResponse mAdSelectionResponse;
+ FledgeErrorResponse mFledgeErrorResponse;
+
+ AdSelectionTestCallback(CountDownLatch countDownLatch) {
+ mCountDownLatch = countDownLatch;
+ mAdSelectionResponse = null;
+ mFledgeErrorResponse = null;
+ }
+
+ @Override
+ public void onSuccess(AdSelectionResponse adSelectionResponse) throws RemoteException {
+ mIsSuccess = true;
+ mAdSelectionResponse = adSelectionResponse;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void onFailure(FledgeErrorResponse fledgeErrorResponse) throws RemoteException {
+ mIsSuccess = false;
+ mFledgeErrorResponse = fledgeErrorResponse;
+ mCountDownLatch.countDown();
+ }
+ }
+}