1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.adservices.debuggablects; 18 19 import static com.android.adservices.service.FlagsConstants.KEY_FLEDGE_HTTP_CACHE_ENABLE; 20 import static com.android.adservices.service.FlagsConstants.KEY_FLEDGE_ON_DEVICE_AUCTION_SHOULD_USE_UNIFIED_TABLES; 21 import static com.android.adservices.service.FlagsConstants.KEY_FLEDGE_REGISTER_AD_BEACON_ENABLED; 22 23 import static com.google.common.truth.Truth.assertThat; 24 25 import static org.junit.Assert.assertThrows; 26 import static org.junit.Assume.assumeTrue; 27 28 import android.adservices.adid.AdId; 29 import android.adservices.adid.AdIdCompatibleManager; 30 import android.adservices.adselection.AdSelectionConfig; 31 import android.adservices.adselection.AdSelectionFromOutcomesConfig; 32 import android.adservices.adselection.AdSelectionOutcome; 33 import android.adservices.common.AdTechIdentifier; 34 import android.adservices.customaudience.CustomAudience; 35 import android.adservices.customaudience.FetchAndJoinCustomAudienceRequest; 36 import android.adservices.utils.FledgeScenarioTest; 37 import android.adservices.utils.ScenarioDispatcher; 38 import android.adservices.utils.ScenarioDispatcherFactory; 39 import android.adservices.utils.Scenarios; 40 import android.net.Uri; 41 import android.util.Log; 42 43 import com.android.adservices.common.AdServicesOutcomeReceiverForTests; 44 import com.android.adservices.shared.testing.annotations.SetFlagDisabled; 45 import com.android.adservices.shared.testing.annotations.SetFlagEnabled; 46 import com.android.compatibility.common.util.ShellUtils; 47 48 import com.google.common.util.concurrent.MoreExecutors; 49 50 import org.junit.Test; 51 52 import java.time.Instant; 53 import java.time.temporal.ChronoUnit; 54 import java.util.List; 55 import java.util.Objects; 56 import java.util.concurrent.ExecutionException; 57 import java.util.concurrent.TimeUnit; 58 import java.util.concurrent.TimeoutException; 59 60 @SetFlagDisabled(KEY_FLEDGE_HTTP_CACHE_ENABLE) 61 public class AdSelectionTest extends FledgeScenarioTest { 62 63 /** 64 * End-to-end test for ad selection. 65 * 66 * <p>Covers the following Remarketing CUJs: 67 * 68 * <ul> 69 * <li><b>001</b>: A buyer can provide bidding logic using JS 70 * <li><b>002</b>: A seller can provide scoring logic using JS 71 * <li><b>035</b>: A buyer can provide the trusted signals to be used during ad selection 72 * </ul> 73 */ 74 @Test testAdSelection_withBiddingAndScoringLogic_happyPath()75 public void testAdSelection_withBiddingAndScoringLogic_happyPath() throws Exception { 76 ScenarioDispatcher dispatcher = 77 setupDispatcher( 78 ScenarioDispatcherFactory.createFromScenarioFileWithRandomPrefix( 79 "scenarios/remarketing-cuj-default.json")); 80 AdSelectionConfig adSelectionConfig = 81 makeAdSelectionConfig(dispatcher.getBaseAddressWithPrefix()); 82 83 try { 84 joinCustomAudience(SHIRTS_CA); 85 AdSelectionOutcome result = doSelectAds(adSelectionConfig); 86 assertThat(result.hasOutcome()).isTrue(); 87 } finally { 88 leaveCustomAudience(SHIRTS_CA); 89 } 90 91 assertThat(dispatcher.getCalledPaths()) 92 .containsAtLeastElementsIn(dispatcher.getVerifyCalledPaths()); 93 } 94 95 /** 96 * {@link AdSelectionTest#testAdSelection_withBiddingAndScoringLogic_happyPath} with flag {@link 97 * KEY_FLEDGE_ON_DEVICE_AUCTION_SHOULD_USE_UNIFIED_TABLES} turned on. 98 */ 99 @Test 100 @SetFlagEnabled(KEY_FLEDGE_ON_DEVICE_AUCTION_SHOULD_USE_UNIFIED_TABLES) testAdSelection_withUnifiedTable_withBiddingAndScoringLogic_happyPath()101 public void testAdSelection_withUnifiedTable_withBiddingAndScoringLogic_happyPath() 102 throws Exception { 103 testAdSelection_withAdCostInUrl_happyPath(); 104 } 105 106 /** 107 * Test for ad selection with V3 bidding logic. 108 * 109 * <p>Covers the following Remarketing CUJs: 110 * 111 * <ul> 112 * <li><b>119</b>: A ad selection can be run with V3 bidding logic without override 113 * </ul> 114 */ 115 @Test testAdSelection_withBiddingLogicV3_happyPath()116 public void testAdSelection_withBiddingLogicV3_happyPath() throws Exception { 117 ScenarioDispatcher dispatcher = 118 setupDispatcher( 119 ScenarioDispatcherFactory.createFromScenarioFileWithRandomPrefix( 120 "scenarios/remarketing-cuj-119.json")); 121 AdSelectionConfig adSelectionConfig = 122 makeAdSelectionConfig(dispatcher.getBaseAddressWithPrefix()); 123 124 try { 125 joinCustomAudience(SHOES_CA); 126 overrideBiddingLogicVersionToV3(true); 127 AdSelectionOutcome result = doSelectAds(adSelectionConfig); 128 assertThat(result.hasOutcome()).isTrue(); 129 assertThat(result.getRenderUri()).isNotNull(); 130 } finally { 131 overrideBiddingLogicVersionToV3(false); 132 leaveCustomAudience(SHOES_CA); 133 } 134 135 assertThat(dispatcher.getCalledPaths()) 136 .containsAtLeastElementsIn(dispatcher.getVerifyCalledPaths()); 137 } 138 139 /** 140 * Test that buyers can specify an adCost in generateBid that is found in the buyer impression 141 * reporting URI (Remarketing CUJ 160). 142 */ 143 @Test testAdSelection_withAdCostInUrl_happyPath()144 public void testAdSelection_withAdCostInUrl_happyPath() throws Exception { 145 ScenarioDispatcher dispatcher = 146 setupDispatcher( 147 ScenarioDispatcherFactory.createFromScenarioFileWithRandomPrefix( 148 "scenarios/remarketing-cuj-160.json")); 149 AdSelectionConfig adSelectionConfig = 150 makeAdSelectionConfig(dispatcher.getBaseAddressWithPrefix()); 151 long adSelectionId; 152 153 try { 154 overrideCpcBillingEnabled(true); 155 joinCustomAudience(SHOES_CA); 156 AdSelectionOutcome result = doSelectAds(adSelectionConfig); 157 adSelectionId = result.getAdSelectionId(); 158 assertThat(result.hasOutcome()).isTrue(); 159 assertThat(result.getRenderUri()).isNotNull(); 160 } finally { 161 overrideCpcBillingEnabled(false); 162 leaveCustomAudience(SHOES_CA); 163 } 164 doReportImpression(adSelectionId, adSelectionConfig); 165 166 assertThat(dispatcher.getCalledPaths()) 167 .containsAtLeastElementsIn(dispatcher.getVerifyCalledPaths()); 168 } 169 170 /** 171 * Test that buyers can specify an adCost in generateBid that reported (Remarketing CUJ 161). 172 */ 173 @Test 174 @SetFlagEnabled(KEY_FLEDGE_REGISTER_AD_BEACON_ENABLED) testAdSelection_withAdCostInUrl_adCostIsReported()175 public void testAdSelection_withAdCostInUrl_adCostIsReported() throws Exception { 176 ScenarioDispatcher dispatcher = 177 setupDispatcher( 178 ScenarioDispatcherFactory.createFromScenarioFileWithRandomPrefix( 179 "scenarios/remarketing-cuj-161.json")); 180 AdSelectionConfig adSelectionConfig = 181 makeAdSelectionConfig(dispatcher.getBaseAddressWithPrefix()); 182 long adSelectionId; 183 184 try { 185 overrideCpcBillingEnabled(true); 186 joinCustomAudience(SHOES_CA); 187 AdSelectionOutcome result = doSelectAds(adSelectionConfig); 188 adSelectionId = result.getAdSelectionId(); 189 doReportImpression(adSelectionId, adSelectionConfig); 190 doReportEvent(adSelectionId, "click"); 191 } finally { 192 overrideCpcBillingEnabled(false); 193 leaveCustomAudience(SHOES_CA); 194 } 195 196 assertThat(dispatcher.getCalledPaths()) 197 .containsAtLeastElementsIn(dispatcher.getVerifyCalledPaths()); 198 } 199 200 /** 201 * Test that custom audience can be successfully fetched from a server and joined to participate 202 * in a successful ad selection (Remarketing CUJ 169). 203 */ 204 @Test testAdSelection_withFetchCustomAudience_fetchesAndReturnsSuccessfully()205 public void testAdSelection_withFetchCustomAudience_fetchesAndReturnsSuccessfully() 206 throws Exception { 207 ScenarioDispatcher dispatcher = 208 setupDispatcher( 209 ScenarioDispatcherFactory.createFromScenarioFileWithRandomPrefix( 210 "scenarios/remarketing-cuj-fetchCA.json")); 211 AdSelectionConfig adSelectionConfig = 212 makeAdSelectionConfig(dispatcher.getBaseAddressWithPrefix()); 213 String customAudienceName = "hats"; 214 215 try { 216 CustomAudience customAudience = makeCustomAudience(customAudienceName).build(); 217 ShellUtils.runShellCommand( 218 "device_config put adservices fledge_fetch_custom_audience_enabled true"); 219 mCustomAudienceClient 220 .fetchAndJoinCustomAudience( 221 new FetchAndJoinCustomAudienceRequest.Builder( 222 Uri.parse( 223 dispatcher.getBaseAddressWithPrefix().toString() 224 + Scenarios.FETCH_CA_PATH)) 225 .setActivationTime(customAudience.getActivationTime()) 226 .setExpirationTime(customAudience.getExpirationTime()) 227 .setName(customAudience.getName()) 228 .setUserBiddingSignals(customAudience.getUserBiddingSignals()) 229 .build()) 230 .get(5, TimeUnit.SECONDS); 231 Log.d(TAG, "Joined Custom Audience: " + customAudienceName); 232 AdSelectionOutcome result = doSelectAds(adSelectionConfig); 233 assertThat(result.hasOutcome()).isTrue(); 234 assertThat(result.getRenderUri()).isNotNull(); 235 } finally { 236 ShellUtils.runShellCommand( 237 "device_config put adservices fledge_fetch_custom_audience_enabled false"); 238 leaveCustomAudience(customAudienceName); 239 } 240 241 assertThat(dispatcher.getCalledPaths()) 242 .containsAtLeastElementsIn(dispatcher.getVerifyCalledPaths()); 243 } 244 245 /** Test that ad selection fails with an expired custom audience. */ 246 @Test testAdSelection_withShortlyExpiringCustomAudience_selectAdsThrowsException()247 public void testAdSelection_withShortlyExpiringCustomAudience_selectAdsThrowsException() 248 throws Exception { 249 ScenarioDispatcher dispatcher = 250 setupDispatcher( 251 ScenarioDispatcherFactory.createFromScenarioFileWithRandomPrefix( 252 "scenarios/remarketing-cuj-default.json")); 253 AdSelectionConfig config = makeAdSelectionConfig(dispatcher.getBaseAddressWithPrefix()); 254 CustomAudience customAudience = 255 makeCustomAudience(SHOES_CA) 256 .setExpirationTime(Instant.now().plus(5, ChronoUnit.SECONDS)) 257 .build(); 258 259 joinCustomAudience(customAudience); 260 Log.d(TAG, "Joined custom audience"); 261 // Make a call to verify ad selection succeeds before timing out. 262 mAdSelectionClient.selectAds(config).get(TIMEOUT, TimeUnit.SECONDS); 263 Thread.sleep(7000); 264 265 Exception selectAdsException = 266 assertThrows( 267 ExecutionException.class, 268 () -> mAdSelectionClient.selectAds(config).get(TIMEOUT, TimeUnit.SECONDS)); 269 assertThat(selectAdsException.getCause()).isInstanceOf(IllegalStateException.class); 270 } 271 272 /** 273 * Test that not providing any ad selection Ids to selectAds with ad selection outcomes should 274 * result in failure (Remarketing CUJ 071). 275 */ 276 @Test testAdSelectionOutcomes_withNoAdSelectionId_throwsException()277 public void testAdSelectionOutcomes_withNoAdSelectionId_throwsException() throws Exception { 278 ScenarioDispatcher dispatcher = 279 setupDispatcher( 280 ScenarioDispatcherFactory.createFromScenarioFileWithRandomPrefix( 281 "scenarios/remarketing-cuj-default.json")); 282 AdSelectionFromOutcomesConfig config = 283 new AdSelectionFromOutcomesConfig.Builder() 284 .setSeller( 285 AdTechIdentifier.fromString( 286 dispatcher.getBaseAddressWithPrefix().getHost())) 287 .setAdSelectionIds(List.of()) 288 .setSelectionLogicUri( 289 Uri.parse( 290 dispatcher.getBaseAddressWithPrefix() 291 + Scenarios.MEDIATION_LOGIC_PATH)) 292 .setSelectionSignals(makeAdSelectionSignals()) 293 .build(); 294 295 try { 296 Exception selectAdsException = 297 assertThrows( 298 ExecutionException.class, 299 () -> 300 mAdSelectionClient 301 .selectAds(config) 302 .get(TIMEOUT, TimeUnit.SECONDS)); 303 assertThat(selectAdsException.getCause()).isInstanceOf(IllegalArgumentException.class); 304 } finally { 305 leaveCustomAudience(SHIRTS_CA); 306 } 307 } 308 309 /** Test that buyer and seller receive win and loss debug reports (Remarketing CUJ 164). */ 310 @Test testAdSelection_withDebugReporting_happyPath()311 public void testAdSelection_withDebugReporting_happyPath() throws Exception { 312 assumeTrue(isAdIdSupported()); 313 ScenarioDispatcher dispatcher = 314 setupDispatcher( 315 ScenarioDispatcherFactory.createFromScenarioFileWithRandomPrefix( 316 "scenarios/remarketing-cuj-164.json")); 317 AdSelectionConfig adSelectionConfig = 318 makeAdSelectionConfig(dispatcher.getBaseAddressWithPrefix()); 319 320 try { 321 joinCustomAudience(SHOES_CA); 322 joinCustomAudience(SHIRTS_CA); 323 setDebugReportingEnabledForTesting(true); 324 AdSelectionOutcome result = doSelectAds(adSelectionConfig); 325 assertThat(result.hasOutcome()).isTrue(); 326 } finally { 327 setDebugReportingEnabledForTesting(false); 328 leaveCustomAudience(SHOES_CA); 329 joinCustomAudience(SHIRTS_CA); 330 } 331 332 assertThat(dispatcher.getCalledPaths()) 333 .containsAtLeastElementsIn(dispatcher.getVerifyCalledPaths()); 334 } 335 336 /** 337 * Test that buyer and seller do not receive win and loss debug reports if the feature is 338 * disabled (Remarketing CUJ 165). 339 */ 340 @Test testAdSelection_withDebugReportingDisabled_doesNotSend()341 public void testAdSelection_withDebugReportingDisabled_doesNotSend() throws Exception { 342 ScenarioDispatcher dispatcher = 343 setupDispatcher( 344 ScenarioDispatcherFactory.createFromScenarioFileWithRandomPrefix( 345 "scenarios/remarketing-cuj-165.json")); 346 AdSelectionConfig adSelectionConfig = 347 makeAdSelectionConfig(dispatcher.getBaseAddressWithPrefix()); 348 349 try { 350 joinCustomAudience(SHOES_CA); 351 overrideBiddingLogicVersionToV3(true); 352 AdSelectionOutcome result = doSelectAds(adSelectionConfig); 353 assertThat(result.hasOutcome()).isTrue(); 354 } finally { 355 overrideBiddingLogicVersionToV3(false); 356 leaveCustomAudience(SHOES_CA); 357 } 358 359 assertThat(dispatcher.getCalledPaths()) 360 .containsAtLeastElementsIn(dispatcher.getVerifyCalledPaths()); 361 } 362 363 /** 364 * Test that buyer and seller receive win and loss debug reports with reject reason (Remarketing 365 * CUJ 170). 366 */ 367 @Test testAdSelection_withDebugReportingAndRejectReason_happyPath()368 public void testAdSelection_withDebugReportingAndRejectReason_happyPath() throws Exception { 369 assumeTrue(isAdIdSupported()); 370 ScenarioDispatcher dispatcher = 371 setupDispatcher( 372 ScenarioDispatcherFactory.createFromScenarioFileWithRandomPrefix( 373 "scenarios/remarketing-cuj-170.json")); 374 AdSelectionConfig adSelectionConfig = 375 makeAdSelectionConfig(dispatcher.getBaseAddressWithPrefix()); 376 377 try { 378 joinCustomAudience(SHOES_CA); 379 joinCustomAudience(SHIRTS_CA); 380 setDebugReportingEnabledForTesting(true); 381 AdSelectionOutcome result = doSelectAds(adSelectionConfig); 382 assertThat(result.hasOutcome()).isTrue(); 383 } finally { 384 setDebugReportingEnabledForTesting(false); 385 leaveCustomAudience(SHOES_CA); 386 leaveCustomAudience(SHIRTS_CA); 387 } 388 389 assertThat(dispatcher.getCalledPaths()) 390 .containsAtLeastElementsIn(dispatcher.getVerifyCalledPaths()); 391 } 392 393 @Test testAdSelection_withHighLatencyBackend_doesNotWinAuction()394 public void testAdSelection_withHighLatencyBackend_doesNotWinAuction() throws Exception { 395 ScenarioDispatcher dispatcher = 396 setupDispatcher( 397 ScenarioDispatcherFactory.createFromScenarioFileWithRandomPrefix( 398 "scenarios/remarketing-cuj-053.json")); 399 AdSelectionConfig config = makeAdSelectionConfig(dispatcher.getBaseAddressWithPrefix()); 400 401 try { 402 joinCustomAudience(SHIRTS_CA); 403 Exception selectAdsException = 404 assertThrows( 405 ExecutionException.class, 406 () -> 407 mAdSelectionClient 408 .selectAds(config) 409 .get(TIMEOUT, TimeUnit.SECONDS)); 410 assertThat(selectAdsException.getCause()).isInstanceOf(TimeoutException.class); 411 } finally { 412 leaveCustomAudience(SHIRTS_CA); 413 } 414 415 assertThat(dispatcher.getCalledPaths()) 416 .containsAtLeastElementsIn(dispatcher.getVerifyCalledPaths()); 417 } 418 419 @Test testAdSelection_withInvalidScoringUrl_doesNotWinAuction()420 public void testAdSelection_withInvalidScoringUrl_doesNotWinAuction() throws Exception { 421 // ScenarioDispatcher returns 404 for all paths which are not setup from the json file and 422 // we didn't configure a scoring logic url. 423 ScenarioDispatcher dispatcher = 424 setupDispatcher( 425 ScenarioDispatcherFactory.createFromScenarioFileWithRandomPrefix( 426 "scenarios/remarketing-cuj-invalid-scoring-logic-url.json")); 427 AdSelectionConfig config = makeAdSelectionConfig(dispatcher.getBaseAddressWithPrefix()); 428 429 try { 430 joinCustomAudience(SHIRTS_CA); 431 Exception selectAdsException = 432 assertThrows( 433 ExecutionException.class, 434 () -> 435 mAdSelectionClient 436 .selectAds(config) 437 .get(TIMEOUT, TimeUnit.SECONDS)); 438 assertThat( 439 selectAdsException.getCause() instanceof TimeoutException 440 || selectAdsException.getCause() 441 instanceof IllegalStateException) 442 .isTrue(); 443 } finally { 444 leaveCustomAudience(SHIRTS_CA); 445 } 446 447 assertThat(dispatcher.getCalledPaths()) 448 .containsAtLeastElementsIn(dispatcher.getVerifyCalledPaths()); 449 } 450 isAdIdSupported()451 private boolean isAdIdSupported() { 452 AdIdCompatibleManager adIdCompatibleManager; 453 AdServicesOutcomeReceiverForTests<AdId> callback = 454 new AdServicesOutcomeReceiverForTests<>(); 455 try { 456 adIdCompatibleManager = new AdIdCompatibleManager(sContext); 457 adIdCompatibleManager.getAdId(MoreExecutors.directExecutor(), callback); 458 } catch (IllegalStateException e) { 459 Log.d(TAG, "isAdIdAvailable(): IllegalStateException detected in AdId manager."); 460 return false; 461 } 462 463 boolean isAdIdAvailable; 464 try { 465 AdId result = callback.assertSuccess(); 466 isAdIdAvailable = 467 !Objects.isNull(result) 468 && !result.isLimitAdTrackingEnabled() 469 && !result.getAdId().equals(AdId.ZERO_OUT); 470 } catch (InterruptedException e) { 471 Thread.currentThread().interrupt(); 472 Log.d(TAG, "isAdIdSupported(): failed to get AdId due to InterruptedException."); 473 isAdIdAvailable = false; 474 } 475 476 Log.d(TAG, String.format("isAdIdSupported(): %b", isAdIdAvailable)); 477 return isAdIdAvailable; 478 } 479 } 480