1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.adservices.service.appsearch; 18 19 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; 20 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; 21 22 import static com.google.common.truth.Truth.assertThat; 23 24 import static org.junit.Assert.assertThrows; 25 import static org.mockito.ArgumentMatchers.any; 26 import static org.mockito.Mockito.atMost; 27 import static org.mockito.Mockito.verify; 28 29 import android.content.Context; 30 import android.content.pm.Signature; 31 32 import androidx.appsearch.app.AppSearchBatchResult; 33 import androidx.appsearch.app.AppSearchResult; 34 import androidx.appsearch.app.AppSearchSession; 35 import androidx.appsearch.app.GenericDocument; 36 import androidx.appsearch.app.GlobalSearchSession; 37 import androidx.appsearch.app.PackageIdentifier; 38 import androidx.appsearch.app.SearchResult; 39 import androidx.appsearch.app.SearchResults; 40 import androidx.appsearch.app.SetSchemaRequest; 41 import androidx.appsearch.app.SetSchemaResponse; 42 import androidx.test.core.app.ApplicationProvider; 43 import androidx.test.filters.SmallTest; 44 45 import com.android.adservices.AdServicesCommon; 46 import com.android.adservices.common.AdServicesDeviceSupportedRule; 47 import com.android.adservices.concurrency.AdServicesExecutors; 48 import com.android.adservices.mockito.AdServicesExtendedMockitoRule; 49 import com.android.adservices.service.Flags; 50 import com.android.adservices.service.FlagsFactory; 51 import com.android.adservices.service.consent.ConsentConstants; 52 53 import com.google.common.util.concurrent.Futures; 54 import com.google.common.util.concurrent.ListenableFuture; 55 import com.google.common.util.concurrent.ListeningExecutorService; 56 import com.google.common.util.concurrent.MoreExecutors; 57 58 import org.junit.Before; 59 import org.junit.Rule; 60 import org.junit.Test; 61 import org.mockito.Mock; 62 import org.mockito.Mockito; 63 import org.mockito.MockitoAnnotations; 64 65 import java.util.List; 66 import java.util.concurrent.ExecutionException; 67 import java.util.concurrent.Executor; 68 import java.util.concurrent.Executors; 69 import java.util.concurrent.TimeUnit; 70 import java.util.concurrent.TimeoutException; 71 72 @SmallTest 73 public class AppSearchDaoTest { 74 @Mock SearchResults mSearchResults; 75 @Mock List<SearchResult> mMockPage; 76 @Mock GlobalSearchSession mGlobalSearchSession; 77 @Mock AppSearchSession mAppSearchSession; 78 @Mock Flags mFlags; 79 private final Executor mExecutor = AdServicesExecutors.getBackgroundExecutor(); 80 private final Context mContext = ApplicationProvider.getApplicationContext(); 81 private String mAdServicesPackageName; 82 private static final String ID = "1"; 83 private static final String NAMESPACE = "consent"; 84 private static final String API_TYPE = "CONSENT-TOPICS"; 85 private static final String CONSENT = "true"; 86 private static final String TEST = "test"; 87 private static final String SHA = 88 "686d5c450e00ebe600f979300a29234644eade42f24ede07a073f2bc6b94a3a2"; 89 private static final PackageIdentifier PACKAGE_IDENTIFIER = 90 new PackageIdentifier( 91 /* packageName= */ TEST, 92 /* sha256Certificate= */ new Signature(SHA).toByteArray()); 93 94 private static final int APPSEARCH_READ_TIMEOUT_MS = 500; 95 private static final int APPSEARCH_WRITE_TIMEOUT_MS = 200; 96 97 @Rule(order = 0) 98 public final AdServicesDeviceSupportedRule adServicesDeviceSupportedRule = 99 new AdServicesDeviceSupportedRule(); 100 101 @Rule(order = 1) 102 public final AdServicesExtendedMockitoRule adServicesExtendedMockitoRule = 103 new AdServicesExtendedMockitoRule.Builder(this).mockStatic(FlagsFactory.class).build(); 104 105 @Before before()106 public void before() { 107 // TODO(b/347043278): must be set inside @Before so it's not called when device is not 108 // supported 109 mAdServicesPackageName = AppSearchConsentWorker.getAdServicesPackageName(mContext); 110 MockitoAnnotations.initMocks(this); 111 when(mFlags.getAppsearchWriterAllowListOverride()).thenReturn(""); 112 when(mFlags.getAppSearchReadTimeout()).thenReturn(APPSEARCH_READ_TIMEOUT_MS); 113 when(mFlags.getAppSearchWriteTimeout()).thenReturn(APPSEARCH_WRITE_TIMEOUT_MS); 114 doReturn(mFlags).when(FlagsFactory::getFlags); 115 } 116 117 @Test testIterateSearchResults_emptyPage()118 public void testIterateSearchResults_emptyPage() throws Exception { 119 when(mMockPage.isEmpty()).thenReturn(true); 120 when(mSearchResults.getNextPageAsync()).thenReturn(Futures.immediateFuture(mMockPage)); 121 ListenableFuture<AppSearchConsentDao> result = 122 AppSearchDao.iterateSearchResults( 123 AppSearchConsentDao.class, mSearchResults, mExecutor); 124 assertThat(result.get()).isEqualTo(null); 125 } 126 127 @Test testIterateSearchResults()128 public void testIterateSearchResults() throws Exception { 129 AppSearchConsentDao dao = new AppSearchConsentDao(ID, ID, NAMESPACE, API_TYPE, CONSENT); 130 when(mMockPage.isEmpty()).thenReturn(false); 131 when(mSearchResults.getNextPageAsync()).thenReturn(Futures.immediateFuture(mMockPage)); 132 GenericDocument document = 133 new GenericDocument.Builder(NAMESPACE, ID, dao.getClass().getSimpleName()) 134 .setPropertyString("userId", ID) 135 .setPropertyString("consent", CONSENT) 136 .setPropertyString("apiType", API_TYPE) 137 .build(); 138 SearchResult searchResult = 139 new SearchResult.Builder(TEST, TEST).setGenericDocument(document).build(); 140 when(mMockPage.get(0)).thenReturn(searchResult); 141 142 ListenableFuture<AppSearchConsentDao> result = 143 AppSearchDao.iterateSearchResults( 144 AppSearchConsentDao.class, mSearchResults, mExecutor); 145 assertThat(result.get()).isEqualTo(dao); 146 } 147 148 @Test testGetAllowedPackages_noOverride()149 public void testGetAllowedPackages_noOverride() { 150 when(mFlags.getAppsearchWriterAllowListOverride()).thenReturn(""); 151 152 String expected = 153 mAdServicesPackageName.replace( 154 AdServicesCommon.ADSERVICES_APK_PACKAGE_NAME_SUFFIX, 155 AdServicesCommon.ADEXTSERVICES_PACKAGE_NAME_SUFFIX); 156 157 List<String> allowedPackages = AppSearchDao.getAllowedPackages(mAdServicesPackageName); 158 159 assertThat(allowedPackages.size()).isEqualTo(1); 160 assertThat(allowedPackages.get(0)).isEqualTo(expected); 161 } 162 163 @Test testGetAllowedPackages_withOverride()164 public void testGetAllowedPackages_withOverride() { 165 String allowedPackage = "allowed.package"; 166 when(mFlags.getAppsearchWriterAllowListOverride()).thenReturn(allowedPackage); 167 168 List<String> allowedPackages = AppSearchDao.getAllowedPackages(mAdServicesPackageName); 169 170 assertThat(allowedPackages.size()).isEqualTo(1); 171 assertThat(allowedPackages.get(0)).isEqualTo(allowedPackage); 172 } 173 174 @Test testReadConsentData_emptyQuery()175 public void testReadConsentData_emptyQuery() { 176 AppSearchDao dao = 177 AppSearchDao.readConsentData( 178 AppSearchConsentDao.class, 179 Futures.immediateFuture(mGlobalSearchSession), 180 mExecutor, 181 NAMESPACE, 182 null, 183 mAdServicesPackageName); 184 assertThat(dao).isEqualTo(null); 185 186 AppSearchDao dao2 = 187 AppSearchDao.readConsentData( 188 AppSearchConsentDao.class, 189 Futures.immediateFuture(mGlobalSearchSession), 190 mExecutor, 191 NAMESPACE, 192 "", 193 mAdServicesPackageName); 194 assertThat(dao2).isEqualTo(null); 195 } 196 197 @Test testReadConsentData()198 public void testReadConsentData() { 199 when(mMockPage.isEmpty()).thenReturn(false); 200 when(mSearchResults.getNextPageAsync()).thenReturn(Futures.immediateFuture(mMockPage)); 201 AppSearchConsentDao dao = new AppSearchConsentDao(ID, ID, NAMESPACE, API_TYPE, CONSENT); 202 GenericDocument document = 203 new GenericDocument.Builder(NAMESPACE, ID, dao.getClass().getSimpleName()) 204 .setPropertyString("userId", ID) 205 .setPropertyString("consent", CONSENT) 206 .setPropertyString("apiType", API_TYPE) 207 .build(); 208 SearchResult searchResult = 209 new SearchResult.Builder(TEST, TEST).setGenericDocument(document).build(); 210 when(mMockPage.get(0)).thenReturn(searchResult); 211 when(mGlobalSearchSession.search(any(), any())).thenReturn(mSearchResults); 212 AppSearchDao result = 213 AppSearchDao.readConsentData( 214 AppSearchConsentDao.class, 215 Futures.immediateFuture(mGlobalSearchSession), 216 mExecutor, 217 NAMESPACE, 218 TEST, 219 mAdServicesPackageName); 220 assertThat(result).isEqualTo(dao); 221 } 222 223 @Test testReadConsentData_timeout()224 public void testReadConsentData_timeout() { 225 AppSearchDao result = 226 AppSearchDao.readConsentData( 227 AppSearchConsentDao.class, 228 getLongRunningOperation(mGlobalSearchSession), 229 mExecutor, 230 NAMESPACE, 231 TEST, 232 mAdServicesPackageName); 233 assertThat(result).isNull(); 234 } 235 getLongRunningOperation(T result)236 private <T> ListenableFuture<T> getLongRunningOperation(T result) { 237 // Wait for a time that's longer than the AppSearch read timeout, then return the result. 238 ListeningExecutorService ls = 239 MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()); 240 return ls.submit( 241 () -> { 242 TimeUnit.MILLISECONDS.sleep(APPSEARCH_READ_TIMEOUT_MS + 500); 243 return result; 244 }); 245 } 246 247 @Test testReadAppSearchData_emptyQuery()248 public void testReadAppSearchData_emptyQuery() { 249 AppSearchDao dao = 250 AppSearchDao.readAppSearchSessionData( 251 AppSearchConsentDao.class, 252 Futures.immediateFuture(mAppSearchSession), 253 mExecutor, 254 NAMESPACE, 255 null, 256 mAdServicesPackageName); 257 assertThat(dao).isEqualTo(null); 258 259 AppSearchDao dao2 = 260 AppSearchDao.readAppSearchSessionData( 261 AppSearchConsentDao.class, 262 Futures.immediateFuture(mAppSearchSession), 263 mExecutor, 264 NAMESPACE, 265 "", 266 mAdServicesPackageName); 267 assertThat(dao2).isEqualTo(null); 268 } 269 270 @Test testReadAppSearchData()271 public void testReadAppSearchData() { 272 when(mMockPage.isEmpty()).thenReturn(false); 273 when(mSearchResults.getNextPageAsync()).thenReturn(Futures.immediateFuture(mMockPage)); 274 AppSearchConsentDao dao = new AppSearchConsentDao(ID, ID, NAMESPACE, API_TYPE, CONSENT); 275 GenericDocument document = 276 new GenericDocument.Builder(NAMESPACE, ID, dao.getClass().getSimpleName()) 277 .setPropertyString("userId", ID) 278 .setPropertyString("consent", CONSENT) 279 .setPropertyString("apiType", API_TYPE) 280 .build(); 281 SearchResult searchResult = 282 new SearchResult.Builder(TEST, TEST).setGenericDocument(document).build(); 283 when(mMockPage.get(0)).thenReturn(searchResult); 284 when(mAppSearchSession.search(any(), any())).thenReturn(mSearchResults); 285 AppSearchDao result = 286 AppSearchDao.readAppSearchSessionData( 287 AppSearchConsentDao.class, 288 Futures.immediateFuture(mAppSearchSession), 289 mExecutor, 290 NAMESPACE, 291 TEST, 292 mAdServicesPackageName); 293 assertThat(result).isEqualTo(dao); 294 } 295 296 @Test testWriteConsentData_failure()297 public void testWriteConsentData_failure() { 298 AppSearchSession mockSession = Mockito.mock(AppSearchSession.class); 299 verify(mockSession, atMost(1)).setSchemaAsync(any(SetSchemaRequest.class)); 300 301 SetSchemaResponse mockResponse = Mockito.mock(SetSchemaResponse.class); 302 when(mockSession.setSchemaAsync(any(SetSchemaRequest.class))) 303 .thenReturn(Futures.immediateFuture(mockResponse)); 304 305 AppSearchResult<Void> mockResult = 306 AppSearchResult.newFailedResult(AppSearchResult.RESULT_INVALID_ARGUMENT, "test"); 307 SetSchemaResponse.MigrationFailure failure = 308 new SetSchemaResponse.MigrationFailure( 309 /* namespace= */ TEST, 310 /* documentId= */ TEST, 311 /* schemaType= */ TEST, 312 /* failedResult= */ mockResult); 313 when(mockResponse.getMigrationFailures()).thenReturn(List.of(failure)); 314 // We can't use the base class instance since writing will fail without the necessary 315 // Document fields defined on the class, so we use a subclass instance. 316 AppSearchConsentDao dao = new AppSearchConsentDao(ID, ID, NAMESPACE, API_TYPE, CONSENT); 317 Exception e = 318 assertThrows( 319 RuntimeException.class, 320 () -> 321 dao.writeData( 322 Futures.immediateFuture(mockSession), 323 List.of(PACKAGE_IDENTIFIER), 324 mExecutor)); 325 // Schema migration throws a RuntimeException, which gets wrapped into an ExecutionException 326 // by the get() call. The catch block then wraps this into another RuntimeException. 327 assertThat(e).hasMessageThat().isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); 328 assertThat(e).hasCauseThat().isNotNull(); 329 assertThat(e).hasCauseThat().isInstanceOf(ExecutionException.class); 330 assertThat(e).hasCauseThat().hasCauseThat().isNotNull(); 331 assertThat(e).hasCauseThat().hasCauseThat().isInstanceOf(RuntimeException.class); 332 assertThat(e) 333 .hasCauseThat() 334 .hasCauseThat() 335 .hasMessageThat() 336 .isEqualTo( 337 ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE 338 + " Migration failure: [FAILURE(3)]: test"); 339 } 340 341 @Test testWriteConsentData()342 public void testWriteConsentData() throws Exception { 343 AppSearchSession mockSession = Mockito.mock(AppSearchSession.class); 344 verify(mockSession, atMost(1)).setSchemaAsync(any(SetSchemaRequest.class)); 345 346 SetSchemaResponse mockResponse = Mockito.mock(SetSchemaResponse.class); 347 when(mockSession.setSchemaAsync(any(SetSchemaRequest.class))) 348 .thenReturn(Futures.immediateFuture(mockResponse)); 349 350 verify(mockResponse, atMost(1)).getMigrationFailures(); 351 when(mockResponse.getMigrationFailures()).thenReturn(List.of()); 352 // We can't use the base class instance since writing will fail without the necessary 353 // Document fields defined on the class, so we use a subclass instance. 354 AppSearchConsentDao dao = new AppSearchConsentDao(ID, ID, NAMESPACE, API_TYPE, CONSENT); 355 AppSearchBatchResult<String, Void> result = Mockito.mock(AppSearchBatchResult.class); 356 when(mockSession.putAsync(any())).thenReturn(Futures.immediateFuture(result)); 357 358 // Verify that no exception is thrown. 359 AppSearchBatchResult<String, Void> output = 360 dao.writeData( 361 Futures.immediateFuture(mockSession), 362 List.of(PACKAGE_IDENTIFIER), 363 mExecutor); 364 assertThat(output).isNotNull(); 365 } 366 367 @Test testWriteConsentData_timeout()368 public void testWriteConsentData_timeout() throws Exception { 369 AppSearchSession mockSession = Mockito.mock(AppSearchSession.class); 370 verify(mockSession, atMost(1)).setSchemaAsync(any(SetSchemaRequest.class)); 371 372 SetSchemaResponse mockResponse = Mockito.mock(SetSchemaResponse.class); 373 when(mockSession.setSchemaAsync(any(SetSchemaRequest.class))) 374 .thenReturn(Futures.immediateFuture(mockResponse)); 375 376 verify(mockResponse, atMost(1)).getMigrationFailures(); 377 when(mockResponse.getMigrationFailures()).thenReturn(List.of()); 378 // We can't use the base class instance since writing will fail without the necessary 379 // Document fields defined on the class, so we use a subclass instance. 380 AppSearchConsentDao dao = new AppSearchConsentDao(ID, ID, NAMESPACE, API_TYPE, CONSENT); 381 AppSearchBatchResult<String, Void> result = Mockito.mock(AppSearchBatchResult.class); 382 when(mockSession.putAsync(any())).thenReturn(getLongRunningOperation(result)); 383 384 // Verify exception due to timeout 385 RuntimeException e = 386 assertThrows( 387 RuntimeException.class, 388 () -> 389 dao.writeData( 390 Futures.immediateFuture(mockSession), 391 List.of(PACKAGE_IDENTIFIER), 392 mExecutor)); 393 assertThat(e).hasMessageThat().isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); 394 assertThat(e).hasCauseThat().isNotNull(); 395 assertThat(e).hasCauseThat().isInstanceOf(TimeoutException.class); 396 } 397 398 @Test testDeleteConsentData_failure()399 public void testDeleteConsentData_failure() { 400 AppSearchSession mockSession = Mockito.mock(AppSearchSession.class); 401 verify(mockSession, atMost(1)).setSchemaAsync(any(SetSchemaRequest.class)); 402 403 SetSchemaResponse mockResponse = Mockito.mock(SetSchemaResponse.class); 404 when(mockSession.setSchemaAsync(any(SetSchemaRequest.class))) 405 .thenReturn(Futures.immediateFuture(mockResponse)); 406 407 AppSearchResult<String> mockResult = 408 AppSearchResult.newFailedResult(AppSearchResult.RESULT_INVALID_ARGUMENT, "test"); 409 SetSchemaResponse.MigrationFailure failure = 410 new SetSchemaResponse.MigrationFailure( 411 /* namespace= */ TEST, 412 /* documentId= */ TEST, 413 /* schemaType= */ TEST, 414 /* failedResult= */ mockResult); 415 when(mockResponse.getMigrationFailures()).thenReturn(List.of(failure)); 416 // We can't use the base class instance since writing will fail without the necessary 417 // Document fields defined on the class, so we use a subclass instance. 418 Exception e = 419 assertThrows( 420 RuntimeException.class, 421 () -> 422 AppSearchDao.deleteData( 423 AppSearchConsentDao.class, 424 Futures.immediateFuture(mockSession), 425 mExecutor, 426 TEST, 427 NAMESPACE)); 428 429 // Schema migration throws a RuntimeException, which gets wrapped into an ExecutionException 430 // by the get() call. The catch block then wraps this into another RuntimeException. 431 assertThat(e).hasMessageThat().isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); 432 assertThat(e).hasCauseThat().isNotNull(); 433 assertThat(e).hasCauseThat().isInstanceOf(ExecutionException.class); 434 assertThat(e).hasCauseThat().hasCauseThat().isNotNull(); 435 assertThat(e).hasCauseThat().hasCauseThat().isInstanceOf(RuntimeException.class); 436 assertThat(e) 437 .hasCauseThat() 438 .hasCauseThat() 439 .hasMessageThat() 440 .isEqualTo( 441 ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE 442 + " Migration failure: [FAILURE(3)]: test"); 443 } 444 445 @Test testDeleteConsentData()446 public void testDeleteConsentData() throws Exception { 447 AppSearchSession mockSession = Mockito.mock(AppSearchSession.class); 448 verify(mockSession, atMost(1)).setSchemaAsync(any(SetSchemaRequest.class)); 449 450 SetSchemaResponse mockResponse = Mockito.mock(SetSchemaResponse.class); 451 when(mockSession.setSchemaAsync(any(SetSchemaRequest.class))) 452 .thenReturn(Futures.immediateFuture(mockResponse)); 453 454 verify(mockResponse, atMost(1)).getMigrationFailures(); 455 when(mockResponse.getMigrationFailures()).thenReturn(List.of()); 456 // We can't use the base class instance since writing will fail without the necessary 457 // Document fields defined on the class, so we use a subclass instance. 458 AppSearchBatchResult<String, Void> result = Mockito.mock(AppSearchBatchResult.class); 459 when(mockSession.removeAsync(any())).thenReturn(Futures.immediateFuture(result)); 460 461 // Verify that no exception is thrown. 462 AppSearchBatchResult<String, Void> output = 463 AppSearchDao.deleteData( 464 AppSearchConsentDao.class, 465 Futures.immediateFuture(mockSession), 466 mExecutor, 467 TEST, 468 NAMESPACE); 469 assertThat(output).isNotNull(); 470 } 471 472 @Test testDeleteConsentData_timeout()473 public void testDeleteConsentData_timeout() throws Exception { 474 AppSearchSession mockSession = Mockito.mock(AppSearchSession.class); 475 verify(mockSession, atMost(1)).setSchemaAsync(any(SetSchemaRequest.class)); 476 477 SetSchemaResponse mockResponse = Mockito.mock(SetSchemaResponse.class); 478 when(mockSession.setSchemaAsync(any(SetSchemaRequest.class))) 479 .thenReturn(Futures.immediateFuture(mockResponse)); 480 481 verify(mockResponse, atMost(1)).getMigrationFailures(); 482 when(mockResponse.getMigrationFailures()).thenReturn(List.of()); 483 // We can't use the base class instance since writing will fail without the necessary 484 // Document fields defined on the class, so we use a subclass instance. 485 AppSearchBatchResult<String, Void> result = Mockito.mock(AppSearchBatchResult.class); 486 when(mockSession.removeAsync(any())).thenReturn(getLongRunningOperation(result)); 487 488 // Verify exception due to timeout 489 RuntimeException e = 490 assertThrows( 491 RuntimeException.class, 492 () -> 493 AppSearchDao.deleteData( 494 AppSearchConsentDao.class, 495 Futures.immediateFuture(mockSession), 496 mExecutor, 497 TEST, 498 NAMESPACE)); 499 assertThat(e).hasMessageThat().isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); 500 assertThat(e).hasCauseThat().isNotNull(); 501 assertThat(e).hasCauseThat().isInstanceOf(TimeoutException.class); 502 } 503 } 504