blob: 2fb629b78bbd6ccb386045e7765452e35b866226 [file] [log] [blame]
/*
* 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 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.stats.AdServicesStatsLog.AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS;
import static com.android.adservices.stats.FledgeApiCallStatsMatcher.aCallStatForFledgeApiWithStatus;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
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.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.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.RemoteException;
import androidx.room.Room;
import androidx.test.core.app.ApplicationProvider;
import com.android.adservices.customaudience.DBCustomAudienceFixture;
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.service.Flags;
import com.android.adservices.service.FlagsFactory;
import com.android.adservices.service.common.AppImportanceFilter;
import com.android.adservices.service.consent.AdServicesApiConsent;
import com.android.adservices.service.consent.ConsentManager;
import com.android.adservices.service.stats.AdServicesLogger;
import com.android.adservices.service.stats.AdServicesLoggerImpl;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
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.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.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 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 AdSelectionRunnerTest {
private static final String TAG = AdSelectionRunnerTest.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 = 100;
private static final String MY_APP_PACKAGE_NAME = CommonFixture.TEST_PACKAGE_NAME;
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;
private Flags mFlags;
private Context mContext;
private ExecutorService mExecutorService;
private CustomAudienceDao mCustomAudienceDao;
private AdSelectionEntryDao mAdSelectionEntryDao;
@Spy private AdServicesLogger mAdServicesLoggerSpy = AdServicesLoggerImpl.getInstance();
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)
.strictness(Strictness.LENIENT)
.initMocks(this)
.startMocking();
mContext = ApplicationProvider.getApplicationContext();
mExecutorService = Executors.newFixedThreadPool(20);
mCustomAudienceDao =
Room.inMemoryDatabaseBuilder(mContext, CustomAudienceDatabase.class)
.build()
.customAudienceDao();
mAdSelectionEntryDao =
Room.inMemoryDatabaseBuilder(mContext, AdSelectionDatabase.class)
.build()
.adSelectionEntryDao();
mAdSelectionConfigBuilder =
AdSelectionConfigFixture.anAdSelectionConfigBuilder()
.setCustomAudienceBuyers(Arrays.asList(BUYER_1, BUYER_2));
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);
mFlags =
new Flags() {
@Override
public long getAdSelectionOverallTimeoutMs() {
return 300;
}
};
}
private DBCustomAudience createDBCustomAudience(final AdTechIdentifier buyer) {
return DBCustomAudienceFixture.getValidBuilderByBuyer(buyer)
.setOwner(buyer.toString() + CustomAudienceFixture.VALID_OWNER)
.setName(buyer.toString() + 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(any());
// 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,
adSelectionConfig);
doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer2)))
.when(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer2,
adSelectionConfig.getAdSelectionSignals(),
adSelectionConfig
.getPerBuyerSignals()
.get(mDBCustomAudienceForBuyer2.getBuyer()),
AdSelectionSignals.EMPTY,
adSelectionConfig);
// 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);
mAdSelectionRunner =
new AdSelectionRunner(
mContext,
mCustomAudienceDao,
mAdSelectionEntryDao,
mExecutorService,
mConsentManagerMock,
mMockAdsScoreGenerator,
mMockAdBidGenerator,
mMockAdSelectionIdGenerator,
mClock,
mAdServicesLoggerSpy,
mAppImportanceFilter,
mFlags,
CALLER_UID);
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,
adSelectionConfig);
verify(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer2,
adSelectionConfig.getAdSelectionSignals(),
adSelectionConfig
.getPerBuyerSignals()
.get(mDBCustomAudienceForBuyer2.getBuyer()),
AdSelectionSignals.EMPTY,
adSelectionConfig);
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(mAdServicesLoggerSpy)
.logFledgeApiCallStats(
AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
AdServicesStatusUtils.STATUS_SUCCESS);
verify(mAdServicesLoggerSpy)
.logApiCallStats(
aCallStatForFledgeApiWithStatus(
AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
AdServicesStatusUtils.STATUS_SUCCESS));
}
@Test
public void testRunAdSelectionWithRevokedUserConsentSuccess() throws AdServicesException {
doReturn(mFlags).when(FlagsFactory::getFlags);
doReturn(AdServicesApiConsent.REVOKED).when(mConsentManagerMock).getConsent(any());
// 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);
mAdSelectionRunner =
new AdSelectionRunner(
mContext,
mCustomAudienceDao,
mAdSelectionEntryDao,
mExecutorService,
mConsentManagerMock,
mMockAdsScoreGenerator,
mMockAdBidGenerator,
mMockAdSelectionIdGenerator,
mClock,
mAdServicesLoggerSpy,
mAppImportanceFilter,
mFlags,
CALLER_UID);
assertFalse(mAdSelectionEntryDao.doesAdSelectionIdExist(AD_SELECTION_ID));
AdSelectionTestCallback resultsCallback =
invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
verify(mMockAdBidGenerator, never()).runAdBiddingPerCA(any(), 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(mAdServicesLoggerSpy)
.logFledgeApiCallStats(
AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
AdServicesStatusUtils.STATUS_USER_CONSENT_REVOKED);
verify(mAdServicesLoggerSpy)
.logApiCallStats(
aCallStatForFledgeApiWithStatus(
AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
AdServicesStatusUtils.STATUS_USER_CONSENT_REVOKED));
}
@Test
public void testRunAdSelectionMissingBuyerSignals() throws AdServicesException {
when(mClock.instant()).thenReturn(Clock.systemUTC().instant());
doReturn(mFlags).when(FlagsFactory::getFlags);
doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent(any());
// 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,
adSelectionConfig);
doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer2)))
.when(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer2,
adSelectionConfig.getAdSelectionSignals(),
AdSelectionSignals.EMPTY,
AdSelectionSignals.EMPTY,
adSelectionConfig);
// 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()
.getBiddingLogicUrl())
.setContextualSignals("{}")
.setCallerPackageName(MY_APP_PACKAGE_NAME)
.build();
when(mMockAdSelectionIdGenerator.generateId()).thenReturn(AD_SELECTION_ID);
mAdSelectionRunner =
new AdSelectionRunner(
mContext,
mCustomAudienceDao,
mAdSelectionEntryDao,
mExecutorService,
mConsentManagerMock,
mMockAdsScoreGenerator,
mMockAdBidGenerator,
mMockAdSelectionIdGenerator,
mClock,
mAdServicesLoggerSpy,
mAppImportanceFilter,
mFlags,
CALLER_UID);
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,
adSelectionConfig);
verify(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer2,
adSelectionConfig.getAdSelectionSignals(),
AdSelectionSignals.EMPTY,
AdSelectionSignals.EMPTY,
adSelectionConfig);
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(mAdServicesLoggerSpy)
.logFledgeApiCallStats(
AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
AdServicesStatusUtils.STATUS_SUCCESS);
verify(mAdServicesLoggerSpy)
.logApiCallStats(
aCallStatForFledgeApiWithStatus(
AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
AdServicesStatusUtils.STATUS_SUCCESS));
}
@Test
public void testRunAdSelectionNoCAs() {
when(mClock.instant()).thenReturn(Clock.systemUTC().instant());
doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent(any());
// 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);
mAdSelectionRunner =
new AdSelectionRunner(
mContext,
mCustomAudienceDao,
mAdSelectionEntryDao,
mExecutorService,
mConsentManagerMock,
mMockAdsScoreGenerator,
mMockAdBidGenerator,
mMockAdSelectionIdGenerator,
mClock,
mAdServicesLoggerSpy,
mAppImportanceFilter,
mFlags,
CALLER_UID);
AdSelectionTestCallback resultsCallback =
invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
assertFalse(resultsCallback.mIsSuccess);
verifyErrorMessageIsCorrect(
resultsCallback.mFledgeErrorResponse.getErrorMessage(), ERROR_NO_CA_AVAILABLE);
verify(mAdServicesLoggerSpy)
.logFledgeApiCallStats(
AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
AdServicesStatusUtils.STATUS_INTERNAL_ERROR);
verify(mAdServicesLoggerSpy)
.logApiCallStats(
aCallStatForFledgeApiWithStatus(
AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
AdServicesStatusUtils.STATUS_INTERNAL_ERROR));
}
@Test
public void testRunAdSelectionCallerNotInForeground_fails() {
doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent(any());
// 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 AdSelectionRunner(
mContext,
mCustomAudienceDao,
mAdSelectionEntryDao,
mExecutorService,
mConsentManagerMock,
mMockAdsScoreGenerator,
mMockAdBidGenerator,
mMockAdSelectionIdGenerator,
mClock,
mAdServicesLoggerSpy,
mAppImportanceFilter,
mFlags,
CALLER_UID);
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(any());
mFlags =
new Flags() {
@Override
public boolean getEnforceForegroundStatusForFledgeRunAdSelection() {
return false;
}
};
// Creating ad selection config for happy case with all the buyers in place
AdSelectionConfig adSelectionConfig = mAdSelectionConfigBuilder.build();
mAdSelectionRunner =
new AdSelectionRunner(
mContext,
mCustomAudienceDao,
mAdSelectionEntryDao,
mExecutorService,
mConsentManagerMock,
mMockAdsScoreGenerator,
mMockAdBidGenerator,
mMockAdSelectionIdGenerator,
mClock,
mAdServicesLoggerSpy,
mAppImportanceFilter,
mFlags,
CALLER_UID);
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(any());
// 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,
adSelectionConfig);
doReturn(FluentFuture.from(Futures.immediateFuture(null)))
.when(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer2,
adSelectionConfig.getAdSelectionSignals(),
adSelectionConfig
.getPerBuyerSignals()
.get(mDBCustomAudienceForBuyer2.getBuyer()),
AdSelectionSignals.EMPTY,
adSelectionConfig);
// 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);
mAdSelectionRunner =
new AdSelectionRunner(
mContext,
mCustomAudienceDao,
mAdSelectionEntryDao,
mExecutorService,
mConsentManagerMock,
mMockAdsScoreGenerator,
mMockAdBidGenerator,
mMockAdSelectionIdGenerator,
mClock,
mAdServicesLoggerSpy,
mAppImportanceFilter,
mFlags,
CALLER_UID);
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()
.getBiddingLogicUrl())
.setContextualSignals("{}")
.setCallerPackageName(MY_APP_PACKAGE_NAME)
.build();
verify(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer1,
adSelectionConfig.getAdSelectionSignals(),
adSelectionConfig
.getPerBuyerSignals()
.get(mDBCustomAudienceForBuyer1.getBuyer()),
AdSelectionSignals.EMPTY,
adSelectionConfig);
verify(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer2,
adSelectionConfig.getAdSelectionSignals(),
adSelectionConfig
.getPerBuyerSignals()
.get(mDBCustomAudienceForBuyer2.getBuyer()),
AdSelectionSignals.EMPTY,
adSelectionConfig);
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(mAdServicesLoggerSpy)
.logFledgeApiCallStats(
AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
AdServicesStatusUtils.STATUS_SUCCESS);
verify(mAdServicesLoggerSpy)
.logApiCallStats(
aCallStatForFledgeApiWithStatus(
AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
AdServicesStatusUtils.STATUS_SUCCESS));
}
@Test
public void testRunAdSelectionBiddingFailure() {
when(mClock.instant()).thenReturn(Clock.systemUTC().instant());
doReturn(mFlags).when(FlagsFactory::getFlags);
doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent(any());
// 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,
adSelectionConfig);
doReturn(FluentFuture.from(Futures.immediateFuture(null)))
.when(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer2,
adSelectionConfig.getAdSelectionSignals(),
adSelectionConfig
.getPerBuyerSignals()
.get(mDBCustomAudienceForBuyer2.getBuyer()),
AdSelectionSignals.EMPTY,
adSelectionConfig);
// If the result of bidding is empty, then we should not even attempt to run scoring
verifyZeroInteractions(mMockAdsScoreGenerator);
mAdSelectionRunner =
new AdSelectionRunner(
mContext,
mCustomAudienceDao,
mAdSelectionEntryDao,
mExecutorService,
mConsentManagerMock,
mMockAdsScoreGenerator,
mMockAdBidGenerator,
mMockAdSelectionIdGenerator,
mClock,
mAdServicesLoggerSpy,
mAppImportanceFilter,
mFlags,
CALLER_UID);
AdSelectionTestCallback resultsCallback =
invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
verify(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer1,
adSelectionConfig.getAdSelectionSignals(),
adSelectionConfig
.getPerBuyerSignals()
.get(mDBCustomAudienceForBuyer1.getBuyer()),
AdSelectionSignals.EMPTY,
adSelectionConfig);
verify(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer2,
adSelectionConfig.getAdSelectionSignals(),
adSelectionConfig
.getPerBuyerSignals()
.get(mDBCustomAudienceForBuyer2.getBuyer()),
AdSelectionSignals.EMPTY,
adSelectionConfig);
assertFalse(resultsCallback.mIsSuccess);
verifyErrorMessageIsCorrect(
resultsCallback.mFledgeErrorResponse.getErrorMessage(),
ERROR_NO_VALID_BIDS_FOR_SCORING);
verify(mAdServicesLoggerSpy)
.logFledgeApiCallStats(
AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
AdServicesStatusUtils.STATUS_INTERNAL_ERROR);
verify(mAdServicesLoggerSpy)
.logApiCallStats(
aCallStatForFledgeApiWithStatus(
AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
AdServicesStatusUtils.STATUS_INTERNAL_ERROR));
}
@Test
public void testRunAdSelectionScoringFailure() throws AdServicesException {
when(mClock.instant()).thenReturn(Clock.systemUTC().instant());
doReturn(mFlags).when(FlagsFactory::getFlags);
doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent(any());
// 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,
adSelectionConfig);
doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer2)))
.when(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer2,
adSelectionConfig.getAdSelectionSignals(),
adSelectionConfig
.getPerBuyerSignals()
.get(mDBCustomAudienceForBuyer2.getBuyer()),
AdSelectionSignals.EMPTY,
adSelectionConfig);
// 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))));
mAdSelectionRunner =
new AdSelectionRunner(
mContext,
mCustomAudienceDao,
mAdSelectionEntryDao,
mExecutorService,
mConsentManagerMock,
mMockAdsScoreGenerator,
mMockAdBidGenerator,
mMockAdSelectionIdGenerator,
mClock,
mAdServicesLoggerSpy,
mAppImportanceFilter,
mFlags,
CALLER_UID);
AdSelectionTestCallback resultsCallback =
invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
verify(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer1,
adSelectionConfig.getAdSelectionSignals(),
adSelectionConfig
.getPerBuyerSignals()
.get(mDBCustomAudienceForBuyer1.getBuyer()),
AdSelectionSignals.EMPTY,
adSelectionConfig);
verify(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer2,
adSelectionConfig.getAdSelectionSignals(),
adSelectionConfig
.getPerBuyerSignals()
.get(mDBCustomAudienceForBuyer2.getBuyer()),
AdSelectionSignals.EMPTY,
adSelectionConfig);
verify(mMockAdsScoreGenerator).runAdScoring(mAdBiddingOutcomeList, adSelectionConfig);
assertFalse(resultsCallback.mIsSuccess);
verifyErrorMessageIsCorrect(
resultsCallback.mFledgeErrorResponse.getErrorMessage(), ERROR_NO_WINNING_AD_FOUND);
verify(mAdServicesLoggerSpy)
.logFledgeApiCallStats(
AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
AdServicesStatusUtils.STATUS_INTERNAL_ERROR);
verify(mAdServicesLoggerSpy)
.logApiCallStats(
aCallStatForFledgeApiWithStatus(
AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
AdServicesStatusUtils.STATUS_INTERNAL_ERROR));
}
@Test
public void testRunAdSelectionNegativeScoring() throws AdServicesException {
when(mClock.instant()).thenReturn(Clock.systemUTC().instant());
doReturn(mFlags).when(FlagsFactory::getFlags);
doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent(any());
// 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,
adSelectionConfig);
doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer2)))
.when(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer2,
adSelectionConfig.getAdSelectionSignals(),
adSelectionConfig
.getPerBuyerSignals()
.get(mDBCustomAudienceForBuyer2.getBuyer()),
AdSelectionSignals.EMPTY,
adSelectionConfig);
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))));
mAdSelectionRunner =
new AdSelectionRunner(
mContext,
mCustomAudienceDao,
mAdSelectionEntryDao,
mExecutorService,
mConsentManagerMock,
mMockAdsScoreGenerator,
mMockAdBidGenerator,
mMockAdSelectionIdGenerator,
mClock,
mAdServicesLoggerSpy,
mAppImportanceFilter,
mFlags,
CALLER_UID);
AdSelectionTestCallback resultsCallback =
invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
verify(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer1,
adSelectionConfig.getAdSelectionSignals(),
adSelectionConfig
.getPerBuyerSignals()
.get(mDBCustomAudienceForBuyer1.getBuyer()),
AdSelectionSignals.EMPTY,
adSelectionConfig);
verify(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer2,
adSelectionConfig.getAdSelectionSignals(),
adSelectionConfig
.getPerBuyerSignals()
.get(mDBCustomAudienceForBuyer2.getBuyer()),
AdSelectionSignals.EMPTY,
adSelectionConfig);
verify(mMockAdsScoreGenerator).runAdScoring(mAdBiddingOutcomeList, adSelectionConfig);
assertFalse(resultsCallback.mIsSuccess);
verifyErrorMessageIsCorrect(
resultsCallback.mFledgeErrorResponse.getErrorMessage(), ERROR_NO_WINNING_AD_FOUND);
verify(mAdServicesLoggerSpy)
.logFledgeApiCallStats(
AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
AdServicesStatusUtils.STATUS_INTERNAL_ERROR);
verify(mAdServicesLoggerSpy)
.logApiCallStats(
aCallStatForFledgeApiWithStatus(
AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
AdServicesStatusUtils.STATUS_INTERNAL_ERROR));
}
@Test
public void testRunAdSelectionPartialNegativeScoring() throws AdServicesException {
when(mClock.instant()).thenReturn(Clock.systemUTC().instant());
doReturn(mFlags).when(FlagsFactory::getFlags);
doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent(any());
// 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,
adSelectionConfig);
doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer2)))
.when(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer2,
adSelectionConfig.getAdSelectionSignals(),
adSelectionConfig
.getPerBuyerSignals()
.get(mDBCustomAudienceForBuyer2.getBuyer()),
AdSelectionSignals.EMPTY,
adSelectionConfig);
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);
mAdSelectionRunner =
new AdSelectionRunner(
mContext,
mCustomAudienceDao,
mAdSelectionEntryDao,
mExecutorService,
mConsentManagerMock,
mMockAdsScoreGenerator,
mMockAdBidGenerator,
mMockAdSelectionIdGenerator,
mClock,
mAdServicesLoggerSpy,
mAppImportanceFilter,
mFlags,
CALLER_UID);
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,
adSelectionConfig);
verify(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer2,
adSelectionConfig.getAdSelectionSignals(),
adSelectionConfig
.getPerBuyerSignals()
.get(mDBCustomAudienceForBuyer2.getBuyer()),
AdSelectionSignals.EMPTY,
adSelectionConfig);
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()
.getBiddingLogicUrl())
.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(mAdServicesLoggerSpy)
.logFledgeApiCallStats(
AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
AdServicesStatusUtils.STATUS_SUCCESS);
verify(mAdServicesLoggerSpy)
.logApiCallStats(
aCallStatForFledgeApiWithStatus(
AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
AdServicesStatusUtils.STATUS_SUCCESS));
}
@Test
public void testRunAdSelectionScoringException() throws AdServicesException {
when(mClock.instant()).thenReturn(Clock.systemUTC().instant());
doReturn(mFlags).when(FlagsFactory::getFlags);
doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent(any());
// 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,
adSelectionConfig);
doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer2)))
.when(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer2,
adSelectionConfig.getAdSelectionSignals(),
adSelectionConfig
.getPerBuyerSignals()
.get(mDBCustomAudienceForBuyer2.getBuyer()),
AdSelectionSignals.EMPTY,
adSelectionConfig);
// 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));
mAdSelectionRunner =
new AdSelectionRunner(
mContext,
mCustomAudienceDao,
mAdSelectionEntryDao,
mExecutorService,
mConsentManagerMock,
mMockAdsScoreGenerator,
mMockAdBidGenerator,
mMockAdSelectionIdGenerator,
mClock,
mAdServicesLoggerSpy,
mAppImportanceFilter,
mFlags,
CALLER_UID);
AdSelectionTestCallback resultsCallback =
invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
verify(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer1,
adSelectionConfig.getAdSelectionSignals(),
adSelectionConfig
.getPerBuyerSignals()
.get(mDBCustomAudienceForBuyer1.getBuyer()),
AdSelectionSignals.EMPTY,
adSelectionConfig);
verify(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer2,
adSelectionConfig.getAdSelectionSignals(),
adSelectionConfig
.getPerBuyerSignals()
.get(mDBCustomAudienceForBuyer2.getBuyer()),
AdSelectionSignals.EMPTY,
adSelectionConfig);
assertFalse(resultsCallback.mIsSuccess);
verifyErrorMessageIsCorrect(
resultsCallback.mFledgeErrorResponse.getErrorMessage(), ERROR_INVALID_JSON);
verify(mAdServicesLoggerSpy)
.logFledgeApiCallStats(
AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
AdServicesStatusUtils.STATUS_INTERNAL_ERROR);
verify(mAdServicesLoggerSpy)
.logApiCallStats(
aCallStatForFledgeApiWithStatus(
AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
AdServicesStatusUtils.STATUS_INTERNAL_ERROR));
}
@Test
public void testRunAdSelectionOrchestrationTimesOut() throws AdServicesException {
when(mClock.instant()).thenReturn(Clock.systemUTC().instant());
doReturn(mFlags).when(FlagsFactory::getFlags);
doReturn(AdServicesApiConsent.GIVEN).when(mConsentManagerMock).getConsent(any());
Flags flagsWithSmallerLimits =
new Flags() {
@Override
public long getAdSelectionOverallTimeoutMs() {
return 100;
}
};
// 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,
adSelectionConfig);
doReturn(FluentFuture.from(Futures.immediateFuture(mAdBiddingOutcomeForBuyer2)))
.when(mMockAdBidGenerator)
.runAdBiddingPerCA(
mDBCustomAudienceForBuyer2,
adSelectionConfig.getAdSelectionSignals(),
adSelectionConfig
.getPerBuyerSignals()
.get(mDBCustomAudienceForBuyer2.getBuyer()),
AdSelectionSignals.EMPTY,
adSelectionConfig);
// 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)));
mAdSelectionRunner =
new AdSelectionRunner(
mContext,
mCustomAudienceDao,
mAdSelectionEntryDao,
mExecutorService,
mConsentManagerMock,
mMockAdsScoreGenerator,
mMockAdBidGenerator,
mMockAdSelectionIdGenerator,
mClock,
mAdServicesLoggerSpy,
mAppImportanceFilter,
flagsWithSmallerLimits,
CALLER_UID);
assertFalse(mAdSelectionEntryDao.doesAdSelectionIdExist(AD_SELECTION_ID));
AdSelectionTestCallback resultsCallback =
invokeRunAdSelection(mAdSelectionRunner, adSelectionConfig, MY_APP_PACKAGE_NAME);
assertFalse(resultsCallback.mIsSuccess);
verifyErrorMessageIsCorrect(
resultsCallback.mFledgeErrorResponse.getErrorMessage(), AD_SELECTION_TIMED_OUT);
}
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(mAdServicesLoggerSpy).logApiCallStats(any());
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));
mExecutorService.shutdown();
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();
}
}
}