blob: 91c52a3e4785845841ac8c416236e69bad593be7 [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 android.adservices.adselection.AdSelectionFromOutcomesConfigFixture.SAMPLE_SELLER;
import static android.adservices.common.AdServicesStatusUtils.STATUS_INVALID_ARGUMENT;
import static android.adservices.common.AdServicesStatusUtils.STATUS_TIMEOUT;
import static android.adservices.common.AdServicesStatusUtils.STATUS_USER_CONSENT_REVOKED;
import static com.android.adservices.service.PhFlagsFixture.EXTENDED_FLEDGE_AD_SELECTION_FROM_OUTCOMES_OVERALL_TIMEOUT_MS;
import static com.android.adservices.service.PhFlagsFixture.EXTENDED_FLEDGE_AD_SELECTION_SELECTING_OUTCOME_TIMEOUT_MS;
import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
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.verify;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.never;
import android.adservices.adselection.AdSelectionCallback;
import android.adservices.adselection.AdSelectionFromOutcomesConfig;
import android.adservices.adselection.AdSelectionFromOutcomesConfigFixture;
import android.adservices.adselection.AdSelectionFromOutcomesInput;
import android.adservices.adselection.AdSelectionResponse;
import android.adservices.adselection.CustomAudienceSignalsFixture;
import android.adservices.common.CommonFixture;
import android.adservices.common.FledgeErrorResponse;
import android.annotation.NonNull;
import android.content.Context;
import android.net.Uri;
import android.os.Process;
import android.os.RemoteException;
import androidx.room.Room;
import androidx.test.core.app.ApplicationProvider;
import com.android.adservices.concurrency.AdServicesExecutors;
import com.android.adservices.data.adselection.AdSelectionDatabase;
import com.android.adservices.data.adselection.AdSelectionEntryDao;
import com.android.adservices.data.adselection.CustomAudienceSignals;
import com.android.adservices.data.adselection.DBAdSelection;
import com.android.adservices.service.Flags;
import com.android.adservices.service.common.AdSelectionServiceFilter;
import com.android.adservices.service.common.Throttler;
import com.android.adservices.service.consent.ConsentManager;
import com.android.adservices.service.exception.FilterException;
import com.android.adservices.service.stats.AdServicesLogger;
import com.android.adservices.service.stats.AdServicesLoggerImpl;
import com.android.adservices.service.stats.AdServicesStatsLog;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoSession;
import org.mockito.quality.Strictness;
import java.time.Instant;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
public class OutcomeSelectionRunnerTest {
private static final int CALLER_UID = Process.myUid();
private static final String MY_APP_PACKAGE_NAME = CommonFixture.TEST_PACKAGE_NAME;
private static final String ANOTHER_CALLER_PACKAGE_NAME = "another.caller.package";
private static final Uri RENDER_URI_1 = Uri.parse("https://www.domain.com/advert1/");
private static final Uri RENDER_URI_2 = Uri.parse("https://www.domain.com/advert2/");
private static final Uri RENDER_URI_3 = Uri.parse("https://www.domain.com/advert3/");
private static final long AD_SELECTION_ID_1 = 1;
private static final long AD_SELECTION_ID_2 = 2;
private static final long AD_SELECTION_ID_3 = 3;
private static final double BID_1 = 10.0;
private static final double BID_2 = 20.0;
private static final double BID_3 = 30.0;
private static final AdSelectionIdWithBidAndRenderUri AD_SELECTION_WITH_BID_1 =
AdSelectionIdWithBidAndRenderUri.builder()
.setAdSelectionId(AD_SELECTION_ID_1)
.setBid(BID_1)
.setRenderUri(RENDER_URI_1)
.build();
private static final AdSelectionIdWithBidAndRenderUri AD_SELECTION_WITH_BID_2 =
AdSelectionIdWithBidAndRenderUri.builder()
.setAdSelectionId(AD_SELECTION_ID_2)
.setBid(BID_2)
.setRenderUri(RENDER_URI_2)
.build();
private static final AdSelectionIdWithBidAndRenderUri AD_SELECTION_WITH_BID_3 =
AdSelectionIdWithBidAndRenderUri.builder()
.setAdSelectionId(AD_SELECTION_ID_3)
.setBid(BID_3)
.setRenderUri(RENDER_URI_3)
.build();
private final Context mContext = ApplicationProvider.getApplicationContext();
private AdSelectionEntryDao mAdSelectionEntryDao;
@Mock private AdOutcomeSelector mAdOutcomeSelectorMock;
private OutcomeSelectionRunner mOutcomeSelectionRunner;
private Flags mFlags = new OutcomeSelectionRunnerTestFlags();
private final AdServicesLogger mAdServicesLoggerMock =
ExtendedMockito.mock(AdServicesLoggerImpl.class);
private MockitoSession mStaticMockSession = null;
private ListeningExecutorService mBlockingExecutorService;
@Mock private AdSelectionServiceFilter mAdSelectionServiceFilter;
@Before
public void setup() {
mBlockingExecutorService = AdServicesExecutors.getBlockingExecutor();
mStaticMockSession =
ExtendedMockito.mockitoSession()
// .spyStatic(JSScriptEngine.class)
// mAdServicesLoggerMock is not referenced in many tests
.strictness(Strictness.LENIENT)
.initMocks(this)
.startMocking();
mAdSelectionEntryDao =
Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
AdSelectionDatabase.class)
.build()
.adSelectionEntryDao();
mOutcomeSelectionRunner =
new OutcomeSelectionRunner(
CALLER_UID,
mAdOutcomeSelectorMock,
mAdSelectionEntryDao,
mBlockingExecutorService,
AdServicesExecutors.getLightWeightExecutor(),
AdServicesExecutors.getScheduler(),
mAdServicesLoggerMock,
mContext,
mFlags,
mAdSelectionServiceFilter);
doNothing()
.when(mAdSelectionServiceFilter)
.filterRequest(
SAMPLE_SELLER,
MY_APP_PACKAGE_NAME,
true,
true,
CALLER_UID,
AdServicesStatsLog.AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN,
Throttler.ApiKey.FLEDGE_API_SELECT_ADS);
}
@After
public void tearDown() {
if (mStaticMockSession != null) {
mStaticMockSession.finishMocking();
}
}
@Test
public void testRunOutcomeSelectionInvalidAdSelectionConfigFromOutcomes() {
List<AdSelectionIdWithBidAndRenderUri> AdSelectionIdWithBidAndRenderUris =
List.of(AD_SELECTION_WITH_BID_1, AD_SELECTION_WITH_BID_2, AD_SELECTION_WITH_BID_3);
persistAdSelectionEntry(AdSelectionIdWithBidAndRenderUris.get(0), MY_APP_PACKAGE_NAME);
// Not persisting index 1
// Persisting index 2 with a different package name
persistAdSelectionEntry(
AdSelectionIdWithBidAndRenderUris.get(2), ANOTHER_CALLER_PACKAGE_NAME);
List<Long> adOutcomesConfigParam =
AdSelectionIdWithBidAndRenderUris.stream()
.map(AdSelectionIdWithBidAndRenderUri::getAdSelectionId)
.collect(Collectors.toList());
AdSelectionFromOutcomesConfig config =
AdSelectionFromOutcomesConfigFixture.anAdSelectionFromOutcomesConfig(
adOutcomesConfigParam);
AdSelectionTestCallback resultsCallback =
invokeRunAdSelectionFromOutcomes(
mOutcomeSelectionRunner, config, MY_APP_PACKAGE_NAME);
verify(mAdOutcomeSelectorMock, never()).runAdOutcomeSelector(any(), any());
assertFalse(resultsCallback.mIsSuccess);
assertEquals(STATUS_INVALID_ARGUMENT, resultsCallback.mFledgeErrorResponse.getStatusCode());
verify(mAdServicesLoggerMock)
.logFledgeApiCallStats(
eq(AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN),
eq(STATUS_INVALID_ARGUMENT),
anyInt());
}
@Test
public void testRunOutcomeSelectionRevokedUserConsentEmptyResult() {
doThrow(new FilterException(new ConsentManager.RevokedConsentException()))
.when(mAdSelectionServiceFilter)
.filterRequest(
SAMPLE_SELLER,
MY_APP_PACKAGE_NAME,
true,
true,
CALLER_UID,
AdServicesStatsLog.AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN,
Throttler.ApiKey.FLEDGE_API_SELECT_ADS);
List<AdSelectionIdWithBidAndRenderUri> adSelectionIdWithBidAndRenderUris =
List.of(AD_SELECTION_WITH_BID_1, AD_SELECTION_WITH_BID_2, AD_SELECTION_WITH_BID_3);
for (AdSelectionIdWithBidAndRenderUri idWithBid : adSelectionIdWithBidAndRenderUris) {
persistAdSelectionEntry(idWithBid, MY_APP_PACKAGE_NAME);
}
List<Long> adOutcomesConfigParam =
adSelectionIdWithBidAndRenderUris.stream()
.map(AdSelectionIdWithBidAndRenderUri::getAdSelectionId)
.collect(Collectors.toList());
AdSelectionFromOutcomesConfig config =
AdSelectionFromOutcomesConfigFixture.anAdSelectionFromOutcomesConfig(
adOutcomesConfigParam);
AdSelectionTestCallback resultsCallback =
invokeRunAdSelectionFromOutcomes(
mOutcomeSelectionRunner, config, MY_APP_PACKAGE_NAME);
verify(mAdOutcomeSelectorMock, never()).runAdOutcomeSelector(any(), any());
assertTrue(resultsCallback.mIsSuccess);
assertNull(resultsCallback.mAdSelectionResponse);
// Confirm a duplicate log entry does not exist.
// AdSelectionServiceFilter ensures the failing assertion is logged internally.
verify(mAdServicesLoggerMock, never())
.logFledgeApiCallStats(
eq(AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN),
eq(STATUS_USER_CONSENT_REVOKED),
anyInt());
}
@Test
public void testRunOutcomeSelectionOrchestrationTimeoutFailure() {
mFlags =
new Flags() {
@Override
public long getAdSelectionSelectingOutcomeTimeoutMs() {
return 300;
}
@Override
public boolean getDisableFledgeEnrollmentCheck() {
return true;
}
@Override
public long getAdSelectionFromOutcomesOverallTimeoutMs() {
return 100;
}
};
List<AdSelectionIdWithBidAndRenderUri> adSelectionIdWithBidAndRenderUris =
List.of(AD_SELECTION_WITH_BID_1, AD_SELECTION_WITH_BID_2, AD_SELECTION_WITH_BID_3);
for (AdSelectionIdWithBidAndRenderUri idWithBid : adSelectionIdWithBidAndRenderUris) {
persistAdSelectionEntry(idWithBid, MY_APP_PACKAGE_NAME);
}
List<Long> adOutcomesConfigParam =
adSelectionIdWithBidAndRenderUris.stream()
.map(AdSelectionIdWithBidAndRenderUri::getAdSelectionId)
.collect(Collectors.toList());
AdSelectionFromOutcomesConfig config =
AdSelectionFromOutcomesConfigFixture.anAdSelectionFromOutcomesConfig(
adOutcomesConfigParam);
GenericListMatcher matcher = new GenericListMatcher(adSelectionIdWithBidAndRenderUris);
doAnswer((ignored) -> getSelectedOutcomeWithDelay(AD_SELECTION_ID_1, mFlags))
.when(mAdOutcomeSelectorMock)
.runAdOutcomeSelector(argThat(matcher), eq(config));
OutcomeSelectionRunner outcomeSelectionRunner =
new OutcomeSelectionRunner(
CALLER_UID,
mAdOutcomeSelectorMock,
mAdSelectionEntryDao,
mBlockingExecutorService,
AdServicesExecutors.getLightWeightExecutor(),
AdServicesExecutors.getScheduler(),
mAdServicesLoggerMock,
mContext,
mFlags,
mAdSelectionServiceFilter);
AdSelectionTestCallback resultsCallback =
invokeRunAdSelectionFromOutcomes(
outcomeSelectionRunner, config, MY_APP_PACKAGE_NAME);
verify(mAdOutcomeSelectorMock, Mockito.times(1)).runAdOutcomeSelector(any(), any());
assertFalse(resultsCallback.mIsSuccess);
assertNotNull(resultsCallback.mFledgeErrorResponse);
assertEquals(STATUS_TIMEOUT, resultsCallback.mFledgeErrorResponse.getStatusCode());
verify(mAdServicesLoggerMock)
.logFledgeApiCallStats(
eq(AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN),
eq(STATUS_TIMEOUT),
anyInt());
}
private void persistAdSelectionEntry(
AdSelectionIdWithBidAndRenderUri idWithBidAndRenderUri, String callerPackageName) {
final Uri biddingLogicUri1 = Uri.parse("https://www.domain.com/logic/1");
final Instant activationTime = Instant.now();
final String contextualSignals = "contextual_signals";
final CustomAudienceSignals customAudienceSignals =
CustomAudienceSignalsFixture.aCustomAudienceSignals();
final DBAdSelection dbAdSelectionEntry =
new DBAdSelection.Builder()
.setAdSelectionId(idWithBidAndRenderUri.getAdSelectionId())
.setCustomAudienceSignals(customAudienceSignals)
.setContextualSignals(contextualSignals)
.setBiddingLogicUri(biddingLogicUri1)
.setWinningAdRenderUri(idWithBidAndRenderUri.getRenderUri())
.setWinningAdBid(idWithBidAndRenderUri.getBid())
.setCreationTimestamp(activationTime)
.setCallerPackageName(callerPackageName)
.build();
mAdSelectionEntryDao.persistAdSelection(dbAdSelectionEntry);
}
private OutcomeSelectionRunnerTest.AdSelectionTestCallback invokeRunAdSelectionFromOutcomes(
OutcomeSelectionRunner outcomeSelectionRunner,
AdSelectionFromOutcomesConfig config,
String callerPackageName) {
// Counted down in the callback
CountDownLatch countDownLatch = new CountDownLatch(1);
OutcomeSelectionRunnerTest.AdSelectionTestCallback adSelectionTestCallback =
new OutcomeSelectionRunnerTest.AdSelectionTestCallback(countDownLatch);
AdSelectionFromOutcomesInput input =
new AdSelectionFromOutcomesInput.Builder()
.setAdSelectionFromOutcomesConfig(config)
.setCallerPackageName(callerPackageName)
.build();
outcomeSelectionRunner.runOutcomeSelection(input, adSelectionTestCallback);
try {
adSelectionTestCallback.mCountDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
return adSelectionTestCallback;
}
private ListenableFuture<Long> getSelectedOutcomeWithDelay(
Long outcomeId, @NonNull Flags flags) {
return mBlockingExecutorService.submit(
() -> {
Thread.sleep(2 * flags.getAdSelectionFromOutcomesOverallTimeoutMs());
return outcomeId;
});
}
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();
}
}
static class GenericListMatcher
implements ArgumentMatcher<List<AdSelectionIdWithBidAndRenderUri>> {
private final List<AdSelectionIdWithBidAndRenderUri> mTruth;
GenericListMatcher(List<AdSelectionIdWithBidAndRenderUri> truth) {
this.mTruth = truth;
}
@Override
public boolean matches(List<AdSelectionIdWithBidAndRenderUri> argument) {
return mTruth.size() == argument.size()
&& new HashSet<>(mTruth).equals(new HashSet<>(argument));
}
}
private static class OutcomeSelectionRunnerTestFlags implements Flags {
@Override
public long getAdSelectionSelectingOutcomeTimeoutMs() {
return EXTENDED_FLEDGE_AD_SELECTION_SELECTING_OUTCOME_TIMEOUT_MS;
}
@Override
public long getAdSelectionFromOutcomesOverallTimeoutMs() {
return EXTENDED_FLEDGE_AD_SELECTION_FROM_OUTCOMES_OVERALL_TIMEOUT_MS;
}
}
}