diff options
Diffstat (limited to 'adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement')
62 files changed, 12410 insertions, 5836 deletions
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/AsyncRegistrationQueueJobServiceTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/AsyncRegistrationQueueJobServiceTest.java new file mode 100644 index 0000000000..36249ea321 --- /dev/null +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/AsyncRegistrationQueueJobServiceTest.java @@ -0,0 +1,283 @@ +/* + * 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.measurement; + +import static com.android.adservices.service.AdServicesConfig.ASYNC_REGISTRATION_QUEUE_JOB_ID; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.content.Context; + +import com.android.adservices.data.enrollment.EnrollmentDao; +import com.android.adservices.data.measurement.DatastoreManager; +import com.android.adservices.data.measurement.DatastoreManagerFactory; +import com.android.adservices.service.Flags; +import com.android.adservices.service.FlagsFactory; +import com.android.adservices.service.stats.AdServicesLoggerImpl; +import com.android.compatibility.common.util.TestUtils; +import com.android.dx.mockito.inline.extended.ExtendedMockito; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +import java.util.Optional; + +public class AsyncRegistrationQueueJobServiceTest { + + private static final long WAIT_IN_MILLIS = 50L; + private JobScheduler mMockJobScheduler; + private AsyncRegistrationQueueJobService mSpyService; + private DatastoreManager mMockDatastoreManager; + + @Before + public void setUp() { + mSpyService = spy(new AsyncRegistrationQueueJobService()); + mMockJobScheduler = mock(JobScheduler.class); + } + + @Test + public void onStartJob_killSwitchOn() throws Exception { + runWithMocks( + () -> { + // Setup + enableKillSwitch(); + + // Execute + boolean result = mSpyService.onStartJob(mock(JobParameters.class)); + + // Validate + assertFalse(result); + // Allow background thread to execute + Thread.sleep(WAIT_IN_MILLIS); + verify(mSpyService, times(1)).jobFinished(any(), eq(false)); + verify(mMockJobScheduler, times(1)).cancel(eq(ASYNC_REGISTRATION_QUEUE_JOB_ID)); + }); + } + + @Test + public void onStartJob_killSwitchOff() throws Exception { + runWithMocks( + () -> { + // Setup + disableKillSwitch(); + ExtendedMockito.doNothing() + .when( + () -> + AsyncRegistrationQueueJobService.scheduleIfNeeded( + any(), anyBoolean())); + + // Execute + boolean result = mSpyService.onStartJob(mock(JobParameters.class)); + + // Validate + assertTrue(result); + // Allow background thread to execute + Thread.sleep(WAIT_IN_MILLIS); + ExtendedMockito.verify(mSpyService, times(1)).jobFinished(any(), anyBoolean()); + verify(mMockJobScheduler, never()).cancel(eq(ASYNC_REGISTRATION_QUEUE_JOB_ID)); + }); + } + + @Test + public void scheduleIfNeeded_killSwitchOn_dontSchedule() throws Exception { + runWithMocks( + () -> { + // Setup + enableKillSwitch(); + + final Context mockContext = mock(Context.class); + doReturn(mMockJobScheduler) + .when(mockContext) + .getSystemService(JobScheduler.class); + final JobInfo mockJobInfo = mock(JobInfo.class); + doReturn(mockJobInfo) + .when(mMockJobScheduler) + .getPendingJob(eq(ASYNC_REGISTRATION_QUEUE_JOB_ID)); + + // Execute + AsyncRegistrationQueueJobService.scheduleIfNeeded( + mockContext, /* forceSchedule = */ false); + + // Validate + // Allow background thread to execute + Thread.sleep(WAIT_IN_MILLIS); + ExtendedMockito.verify( + () -> AsyncRegistrationQueueJobService.schedule(any(), any()), never()); + verify(mMockJobScheduler, never()) + .getPendingJob(eq(ASYNC_REGISTRATION_QUEUE_JOB_ID)); + }); + } + + @Test + public void scheduleIfNeeded_killSwitchOff_previouslyExecuted_dontForceSchedule_dontSchedule() + throws Exception { + runWithMocks( + () -> { + // Setup + disableKillSwitch(); + + final Context mockContext = mock(Context.class); + doReturn(mMockJobScheduler) + .when(mockContext) + .getSystemService(JobScheduler.class); + final JobInfo mockJobInfo = mock(JobInfo.class); + doReturn(mockJobInfo) + .when(mMockJobScheduler) + .getPendingJob(eq(ASYNC_REGISTRATION_QUEUE_JOB_ID)); + + // Execute + AsyncRegistrationQueueJobService.scheduleIfNeeded( + mockContext, /* forceSchedule = */ false); + + // Validate + // Allow background thread to execute + Thread.sleep(WAIT_IN_MILLIS); + ExtendedMockito.verify( + () -> AsyncRegistrationQueueJobService.schedule(any(), any()), never()); + verify(mMockJobScheduler, times(1)) + .getPendingJob(eq(ASYNC_REGISTRATION_QUEUE_JOB_ID)); + }); + } + + @Test + public void scheduleIfNeeded_killSwitchOff_previouslyExecuted_forceSchedule_schedule() + throws Exception { + runWithMocks( + () -> { + // Setup + disableKillSwitch(); + + final Context mockContext = mock(Context.class); + doReturn(mMockJobScheduler) + .when(mockContext) + .getSystemService(JobScheduler.class); + final JobInfo mockJobInfo = mock(JobInfo.class); + doReturn(mockJobInfo) + .when(mMockJobScheduler) + .getPendingJob(eq(ASYNC_REGISTRATION_QUEUE_JOB_ID)); + + // Execute + AsyncRegistrationQueueJobService.scheduleIfNeeded( + mockContext, /* forceSchedule = */ true); + + // Validate + // Allow background thread to execute + Thread.sleep(WAIT_IN_MILLIS); + ExtendedMockito.verify( + () -> AsyncRegistrationQueueJobService.schedule(any(), any()), + times(1)); + verify(mMockJobScheduler, times(1)) + .getPendingJob(eq(ASYNC_REGISTRATION_QUEUE_JOB_ID)); + }); + } + + @Test + public void scheduleIfNeeded_killSwitchOff_previouslyNotExecuted_dontForceSchedule_schedule() + throws Exception { + runWithMocks( + () -> { + // Setup + disableKillSwitch(); + + final Context mockContext = mock(Context.class); + doReturn(mMockJobScheduler) + .when(mockContext) + .getSystemService(JobScheduler.class); + doReturn(/* noJobInfo = */ null) + .when(mMockJobScheduler) + .getPendingJob(eq(ASYNC_REGISTRATION_QUEUE_JOB_ID)); + + // Execute + AsyncRegistrationQueueJobService.scheduleIfNeeded(mockContext, false); + + // Validate + // Allow background thread to execute + Thread.sleep(WAIT_IN_MILLIS); + ExtendedMockito.verify( + () -> AsyncRegistrationQueueJobService.schedule(any(), any()), + times(1)); + verify(mMockJobScheduler, times(1)) + .getPendingJob(eq(ASYNC_REGISTRATION_QUEUE_JOB_ID)); + }); + } + + private void runWithMocks(TestUtils.RunnableWithThrow execute) throws Exception { + MockitoSession session = + ExtendedMockito.mockitoSession() + .spyStatic(AsyncRegistrationQueueJobService.class) + .spyStatic(AdServicesLoggerImpl.class) + .spyStatic(DatastoreManagerFactory.class) + .spyStatic(EnrollmentDao.class) + .spyStatic(FlagsFactory.class) + .strictness(Strictness.LENIENT) + .startMocking(); + try { + // Setup mock everything in job + mMockDatastoreManager = mock(DatastoreManager.class); + doReturn(Optional.empty()) + .when(mMockDatastoreManager) + .runInTransactionWithResult(any()); + doNothing().when(mSpyService).jobFinished(any(), anyBoolean()); + doReturn(mMockJobScheduler).when(mSpyService).getSystemService(JobScheduler.class); + doReturn(Mockito.mock(Context.class)).when(mSpyService).getApplicationContext(); + ExtendedMockito.doReturn(mock(EnrollmentDao.class)) + .when(() -> EnrollmentDao.getInstance(any())); + ExtendedMockito.doReturn(mock(AdServicesLoggerImpl.class)) + .when(AdServicesLoggerImpl::getInstance); + ExtendedMockito.doNothing() + .when(() -> AsyncRegistrationQueueJobService.schedule(any(), any())); + ExtendedMockito.doReturn(mMockDatastoreManager) + .when(() -> DatastoreManagerFactory.getDatastoreManager(any())); + + // Execute + execute.run(); + } finally { + session.finishMocking(); + } + } + + private void enableKillSwitch() { + toggleKillSwitch(true); + } + + private void disableKillSwitch() { + toggleKillSwitch(false); + } + + private void toggleKillSwitch(boolean value) { + Flags mockFlags = Mockito.mock(Flags.class); + ExtendedMockito.doReturn(mockFlags).when(FlagsFactory::getFlags); + ExtendedMockito.doReturn(value).when(mockFlags).getRegistrationJobQueueKillSwitch(); + } +} diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/AsyncRegistrationQueueRunnerTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/AsyncRegistrationQueueRunnerTest.java new file mode 100644 index 0000000000..5912fb3418 --- /dev/null +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/AsyncRegistrationQueueRunnerTest.java @@ -0,0 +1,1787 @@ +/* + * 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.measurement; + +import static com.android.adservices.service.measurement.attribution.TriggerContentProvider.TRIGGER_URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyShort; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.RemoteException; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.adservices.data.DbHelper; +import com.android.adservices.data.enrollment.EnrollmentDao; +import com.android.adservices.data.measurement.DatastoreException; +import com.android.adservices.data.measurement.DatastoreManager; +import com.android.adservices.data.measurement.IMeasurementDao; +import com.android.adservices.data.measurement.ITransaction; +import com.android.adservices.data.measurement.MeasurementTables; +import com.android.adservices.service.FlagsFactory; +import com.android.adservices.service.enrollment.EnrollmentData; +import com.android.adservices.service.measurement.registration.AsyncSourceFetcher; +import com.android.adservices.service.measurement.registration.AsyncTriggerFetcher; +import com.android.adservices.service.measurement.util.AsyncFetchStatus; +import com.android.adservices.service.measurement.util.AsyncRedirect; +import com.android.adservices.service.measurement.util.UnsignedLong; +import com.android.dx.mockito.inline.extended.ExtendedMockito; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; +import org.mockito.stubbing.Answer; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** Unit tests for {@link AsyncRegistrationQueueRunnerTest} */ +public class AsyncRegistrationQueueRunnerTest { + private static final Context sDefaultContext = ApplicationProvider.getApplicationContext(); + private static final String DEFAULT_ENROLLMENT_ID = "enrollment_id"; + private static final Uri DEFAULT_REGISTRANT = Uri.parse("android-app://com.registrant"); + private static final Uri DEFAULT_VERIFIED_DESTINATION = Uri.parse("android-app://com.example"); + private static final Uri APP_TOP_ORIGIN = + Uri.parse("android-app://" + sDefaultContext.getPackageName()); + private static final Uri WEB_TOP_ORIGIN = Uri.parse("https://example.com"); + private static final Uri REGISTRATION_URI = Uri.parse("https://foo.com/bar?ad=134"); + private static final String LIST_TYPE_REDIRECT_URI_1 = "https://foo.com"; + private static final String LIST_TYPE_REDIRECT_URI_2 = "https://bar.com"; + private static final String LOCATION_TYPE_REDIRECT_URI = "https://baz.com"; + private static final Uri WEB_DESTINATION = Uri.parse("https://web-destination.com"); + private static final Uri APP_DESTINATION = Uri.parse("android-app://com.app_destination"); + private static final Source SOURCE_1 = + SourceFixture.getValidSourceBuilder() + .setEventId(new UnsignedLong(1L)) + .setPublisher(APP_TOP_ORIGIN) + .setAppDestination(Uri.parse("android-app://com.destination1")) + .setWebDestination(Uri.parse("https://web-destination1.com")) + .setEnrollmentId(DEFAULT_ENROLLMENT_ID) + .setRegistrant(Uri.parse("android-app://com.example")) + .setEventTime(new Random().nextLong()) + .setExpiryTime(8640000010L) + .setPriority(100L) + .setSourceType(Source.SourceType.EVENT) + .setAttributionMode(Source.AttributionMode.TRUTHFULLY) + .setDebugKey(new UnsignedLong(47823478789L)) + .build(); + + private AsyncSourceFetcher mAsyncSourceFetcher; + private AsyncTriggerFetcher mAsyncTriggerFetcher; + + @Mock private IMeasurementDao mMeasurementDao; + @Mock private Source mMockedSource; + @Mock private Trigger mMockedTrigger; + @Mock private ITransaction mTransaction; + @Mock private EnrollmentDao mEnrollmentDao; + @Mock private ContentResolver mContentResolver; + @Mock private ContentProviderClient mMockContentProviderClient; + + private MockitoSession mStaticMockSession; + + private static EnrollmentData getEnrollment(String enrollmentId) { + return new EnrollmentData.Builder().setEnrollmentId(enrollmentId).build(); + } + + class FakeDatastoreManager extends DatastoreManager { + + @Override + public ITransaction createNewTransaction() { + return mTransaction; + } + + @Override + public IMeasurementDao getMeasurementDao() { + return mMeasurementDao; + } + } + + @After + public void cleanup() { + SQLiteDatabase db = DbHelper.getInstance(sDefaultContext).safeGetWritableDatabase(); + for (String table : MeasurementTables.ALL_MSMT_TABLES) { + db.delete(table, null, null); + } + mStaticMockSession.finishMocking(); + } + + @Before + public void before() throws RemoteException { + mStaticMockSession = + ExtendedMockito.mockitoSession() + .spyStatic(FlagsFactory.class) + .strictness(Strictness.WARN) + .startMocking(); + ExtendedMockito.doReturn(FlagsFactory.getFlagsForTest()).when(FlagsFactory::getFlags); + + mAsyncSourceFetcher = spy(new AsyncSourceFetcher(sDefaultContext)); + mAsyncTriggerFetcher = spy(new AsyncTriggerFetcher(sDefaultContext)); + MockitoAnnotations.initMocks(this); + when(mEnrollmentDao.getEnrollmentDataFromMeasurementUrl(any())) + .thenReturn(getEnrollment(DEFAULT_ENROLLMENT_ID)); + when(mContentResolver.acquireContentProviderClient(TRIGGER_URI)) + .thenReturn(mMockContentProviderClient); + when(mMockContentProviderClient.insert(any(), any())).thenReturn(TRIGGER_URI); + } + + @Test + public void test_runAsyncRegistrationQueueWorker_appSource_success() throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForAppSource(); + + Answer<?> answerAsyncSourceFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.SUCCESS); + AsyncRedirect asyncRedirect = invocation.getArgument(2); + asyncRedirect.addToRedirects(List.of( + Uri.parse("https://example.com/sF1"), + Uri.parse("https://example.com/sF2"))); + return Optional.of(mMockedSource); + }; + doAnswer(answerAsyncSourceFetcher) + .when(mAsyncSourceFetcher) + .fetchSource(any(), any(), any()); + + Source.FakeReport sf = + new Source.FakeReport( + new UnsignedLong(1L), 1L, Uri.parse("https://example.com/sF")); + List<Source.FakeReport> eventReportList = Collections.singletonList(sf); + when(mMockedSource.assignAttributionModeAndGenerateFakeReports()) + .thenReturn(eventReportList); + + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + // Assertions + verify(mAsyncSourceFetcher, times(1)) + .fetchSource(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + verify(mMeasurementDao, times(1)).insertEventReport(any(EventReport.class)); + verify(mMeasurementDao, times(1)).insertSource(any(Source.class)); + verify(mMeasurementDao, times(2)).insertAsyncRegistration(any(AsyncRegistration.class)); + verify(mMeasurementDao, times(1)).deleteAsyncRegistration(any(String.class)); + } + + // Tests for redirect types + + @Test + public void runAsyncRegistrationQueueWorker_appSource_defaultRegistration_redirectTypeList() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForAppSource(); + Answer<Optional<Source>> answerAsyncSourceFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.SUCCESS); + AsyncRedirect asyncRedirect = invocation.getArgument(2); + asyncRedirect.addToRedirects(List.of( + Uri.parse(LIST_TYPE_REDIRECT_URI_1), + Uri.parse(LIST_TYPE_REDIRECT_URI_2))); + asyncRedirect.setRedirectType(AsyncRegistration.RedirectType.NONE); + return Optional.of(mMockedSource); + }; + doAnswer(answerAsyncSourceFetcher) + .when(mAsyncSourceFetcher) + .fetchSource(any(), any(), any()); + + List<Source.FakeReport> eventReportList = Collections.singletonList( + new Source.FakeReport(new UnsignedLong(1L), 1L, APP_DESTINATION)); + when(mMockedSource.assignAttributionModeAndGenerateFakeReports()) + .thenReturn(eventReportList); + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + ArgumentCaptor<AsyncRegistration> asyncRegistrationArgumentCaptor = + ArgumentCaptor.forClass(AsyncRegistration.class); + + // Assertions + verify(mAsyncSourceFetcher, times(1)) + .fetchSource(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + verify(mMeasurementDao, times(1)).insertEventReport(any(EventReport.class)); + verify(mMeasurementDao, times(1)).insertSource(any(Source.class)); + verify(mMeasurementDao, times(2)).insertAsyncRegistration( + asyncRegistrationArgumentCaptor.capture()); + + // Assert 'none' type redirect and values + Assert.assertEquals(2, asyncRegistrationArgumentCaptor.getAllValues().size()); + + AsyncRegistration asyncReg1 = asyncRegistrationArgumentCaptor.getAllValues().get(0); + Assert.assertEquals(Uri.parse(LIST_TYPE_REDIRECT_URI_1), asyncReg1.getRegistrationUri()); + Assert.assertEquals(AsyncRegistration.RedirectType.NONE, asyncReg1.getRedirectType()); + Assert.assertEquals(1, asyncReg1.getRedirectCount()); + + AsyncRegistration asyncReg2 = asyncRegistrationArgumentCaptor.getAllValues().get(1); + Assert.assertEquals(Uri.parse(LIST_TYPE_REDIRECT_URI_2), asyncReg2.getRegistrationUri()); + Assert.assertEquals(AsyncRegistration.RedirectType.NONE, asyncReg2.getRedirectType()); + Assert.assertEquals(1, asyncReg2.getRedirectCount()); + + verify(mMeasurementDao, times(1)).deleteAsyncRegistration(any(String.class)); + } + + @Test + public void runAsyncRegistrationQueueWorker_appSource_defaultRegistration_redirectTypeLocation() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForAppSource(); + Answer<Optional<Source>> answerAsyncSourceFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.SUCCESS); + AsyncRedirect asyncRedirect = invocation.getArgument(2); + asyncRedirect.addToRedirects(List.of( + Uri.parse(LOCATION_TYPE_REDIRECT_URI))); + asyncRedirect.setRedirectType(AsyncRegistration.RedirectType.DAISY_CHAIN); + return Optional.of(mMockedSource); + }; + doAnswer(answerAsyncSourceFetcher) + .when(mAsyncSourceFetcher) + .fetchSource(any(), any(), any()); + + List<Source.FakeReport> eventReportList = Collections.singletonList( + new Source.FakeReport(new UnsignedLong(1L), 1L, APP_DESTINATION)); + when(mMockedSource.assignAttributionModeAndGenerateFakeReports()) + .thenReturn(eventReportList); + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + ArgumentCaptor<AsyncRegistration> asyncRegistrationArgumentCaptor = + ArgumentCaptor.forClass(AsyncRegistration.class); + + // Assertions + verify(mAsyncSourceFetcher, times(1)) + .fetchSource(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + verify(mMeasurementDao, times(1)).insertEventReport(any(EventReport.class)); + verify(mMeasurementDao, times(1)).insertSource(any(Source.class)); + verify(mMeasurementDao, times(1)).insertAsyncRegistration( + asyncRegistrationArgumentCaptor.capture()); + + // Assert 'location' type redirect and value + Assert.assertEquals(1, asyncRegistrationArgumentCaptor.getAllValues().size()); + AsyncRegistration asyncReg = asyncRegistrationArgumentCaptor.getAllValues().get(0); + Assert.assertEquals(Uri.parse(LOCATION_TYPE_REDIRECT_URI), asyncReg.getRegistrationUri()); + Assert.assertEquals(AsyncRegistration.RedirectType.DAISY_CHAIN, asyncReg.getRedirectType()); + Assert.assertEquals(1, asyncReg.getRedirectCount()); + + verify(mMeasurementDao, times(1)).deleteAsyncRegistration(any(String.class)); + } + + @Test + public void runAsyncRegistrationQueueWorker_appSource_middleRegistration_redirectTypeLocation() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForAppSource( + AsyncRegistration.RedirectType.DAISY_CHAIN, 3); + Answer<Optional<Source>> answerAsyncSourceFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.SUCCESS); + AsyncRedirect asyncRedirect = invocation.getArgument(2); + asyncRedirect.addToRedirects(List.of( + Uri.parse(LOCATION_TYPE_REDIRECT_URI))); + asyncRedirect.setRedirectType(AsyncRegistration.RedirectType.DAISY_CHAIN); + return Optional.of(mMockedSource); + }; + doAnswer(answerAsyncSourceFetcher) + .when(mAsyncSourceFetcher) + .fetchSource(any(), any(), any()); + + List<Source.FakeReport> eventReportList = Collections.singletonList( + new Source.FakeReport(new UnsignedLong(1L), 1L, APP_DESTINATION)); + when(mMockedSource.assignAttributionModeAndGenerateFakeReports()) + .thenReturn(eventReportList); + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + ArgumentCaptor<AsyncRegistration> asyncRegistrationArgumentCaptor = + ArgumentCaptor.forClass(AsyncRegistration.class); + + // Assertions + verify(mAsyncSourceFetcher, times(1)) + .fetchSource(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + verify(mMeasurementDao, times(1)).insertEventReport(any(EventReport.class)); + verify(mMeasurementDao, times(1)).insertSource(any(Source.class)); + verify(mMeasurementDao, times(1)).insertAsyncRegistration( + asyncRegistrationArgumentCaptor.capture()); + + // Assert 'location' type redirect and value + Assert.assertEquals(1, asyncRegistrationArgumentCaptor.getAllValues().size()); + AsyncRegistration asyncReg = asyncRegistrationArgumentCaptor.getAllValues().get(0); + Assert.assertEquals(Uri.parse(LOCATION_TYPE_REDIRECT_URI), asyncReg.getRegistrationUri()); + Assert.assertEquals(AsyncRegistration.RedirectType.DAISY_CHAIN, asyncReg.getRedirectType()); + Assert.assertEquals(4, asyncReg.getRedirectCount()); + + verify(mMeasurementDao, times(1)).deleteAsyncRegistration(any(String.class)); + } + + @Test + public void runAsyncRegistrationQueueWorker_appTrigger_defaultRegistration_redirectTypeList() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForAppTrigger(); + + Answer<Optional<Trigger>> answerAsyncTriggerFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.SUCCESS); + AsyncRedirect asyncRedirect = invocation.getArgument(2); + asyncRedirect.addToRedirects(List.of( + Uri.parse(LIST_TYPE_REDIRECT_URI_1), + Uri.parse(LIST_TYPE_REDIRECT_URI_2))); + asyncRedirect.setRedirectType(AsyncRegistration.RedirectType.NONE); + return Optional.of(mMockedTrigger); + }; + doAnswer(answerAsyncTriggerFetcher) + .when(mAsyncTriggerFetcher) + .fetchTrigger(any(), any(), any()); + + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + ArgumentCaptor<AsyncRegistration> asyncRegistrationArgumentCaptor = + ArgumentCaptor.forClass(AsyncRegistration.class); + + // Assertions + verify(mAsyncTriggerFetcher, times(1)) + .fetchTrigger(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + verify(mMeasurementDao, times(1)).insertTrigger(any(Trigger.class)); + verify(mMeasurementDao, times(2)).insertAsyncRegistration( + asyncRegistrationArgumentCaptor.capture()); + + // Assert 'none' type redirect and values + Assert.assertEquals(2, asyncRegistrationArgumentCaptor.getAllValues().size()); + + AsyncRegistration asyncReg1 = asyncRegistrationArgumentCaptor.getAllValues().get(0); + Assert.assertEquals(Uri.parse(LIST_TYPE_REDIRECT_URI_1), asyncReg1.getRegistrationUri()); + Assert.assertEquals(AsyncRegistration.RedirectType.NONE, asyncReg1.getRedirectType()); + Assert.assertEquals(1, asyncReg1.getRedirectCount()); + + AsyncRegistration asyncReg2 = asyncRegistrationArgumentCaptor.getAllValues().get(1); + Assert.assertEquals(Uri.parse(LIST_TYPE_REDIRECT_URI_2), asyncReg2.getRegistrationUri()); + Assert.assertEquals(AsyncRegistration.RedirectType.NONE, asyncReg2.getRedirectType()); + Assert.assertEquals(1, asyncReg2.getRedirectCount()); + + verify(mMeasurementDao, times(1)).deleteAsyncRegistration(any(String.class)); + } + + @Test + public void runAsyncRegistrationQueueWorker_appTrigger_defaultReg_redirectTypeLocation() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForAppTrigger(); + + Answer<Optional<Trigger>> answerAsyncTriggerFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.SUCCESS); + AsyncRedirect asyncRedirect = invocation.getArgument(2); + asyncRedirect.addToRedirects(List.of( + Uri.parse(LOCATION_TYPE_REDIRECT_URI))); + asyncRedirect.setRedirectType(AsyncRegistration.RedirectType.DAISY_CHAIN); + return Optional.of(mMockedTrigger); + }; + doAnswer(answerAsyncTriggerFetcher) + .when(mAsyncTriggerFetcher) + .fetchTrigger(any(), any(), any()); + + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + ArgumentCaptor<AsyncRegistration> asyncRegistrationArgumentCaptor = + ArgumentCaptor.forClass(AsyncRegistration.class); + + // Assertions + verify(mAsyncTriggerFetcher, times(1)) + .fetchTrigger(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + verify(mMeasurementDao, times(1)).insertTrigger(any(Trigger.class)); + verify(mMeasurementDao, times(1)).insertAsyncRegistration( + asyncRegistrationArgumentCaptor.capture()); + + // Assert 'location' type redirect and values + Assert.assertEquals(1, asyncRegistrationArgumentCaptor.getAllValues().size()); + + AsyncRegistration asyncReg = asyncRegistrationArgumentCaptor.getAllValues().get(0); + Assert.assertEquals(Uri.parse(LOCATION_TYPE_REDIRECT_URI), asyncReg.getRegistrationUri()); + Assert.assertEquals(AsyncRegistration.RedirectType.DAISY_CHAIN, asyncReg.getRedirectType()); + Assert.assertEquals(1, asyncReg.getRedirectCount()); + + verify(mMeasurementDao, times(1)).deleteAsyncRegistration(any(String.class)); + } + + @Test + public void runAsyncRegistrationQueueWorker_appTrigger_middleRegistration_redirectTypeLocation() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForAppTrigger( + AsyncRegistration.RedirectType.DAISY_CHAIN, 4); + + Answer<Optional<Trigger>> answerAsyncTriggerFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.SUCCESS); + AsyncRedirect asyncRedirect = invocation.getArgument(2); + asyncRedirect.addToRedirects(List.of( + Uri.parse(LOCATION_TYPE_REDIRECT_URI))); + asyncRedirect.setRedirectType(AsyncRegistration.RedirectType.DAISY_CHAIN); + return Optional.of(mMockedTrigger); + }; + doAnswer(answerAsyncTriggerFetcher) + .when(mAsyncTriggerFetcher) + .fetchTrigger(any(), any(), any()); + + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + ArgumentCaptor<AsyncRegistration> asyncRegistrationArgumentCaptor = + ArgumentCaptor.forClass(AsyncRegistration.class); + + // Assertions + verify(mAsyncTriggerFetcher, times(1)) + .fetchTrigger(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + verify(mMeasurementDao, times(1)).insertTrigger(any(Trigger.class)); + verify(mMeasurementDao, times(1)).insertAsyncRegistration( + asyncRegistrationArgumentCaptor.capture()); + + // Assert 'location' type redirect and values + Assert.assertEquals(1, asyncRegistrationArgumentCaptor.getAllValues().size()); + + AsyncRegistration asyncReg = asyncRegistrationArgumentCaptor.getAllValues().get(0); + Assert.assertEquals(Uri.parse(LOCATION_TYPE_REDIRECT_URI), asyncReg.getRegistrationUri()); + Assert.assertEquals(AsyncRegistration.RedirectType.DAISY_CHAIN, asyncReg.getRedirectType()); + Assert.assertEquals(5, asyncReg.getRedirectCount()); + + verify(mMeasurementDao, times(1)).deleteAsyncRegistration(any(String.class)); + } + + // End tests for redirect types + + @Test + public void test_runAsyncRegistrationQueueWorker_appSource_noRedirects_success() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForAppSource(); + + Answer<?> answerAsyncSourceFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.SUCCESS); + return Optional.of(mMockedSource); + }; + doAnswer(answerAsyncSourceFetcher) + .when(mAsyncSourceFetcher) + .fetchSource(any(), any(), any()); + + Source.FakeReport sf = + new Source.FakeReport( + new UnsignedLong(1L), 1L, Uri.parse("https://example.com/sF")); + List<Source.FakeReport> eventReportList = Collections.singletonList(sf); + when(mMockedSource.assignAttributionModeAndGenerateFakeReports()) + .thenReturn(eventReportList); + + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + // Assertions + verify(mAsyncSourceFetcher, times(1)) + .fetchSource(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + verify(mMeasurementDao, times(1)).insertEventReport(any(EventReport.class)); + verify(mMeasurementDao, times(1)).insertSource(any(Source.class)); + verify(mMeasurementDao, never()).insertAsyncRegistration(any(AsyncRegistration.class)); + verify(mMeasurementDao, times(1)).deleteAsyncRegistration(any(String.class)); + } + + @Test + public void test_runAsyncRegistrationQueueWorker_appSource_adTechUnavailable() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForAppSource(); + + Answer<?> answerAsyncSourceFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.SERVER_UNAVAILABLE); + AsyncRedirect asyncRedirect = invocation.getArgument(2); + asyncRedirect.addToRedirects(List.of( + Uri.parse("https://example.com/sF1"), + Uri.parse("https://example.com/sF2"))); + return Optional.empty(); + }; + doAnswer(answerAsyncSourceFetcher) + .when(mAsyncSourceFetcher) + .fetchSource(any(), any(), any()); + + Source.FakeReport sf = + new Source.FakeReport( + new UnsignedLong(1L), 1L, Uri.parse("https://example.com/sF")); + List<Source.FakeReport> eventReportList = Collections.singletonList(sf); + when(mMockedSource.assignAttributionModeAndGenerateFakeReports()) + .thenReturn(eventReportList); + + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + // Assertions + verify(mAsyncSourceFetcher, times(1)) + .fetchSource(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + ArgumentCaptor<AsyncRegistration> asyncRegistrationArgumentCaptor = + ArgumentCaptor.forClass(AsyncRegistration.class); + verify(mMeasurementDao, times(1)) + .updateRetryCount(asyncRegistrationArgumentCaptor.capture()); + Assert.assertEquals(1, asyncRegistrationArgumentCaptor.getAllValues().size()); + Assert.assertEquals( + 1, asyncRegistrationArgumentCaptor.getAllValues().get(0).getRetryCount()); + + verify(mMeasurementDao, never()).insertEventReport(any(EventReport.class)); + verify(mMeasurementDao, never()).insertSource(any(Source.class)); + verify(mMeasurementDao, never()).insertAsyncRegistration(any(AsyncRegistration.class)); + verify(mMeasurementDao, never()).deleteAsyncRegistration(any(String.class)); + } + + @Test + public void test_runAsyncRegistrationQueueWorker_appSource_NetworkError() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForAppSource(); + + Answer<?> answerAsyncSourceFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.NETWORK_ERROR); + AsyncRedirect asyncRedirect = invocation.getArgument(2); + asyncRedirect.addToRedirects(List.of( + Uri.parse("https://example.com/sF1"), + Uri.parse("https://example.com/sF2"))); + return Optional.empty(); + }; + doAnswer(answerAsyncSourceFetcher) + .when(mAsyncSourceFetcher) + .fetchSource(any(), any(), any()); + + Source.FakeReport sf = + new Source.FakeReport( + new UnsignedLong(1L), 1L, Uri.parse("https://example.com/sF")); + List<Source.FakeReport> eventReportList = Collections.singletonList(sf); + when(mMockedSource.assignAttributionModeAndGenerateFakeReports()) + .thenReturn(eventReportList); + + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + // Assertions + verify(mAsyncSourceFetcher, times(1)) + .fetchSource(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + + ArgumentCaptor<AsyncRegistration> asyncRegistrationArgumentCaptor = + ArgumentCaptor.forClass(AsyncRegistration.class); + verify(mMeasurementDao, times(1)) + .updateRetryCount(asyncRegistrationArgumentCaptor.capture()); + Assert.assertEquals(1, asyncRegistrationArgumentCaptor.getAllValues().size()); + Assert.assertEquals( + 1, asyncRegistrationArgumentCaptor.getAllValues().get(0).getRetryCount()); + + verify(mMeasurementDao, never()).insertEventReport(any(EventReport.class)); + verify(mMeasurementDao, never()).insertSource(any(Source.class)); + verify(mMeasurementDao, never()).insertAsyncRegistration(any(AsyncRegistration.class)); + verify(mMeasurementDao, never()).deleteAsyncRegistration(any(String.class)); + } + + @Test + public void test_runAsyncRegistrationQueueWorker_appSource_parsingError() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForAppSource(); + + Answer<?> answerAsyncSourceFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.PARSING_ERROR); + return Optional.empty(); + }; + doAnswer(answerAsyncSourceFetcher) + .when(mAsyncSourceFetcher) + .fetchSource(any(), any(), any()); + + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + // Assertions + verify(mAsyncSourceFetcher, times(1)) + .fetchSource(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + verify(mMeasurementDao, never()).updateRetryCount(any()); + verify(mMeasurementDao, never()).insertEventReport(any(EventReport.class)); + verify(mMeasurementDao, never()).insertSource(any(Source.class)); + verify(mMeasurementDao, never()).insertAsyncRegistration(any(AsyncRegistration.class)); + verify(mMeasurementDao, times(1)).deleteAsyncRegistration(any(String.class)); + } + + @Test + public void test_runAsyncRegistrationQueueWorker_appTrigger_success() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForAppTrigger(); + + Answer<?> answerAsyncTriggerFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.SUCCESS); + AsyncRedirect asyncRedirect = invocation.getArgument(2); + asyncRedirect.addToRedirects(List.of( + Uri.parse("https://example.com/sF1"), + Uri.parse("https://example.com/sF2"))); + return Optional.of(mMockedTrigger); + }; + doAnswer(answerAsyncTriggerFetcher) + .when(mAsyncTriggerFetcher) + .fetchTrigger(any(), any(), any()); + + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + // Assertions + verify(mAsyncTriggerFetcher, times(1)) + .fetchTrigger(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + verify(mMeasurementDao, times(1)).insertTrigger(any(Trigger.class)); + verify(mMeasurementDao, times(2)).insertAsyncRegistration(any(AsyncRegistration.class)); + verify(mMeasurementDao, times(1)).deleteAsyncRegistration(any(String.class)); + } + + @Test + public void test_runAsyncRegistrationQueueWorker_appTrigger_noRedirects_success() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForAppTrigger(); + + Answer<?> answerAsyncTriggerFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.SUCCESS); + return Optional.of(mMockedTrigger); + }; + doAnswer(answerAsyncTriggerFetcher) + .when(mAsyncTriggerFetcher) + .fetchTrigger(any(), any(), any()); + + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + // Assertions + verify(mAsyncTriggerFetcher, times(1)) + .fetchTrigger(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + verify(mMeasurementDao, times(1)).insertTrigger(any(Trigger.class)); + verify(mMeasurementDao, never()).insertAsyncRegistration(any(AsyncRegistration.class)); + verify(mMeasurementDao, times(1)).deleteAsyncRegistration(any(String.class)); + } + + @Test + public void test_runAsyncRegistrationQueueWorker_appTrigger_adTechUnavailable() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForAppTrigger(); + + Answer<?> answerAsyncTriggerFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.SERVER_UNAVAILABLE); + AsyncRedirect asyncRedirect = invocation.getArgument(2); + asyncRedirect.addToRedirects(List.of( + Uri.parse("https://example.com/sF1"), + Uri.parse("https://example.com/sF2"))); + return Optional.of(mMockedSource); + }; + doAnswer(answerAsyncTriggerFetcher) + .when(mAsyncTriggerFetcher) + .fetchTrigger(any(), any(), any()); + + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + // Assertions + verify(mAsyncTriggerFetcher, times(1)) + .fetchTrigger(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + ArgumentCaptor<AsyncRegistration> asyncRegistrationArgumentCaptor = + ArgumentCaptor.forClass(AsyncRegistration.class); + verify(mMeasurementDao, times(1)) + .updateRetryCount(asyncRegistrationArgumentCaptor.capture()); + Assert.assertEquals(1, asyncRegistrationArgumentCaptor.getAllValues().size()); + Assert.assertEquals( + 1, asyncRegistrationArgumentCaptor.getAllValues().get(0).getRetryCount()); + verify(mMeasurementDao, never()).insertTrigger(any(Trigger.class)); + verify(mMeasurementDao, never()).insertAsyncRegistration(any(AsyncRegistration.class)); + verify(mMeasurementDao, never()).deleteAsyncRegistration(any(String.class)); + } + + @Test + public void test_runAsyncRegistrationQueueWorker_appTrigger_networkError() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForAppTrigger(); + + Answer<?> answerAsyncTriggerFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.NETWORK_ERROR); + AsyncRedirect asyncRedirect = invocation.getArgument(2); + asyncRedirect.addToRedirects(List.of( + Uri.parse("https://example.com/sF1"), + Uri.parse("https://example.com/sF2"))); + return Optional.of(mMockedSource); + }; + doAnswer(answerAsyncTriggerFetcher) + .when(mAsyncTriggerFetcher) + .fetchTrigger(any(), any(), any()); + + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + // Assertions + verify(mAsyncTriggerFetcher, times(1)) + .fetchTrigger(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + ArgumentCaptor<AsyncRegistration> asyncRegistrationArgumentCaptor = + ArgumentCaptor.forClass(AsyncRegistration.class); + verify(mMeasurementDao, times(1)) + .updateRetryCount(asyncRegistrationArgumentCaptor.capture()); + Assert.assertEquals(1, asyncRegistrationArgumentCaptor.getAllValues().size()); + Assert.assertEquals( + 1, asyncRegistrationArgumentCaptor.getAllValues().get(0).getRetryCount()); + verify(mMeasurementDao, never()).insertTrigger(any(Trigger.class)); + verify(mMeasurementDao, never()).insertAsyncRegistration(any(AsyncRegistration.class)); + verify(mMeasurementDao, never()).deleteAsyncRegistration(any(String.class)); + } + + @Test + public void test_runAsyncRegistrationQueueWorker_appTrigger_parsingError() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForAppTrigger(); + + Answer<?> answerAsyncTriggerFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.PARSING_ERROR); + AsyncRedirect asyncRedirect = invocation.getArgument(2); + asyncRedirect.addToRedirects(List.of( + Uri.parse("https://example.com/sF1"), + Uri.parse("https://example.com/sF2"))); + return Optional.empty(); + }; + doAnswer(answerAsyncTriggerFetcher) + .when(mAsyncTriggerFetcher) + .fetchTrigger(any(), any(), any()); + + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + // Assertions + verify(mAsyncTriggerFetcher, times(1)) + .fetchTrigger(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + verify(mMeasurementDao, never()).updateRetryCount(any()); + verify(mMeasurementDao, never()).insertTrigger(any(Trigger.class)); + verify(mMeasurementDao, never()).insertAsyncRegistration(any(AsyncRegistration.class)); + verify(mMeasurementDao, times(1)).deleteAsyncRegistration(any(String.class)); + } + + @Test + public void test_runAsyncRegistrationQueueWorker_webSource_success() throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForWebSource(); + + Answer<?> answerAsyncSourceFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.SUCCESS); + return Optional.of(mMockedSource); + }; + doAnswer(answerAsyncSourceFetcher) + .when(mAsyncSourceFetcher) + .fetchSource(any(), any(), any()); + + Source.FakeReport sf = + new Source.FakeReport( + new UnsignedLong(1L), 1L, Uri.parse("https://example.com/sF")); + List<Source.FakeReport> eventReportList = Collections.singletonList(sf); + when(mMockedSource.assignAttributionModeAndGenerateFakeReports()) + .thenReturn(eventReportList); + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + // Assertions + verify(mAsyncSourceFetcher, times(1)) + .fetchSource(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + verify(mMeasurementDao, times(1)).insertEventReport(any(EventReport.class)); + verify(mMeasurementDao, times(1)).insertSource(any(Source.class)); + verify(mMeasurementDao, never()).insertAsyncRegistration(any(AsyncRegistration.class)); + verify(mMeasurementDao, times(1)).deleteAsyncRegistration(any(String.class)); + } + + @Test + public void test_runAsyncRegistrationQueueWorker_webSource_adTechUnavailable() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForWebSource(); + + Answer<?> answerAsyncSourceFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.SERVER_UNAVAILABLE); + return Optional.empty(); + }; + doAnswer(answerAsyncSourceFetcher) + .when(mAsyncSourceFetcher) + .fetchSource(any(), any(), any()); + + Source.FakeReport sf = + new Source.FakeReport( + new UnsignedLong(1L), 1L, Uri.parse("https://example.com/sF")); + List<Source.FakeReport> eventReportList = Collections.singletonList(sf); + when(mMockedSource.assignAttributionModeAndGenerateFakeReports()) + .thenReturn(eventReportList); + + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + // Assertions + verify(mAsyncSourceFetcher, times(1)) + .fetchSource(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + ArgumentCaptor<AsyncRegistration> asyncRegistrationArgumentCaptor = + ArgumentCaptor.forClass(AsyncRegistration.class); + verify(mMeasurementDao, times(1)) + .updateRetryCount(asyncRegistrationArgumentCaptor.capture()); + Assert.assertEquals(1, asyncRegistrationArgumentCaptor.getAllValues().size()); + Assert.assertEquals( + 1, asyncRegistrationArgumentCaptor.getAllValues().get(0).getRetryCount()); + verify(mMeasurementDao, never()).insertEventReport(any(EventReport.class)); + verify(mMeasurementDao, never()).insertSource(any(Source.class)); + verify(mMeasurementDao, never()).insertAsyncRegistration(any(AsyncRegistration.class)); + verify(mMeasurementDao, never()).deleteAsyncRegistration(any(String.class)); + } + + @Test + public void test_runAsyncRegistrationQueueWorker_webSource_NetworkError() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForWebSource(); + + Answer<?> answerAsyncSourceFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.NETWORK_ERROR); + return Optional.empty(); + }; + doAnswer(answerAsyncSourceFetcher) + .when(mAsyncSourceFetcher) + .fetchSource(any(), any(), any()); + + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + // Assertions + verify(mAsyncSourceFetcher, times(1)) + .fetchSource(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + ArgumentCaptor<AsyncRegistration> asyncRegistrationArgumentCaptor = + ArgumentCaptor.forClass(AsyncRegistration.class); + verify(mMeasurementDao, times(1)) + .updateRetryCount(asyncRegistrationArgumentCaptor.capture()); + Assert.assertEquals(1, asyncRegistrationArgumentCaptor.getAllValues().size()); + Assert.assertEquals( + 1, asyncRegistrationArgumentCaptor.getAllValues().get(0).getRetryCount()); + verify(mMeasurementDao, never()).insertEventReport(any(EventReport.class)); + verify(mMeasurementDao, never()).insertSource(any(Source.class)); + verify(mMeasurementDao, never()).insertAsyncRegistration(any(AsyncRegistration.class)); + verify(mMeasurementDao, never()).deleteAsyncRegistration(any(String.class)); + } + + @Test + public void test_runAsyncRegistrationQueueWorker_webSource_parsingError() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForWebSource(); + + Answer<?> answerAsyncSourceFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.PARSING_ERROR); + return Optional.empty(); + }; + doAnswer(answerAsyncSourceFetcher) + .when(mAsyncSourceFetcher) + .fetchSource(any(), any(), any()); + + Source.FakeReport sf = + new Source.FakeReport( + new UnsignedLong(1L), 1L, Uri.parse("https://example.com/sF")); + List<Source.FakeReport> eventReportList = Collections.singletonList(sf); + when(mMockedSource.assignAttributionModeAndGenerateFakeReports()) + .thenReturn(eventReportList); + + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + // Assertions + verify(mAsyncSourceFetcher, times(1)) + .fetchSource(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + verify(mMeasurementDao, never()).updateRetryCount(any()); + verify(mMeasurementDao, never()).insertEventReport(any(EventReport.class)); + verify(mMeasurementDao, never()).insertSource(any(Source.class)); + verify(mMeasurementDao, never()).insertAsyncRegistration(any(AsyncRegistration.class)); + verify(mMeasurementDao, times(1)).deleteAsyncRegistration(any(String.class)); + } + + @Test + public void test_runAsyncRegistrationQueueWorker_webTrigger_success() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForWebTrigger(); + + Answer<?> answerAsyncTriggerFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.SUCCESS); + return Optional.of(mMockedTrigger); + }; + doAnswer(answerAsyncTriggerFetcher) + .when(mAsyncTriggerFetcher) + .fetchTrigger(any(), any(), any()); + + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + // Assertions + verify(mAsyncTriggerFetcher, times(1)) + .fetchTrigger(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + verify(mMeasurementDao, times(1)).insertTrigger(any(Trigger.class)); + verify(mMeasurementDao, never()).insertAsyncRegistration(any(AsyncRegistration.class)); + verify(mMeasurementDao, times(1)).deleteAsyncRegistration(any(String.class)); + } + + @Test + public void test_runAsyncRegistrationQueueWorker_webTrigger_adTechUnavailable() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForWebTrigger(); + + Answer<?> answerAsyncTriggerFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.SERVER_UNAVAILABLE); + return Optional.empty(); + }; + doAnswer(answerAsyncTriggerFetcher) + .when(mAsyncTriggerFetcher) + .fetchTrigger(any(), any(), any()); + + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + // Assertions + verify(mAsyncTriggerFetcher, times(1)) + .fetchTrigger(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + ArgumentCaptor<AsyncRegistration> asyncRegistrationArgumentCaptor = + ArgumentCaptor.forClass(AsyncRegistration.class); + verify(mMeasurementDao, times(1)) + .updateRetryCount(asyncRegistrationArgumentCaptor.capture()); + Assert.assertEquals(1, asyncRegistrationArgumentCaptor.getAllValues().size()); + Assert.assertEquals( + 1, asyncRegistrationArgumentCaptor.getAllValues().get(0).getRetryCount()); + verify(mMeasurementDao, never()).insertTrigger(any(Trigger.class)); + verify(mMeasurementDao, never()).insertAsyncRegistration(any(AsyncRegistration.class)); + verify(mMeasurementDao, never()).deleteAsyncRegistration(any(String.class)); + } + + @Test + public void test_runAsyncRegistrationQueueWorker_webTrigger_networkError() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForWebTrigger(); + + Answer<?> answerAsyncTriggerFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.NETWORK_ERROR); + return Optional.empty(); + }; + doAnswer(answerAsyncTriggerFetcher) + .when(mAsyncTriggerFetcher) + .fetchTrigger(any(), any(), any()); + + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + // Assertions + verify(mAsyncTriggerFetcher, times(1)) + .fetchTrigger(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + ArgumentCaptor<AsyncRegistration> asyncRegistrationArgumentCaptor = + ArgumentCaptor.forClass(AsyncRegistration.class); + verify(mMeasurementDao, times(1)) + .updateRetryCount(asyncRegistrationArgumentCaptor.capture()); + Assert.assertEquals(1, asyncRegistrationArgumentCaptor.getAllValues().size()); + Assert.assertEquals( + 1, asyncRegistrationArgumentCaptor.getAllValues().get(0).getRetryCount()); + verify(mMeasurementDao, never()).insertTrigger(any(Trigger.class)); + verify(mMeasurementDao, never()).deleteAsyncRegistration(any(String.class)); + verify(mMeasurementDao, never()).insertAsyncRegistration(any(AsyncRegistration.class)); + } + + @Test + public void test_runAsyncRegistrationQueueWorker_webTrigger_parsingError() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + AsyncRegistration validAsyncRegistration = createAsyncRegistrationForWebTrigger(); + + Answer<?> answerAsyncTriggerFetcher = + invocation -> { + AsyncFetchStatus asyncFetchStatus = invocation.getArgument(1); + asyncFetchStatus.setStatus(AsyncFetchStatus.ResponseStatus.PARSING_ERROR); + return Optional.empty(); + }; + doAnswer(answerAsyncTriggerFetcher) + .when(mAsyncTriggerFetcher) + .fetchTrigger(any(), any(), any()); + + when(mMeasurementDao.fetchNextQueuedAsyncRegistration(anyShort(), any())) + .thenReturn(validAsyncRegistration); + + // Execution + asyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker(1L, (short) 5); + + // Assertions + verify(mAsyncTriggerFetcher, times(1)) + .fetchTrigger(any(AsyncRegistration.class), any(AsyncFetchStatus.class), any()); + verify(mMeasurementDao, never()).updateRetryCount(any()); + verify(mMeasurementDao, never()).insertTrigger(any(Trigger.class)); + verify(mMeasurementDao, never()).insertAsyncRegistration(any(AsyncRegistration.class)); + verify(mMeasurementDao, times(1)).deleteAsyncRegistration(any(String.class)); + } + + @Test + public void insertSource_withFakeReportsFalseAppAttribution_accountsForFakeReportAttribution() + throws DatastoreException { + // Setup + int fakeReportsCount = 2; + Source source = + spy( + SourceFixture.getValidSourceBuilder() + .setAppDestination( + SourceFixture.ValidSourceParams.ATTRIBUTION_DESTINATION) + .setWebDestination(null) + .build()); + List<Source.FakeReport> fakeReports = + createFakeReports( + source, + fakeReportsCount, + SourceFixture.ValidSourceParams.ATTRIBUTION_DESTINATION); + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + Answer<?> falseAttributionAnswer = + (arg) -> { + source.setAttributionMode(Source.AttributionMode.FALSELY); + return fakeReports; + }; + doAnswer(falseAttributionAnswer).when(source).assignAttributionModeAndGenerateFakeReports(); + ArgumentCaptor<Attribution> attributionRateLimitArgCaptor = + ArgumentCaptor.forClass(Attribution.class); + + // Execution + asyncRegistrationQueueRunner.insertSourcesFromTransaction(source, mMeasurementDao); + + // Assertion + verify(mMeasurementDao).insertSource(source); + verify(mMeasurementDao, times(2)).insertEventReport(any()); + verify(mMeasurementDao).insertAttribution(attributionRateLimitArgCaptor.capture()); + + assertEquals( + new Attribution.Builder() + .setDestinationOrigin(source.getAppDestination().toString()) + .setDestinationSite(source.getAppDestination().toString()) + .setEnrollmentId(source.getEnrollmentId()) + .setSourceOrigin(source.getPublisher().toString()) + .setSourceSite(source.getPublisher().toString()) + .setRegistrant(source.getRegistrant().toString()) + .setTriggerTime(source.getEventTime()) + .build(), + attributionRateLimitArgCaptor.getValue()); + } + + @Test + public void insertSource_withFakeReportsFalseWebAttribution_accountsForFakeReportAttribution() + throws DatastoreException { + // Setup + int fakeReportsCount = 2; + Source source = + spy( + SourceFixture.getValidSourceBuilder() + .setAppDestination(null) + .setWebDestination(SourceFixture.ValidSourceParams.WEB_DESTINATION) + .build()); + List<Source.FakeReport> fakeReports = + createFakeReports( + source, fakeReportsCount, SourceFixture.ValidSourceParams.WEB_DESTINATION); + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + Answer<?> falseAttributionAnswer = + (arg) -> { + source.setAttributionMode(Source.AttributionMode.FALSELY); + return fakeReports; + }; + doAnswer(falseAttributionAnswer).when(source).assignAttributionModeAndGenerateFakeReports(); + ArgumentCaptor<Attribution> attributionRateLimitArgCaptor = + ArgumentCaptor.forClass(Attribution.class); + + // Execution + asyncRegistrationQueueRunner.insertSourcesFromTransaction(source, mMeasurementDao); + + // Assertion + verify(mMeasurementDao).insertSource(source); + verify(mMeasurementDao, times(2)).insertEventReport(any()); + verify(mMeasurementDao).insertAttribution(attributionRateLimitArgCaptor.capture()); + + assertEquals( + new Attribution.Builder() + .setDestinationOrigin(source.getWebDestination().toString()) + .setDestinationSite(source.getWebDestination().toString()) + .setEnrollmentId(source.getEnrollmentId()) + .setSourceOrigin(source.getPublisher().toString()) + .setSourceSite(source.getPublisher().toString()) + .setRegistrant(source.getRegistrant().toString()) + .setTriggerTime(source.getEventTime()) + .build(), + attributionRateLimitArgCaptor.getValue()); + } + + @Test + public void insertSource_withFalseAppAndWebAttribution_accountsForFakeReportAttribution() + throws DatastoreException { + // Setup + int fakeReportsCount = 2; + Source source = + spy( + SourceFixture.getValidSourceBuilder() + .setAppDestination( + SourceFixture.ValidSourceParams.ATTRIBUTION_DESTINATION) + .setWebDestination(SourceFixture.ValidSourceParams.WEB_DESTINATION) + .build()); + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + + List<Source.FakeReport> fakeReports = + createFakeReports( + source, + fakeReportsCount, + SourceFixture.ValidSourceParams.ATTRIBUTION_DESTINATION); + + Answer<?> falseAttributionAnswer = + (arg) -> { + source.setAttributionMode(Source.AttributionMode.FALSELY); + return fakeReports; + }; + ArgumentCaptor<Attribution> attributionRateLimitArgCaptor = + ArgumentCaptor.forClass(Attribution.class); + doAnswer(falseAttributionAnswer).when(source).assignAttributionModeAndGenerateFakeReports(); + + // Execution + asyncRegistrationQueueRunner.insertSourcesFromTransaction(source, mMeasurementDao); + + // Assertion + verify(mMeasurementDao).insertSource(source); + verify(mMeasurementDao, times(2)).insertEventReport(any()); + verify(mMeasurementDao, times(2)) + .insertAttribution(attributionRateLimitArgCaptor.capture()); + assertEquals( + new Attribution.Builder() + .setDestinationOrigin(source.getAppDestination().toString()) + .setDestinationSite(source.getAppDestination().toString()) + .setEnrollmentId(source.getEnrollmentId()) + .setSourceOrigin(source.getPublisher().toString()) + .setSourceSite(source.getPublisher().toString()) + .setRegistrant(source.getRegistrant().toString()) + .setTriggerTime(source.getEventTime()) + .build(), + attributionRateLimitArgCaptor.getAllValues().get(0)); + + assertEquals( + new Attribution.Builder() + .setDestinationOrigin(source.getWebDestination().toString()) + .setDestinationSite(source.getWebDestination().toString()) + .setEnrollmentId(source.getEnrollmentId()) + .setSourceOrigin(source.getPublisher().toString()) + .setSourceSite(source.getPublisher().toString()) + .setRegistrant(source.getRegistrant().toString()) + .setTriggerTime(source.getEventTime()) + .build(), + attributionRateLimitArgCaptor.getAllValues().get(1)); + } + + @Test + public void insertSource_withFakeReportsNeverAppAttribution_accountsForFakeReportAttribution() + throws DatastoreException { + // Setup + Source source = + spy( + SourceFixture.getValidSourceBuilder() + .setAppDestination( + SourceFixture.ValidSourceParams.ATTRIBUTION_DESTINATION) + .setWebDestination(null) + .build()); + List<Source.FakeReport> fakeReports = Collections.emptyList(); + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + getSpyAsyncRegistrationQueueRunner(); + Answer<?> neverAttributionAnswer = + (arg) -> { + source.setAttributionMode(Source.AttributionMode.NEVER); + return fakeReports; + }; + doAnswer(neverAttributionAnswer).when(source).assignAttributionModeAndGenerateFakeReports(); + ArgumentCaptor<Attribution> attributionRateLimitArgCaptor = + ArgumentCaptor.forClass(Attribution.class); + + // Execution + asyncRegistrationQueueRunner.insertSourcesFromTransaction(source, mMeasurementDao); + + // Assertion + verify(mMeasurementDao).insertSource(source); + verify(mMeasurementDao, never()).insertEventReport(any()); + verify(mMeasurementDao).insertAttribution(attributionRateLimitArgCaptor.capture()); + + assertEquals( + new Attribution.Builder() + .setDestinationOrigin(source.getAppDestination().toString()) + .setDestinationSite(source.getAppDestination().toString()) + .setEnrollmentId(source.getEnrollmentId()) + .setSourceOrigin(source.getPublisher().toString()) + .setSourceSite(source.getPublisher().toString()) + .setRegistrant(source.getRegistrant().toString()) + .setTriggerTime(source.getEventTime()) + .build(), + attributionRateLimitArgCaptor.getValue()); + } + + @Test + public void testRegister_registrationTypeSource_sourceFetchSuccess() throws DatastoreException { + // setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + spy( + new AsyncRegistrationQueueRunner( + mContentResolver, + mAsyncSourceFetcher, + mAsyncTriggerFetcher, + mEnrollmentDao, + new FakeDatastoreManager())); + + // Execution + when(mMeasurementDao.countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( + any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong())) + .thenReturn(Integer.valueOf(0)); + when(mMeasurementDao.countDistinctEnrollmentsPerPublisherXDestinationInSource( + any(), anyInt(), any(), any(), anyLong(), anyLong())) + .thenReturn(Integer.valueOf(0)); + asyncRegistrationQueueRunner.isSourceAllowedToInsert( + SOURCE_1, SOURCE_1.getPublisher(), EventSurfaceType.APP, mMeasurementDao); + + // Assertions + verify(mMeasurementDao, times(2)) + .countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( + any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong()); + verify(mMeasurementDao, times(2)) + .countDistinctEnrollmentsPerPublisherXDestinationInSource( + any(), anyInt(), any(), any(), anyLong(), anyLong()); + } + + @Test + public void testRegister_registrationTypeSource_exceedsPrivacyParam_destination() + throws DatastoreException { + // setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + spy( + new AsyncRegistrationQueueRunner( + mContentResolver, + mAsyncSourceFetcher, + mAsyncTriggerFetcher, + mEnrollmentDao, + new FakeDatastoreManager())); + + // Execution + when(mMeasurementDao.countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( + any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong())) + .thenReturn(Integer.valueOf(100)); + when(mMeasurementDao.countDistinctEnrollmentsPerPublisherXDestinationInSource( + any(), anyInt(), any(), any(), anyLong(), anyLong())) + .thenReturn(Integer.valueOf(0)); + boolean status = + asyncRegistrationQueueRunner.isSourceAllowedToInsert( + SOURCE_1, SOURCE_1.getPublisher(), EventSurfaceType.APP, mMeasurementDao); + + // Assert + assertFalse(status); + verify(mMeasurementDao, times(1)) + .countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( + any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong()); + verify(mMeasurementDao, never()) + .countDistinctEnrollmentsPerPublisherXDestinationInSource( + any(), anyInt(), any(), any(), anyLong(), anyLong()); + } + + @Test + public void testRegister_registrationTypeSource_exceedsMaxSourcesLimit() + throws DatastoreException { + // setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + spy( + new AsyncRegistrationQueueRunner( + mContentResolver, + mAsyncSourceFetcher, + mAsyncTriggerFetcher, + mEnrollmentDao, + new FakeDatastoreManager())); + + // Execution + doReturn(SystemHealthParams.MAX_SOURCES_PER_PUBLISHER) + .when(mMeasurementDao) + .getNumSourcesPerPublisher(any(), anyInt()); + boolean status = + asyncRegistrationQueueRunner.isSourceAllowedToInsert( + SOURCE_1, SOURCE_1.getPublisher(), EventSurfaceType.APP, mMeasurementDao); + + // Assert + assertFalse(status); + verify(mMeasurementDao, times(1)).getNumSourcesPerPublisher(any(), anyInt()); + } + + @Test + public void testRegister_registrationTypeSource_exceedsPrivacyParam_adTech() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + spy( + new AsyncRegistrationQueueRunner( + mContentResolver, + mAsyncSourceFetcher, + mAsyncTriggerFetcher, + mEnrollmentDao, + new FakeDatastoreManager())); + // Execution + when(mMeasurementDao.countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( + any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong())) + .thenReturn(Integer.valueOf(0)); + when(mMeasurementDao.countDistinctEnrollmentsPerPublisherXDestinationInSource( + any(), anyInt(), any(), any(), anyLong(), anyLong())) + .thenReturn(Integer.valueOf(100)); + boolean status = + asyncRegistrationQueueRunner.isSourceAllowedToInsert( + SOURCE_1, SOURCE_1.getPublisher(), EventSurfaceType.APP, mMeasurementDao); + + // Assert + assertFalse(status); + verify(mMeasurementDao, times(1)) + .countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( + any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong()); + verify(mMeasurementDao, times(1)) + .countDistinctEnrollmentsPerPublisherXDestinationInSource( + any(), anyInt(), any(), any(), anyLong(), anyLong()); + } + + @Test + public void testRegisterWebSource_exceedsPrivacyParam_destination() + throws RemoteException, DatastoreException { + // setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + spy( + new AsyncRegistrationQueueRunner( + mContentResolver, + mAsyncSourceFetcher, + mAsyncTriggerFetcher, + mEnrollmentDao, + new FakeDatastoreManager())); + + // Execution + when(mMeasurementDao.countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( + any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong())) + .thenReturn(Integer.valueOf(100)); + when(mMeasurementDao.countDistinctEnrollmentsPerPublisherXDestinationInSource( + any(), anyInt(), any(), any(), anyLong(), anyLong())) + .thenReturn(Integer.valueOf(0)); + boolean status = + asyncRegistrationQueueRunner.isSourceAllowedToInsert( + SOURCE_1, SOURCE_1.getPublisher(), EventSurfaceType.APP, mMeasurementDao); + + // Assert + assertFalse(status); + verify(mMockContentProviderClient, never()).insert(any(), any()); + verify(mMeasurementDao, times(1)) + .countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( + any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong()); + verify(mMeasurementDao, never()) + .countDistinctEnrollmentsPerPublisherXDestinationInSource( + any(), anyInt(), any(), any(), anyLong(), anyLong()); + } + + @Test + public void testRegisterWebSource_exceedsPrivacyParam_adTech() throws DatastoreException { + // setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + spy( + new AsyncRegistrationQueueRunner( + mContentResolver, + mAsyncSourceFetcher, + mAsyncTriggerFetcher, + mEnrollmentDao, + new FakeDatastoreManager())); + + // Execution + when(mMeasurementDao.countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( + any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong())) + .thenReturn(Integer.valueOf(0)); + when(mMeasurementDao.countDistinctEnrollmentsPerPublisherXDestinationInSource( + any(), anyInt(), any(), any(), anyLong(), anyLong())) + .thenReturn(Integer.valueOf(100)); + + boolean status = + asyncRegistrationQueueRunner.isSourceAllowedToInsert( + SOURCE_1, SOURCE_1.getPublisher(), EventSurfaceType.APP, mMeasurementDao); + + // Assert + assertFalse(status); + verify(mMeasurementDao, times(1)) + .countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( + any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong()); + verify(mMeasurementDao, times(1)) + .countDistinctEnrollmentsPerPublisherXDestinationInSource( + any(), anyInt(), any(), any(), anyLong(), anyLong()); + } + + @Test + public void testRegisterWebSource_exceedsMaxSourcesLimit() throws DatastoreException { + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + spy( + new AsyncRegistrationQueueRunner( + mContentResolver, + mAsyncSourceFetcher, + mAsyncTriggerFetcher, + mEnrollmentDao, + new FakeDatastoreManager())); + doReturn(SystemHealthParams.MAX_SOURCES_PER_PUBLISHER) + .when(mMeasurementDao) + .getNumSourcesPerPublisher(any(), anyInt()); + + // Execution + boolean status = + asyncRegistrationQueueRunner.isSourceAllowedToInsert( + SOURCE_1, SOURCE_1.getPublisher(), EventSurfaceType.APP, mMeasurementDao); + + // Assertions + assertFalse(status); + } + + @Test + public void testRegisterWebSource_LimitsMaxSources_ForWebPublisher_WitheTLDMatch() + throws DatastoreException { + // Setup + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = + spy( + new AsyncRegistrationQueueRunner( + mContentResolver, + mAsyncSourceFetcher, + mAsyncTriggerFetcher, + mEnrollmentDao, + new FakeDatastoreManager())); + + doReturn(SystemHealthParams.MAX_SOURCES_PER_PUBLISHER) + .when(mMeasurementDao) + .getNumSourcesPerPublisher(any(), anyInt()); + + // Execution + boolean status = + asyncRegistrationQueueRunner.isSourceAllowedToInsert( + SOURCE_1, SOURCE_1.getPublisher(), EventSurfaceType.APP, mMeasurementDao); + + // Assertions + assertFalse(status); + } + + private List<Source.FakeReport> createFakeReports(Source source, int count, Uri destination) { + return IntStream.range(0, count) + .mapToObj( + x -> + new Source.FakeReport( + new UnsignedLong(0L), + source.getReportingTimeForNoising(0), + destination)) + .collect(Collectors.toList()); + } + + private static AsyncRegistration createAsyncRegistrationForAppSource() { + return createAsyncRegistrationForAppSource(AsyncRegistration.RedirectType.ANY, 0); + } + + private static AsyncRegistration createAsyncRegistrationForAppSource( + @AsyncRegistration.RedirectType int redirectType, int redirectCount) { + return new AsyncRegistration.Builder() + .setId(UUID.randomUUID().toString()) + .setEnrollmentId(DEFAULT_ENROLLMENT_ID) + .setRegistrationUri(REGISTRATION_URI) + // null .setWebDestination(webDestination) + // null .setOsDestination(osDestination) + .setRegistrant(DEFAULT_REGISTRANT) + // null .setVerifiedDestination(null) + .setTopOrigin(APP_TOP_ORIGIN) + .setType(AsyncRegistration.RegistrationType.APP_SOURCE.ordinal()) + .setSourceType(Source.SourceType.EVENT) + .setRequestTime(System.currentTimeMillis()) + .setRetryCount(0) + .setLastProcessingTime(System.currentTimeMillis()) + .setRedirectType(redirectType) + .setRedirectCount(redirectCount) + .setDebugKeyAllowed(true) + .build(); + } + + private static AsyncRegistration createAsyncRegistrationForAppTrigger() { + return createAsyncRegistrationForAppTrigger(AsyncRegistration.RedirectType.ANY, 0); + } + + private static AsyncRegistration createAsyncRegistrationForAppTrigger( + @AsyncRegistration.RedirectType int redirectType, int redirectCount) { + return new AsyncRegistration.Builder() + .setId(UUID.randomUUID().toString()) + .setEnrollmentId(DEFAULT_ENROLLMENT_ID) + .setRegistrationUri(REGISTRATION_URI) + // null .setWebDestination(webDestination) + // null .setOsDestination(osDestination) + .setRegistrant(DEFAULT_REGISTRANT) + // null .setVerifiedDestination(null) + .setTopOrigin(APP_TOP_ORIGIN) + .setType(AsyncRegistration.RegistrationType.APP_TRIGGER.ordinal()) + // null .setSourceType(null) + .setRequestTime(System.currentTimeMillis()) + .setRetryCount(0) + .setLastProcessingTime(System.currentTimeMillis()) + .setRedirectType(redirectType) + .setRedirectCount(redirectCount) + .setDebugKeyAllowed(true) + .build(); + } + + private static AsyncRegistration createAsyncRegistrationForWebSource() { + return new AsyncRegistration.Builder() + .setId(UUID.randomUUID().toString()) + .setEnrollmentId(DEFAULT_ENROLLMENT_ID) + .setRegistrationUri(REGISTRATION_URI) + .setWebDestination(WEB_DESTINATION) + .setOsDestination(APP_DESTINATION) + .setRegistrant(DEFAULT_REGISTRANT) + .setVerifiedDestination(DEFAULT_VERIFIED_DESTINATION) + .setTopOrigin(WEB_TOP_ORIGIN) + .setType(AsyncRegistration.RegistrationType.WEB_SOURCE.ordinal()) + .setSourceType(Source.SourceType.EVENT) + .setRequestTime(System.currentTimeMillis()) + .setRetryCount(0) + .setLastProcessingTime(System.currentTimeMillis()) + .setRedirectType(AsyncRegistration.RedirectType.NONE) + .setDebugKeyAllowed(true) + .build(); + } + + private static AsyncRegistration createAsyncRegistrationForWebTrigger() { + return new AsyncRegistration.Builder() + .setId(UUID.randomUUID().toString()) + .setEnrollmentId(DEFAULT_ENROLLMENT_ID) + .setRegistrationUri(REGISTRATION_URI) + // null .setWebDestination(webDestination) + // null .setOsDestination(osDestination) + .setRegistrant(DEFAULT_REGISTRANT) + // null .setVerifiedDestination(null) + .setTopOrigin(WEB_TOP_ORIGIN) + .setType(AsyncRegistration.RegistrationType.WEB_TRIGGER.ordinal()) + // null .setSourceType(null) + .setRequestTime(System.currentTimeMillis()) + .setRetryCount(0) + .setLastProcessingTime(System.currentTimeMillis()) + .setRedirectType(AsyncRegistration.RedirectType.NONE) + .setDebugKeyAllowed(true) + .build(); + } + + private AsyncRegistrationQueueRunner getSpyAsyncRegistrationQueueRunner() { + return spy(new AsyncRegistrationQueueRunner( + mContentResolver, + mAsyncSourceFetcher, + mAsyncTriggerFetcher, + mEnrollmentDao, + new FakeDatastoreManager())); + } +} diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/AttributionTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/AttributionTest.java index 56893ca647..8067da5e71 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/AttributionTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/AttributionTest.java @@ -22,6 +22,8 @@ import static org.junit.Assert.assertThrows; import org.junit.Test; +import java.util.UUID; + public class AttributionTest { private static final String ID = "AR1"; private static final String DESTINATION_ORIGIN = "https://destination.com/origin"; @@ -33,6 +35,8 @@ public class AttributionTest { private static final long TRIGGER_TIME = 10000L; private static final String SOME_OTHER_STRING = "some_other"; private static final long SOME_OTHER_LONG = 1L; + private static final String SOURCE_ID = UUID.randomUUID().toString(); + private static final String TRIGGER_ID = UUID.randomUUID().toString(); @Test public void equals_pass() { @@ -75,6 +79,12 @@ public class AttributionTest { assertNotEquals( createExampleAttributionBuilder().setTriggerTime(SOME_OTHER_LONG).build(), createExampleAttributionBuilder().build()); + assertNotEquals( + createExampleAttributionBuilder().setSourceId(SOME_OTHER_STRING).build(), + createExampleAttributionBuilder().build()); + assertNotEquals( + createExampleAttributionBuilder().setTriggerId(SOME_OTHER_STRING).build(), + createExampleAttributionBuilder().build()); } @Test @@ -128,6 +138,15 @@ public class AttributionTest { .build() .hashCode(), createExampleAttributionBuilder().build().hashCode()); + assertNotEquals( + createExampleAttributionBuilder().setSourceId(SOME_OTHER_STRING).build().hashCode(), + createExampleAttributionBuilder().build().hashCode()); + assertNotEquals( + createExampleAttributionBuilder() + .setTriggerId(SOME_OTHER_STRING) + .build() + .hashCode(), + createExampleAttributionBuilder().build().hashCode()); } @Test @@ -150,6 +169,8 @@ public class AttributionTest { assertEquals( ENROLLMENT_ID, createExampleAttributionBuilder().build().getEnrollmentId()); + assertEquals(SOURCE_ID, createExampleAttributionBuilder().build().getSourceId()); + assertEquals(TRIGGER_ID, createExampleAttributionBuilder().build().getTriggerId()); } @Test @@ -186,6 +207,8 @@ public class AttributionTest { .setDestinationOrigin(DESTINATION_ORIGIN) .setDestinationSite(DESTINATION_SITE) .setSourceOrigin(PUBLISHER_ORIGIN) - .setSourceSite(PUBLISHER_SITE); + .setSourceSite(PUBLISHER_SITE) + .setSourceId(SOURCE_ID) + .setTriggerId(TRIGGER_ID); } } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/DeleteExpiredJobServiceTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/DeleteExpiredJobServiceTest.java index 4d3a471fcd..a515361a69 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/DeleteExpiredJobServiceTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/DeleteExpiredJobServiceTest.java @@ -19,6 +19,7 @@ package com.android.adservices.service.measurement; import static com.android.adservices.service.AdServicesConfig.MEASUREMENT_DELETE_EXPIRED_JOB_ID; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -35,30 +36,28 @@ import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.content.Context; -import android.provider.DeviceConfig; import com.android.adservices.data.measurement.DatastoreManager; import com.android.adservices.data.measurement.DatastoreManagerFactory; +import com.android.adservices.service.AdServicesConfig; +import com.android.adservices.service.Flags; +import com.android.adservices.service.FlagsFactory; import com.android.compatibility.common.util.TestUtils; import com.android.dx.mockito.inline.extended.ExtendedMockito; -import com.android.modules.utils.testing.TestableDeviceConfig; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; +import java.util.concurrent.TimeUnit; + /** * Unit test for {@link DeleteExpiredJobService */ public class DeleteExpiredJobServiceTest { - // This rule is used for configuring P/H flags - @Rule - public final TestableDeviceConfig.TestableDeviceConfigRule mDeviceConfigRule = - new TestableDeviceConfig.TestableDeviceConfigRule(); - private static final long WAIT_IN_MILLIS = 50L; private DatastoreManager mMockDatastoreManager; @@ -75,10 +74,11 @@ public class DeleteExpiredJobServiceTest { @Test public void onStartJob_killSwitchOn() throws Exception { - enableKillSwitch(); - runWithMocks( () -> { + // Setup + enableKillSwitch(); + // Execute boolean result = mSpyService.onStartJob(Mockito.mock(JobParameters.class)); @@ -95,10 +95,11 @@ public class DeleteExpiredJobServiceTest { @Test public void onStartJob_killSwitchOff() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { + // Setup + disableKillSwitch(); + // Execute boolean result = mSpyService.onStartJob(Mockito.mock(JobParameters.class)); @@ -115,11 +116,11 @@ public class DeleteExpiredJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOn_dontSchedule() throws Exception { - enableKillSwitch(); - runWithMocks( () -> { // Setup + enableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -146,11 +147,11 @@ public class DeleteExpiredJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOff_previouslyExecuted_dontForceSchedule_dontSchedule() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { // Setup + disableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -177,11 +178,11 @@ public class DeleteExpiredJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOff_previouslyExecuted_forceSchedule_schedule() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { // Setup + disableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -208,11 +209,11 @@ public class DeleteExpiredJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOff_previouslyNotExecuted_dontForceSchedule_schedule() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { // Setup + disableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -234,11 +235,28 @@ public class DeleteExpiredJobServiceTest { }); } + @Test + public void testSchedule_jobInfoIsPersisted() { + // Setup + final JobScheduler jobScheduler = mock(JobScheduler.class); + final ArgumentCaptor<JobInfo> captor = ArgumentCaptor.forClass(JobInfo.class); + + // Execute + DeleteExpiredJobService.schedule(mock(Context.class), jobScheduler); + + // Validate + verify(jobScheduler, times(1)).schedule(captor.capture()); + assertNotNull(captor.getValue()); + assertTrue(captor.getValue().isPersisted()); + } + private void runWithMocks(TestUtils.RunnableWithThrow execute) throws Exception { MockitoSession session = ExtendedMockito.mockitoSession() + .spyStatic(AdServicesConfig.class) .spyStatic(DatastoreManagerFactory.class) .spyStatic(DeleteExpiredJobService.class) + .spyStatic(FlagsFactory.class) .strictness(Strictness.LENIENT) .startMocking(); try { @@ -247,6 +265,8 @@ public class DeleteExpiredJobServiceTest { doReturn(true).when(mMockDatastoreManager).runInTransaction(any()); doNothing().when(mSpyService).jobFinished(any(), anyBoolean()); doReturn(mMockJobScheduler).when(mSpyService).getSystemService(JobScheduler.class); + ExtendedMockito.doReturn(TimeUnit.HOURS.toMillis(24)) + .when(AdServicesConfig::getMeasurementDeleteExpiredJobPeriodMs); ExtendedMockito.doReturn(mMockDatastoreManager) .when(() -> DatastoreManagerFactory.getDatastoreManager(any())); ExtendedMockito.doNothing().when(() -> DeleteExpiredJobService.schedule(any(), any())); @@ -267,10 +287,8 @@ public class DeleteExpiredJobServiceTest { } private void toggleKillSwitch(boolean value) { - DeviceConfig.setProperty( - DeviceConfig.NAMESPACE_ADSERVICES, - "measurement_job_delete_expired_kill_switch", - Boolean.toString(value), - /* makeDefault */ false); + Flags mockFlags = Mockito.mock(Flags.class); + ExtendedMockito.doReturn(mockFlags).when(FlagsFactory::getFlags); + ExtendedMockito.doReturn(value).when(mockFlags).getMeasurementJobDeleteExpiredKillSwitch(); } } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/DeleteUninstalledJobServiceTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/DeleteUninstalledJobServiceTest.java new file mode 100644 index 0000000000..e0efb75298 --- /dev/null +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/DeleteUninstalledJobServiceTest.java @@ -0,0 +1,286 @@ +/* + * 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.measurement; + +import static com.android.adservices.service.AdServicesConfig.MEASUREMENT_DELETE_UNINSTALLED_JOB_ID; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.content.Context; + +import com.android.adservices.service.AdServicesConfig; +import com.android.adservices.service.Flags; +import com.android.adservices.service.FlagsFactory; +import com.android.compatibility.common.util.TestUtils; +import com.android.dx.mockito.inline.extended.ExtendedMockito; + +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoSession; +import org.mockito.Spy; +import org.mockito.quality.Strictness; + +import java.util.concurrent.TimeUnit; + +public class DeleteUninstalledJobServiceTest { + private static final long WAIT_IN_MILLIS = 50L; + + @Mock private JobScheduler mMockJobScheduler; + @Mock private MeasurementImpl mMockMeasurementImpl; + + @Spy private DeleteUninstalledJobService mSpyService; + + @Test + public void onStartJob_killSwitchOn() throws Exception { + runWithMocks( + () -> { + // Setup + enableKillSwitch(); + + // Execute + boolean result = mSpyService.onStartJob(Mockito.mock(JobParameters.class)); + + // Validate + assertFalse(result); + + // Allow background thread to execute + Thread.sleep(WAIT_IN_MILLIS); + verify(mMockMeasurementImpl, never()).deleteAllUninstalledMeasurementData(); + verify(mSpyService, times(1)).jobFinished(any(), eq(false)); + verify(mMockJobScheduler, times(1)) + .cancel(eq(MEASUREMENT_DELETE_UNINSTALLED_JOB_ID)); + }); + } + + @Test + public void onStartJob_killSwitchOff() throws Exception { + runWithMocks( + () -> { + // Setup + disableKillSwitch(); + + // Execute + boolean result = mSpyService.onStartJob(Mockito.mock(JobParameters.class)); + + // Validate + assertTrue(result); + + // Allow background thread to execute + Thread.sleep(WAIT_IN_MILLIS); + verify(mMockMeasurementImpl, times(1)).deleteAllUninstalledMeasurementData(); + verify(mSpyService, times(1)).jobFinished(any(), anyBoolean()); + verify(mMockJobScheduler, never()) + .cancel(eq(MEASUREMENT_DELETE_UNINSTALLED_JOB_ID)); + }); + } + + @Test + public void scheduleIfNeeded_killSwitchOn_dontSchedule() throws Exception { + runWithMocks( + () -> { + // Setup + enableKillSwitch(); + + final Context mockContext = mock(Context.class); + doReturn(mMockJobScheduler) + .when(mockContext) + .getSystemService(JobScheduler.class); + final JobInfo mockJobInfo = mock(JobInfo.class); + doReturn(mockJobInfo) + .when(mMockJobScheduler) + .getPendingJob(eq(MEASUREMENT_DELETE_UNINSTALLED_JOB_ID)); + + // Execute + DeleteUninstalledJobService.scheduleIfNeeded( + mockContext, /* forceSchedule = */ false); + + // Validate + // Allow background thread to execute + Thread.sleep(WAIT_IN_MILLIS); + ExtendedMockito.verify( + () -> DeleteUninstalledJobService.schedule(any(), any()), never()); + verify(mMockJobScheduler, never()) + .getPendingJob(eq(MEASUREMENT_DELETE_UNINSTALLED_JOB_ID)); + }); + } + + @Test + public void scheduleIfNeeded_killSwitchOff_previouslyExecuted_dontForceSchedule_dontSchedule() + throws Exception { + runWithMocks( + () -> { + // Setup + disableKillSwitch(); + + final Context mockContext = mock(Context.class); + doReturn(mMockJobScheduler) + .when(mockContext) + .getSystemService(JobScheduler.class); + final JobInfo mockJobInfo = mock(JobInfo.class); + doReturn(mockJobInfo) + .when(mMockJobScheduler) + .getPendingJob(eq(MEASUREMENT_DELETE_UNINSTALLED_JOB_ID)); + + // Execute + DeleteUninstalledJobService.scheduleIfNeeded( + mockContext, /* forceSchedule = */ false); + + // Validate + // Allow background thread to execute + Thread.sleep(WAIT_IN_MILLIS); + ExtendedMockito.verify( + () -> DeleteUninstalledJobService.schedule(any(), any()), never()); + verify(mMockJobScheduler, times(1)) + .getPendingJob(eq(MEASUREMENT_DELETE_UNINSTALLED_JOB_ID)); + }); + } + + @Test + public void scheduleIfNeeded_killSwitchOff_previouslyExecuted_forceSchedule_schedule() + throws Exception { + runWithMocks( + () -> { + // Setup + disableKillSwitch(); + + final Context mockContext = mock(Context.class); + doReturn(mMockJobScheduler) + .when(mockContext) + .getSystemService(JobScheduler.class); + final JobInfo mockJobInfo = mock(JobInfo.class); + doReturn(mockJobInfo) + .when(mMockJobScheduler) + .getPendingJob(eq(MEASUREMENT_DELETE_UNINSTALLED_JOB_ID)); + + // Execute + DeleteUninstalledJobService.scheduleIfNeeded( + mockContext, /* forceSchedule = */ true); + + // Validate + // Allow background thread to execute + Thread.sleep(WAIT_IN_MILLIS); + ExtendedMockito.verify( + () -> DeleteUninstalledJobService.schedule(any(), any()), times(1)); + verify(mMockJobScheduler, times(1)) + .getPendingJob(eq(MEASUREMENT_DELETE_UNINSTALLED_JOB_ID)); + }); + } + + @Test + public void scheduleIfNeeded_killSwitchOff_previouslyNotExecuted_dontForceSchedule_schedule() + throws Exception { + runWithMocks( + () -> { + // Setup + disableKillSwitch(); + + final Context mockContext = mock(Context.class); + doReturn(mMockJobScheduler) + .when(mockContext) + .getSystemService(JobScheduler.class); + doReturn(/* noJobInfo = */ null) + .when(mMockJobScheduler) + .getPendingJob(eq(MEASUREMENT_DELETE_UNINSTALLED_JOB_ID)); + + // Execute + DeleteUninstalledJobService.scheduleIfNeeded(mockContext, false); + + // Validate + // Allow background thread to execute + Thread.sleep(WAIT_IN_MILLIS); + ExtendedMockito.verify( + () -> DeleteUninstalledJobService.schedule(any(), any()), times(1)); + verify(mMockJobScheduler, times(1)) + .getPendingJob(eq(MEASUREMENT_DELETE_UNINSTALLED_JOB_ID)); + }); + } + + @Test + public void testSchedule_jobInfoIsPersisted() { + // Setup + final JobScheduler jobScheduler = mock(JobScheduler.class); + final ArgumentCaptor<JobInfo> captor = ArgumentCaptor.forClass(JobInfo.class); + + // Execute + DeleteUninstalledJobService.schedule(mock(Context.class), jobScheduler); + + // Validate + verify(jobScheduler, times(1)).schedule(captor.capture()); + assertNotNull(captor.getValue()); + assertTrue(captor.getValue().isPersisted()); + } + + private void runWithMocks(TestUtils.RunnableWithThrow execute) throws Exception { + MockitoSession session = + ExtendedMockito.mockitoSession() + .initMocks(this) + .spyStatic(AdServicesConfig.class) + .spyStatic(MeasurementImpl.class) + .spyStatic(DeleteUninstalledJobService.class) + .spyStatic(FlagsFactory.class) + .strictness(Strictness.LENIENT) + .startMocking(); + try { + // Setup mock everything in job + ExtendedMockito.doReturn(mMockMeasurementImpl) + .when(() -> MeasurementImpl.getInstance(any())); + doNothing().when(mSpyService).jobFinished(any(), anyBoolean()); + doReturn(mMockJobScheduler).when(mSpyService).getSystemService(JobScheduler.class); + ExtendedMockito.doReturn(TimeUnit.HOURS.toMillis(24)) + .when(AdServicesConfig::getMeasurementDeleteUninstalledJobPeriodMs); + ExtendedMockito.doNothing() + .when(() -> DeleteUninstalledJobService.schedule(any(), any())); + + // Execute + execute.run(); + } finally { + session.finishMocking(); + } + } + + private void enableKillSwitch() { + toggleKillSwitch(true); + } + + private void disableKillSwitch() { + toggleKillSwitch(false); + } + + private void toggleKillSwitch(boolean value) { + Flags mockFlags = Mockito.mock(Flags.class); + ExtendedMockito.doReturn(mockFlags).when(FlagsFactory::getFlags); + ExtendedMockito.doReturn(value) + .when(mockFlags) + .getMeasurementJobDeleteUninstalledKillSwitch(); + } +} diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2EDenoisedMockTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2EDenoisedMockTest.java index d0871327ec..8b79440f80 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2EDenoisedMockTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2EDenoisedMockTest.java @@ -35,22 +35,29 @@ import java.util.Collection; public class E2EDenoisedMockTest extends E2EMockTest { private static final String TEST_DIR_NAME = "msmt_e2e_tests"; - @Parameterized.Parameters(name = "{2}") + @Parameterized.Parameters(name = "{3}") public static Collection<Object[]> getData() throws IOException, JSONException { return data(TEST_DIR_NAME); } public E2EDenoisedMockTest(Collection<Action> actions, ReportObjects expectedOutput, - String name) throws DatastoreException { - super(actions, expectedOutput, name); + PrivacyParamsProvider privacyParamsProvider, String name) throws DatastoreException { + super(actions, expectedOutput, privacyParamsProvider, name); mAttributionHelper = TestObjectProvider.getAttributionJobHandler(sDatastoreManager); mMeasurementImpl = TestObjectProvider.getMeasurementImpl( - TestObjectProvider.Type.DENOISED, sDatastoreManager, - mSourceFetcher, - mTriggerFetcher, mClickVerifier, - mFlags); + mFlags, + mMeasurementDataDeleter, + sEnrollmentDao); + + mAsyncRegistrationQueueRunner = + TestObjectProvider.getAsyncRegistrationQueueRunner( + TestObjectProvider.Type.DENOISED, + sDatastoreManager, + mAsyncSourceFetcher, + mAsyncTriggerFetcher, + sEnrollmentDao); } } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2EImpressionNoiseMockTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2EImpressionNoiseMockTest.java index c01225741b..9970418349 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2EImpressionNoiseMockTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2EImpressionNoiseMockTest.java @@ -51,23 +51,29 @@ public class E2EImpressionNoiseMockTest extends E2EMockTest { String TRIGGER_DATA = "trigger_data"; } - @Parameterized.Parameters(name = "{2}") + @Parameterized.Parameters(name = "{3}") public static Collection<Object[]> getData() throws IOException, JSONException { return data(TEST_DIR_NAME); } public E2EImpressionNoiseMockTest(Collection<Action> actions, ReportObjects expectedOutput, - String name) throws DatastoreException { - super(actions, expectedOutput, name); + PrivacyParamsProvider privacyParamsProvider, String name) throws DatastoreException { + super(actions, expectedOutput, privacyParamsProvider, name); mAttributionHelper = TestObjectProvider.getAttributionJobHandler(sDatastoreManager); mMeasurementImpl = TestObjectProvider.getMeasurementImpl( - TestObjectProvider.Type.NOISY, sDatastoreManager, - mSourceFetcher, - mTriggerFetcher, mClickVerifier, - mFlags); + mFlags, + mMeasurementDataDeleter, + sEnrollmentDao); + mAsyncRegistrationQueueRunner = + TestObjectProvider.getAsyncRegistrationQueueRunner( + TestObjectProvider.Type.NOISY, + sDatastoreManager, + mAsyncSourceFetcher, + mAsyncTriggerFetcher, + sEnrollmentDao); getExpectedTriggerDataDistributions(); } @@ -79,7 +85,7 @@ public class E2EImpressionNoiseMockTest extends E2EMockTest { // is currently supporting only one reporting job, which batches multiple reports at once, // although each is a separate network request. for (int i = 0; i < destinations.size(); i++) { - String uri = destinations.get(i).toString(); + String uri = getReportUrl(ReportType.EVENT, destinations.get(i).toString()); JSONObject payload = payloads.get(i); String eventId = payload.getString(PayloadKeys.EVENT_ID); String triggerData = payload.getString(PayloadKeys.TRIGGER_DATA); @@ -94,7 +100,8 @@ public class E2EImpressionNoiseMockTest extends E2EMockTest { for (String key : mActualTriggerDataDistributions.keySet()) { if (!mExpectedTriggerDataDistributions.containsKey(key)) { Assert.assertTrue(getTestFailureMessage( - "Missing key in expected trigger data distributions"), false); + "Missing key in expected trigger data distributions" + + getDatastoreState()), false); } } boolean testPassed = false; @@ -113,8 +120,10 @@ public class E2EImpressionNoiseMockTest extends E2EMockTest { } } } - Assert.assertTrue(getTestFailureMessage( - "Trigger data distributions were the same"), testPassed); + Assert.assertTrue( + getTestFailureMessage( + "Trigger data distributions were the same " + getDatastoreState()), + testPassed); } private void getExpectedTriggerDataDistributions() { diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2EInteropMockTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2EInteropMockTest.java new file mode 100644 index 0000000000..43dde18842 --- /dev/null +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2EInteropMockTest.java @@ -0,0 +1,247 @@ +/* + * 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.measurement; + +import static com.android.adservices.service.measurement.PrivacyParams.MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS; +import static com.android.adservices.service.measurement.PrivacyParams.MIN_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS; + +import android.adservices.measurement.RegistrationRequest; +import android.net.Uri; +import android.util.Log; + +import com.android.adservices.data.measurement.DatastoreException; +import com.android.adservices.service.measurement.actions.Action; +import com.android.adservices.service.measurement.actions.RegisterSource; +import com.android.adservices.service.measurement.actions.RegisterTrigger; +import com.android.adservices.service.measurement.actions.ReportObjects; +import com.android.adservices.service.measurement.util.Enrollment; +import com.android.adservices.service.measurement.util.UnsignedLong; +import com.android.adservices.service.measurement.util.Web; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assert; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * End-to-end test from source and trigger registration to attribution reporting, using mocked HTTP + * requests. + * + * Tests in assets/msmt_interop_tests/ directory were copied from + * https://source.chromium.org/chromium/chromium/src/+/main:content/test/data/attribution_reporting/interop/ + * on October 15, 2022 + */ +@RunWith(Parameterized.class) +public class E2EInteropMockTest extends E2EMockTest { + private static final String LOG_TAG = "msmt_e2e_interop_mock_test"; + private static final String TEST_DIR_NAME = "msmt_interop_tests"; + private static final String ANDROID_APP_SCHEME = "android-app"; + + @Parameterized.Parameters(name = "{3}") + public static Collection<Object[]> getData() throws IOException, JSONException { + return data(TEST_DIR_NAME); + } + + public E2EInteropMockTest(Collection<Action> actions, ReportObjects expectedOutput, + PrivacyParamsProvider privacyParamsProvider, String name) throws DatastoreException { + super(actions, expectedOutput, privacyParamsProvider, name); + mAttributionHelper = TestObjectProvider.getAttributionJobHandler(sDatastoreManager); + mMeasurementImpl = + TestObjectProvider.getMeasurementImpl( + sDatastoreManager, + mClickVerifier, + mFlags, + mMeasurementDataDeleter, + sEnrollmentDao); + mAsyncRegistrationQueueRunner = + TestObjectProvider.getAsyncRegistrationQueueRunner( + TestObjectProvider.Type.DENOISED, + sDatastoreManager, + mAsyncSourceFetcher, + mAsyncTriggerFetcher, + sEnrollmentDao); + } + + @Override + void processAction(RegisterSource sourceRegistration) throws IOException { + RegistrationRequest request = sourceRegistration.mRegistrationRequest; + // For interop tests, we currently expect only one HTTPS response per registration with no + // redirects, partly due to differences in redirect handling across attribution APIs. + for (String uri : sourceRegistration.mUriToResponseHeadersMap.keySet()) { + updateEnrollment(uri); + Source source = getSource( + sourceRegistration.getPublisher(), + sourceRegistration.mTimestamp, + uri, + request, + getNextResponse(sourceRegistration.mUriToResponseHeadersMap, uri)); + Assert.assertTrue( + "measurementDao.insertSource failed", + sDatastoreManager.runInTransaction( + measurementDao -> { + if (AsyncRegistrationQueueRunner.isSourceAllowedToInsert( + source, + source.getPublisher(), + EventSurfaceType.WEB, + measurementDao)) { + measurementDao.insertSource(source); + } + })); + } + } + + @Override + void processAction(RegisterTrigger triggerRegistration) throws IOException { + RegistrationRequest request = triggerRegistration.mRegistrationRequest; + // For interop tests, we currently expect only one HTTPS response per registration with no + // redirects, partly due to differences in redirect handling across attribution APIs. + for (String uri : triggerRegistration.mUriToResponseHeadersMap.keySet()) { + updateEnrollment(uri); + Trigger trigger = getTrigger( + triggerRegistration.getDestination(), + triggerRegistration.mTimestamp, + uri, + request, + getNextResponse(triggerRegistration.mUriToResponseHeadersMap, uri)); + Assert.assertTrue( + "measurementDao.insertTrigger failed", + sDatastoreManager.runInTransaction( + measurementDao -> + measurementDao.insertTrigger(trigger))); + } + Assert.assertTrue("AttributionJobHandler.performPendingAttributions returned false", + mAttributionHelper.performPendingAttributions()); + } + + private static Source getSource(String publisher, long timestamp, String uri, + RegistrationRequest request, Map<String, List<String>> headers) { + try { + Source.Builder sourceBuilder = new Source.Builder(); + String enrollmentId = + Enrollment.maybeGetEnrollmentId(Uri.parse(uri), sEnrollmentDao).get(); + sourceBuilder.setEnrollmentId(enrollmentId); + sourceBuilder.setPublisher(Uri.parse(publisher)); + sourceBuilder.setPublisherType(EventSurfaceType.WEB); + sourceBuilder.setEventTime(timestamp); + sourceBuilder.setSourceType(getSourceType(request)); + sourceBuilder.setAttributionMode(Source.AttributionMode.TRUTHFULLY); + sourceBuilder.setRegistrant(getRegistrant(request.getPackageName())); + List<String> field = headers.get("Attribution-Reporting-Register-Source"); + JSONObject json = new JSONObject(field.get(0)); + sourceBuilder.setEventId(new UnsignedLong(json.getString("source_event_id"))); + if (!json.isNull("expiry")) { + long offset = + TimeUnit.SECONDS.toMillis( + extractValidNumberInRange( + json.getLong("expiry"), + MIN_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS, + MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS)); + sourceBuilder.setExpiryTime(timestamp + offset); + } else { + sourceBuilder.setExpiryTime( + timestamp + + TimeUnit.SECONDS.toMillis( + MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS)); + } + if (!json.isNull("priority")) { + sourceBuilder.setPriority(json.getLong("priority")); + } + if (!json.isNull("debug_key")) { + sourceBuilder.setDebugKey(new UnsignedLong(json.getString("debug_key"))); + } + if (!json.isNull("filter_data")) { + sourceBuilder.setFilterData(json.getJSONObject("filter_data").toString()); + } + sourceBuilder.setWebDestination(Web.topPrivateDomainAndScheme( + Uri.parse(json.getString("destination"))).get()); + if (!json.isNull("aggregation_keys")) { + sourceBuilder.setAggregateSource(json.getJSONObject("aggregation_keys").toString()); + } + return sourceBuilder.build(); + } catch (JSONException e) { + Log.e(LOG_TAG, "Failed to parse source registration. %s", e); + return null; + } + } + + private static Trigger getTrigger(String destination, long timestamp, String uri, + RegistrationRequest request, Map<String, List<String>> headers) { + try { + Trigger.Builder triggerBuilder = new Trigger.Builder(); + String enrollmentId = + Enrollment.maybeGetEnrollmentId(Uri.parse(uri), sEnrollmentDao).get(); + triggerBuilder.setEnrollmentId(enrollmentId); + triggerBuilder.setAttributionDestination(Uri.parse(destination)); + triggerBuilder.setDestinationType(EventSurfaceType.WEB); + triggerBuilder.setTriggerTime(timestamp); + triggerBuilder.setRegistrant(getRegistrant(request.getPackageName())); + List<String> field = headers.get("Attribution-Reporting-Register-Trigger"); + JSONObject json = new JSONObject(field.get(0)); + if (!json.isNull("event_trigger_data")) { + triggerBuilder.setEventTriggers(json.getJSONArray("event_trigger_data").toString()); + } + if (!json.isNull("aggregatable_trigger_data")) { + triggerBuilder.setAggregateTriggerData( + json.getJSONArray("aggregatable_trigger_data").toString()); + } + if (!json.isNull("aggregatable_values")) { + triggerBuilder.setAggregateValues( + json.getJSONObject("aggregatable_values").toString()); + } + if (!json.isNull("filters")) { + triggerBuilder.setFilters(json.getString("filters")); + } + if (!json.isNull("not_filters")) { + triggerBuilder.setNotFilters(json.getString("not_filters")); + } + if (!json.isNull("debug_key")) { + triggerBuilder.setDebugKey(new UnsignedLong(json.getString("debug_key"))); + } + return triggerBuilder.build(); + } catch (JSONException e) { + Log.e(LOG_TAG, "Failed to parse trigger registration. %s", e); + return null; + } + } + + private static Source.SourceType getSourceType(RegistrationRequest request) { + return request.getInputEvent() == null + ? Source.SourceType.EVENT + : Source.SourceType.NAVIGATION; + } + + private static Uri getRegistrant(String packageName) { + return Uri.parse(ANDROID_APP_SCHEME + "://" + packageName); + } + + private static long extractValidNumberInRange(long value, long lowerLimit, long upperLimit) { + if (value < lowerLimit) { + return lowerLimit; + } else if (value > upperLimit) { + return upperLimit; + } + + return value; + } +} diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2EMockStatic.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2EMockStatic.java new file mode 100644 index 0000000000..372558b3d0 --- /dev/null +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2EMockStatic.java @@ -0,0 +1,84 @@ +/* + * 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.measurement; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; + +import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder; +import com.android.modules.utils.testing.StaticMockFixture; +import com.android.modules.utils.testing.StaticMockFixtureRule; +import com.android.modules.utils.testing.TestableDeviceConfig; + +import org.mockito.stubbing.Answer; + +/** + * Combines TestableDeviceConfig with other needed static mocks. + */ +public final class E2EMockStatic implements StaticMockFixture { + + private final E2ETest.PrivacyParamsProvider mPrivacyParams; + + public E2EMockStatic(E2ETest.PrivacyParamsProvider privacyParamsProvider) { + mPrivacyParams = privacyParamsProvider; + } + /** + * {@inheritDoc} + */ + @Override + public StaticMockitoSessionBuilder setUpMockedClasses( + StaticMockitoSessionBuilder sessionBuilder) { + sessionBuilder.spyStatic(PrivacyParams.class); + return sessionBuilder; + } + + /** + * {@inheritDoc} + */ + @Override + public void setUpMockBehaviors() { + doAnswer((Answer<Integer>) invocation -> + mPrivacyParams.getMaxAttributionPerRateLimitWindow()) + .when(() -> PrivacyParams.getMaxAttributionPerRateLimitWindow()); + doAnswer((Answer<Integer>) invocation -> + mPrivacyParams.getNavigationTriggerDataCardinality()) + .when(() -> PrivacyParams.getNavigationTriggerDataCardinality()); + doAnswer((Answer<Integer>) invocation -> + mPrivacyParams.getMaxDistinctEnrollmentsPerPublisherXDestinationInAttribution()) + .when(() -> PrivacyParams + .getMaxDistinctEnrollmentsPerPublisherXDestinationInAttribution()); + doAnswer((Answer<Integer>) invocation -> + mPrivacyParams.getMaxDistinctDestinationsPerPublisherXEnrollmentInActiveSource()) + .when(() -> PrivacyParams + .getMaxDistinctDestinationsPerPublisherXEnrollmentInActiveSource()); + doAnswer((Answer<Integer>) invocation -> + mPrivacyParams.getMaxDistinctEnrollmentsPerPublisherXDestinationInSource()) + .when(() -> PrivacyParams + .getMaxDistinctEnrollmentsPerPublisherXDestinationInSource()); + } + + /** + * {@inheritDoc} + */ + @Override + public void tearDown() { } + + public static class E2EMockStaticRule extends StaticMockFixtureRule { + public E2EMockStaticRule(E2ETest.PrivacyParamsProvider privacyParamsProvider) { + super(TestableDeviceConfig::new, () -> new E2EMockStatic(privacyParamsProvider)); + } + } +} diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2EMockTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2EMockTest.java index d51526d285..07ece39b7f 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2EMockTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2EMockTest.java @@ -16,6 +16,8 @@ package com.android.adservices.service.measurement; +import static com.android.adservices.ResultCode.RESULT_OK; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.mock; @@ -23,24 +25,41 @@ import static org.mockito.Mockito.when; import android.net.Uri; +import androidx.test.core.app.ApplicationProvider; + +import com.android.adservices.HpkeJni; +import com.android.adservices.data.DbTestUtil; +import com.android.adservices.data.enrollment.EnrollmentDao; +import com.android.adservices.data.measurement.DatastoreManager; +import com.android.adservices.data.measurement.SQLDatastoreManager; +import com.android.adservices.data.measurement.deletion.MeasurementDataDeleter; import com.android.adservices.service.Flags; import com.android.adservices.service.FlagsFactory; +import com.android.adservices.service.enrollment.EnrollmentData; import com.android.adservices.service.measurement.actions.Action; +import com.android.adservices.service.measurement.actions.AggregateReportingJob; +import com.android.adservices.service.measurement.actions.EventReportingJob; +import com.android.adservices.service.measurement.actions.InstallApp; import com.android.adservices.service.measurement.actions.RegisterSource; import com.android.adservices.service.measurement.actions.RegisterTrigger; import com.android.adservices.service.measurement.actions.RegisterWebSource; import com.android.adservices.service.measurement.actions.RegisterWebTrigger; import com.android.adservices.service.measurement.actions.ReportObjects; -import com.android.adservices.service.measurement.actions.ReportingJob; +import com.android.adservices.service.measurement.actions.UninstallApp; +import com.android.adservices.service.measurement.aggregation.AggregateCryptoFixture; +import com.android.adservices.service.measurement.attribution.AttributionJobHandlerWrapper; import com.android.adservices.service.measurement.inputverification.ClickVerifier; -import com.android.adservices.service.measurement.registration.SourceFetcher; -import com.android.adservices.service.measurement.registration.TriggerFetcher; +import com.android.adservices.service.measurement.registration.AsyncSourceFetcher; +import com.android.adservices.service.measurement.registration.AsyncTriggerFetcher; import com.android.adservices.service.measurement.reporting.AggregateReportingJobHandlerWrapper; import com.android.adservices.service.measurement.reporting.EventReportingJobHandlerWrapper; +import com.android.adservices.service.stats.AdServicesLoggerImpl; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import org.junit.Assert; +import org.junit.Rule; import org.mockito.Mockito; import org.mockito.stubbing.Answer; @@ -52,8 +71,11 @@ import java.util.ArrayList; import java.util.Base64; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import javax.net.ssl.HttpsURLConnection; @@ -71,73 +93,184 @@ import co.nstant.in.cbor.model.UnicodeString; * <p>Consider @RunWith(Parameterized.class) */ public abstract class E2EMockTest extends E2ETest { - SourceFetcher mSourceFetcher; - TriggerFetcher mTriggerFetcher; + + static EnrollmentDao sEnrollmentDao = + new EnrollmentDao( + ApplicationProvider.getApplicationContext(), DbTestUtil.getDbHelperForTest()); + static DatastoreManager sDatastoreManager = + new SQLDatastoreManager(DbTestUtil.getDbHelperForTest()); + + // Class extensions may choose to disable or enable added noise. + AttributionJobHandlerWrapper mAttributionHelper; + MeasurementImpl mMeasurementImpl; ClickVerifier mClickVerifier; + MeasurementDataDeleter mMeasurementDataDeleter; Flags mFlags; + AsyncRegistrationQueueRunner mAsyncRegistrationQueueRunner; + AsyncSourceFetcher mAsyncSourceFetcher; + AsyncTriggerFetcher mAsyncTriggerFetcher; - E2EMockTest(Collection<Action> actions, ReportObjects expectedOutput, String name) { + private static final long MAX_RECORDS_PROCESSED = 20L; + private static final short ASYNC_REG_RETRY_LIMIT = 1; + private final AtomicInteger mEnrollmentCount = new AtomicInteger(); + private final Set<String> mSeenUris = new HashSet<>(); + private final Map<String, String> mUriToEnrollmentId = new HashMap<>(); + + @Rule + public final E2EMockStatic.E2EMockStaticRule mE2EMockStaticRule; + + E2EMockTest(Collection<Action> actions, ReportObjects expectedOutput, + PrivacyParamsProvider privacyParamsProvider, String name) { super(actions, expectedOutput, name); - mSourceFetcher = Mockito.spy(new SourceFetcher(sContext)); - mTriggerFetcher = Mockito.spy(new TriggerFetcher(sContext)); mClickVerifier = Mockito.mock(ClickVerifier.class); mFlags = FlagsFactory.getFlagsForTest(); + mE2EMockStaticRule = new E2EMockStatic.E2EMockStaticRule(privacyParamsProvider); + mMeasurementDataDeleter = Mockito.spy(new MeasurementDataDeleter(sDatastoreManager)); + mAsyncSourceFetcher = + Mockito.spy( + new AsyncSourceFetcher( + sEnrollmentDao, + FlagsFactory.getFlagsForTest(), + AdServicesLoggerImpl.getInstance())); + mAsyncTriggerFetcher = + Mockito.spy( + new AsyncTriggerFetcher( + sEnrollmentDao, + FlagsFactory.getFlagsForTest(), + AdServicesLoggerImpl.getInstance())); when(mClickVerifier.isInputEventVerifiable(any(), anyLong())).thenReturn(true); } @Override void prepareRegistrationServer(RegisterSource sourceRegistration) throws IOException { for (String uri : sourceRegistration.mUriToResponseHeadersMap.keySet()) { + updateEnrollment(uri); HttpsURLConnection urlConnection = mock(HttpsURLConnection.class); when(urlConnection.getResponseCode()).thenReturn(200); Answer<Map<String, List<String>>> headerFieldsMockAnswer = invocation -> getNextResponse(sourceRegistration.mUriToResponseHeadersMap, uri); Mockito.doAnswer(headerFieldsMockAnswer).when(urlConnection).getHeaderFields(); - Mockito.doReturn(urlConnection).when(mSourceFetcher).openUrl(new URL(uri)); + Mockito.doReturn(urlConnection).when(mAsyncSourceFetcher).openUrl(new URL(uri)); } } @Override - void prepareRegistrationServer(RegisterTrigger triggerRegistration) - throws IOException { + void prepareRegistrationServer(RegisterTrigger triggerRegistration) throws IOException { for (String uri : triggerRegistration.mUriToResponseHeadersMap.keySet()) { + updateEnrollment(uri); HttpsURLConnection urlConnection = mock(HttpsURLConnection.class); when(urlConnection.getResponseCode()).thenReturn(200); Answer<Map<String, List<String>>> headerFieldsMockAnswer = invocation -> getNextResponse(triggerRegistration.mUriToResponseHeadersMap, uri); Mockito.doAnswer(headerFieldsMockAnswer).when(urlConnection).getHeaderFields(); - Mockito.doReturn(urlConnection).when(mTriggerFetcher).openUrl(new URL(uri)); + Mockito.doReturn(urlConnection).when(mAsyncTriggerFetcher).openUrl(new URL(uri)); } } @Override void prepareRegistrationServer(RegisterWebSource sourceRegistration) throws IOException { for (String uri : sourceRegistration.mUriToResponseHeadersMap.keySet()) { + updateEnrollment(uri); HttpsURLConnection urlConnection = mock(HttpsURLConnection.class); when(urlConnection.getResponseCode()).thenReturn(200); Answer<Map<String, List<String>>> headerFieldsMockAnswer = invocation -> getNextResponse(sourceRegistration.mUriToResponseHeadersMap, uri); Mockito.doAnswer(headerFieldsMockAnswer).when(urlConnection).getHeaderFields(); - Mockito.doReturn(urlConnection).when(mSourceFetcher).openUrl(new URL(uri)); + Mockito.doReturn(urlConnection).when(mAsyncSourceFetcher).openUrl(new URL(uri)); } } @Override void prepareRegistrationServer(RegisterWebTrigger triggerRegistration) throws IOException { for (String uri : triggerRegistration.mUriToResponseHeadersMap.keySet()) { + updateEnrollment(uri); HttpsURLConnection urlConnection = mock(HttpsURLConnection.class); when(urlConnection.getResponseCode()).thenReturn(200); Answer<Map<String, List<String>>> headerFieldsMockAnswer = invocation -> getNextResponse(triggerRegistration.mUriToResponseHeadersMap, uri); Mockito.doAnswer(headerFieldsMockAnswer).when(urlConnection).getHeaderFields(); - Mockito.doReturn(urlConnection).when(mTriggerFetcher).openUrl(new URL(uri)); + Mockito.doReturn(urlConnection).when(mAsyncTriggerFetcher).openUrl(new URL(uri)); } } @Override - void processAction(ReportingJob reportingJob) throws IOException, JSONException { + void processAction(RegisterSource sourceRegistration) throws IOException { + prepareRegistrationServer(sourceRegistration); + Assert.assertEquals( + "MeasurementImpl.register source failed", + RESULT_OK, + mMeasurementImpl.register( + sourceRegistration.mRegistrationRequest, sourceRegistration.mTimestamp)); + mAsyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker( + MAX_RECORDS_PROCESSED, ASYNC_REG_RETRY_LIMIT); + } + + @Override + void processAction(RegisterWebSource sourceRegistration) throws IOException { + prepareRegistrationServer(sourceRegistration); + Assert.assertEquals( + "MeasurementImpl.registerWebSource failed", + RESULT_OK, + mMeasurementImpl.registerWebSource( + sourceRegistration.mRegistrationRequest, sourceRegistration.mTimestamp)); + mAsyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker( + MAX_RECORDS_PROCESSED, ASYNC_REG_RETRY_LIMIT); + } + + @Override + void processAction(RegisterTrigger triggerRegistration) throws IOException { + prepareRegistrationServer(triggerRegistration); + Assert.assertEquals( + "MeasurementImpl.register trigger failed", + RESULT_OK, + mMeasurementImpl.register( + triggerRegistration.mRegistrationRequest, triggerRegistration.mTimestamp)); + mAsyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker( + MAX_RECORDS_PROCESSED, ASYNC_REG_RETRY_LIMIT); + Assert.assertTrue("AttributionJobHandler.performPendingAttributions returned false", + mAttributionHelper.performPendingAttributions()); + } + + @Override + void processAction(RegisterWebTrigger triggerRegistration) throws IOException { + prepareRegistrationServer(triggerRegistration); + Assert.assertEquals( + "MeasurementImpl.registerWebTrigger failed", + RESULT_OK, + mMeasurementImpl.registerWebTrigger( + triggerRegistration.mRegistrationRequest, triggerRegistration.mTimestamp)); + mAsyncRegistrationQueueRunner.runAsyncRegistrationQueueWorker( + MAX_RECORDS_PROCESSED, ASYNC_REG_RETRY_LIMIT); + Assert.assertTrue( + "AttributionJobHandler.performPendingAttributions returned false", + mAttributionHelper.performPendingAttributions()); + } + + @Override + void processAction(InstallApp installApp) { + Assert.assertTrue( + "measurementDao.doInstallAttribution failed", + sDatastoreManager.runInTransaction( + measurementDao -> + measurementDao.doInstallAttribution( + installApp.mUri, installApp.mTimestamp))); + } + + @Override + void processAction(UninstallApp uninstallApp) { + Assert.assertTrue( + "measurementDao.undoInstallAttribution failed", + sDatastoreManager.runInTransaction( + measurementDao -> { + measurementDao.deleteAppRecords(uninstallApp.mUri); + measurementDao.undoInstallAttribution(uninstallApp.mUri); + })); + } + + @Override + void processAction(EventReportingJob reportingJob) throws IOException, JSONException { Object[] eventCaptures = EventReportingJobHandlerWrapper .spyPerformScheduledPendingReportsInWindow( sEnrollmentDao, @@ -146,19 +279,22 @@ public abstract class E2EMockTest extends E2ETest { - SystemHealthParams.MAX_EVENT_REPORT_UPLOAD_RETRY_WINDOW_MS, reportingJob.mTimestamp); + processEventReports( + (List<EventReport>) eventCaptures[0], + (List<Uri>) eventCaptures[1], + (List<JSONObject>) eventCaptures[2]); + } + + @Override + void processAction(AggregateReportingJob reportingJob) throws IOException, JSONException { Object[] aggregateCaptures = AggregateReportingJobHandlerWrapper .spyPerformScheduledPendingReportsInWindow( sEnrollmentDao, sDatastoreManager, reportingJob.mTimestamp - - SystemHealthParams.MAX_EVENT_REPORT_UPLOAD_RETRY_WINDOW_MS, + - SystemHealthParams.MAX_AGGREGATE_REPORT_UPLOAD_RETRY_WINDOW_MS, reportingJob.mTimestamp); - processEventReports( - (List<EventReport>) eventCaptures[0], - (List<Uri>) eventCaptures[1], - (List<JSONObject>) eventCaptures[2]); - processAggregateReports( (List<Uri>) aggregateCaptures[0], (List<JSONObject>) aggregateCaptures[1]); @@ -198,8 +334,8 @@ public abstract class E2EMockTest extends E2ETest { for (int i = 0; i < destinations.size(); i++) { JSONObject sharedInfo = new JSONObject(payloads.get(i).getString("shared_info")); result.add(new JSONObject() - .put(TestFormatJsonMapping.REPORT_TIME_KEY, - sharedInfo.getLong("scheduled_report_time") * 1000) + .put(TestFormatJsonMapping.REPORT_TIME_KEY, String.valueOf( + sharedInfo.getLong("scheduled_report_time") * 1000)) .put(TestFormatJsonMapping.REPORT_TO_KEY, destinations.get(i).toString()) .put(TestFormatJsonMapping.PAYLOAD_KEY, getAggregatablePayloadForTest(sharedInfo, payloads.get(i)))); @@ -212,37 +348,55 @@ public abstract class E2EMockTest extends E2ETest { String payload = data.getJSONArray("aggregation_service_payloads") .getJSONObject(0) - .getString("debug_cleartext_payload"); - return new JSONObject() - .put( - AggregateReportPayloadKeys.ATTRIBUTION_DESTINATION, - sharedInfo.getString("attribution_destination")) - .put(AggregateReportPayloadKeys.HISTOGRAMS, getAggregateHistograms(payload)); + .getString("payload"); + + final byte[] decryptedPayload = + HpkeJni.decrypt( + decode(AggregateCryptoFixture.getPrivateKeyBase64()), + decode(payload), + (AggregateCryptoFixture.getSharedInfoPrefix() + sharedInfo.toString()) + .getBytes()); + + String sourceDebugKey = data.optString(AggregateReportPayloadKeys.SOURCE_DEBUG_KEY); + String triggerDebugKey = data.optString(AggregateReportPayloadKeys.TRIGGER_DEBUG_KEY); + JSONObject aggregateJson = + new JSONObject() + .put( + AggregateReportPayloadKeys.ATTRIBUTION_DESTINATION, + sharedInfo.getString("attribution_destination")) + .put( + AggregateReportPayloadKeys.HISTOGRAMS, + getAggregateHistograms(decryptedPayload)); + if (!sourceDebugKey.isEmpty()) { + aggregateJson.put(AggregateReportPayloadKeys.SOURCE_DEBUG_KEY, sourceDebugKey); + } + if (!triggerDebugKey.isEmpty()) { + aggregateJson.put(AggregateReportPayloadKeys.TRIGGER_DEBUG_KEY, triggerDebugKey); + } + return aggregateJson; } - private static JSONArray getAggregateHistograms(String payloadJsonBase64) throws JSONException { + private static JSONArray getAggregateHistograms(byte[] encodedCborPayload) + throws JSONException { List<JSONObject> result = new ArrayList<>(); try { - final byte[] payloadJson = Base64.getDecoder().decode(payloadJsonBase64); final List<DataItem> dataItems = - new CborDecoder(new ByteArrayInputStream(payloadJson)).decode(); + new CborDecoder(new ByteArrayInputStream(encodedCborPayload)).decode(); final co.nstant.in.cbor.model.Map payload = (co.nstant.in.cbor.model.Map) dataItems.get(0); final Array payloadArray = (Array) payload.get(new UnicodeString("data")); for (DataItem i : payloadArray.getDataItems()) { co.nstant.in.cbor.model.Map m = (co.nstant.in.cbor.model.Map) i; + Object value = + "0x" + + new BigInteger( + ((ByteString) m.get(new UnicodeString("bucket"))) + .getBytes()) + .toString(16); result.add( new JSONObject() - .put( - AggregateHistogramKeys.BUCKET, - new BigInteger( - ((ByteString) - m.get( - new UnicodeString( - "bucket"))) - .getBytes()) - .toString()) + .put(AggregateHistogramKeys.BUCKET, value) .put( AggregateHistogramKeys.VALUE, new BigInteger( @@ -251,7 +405,7 @@ public abstract class E2EMockTest extends E2ETest { new UnicodeString( "value"))) .getBytes()) - .toString())); + .intValue())); } } catch (CborException e) { throw new JSONException(e); @@ -260,9 +414,53 @@ public abstract class E2EMockTest extends E2ETest { return new JSONArray(result); } - private static Map<String, List<String>> getNextResponse( + protected static Map<String, List<String>> getNextResponse( Map<String, List<Map<String, List<String>>>> uriToResponseHeadersMap, String uri) { List<Map<String, List<String>>> responseList = uriToResponseHeadersMap.get(uri); return responseList.remove(0); } + + void updateEnrollment(String uri) { + if (mSeenUris.contains(uri)) { + return; + } + mSeenUris.add(uri); + String enrollmentId = getEnrollmentId(uri); + Set<String> attributionRegistrationUrls; + EnrollmentData enrollmentData = sEnrollmentDao.getEnrollmentData(enrollmentId); + if (enrollmentData != null) { + sEnrollmentDao.delete(enrollmentId); + attributionRegistrationUrls = new HashSet<>( + enrollmentData.getAttributionSourceRegistrationUrl()); + attributionRegistrationUrls.addAll( + enrollmentData.getAttributionTriggerRegistrationUrl()); + attributionRegistrationUrls.add(uri); + } else { + attributionRegistrationUrls = Set.of(uri); + } + Uri registrationUri = Uri.parse(uri); + String reportingUrl = registrationUri.getScheme() + "://" + registrationUri.getAuthority(); + insertEnrollment(enrollmentId, reportingUrl, new ArrayList<>(attributionRegistrationUrls)); + } + + private void insertEnrollment(String enrollmentId, String reportingUrl, + List<String> attributionRegistrationUrls) { + EnrollmentData enrollmentData = new EnrollmentData.Builder() + .setEnrollmentId(enrollmentId) + .setAttributionSourceRegistrationUrl(attributionRegistrationUrls) + .setAttributionTriggerRegistrationUrl(attributionRegistrationUrls) + .setAttributionReportingUrl(List.of(reportingUrl)) + .build(); + Assert.assertTrue(sEnrollmentDao.insert(enrollmentData)); + } + + private String getEnrollmentId(String uri) { + String authority = Uri.parse(uri).getAuthority(); + return mUriToEnrollmentId.computeIfAbsent(authority, k -> + "enrollment-id-" + mEnrollmentCount.incrementAndGet()); + } + + private static byte[] decode(String value) { + return Base64.getDecoder().decode(value.getBytes()); + } } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2ETest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2ETest.java index be05aa9275..ca35680758 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2ETest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/E2ETest.java @@ -19,11 +19,14 @@ package com.android.adservices.service.measurement; import static android.view.MotionEvent.ACTION_BUTTON_PRESS; import static android.view.MotionEvent.obtain; -import static com.android.adservices.ResultCode.RESULT_OK; +import static com.android.adservices.service.measurement.reporting.AggregateReportSender.AGGREGATE_ATTRIBUTION_REPORT_URI_PATH; +import static com.android.adservices.service.measurement.reporting.EventReportSender.EVENT_ATTRIBUTION_REPORT_URI_PATH; import android.content.AttributionSource; import android.content.Context; import android.content.res.AssetManager; +import android.database.Cursor; +import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.view.InputDevice; import android.view.InputEvent; @@ -33,22 +36,17 @@ import android.view.MotionEvent.PointerProperties; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; -import com.android.adservices.data.DbHelper; -import com.android.adservices.data.enrollment.EnrollmentDao; -import com.android.adservices.data.measurement.DatastoreManager; -import com.android.adservices.data.measurement.DatastoreManagerFactory; -import com.android.adservices.service.enrollment.EnrollmentData; +import com.android.adservices.data.DbTestUtil; import com.android.adservices.service.measurement.actions.Action; +import com.android.adservices.service.measurement.actions.AggregateReportingJob; +import com.android.adservices.service.measurement.actions.EventReportingJob; import com.android.adservices.service.measurement.actions.InstallApp; import com.android.adservices.service.measurement.actions.RegisterSource; import com.android.adservices.service.measurement.actions.RegisterTrigger; import com.android.adservices.service.measurement.actions.RegisterWebSource; import com.android.adservices.service.measurement.actions.RegisterWebTrigger; import com.android.adservices.service.measurement.actions.ReportObjects; -import com.android.adservices.service.measurement.actions.ReportingJob; import com.android.adservices.service.measurement.actions.UninstallApp; -import com.android.adservices.service.measurement.attribution.AttributionJobHandlerWrapper; -import com.android.modules.utils.testing.TestableDeviceConfig; import com.google.common.collect.ImmutableList; @@ -56,7 +54,6 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.junit.Assert; -import org.junit.Rule; import org.junit.Test; import java.io.IOException; @@ -73,7 +70,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.UUID; import java.util.concurrent.TimeUnit; /** @@ -84,61 +80,24 @@ import java.util.concurrent.TimeUnit; * Consider @RunWith(Parameterized.class) */ public abstract class E2ETest { - @Rule - public final TestableDeviceConfig.TestableDeviceConfigRule mDeviceConfigRule = - new TestableDeviceConfig.TestableDeviceConfigRule(); - // Used to fuzzy-match expected report (not delivery) time private static final long REPORT_TIME_EPSILON = TimeUnit.HOURS.toMillis(2); - private static final EnrollmentData AD_TECH_1 = - new EnrollmentData.Builder() - .setEnrollmentId(UUID.randomUUID().toString()) - .setCompanyId("ad-tech-1") - .setSdkNames("sdk") - .setAttributionSourceRegistrationUrl(Arrays.asList("https://www.ad-tech1.com")) - .setAttributionTriggerRegistrationUrl(Arrays.asList("https://www.ad-tech1.com")) - .setAttributionReportingUrl(Arrays.asList("https://www.ad-tech1.com")) - .setRemarketingResponseBasedRegistrationUrl( - Arrays.asList("https://www.ad-tech1.com")) - .setEncryptionKeyUrl(Arrays.asList("https://www.ad-tech1.com/keys")) - .build(); - private static final EnrollmentData AD_TECH_2 = - new EnrollmentData.Builder() - .setEnrollmentId(UUID.randomUUID().toString()) - .setCompanyId("ad-tech-2") - .setSdkNames("sdk") - .setAttributionSourceRegistrationUrl(Arrays.asList("https://www.ad-tech2.com")) - .setAttributionTriggerRegistrationUrl(Arrays.asList("https://www.ad-tech2.com")) - .setAttributionReportingUrl(Arrays.asList("https://www.ad-tech2.com")) - .setRemarketingResponseBasedRegistrationUrl( - Arrays.asList("https://www.ad-tech2.com")) - .setEncryptionKeyUrl(Arrays.asList("https://www.ad-tech2.com/keys")) - .build(); - private static final EnrollmentData AD_TECH_3 = - new EnrollmentData.Builder() - .setEnrollmentId(UUID.randomUUID().toString()) - .setCompanyId("ad-tech-3") - .setSdkNames("sdk") - .setAttributionSourceRegistrationUrl(Arrays.asList("https://www.ad-tech3.com")) - .setAttributionTriggerRegistrationUrl(Arrays.asList("https://www.ad-tech3.com")) - .setAttributionReportingUrl(Arrays.asList("https://www.ad-tech3.com")) - .setRemarketingResponseBasedRegistrationUrl( - Arrays.asList("https://www.ad-tech3.com")) - .setEncryptionKeyUrl(Arrays.asList("https://www.ad-tech3.com/keys")) - .build(); static final Context sContext = ApplicationProvider.getApplicationContext(); - static final EnrollmentDao sEnrollmentDao = EnrollmentDao.getInstance( - ApplicationProvider.getApplicationContext()); - static final DatastoreManager sDatastoreManager = DatastoreManagerFactory.getDatastoreManager( - ApplicationProvider.getApplicationContext()); private final Collection<Action> mActionsList; final ReportObjects mExpectedOutput; // Extenders of the class populate in their own ways this container for actual output. final ReportObjects mActualOutput; - // Class extensions may choose to disable or enable added noise. - AttributionJobHandlerWrapper mAttributionHelper; - MeasurementImpl mMeasurementImpl; + + enum ReportType { + EVENT, + AGGREGATE + } + + private enum OutputType { + EXPECTED, + ACTUAL + } private interface EventReportPayloadKeys { // Keys used to compare actual with expected output @@ -153,6 +112,8 @@ public abstract class E2ETest { interface AggregateReportPayloadKeys { String ATTRIBUTION_DESTINATION = "attribution_destination"; String HISTOGRAMS = "histograms"; + String SOURCE_DEBUG_KEY = "source_debug_key"; + String TRIGGER_DEBUG_KEY = "trigger_debug_key"; } interface AggregateHistogramKeys { @@ -161,6 +122,7 @@ public abstract class E2ETest { } public interface TestFormatJsonMapping { + String API_CONFIG_KEY = "api_config"; String TEST_INPUT_KEY = "input"; String TEST_OUTPUT_KEY = "output"; String SOURCE_REGISTRATIONS_KEY = "sources"; @@ -174,12 +136,14 @@ public abstract class E2ETest { String URI_TO_RESPONSE_HEADERS_RESPONSE_KEY = "response"; String REGISTRATION_REQUEST_KEY = "registration_request"; String ATTRIBUTION_SOURCE_KEY = "registrant"; + String ATTRIBUTION_SOURCE_DEFAULT = "com.interop.app"; String SOURCE_TOP_ORIGIN_URI_KEY = "source_origin"; String TRIGGER_TOP_ORIGIN_URI_KEY = "destination_origin"; String SOURCE_APP_DESTINATION_URI_KEY = "app_destination"; String SOURCE_WEB_DESTINATION_URI_KEY = "web_destination"; String SOURCE_VERIFIED_DESTINATION_URI_KEY = "verified_destination"; String REGISTRATION_URI_KEY = "attribution_src_url"; + String IS_ADID_PERMISSION_GRANTED_KEY = "is_adid_permission_granted"; String INPUT_EVENT_KEY = "source_type"; String SOURCE_VIEW_TYPE = "event"; String TIMESTAMP_KEY = "timestamp"; @@ -192,7 +156,90 @@ public abstract class E2ETest { String REPORT_TIME_KEY = "report_time"; String REPORT_TO_KEY = "report_url"; String PAYLOAD_KEY = "payload"; - String DEBUG_KEY = "debug_key"; + } + + private interface ApiConfigKeys { + String RATE_LIMIT_MAX_ATTRIBUTIONS = "rate_limit_max_attributions"; + String NAVIGATION_SOURCE_TRIGGER_DATA_CARDINALITY = + "navigation_source_trigger_data_cardinality"; + String RATE_LIMIT_MAX_ATTRIBUTION_REPORTING_ORIGINS = + "rate_limit_max_attribution_reporting_origins"; + String MAX_DESTINATIONS_PER_SOURCE_SITE_REPORTING_ORIGIN = + "max_destinations_per_source_site_reporting_origin"; + String RATE_LIMIT_MAX_SOURCE_REGISTRATION_REPORTING_ORIGINS = + "rate_limit_max_source_registration_reporting_origins"; + } + + public static class PrivacyParamsProvider { + private Integer mMaxAttributionPerRateLimitWindow; + private Integer mNavigationTriggerDataCardinality; + private Integer mMaxDistinctEnrollmentsPerPublisherXDestinationInAttribution; + private Integer mMaxDistinctDestinationsPerPublisherXEnrollmentInActiveSource; + private Integer mMaxDistinctEnrollmentsPerPublisherXDestinationInSource; + + public PrivacyParamsProvider(JSONObject json) throws JSONException { + if (!json.isNull(ApiConfigKeys.RATE_LIMIT_MAX_ATTRIBUTIONS)) { + mMaxAttributionPerRateLimitWindow = json.getInt( + ApiConfigKeys.RATE_LIMIT_MAX_ATTRIBUTIONS); + } else { + mMaxAttributionPerRateLimitWindow = + PrivacyParams.getMaxAttributionPerRateLimitWindow(); + } + if (!json.isNull(ApiConfigKeys.NAVIGATION_SOURCE_TRIGGER_DATA_CARDINALITY)) { + mNavigationTriggerDataCardinality = json.getInt( + ApiConfigKeys.NAVIGATION_SOURCE_TRIGGER_DATA_CARDINALITY); + } else { + mNavigationTriggerDataCardinality = + PrivacyParams.getNavigationTriggerDataCardinality(); + } + if (!json.isNull(ApiConfigKeys + .RATE_LIMIT_MAX_ATTRIBUTION_REPORTING_ORIGINS)) { + mMaxDistinctEnrollmentsPerPublisherXDestinationInAttribution = json.getInt( + ApiConfigKeys.RATE_LIMIT_MAX_ATTRIBUTION_REPORTING_ORIGINS); + } else { + mMaxDistinctEnrollmentsPerPublisherXDestinationInAttribution = + PrivacyParams + .getMaxDistinctEnrollmentsPerPublisherXDestinationInAttribution(); + } + if (!json.isNull(ApiConfigKeys + .MAX_DESTINATIONS_PER_SOURCE_SITE_REPORTING_ORIGIN)) { + mMaxDistinctDestinationsPerPublisherXEnrollmentInActiveSource = json.getInt( + ApiConfigKeys.MAX_DESTINATIONS_PER_SOURCE_SITE_REPORTING_ORIGIN); + } else { + mMaxDistinctDestinationsPerPublisherXEnrollmentInActiveSource = + PrivacyParams + .getMaxDistinctDestinationsPerPublisherXEnrollmentInActiveSource(); + } + if (!json.isNull(ApiConfigKeys + .RATE_LIMIT_MAX_SOURCE_REGISTRATION_REPORTING_ORIGINS)) { + mMaxDistinctEnrollmentsPerPublisherXDestinationInSource = json.getInt( + ApiConfigKeys.RATE_LIMIT_MAX_SOURCE_REGISTRATION_REPORTING_ORIGINS); + } else { + mMaxDistinctEnrollmentsPerPublisherXDestinationInSource = + PrivacyParams + .getMaxDistinctEnrollmentsPerPublisherXDestinationInSource(); + } + } + + public Integer getMaxAttributionPerRateLimitWindow() { + return mMaxAttributionPerRateLimitWindow; + } + + public Integer getNavigationTriggerDataCardinality() { + return mNavigationTriggerDataCardinality; + } + + public Integer getMaxDistinctEnrollmentsPerPublisherXDestinationInAttribution() { + return mMaxDistinctEnrollmentsPerPublisherXDestinationInAttribution; + } + + public Integer getMaxDistinctDestinationsPerPublisherXEnrollmentInActiveSource() { + return mMaxDistinctDestinationsPerPublisherXEnrollmentInActiveSource; + } + + public Integer getMaxDistinctEnrollmentsPerPublisherXDestinationInSource() { + return mMaxDistinctEnrollmentsPerPublisherXDestinationInSource; + } } static Collection<Object[]> data(String testDirName) throws IOException, JSONException { @@ -205,8 +252,8 @@ public abstract class E2ETest { return getTestCasesFrom(inputStreams, testDirectoryList); } - public static Map<String, List<Map<String, List<String>>>> - getUriToResponseHeadersMap(JSONObject obj) throws JSONException { + public static Map<String, List<Map<String, List<String>>>> getUriToResponseHeadersMap( + JSONObject obj) throws JSONException { JSONArray uriToResArray = obj.getJSONArray( TestFormatJsonMapping.URI_TO_RESPONSE_HEADERS_KEY); Map<String, List<Map<String, List<String>>>> uriToResponseHeadersMap = new HashMap<>(); @@ -270,8 +317,16 @@ public abstract class E2ETest { 0 /*int flags*/); } + static String getReportUrl(ReportType reportType, String origin) { + return origin + + "/" + + (reportType == ReportType.EVENT + ? EVENT_ATTRIBUTION_REPORT_URI_PATH + : AGGREGATE_ATTRIBUTION_REPORT_URI_PATH); + } + static void clearDatabase() { - SQLiteDatabase db = DbHelper.getInstance(sContext).getWritableDatabase(); + SQLiteDatabase db = DbTestUtil.getDbHelperForTest().getWritableDatabase(); emptyTables(db); } @@ -286,8 +341,6 @@ public abstract class E2ETest { @Test public void runTest() throws IOException, JSONException { clearDatabase(); - unseedEnrollment(); - seedEnrollment(); for (Action action : mActionsList) { if (action instanceof RegisterSource) { processAction((RegisterSource) action); @@ -297,8 +350,10 @@ public abstract class E2ETest { processAction((RegisterWebSource) action); } else if (action instanceof RegisterWebTrigger) { processAction((RegisterWebTrigger) action); - } else if (action instanceof ReportingJob) { - processAction((ReportingJob) action); + } else if (action instanceof EventReportingJob) { + processAction((EventReportingJob) action); + } else if (action instanceof AggregateReportingJob) { + processAction((AggregateReportingJob) action); } else if (action instanceof InstallApp) { processAction((InstallApp) action); } else if (action instanceof UninstallApp) { @@ -307,14 +362,20 @@ public abstract class E2ETest { } evaluateResults(); clearDatabase(); - unseedEnrollment(); } /** * The reporting job may be handled differently depending on whether network requests are mocked * or a test server is used. */ - abstract void processAction(ReportingJob reportingJob) throws IOException, JSONException; + abstract void processAction(EventReportingJob reportingJob) throws IOException, JSONException; + + /** + * The reporting job may be handled differently depending on whether network requests are mocked + * or a test server is used. + */ + abstract void processAction(AggregateReportingJob reportingJob) + throws IOException, JSONException; /** * Override with HTTP response mocks, for example. @@ -336,44 +397,57 @@ public abstract class E2ETest { abstract void prepareRegistrationServer(RegisterWebTrigger triggerRegistration) throws IOException; - private static int hashForEventReportObject(JSONObject obj) { + private static int hashForEventReportObject(OutputType outputType, JSONObject obj) { int n = EventReportPayloadKeys.STRINGS.size(); Object[] objArray = new Object[n + 2]; // We cannot use report time due to fuzzy matching between actual and expected output. - objArray[0] = obj.optString(TestFormatJsonMapping.REPORT_TO_KEY, ""); + String url = obj.optString(TestFormatJsonMapping.REPORT_TO_KEY, ""); + objArray[0] = + outputType == OutputType.EXPECTED ? url : getReportUrl(ReportType.EVENT, url); JSONObject payload = obj.optJSONObject(TestFormatJsonMapping.PAYLOAD_KEY); - objArray[1] = payload.optDouble(EventReportPayloadKeys.DOUBLE, 0); + objArray[1] = normaliseDouble(payload.optDouble(EventReportPayloadKeys.DOUBLE, 0)); for (int i = 0; i < n; i++) { objArray[i + 2] = payload.optString(EventReportPayloadKeys.STRINGS.get(i), ""); } return Arrays.hashCode(objArray); } - private static int hashForAggregateReportObject(JSONObject obj) { - Object[] objArray = new Object[3]; + private static int hashForAggregateReportObject(OutputType outputType, + JSONObject obj) { + Object[] objArray = new Object[5]; // We cannot use report time due to fuzzy matching between actual and expected output. - objArray[0] = obj.optString(TestFormatJsonMapping.REPORT_TO_KEY, ""); + String url = obj.optString(TestFormatJsonMapping.REPORT_TO_KEY, ""); + objArray[0] = + outputType == OutputType.EXPECTED ? url : getReportUrl(ReportType.AGGREGATE, url); JSONObject payload = obj.optJSONObject(TestFormatJsonMapping.PAYLOAD_KEY); objArray[1] = payload.optString(AggregateReportPayloadKeys.ATTRIBUTION_DESTINATION, ""); // To compare histograms, we already converted them to an ordered string of value pairs. objArray[2] = getComparableHistograms( payload.optJSONArray(AggregateReportPayloadKeys.HISTOGRAMS)); + objArray[3] = payload.optString(AggregateReportPayloadKeys.SOURCE_DEBUG_KEY, ""); + objArray[4] = payload.optString(AggregateReportPayloadKeys.TRIGGER_DEBUG_KEY, ""); return Arrays.hashCode(objArray); } + // Used in interop tests, where we have known discrepancies. + private static double normaliseDouble(double d) { + return d == 0.0024263D ? 0.0024D : d; + } + private static long reportTimeFrom(JSONObject obj) { return obj.optLong(TestFormatJsonMapping.REPORT_TIME_KEY, 0); } - private static boolean matchReportTimeAndReportTo(JSONObject obj1, JSONObject obj2) - throws JSONException { + // 'obj1' is the expected result, 'obj2' is the actual result. + private static boolean matchReportTimeAndReportTo(ReportType reportType, JSONObject obj1, + JSONObject obj2) throws JSONException { if (Math.abs(obj1.getLong(TestFormatJsonMapping.REPORT_TIME_KEY) - obj2.getLong(TestFormatJsonMapping.REPORT_TIME_KEY)) > REPORT_TIME_EPSILON) { return false; } if (!obj1.getString(TestFormatJsonMapping.REPORT_TO_KEY).equals( - obj2.getString(TestFormatJsonMapping.REPORT_TO_KEY))) { + getReportUrl(reportType, obj2.getString(TestFormatJsonMapping.REPORT_TO_KEY)))) { return false; } return true; @@ -383,8 +457,8 @@ public abstract class E2ETest { throws JSONException { JSONObject payload1 = obj1.getJSONObject(TestFormatJsonMapping.PAYLOAD_KEY); JSONObject payload2 = obj2.getJSONObject(TestFormatJsonMapping.PAYLOAD_KEY); - if (payload1.getDouble(EventReportPayloadKeys.DOUBLE) - != payload2.getDouble(EventReportPayloadKeys.DOUBLE)) { + if (normaliseDouble(payload1.getDouble(EventReportPayloadKeys.DOUBLE)) + != normaliseDouble(payload2.getDouble(EventReportPayloadKeys.DOUBLE))) { return false; } for (String key : EventReportPayloadKeys.STRINGS) { @@ -392,7 +466,7 @@ public abstract class E2ETest { return false; } } - return matchReportTimeAndReportTo(obj1, obj2); + return matchReportTimeAndReportTo(ReportType.EVENT, obj1, obj2); } private static boolean areEqualAggregateReportJsons(JSONObject obj1, JSONObject obj2) @@ -403,12 +477,20 @@ public abstract class E2ETest { payload2.optString(AggregateReportPayloadKeys.ATTRIBUTION_DESTINATION, ""))) { return false; } + if (!payload1.optString(AggregateReportPayloadKeys.SOURCE_DEBUG_KEY, "") + .equals(payload2.optString(AggregateReportPayloadKeys.SOURCE_DEBUG_KEY, ""))) { + return false; + } + if (!payload1.optString(AggregateReportPayloadKeys.TRIGGER_DEBUG_KEY, "") + .equals(payload2.optString(AggregateReportPayloadKeys.TRIGGER_DEBUG_KEY, ""))) { + return false; + } JSONArray histograms1 = payload1.optJSONArray(AggregateReportPayloadKeys.HISTOGRAMS); JSONArray histograms2 = payload2.optJSONArray(AggregateReportPayloadKeys.HISTOGRAMS); if (!getComparableHistograms(histograms1).equals(getComparableHistograms(histograms2))) { return false; } - return matchReportTimeAndReportTo(obj1, obj2); + return matchReportTimeAndReportTo(ReportType.AGGREGATE, obj1, obj2); } private static String getComparableHistograms(@Nullable JSONArray arr) { @@ -429,20 +511,24 @@ public abstract class E2ETest { } } - private static void sortEventReportObjects(List<JSONObject> eventReportObjects) { + private static void sortEventReportObjects(OutputType outputType, + List<JSONObject> eventReportObjects) { eventReportObjects.sort( // Report time can vary across implementations so cannot be included in the hash; // they should be similarly ordered, however, so we can use them to sort. Comparator.comparing(E2ETest::reportTimeFrom) - .thenComparing(E2ETest::hashForEventReportObject)); + .thenComparing(obj -> hashForEventReportObject(outputType, obj))); } - private static void sortAggregateReportObjects(List<JSONObject> aggregateReportObjects) { + private static void sortAggregateReportObjects(OutputType outputType, + List<JSONObject> aggregateReportObjects) { aggregateReportObjects.sort( - // Report time can vary across implementations so cannot be included in the hash; - // they should be similarly ordered, however, so we can use them to sort. - Comparator.comparing(E2ETest::reportTimeFrom) - .thenComparing(E2ETest::hashForAggregateReportObject)); + // Unlike event reports (sorted elsewhere in this file), aggregate reports are + // scheduled with randomised times, and using report time for sorting can result + // in unexpected variations in the sort order, depending on test timing. Without + // time ordering, we rely on other data across the reports to yield different + // hash codes. + Comparator.comparing(obj -> hashForAggregateReportObject(outputType, obj))); } private static boolean areEqual(ReportObjects p1, ReportObjects p2) throws JSONException { @@ -467,13 +553,22 @@ public abstract class E2ETest { private static String getTestFailureMessage(ReportObjects expectedOutput, ReportObjects actualOutput) { - return String.format("Actual output does not match expected.\n\n" - + "(Note that report IDs are ignored in comparisons since they are not known in" - + " advance.)\n\nEvent report objects:\n%s\n\n" - + "Expected aggregate report objects: %s\n\n" - + "Actual aggregate report objects: %s\n", - prettify(expectedOutput.mEventReportObjects, actualOutput.mEventReportObjects), - expectedOutput.mAggregateReportObjects, actualOutput.mAggregateReportObjects); + return String.format( + "Actual output does not match expected.\n\n" + + "(Note that displayed randomized_trigger_rate and report_url are not" + + " normalised.\n" + + "Note that report IDs are ignored in comparisons since they are not" + + " known in advance.)\n\n" + + "Event report objects:\n" + + "%s\n\n" + + "Expected aggregate report objects: %s\n\n" + + "Actual aggregate report objects: %s\n", + prettify( + expectedOutput.mEventReportObjects, + actualOutput.mEventReportObjects), + expectedOutput.mAggregateReportObjects, + actualOutput.mAggregateReportObjects) + + getDatastoreState(); } private static String prettify(List<JSONObject> expected, List<JSONObject> actual) { @@ -502,6 +597,11 @@ public abstract class E2ETest { .append(" ::: ") .append(obj2.optString(TestFormatJsonMapping.REPORT_TIME_KEY)) .append("\n"); + result.append(TestFormatJsonMapping.REPORT_TO_KEY + ": ") + .append(obj1.optString(TestFormatJsonMapping.REPORT_TO_KEY)) + .append(" ::: ") + .append(obj2.optString(TestFormatJsonMapping.REPORT_TO_KEY)) + .append("\n"); JSONObject payload1 = obj1.optJSONObject(TestFormatJsonMapping.PAYLOAD_KEY); JSONObject payload2 = obj2.optJSONObject(TestFormatJsonMapping.PAYLOAD_KEY); for (String key : EventReportPayloadKeys.STRINGS) { @@ -535,6 +635,47 @@ public abstract class E2ETest { return result.toString(); } + protected static String getDatastoreState() { + StringBuilder result = new StringBuilder(); + SQLiteDatabase db = DbTestUtil.getDbHelperForTest().getWritableDatabase(); + List<String> tableNames = + ImmutableList.of( + "msmt_source", + "msmt_trigger", + "msmt_attribution", + "msmt_event_report", + "msmt_aggregate_report", + "enrollment_data", + "msmt_async_registration_contract"); + for (String tableName : tableNames) { + result.append("\n" + tableName + ":\n"); + result.append(getTableState(db, tableName)); + } + return result.toString(); + } + + private static String getTableState(SQLiteDatabase db, String tableName) { + Cursor cursor = getAllRows(db, tableName); + StringBuilder result = new StringBuilder(); + while (cursor.moveToNext()) { + result.append("\n" + DatabaseUtils.dumpCurrentRowToString(cursor)); + } + return result.toString(); + } + + private static Cursor getAllRows(SQLiteDatabase db, String tableName) { + return db.query( + /* boolean distinct */ false, + tableName, + /* String[] columns */ null, + /* String selection */ null, + /* String[] selectionArgs */ null, + /* String groupBy */ null, + /* String having */ null, + /* String orderBy */ null, + /* String limit */ null); + } + private static Set<Long> getExpiryTimesFrom( Collection<List<Map<String, List<String>>>> responseHeadersCollection) throws JSONException { @@ -556,7 +697,7 @@ public abstract class E2ETest { return expiryTimes; } - private static Set<Action> maybeAddReportingJobTimes( + private static Set<Action> maybeAddEventReportingJobTimes( long sourceTime, Collection<List<Map<String, List<String>>>> responseHeaders) throws JSONException { Set<Action> reportingJobsActions = new HashSet<>(); @@ -569,7 +710,8 @@ public abstract class E2ETest { validExpiry = PrivacyParams.MIN_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS; } long jobTime = sourceTime + 1000 * validExpiry + 3600000L; - reportingJobsActions.add(new ReportingJob(jobTime)); + + reportingJobsActions.add(new EventReportingJob(jobTime)); } return reportingJobsActions; @@ -580,7 +722,8 @@ public abstract class E2ETest { * tests. * * @return A collection of Object arrays, each with - * {@code [Collection<Object> actions, ReportObjects expectedOutput, String name]} + * {@code [Collection<Object> actions, ReportObjects expectedOutput, + * PrivacyParamsProvider privacyParamsProvider, String name]} */ private static Collection<Object[]> getTestCasesFrom(List<InputStream> inputStreams, String[] filenames) throws IOException, JSONException { @@ -593,7 +736,7 @@ public abstract class E2ETest { inputStreams.get(i).close(); String json = new String(buffer, StandardCharsets.UTF_8); - JSONObject testObj = new JSONObject(json); + JSONObject testObj = new JSONObject(json.replaceAll("\\.test(?=[\"\\/])", ".com")); String name = filenames[i]; JSONObject input = testObj.getJSONObject(TestFormatJsonMapping.TEST_INPUT_KEY); JSONObject output = testObj.getJSONObject(TestFormatJsonMapping.TEST_OUTPUT_KEY); @@ -602,7 +745,7 @@ public abstract class E2ETest { List<Action> actions = new ArrayList<>(); actions.addAll(createSourceBasedActions(input)); - actions.addAll(createTriggerActions(input)); + actions.addAll(createTriggerBasedActions(input)); actions.addAll(createInstallActions(input)); actions.addAll(createUninstallActions(input)); @@ -610,7 +753,13 @@ public abstract class E2ETest { ReportObjects expectedOutput = getExpectedOutput(output); - testCases.add(new Object[] {actions, expectedOutput, name}); + JSONObject ApiConfigObj = testObj.isNull(TestFormatJsonMapping.API_CONFIG_KEY) + ? new JSONObject() + : testObj.getJSONObject(TestFormatJsonMapping.API_CONFIG_KEY); + + PrivacyParamsProvider privacyParamsProvider = new PrivacyParamsProvider(ApiConfigObj); + + testCases.add(new Object[] {actions, expectedOutput, privacyParamsProvider, name}); } return testCases; @@ -619,7 +768,7 @@ public abstract class E2ETest { private static List<Action> createSourceBasedActions(JSONObject input) throws JSONException { List<Action> actions = new ArrayList<>(); // Set avoids duplicate reporting times across sources to do attribution upon. - Set<Action> reportingJobActions = new HashSet<>(); + Set<Action> eventReportingJobActions = new HashSet<>(); if (!input.isNull(TestFormatJsonMapping.SOURCE_REGISTRATIONS_KEY)) { JSONArray sourceRegistrationArray = input.getJSONArray( TestFormatJsonMapping.SOURCE_REGISTRATIONS_KEY); @@ -628,8 +777,8 @@ public abstract class E2ETest { new RegisterSource(sourceRegistrationArray.getJSONObject(j)); actions.add(sourceRegistration); // Add corresponding reporting job time actions - reportingJobActions.addAll( - maybeAddReportingJobTimes( + eventReportingJobActions.addAll( + maybeAddEventReportingJobTimes( sourceRegistration.mTimestamp, sourceRegistration.mUriToResponseHeadersMap.values())); } @@ -643,18 +792,20 @@ public abstract class E2ETest { new RegisterWebSource(webSourceRegistrationArray.getJSONObject(j)); actions.add(webSource); // Add corresponding reporting job time actions - reportingJobActions.addAll( - maybeAddReportingJobTimes( + eventReportingJobActions.addAll( + maybeAddEventReportingJobTimes( webSource.mTimestamp, webSource.mUriToResponseHeadersMap.values())); } } - actions.addAll(reportingJobActions); + actions.addAll(eventReportingJobActions); return actions; } - private static List<Action> createTriggerActions(JSONObject input) throws JSONException { + private static List<Action> createTriggerBasedActions(JSONObject input) throws JSONException { List<Action> actions = new ArrayList<>(); + long firstTriggerTime = Long.MAX_VALUE; + long lastTriggerTime = -1; if (!input.isNull(TestFormatJsonMapping.TRIGGER_KEY)) { JSONArray triggerRegistrationArray = input.getJSONArray(TestFormatJsonMapping.TRIGGER_KEY); @@ -662,6 +813,8 @@ public abstract class E2ETest { RegisterTrigger triggerRegistration = new RegisterTrigger(triggerRegistrationArray.getJSONObject(j)); actions.add(triggerRegistration); + firstTriggerTime = Math.min(firstTriggerTime, triggerRegistration.mTimestamp); + lastTriggerTime = Math.max(lastTriggerTime, triggerRegistration.mTimestamp); } } @@ -672,9 +825,31 @@ public abstract class E2ETest { RegisterWebTrigger webTrigger = new RegisterWebTrigger(webTriggerRegistrationArray.getJSONObject(j)); actions.add(webTrigger); + firstTriggerTime = Math.min(firstTriggerTime, webTrigger.mTimestamp); + lastTriggerTime = Math.max(lastTriggerTime, webTrigger.mTimestamp); } } + // Aggregate reports are scheduled close to trigger time. Add aggregate report jobs to cover + // the time span outlined by triggers. + List<Action> aggregateReportingJobActions = new ArrayList<>(); + long window = SystemHealthParams.MAX_AGGREGATE_REPORT_UPLOAD_RETRY_WINDOW_MS - 10; + long t = firstTriggerTime; + + do { + t += window; + aggregateReportingJobActions.add(new AggregateReportingJob(t)); + } while (t <= lastTriggerTime); + + // Account for edge case of t between lastTriggerTime and the latter's max report delay. + if (t <= lastTriggerTime + PrivacyParams.AGGREGATE_MAX_REPORT_DELAY) { + // t must be greater than lastTriggerTime so adding max report + // delay should be beyond the report delay for lastTriggerTime. + aggregateReportingJobActions.add(new AggregateReportingJob(t + + PrivacyParams.AGGREGATE_MAX_REPORT_DELAY)); + } + + actions.addAll(aggregateReportingJobActions); return actions; } @@ -706,36 +881,28 @@ public abstract class E2ETest { private static ReportObjects getExpectedOutput(JSONObject output) throws JSONException { List<JSONObject> eventReportObjects = new ArrayList<>(); - JSONArray eventReportObjectsArray = output.getJSONArray( - TestFormatJsonMapping.EVENT_REPORT_OBJECTS_KEY); - for (int i = 0; i < eventReportObjectsArray.length(); i++) { - JSONObject obj = eventReportObjectsArray.getJSONObject(i); - String adTechDomain = obj.getString(TestFormatJsonMapping.REPORT_TO_KEY); - eventReportObjects.add(obj.put(TestFormatJsonMapping.REPORT_TO_KEY, adTechDomain)); + if (!output.isNull(TestFormatJsonMapping.EVENT_REPORT_OBJECTS_KEY)) { + JSONArray eventReportObjectsArray = output.getJSONArray( + TestFormatJsonMapping.EVENT_REPORT_OBJECTS_KEY); + for (int i = 0; i < eventReportObjectsArray.length(); i++) { + JSONObject obj = eventReportObjectsArray.getJSONObject(i); + String adTechDomain = obj.getString(TestFormatJsonMapping.REPORT_TO_KEY); + eventReportObjects.add(obj.put(TestFormatJsonMapping.REPORT_TO_KEY, adTechDomain)); + } } List<JSONObject> aggregateReportObjects = new ArrayList<>(); - JSONArray aggregateReportObjectsArray = - output.getJSONArray(TestFormatJsonMapping.AGGREGATE_REPORT_OBJECTS_KEY); - for (int i = 0; i < aggregateReportObjectsArray.length(); i++) { - aggregateReportObjects.add(aggregateReportObjectsArray.getJSONObject(i)); + if (!output.isNull(TestFormatJsonMapping.AGGREGATE_REPORT_OBJECTS_KEY)) { + JSONArray aggregateReportObjectsArray = + output.getJSONArray(TestFormatJsonMapping.AGGREGATE_REPORT_OBJECTS_KEY); + for (int i = 0; i < aggregateReportObjectsArray.length(); i++) { + aggregateReportObjects.add(aggregateReportObjectsArray.getJSONObject(i)); + } } return new ReportObjects(eventReportObjects, aggregateReportObjects); } - private static void seedEnrollment() { - sEnrollmentDao.insert(AD_TECH_1); - sEnrollmentDao.insert(AD_TECH_2); - sEnrollmentDao.insert(AD_TECH_3); - } - - private static void unseedEnrollment() { - sEnrollmentDao.delete(AD_TECH_1.getEnrollmentId()); - sEnrollmentDao.delete(AD_TECH_2.getEnrollmentId()); - sEnrollmentDao.delete(AD_TECH_3.getEnrollmentId()); - } - /** * Empties measurement database tables, used for test cleanup. */ @@ -745,72 +912,27 @@ public abstract class E2ETest { db.delete("msmt_event_report", null, null); db.delete("msmt_attribution", null, null); db.delete("msmt_aggregate_report", null, null); + db.delete("enrollment_data", null, null); + db.delete("msmt_async_registration_contract", null, null); } - void processAction(RegisterSource sourceRegistration) throws IOException { - prepareRegistrationServer(sourceRegistration); - Assert.assertEquals( - "MeasurementImpl.register source failed", - RESULT_OK, - mMeasurementImpl.register( - sourceRegistration.mRegistrationRequest, sourceRegistration.mTimestamp)); - } - - void processAction(RegisterWebSource sourceRegistration) throws IOException { - prepareRegistrationServer(sourceRegistration); - Assert.assertEquals( - "MeasurementImpl.registerWebSource failed", - RESULT_OK, - mMeasurementImpl.registerWebSource( - sourceRegistration.mRegistrationRequest, sourceRegistration.mTimestamp)); - } - - void processAction(RegisterWebTrigger triggerRegistration) throws IOException { - prepareRegistrationServer(triggerRegistration); - Assert.assertEquals( - "MeasurementImpl.registerWebTrigger failed", - RESULT_OK, - mMeasurementImpl.registerWebTrigger( - triggerRegistration.mRegistrationRequest, triggerRegistration.mTimestamp)); - Assert.assertTrue( - "AttributionJobHandler.performPendingAttributions returned false", - mAttributionHelper.performPendingAttributions()); - } - - void processAction(RegisterTrigger triggerRegistration) throws IOException { - prepareRegistrationServer(triggerRegistration); - Assert.assertEquals( - "MeasurementImpl.register trigger failed", - RESULT_OK, - mMeasurementImpl.register( - triggerRegistration.mRegistrationRequest, triggerRegistration.mTimestamp)); - Assert.assertTrue("AttributionJobHandler.performPendingAttributions returned false", - mAttributionHelper.performPendingAttributions()); - } - - void processAction(InstallApp installApp) { - Assert.assertTrue( - "measurementDao.doInstallAttribution failed", - sDatastoreManager.runInTransaction( - measurementDao -> - measurementDao.doInstallAttribution( - installApp.mUri, installApp.mTimestamp))); - } - - void processAction(UninstallApp uninstallApp) { - Assert.assertTrue("measurementDao.undoInstallAttribution failed", - sDatastoreManager.runInTransaction( - measurementDao -> { - measurementDao.deleteAppRecords(uninstallApp.mUri); - measurementDao.undoInstallAttribution(uninstallApp.mUri); - })); - } + abstract void processAction(RegisterSource sourceRegistration) throws IOException; + + abstract void processAction(RegisterWebSource sourceRegistration) throws IOException; + + abstract void processAction(RegisterTrigger triggerRegistration) throws IOException; + + abstract void processAction(RegisterWebTrigger triggerRegistration) throws IOException; + + abstract void processAction(InstallApp installApp); + + abstract void processAction(UninstallApp uninstallApp); void evaluateResults() throws JSONException { - sortEventReportObjects(mExpectedOutput.mEventReportObjects); - sortEventReportObjects(mActualOutput.mEventReportObjects); - sortAggregateReportObjects(mExpectedOutput.mAggregateReportObjects); - sortAggregateReportObjects(mActualOutput.mAggregateReportObjects); + sortEventReportObjects(OutputType.EXPECTED, mExpectedOutput.mEventReportObjects); + sortEventReportObjects(OutputType.ACTUAL, mActualOutput.mEventReportObjects); + sortAggregateReportObjects(OutputType.EXPECTED, mExpectedOutput.mAggregateReportObjects); + sortAggregateReportObjects(OutputType.ACTUAL, mActualOutput.mAggregateReportObjects); Assert.assertTrue(getTestFailureMessage(mExpectedOutput, mActualOutput), areEqual(mExpectedOutput, mActualOutput)); } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/EnqueueAsyncRegistrationTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/EnqueueAsyncRegistrationTest.java new file mode 100644 index 0000000000..45ccbbbaca --- /dev/null +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/EnqueueAsyncRegistrationTest.java @@ -0,0 +1,840 @@ +/* + * 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.measurement; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import android.adservices.measurement.RegistrationRequest; +import android.adservices.measurement.WebSourceParams; +import android.adservices.measurement.WebSourceRegistrationRequest; +import android.adservices.measurement.WebTriggerParams; +import android.adservices.measurement.WebTriggerRegistrationRequest; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.RemoteException; +import android.view.InputEvent; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.adservices.data.DbTestUtil; +import com.android.adservices.data.enrollment.EnrollmentDao; +import com.android.adservices.data.measurement.DatastoreManager; +import com.android.adservices.data.measurement.MeasurementTables; +import com.android.adservices.data.measurement.SQLDatastoreManager; +import com.android.adservices.data.measurement.SqliteObjectMapper; +import com.android.adservices.service.FlagsFactory; +import com.android.adservices.service.enrollment.EnrollmentData; +import com.android.adservices.service.measurement.registration.EnqueueAsyncRegistration; +import com.android.dx.mockito.inline.extended.ExtendedMockito; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +import java.util.ArrayList; +import java.util.List; + +public class EnqueueAsyncRegistrationTest { + + private static Context sDefaultContext = ApplicationProvider.getApplicationContext(); + private static final Uri REGISTRATION_URI_1 = Uri.parse("https://foo.com/bar?ad=134"); + private static final Uri REGISTRATION_URI_2 = Uri.parse("https://foo.com/bar?ad=256"); + private static final String DEFAULT_ENROLLMENT = "enrollment-id"; + private static final WebSourceParams INPUT_SOURCE_REGISTRATION_1 = + new WebSourceParams.Builder(REGISTRATION_URI_1).setDebugKeyAllowed(true).build(); + + private static final WebSourceParams INPUT_SOURCE_REGISTRATION_2 = + new WebSourceParams.Builder(REGISTRATION_URI_2).setDebugKeyAllowed(false).build(); + + private static final WebTriggerParams INPUT_TRIGGER_REGISTRATION_1 = + new WebTriggerParams.Builder(REGISTRATION_URI_1).setDebugKeyAllowed(true).build(); + + private static final WebTriggerParams INPUT_TRIGGER_REGISTRATION_2 = + new WebTriggerParams.Builder(REGISTRATION_URI_2).setDebugKeyAllowed(false).build(); + + private static List<WebSourceParams> sSourceParamsList = new ArrayList<>(); + + private static List<WebTriggerParams> sTriggerParamsList = new ArrayList<>(); + + static { + sSourceParamsList.add(INPUT_SOURCE_REGISTRATION_1); + sSourceParamsList.add(INPUT_SOURCE_REGISTRATION_2); + sTriggerParamsList.add(INPUT_TRIGGER_REGISTRATION_1); + sTriggerParamsList.add(INPUT_TRIGGER_REGISTRATION_2); + } + + @Mock private DatastoreManager mDatastoreManagerMock; + @Mock private EnrollmentDao mEnrollmentDao; + @Mock private InputEvent mInputEvent; + + private MockitoSession mStaticMockSession; + + private static final WebSourceRegistrationRequest + VALID_WEB_SOURCE_REGISTRATION_NULL_INPUT_EVENT = + new WebSourceRegistrationRequest.Builder( + sSourceParamsList, Uri.parse("android-app://example.com/aD1")) + .setWebDestination(Uri.parse("android-app://example.com/aD1")) + .setAppDestination(Uri.parse("android-app://example.com/aD1")) + .setVerifiedDestination(Uri.parse("android-app://example.com/aD1")) + .build(); + + private static final WebTriggerRegistrationRequest VALID_WEB_TRIGGER_REGISTRATION = + new WebTriggerRegistrationRequest.Builder( + sTriggerParamsList, Uri.parse("android-app://com.e.abc")) + .build(); + + private static EnrollmentData getEnrollment(String enrollmentId) { + return new EnrollmentData.Builder().setEnrollmentId(enrollmentId).build(); + } + + @After + public void cleanup() { + SQLiteDatabase db = DbTestUtil.getDbHelperForTest().safeGetWritableDatabase(); + for (String table : MeasurementTables.ALL_MSMT_TABLES) { + db.delete(table, null, null); + } + mStaticMockSession.finishMocking(); + } + + @Before + public void before() throws RemoteException { + mStaticMockSession = + ExtendedMockito.mockitoSession() + .spyStatic(FlagsFactory.class) + .strictness(Strictness.WARN) + .startMocking(); + ExtendedMockito.doReturn(FlagsFactory.getFlagsForTest()).when(FlagsFactory::getFlags); + MockitoAnnotations.initMocks(this); + when(mEnrollmentDao.getEnrollmentDataFromMeasurementUrl(any())) + .thenReturn(getEnrollment(DEFAULT_ENROLLMENT)); + } + + @Test + public void test_appSourceRegistrationRequest_event_isValid() { + DatastoreManager datastoreManager = + new SQLDatastoreManager(DbTestUtil.getDbHelperForTest()); + RegistrationRequest registrationRequest = + new RegistrationRequest.Builder() + .setRegistrationType(RegistrationRequest.REGISTER_SOURCE) + .setRegistrationUri(Uri.parse("http://baz.com")) + .setPackageName(sDefaultContext.getAttributionSource().getPackageName()) + .build(); + + Assert.assertTrue( + EnqueueAsyncRegistration.appSourceOrTriggerRegistrationRequest( + registrationRequest, + Uri.parse("android-app://com.destination"), + System.currentTimeMillis(), + mEnrollmentDao, + datastoreManager)); + + try (Cursor cursor = + DbTestUtil.getDbHelperForTest() + .getReadableDatabase() + .query( + MeasurementTables.AsyncRegistrationContract.TABLE, + null, + null, + null, + null, + null, + null)) { + Assert.assertTrue(cursor.moveToNext()); + AsyncRegistration asyncRegistration = + SqliteObjectMapper.constructAsyncRegistration(cursor); + Assert.assertNotNull(asyncRegistration); + Assert.assertNotNull(asyncRegistration.getRegistrationUri()); + Assert.assertEquals( + registrationRequest.getRegistrationUri(), + asyncRegistration.getRegistrationUri()); + Assert.assertEquals( + Uri.parse("android-app://com.destination"), asyncRegistration.getTopOrigin()); + Assert.assertNotNull(asyncRegistration.getRegistrationUri()); + Assert.assertNotNull(asyncRegistration.getRegistrant()); + Assert.assertEquals( + Uri.parse("android-app://com.destination"), asyncRegistration.getRegistrant()); + Assert.assertNotNull(asyncRegistration.getSourceType()); + Assert.assertEquals(Source.SourceType.EVENT, asyncRegistration.getSourceType()); + Assert.assertNotNull(asyncRegistration.getType()); + Assert.assertEquals( + AsyncRegistration.RegistrationType.APP_SOURCE, asyncRegistration.getType()); + } + } + + @Test + public void test_appSourceRegistrationRequest_navigation_isValid() { + DatastoreManager datastoreManager = + new SQLDatastoreManager(DbTestUtil.getDbHelperForTest()); + RegistrationRequest registrationRequest = + new RegistrationRequest.Builder() + .setRegistrationType(RegistrationRequest.REGISTER_SOURCE) + .setRegistrationUri(Uri.parse("http://baz.com")) + .setPackageName(sDefaultContext.getAttributionSource().getPackageName()) + .setInputEvent(mInputEvent) + .build(); + + Assert.assertTrue( + EnqueueAsyncRegistration.appSourceOrTriggerRegistrationRequest( + registrationRequest, + Uri.parse("android-app://com.destination"), + System.currentTimeMillis(), + mEnrollmentDao, + datastoreManager)); + + try (Cursor cursor = + DbTestUtil.getDbHelperForTest() + .getReadableDatabase() + .query( + MeasurementTables.AsyncRegistrationContract.TABLE, + null, + null, + null, + null, + null, + null)) { + Assert.assertTrue(cursor.moveToNext()); + AsyncRegistration asyncRegistration = + SqliteObjectMapper.constructAsyncRegistration(cursor); + Assert.assertNotNull(asyncRegistration); + Assert.assertNotNull(asyncRegistration.getRegistrationUri()); + Assert.assertEquals( + registrationRequest.getRegistrationUri(), + asyncRegistration.getRegistrationUri()); + Assert.assertEquals( + Uri.parse("android-app://com.destination"), asyncRegistration.getTopOrigin()); + Assert.assertNotNull(asyncRegistration.getRegistrationUri()); + Assert.assertNotNull(asyncRegistration.getRegistrant()); + Assert.assertEquals( + Uri.parse("android-app://com.destination"), asyncRegistration.getRegistrant()); + Assert.assertNotNull(asyncRegistration.getSourceType()); + Assert.assertEquals(Source.SourceType.NAVIGATION, asyncRegistration.getSourceType()); + Assert.assertNotNull(asyncRegistration.getType()); + Assert.assertEquals( + AsyncRegistration.RegistrationType.APP_SOURCE, asyncRegistration.getType()); + } + } + + @Test + public void test_appTriggerRegistrationRequest_isValid() { + DatastoreManager datastoreManager = + new SQLDatastoreManager(DbTestUtil.getDbHelperForTest()); + RegistrationRequest registrationRequest = + new RegistrationRequest.Builder() + .setRegistrationType(RegistrationRequest.REGISTER_TRIGGER) + .setRegistrationUri(Uri.parse("http://baz.com")) + .setPackageName(sDefaultContext.getAttributionSource().getPackageName()) + .build(); + + Assert.assertTrue( + EnqueueAsyncRegistration.appSourceOrTriggerRegistrationRequest( + registrationRequest, + Uri.parse("android-app://com.destination"), + System.currentTimeMillis(), + mEnrollmentDao, + datastoreManager)); + + try (Cursor cursor = + DbTestUtil.getDbHelperForTest() + .getReadableDatabase() + .query( + MeasurementTables.AsyncRegistrationContract.TABLE, + null, + null, + null, + null, + null, + null)) { + Assert.assertTrue(cursor.moveToNext()); + AsyncRegistration asyncRegistration = + SqliteObjectMapper.constructAsyncRegistration(cursor); + Assert.assertNotNull(asyncRegistration); + Assert.assertNotNull(asyncRegistration.getRegistrationUri()); + Assert.assertEquals( + registrationRequest.getRegistrationUri(), + asyncRegistration.getRegistrationUri()); + Assert.assertEquals( + Uri.parse("android-app://com.destination"), asyncRegistration.getTopOrigin()); + Assert.assertNotNull(asyncRegistration.getRegistrationUri()); + Assert.assertNotNull(asyncRegistration.getRegistrant()); + Assert.assertEquals( + Uri.parse("android-app://com.destination"), asyncRegistration.getRegistrant()); + Assert.assertNull(asyncRegistration.getSourceType()); + } + } + + @Test + public void test_webSourceRegistrationRequest_event_isValid() { + DatastoreManager datastoreManager = + new SQLDatastoreManager(DbTestUtil.getDbHelperForTest()); + Assert.assertTrue( + EnqueueAsyncRegistration.webSourceRegistrationRequest( + VALID_WEB_SOURCE_REGISTRATION_NULL_INPUT_EVENT, + Uri.parse("android-app://com.destination"), + System.currentTimeMillis(), + mEnrollmentDao, + datastoreManager)); + + try (Cursor cursor = + DbTestUtil.getDbHelperForTest() + .getReadableDatabase() + .query( + MeasurementTables.AsyncRegistrationContract.TABLE, + null, + null, + null, + null, + null, + null)) { + + Assert.assertTrue(cursor.moveToNext()); + AsyncRegistration asyncRegistration = + SqliteObjectMapper.constructAsyncRegistration(cursor); + if (asyncRegistration + .getRegistrationUri() + .equals( + VALID_WEB_SOURCE_REGISTRATION_NULL_INPUT_EVENT + .getSourceParams() + .get(0) + .getRegistrationUri())) { + Assert.assertNotNull(asyncRegistration.getRegistrationUri()); + Assert.assertEquals(REGISTRATION_URI_1, asyncRegistration.getRegistrationUri()); + Assert.assertEquals( + AsyncRegistration.RedirectType.NONE, + asyncRegistration.getRedirectType()); + Assert.assertEquals(Source.SourceType.EVENT, asyncRegistration.getSourceType()); + Assert.assertEquals( + Uri.parse("android-app://com.destination"), + asyncRegistration.getRegistrant()); + Assert.assertNotNull(asyncRegistration.getWebDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistration.getWebDestination()); + Assert.assertNotNull(asyncRegistration.getOsDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistration.getOsDestination()); + Assert.assertNotNull(asyncRegistration.getVerifiedDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistration.getVerifiedDestination()); + Assert.assertNotNull(asyncRegistration.getType()); + Assert.assertEquals( + AsyncRegistration.RegistrationType.WEB_SOURCE, asyncRegistration.getType()); + + Assert.assertTrue(cursor.moveToNext()); + AsyncRegistration asyncRegistrationTwo = + SqliteObjectMapper.constructAsyncRegistration(cursor); + + Assert.assertNotNull(asyncRegistrationTwo.getRegistrationUri()); + Assert.assertEquals(REGISTRATION_URI_2, asyncRegistrationTwo.getRegistrationUri()); + Assert.assertEquals( + AsyncRegistration.RedirectType.NONE, + asyncRegistrationTwo.getRedirectType()); + Assert.assertEquals(Source.SourceType.EVENT, asyncRegistrationTwo.getSourceType()); + Assert.assertEquals( + Uri.parse("android-app://com.destination"), + asyncRegistrationTwo.getRegistrant()); + Assert.assertNotNull(asyncRegistrationTwo.getWebDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistrationTwo.getWebDestination()); + Assert.assertNotNull(asyncRegistrationTwo.getOsDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistrationTwo.getOsDestination()); + Assert.assertNotNull(asyncRegistrationTwo.getVerifiedDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistrationTwo.getVerifiedDestination()); + Assert.assertNotNull(asyncRegistrationTwo.getTopOrigin()); + Assert.assertEquals( + VALID_WEB_SOURCE_REGISTRATION_NULL_INPUT_EVENT.getTopOriginUri(), + asyncRegistrationTwo.getTopOrigin()); + Assert.assertNotNull(asyncRegistrationTwo.getType()); + Assert.assertEquals( + AsyncRegistration.RegistrationType.WEB_SOURCE, + asyncRegistrationTwo.getType()); + } else if (asyncRegistration + .getRegistrationUri() + .equals( + VALID_WEB_SOURCE_REGISTRATION_NULL_INPUT_EVENT + .getSourceParams() + .get(1) + .getRegistrationUri())) { + Assert.assertNotNull(asyncRegistration.getRegistrationUri()); + Assert.assertEquals(REGISTRATION_URI_2, asyncRegistration.getRegistrationUri()); + Assert.assertEquals( + AsyncRegistration.RedirectType.NONE, + asyncRegistration.getRedirectType()); + Assert.assertEquals(Source.SourceType.EVENT, asyncRegistration.getSourceType()); + Assert.assertEquals( + Uri.parse("android-app://com.destination"), + asyncRegistration.getRegistrant()); + Assert.assertNotNull(asyncRegistration.getWebDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistration.getWebDestination()); + Assert.assertNotNull(asyncRegistration.getOsDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistration.getOsDestination()); + Assert.assertNotNull(asyncRegistration.getVerifiedDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistration.getVerifiedDestination()); + Assert.assertNotNull(asyncRegistration.getTopOrigin()); + Assert.assertEquals( + VALID_WEB_SOURCE_REGISTRATION_NULL_INPUT_EVENT.getTopOriginUri(), + asyncRegistration.getTopOrigin()); + Assert.assertNotNull(asyncRegistration.getType()); + Assert.assertEquals( + AsyncRegistration.RegistrationType.WEB_SOURCE, asyncRegistration.getType()); + + Assert.assertTrue(cursor.moveToNext()); + AsyncRegistration asyncRegistrationTwo = + SqliteObjectMapper.constructAsyncRegistration(cursor); + Assert.assertNotNull(asyncRegistrationTwo.getRegistrationUri()); + Assert.assertEquals(REGISTRATION_URI_1, asyncRegistrationTwo.getRegistrationUri()); + Assert.assertEquals( + AsyncRegistration.RedirectType.NONE, + asyncRegistrationTwo.getRedirectType()); + Assert.assertEquals(Source.SourceType.EVENT, asyncRegistrationTwo.getSourceType()); + Assert.assertEquals( + Uri.parse("android-app://com.destination"), + asyncRegistrationTwo.getRegistrant()); + Assert.assertNotNull(asyncRegistrationTwo.getWebDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistrationTwo.getWebDestination()); + Assert.assertNotNull(asyncRegistrationTwo.getOsDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistrationTwo.getOsDestination()); + Assert.assertNotNull(asyncRegistrationTwo.getVerifiedDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistrationTwo.getVerifiedDestination()); + Assert.assertNotNull(asyncRegistrationTwo.getTopOrigin()); + Assert.assertEquals( + VALID_WEB_SOURCE_REGISTRATION_NULL_INPUT_EVENT.getTopOriginUri(), + asyncRegistrationTwo.getTopOrigin()); + Assert.assertNotNull(asyncRegistrationTwo.getType()); + Assert.assertEquals( + AsyncRegistration.RegistrationType.WEB_SOURCE, + asyncRegistrationTwo.getType()); + } else { + Assert.fail(); + } + } + } + + @Test + public void test_webSourceRegistrationRequest_navigation_isValid() { + DatastoreManager datastoreManager = + new SQLDatastoreManager(DbTestUtil.getDbHelperForTest()); + List<WebSourceParams> sourceParamsList = new ArrayList<>(); + sourceParamsList.add(INPUT_SOURCE_REGISTRATION_1); + sourceParamsList.add(INPUT_SOURCE_REGISTRATION_2); + WebSourceRegistrationRequest validWebSourceRegistration = + new WebSourceRegistrationRequest.Builder( + sourceParamsList, Uri.parse("android-app://example.com/aD1")) + .setWebDestination(Uri.parse("android-app://example.com/aD1")) + .setAppDestination(Uri.parse("android-app://example.com/aD1")) + .setVerifiedDestination(Uri.parse("android-app://example.com/aD1")) + .setInputEvent(mInputEvent) + .build(); + Assert.assertTrue( + EnqueueAsyncRegistration.webSourceRegistrationRequest( + validWebSourceRegistration, + Uri.parse("android-app://com.destination"), + System.currentTimeMillis(), + mEnrollmentDao, + datastoreManager)); + + try (Cursor cursor = + DbTestUtil.getDbHelperForTest() + .getReadableDatabase() + .query( + MeasurementTables.AsyncRegistrationContract.TABLE, + null, + null, + null, + null, + null, + null)) { + + Assert.assertTrue(cursor.moveToNext()); + AsyncRegistration asyncRegistration = + SqliteObjectMapper.constructAsyncRegistration(cursor); + if (asyncRegistration + .getRegistrationUri() + .equals( + validWebSourceRegistration + .getSourceParams() + .get(0) + .getRegistrationUri())) { + Assert.assertNotNull(asyncRegistration.getRegistrationUri()); + Assert.assertEquals(REGISTRATION_URI_1, asyncRegistration.getRegistrationUri()); + Assert.assertEquals( + AsyncRegistration.RedirectType.NONE, + asyncRegistration.getRedirectType()); + Assert.assertEquals( + Source.SourceType.NAVIGATION, asyncRegistration.getSourceType()); + Assert.assertEquals( + Uri.parse("android-app://com.destination"), + asyncRegistration.getRegistrant()); + Assert.assertNotNull(asyncRegistration.getWebDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistration.getWebDestination()); + Assert.assertNotNull(asyncRegistration.getOsDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistration.getOsDestination()); + Assert.assertNotNull(asyncRegistration.getVerifiedDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistration.getVerifiedDestination()); + Assert.assertNotNull(asyncRegistration.getType()); + Assert.assertEquals( + AsyncRegistration.RegistrationType.WEB_SOURCE, asyncRegistration.getType()); + + Assert.assertTrue(cursor.moveToNext()); + AsyncRegistration asyncRegistrationTwo = + SqliteObjectMapper.constructAsyncRegistration(cursor); + + Assert.assertNotNull(asyncRegistrationTwo.getRegistrationUri()); + Assert.assertEquals(REGISTRATION_URI_2, asyncRegistrationTwo.getRegistrationUri()); + Assert.assertEquals( + AsyncRegistration.RedirectType.NONE, + asyncRegistrationTwo.getRedirectType()); + Assert.assertEquals( + Source.SourceType.NAVIGATION, asyncRegistrationTwo.getSourceType()); + Assert.assertEquals( + Uri.parse("android-app://com.destination"), + asyncRegistrationTwo.getRegistrant()); + Assert.assertNotNull(asyncRegistrationTwo.getWebDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistrationTwo.getWebDestination()); + Assert.assertNotNull(asyncRegistrationTwo.getOsDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistrationTwo.getOsDestination()); + Assert.assertNotNull(asyncRegistrationTwo.getVerifiedDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistrationTwo.getVerifiedDestination()); + Assert.assertNotNull(asyncRegistrationTwo.getTopOrigin()); + Assert.assertEquals( + validWebSourceRegistration.getTopOriginUri(), + asyncRegistrationTwo.getTopOrigin()); + Assert.assertNotNull(asyncRegistrationTwo.getType()); + Assert.assertEquals( + AsyncRegistration.RegistrationType.WEB_SOURCE, + asyncRegistrationTwo.getType()); + } else if (asyncRegistration + .getRegistrationUri() + .equals( + validWebSourceRegistration + .getSourceParams() + .get(1) + .getRegistrationUri())) { + Assert.assertNotNull(asyncRegistration.getRegistrationUri()); + Assert.assertEquals(REGISTRATION_URI_2, asyncRegistration.getRegistrationUri()); + Assert.assertEquals( + AsyncRegistration.RedirectType.NONE, + asyncRegistration.getRedirectType()); + Assert.assertEquals( + Source.SourceType.NAVIGATION, asyncRegistration.getSourceType()); + Assert.assertEquals( + Uri.parse("android-app://com.destination"), + asyncRegistration.getRegistrant()); + Assert.assertNotNull(asyncRegistration.getWebDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistration.getWebDestination()); + Assert.assertNotNull(asyncRegistration.getOsDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistration.getOsDestination()); + Assert.assertNotNull(asyncRegistration.getVerifiedDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistration.getVerifiedDestination()); + Assert.assertNotNull(asyncRegistration.getTopOrigin()); + Assert.assertEquals( + validWebSourceRegistration.getTopOriginUri(), + asyncRegistration.getTopOrigin()); + Assert.assertNotNull(asyncRegistration.getType()); + Assert.assertEquals( + AsyncRegistration.RegistrationType.WEB_SOURCE, asyncRegistration.getType()); + + Assert.assertTrue(cursor.moveToNext()); + AsyncRegistration asyncRegistrationTwo = + SqliteObjectMapper.constructAsyncRegistration(cursor); + Assert.assertNotNull(asyncRegistrationTwo.getRegistrationUri()); + Assert.assertEquals(REGISTRATION_URI_1, asyncRegistrationTwo.getRegistrationUri()); + Assert.assertEquals( + AsyncRegistration.RedirectType.NONE, + asyncRegistrationTwo.getRedirectType()); + Assert.assertEquals( + Source.SourceType.NAVIGATION, asyncRegistrationTwo.getSourceType()); + Assert.assertEquals( + Uri.parse("android-app://com.destination"), + asyncRegistrationTwo.getRegistrant()); + Assert.assertNotNull(asyncRegistrationTwo.getWebDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistrationTwo.getWebDestination()); + Assert.assertNotNull(asyncRegistrationTwo.getOsDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistrationTwo.getOsDestination()); + Assert.assertNotNull(asyncRegistrationTwo.getVerifiedDestination()); + Assert.assertEquals( + Uri.parse("android-app://example.com/aD1"), + asyncRegistrationTwo.getVerifiedDestination()); + Assert.assertNotNull(asyncRegistrationTwo.getTopOrigin()); + Assert.assertEquals( + validWebSourceRegistration.getTopOriginUri(), + asyncRegistrationTwo.getTopOrigin()); + Assert.assertNotNull(asyncRegistrationTwo.getType()); + Assert.assertEquals( + AsyncRegistration.RegistrationType.WEB_SOURCE, + asyncRegistrationTwo.getType()); + } else { + Assert.fail(); + } + } + } + + @Test + public void test_webTriggerRegistrationRequest_isValid() { + DatastoreManager datastoreManager = + new SQLDatastoreManager(DbTestUtil.getDbHelperForTest()); + Assert.assertTrue( + EnqueueAsyncRegistration.webTriggerRegistrationRequest( + VALID_WEB_TRIGGER_REGISTRATION, + Uri.parse("android-app://com.destination"), + System.currentTimeMillis(), + mEnrollmentDao, + datastoreManager)); + + try (Cursor cursor = + DbTestUtil.getDbHelperForTest() + .getReadableDatabase() + .query( + MeasurementTables.AsyncRegistrationContract.TABLE, + null, + null, + null, + null, + null, + null)) { + Assert.assertTrue(cursor.moveToNext()); + AsyncRegistration asyncRegistration = + SqliteObjectMapper.constructAsyncRegistration(cursor); + if (asyncRegistration + .getRegistrationUri() + .equals( + VALID_WEB_TRIGGER_REGISTRATION + .getTriggerParams() + .get(0) + .getRegistrationUri())) { + Assert.assertNotNull(asyncRegistration.getRegistrationUri()); + Assert.assertEquals(REGISTRATION_URI_1, asyncRegistration.getRegistrationUri()); + Assert.assertEquals( + AsyncRegistration.RedirectType.NONE, + asyncRegistration.getRedirectType()); + Assert.assertEquals(null, asyncRegistration.getSourceType()); + Assert.assertNotNull(asyncRegistration.getRegistrant()); + Assert.assertEquals( + Uri.parse("android-app://com.destination"), + asyncRegistration.getRegistrant()); + Assert.assertNotNull(asyncRegistration.getTopOrigin()); + Assert.assertEquals( + VALID_WEB_TRIGGER_REGISTRATION.getDestination(), + asyncRegistration.getTopOrigin()); + Assert.assertNotNull(asyncRegistration.getType()); + Assert.assertEquals( + AsyncRegistration.RegistrationType.WEB_TRIGGER, + asyncRegistration.getType()); + + Assert.assertTrue(cursor.moveToNext()); + AsyncRegistration asyncRegistrationTwo = + SqliteObjectMapper.constructAsyncRegistration(cursor); + + Assert.assertNotNull(asyncRegistrationTwo.getRegistrationUri()); + Assert.assertEquals(REGISTRATION_URI_2, asyncRegistrationTwo.getRegistrationUri()); + Assert.assertEquals( + AsyncRegistration.RedirectType.NONE, + asyncRegistrationTwo.getRedirectType()); + Assert.assertEquals(null, asyncRegistrationTwo.getSourceType()); + Assert.assertNotNull(asyncRegistration.getRegistrant()); + Assert.assertEquals( + Uri.parse("android-app://com.destination"), + asyncRegistrationTwo.getRegistrant()); + Assert.assertNotNull(asyncRegistrationTwo.getTopOrigin()); + Assert.assertEquals( + VALID_WEB_TRIGGER_REGISTRATION.getDestination(), + asyncRegistrationTwo.getTopOrigin()); + Assert.assertNotNull(asyncRegistrationTwo.getType()); + Assert.assertEquals( + AsyncRegistration.RegistrationType.WEB_TRIGGER, + asyncRegistrationTwo.getType()); + } else if (asyncRegistration + .getRegistrationUri() + .equals( + VALID_WEB_TRIGGER_REGISTRATION + .getTriggerParams() + .get(1) + .getRegistrationUri())) { + Assert.assertNotNull(asyncRegistration.getRegistrationUri()); + Assert.assertEquals(REGISTRATION_URI_2, asyncRegistration.getRegistrationUri()); + Assert.assertEquals( + AsyncRegistration.RedirectType.NONE, + asyncRegistration.getRedirectType()); + Assert.assertEquals(null, asyncRegistration.getSourceType()); + Assert.assertNotNull(asyncRegistration.getRegistrant()); + Assert.assertEquals( + Uri.parse("android-app://com.destination"), + asyncRegistration.getRegistrant()); + Assert.assertNotNull(asyncRegistration.getTopOrigin()); + Assert.assertEquals( + VALID_WEB_TRIGGER_REGISTRATION.getDestination(), + asyncRegistration.getTopOrigin()); + Assert.assertNotNull(asyncRegistration.getType()); + Assert.assertEquals( + AsyncRegistration.RegistrationType.WEB_TRIGGER, + asyncRegistration.getType()); + + Assert.assertTrue(cursor.moveToNext()); + AsyncRegistration asyncRegistrationTwo = + SqliteObjectMapper.constructAsyncRegistration(cursor); + Assert.assertNotNull(asyncRegistrationTwo.getRegistrationUri()); + Assert.assertEquals(REGISTRATION_URI_1, asyncRegistrationTwo.getRegistrationUri()); + Assert.assertEquals( + AsyncRegistration.RedirectType.NONE, + asyncRegistrationTwo.getRedirectType()); + Assert.assertEquals(null, asyncRegistrationTwo.getSourceType()); + Assert.assertNotNull(asyncRegistrationTwo.getRegistrant()); + Assert.assertEquals( + Uri.parse("android-app://com.destination"), + asyncRegistrationTwo.getRegistrant()); + Assert.assertNotNull(asyncRegistrationTwo.getTopOrigin()); + Assert.assertEquals( + VALID_WEB_TRIGGER_REGISTRATION.getDestination(), + asyncRegistrationTwo.getTopOrigin()); + Assert.assertNotNull(asyncRegistrationTwo.getType()); + Assert.assertEquals( + AsyncRegistration.RegistrationType.WEB_TRIGGER, + asyncRegistrationTwo.getType()); + } else { + Assert.fail(); + } + } + } + + @Test + public void test_runInTransactionFail_inValid() { + when(mDatastoreManagerMock.runInTransaction(any())).thenReturn(false); + Assert.assertFalse( + EnqueueAsyncRegistration.webTriggerRegistrationRequest( + VALID_WEB_TRIGGER_REGISTRATION, + Uri.parse("android-app://com.destination"), + System.currentTimeMillis(), + mEnrollmentDao, + mDatastoreManagerMock)); + } + + @Test + public void test_MissingEnrollmentData_inValid() { + when(mEnrollmentDao.getEnrollmentDataFromMeasurementUrl(any())).thenReturn(null); + Assert.assertFalse( + EnqueueAsyncRegistration.webTriggerRegistrationRequest( + VALID_WEB_TRIGGER_REGISTRATION, + Uri.parse("android-app://com.destination"), + System.currentTimeMillis(), + mEnrollmentDao, + mDatastoreManagerMock)); + } + + /** Test that the AsyncRegistration is inserted correctly. */ + @Test + public void test_verifyAsyncRegistrationStoredCorrectly() { + RegistrationRequest registrationRequest = + new RegistrationRequest.Builder() + .setRegistrationType(RegistrationRequest.REGISTER_SOURCE) + .setRegistrationUri(Uri.parse("http://baz.com")) + .setPackageName(sDefaultContext.getAttributionSource().getPackageName()) + .build(); + + DatastoreManager datastoreManager = + new SQLDatastoreManager(DbTestUtil.getDbHelperForTest()); + EnqueueAsyncRegistration.appSourceOrTriggerRegistrationRequest( + registrationRequest, + Uri.parse("android-app://com.destination"), + System.currentTimeMillis(), + mEnrollmentDao, + datastoreManager); + + try (Cursor cursor = + DbTestUtil.getDbHelperForTest() + .getReadableDatabase() + .query( + MeasurementTables.AsyncRegistrationContract.TABLE, + null, + null, + null, + null, + null, + null)) { + + Assert.assertTrue(cursor.moveToNext()); + AsyncRegistration asyncRegistration = + SqliteObjectMapper.constructAsyncRegistration(cursor); + Assert.assertNotNull(asyncRegistration); + Assert.assertNotNull(asyncRegistration.getRegistrationUri()); + Assert.assertEquals( + registrationRequest.getRegistrationUri(), + asyncRegistration.getRegistrationUri()); + Assert.assertEquals( + Uri.parse("android-app://com.destination"), asyncRegistration.getTopOrigin()); + Assert.assertNotNull(asyncRegistration.getRegistrationUri()); + Assert.assertNotNull(asyncRegistration.getRegistrant()); + Assert.assertEquals( + Uri.parse("android-app://com.destination"), asyncRegistration.getRegistrant()); + Assert.assertNotNull(asyncRegistration.getSourceType()); + Assert.assertEquals(Source.SourceType.EVENT, asyncRegistration.getSourceType()); + Assert.assertNotNull(asyncRegistration.getType()); + Assert.assertEquals( + AsyncRegistration.RegistrationType.APP_SOURCE, asyncRegistration.getType()); + Assert.assertEquals( + AsyncRegistration.RedirectType.ANY, + asyncRegistration.getRedirectType()); + } + } +} diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/EventReportTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/EventReportTest.java index ffeed6a45c..4247a8b3b1 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/EventReportTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/EventReportTest.java @@ -25,16 +25,21 @@ import static com.android.adservices.service.measurement.PrivacyParams.NAVIGATIO import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; import android.net.Uri; import androidx.test.filters.SmallTest; +import com.android.adservices.service.measurement.util.UnsignedLong; + import org.json.JSONException; import org.junit.Test; import java.util.List; import java.util.Set; +import java.util.UUID; import java.util.concurrent.TimeUnit; /** Unit tests for {@link EventReport} */ @@ -44,10 +49,12 @@ public final class EventReportTest { private static final long ONE_HOUR_IN_MILLIS = TimeUnit.HOURS.toMillis(1); private static final double DOUBLE_MAX_DELTA = 0.0000001D; private static final long TRIGGER_PRIORITY = 345678L; - private static final Long TRIGGER_DEDUP_KEY = 2345678L; - private static final Long TRIGGER_DATA = 4L; - private static final Long SOURCE_DEBUG_KEY = 237865L; - private static final Long TRIGGER_DEBUG_KEY = 928762L; + private static final UnsignedLong TRIGGER_DEDUP_KEY = new UnsignedLong(2345678L); + private static final UnsignedLong TRIGGER_DATA = new UnsignedLong(4L); + private static final UnsignedLong SOURCE_DEBUG_KEY = new UnsignedLong(237865L); + private static final UnsignedLong TRIGGER_DEBUG_KEY = new UnsignedLong(928762L); + private static final String SOURCE_ID = UUID.randomUUID().toString(); + private static final String TRIGGER_ID = UUID.randomUUID().toString(); private static final Uri APP_DESTINATION = Uri.parse("android-app://example1.app"); private static final Uri WEB_DESTINATION = Uri.parse("https://example1.com"); private static final String EVENT_TRIGGERS = @@ -73,72 +80,84 @@ public final class EventReportTest { public void creation_success() { EventReport eventReport = createExample(); assertEquals("1", eventReport.getId()); - assertEquals(21, eventReport.getSourceId()); + assertEquals(new UnsignedLong(21L), eventReport.getSourceEventId()); assertEquals("enrollment-id", eventReport.getEnrollmentId()); assertEquals("https://bar.com", eventReport.getAttributionDestination().toString()); assertEquals(1000L, eventReport.getTriggerTime()); - assertEquals(8L, eventReport.getTriggerData()); + assertEquals(new UnsignedLong(8L), eventReport.getTriggerData()); assertEquals(2L, eventReport.getTriggerPriority()); - assertEquals(Long.valueOf(3), eventReport.getTriggerDedupKey()); + assertEquals(new UnsignedLong(3L), eventReport.getTriggerDedupKey()); assertEquals(2000L, eventReport.getReportTime()); assertEquals(EventReport.Status.PENDING, eventReport.getStatus()); + assertEquals(EventReport.DebugReportStatus.PENDING, eventReport.getDebugReportStatus()); assertEquals(Source.SourceType.NAVIGATION, eventReport.getSourceType()); assertEquals(SOURCE_DEBUG_KEY, eventReport.getSourceDebugKey()); assertEquals(TRIGGER_DEBUG_KEY, eventReport.getTriggerDebugKey()); + assertEquals(SOURCE_ID, eventReport.getSourceId()); + assertEquals(TRIGGER_ID, eventReport.getTriggerId()); } @Test public void creationSuccessSingleSourceDebugKey() { EventReport eventReport = createExampleSingleSourceDebugKey(); assertEquals("1", eventReport.getId()); - assertEquals(21, eventReport.getSourceId()); + assertEquals(new UnsignedLong(21L), eventReport.getSourceEventId()); assertEquals("enrollment-id", eventReport.getEnrollmentId()); assertEquals("https://bar.com", eventReport.getAttributionDestination().toString()); assertEquals(1000L, eventReport.getTriggerTime()); - assertEquals(8L, eventReport.getTriggerData()); + assertEquals(new UnsignedLong(8L), eventReport.getTriggerData()); assertEquals(2L, eventReport.getTriggerPriority()); - assertEquals(Long.valueOf(3), eventReport.getTriggerDedupKey()); + assertEquals(new UnsignedLong(3L), eventReport.getTriggerDedupKey()); assertEquals(2000L, eventReport.getReportTime()); assertEquals(EventReport.Status.PENDING, eventReport.getStatus()); + assertEquals(EventReport.DebugReportStatus.PENDING, eventReport.getDebugReportStatus()); assertEquals(Source.SourceType.NAVIGATION, eventReport.getSourceType()); assertEquals(SOURCE_DEBUG_KEY, eventReport.getSourceDebugKey()); assertNull(eventReport.getTriggerDebugKey()); + assertEquals(SOURCE_ID, eventReport.getSourceId()); + assertEquals(TRIGGER_ID, eventReport.getTriggerId()); } @Test public void creationSuccessSingleTriggerDebugKey() { EventReport eventReport = createExampleSingleTriggerDebugKey(); assertEquals("1", eventReport.getId()); - assertEquals(21, eventReport.getSourceId()); + assertEquals(new UnsignedLong(21L), eventReport.getSourceEventId()); assertEquals("enrollment-id", eventReport.getEnrollmentId()); assertEquals("https://bar.com", eventReport.getAttributionDestination().toString()); assertEquals(1000L, eventReport.getTriggerTime()); - assertEquals(8L, eventReport.getTriggerData()); + assertEquals(new UnsignedLong(8L), eventReport.getTriggerData()); assertEquals(2L, eventReport.getTriggerPriority()); - assertEquals(Long.valueOf(3), eventReport.getTriggerDedupKey()); + assertEquals(new UnsignedLong(3L), eventReport.getTriggerDedupKey()); assertEquals(2000L, eventReport.getReportTime()); assertEquals(EventReport.Status.PENDING, eventReport.getStatus()); + assertEquals(EventReport.DebugReportStatus.PENDING, eventReport.getDebugReportStatus()); assertEquals(Source.SourceType.NAVIGATION, eventReport.getSourceType()); assertNull(eventReport.getSourceDebugKey()); assertEquals(TRIGGER_DEBUG_KEY, eventReport.getTriggerDebugKey()); + assertEquals(SOURCE_ID, eventReport.getSourceId()); + assertEquals(TRIGGER_ID, eventReport.getTriggerId()); } @Test public void defaults_success() { EventReport eventReport = new EventReport.Builder().build(); assertNull(eventReport.getId()); - assertEquals(0L, eventReport.getSourceId()); + assertNull(eventReport.getSourceEventId()); assertNull(eventReport.getEnrollmentId()); assertNull(eventReport.getAttributionDestination()); assertEquals(0L, eventReport.getTriggerTime()); - assertEquals(0L, eventReport.getTriggerData()); + assertNull(eventReport.getTriggerData()); assertEquals(0L, eventReport.getTriggerPriority()); assertNull(eventReport.getTriggerDedupKey()); assertEquals(0L, eventReport.getReportTime()); assertEquals(EventReport.Status.PENDING, eventReport.getStatus()); + assertEquals(EventReport.DebugReportStatus.NONE, eventReport.getDebugReportStatus()); assertNull(eventReport.getSourceType()); assertNull(eventReport.getSourceDebugKey()); assertNull(eventReport.getTriggerDebugKey()); + assertNull(eventReport.getSourceId()); + assertNull(eventReport.getTriggerId()); } @Test @@ -160,14 +179,87 @@ public final class EventReportTest { assertEquals(TRIGGER_PRIORITY, report.getTriggerPriority()); assertEquals(TRIGGER_DEDUP_KEY, report.getTriggerDedupKey()); // Truncated data 4 % 2 = 0 - assertEquals(0, report.getTriggerData()); + assertEquals(new UnsignedLong(0L), report.getTriggerData()); assertEquals(trigger.getTriggerTime(), report.getTriggerTime()); - assertEquals(source.getEventId(), report.getSourceId()); + assertEquals(source.getEventId(), report.getSourceEventId()); assertEquals(source.getEnrollmentId(), report.getEnrollmentId()); assertEquals(trigger.getAttributionDestination(), report.getAttributionDestination()); assertEquals(source.getExpiryTime() + ONE_HOUR_IN_MILLIS, report.getReportTime()); assertEquals(source.getSourceType(), report.getSourceType()); assertEquals(EVENT_NOISE_PROBABILITY, report.getRandomizedTriggerRate(), DOUBLE_MAX_DELTA); + assertEquals(SOURCE_ID, report.getSourceId()); + assertEquals(TRIGGER_ID, report.getTriggerId()); + } + + @Test + public void testPopulateFromSourceAndTrigger_shouldSetTriggerData0WhenNull() + throws JSONException { + long baseTime = System.currentTimeMillis(); + Source source = + createSourceForTest( + baseTime, Source.SourceType.EVENT, false, APP_DESTINATION, null); + Trigger trigger = + createTriggerForTest(baseTime + TimeUnit.SECONDS.toMillis(10), APP_DESTINATION); + + List<EventTrigger> eventTriggers = trigger.parseEventTriggers(); + EventTrigger eventTrigger = spy(eventTriggers.get(0)); + when(eventTrigger.getTriggerData()).thenReturn(null); + EventReport report = + new EventReport.Builder() + .populateFromSourceAndTrigger(source, trigger, eventTrigger) + .build(); + + assertEquals(TRIGGER_PRIORITY, report.getTriggerPriority()); + assertEquals(TRIGGER_DEDUP_KEY, report.getTriggerDedupKey()); + assertEquals(new UnsignedLong(0L), report.getTriggerData()); + assertEquals(trigger.getTriggerTime(), report.getTriggerTime()); + assertEquals(source.getEventId(), report.getSourceEventId()); + assertEquals(source.getEnrollmentId(), report.getEnrollmentId()); + assertEquals(trigger.getAttributionDestination(), report.getAttributionDestination()); + assertEquals(source.getExpiryTime() + ONE_HOUR_IN_MILLIS, report.getReportTime()); + assertEquals(source.getSourceType(), report.getSourceType()); + assertEquals(EVENT_NOISE_PROBABILITY, report.getRandomizedTriggerRate(), DOUBLE_MAX_DELTA); + assertEquals(SOURCE_ID, report.getSourceId()); + assertEquals(TRIGGER_ID, report.getTriggerId()); + } + + @Test + public void testPopulateFromSourceAndTrigger_shouldTruncateTriggerDataWith64thBit() + throws JSONException { + long baseTime = System.currentTimeMillis(); + Source source = + createSourceForTest( + baseTime, Source.SourceType.NAVIGATION, false, APP_DESTINATION, null); + Trigger trigger = + createTriggerForTest(baseTime + TimeUnit.SECONDS.toMillis(10), APP_DESTINATION); + + List<EventTrigger> eventTriggers = trigger.parseEventTriggers(); + EventTrigger eventTrigger = spy(eventTriggers.get(0)); + when(eventTrigger.getTriggerData()).thenReturn(new UnsignedLong(-50003L)); + EventReport report = + new EventReport.Builder() + .populateFromSourceAndTrigger(source, trigger, eventTrigger) + .build(); + + assertEquals(TRIGGER_PRIORITY, report.getTriggerPriority()); + assertEquals(TRIGGER_DEDUP_KEY, report.getTriggerDedupKey()); + assertEquals(trigger.getTriggerTime(), report.getTriggerTime()); + // uint64 18446744073709501613 = long -50003 + // Truncated data 18446744073709501613 % 8 = 5 + assertEquals(new UnsignedLong(5L), report.getTriggerData()); + assertEquals(source.getEventId(), report.getSourceEventId()); + assertEquals(source.getEnrollmentId(), report.getEnrollmentId()); + assertEquals(APP_DESTINATION, report.getAttributionDestination()); + assertEquals( + source.getEventTime() + + NAVIGATION_EARLY_REPORTING_WINDOW_MILLISECONDS[0] + + ONE_HOUR_IN_MILLIS, + report.getReportTime()); + assertEquals(Source.SourceType.NAVIGATION, report.getSourceType()); + assertEquals( + NAVIGATION_NOISE_PROBABILITY, report.getRandomizedTriggerRate(), DOUBLE_MAX_DELTA); + assertEquals(SOURCE_ID, report.getSourceId()); + assertEquals(TRIGGER_ID, report.getTriggerId()); } @Test @@ -189,14 +281,16 @@ public final class EventReportTest { assertEquals(TRIGGER_PRIORITY, report.getTriggerPriority()); assertEquals(TRIGGER_DEDUP_KEY, report.getTriggerDedupKey()); // Truncated data 4 % 2 = 0 - assertEquals(0, report.getTriggerData()); + assertEquals(new UnsignedLong(0L), report.getTriggerData()); assertEquals(trigger.getTriggerTime(), report.getTriggerTime()); - assertEquals(source.getEventId(), report.getSourceId()); + assertEquals(source.getEventId(), report.getSourceEventId()); assertEquals(source.getEnrollmentId(), report.getEnrollmentId()); assertEquals(trigger.getAttributionDestination(), report.getAttributionDestination()); assertEquals(source.getExpiryTime() + ONE_HOUR_IN_MILLIS, report.getReportTime()); assertEquals(Source.SourceType.EVENT, report.getSourceType()); assertEquals(EVENT_NOISE_PROBABILITY, report.getRandomizedTriggerRate(), DOUBLE_MAX_DELTA); + assertEquals(SOURCE_ID, report.getSourceId()); + assertEquals(TRIGGER_ID, report.getTriggerId()); } @Test @@ -217,7 +311,7 @@ public final class EventReportTest { assertEquals(TRIGGER_PRIORITY, report.getTriggerPriority()); assertEquals(TRIGGER_DEDUP_KEY, report.getTriggerDedupKey()); assertEquals(trigger.getTriggerTime(), report.getTriggerTime()); - assertEquals(source.getEventId(), report.getSourceId()); + assertEquals(source.getEventId(), report.getSourceEventId()); assertEquals(source.getEnrollmentId(), report.getEnrollmentId()); assertEquals(trigger.getAttributionDestination(), report.getAttributionDestination()); assertEquals(source.getExpiryTime() + ONE_HOUR_IN_MILLIS, report.getReportTime()); @@ -226,6 +320,8 @@ public final class EventReportTest { INSTALL_ATTR_EVENT_NOISE_PROBABILITY, report.getRandomizedTriggerRate(), DOUBLE_MAX_DELTA); + assertEquals(SOURCE_ID, report.getSourceId()); + assertEquals(TRIGGER_ID, report.getTriggerId()); } @Test @@ -246,12 +342,14 @@ public final class EventReportTest { assertEquals(TRIGGER_PRIORITY, report.getTriggerPriority()); assertEquals(TRIGGER_DEDUP_KEY, report.getTriggerDedupKey()); assertEquals(trigger.getTriggerTime(), report.getTriggerTime()); - assertEquals(source.getEventId(), report.getSourceId()); + assertEquals(source.getEventId(), report.getSourceEventId()); assertEquals(source.getEnrollmentId(), report.getEnrollmentId()); assertEquals(trigger.getAttributionDestination(), report.getAttributionDestination()); assertEquals(source.getExpiryTime() + ONE_HOUR_IN_MILLIS, report.getReportTime()); assertEquals(Source.SourceType.EVENT, report.getSourceType()); assertEquals(EVENT_NOISE_PROBABILITY, report.getRandomizedTriggerRate(), DOUBLE_MAX_DELTA); + assertEquals(SOURCE_ID, report.getSourceId()); + assertEquals(TRIGGER_ID, report.getTriggerId()); } @Test @@ -273,7 +371,7 @@ public final class EventReportTest { assertEquals(TRIGGER_PRIORITY, report.getTriggerPriority()); assertEquals(TRIGGER_DEDUP_KEY, report.getTriggerDedupKey()); assertEquals(trigger.getTriggerTime(), report.getTriggerTime()); - assertEquals(source.getEventId(), report.getSourceId()); + assertEquals(source.getEventId(), report.getSourceEventId()); assertEquals(source.getEnrollmentId(), report.getEnrollmentId()); assertEquals(APP_DESTINATION, report.getAttributionDestination()); assertEquals( @@ -284,6 +382,8 @@ public final class EventReportTest { assertEquals(Source.SourceType.NAVIGATION, report.getSourceType()); assertEquals( NAVIGATION_NOISE_PROBABILITY, report.getRandomizedTriggerRate(), DOUBLE_MAX_DELTA); + assertEquals(SOURCE_ID, report.getSourceId()); + assertEquals(TRIGGER_ID, report.getTriggerId()); } @Test @@ -305,7 +405,7 @@ public final class EventReportTest { assertEquals(TRIGGER_PRIORITY, report.getTriggerPriority()); assertEquals(TRIGGER_DEDUP_KEY, report.getTriggerDedupKey()); assertEquals(trigger.getTriggerTime(), report.getTriggerTime()); - assertEquals(source.getEventId(), report.getSourceId()); + assertEquals(source.getEventId(), report.getSourceEventId()); assertEquals(source.getEnrollmentId(), report.getEnrollmentId()); assertEquals(trigger.getAttributionDestination(), report.getAttributionDestination()); assertEquals( @@ -314,6 +414,8 @@ public final class EventReportTest { assertEquals(source.getSourceType(), report.getSourceType()); assertEquals( NAVIGATION_NOISE_PROBABILITY, report.getRandomizedTriggerRate(), DOUBLE_MAX_DELTA); + assertEquals(SOURCE_ID, report.getSourceId()); + assertEquals(TRIGGER_ID, report.getTriggerId()); } @Test @@ -334,9 +436,9 @@ public final class EventReportTest { assertEquals(TRIGGER_PRIORITY, report.getTriggerPriority()); assertEquals(TRIGGER_DEDUP_KEY, report.getTriggerDedupKey()); - assertEquals(4, report.getTriggerData()); + assertEquals(new UnsignedLong(4L), report.getTriggerData()); assertEquals(trigger.getTriggerTime(), report.getTriggerTime()); - assertEquals(source.getEventId(), report.getSourceId()); + assertEquals(source.getEventId(), report.getSourceEventId()); assertEquals(source.getEnrollmentId(), report.getEnrollmentId()); assertEquals(trigger.getAttributionDestination(), report.getAttributionDestination()); // One hour after install attributed navigation type window @@ -350,6 +452,8 @@ public final class EventReportTest { INSTALL_ATTR_NAVIGATION_NOISE_PROBABILITY, report.getRandomizedTriggerRate(), DOUBLE_MAX_DELTA); + assertEquals(SOURCE_ID, report.getSourceId()); + assertEquals(TRIGGER_ID, report.getTriggerId()); } @Test @@ -370,9 +474,9 @@ public final class EventReportTest { assertEquals(TRIGGER_PRIORITY, report.getTriggerPriority()); assertEquals(TRIGGER_DEDUP_KEY, report.getTriggerDedupKey()); - assertEquals(4, report.getTriggerData()); + assertEquals(new UnsignedLong(4L), report.getTriggerData()); assertEquals(trigger.getTriggerTime(), report.getTriggerTime()); - assertEquals(source.getEventId(), report.getSourceId()); + assertEquals(source.getEventId(), report.getSourceEventId()); assertEquals(source.getEnrollmentId(), report.getEnrollmentId()); assertEquals(trigger.getAttributionDestination(), report.getAttributionDestination()); // One hour after regular navigation type window (without install attribution consideration) @@ -384,6 +488,8 @@ public final class EventReportTest { assertEquals(source.getSourceType(), report.getSourceType()); assertEquals( NAVIGATION_NOISE_PROBABILITY, report.getRandomizedTriggerRate(), DOUBLE_MAX_DELTA); + assertEquals(SOURCE_ID, report.getSourceId()); + assertEquals(TRIGGER_ID, report.getTriggerId()); } @Test @@ -403,15 +509,16 @@ public final class EventReportTest { final EventReport eventReport2 = new EventReport.Builder() .setId("1") - .setSourceId(22) + .setSourceEventId(new UnsignedLong(22L)) .setEnrollmentId("another-enrollment-id") .setAttributionDestination(Uri.parse("https://bar.com")) .setTriggerTime(1000L) - .setTriggerData(8L) + .setTriggerData(new UnsignedLong(8L)) .setTriggerPriority(2L) - .setTriggerDedupKey(3L) + .setTriggerDedupKey(new UnsignedLong(3L)) .setReportTime(2000L) .setStatus(EventReport.Status.PENDING) + .setStatus(EventReport.DebugReportStatus.PENDING) .setSourceType(Source.SourceType.NAVIGATION) .build(); final Set<EventReport> eventReportSet1 = Set.of(eventReport1); @@ -428,7 +535,8 @@ public final class EventReportTest { Uri appDestination, Uri webDestination) { return SourceFixture.getValidSourceBuilder() - .setEventId(10) + .setId(SOURCE_ID) + .setEventId(new UnsignedLong(10L)) .setSourceType(sourceType) .setInstallCooldownWindow(isInstallAttributable ? 100 : 0) .setEventTime(eventTime) @@ -441,6 +549,7 @@ public final class EventReportTest { private Trigger createTriggerForTest(long eventTime, Uri destination) { return TriggerFixture.getValidTriggerBuilder() + .setId(TRIGGER_ID) .setTriggerTime(eventTime) .setEventTriggers(EVENT_TRIGGERS) .setEnrollmentId("enrollment-id") @@ -451,52 +560,61 @@ public final class EventReportTest { private EventReport createExample() { return new EventReport.Builder() .setId("1") - .setSourceId(21) + .setSourceEventId(new UnsignedLong(21L)) .setEnrollmentId("enrollment-id") .setAttributionDestination(Uri.parse("https://bar.com")) .setTriggerTime(1000L) - .setTriggerData(8L) + .setTriggerData(new UnsignedLong(8L)) .setTriggerPriority(2L) - .setTriggerDedupKey(3L) + .setTriggerDedupKey(new UnsignedLong(3L)) .setReportTime(2000L) .setStatus(EventReport.Status.PENDING) + .setDebugReportStatus(EventReport.DebugReportStatus.PENDING) .setSourceType(Source.SourceType.NAVIGATION) .setSourceDebugKey(SOURCE_DEBUG_KEY) .setTriggerDebugKey(TRIGGER_DEBUG_KEY) + .setSourceId(SOURCE_ID) + .setTriggerId(TRIGGER_ID) .build(); } private EventReport createExampleSingleTriggerDebugKey() { return new EventReport.Builder() .setId("1") - .setSourceId(21) + .setSourceEventId(new UnsignedLong(21L)) .setEnrollmentId("enrollment-id") .setAttributionDestination(Uri.parse("https://bar.com")) .setTriggerTime(1000L) - .setTriggerData(8L) + .setTriggerData(new UnsignedLong(8L)) .setTriggerPriority(2L) - .setTriggerDedupKey(3L) + .setTriggerDedupKey(new UnsignedLong(3L)) .setReportTime(2000L) .setStatus(EventReport.Status.PENDING) + .setDebugReportStatus(EventReport.DebugReportStatus.PENDING) .setSourceType(Source.SourceType.NAVIGATION) .setTriggerDebugKey(TRIGGER_DEBUG_KEY) + .setSourceId(SOURCE_ID) + .setTriggerId(TRIGGER_ID) .build(); } private EventReport createExampleSingleSourceDebugKey() { return new EventReport.Builder() .setId("1") - .setSourceId(21) + .setSourceEventId(new UnsignedLong(21L)) .setEnrollmentId("enrollment-id") .setAttributionDestination(Uri.parse("https://bar.com")) .setTriggerTime(1000L) - .setTriggerData(8L) + .setTriggerData(new UnsignedLong(8L)) .setTriggerPriority(2L) - .setTriggerDedupKey(3L) + .setTriggerDedupKey(new UnsignedLong(3L)) .setReportTime(2000L) .setStatus(EventReport.Status.PENDING) + .setDebugReportStatus(EventReport.DebugReportStatus.PENDING) .setSourceType(Source.SourceType.NAVIGATION) .setSourceDebugKey(SOURCE_DEBUG_KEY) + .setSourceId(SOURCE_ID) + .setTriggerId(TRIGGER_ID) .build(); } } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/EventTriggerTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/EventTriggerTest.java index d31d653bd7..10b4b4bf8b 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/EventTriggerTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/EventTriggerTest.java @@ -18,7 +18,7 @@ package com.android.adservices.service.measurement; import static org.junit.Assert.*; -import com.android.adservices.service.measurement.aggregation.AggregateFilterData; +import com.android.adservices.service.measurement.util.UnsignedLong; import org.json.JSONException; import org.json.JSONObject; @@ -78,29 +78,29 @@ public class EventTriggerTest { EventTrigger eventTrigger1 = new EventTrigger.Builder() .setTriggerPriority(1L) - .setTriggerData(101L) - .setDedupKey(1001L) + .setTriggerData(new UnsignedLong(101L)) + .setDedupKey(new UnsignedLong(1001L)) .setFilter( - new AggregateFilterData.Builder() - .buildAggregateFilterData(sFilterData1) + new FilterData.Builder() + .buildFilterData(sFilterData1) .build()) .setNotFilter( - new AggregateFilterData.Builder() - .buildAggregateFilterData(sNotFilterData1) + new FilterData.Builder() + .buildFilterData(sNotFilterData1) .build()) .build(); EventTrigger eventTrigger2 = new EventTrigger.Builder() .setTriggerPriority(1L) - .setTriggerData(101L) - .setDedupKey(1001L) + .setTriggerData(new UnsignedLong(101L)) + .setDedupKey(new UnsignedLong(1001L)) .setFilter( - new AggregateFilterData.Builder() - .buildAggregateFilterData(sFilterData1) + new FilterData.Builder() + .buildFilterData(sFilterData1) .build()) .setNotFilter( - new AggregateFilterData.Builder() - .buildAggregateFilterData(sNotFilterData1) + new FilterData.Builder() + .buildFilterData(sNotFilterData1) .build()) .build(); @@ -113,35 +113,35 @@ public class EventTriggerTest { new EventTrigger.Builder().setTriggerPriority(1L).build(), new EventTrigger.Builder().setTriggerPriority(2L).build()); assertNotEquals( - new EventTrigger.Builder().setTriggerData(1L).build(), - new EventTrigger.Builder().setTriggerData(2L).build()); + new EventTrigger.Builder().setTriggerData(new UnsignedLong(1L)).build(), + new EventTrigger.Builder().setTriggerData(new UnsignedLong(2L)).build()); assertNotEquals( - new EventTrigger.Builder().setDedupKey(1L).build(), - new EventTrigger.Builder().setDedupKey(2L).build()); + new EventTrigger.Builder().setDedupKey(new UnsignedLong(1L)).build(), + new EventTrigger.Builder().setDedupKey(new UnsignedLong(2L)).build()); assertNotEquals( new EventTrigger.Builder() .setFilter( - new AggregateFilterData.Builder() - .buildAggregateFilterData(sFilterData1) + new FilterData.Builder() + .buildFilterData(sFilterData1) .build()) .build(), new EventTrigger.Builder() .setFilter( - new AggregateFilterData.Builder() - .buildAggregateFilterData(sFilterData2) + new FilterData.Builder() + .buildFilterData(sFilterData2) .build()) .build()); assertNotEquals( new EventTrigger.Builder() .setNotFilter( - new AggregateFilterData.Builder() - .buildAggregateFilterData(sNotFilterData1) + new FilterData.Builder() + .buildFilterData(sNotFilterData1) .build()) .build(), new EventTrigger.Builder() .setNotFilter( - new AggregateFilterData.Builder() - .buildAggregateFilterData(sNotFilterData2) + new FilterData.Builder() + .buildFilterData(sNotFilterData2) .build()) .build()); } @@ -163,15 +163,15 @@ public class EventTriggerTest { EventTrigger eventTrigger2 = new EventTrigger.Builder() .setTriggerPriority(2L) - .setTriggerData(101L) - .setDedupKey(1001L) + .setTriggerData(new UnsignedLong(101L)) + .setDedupKey(new UnsignedLong(1001L)) .setFilter( - new AggregateFilterData.Builder() - .buildAggregateFilterData(sFilterData1) + new FilterData.Builder() + .buildFilterData(sFilterData1) .build()) .setNotFilter( - new AggregateFilterData.Builder() - .buildAggregateFilterData(sNotFilterData1) + new FilterData.Builder() + .buildFilterData(sNotFilterData1) .build()) .build(); Set<EventTrigger> eventTriggerSet1 = Set.of(eventTrigger1); @@ -184,15 +184,15 @@ public class EventTriggerTest { private EventTrigger createExample() throws JSONException { return new EventTrigger.Builder() .setTriggerPriority(1L) - .setTriggerData(101L) - .setDedupKey(1001L) + .setTriggerData(new UnsignedLong(101L)) + .setDedupKey(new UnsignedLong(1001L)) .setFilter( - new AggregateFilterData.Builder() - .buildAggregateFilterData(sFilterData1) + new FilterData.Builder() + .buildFilterData(sFilterData1) .build()) .setNotFilter( - new AggregateFilterData.Builder() - .buildAggregateFilterData(sNotFilterData1) + new FilterData.Builder() + .buildFilterData(sNotFilterData1) .build()) .build(); } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/aggregation/AggregateFilterDataTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/FilterDataTest.java index 644ef1a07a..034b42e7df 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/aggregation/AggregateFilterDataTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/FilterDataTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.adservices.service.measurement.aggregation; +package com.android.adservices.service.measurement; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; @@ -30,13 +30,13 @@ import java.util.List; import java.util.Map; import java.util.Set; -/** Unit tests for {@link AggregateFilterData} */ +/** Unit tests for {@link FilterData} */ @SmallTest -public final class AggregateFilterDataTest { +public final class FilterDataTest { @Test public void testCreation() throws Exception { - AggregateFilterData attributionFilterData = createExample(); + FilterData attributionFilterData = createExample(); assertEquals(attributionFilterData.getAttributionFilterMap().size(), 2); assertEquals(attributionFilterData.getAttributionFilterMap().get("type").size(), 4); @@ -45,16 +45,16 @@ public final class AggregateFilterDataTest { @Test public void testDefaults() throws Exception { - AggregateFilterData data = new AggregateFilterData.Builder().build(); + FilterData data = new FilterData.Builder().build(); assertEquals(data.getAttributionFilterMap().size(), 0); } @Test public void testHashCode_equals() throws Exception { - final AggregateFilterData data1 = createExample(); - final AggregateFilterData data2 = createExample(); - final Set<AggregateFilterData> dataSet1 = Set.of(data1); - final Set<AggregateFilterData> dataSet2 = Set.of(data2); + final FilterData data1 = createExample(); + final FilterData data2 = createExample(); + final Set<FilterData> dataSet1 = Set.of(data1); + final Set<FilterData> dataSet2 = Set.of(data2); assertEquals(data1.hashCode(), data2.hashCode()); assertEquals(data1, data2); assertEquals(dataSet1, dataSet2); @@ -62,29 +62,29 @@ public final class AggregateFilterDataTest { @Test public void testHashCode_notEquals() throws Exception { - final AggregateFilterData data1 = createExample(); + final FilterData data1 = createExample(); Map<String, List<String>> attributionFilterMap = new HashMap<>(); attributionFilterMap.put("type", Arrays.asList("2", "3", "4")); attributionFilterMap.put("ctid", Collections.singletonList("id")); - final AggregateFilterData data2 = - new AggregateFilterData.Builder() + final FilterData data2 = + new FilterData.Builder() .setAttributionFilterMap(attributionFilterMap) .build(); - final Set<AggregateFilterData> dataSet1 = Set.of(data1); - final Set<AggregateFilterData> dataSet2 = Set.of(data2); + final Set<FilterData> dataSet1 = Set.of(data1); + final Set<FilterData> dataSet2 = Set.of(data2); assertNotEquals(data1.hashCode(), data2.hashCode()); assertNotEquals(data1, data2); assertNotEquals(dataSet1, dataSet2); } - private AggregateFilterData createExample() { + private FilterData createExample() { Map<String, List<String>> attributionFilterMap = new HashMap<>(); attributionFilterMap.put("type", Arrays.asList("1", "2", "3", "4")); attributionFilterMap.put("ctid", Collections.singletonList("id")); - return new AggregateFilterData.Builder() + return new FilterData.Builder() .setAttributionFilterMap(attributionFilterMap) .build(); } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/MeasurementHttpClientTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/MeasurementHttpClientTest.java index b52294d8c2..8ccb89a215 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/MeasurementHttpClientTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/MeasurementHttpClientTest.java @@ -28,6 +28,7 @@ import com.android.adservices.MockWebServerRuleFactory; import com.android.modules.utils.testing.TestableDeviceConfig; import com.google.mockwebserver.MockResponse; +import com.google.mockwebserver.MockWebServer; import org.json.JSONObject; import org.junit.Assert; @@ -70,6 +71,33 @@ public final class MeasurementHttpClientTest { } @Test + public void testSetup_headersLeakingInfoAreOverridden() throws Exception { + final MockWebServerRule mMockWebServerRule = MockWebServerRuleFactory.createForHttps(); + MockWebServer server = null; + try { + server = + mMockWebServerRule.startMockWebServer( + request -> { + Assert.assertNotNull(request); + final String userAgentHeader = request.getHeader("user-agent"); + Assert.assertNotNull(userAgentHeader); + Assert.assertEquals("", userAgentHeader); + return new MockResponse().setResponseCode(200); + }); + + final URL url = server.getUrl("/test"); + final HttpURLConnection urlConnection = + (HttpURLConnection) mNetworkConnection.setup(url); + + Assert.assertEquals(200, urlConnection.getResponseCode()); + } finally { + if (server != null) { + server.shutdown(); + } + } + } + + @Test public void testOpenAndSetupConnectionOverrideTimeoutValues_success() throws Exception { DeviceConfig.setProperty( DeviceConfig.NAMESPACE_ADSERVICES, diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/MeasurementImplTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/MeasurementImplTest.java index 7cf2175b20..b81c5f7955 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/MeasurementImplTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/MeasurementImplTest.java @@ -18,73 +18,46 @@ package com.android.adservices.service.measurement; import static android.adservices.common.AdServicesStatusUtils.STATUS_INTERNAL_ERROR; import static android.adservices.common.AdServicesStatusUtils.STATUS_INVALID_ARGUMENT; -import static android.adservices.common.AdServicesStatusUtils.STATUS_IO_ERROR; import static android.adservices.common.AdServicesStatusUtils.STATUS_SUCCESS; import static android.view.MotionEvent.ACTION_BUTTON_PRESS; -import static com.android.adservices.data.measurement.DatastoreManager.ThrowingCheckedConsumer; import static com.android.adservices.service.measurement.attribution.TriggerContentProvider.TRIGGER_URI; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.adservices.measurement.DeletionParam; import android.adservices.measurement.DeletionRequest; -import android.adservices.measurement.MeasurementManager; -import android.adservices.measurement.RegistrationRequest; import android.adservices.measurement.WebSourceParams; import android.adservices.measurement.WebSourceRegistrationRequest; import android.adservices.measurement.WebSourceRegistrationRequestInternal; import android.adservices.measurement.WebTriggerParams; import android.adservices.measurement.WebTriggerRegistrationRequest; import android.adservices.measurement.WebTriggerRegistrationRequestInternal; -import android.content.AttributionSource; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.RemoteException; -import android.test.mock.MockContext; -import android.test.mock.MockPackageManager; import android.view.InputEvent; import android.view.MotionEvent; import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.SmallTest; +import com.android.adservices.data.DbTestUtil; import com.android.adservices.data.enrollment.EnrollmentDao; -import com.android.adservices.data.measurement.DatastoreException; import com.android.adservices.data.measurement.DatastoreManager; -import com.android.adservices.data.measurement.DatastoreManagerFactory; -import com.android.adservices.data.measurement.IMeasurementDao; -import com.android.adservices.data.measurement.ITransaction; +import com.android.adservices.data.measurement.SQLDatastoreManager; +import com.android.adservices.data.measurement.deletion.MeasurementDataDeleter; import com.android.adservices.service.Flags; import com.android.adservices.service.FlagsFactory; -import com.android.adservices.service.consent.AdServicesApiConsent; -import com.android.adservices.service.consent.ConsentManager; import com.android.adservices.service.enrollment.EnrollmentData; import com.android.adservices.service.measurement.inputverification.ClickVerifier; -import com.android.adservices.service.measurement.registration.SourceFetcher; -import com.android.adservices.service.measurement.registration.SourceRegistration; -import com.android.adservices.service.measurement.registration.TriggerFetcher; -import com.android.adservices.service.measurement.registration.TriggerRegistration; import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.modules.utils.testing.TestableDeviceConfig; @@ -92,26 +65,16 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.MockitoSession; import org.mockito.Spy; import org.mockito.quality.Strictness; -import org.mockito.stubbing.Answer; import java.time.Instant; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import java.util.stream.IntStream; /** Unit tests for {@link MeasurementImpl} */ @SmallTest @@ -119,160 +82,48 @@ public final class MeasurementImplTest { @Rule public final TestableDeviceConfig.TestableDeviceConfigRule mDeviceConfigRule = new TestableDeviceConfig.TestableDeviceConfigRule(); - private static final Context DEFAULT_CONTEXT = ApplicationProvider.getApplicationContext(); - private static final Uri URI_WITHOUT_APP_SCHEME = Uri.parse("com.example.abc"); private static final Uri DEFAULT_URI = Uri.parse("android-app://com.example.abc"); private static final Uri REGISTRATION_URI_1 = Uri.parse("https://foo.com/bar?ad=134"); private static final Uri REGISTRATION_URI_2 = Uri.parse("https://foo.com/bar?ad=256"); private static final String DEFAULT_ENROLLMENT = "enrollment-id"; + private static final Uri INVALID_WEB_DESTINATION = Uri.parse("https://example.not_a_tld"); private static final Uri WEB_DESTINATION = Uri.parse("https://web-destination.com"); - private static final Uri WEB_DESTINATION_WITH_SUBDOMAIN = - Uri.parse("https://subdomain.web-destination.com"); private static final Uri OTHER_WEB_DESTINATION = Uri.parse("https://other-web-destination.com"); - private static final Uri INVALID_WEB_DESTINATION = Uri.parse("https://example.not_a_tld"); private static final Uri APP_DESTINATION = Uri.parse("android-app://com.app_destination"); private static final Uri OTHER_APP_DESTINATION = Uri.parse("android-app://com.other_app_destination"); - private static final String ANDROID_APP_SCHEME = "android-app://"; - private static final String VENDING_PREFIX = "https://play.google.com/store/apps/details?id="; - private static final RegistrationRequest SOURCE_REGISTRATION_REQUEST = - createRegistrationRequest(RegistrationRequest.REGISTER_SOURCE); - private static final RegistrationRequest TRIGGER_REGISTRATION_REQUEST = - createRegistrationRequest(RegistrationRequest.REGISTER_TRIGGER); - private static final String TOP_LEVEL_FILTERS_JSON_STRING = - "{\n" - + " \"key_1\": [\"value_1\", \"value_2\"],\n" - + " \"key_2\": [\"value_1\", \"value_2\"]\n" - + "}\n"; - private static final long TRIGGER_PRIORITY = 345678L; - private static final Long TRIGGER_DEDUP_KEY = 2345678L; - private static final Long TRIGGER_DATA = 1L; - private static final String EVENT_TRIGGERS = - "[\n" - + "{\n" - + " \"trigger_data\": \"" - + TRIGGER_DATA - + "\",\n" - + " \"priority\": \"" - + TRIGGER_PRIORITY - + "\",\n" - + " \"deduplication_key\": \"" - + TRIGGER_DEDUP_KEY - + "\",\n" - + " \"filters\": {\n" - + " \"source_type\": [\"navigation\"],\n" - + " \"key_1\": [\"value_1\"] \n" - + " }\n" - + "}" - + "]\n"; - private static final TriggerRegistration VALID_TRIGGER_REGISTRATION = - new TriggerRegistration.Builder() - .setTopOrigin(Uri.parse("https://foo.com")) - .setEnrollmentId(DEFAULT_ENROLLMENT) - .setEventTriggers(EVENT_TRIGGERS) - .setAggregateTriggerData( - "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," - + "\"not_filters\":{\"product\":[\"1\"]}}," - + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]") - .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") - .setFilters(TOP_LEVEL_FILTERS_JSON_STRING) - .build(); - private static final SourceRegistration VALID_SOURCE_REGISTRATION_1 = - new com.android.adservices.service.measurement.registration.SourceRegistration.Builder() - .setSourceEventId(1L) - .setSourcePriority(100L) - .setAppDestination(Uri.parse("android-app://com.destination")) - .setWebDestination(Uri.parse("https://web-destination.com")) - .setExpiry(8640000010L) - .setInstallAttributionWindow(841839879274L) - .setInstallCooldownWindow(8418398274L) - .setEnrollmentId(DEFAULT_ENROLLMENT) - .setTopOrigin(Uri.parse("android-app://com.source")) - .build(); - private static final SourceRegistration VALID_SOURCE_REGISTRATION_2 = - new com.android.adservices.service.measurement.registration.SourceRegistration.Builder() - .setSourceEventId(2) - .setSourcePriority(200L) - .setAppDestination(Uri.parse("android-app://com.destination2")) - .setWebDestination(Uri.parse("https://web-destination2.com")) - .setExpiry(865000010L) - .setInstallAttributionWindow(841839879275L) - .setInstallCooldownWindow(7418398274L) - .setEnrollmentId(DEFAULT_ENROLLMENT) - .setTopOrigin(Uri.parse("android-app://com.source2")) - .build(); + private static final WebSourceParams INPUT_SOURCE_REGISTRATION_1 = new WebSourceParams.Builder(REGISTRATION_URI_1).setDebugKeyAllowed(true).build(); - private static final WebSourceParams INPUT_SOURCE_REGISTRATION_2 = new WebSourceParams.Builder(REGISTRATION_URI_2).setDebugKeyAllowed(false).build(); - private static final WebTriggerParams INPUT_TRIGGER_REGISTRATION_1 = new WebTriggerParams.Builder(REGISTRATION_URI_1).setDebugKeyAllowed(true).build(); - private static final WebTriggerParams INPUT_TRIGGER_REGISTRATION_2 = new WebTriggerParams.Builder(REGISTRATION_URI_2).setDebugKeyAllowed(false).build(); - private static final long REQUEST_TIME = 10000L; @Spy private DatastoreManager mDatastoreManager = - DatastoreManagerFactory.getDatastoreManager(DEFAULT_CONTEXT); + new SQLDatastoreManager(DbTestUtil.getDbHelperForTest()); @Mock private ContentProviderClient mMockContentProviderClient; @Mock private ContentResolver mContentResolver; - @Mock - private SourceFetcher mSourceFetcher; - @Mock - private TriggerFetcher mTriggerFetcher; - @Mock - private IMeasurementDao mMeasurementDao; - @Mock - private ConsentManager mConsentManager; @Mock private ClickVerifier mClickVerifier; private MeasurementImpl mMeasurementImpl; @Mock - ITransaction mTransaction; - @Mock EnrollmentDao mEnrollmentDao; + @Mock MeasurementDataDeleter mMeasurementDataDeleter; private static EnrollmentData getEnrollment(String enrollmentId) { return new EnrollmentData.Builder().setEnrollmentId(enrollmentId).build(); } - - class FakeDatastoreManager extends DatastoreManager { - - @Override - public ITransaction createNewTransaction() { - return mTransaction; - } - - @Override - public IMeasurementDao getMeasurementDao() { - return mMeasurementDao; - } - } - - private interface AppVendorPackages { - String PLAY_STORE = "com.android.vending"; - } - public static InputEvent getInputEvent() { return MotionEvent.obtain(0, 0, ACTION_BUTTON_PRESS, 0, 0, 0); } - - private static RegistrationRequest createRegistrationRequest(int type) { - return new RegistrationRequest.Builder() - .setRegistrationUri(REGISTRATION_URI_1) - .setTopOriginUri(DEFAULT_URI) - .setPackageName(DEFAULT_CONTEXT.getAttributionSource().getPackageName()) - .setRegistrationType(type) - .build(); - } - private static WebTriggerRegistrationRequestInternal createWebTriggerRegistrationRequest( Uri destination) { WebTriggerRegistrationRequest webTriggerRegistrationRequest = @@ -284,29 +135,32 @@ public final class MeasurementImplTest { return new WebTriggerRegistrationRequestInternal.Builder( webTriggerRegistrationRequest, DEFAULT_CONTEXT.getAttributionSource().getPackageName()) + .setAdIdPermissionGranted(true) .build(); } - private static WebSourceRegistrationRequestInternal createWebSourceRegistrationRequest( Uri appDestination, Uri webDestination, Uri verifiedDestination) { - + return createWebSourceRegistrationRequest( + appDestination, webDestination, verifiedDestination, DEFAULT_URI); + } + private static WebSourceRegistrationRequestInternal createWebSourceRegistrationRequest( + Uri appDestination, Uri webDestination, Uri verifiedDestination, Uri topOriginUri) { WebSourceRegistrationRequest sourceRegistrationRequest = new WebSourceRegistrationRequest.Builder( Arrays.asList( INPUT_SOURCE_REGISTRATION_1, INPUT_SOURCE_REGISTRATION_2), - DEFAULT_URI) + topOriginUri) .setAppDestination(appDestination) .setWebDestination(webDestination) .setVerifiedDestination(verifiedDestination) .build(); - return new WebSourceRegistrationRequestInternal.Builder( sourceRegistrationRequest, DEFAULT_CONTEXT.getAttributionSource().getPackageName(), REQUEST_TIME) + .setAdIdPermissionGranted(true) .build(); } - @Before public void before() throws RemoteException { MockitoAnnotations.initMocks(this); @@ -317,264 +171,25 @@ public final class MeasurementImplTest { spy( new MeasurementImpl( DEFAULT_CONTEXT, - mContentResolver, mDatastoreManager, - mSourceFetcher, - mTriggerFetcher, - mClickVerifier)); + mClickVerifier, + mMeasurementDataDeleter, + mEnrollmentDao)); doReturn(true).when(mClickVerifier).isInputEventVerifiable(any(), anyLong()); when(mEnrollmentDao.getEnrollmentDataFromMeasurementUrl(any())) .thenReturn(getEnrollment(DEFAULT_ENROLLMENT)); } @Test - public void testRegister_registrationTypeSource_sourceFetchSuccess() - throws RemoteException, DatastoreException { - // setup - List<SourceRegistration> sourceRegistrationsOut = - Arrays.asList(VALID_SOURCE_REGISTRATION_1, VALID_SOURCE_REGISTRATION_2); - RegistrationRequest registrationRequest = SOURCE_REGISTRATION_REQUEST; - doReturn(Optional.of(sourceRegistrationsOut)).when(mSourceFetcher).fetchSource(any()); - ArgumentCaptor<ThrowingCheckedConsumer> insertionLogicExecutorCaptor = - ArgumentCaptor.forClass(ThrowingCheckedConsumer.class); - - when(mMeasurementDao.countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( - any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong())) - .thenReturn(Integer.valueOf(0)); - when(mMeasurementDao.countDistinctEnrollmentsPerPublisherXDestinationInSource( - any(), anyInt(), any(), any(), anyLong(), anyLong())) - .thenReturn(Integer.valueOf(0)); - DatastoreManager datastoreManager = spy(new FakeDatastoreManager()); - - // Test - MeasurementImpl measurementImpl = - spy( - new MeasurementImpl( - null, - mContentResolver, - datastoreManager, - mSourceFetcher, - mTriggerFetcher, - mClickVerifier)); - - long eventTime = System.currentTimeMillis(); - // Disable Impression Noise - doReturn(Collections.emptyList()).when(measurementImpl).generateFakeEventReports(any()); - final int result = measurementImpl.register(registrationRequest, eventTime); - - // Assert - assertEquals(STATUS_SUCCESS, result); - verify(mMockContentProviderClient, never()).insert(any(), any()); - verify(mSourceFetcher, times(1)).fetchSource(any()); - verify(datastoreManager, times(2)) - .runInTransaction(insertionLogicExecutorCaptor.capture()); - verify(mMeasurementDao, times(4)) - .countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( - any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong()); - verify(mMeasurementDao, times(4)).countDistinctEnrollmentsPerPublisherXDestinationInSource( - any(), anyInt(), any(), any(), anyLong(), anyLong()); - verify(mTriggerFetcher, never()).fetchTrigger(any()); - - List<ThrowingCheckedConsumer> insertionLogicExecutor = - insertionLogicExecutorCaptor.getAllValues(); - assertEquals(2, insertionLogicExecutor.size()); - - verifyInsertSource( - registrationRequest, - VALID_SOURCE_REGISTRATION_1, - eventTime, - VALID_SOURCE_REGISTRATION_1.getAppDestination(), - VALID_SOURCE_REGISTRATION_1.getWebDestination()); - verifyInsertSource( - registrationRequest, - VALID_SOURCE_REGISTRATION_2, - eventTime, - VALID_SOURCE_REGISTRATION_1.getAppDestination(), - VALID_SOURCE_REGISTRATION_1.getWebDestination()); - } - - @Test - public void testRegister_registrationTypeSource_sourceFetchFailure() { - when(mSourceFetcher.fetchSource(any())).thenReturn(Optional.empty()); - - // Disable Impression Noise - doReturn(Collections.emptyList()).when(mMeasurementImpl).generateFakeEventReports(any()); - final int result = - mMeasurementImpl.register(SOURCE_REGISTRATION_REQUEST, System.currentTimeMillis()); - // STATUS_IO_ERROR is expected when fetchSource returns Optional.empty() - assertEquals(STATUS_IO_ERROR, result); - verify(mSourceFetcher, times(1)).fetchSource(any()); - verify(mTriggerFetcher, never()).fetchTrigger(any()); - } - - @Test - public void testRegister_registrationTypeSource_exceedsPrivacyParam_destination() - throws RemoteException, DatastoreException { - // setup - List<SourceRegistration> sourceRegistrationsOut = - Arrays.asList(VALID_SOURCE_REGISTRATION_1, VALID_SOURCE_REGISTRATION_2); - RegistrationRequest registrationRequest = SOURCE_REGISTRATION_REQUEST; - doReturn(Optional.of(sourceRegistrationsOut)).when(mSourceFetcher).fetchSource(any()); - ArgumentCaptor<ThrowingCheckedConsumer> insertionLogicExecutorCaptor = - ArgumentCaptor.forClass(ThrowingCheckedConsumer.class); - - when(mMeasurementDao.countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( - any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong())) - .thenReturn(Integer.valueOf(100)); - when(mMeasurementDao.countDistinctEnrollmentsPerPublisherXDestinationInSource( - any(), anyInt(), any(), any(), anyLong(), anyLong())) - .thenReturn(Integer.valueOf(0)); - DatastoreManager datastoreManager = spy(new FakeDatastoreManager()); - - // Test - MeasurementImpl measurement = - spy( - new MeasurementImpl( - null, - mContentResolver, - datastoreManager, - mSourceFetcher, - mTriggerFetcher, - mClickVerifier)); - - long eventTime = System.currentTimeMillis(); - // Disable Impression Noise - doReturn(Collections.emptyList()).when(measurement).generateFakeEventReports(any()); - final int result = measurement.register(registrationRequest, eventTime); - - // Assert - assertEquals(STATUS_SUCCESS, result); - verify(mMockContentProviderClient, never()).insert(any(), any()); - verify(mSourceFetcher, times(1)).fetchSource(any()); - verify(datastoreManager, never()) - .runInTransaction(insertionLogicExecutorCaptor.capture()); - verify(mMeasurementDao, times(2)) - .countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( - any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong()); - verify(mMeasurementDao, never()).countDistinctEnrollmentsPerPublisherXDestinationInSource( - any(), anyInt(), any(), any(), anyLong(), anyLong()); - verify(mTriggerFetcher, never()).fetchTrigger(any()); - } - - @Test - public void testRegister_registrationTypeSource_exceedsPrivacyParam_adTech() - throws RemoteException, DatastoreException { - // setup - List<SourceRegistration> sourceRegistrationsOut = - Arrays.asList(VALID_SOURCE_REGISTRATION_1, VALID_SOURCE_REGISTRATION_2); - RegistrationRequest registrationRequest = SOURCE_REGISTRATION_REQUEST; - doReturn(Optional.of(sourceRegistrationsOut)).when(mSourceFetcher).fetchSource(any()); - ArgumentCaptor<ThrowingCheckedConsumer> insertionLogicExecutorCaptor = - ArgumentCaptor.forClass(ThrowingCheckedConsumer.class); - - when(mMeasurementDao.countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( - any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong())) - .thenReturn(Integer.valueOf(0)); - when(mMeasurementDao.countDistinctEnrollmentsPerPublisherXDestinationInSource( - any(), anyInt(), any(), any(), anyLong(), anyLong())) - .thenReturn(Integer.valueOf(100)) - .thenReturn(Integer.valueOf(0)); - DatastoreManager datastoreManager = spy(new FakeDatastoreManager()); - - // Test - MeasurementImpl measurement = - spy( - new MeasurementImpl( - null, - mContentResolver, - datastoreManager, - mSourceFetcher, - mTriggerFetcher, - mClickVerifier)); - - long eventTime = System.currentTimeMillis(); - // Disable Impression Noise - doReturn(Collections.emptyList()).when(measurement).generateFakeEventReports(any()); - final int result = measurement.register(registrationRequest, eventTime); - - // Assert - assertEquals(STATUS_SUCCESS, result); - verify(mMockContentProviderClient, never()).insert(any(), any()); - verify(mSourceFetcher, times(1)).fetchSource(any()); - verify(datastoreManager, times(1)) - .runInTransaction(insertionLogicExecutorCaptor.capture()); - verify(mMeasurementDao, times(4)) - .countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( - any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong()); - verify(mMeasurementDao, times(3)).countDistinctEnrollmentsPerPublisherXDestinationInSource( - any(), anyInt(), any(), any(), anyLong(), anyLong()); - verify(mTriggerFetcher, never()).fetchTrigger(any()); - - List<ThrowingCheckedConsumer> insertionLogicExecutor = - insertionLogicExecutorCaptor.getAllValues(); - assertEquals(1, insertionLogicExecutor.size()); - - // First registration was removed for exceeding the privacy bound. - verifyInsertSource( - registrationRequest, - VALID_SOURCE_REGISTRATION_2, - eventTime, - VALID_SOURCE_REGISTRATION_1.getAppDestination(), - VALID_SOURCE_REGISTRATION_1.getWebDestination()); - } - - @Test - public void testRegister_registrationTypeTrigger_triggerFetchSuccess() throws Exception { - ArgumentCaptor<ThrowingCheckedConsumer> consumerArgumentCaptor = - ArgumentCaptor.forClass(ThrowingCheckedConsumer.class); - final long triggerTime = System.currentTimeMillis(); - Answer<Optional<List<TriggerRegistration>>> populateTriggerRegistrations = - invocation -> { - List<TriggerRegistration> triggerRegs = new ArrayList<>(); - triggerRegs.add(VALID_TRIGGER_REGISTRATION); - return Optional.of(triggerRegs); - }; - doAnswer(populateTriggerRegistrations) - .when(mTriggerFetcher) - .fetchTrigger(TRIGGER_REGISTRATION_REQUEST); - - // Execution - final int result = mMeasurementImpl.register(TRIGGER_REGISTRATION_REQUEST, triggerTime); - verify(mDatastoreManager).runInTransaction(consumerArgumentCaptor.capture()); - consumerArgumentCaptor.getValue().accept(mMeasurementDao); - - // Assertions - assertEquals(STATUS_SUCCESS, result); - verify(mMockContentProviderClient).insert(any(), any()); - verify(mSourceFetcher, never()).fetchSource(any()); - verify(mTriggerFetcher, times(1)).fetchTrigger(any()); - Trigger trigger = - createTrigger( - triggerTime, - DEFAULT_CONTEXT.getAttributionSource(), - DEFAULT_URI, - EventSurfaceType.APP); - verify(mMeasurementDao).insertTrigger(trigger); - } - - @Test - public void testRegister_registrationTypeTrigger_triggerFetchFailure() throws RemoteException { - when(mTriggerFetcher.fetchTrigger(any())).thenReturn(Optional.empty()); - - final int result = - mMeasurementImpl.register(TRIGGER_REGISTRATION_REQUEST, System.currentTimeMillis()); - assertEquals(STATUS_IO_ERROR, result); - - verify(mMockContentProviderClient, never()).insert(any(), any()); - verify(mSourceFetcher, never()).fetchSource(any()); - verify(mTriggerFetcher, times(1)).fetchTrigger(any()); - } - - @Test public void testDeleteRegistrations_successfulNoOptionalParameters() { MeasurementImpl measurement = new MeasurementImpl( DEFAULT_CONTEXT, - mContentResolver, - DatastoreManagerFactory.getDatastoreManager(DEFAULT_CONTEXT), - mSourceFetcher, - mTriggerFetcher, - mClickVerifier); + new SQLDatastoreManager(DbTestUtil.getDbHelperForTest()), + mClickVerifier, + mMeasurementDataDeleter, + mEnrollmentDao); + doReturn(true).when(mMeasurementDataDeleter).delete(any()); final int result = measurement.deleteRegistrations( new DeletionParam.Builder() @@ -589,7 +204,18 @@ public final class MeasurementImplTest { } @Test + public void testRegisterWebSource_verifiedDestination_webDestinationMismatch() { + final int result = + mMeasurementImpl.registerWebSource( + createWebSourceRegistrationRequest( + APP_DESTINATION, WEB_DESTINATION, OTHER_WEB_DESTINATION), + System.currentTimeMillis()); + assertEquals(STATUS_INVALID_ARGUMENT, result); + } + + @Test public void testDeleteRegistrations_successfulWithRange() { + doReturn(true).when(mMeasurementDataDeleter).delete(any()); final int result = mMeasurementImpl.deleteRegistrations( new DeletionParam.Builder() @@ -607,24 +233,25 @@ public final class MeasurementImplTest { @Test public void testDeleteRegistrations_successfulWithOrigin() { - final int result = - mMeasurementImpl.deleteRegistrations( - new DeletionParam.Builder() - .setPackageName( - DEFAULT_CONTEXT.getAttributionSource().getPackageName()) - .setDomainUris(Collections.emptyList()) - .setOriginUris(Collections.singletonList(DEFAULT_URI)) - .setMatchBehavior(DeletionRequest.MATCH_BEHAVIOR_DELETE) - .setDeletionMode(DeletionRequest.DELETION_MODE_ALL) - .setStart(Instant.ofEpochMilli(Long.MIN_VALUE)) - .setEnd(Instant.ofEpochMilli(Long.MAX_VALUE)) - .build()); + DeletionParam deletionParam = + new DeletionParam.Builder() + .setPackageName(DEFAULT_CONTEXT.getAttributionSource().getPackageName()) + .setDomainUris(Collections.emptyList()) + .setOriginUris(Collections.singletonList(DEFAULT_URI)) + .setMatchBehavior(DeletionRequest.MATCH_BEHAVIOR_DELETE) + .setDeletionMode(DeletionRequest.DELETION_MODE_ALL) + .setStart(Instant.ofEpochMilli(Long.MIN_VALUE)) + .setEnd(Instant.ofEpochMilli(Long.MAX_VALUE)) + .build(); + when(mMeasurementDataDeleter.delete(deletionParam)).thenReturn(true); + final int result = mMeasurementImpl.deleteRegistrations(deletionParam); assertEquals(STATUS_SUCCESS, result); } @Test public void testDeleteRegistrations_internalError() { doReturn(false).when(mDatastoreManager).runInTransaction(any()); + doReturn(false).when(mMeasurementDataDeleter).delete(any()); final int result = mMeasurementImpl.deleteRegistrations( new DeletionParam.Builder() @@ -638,607 +265,34 @@ public final class MeasurementImplTest { .setEnd(Instant.MAX) .build()); assertEquals(STATUS_INTERNAL_ERROR, result); - } - - @Test - public void testSourceRegistration_callsImpressionNoiseCreator() throws DatastoreException { - long eventTime = System.currentTimeMillis(); - long expiry = TimeUnit.DAYS.toSeconds(20); - // Creating source for easy comparison - Source sampleSource = - SourceFixture.getValidSourceBuilder() - .setSourceType(Source.SourceType.NAVIGATION) - .setExpiryTime(eventTime + TimeUnit.SECONDS.toMillis(expiry)) - .setEventTime(eventTime) - .setPublisher(DEFAULT_URI) - .setAppDestination(Uri.parse("android-app://com.example.abc")) - .setEventId(123L) - .build(); - // Mocking fetchSource call to populate source registrations. - List<SourceRegistration> sourceRegistrations = - Collections.singletonList( - new SourceRegistration.Builder() - .setSourceEventId(sampleSource.getEventId()) - .setAppDestination(sampleSource.getAppDestination()) - .setTopOrigin(sampleSource.getPublisher()) - .setExpiry(expiry) - .setEnrollmentId(DEFAULT_ENROLLMENT) - .build()); - doReturn(Optional.of(sourceRegistrations)).when(mSourceFetcher).fetchSource(any()); - - when(mMeasurementDao.countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( - any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong())) - .thenReturn(Integer.valueOf(0)); - when(mMeasurementDao.countDistinctEnrollmentsPerPublisherXDestinationInSource( - any(), anyInt(), any(), any(), anyLong(), anyLong())) - .thenReturn(Integer.valueOf(0)); - - MeasurementImpl measurementImpl = - spy( - new MeasurementImpl( - null, - mContentResolver, - new FakeDatastoreManager(), - mSourceFetcher, - mTriggerFetcher, - mClickVerifier)); - - InputEvent inputEvent = getInputEvent(); - final int result = - measurementImpl.register( - new RegistrationRequest.Builder() - .setRegistrationUri(REGISTRATION_URI_1) - .setTopOriginUri(DEFAULT_URI) - .setPackageName( - DEFAULT_CONTEXT.getAttributionSource().getPackageName()) - .setRegistrationType(RegistrationRequest.REGISTER_SOURCE) - .setInputEvent(inputEvent) - .build(), - eventTime); - assertEquals(STATUS_SUCCESS, result); - ArgumentCaptor<Source> sourceArgs = ArgumentCaptor.forClass(Source.class); - verify(measurementImpl).generateFakeEventReports(sourceArgs.capture()); - Source capturedSource = sourceArgs.getValue(); - assertEquals(sampleSource.getSourceType(), capturedSource.getSourceType()); - assertEquals(sampleSource.getEventId(), capturedSource.getEventId()); - assertEquals(sampleSource.getEventTime(), capturedSource.getEventTime()); - assertEquals(sampleSource.getAggregateSource(), capturedSource.getAggregateSource()); - assertEquals(sampleSource.getAppDestination(), capturedSource.getAppDestination()); - assertEquals(sampleSource.getPublisher(), capturedSource.getPublisher()); - assertEquals(sampleSource.getPriority(), capturedSource.getPriority()); - - // Check Attribution Mode assignment - assertNotEquals(Source.AttributionMode.UNASSIGNED, capturedSource.getAttributionMode()); - } - - @Test - public void testGetSourceEventReports() { - long eventTime = System.currentTimeMillis(); - Source source = - spy( - SourceFixture.getValidSourceBuilder() - .setEventId(123L) - .setEventTime(eventTime) - .setExpiryTime(eventTime + TimeUnit.DAYS.toMillis(20)) - .setSourceType(Source.SourceType.NAVIGATION) - .setAppDestination(DEFAULT_URI) - .setPublisher(DEFAULT_URI) - .build()); - when(source.getRandomAttributionProbability()).thenReturn(1.1D); - DatastoreManager mockDatastoreManager = Mockito.mock(DatastoreManager.class); - SourceFetcher mockSourceFetcher = Mockito.mock(SourceFetcher.class); - TriggerFetcher mockTriggerFetcher = Mockito.mock(TriggerFetcher.class); - - List<EventReport> fakeEventReports = mMeasurementImpl.generateFakeEventReports(source); - - // Generate valid report times - Set<Long> reportingTimes = new HashSet<>(); - reportingTimes.add( - source.getReportingTime( - eventTime + TimeUnit.DAYS.toMillis(1), EventSurfaceType.APP)); - reportingTimes.add( - source.getReportingTime( - eventTime + TimeUnit.DAYS.toMillis(3), EventSurfaceType.APP)); - reportingTimes.add( - source.getReportingTime( - eventTime + TimeUnit.DAYS.toMillis(8), EventSurfaceType.APP)); - - for (EventReport report : fakeEventReports) { - Assert.assertEquals(source.getEventId(), report.getSourceId()); - Assert.assertTrue(reportingTimes.stream().anyMatch(x -> x == report.getReportTime())); - Assert.assertEquals(source.getEventTime(), report.getTriggerTime()); - Assert.assertEquals(0, report.getTriggerPriority()); - Assert.assertEquals(source.getAppDestination(), report.getAttributionDestination()); - Assert.assertTrue(report.getTriggerData() - < source.getTriggerDataCardinality()); - Assert.assertNull(report.getTriggerDedupKey()); - Assert.assertEquals(EventReport.Status.PENDING, report.getStatus()); - Assert.assertEquals(source.getSourceType(), report.getSourceType()); - Assert.assertEquals(source.getRandomAttributionProbability(), - report.getRandomizedTriggerRate(), /* delta= */ 0.00001D); - } - } - - @Test - public void testInstallAttribution() throws DatastoreException { - // Setup - long systemTime = System.currentTimeMillis(); - - ArgumentCaptor<ThrowingCheckedConsumer> consumerArgumentCaptor = - ArgumentCaptor.forClass(ThrowingCheckedConsumer.class); - - // Execution - mMeasurementImpl.doInstallAttribution(URI_WITHOUT_APP_SCHEME, systemTime); - verify(mDatastoreManager).runInTransaction(consumerArgumentCaptor.capture()); - - consumerArgumentCaptor.getValue().accept(mMeasurementDao); - - // Assertion - verify(mMeasurementDao).doInstallAttribution(DEFAULT_URI, systemTime); - } - - @Test - public void testGetMeasurementApiStatus_enabled() { - MockitoSession session = - ExtendedMockito.mockitoSession() - .spyStatic(ConsentManager.class) - .initMocks(this) - .startMocking(); - try { - ExtendedMockito.doReturn(AdServicesApiConsent.GIVEN) - .when(mConsentManager) - .getConsent(any()); - ExtendedMockito.doReturn(mConsentManager).when(() -> ConsentManager.getInstance(any())); - final int result = mMeasurementImpl.getMeasurementApiStatus(); - assertEquals(MeasurementManager.MEASUREMENT_API_STATE_ENABLED, result); - } finally { - session.finishMocking(); } - } - - @Test - public void testGetMeasurementApiStatus_disabled() { - MockitoSession session = - ExtendedMockito.mockitoSession() - .spyStatic(ConsentManager.class) - .initMocks(this) - .startMocking(); - try { - ExtendedMockito.doReturn(AdServicesApiConsent.REVOKED) - .when(mConsentManager) - .getConsent(any()); - ExtendedMockito.doReturn(mConsentManager).when(() -> ConsentManager.getInstance(any())); - final int result = mMeasurementImpl.getMeasurementApiStatus(); - assertEquals(MeasurementManager.MEASUREMENT_API_STATE_DISABLED, result); - } finally { - session.finishMocking(); - } - } - - @Test - public void testDeleteAllMeasurementData() throws DatastoreException { - ArgumentCaptor<ThrowingCheckedConsumer> consumerArgumentCaptor = - ArgumentCaptor.forClass(ThrowingCheckedConsumer.class); - - // Execution - mMeasurementImpl.deleteAllMeasurementData(Collections.emptyList()); - verify(mDatastoreManager).runInTransaction(consumerArgumentCaptor.capture()); - - consumerArgumentCaptor.getValue().accept(mMeasurementDao); - verify(mMeasurementDao, times(1)).deleteAllMeasurementData(any()); - } - - @Test - public void registerWebSource_sourceFetchSuccess() throws RemoteException, DatastoreException { - // setup - List<SourceRegistration> sourceRegistrationsOut = - Arrays.asList(VALID_SOURCE_REGISTRATION_1, VALID_SOURCE_REGISTRATION_2); - WebSourceRegistrationRequestInternal registrationRequest = - createWebSourceRegistrationRequest(APP_DESTINATION, WEB_DESTINATION, null); - doReturn(Optional.of(sourceRegistrationsOut)) - .when(mSourceFetcher) - .fetchWebSources(any(), anyBoolean()); - ArgumentCaptor<ThrowingCheckedConsumer> insertionLogicExecutorCaptor = - ArgumentCaptor.forClass(ThrowingCheckedConsumer.class); - - when(mMeasurementDao.countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( - any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong())) - .thenReturn(Integer.valueOf(0)); - when(mMeasurementDao.countDistinctEnrollmentsPerPublisherXDestinationInSource( - any(), anyInt(), any(), any(), anyLong(), anyLong())) - .thenReturn(Integer.valueOf(0)); - DatastoreManager datastoreManager = spy(new FakeDatastoreManager()); - - // Test - MeasurementImpl measurementImpl = - spy( - new MeasurementImpl( - null, - mContentResolver, - datastoreManager, - mSourceFetcher, - mTriggerFetcher, - mClickVerifier)); - - long eventTime = System.currentTimeMillis(); - // Disable Impression Noise - doReturn(Collections.emptyList()).when(measurementImpl).generateFakeEventReports(any()); - final int result = measurementImpl.registerWebSource(registrationRequest, eventTime); - - // Assert - assertEquals(STATUS_SUCCESS, result); - verify(mMockContentProviderClient, never()).insert(any(), any()); - verify(mSourceFetcher, times(1)).fetchWebSources(any(), anyBoolean()); - verify(datastoreManager, times(2)) - .runInTransaction(insertionLogicExecutorCaptor.capture()); - verify(mMeasurementDao, times(4)) - .countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( - any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong()); - verify(mMeasurementDao, times(4)).countDistinctEnrollmentsPerPublisherXDestinationInSource( - any(), anyInt(), any(), any(), anyLong(), anyLong()); - verify(mTriggerFetcher, never()).fetchWebTriggers(any(), anyBoolean()); - - List<ThrowingCheckedConsumer> insertionLogicExecutor = - insertionLogicExecutorCaptor.getAllValues(); - assertEquals(2, insertionLogicExecutor.size()); - - verifyInsertSource( - registrationRequest, - VALID_SOURCE_REGISTRATION_1, - eventTime, - VALID_SOURCE_REGISTRATION_1.getAppDestination(), - VALID_SOURCE_REGISTRATION_1.getWebDestination()); - verifyInsertSource( - registrationRequest, - VALID_SOURCE_REGISTRATION_2, - eventTime, - VALID_SOURCE_REGISTRATION_1.getAppDestination(), - VALID_SOURCE_REGISTRATION_1.getWebDestination()); - } - - @Test - public void registerWebSource_sourceFetchFailure() { - when(mSourceFetcher.fetchWebSources(any(), anyBoolean())).thenReturn(Optional.empty()); - - // Disable Impression Noise - doReturn(Collections.emptyList()).when(mMeasurementImpl).generateFakeEventReports(any()); - final int result = - mMeasurementImpl.registerWebSource( - createWebSourceRegistrationRequest(APP_DESTINATION, WEB_DESTINATION, null), - System.currentTimeMillis()); - assertEquals(STATUS_IO_ERROR, result); - verify(mSourceFetcher, times(1)).fetchWebSources(any(), anyBoolean()); - verify(mTriggerFetcher, never()).fetchWebTriggers(any(), anyBoolean()); - } @Test - public void registerWebSource_invalidWebDestination() { + public void testRegisterWebSource_invalidWebDestination() { final int result = mMeasurementImpl.registerWebSource( createWebSourceRegistrationRequest(null, INVALID_WEB_DESTINATION, null), System.currentTimeMillis()); assertEquals(STATUS_INVALID_ARGUMENT, result); - verify(mSourceFetcher, never()).fetchWebSources(any(), anyBoolean()); - } - - @Test - public void registerWebSource_exceedsPrivacyParam_destination() - throws RemoteException, DatastoreException { - // setup - List<SourceRegistration> sourceRegistrationsOut = - Arrays.asList(VALID_SOURCE_REGISTRATION_1, VALID_SOURCE_REGISTRATION_2); - WebSourceRegistrationRequestInternal registrationRequest = - createWebSourceRegistrationRequest(APP_DESTINATION, WEB_DESTINATION, null); - doReturn(Optional.of(sourceRegistrationsOut)) - .when(mSourceFetcher) - .fetchWebSources(any(), anyBoolean()); - ArgumentCaptor<ThrowingCheckedConsumer> insertionLogicExecutorCaptor = - ArgumentCaptor.forClass(ThrowingCheckedConsumer.class); - - when(mMeasurementDao.countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( - any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong())) - .thenReturn(Integer.valueOf(100)); - when(mMeasurementDao.countDistinctEnrollmentsPerPublisherXDestinationInSource( - any(), anyInt(), any(), any(), anyLong(), anyLong())) - .thenReturn(Integer.valueOf(0)); - DatastoreManager datastoreManager = spy(new FakeDatastoreManager()); - - // Test - MeasurementImpl measurement = - spy( - new MeasurementImpl( - null, - mContentResolver, - datastoreManager, - mSourceFetcher, - mTriggerFetcher, - mClickVerifier)); - - long eventTime = System.currentTimeMillis(); - // Disable Impression Noise - doReturn(Collections.emptyList()).when(measurement).generateFakeEventReports(any()); - final int result = measurement.registerWebSource(registrationRequest, eventTime); - - // Assert - assertEquals(STATUS_SUCCESS, result); - verify(mMockContentProviderClient, never()).insert(any(), any()); - verify(mSourceFetcher, times(1)).fetchWebSources(any(), anyBoolean()); - verify(datastoreManager, never()) - .runInTransaction(insertionLogicExecutorCaptor.capture()); - verify(mMeasurementDao, times(2)) - .countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( - any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong()); - verify(mMeasurementDao, never()).countDistinctEnrollmentsPerPublisherXDestinationInSource( - any(), anyInt(), any(), any(), anyLong(), anyLong()); - verify(mTriggerFetcher, never()).fetchWebTriggers(any(), anyBoolean()); - } - - @Test - public void registerWebSource_exceedsPrivacyParam_adTech() - throws RemoteException, DatastoreException { - // setup - List<SourceRegistration> sourceRegistrationsOut = - Arrays.asList(VALID_SOURCE_REGISTRATION_1, VALID_SOURCE_REGISTRATION_2); - WebSourceRegistrationRequestInternal registrationRequest = - createWebSourceRegistrationRequest(APP_DESTINATION, WEB_DESTINATION, null); - doReturn(Optional.of(sourceRegistrationsOut)) - .when(mSourceFetcher) - .fetchWebSources(any(), anyBoolean()); - ArgumentCaptor<ThrowingCheckedConsumer> insertionLogicExecutorCaptor = - ArgumentCaptor.forClass(ThrowingCheckedConsumer.class); - - when(mMeasurementDao.countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( - any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong())) - .thenReturn(Integer.valueOf(0)); - when(mMeasurementDao.countDistinctEnrollmentsPerPublisherXDestinationInSource( - any(), anyInt(), any(), any(), anyLong(), anyLong())) - .thenReturn(Integer.valueOf(100)) - .thenReturn(Integer.valueOf(0)); - DatastoreManager datastoreManager = spy(new FakeDatastoreManager()); - - // Test - MeasurementImpl measurement = - spy( - new MeasurementImpl( - null, - mContentResolver, - datastoreManager, - mSourceFetcher, - mTriggerFetcher, - mClickVerifier)); - - long eventTime = System.currentTimeMillis(); - // Disable Impression Noise - doReturn(Collections.emptyList()).when(measurement).generateFakeEventReports(any()); - final int result = measurement.registerWebSource(registrationRequest, eventTime); - - // Assert - assertEquals(STATUS_SUCCESS, result); - verify(mMockContentProviderClient, never()).insert(any(), any()); - verify(mSourceFetcher, times(1)).fetchWebSources(any(), anyBoolean()); - verify(datastoreManager, times(1)) - .runInTransaction(insertionLogicExecutorCaptor.capture()); - verify(mMeasurementDao, times(4)) - .countDistinctDestinationsPerPublisherXEnrollmentInActiveSource( - any(), anyInt(), any(), any(), anyInt(), anyLong(), anyLong()); - verify(mMeasurementDao, times(3)).countDistinctEnrollmentsPerPublisherXDestinationInSource( - any(), anyInt(), any(), any(), anyLong(), anyLong()); - verify(mTriggerFetcher, never()).fetchWebTriggers(any(), anyBoolean()); - - List<ThrowingCheckedConsumer> insertionLogicExecutor = - insertionLogicExecutorCaptor.getAllValues(); - assertEquals(1, insertionLogicExecutor.size()); - - // First registration was removed for exceeding the privacy bound. - verifyInsertSource( - registrationRequest, - VALID_SOURCE_REGISTRATION_2, - eventTime, - VALID_SOURCE_REGISTRATION_1.getAppDestination(), - VALID_SOURCE_REGISTRATION_1.getWebDestination()); - } - - @Test - public void registerWebSource_webDestinationIsValidWhenNull() { - when(mSourceFetcher.fetchSource(any())).thenReturn(Optional.empty()); - - final int result = - mMeasurementImpl.registerWebSource( - createWebSourceRegistrationRequest(APP_DESTINATION, null, null), - System.currentTimeMillis()); - // STATUS_IO_ERROR is expected when fetchSource returns Optional.empty(); - // it means validation passed and the procedure called the fetcher. - assertEquals(STATUS_IO_ERROR, result); - verify(mSourceFetcher, times(1)).fetchWebSources(any(), anyBoolean()); - } - - @Test - public void registerWebSource_verifiedDestination_exactWebDestinationMatch() { - when(mSourceFetcher.fetchSource(any())).thenReturn(Optional.empty()); - - final int result = - mMeasurementImpl.registerWebSource( - createWebSourceRegistrationRequest( - APP_DESTINATION, WEB_DESTINATION, WEB_DESTINATION), - System.currentTimeMillis()); - // STATUS_IO_ERROR is expected when fetchSource returns Optional.empty(); - // it means validation passed and the procedure called the fetcher. - assertEquals(STATUS_IO_ERROR, result); - verify(mSourceFetcher, times(1)).fetchWebSources(any(), anyBoolean()); - } - - @Test - public void registerWebSource_verifiedDestination_topPrivateDomainMatch() { - when(mSourceFetcher.fetchSource(any())).thenReturn(Optional.empty()); - - final int result = - mMeasurementImpl.registerWebSource( - createWebSourceRegistrationRequest( - APP_DESTINATION, WEB_DESTINATION, WEB_DESTINATION_WITH_SUBDOMAIN), - System.currentTimeMillis()); - // STATUS_IO_ERROR is expected when fetchSource returns Optional.empty(); - // it means validation passed and the procedure called the fetcher. - assertEquals(STATUS_IO_ERROR, result); - verify(mSourceFetcher, times(1)).fetchWebSources(any(), anyBoolean()); } @Test - public void registerWebSource_verifiedDestination_webDestinationMismatch() { - when(mSourceFetcher.fetchSource(any())).thenReturn(Optional.empty()); - + public void testRegisterWebTrigger_invalidDestination() throws RemoteException { final int result = - mMeasurementImpl.registerWebSource( - createWebSourceRegistrationRequest( - APP_DESTINATION, WEB_DESTINATION, OTHER_WEB_DESTINATION), + mMeasurementImpl.registerWebTrigger( + createWebTriggerRegistrationRequest(INVALID_WEB_DESTINATION), System.currentTimeMillis()); assertEquals(STATUS_INVALID_ARGUMENT, result); - verify(mSourceFetcher, times(0)).fetchWebSources(any(), anyBoolean()); } - @Test - public void registerWebSource_verifiedDestination_appDestinationMatch() { - when(mSourceFetcher.fetchSource(any())).thenReturn(Optional.empty()); - - final int result = - mMeasurementImpl.registerWebSource( - createWebSourceRegistrationRequest( - APP_DESTINATION, WEB_DESTINATION, APP_DESTINATION), - System.currentTimeMillis()); - // STATUS_IO_ERROR is expected when fetchSource returns Optional.empty(); - // it means validation passed and the procedure called the fetcher. - assertEquals(STATUS_IO_ERROR, result); - verify(mSourceFetcher, times(1)).fetchWebSources(any(), anyBoolean()); - } - - @Test - public void registerWebSource_verifiedDestination_appDestinationMismatch() { - when(mSourceFetcher.fetchSource(any())).thenReturn(Optional.empty()); - + public void testRegisterWebSource_verifiedDestination_appDestinationMismatch() { final int result = mMeasurementImpl.registerWebSource( createWebSourceRegistrationRequest( APP_DESTINATION, WEB_DESTINATION, OTHER_APP_DESTINATION), System.currentTimeMillis()); assertEquals(STATUS_INVALID_ARGUMENT, result); - verify(mSourceFetcher, times(0)).fetchWebSources(any(), anyBoolean()); - } - - @Test - public void registerWebSource_verifiedDestination_vendingMatch() { - when(mSourceFetcher.fetchWebSources(any(), anyBoolean())).thenReturn(Optional.empty()); - Uri vendingUri = Uri.parse(VENDING_PREFIX + APP_DESTINATION.getHost()); - MeasurementImpl measurementImpl = getMeasurementImplWithMockedIntentResolution(); - final int result = measurementImpl.registerWebSource( - createWebSourceRegistrationRequest( - APP_DESTINATION, WEB_DESTINATION, vendingUri), - System.currentTimeMillis()); - // STATUS_IO_ERROR is expected when fetchSource returns Optional.empty(); - // it means validation passed and the procedure called the fetcher. - assertEquals(STATUS_IO_ERROR, result); - verify(mSourceFetcher, times(1)).fetchWebSources(any(), anyBoolean()); - } - - @Test - public void registerWebSource_verifiedDestination_vendingMismatch() { - when(mSourceFetcher.fetchWebSources(any(), anyBoolean())).thenReturn(Optional.empty()); - Uri vendingUri = Uri.parse(VENDING_PREFIX + OTHER_APP_DESTINATION.getHost()); - MeasurementImpl measurementImpl = getMeasurementImplWithMockedIntentResolution(); - final int result = measurementImpl.registerWebSource( - createWebSourceRegistrationRequest( - APP_DESTINATION, WEB_DESTINATION, vendingUri), - System.currentTimeMillis()); - assertEquals(STATUS_INVALID_ARGUMENT, result); - verify(mSourceFetcher, times(0)).fetchWebSources(any(), anyBoolean()); - } - - @Test - public void registerWebTrigger_triggerFetchSuccess() throws Exception { - // Setup - when(mTriggerFetcher.fetchWebTriggers(any(), anyBoolean())) - .thenReturn(Optional.of(Collections.singletonList(VALID_TRIGGER_REGISTRATION))); - - ArgumentCaptor<ThrowingCheckedConsumer> consumerArgumentCaptor = - ArgumentCaptor.forClass(ThrowingCheckedConsumer.class); - final long triggerTime = System.currentTimeMillis(); - - // Execution - final int result = - mMeasurementImpl.registerWebTrigger( - createWebTriggerRegistrationRequest(WEB_DESTINATION), triggerTime); - - verify(mDatastoreManager).runInTransaction(consumerArgumentCaptor.capture()); - consumerArgumentCaptor.getValue().accept(mMeasurementDao); - - // Assertions - assertEquals(STATUS_SUCCESS, result); - verify(mMockContentProviderClient).insert(any(), any()); - verify(mSourceFetcher, never()).fetchSource(any()); - verify(mTriggerFetcher, times(1)).fetchWebTriggers(any(), anyBoolean()); - verify(mMeasurementDao) - .insertTrigger( - eq( - createTrigger( - triggerTime, - DEFAULT_CONTEXT.getAttributionSource(), - WEB_DESTINATION, - EventSurfaceType.WEB))); - } - - @Test - public void registerWebTrigger_triggerFetchFailure() throws RemoteException { - when(mTriggerFetcher.fetchWebTriggers(any(), anyBoolean())).thenReturn(Optional.empty()); - - final int result = - mMeasurementImpl.registerWebTrigger( - createWebTriggerRegistrationRequest(WEB_DESTINATION), - System.currentTimeMillis()); - - assertEquals(STATUS_IO_ERROR, result); - verify(mMockContentProviderClient, never()).insert(any(), any()); - verify(mSourceFetcher, never()).fetchWebSources(any(), anyBoolean()); - verify(mTriggerFetcher, times(1)).fetchWebTriggers(any(), anyBoolean()); - } - - @Test - - public void registerWebTrigger_invalidDestination() throws RemoteException { - final int result = - mMeasurementImpl.registerWebTrigger( - createWebTriggerRegistrationRequest(INVALID_WEB_DESTINATION), - System.currentTimeMillis()); - assertEquals(STATUS_INVALID_ARGUMENT, result); - verify(mTriggerFetcher, never()).fetchWebTriggers(any(), anyBoolean()); } - - @Test - public void testRegister_registrationTypeSource_clickNotVerifiedFailure() { - // setup - List<SourceRegistration> sourceRegistrationsOut = - Arrays.asList(VALID_SOURCE_REGISTRATION_1, VALID_SOURCE_REGISTRATION_2); - doReturn(Optional.of(sourceRegistrationsOut)).when(mSourceFetcher).fetchSource(any()); - - // Disable Impression Noise - doReturn(Collections.emptyList()).when(mMeasurementImpl).generateFakeEventReports(any()); - - doReturn(false).when(mClickVerifier).isInputEventVerifiable(any(), anyLong()); - - RegistrationRequest registrationRequest = - new RegistrationRequest.Builder() - .setRegistrationUri(REGISTRATION_URI_1) - .setTopOriginUri(DEFAULT_URI) - .setPackageName(DEFAULT_CONTEXT.getAttributionSource().getPackageName()) - .setRegistrationType(RegistrationRequest.REGISTER_SOURCE) - .setInputEvent(getInputEvent()) - .build(); - - long eventTime = System.currentTimeMillis(); - final int result = mMeasurementImpl.register(registrationRequest, eventTime); - - // Assert - assertEquals(STATUS_SUCCESS, result); - verify(mSourceFetcher, times(1)).fetchSource(any()); - } - @Test public void testGetSourceType_verifiedInputEvent_returnsNavigationSourceType() { doReturn(true).when(mClickVerifier).isInputEventVerifiable(any(), anyLong()); @@ -1246,12 +300,10 @@ public final class MeasurementImplTest { Source.SourceType.NAVIGATION, mMeasurementImpl.getSourceType(getInputEvent(), 1000L)); } - @Test public void testGetSourceType_noInputEventGiven() { assertEquals(Source.SourceType.EVENT, mMeasurementImpl.getSourceType(null, 1000L)); } - @Test public void testGetSourceType_inputEventNotVerifiable_returnsEventSourceType() { doReturn(false).when(mClickVerifier).isInputEventVerifiable(any(), anyLong()); @@ -1276,11 +328,10 @@ public final class MeasurementImplTest { MeasurementImpl measurementImpl = new MeasurementImpl( DEFAULT_CONTEXT, - mContentResolver, mDatastoreManager, - mSourceFetcher, - mTriggerFetcher, - mockClickVerifier); + mockClickVerifier, + mMeasurementDataDeleter, + mEnrollmentDao); // Because click verification is disabled, the SourceType is NAVIGATION even if the // input event is not verifiable. @@ -1293,381 +344,4 @@ public final class MeasurementImplTest { session.finishMocking(); } } - - @Test - public void insertSource_withFakeReportsFalseAppAttribution_accountsForFakeReportAttribution() - throws DatastoreException { - // Setup - int fakeReportsCount = 2; - Source source = - spy( - SourceFixture.getValidSourceBuilder() - .setAppDestination( - SourceFixture.ValidSourceParams.ATTRIBUTION_DESTINATION) - .setWebDestination(null) - .build()); - List<Source.FakeReport> fakeReports = - createFakeReports( - source, - fakeReportsCount, - SourceFixture.ValidSourceParams.ATTRIBUTION_DESTINATION); - MeasurementImpl measurementImpl = - new MeasurementImpl( - null, - mContentResolver, - mDatastoreManager, - mSourceFetcher, - mTriggerFetcher, - mClickVerifier); - Answer<?> falseAttributionAnswer = - (arg) -> { - source.setAttributionMode(Source.AttributionMode.FALSELY); - return fakeReports; - }; - doAnswer(falseAttributionAnswer).when(source).assignAttributionModeAndGenerateFakeReports(); - ArgumentCaptor<ThrowingCheckedConsumer> consumerArgCaptor = - ArgumentCaptor.forClass(ThrowingCheckedConsumer.class); - ArgumentCaptor<Attribution> attributionRateLimitArgCaptor = - ArgumentCaptor.forClass(Attribution.class); - - // Execution - measurementImpl.insertSource(source); - - // Assertion - verify(mDatastoreManager).runInTransaction(consumerArgCaptor.capture()); - - consumerArgCaptor.getValue().accept(mMeasurementDao); - - verify(mMeasurementDao).insertSource(source); - verify(mMeasurementDao, times(2)).insertEventReport(any()); - verify(mMeasurementDao).insertAttribution(attributionRateLimitArgCaptor.capture()); - - assertEquals( - new Attribution.Builder() - .setDestinationOrigin(source.getAppDestination().toString()) - .setDestinationSite(source.getAppDestination().toString()) - .setEnrollmentId(source.getEnrollmentId()) - .setSourceOrigin(source.getPublisher().toString()) - .setSourceSite(source.getPublisher().toString()) - .setRegistrant(source.getRegistrant().toString()) - .setTriggerTime(source.getEventTime()) - .build(), - attributionRateLimitArgCaptor.getValue()); - } - - @Test - public void insertSource_withFakeReportsFalseWebAttribution_accountsForFakeReportAttribution() - throws DatastoreException { - // Setup - int fakeReportsCount = 2; - Source source = - spy( - SourceFixture.getValidSourceBuilder() - .setAppDestination(null) - .setWebDestination(SourceFixture.ValidSourceParams.WEB_DESTINATION) - .build()); - List<Source.FakeReport> fakeReports = - createFakeReports( - source, fakeReportsCount, SourceFixture.ValidSourceParams.WEB_DESTINATION); - MeasurementImpl measurementImpl = - new MeasurementImpl( - null, - mContentResolver, - mDatastoreManager, - mSourceFetcher, - mTriggerFetcher, - mClickVerifier); - Answer<?> falseAttributionAnswer = - (arg) -> { - source.setAttributionMode(Source.AttributionMode.FALSELY); - return fakeReports; - }; - doAnswer(falseAttributionAnswer).when(source).assignAttributionModeAndGenerateFakeReports(); - ArgumentCaptor<ThrowingCheckedConsumer> consumerArgCaptor = - ArgumentCaptor.forClass(ThrowingCheckedConsumer.class); - ArgumentCaptor<Attribution> attributionRateLimitArgCaptor = - ArgumentCaptor.forClass(Attribution.class); - - // Execution - measurementImpl.insertSource(source); - - // Assertion - verify(mDatastoreManager).runInTransaction(consumerArgCaptor.capture()); - - consumerArgCaptor.getValue().accept(mMeasurementDao); - - verify(mMeasurementDao).insertSource(source); - verify(mMeasurementDao, times(2)).insertEventReport(any()); - verify(mMeasurementDao).insertAttribution(attributionRateLimitArgCaptor.capture()); - - assertEquals( - new Attribution.Builder() - .setDestinationOrigin(source.getWebDestination().toString()) - .setDestinationSite(source.getWebDestination().toString()) - .setEnrollmentId(source.getEnrollmentId()) - .setSourceOrigin(source.getPublisher().toString()) - .setSourceSite(source.getPublisher().toString()) - .setRegistrant(source.getRegistrant().toString()) - .setTriggerTime(source.getEventTime()) - .build(), - attributionRateLimitArgCaptor.getValue()); - } - - @Test - public void insertSource_withFalseAppAndWebAttribution_accountsForFakeReportAttribution() - throws DatastoreException { - // Setup - int fakeReportsCount = 2; - Source source = - spy( - SourceFixture.getValidSourceBuilder() - .setAppDestination( - SourceFixture.ValidSourceParams.ATTRIBUTION_DESTINATION) - .setWebDestination(SourceFixture.ValidSourceParams.WEB_DESTINATION) - .build()); - List<Source.FakeReport> fakeReports = - createFakeReports( - source, - fakeReportsCount, - SourceFixture.ValidSourceParams.ATTRIBUTION_DESTINATION); - MeasurementImpl measurementImpl = - new MeasurementImpl( - null, - mContentResolver, - mDatastoreManager, - mSourceFetcher, - mTriggerFetcher, - mClickVerifier); - Answer<?> falseAttributionAnswer = - (arg) -> { - source.setAttributionMode(Source.AttributionMode.FALSELY); - return fakeReports; - }; - doAnswer(falseAttributionAnswer).when(source).assignAttributionModeAndGenerateFakeReports(); - ArgumentCaptor<ThrowingCheckedConsumer> consumerArgCaptor = - ArgumentCaptor.forClass(ThrowingCheckedConsumer.class); - ArgumentCaptor<Attribution> attributionRateLimitArgCaptor = - ArgumentCaptor.forClass(Attribution.class); - - // Execution - measurementImpl.insertSource(source); - - // Assertion - verify(mDatastoreManager).runInTransaction(consumerArgCaptor.capture()); - - consumerArgCaptor.getValue().accept(mMeasurementDao); - - verify(mMeasurementDao).insertSource(source); - verify(mMeasurementDao, times(2)).insertEventReport(any()); - verify(mMeasurementDao, times(2)) - .insertAttribution(attributionRateLimitArgCaptor.capture()); - - assertEquals( - new Attribution.Builder() - .setDestinationOrigin(source.getAppDestination().toString()) - .setDestinationSite(source.getAppDestination().toString()) - .setEnrollmentId(source.getEnrollmentId()) - .setSourceOrigin(source.getPublisher().toString()) - .setSourceSite(source.getPublisher().toString()) - .setRegistrant(source.getRegistrant().toString()) - .setTriggerTime(source.getEventTime()) - .build(), - attributionRateLimitArgCaptor.getAllValues().get(0)); - - assertEquals( - new Attribution.Builder() - .setDestinationOrigin(source.getWebDestination().toString()) - .setDestinationSite(source.getWebDestination().toString()) - .setEnrollmentId(source.getEnrollmentId()) - .setSourceOrigin(source.getPublisher().toString()) - .setSourceSite(source.getPublisher().toString()) - .setRegistrant(source.getRegistrant().toString()) - .setTriggerTime(source.getEventTime()) - .build(), - attributionRateLimitArgCaptor.getAllValues().get(1)); - } - - @Test - public void insertSource_withFakeReportsNeverAppAttribution_accountsForFakeReportAttribution() - throws DatastoreException { - // Setup - Source source = - spy( - SourceFixture.getValidSourceBuilder() - .setAppDestination( - SourceFixture.ValidSourceParams.ATTRIBUTION_DESTINATION) - .setWebDestination(null) - .build()); - List<Source.FakeReport> fakeReports = Collections.emptyList(); - MeasurementImpl measurementImpl = - new MeasurementImpl( - null, - mContentResolver, - mDatastoreManager, - mSourceFetcher, - mTriggerFetcher, - mClickVerifier); - Answer<?> neverAttributionAnswer = - (arg) -> { - source.setAttributionMode(Source.AttributionMode.NEVER); - return fakeReports; - }; - doAnswer(neverAttributionAnswer).when(source).assignAttributionModeAndGenerateFakeReports(); - ArgumentCaptor<ThrowingCheckedConsumer> consumerArgCaptor = - ArgumentCaptor.forClass(ThrowingCheckedConsumer.class); - ArgumentCaptor<Attribution> attributionRateLimitArgCaptor = - ArgumentCaptor.forClass(Attribution.class); - - // Execution - measurementImpl.insertSource(source); - - // Assertion - verify(mDatastoreManager).runInTransaction(consumerArgCaptor.capture()); - - consumerArgCaptor.getValue().accept(mMeasurementDao); - - verify(mMeasurementDao).insertSource(source); - verify(mMeasurementDao, never()).insertEventReport(any()); - verify(mMeasurementDao).insertAttribution(attributionRateLimitArgCaptor.capture()); - - assertEquals( - new Attribution.Builder() - .setDestinationOrigin(source.getAppDestination().toString()) - .setDestinationSite(source.getAppDestination().toString()) - .setEnrollmentId(source.getEnrollmentId()) - .setSourceOrigin(source.getPublisher().toString()) - .setSourceSite(source.getPublisher().toString()) - .setRegistrant(source.getRegistrant().toString()) - .setTriggerTime(source.getEventTime()) - .build(), - attributionRateLimitArgCaptor.getValue()); - } - - private List<Source.FakeReport> createFakeReports(Source source, int count, Uri destination) { - return IntStream.range(0, count) - .mapToObj( - x -> - new Source.FakeReport( - 0, source.getReportingTimeForNoising(0), destination)) - .collect(Collectors.toList()); - } - - private void verifyInsertSource( - RegistrationRequest registrationRequest, - SourceRegistration sourceRegistration, - long eventTime, - Uri firstSourceDestination, - Uri firstSourceWebDestination) - throws DatastoreException { - Source source = - createSource( - sourceRegistration, - eventTime, - firstSourceDestination, - firstSourceWebDestination, - registrationRequest.getTopOriginUri(), - EventSurfaceType.APP, - registrationRequest.getPackageName()); - verify(mMeasurementDao).insertSource(source); - } - - private void verifyInsertSource( - WebSourceRegistrationRequestInternal registrationRequest, - SourceRegistration sourceRegistration, - long eventTime, - Uri firstSourceDestination, - Uri firstSourceWebDestination) - throws DatastoreException { - Source source = - createSource( - sourceRegistration, - eventTime, - firstSourceDestination, - firstSourceWebDestination, - registrationRequest.getSourceRegistrationRequest().getTopOriginUri(), - EventSurfaceType.WEB, - registrationRequest.getPackageName()); - verify(mMeasurementDao).insertSource(source); - } - - private Source createSource( - SourceRegistration sourceRegistration, - long eventTime, - Uri firstSourceDestination, - Uri firstSourceWebDestination, - Uri topOrigin, - @EventSurfaceType int publisherType, - String packageName) { - return SourceFixture.getValidSourceBuilder() - .setEventId(sourceRegistration.getSourceEventId()) - .setPublisher(topOrigin) - .setPublisherType(publisherType) - .setAppDestination(firstSourceDestination) - .setWebDestination(firstSourceWebDestination) - .setEnrollmentId(DEFAULT_ENROLLMENT) - .setRegistrant(Uri.parse("android-app://" + packageName)) - .setEventTime(eventTime) - .setExpiryTime( - eventTime + TimeUnit.SECONDS.toMillis(sourceRegistration.getExpiry())) - .setPriority(sourceRegistration.getSourcePriority()) - .setSourceType(Source.SourceType.EVENT) - .setInstallAttributionWindow( - TimeUnit.SECONDS.toMillis(sourceRegistration.getInstallAttributionWindow())) - .setInstallCooldownWindow( - TimeUnit.SECONDS.toMillis(sourceRegistration.getInstallCooldownWindow())) - .setAttributionMode(Source.AttributionMode.TRUTHFULLY) - .setAggregateSource(sourceRegistration.getAggregateSource()) - .setAggregateFilterData(sourceRegistration.getAggregateFilterData()) - .setDebugKey(sourceRegistration.getDebugKey()) - .build(); - } - - private Trigger createTrigger(long triggerTime, AttributionSource attributionSource, - Uri destination, @EventSurfaceType int destinationType) { - return TriggerFixture.getValidTriggerBuilder() - .setAttributionDestination(destination) - .setDestinationType(destinationType) - .setEnrollmentId(DEFAULT_ENROLLMENT) - .setRegistrant(Uri.parse(ANDROID_APP_SCHEME + attributionSource.getPackageName())) - .setTriggerTime(triggerTime) - .setEventTriggers(EVENT_TRIGGERS) - .setAggregateTriggerData( - MeasurementImplTest.VALID_TRIGGER_REGISTRATION.getAggregateTriggerData()) - .setAggregateValues( - MeasurementImplTest.VALID_TRIGGER_REGISTRATION.getAggregateValues()) - .setFilters(MeasurementImplTest.VALID_TRIGGER_REGISTRATION.getFilters()) - .setDebugKey(MeasurementImplTest.VALID_TRIGGER_REGISTRATION.getDebugKey()) - .build(); - } - - private MeasurementImpl getMeasurementImplWithMockedIntentResolution() { - ApplicationInfo applicationInfo = new ApplicationInfo(); - applicationInfo.packageName = AppVendorPackages.PLAY_STORE; - ActivityInfo activityInfo = new ActivityInfo(); - activityInfo.applicationInfo = applicationInfo; - activityInfo.name = "non null String"; - ResolveInfo resolveInfo = new ResolveInfo(); - resolveInfo.activityInfo = activityInfo; - MockPackageManager mockPackageManager = - new MockPackageManager() { - @Override - public ResolveInfo resolveActivity(Intent intent, int flags) { - return resolveInfo; - } - }; - MockContext mockContext = - new MockContext() { - @Override - public PackageManager getPackageManager() { - return mockPackageManager; - } - }; - return new MeasurementImpl( - mockContext, - mContentResolver, - mDatastoreManager, - mSourceFetcher, - mTriggerFetcher, - mClickVerifier); - } } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/MeasurementServiceImplTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/MeasurementServiceImplTest.java index 4811f2a5b7..4cd9613a30 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/MeasurementServiceImplTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/MeasurementServiceImplTest.java @@ -156,8 +156,7 @@ public final class MeasurementServiceImplTest { when(mMockMeasurementImpl.registerWebSource( any(WebSourceRegistrationRequestInternal.class), anyLong())) .thenReturn(STATUS_SUCCESS); - when(mConsentManager.getConsent(any(PackageManager.class))) - .thenReturn(AdServicesApiConsent.GIVEN); + when(mConsentManager.getConsent()).thenReturn(AdServicesApiConsent.GIVEN); when(mMockFlags.getWebContextClientAppAllowList()).thenReturn("*"); when(mMockThrottler.tryAcquire(any(), any())).thenReturn(true).thenReturn(false); when(mMockFlags.getWebContextClientAppAllowList()).thenReturn(ALLOW_ALL_PACKAGES); @@ -1206,12 +1205,10 @@ public final class MeasurementServiceImplTest { try { ExtendedMockito.doReturn(1).when(Binder::getCallingUidOrThrow); - ExtendedMockito.doReturn(AdServicesApiConsent.GIVEN) - .when(mConsentManager) - .getConsent(any()); + ExtendedMockito.doReturn(AdServicesApiConsent.GIVEN).when(mConsentManager).getConsent(); ExtendedMockito.doReturn(mConsentManager).when(() -> ConsentManager.getInstance(any())); MeasurementImpl measurementImpl = - new MeasurementImpl(mMockContext, null, null, null, null, null); + new MeasurementImpl(mMockContext, null, null, null, null); when(mMockFlags.getPpapiAppAllowList()).thenReturn("*"); CountDownLatch countDownLatch = new CountDownLatch(1); final AtomicInteger resultWrapper = new AtomicInteger(); @@ -1259,9 +1256,7 @@ public final class MeasurementServiceImplTest { try { ExtendedMockito.doReturn(1).when(Binder::getCallingUidOrThrow); - ExtendedMockito.doReturn(AdServicesApiConsent.GIVEN) - .when(mConsentManager) - .getConsent(any()); + ExtendedMockito.doReturn(AdServicesApiConsent.GIVEN).when(mConsentManager).getConsent(); doNothing() .when(mMockAppImportanceFilter) @@ -1269,7 +1264,7 @@ public final class MeasurementServiceImplTest { ExtendedMockito.doReturn(mConsentManager).when(() -> ConsentManager.getInstance(any())); MeasurementImpl measurementImpl = - new MeasurementImpl(mMockContext, null, null, null, null, null); + new MeasurementImpl(mMockContext, null, null, null, null); when(mMockFlags.getPpapiAppAllowList()).thenReturn("*"); CountDownLatch countDownLatch = new CountDownLatch(1); final AtomicInteger resultWrapper = new AtomicInteger(); @@ -1317,9 +1312,7 @@ public final class MeasurementServiceImplTest { try { ExtendedMockito.doReturn(1).when(Binder::getCallingUidOrThrow); - ExtendedMockito.doReturn(AdServicesApiConsent.GIVEN) - .when(mConsentManager) - .getConsent(any()); + ExtendedMockito.doReturn(AdServicesApiConsent.GIVEN).when(mConsentManager).getConsent(); doThrow(new AppImportanceFilter.WrongCallingApplicationStateException()) .when(mMockAppImportanceFilter) @@ -2137,8 +2130,7 @@ public final class MeasurementServiceImplTest { public void testRegister_userRevokedConsent() { try { mockAccessControl(true, true, true); - when(mConsentManager.getConsent(any(PackageManager.class))) - .thenReturn(AdServicesApiConsent.REVOKED); + when(mConsentManager.getConsent()).thenReturn(AdServicesApiConsent.REVOKED); mMeasurementServiceImpl.register( getDefaultRegistrationSourceRequest(), mCallerMetadata, @@ -2163,9 +2155,7 @@ public final class MeasurementServiceImplTest { public void testRegisterWebSource_userRevokedConsent() { try { mockAccessControl(true, true, true); - doReturn(AdServicesApiConsent.REVOKED) - .when(mConsentManager) - .getConsent(mPackageManager); + doReturn(AdServicesApiConsent.REVOKED).when(mConsentManager).getConsent(); mMeasurementServiceImpl.registerWebSource( createWebSourceRegistrationRequest(), @@ -2191,7 +2181,7 @@ public final class MeasurementServiceImplTest { public void testRegisterWebSource_packageNotAllowListed() throws InterruptedException { try { mockAccessControl(true, true, true); - doReturn(AdServicesApiConsent.GIVEN).when(mConsentManager).getConsent(mPackageManager); + doReturn(AdServicesApiConsent.GIVEN).when(mConsentManager).getConsent(); doReturn(ALLOW_LIST_WITHOUT_TEST_PACKAGE) .when(mMockFlags) .getWebContextClientAppAllowList(); @@ -2226,9 +2216,7 @@ public final class MeasurementServiceImplTest { public void testRegisterWebTrigger_userRevokedConsent() { try { mockAccessControl(true, true, true); - doReturn(AdServicesApiConsent.REVOKED) - .when(mConsentManager) - .getConsent(mPackageManager); + doReturn(AdServicesApiConsent.REVOKED).when(mConsentManager).getConsent(); mMeasurementServiceImpl.registerWebTrigger( createWebTriggerRegistrationRequest(), @@ -2256,7 +2244,7 @@ public final class MeasurementServiceImplTest { try { // Setup mockAccessControl(true, true, true); - doReturn(AdServicesApiConsent.GIVEN).when(mConsentManager).getConsent(mPackageManager); + doReturn(AdServicesApiConsent.GIVEN).when(mConsentManager).getConsent(); doReturn(ALLOW_LIST_WITHOUT_TEST_PACKAGE) .when(mMockFlags) .getWebContextClientAppAllowList(); @@ -2543,7 +2531,6 @@ public final class MeasurementServiceImplTest { return new RegistrationRequest.Builder() .setPackageName(PACKAGE_NAME) .setRegistrationUri(Uri.parse("https://registration-uri.com")) - .setTopOriginUri(Uri.parse("android-app://com.example")) .setRegistrationType(RegistrationRequest.REGISTER_SOURCE) .build(); } @@ -2552,7 +2539,6 @@ public final class MeasurementServiceImplTest { return new RegistrationRequest.Builder() .setPackageName(PACKAGE_NAME) .setRegistrationUri(Uri.parse("https://registration-uri.com")) - .setTopOriginUri(Uri.parse("android-app://com.example")) .setRegistrationType(RegistrationRequest.REGISTER_TRIGGER) .build(); } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/SourceTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/SourceTest.java index 6bc9730a08..60f80cd8e8 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/SourceTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/SourceTest.java @@ -29,9 +29,9 @@ import android.net.Uri; import androidx.annotation.Nullable; import com.android.adservices.service.measurement.aggregation.AggregatableAttributionSource; -import com.android.adservices.service.measurement.aggregation.AggregateFilterData; import com.android.adservices.service.measurement.noising.ImpressionNoiseParams; import com.android.adservices.service.measurement.noising.ImpressionNoiseUtil; +import com.android.adservices.service.measurement.util.UnsignedLong; import org.json.JSONArray; import org.json.JSONException; @@ -51,8 +51,8 @@ import java.util.stream.LongStream; public class SourceTest { private static final double ZERO_DELTA = 0D; - private static final Long DEBUG_KEY_1 = 81786463L; - private static final Long DEBUG_KEY_2 = 23487834L; + private static final UnsignedLong DEBUG_KEY_1 = new UnsignedLong(81786463L); + private static final UnsignedLong DEBUG_KEY_2 = new UnsignedLong(23487834L); @Test public void testDefaults() { @@ -77,10 +77,10 @@ public class SourceTest { aggregateSource.put(jsonObject1); aggregateSource.put(jsonObject2); - JSONObject aggregateFilterData = new JSONObject(); - aggregateFilterData.put( + JSONObject filterData = new JSONObject(); + filterData.put( "conversion_subdomain", Collections.singletonList("electronics.megastore")); - aggregateFilterData.put("product", Arrays.asList("1234", "2345")); + filterData.put("product", Arrays.asList("1234", "2345")); assertEquals( new Source.Builder() .setEnrollmentId("enrollment-id") @@ -89,15 +89,18 @@ public class SourceTest { .setPublisher(Uri.parse("https://example.com/aS")) .setPublisherType(EventSurfaceType.WEB) .setId("1") - .setEventId(2L) + .setEventId(new UnsignedLong(2L)) .setPriority(3L) .setEventTime(5L) .setExpiryTime(5L) - .setDedupKeys(LongStream.range(0, 2).boxed().collect(Collectors.toList())) + .setDedupKeys(LongStream.range(0, 2) + .boxed() + .map(UnsignedLong::new) + .collect(Collectors.toList())) .setStatus(Source.Status.ACTIVE) .setSourceType(Source.SourceType.EVENT) .setRegistrant(Uri.parse("android-app://com.example.abc")) - .setAggregateFilterData(aggregateFilterData.toString()) + .setFilterData(filterData.toString()) .setAggregateSource(aggregateSource.toString()) .setAggregateContributions(50001) .setDebugKey(DEBUG_KEY_1) @@ -111,15 +114,18 @@ public class SourceTest { .setPublisher(Uri.parse("https://example.com/aS")) .setPublisherType(EventSurfaceType.WEB) .setId("1") - .setEventId(2L) + .setEventId(new UnsignedLong(2L)) .setPriority(3L) .setEventTime(5L) .setExpiryTime(5L) - .setDedupKeys(LongStream.range(0, 2).boxed().collect(Collectors.toList())) + .setDedupKeys(LongStream.range(0, 2) + .boxed() + .map(UnsignedLong::new) + .collect(Collectors.toList())) .setStatus(Source.Status.ACTIVE) .setSourceType(Source.SourceType.EVENT) .setRegistrant(Uri.parse("android-app://com.example.abc")) - .setAggregateFilterData(aggregateFilterData.toString()) + .setFilterData(filterData.toString()) .setAggregateSource(aggregateSource.toString()) .setAggregateContributions(50001) .setDebugKey(DEBUG_KEY_1) @@ -134,8 +140,8 @@ public class SourceTest { SourceFixture.getValidSourceBuilder().setId("1").build(), SourceFixture.getValidSourceBuilder().setId("2").build()); assertNotEquals( - SourceFixture.getValidSourceBuilder().setEventId(1).build(), - SourceFixture.getValidSourceBuilder().setEventId(2).build()); + SourceFixture.getValidSourceBuilder().setEventId(new UnsignedLong(1L)).build(), + SourceFixture.getValidSourceBuilder().setEventId(new UnsignedLong(2L)).build()); assertNotEquals( SourceFixture.getValidSourceBuilder() .setAppDestination(Uri.parse("android-app://1.com")) @@ -188,11 +194,11 @@ public class SourceTest { .setStatus(Source.Status.IGNORED).build()); assertNotEquals( SourceFixture.getValidSourceBuilder() - .setDedupKeys(LongStream.range(0, 2).boxed() + .setDedupKeys(LongStream.range(0, 2).boxed().map(UnsignedLong::new) .collect(Collectors.toList())) .build(), SourceFixture.getValidSourceBuilder() - .setDedupKeys(LongStream.range(1, 3).boxed() + .setDedupKeys(LongStream.range(1, 3).boxed().map(UnsignedLong::new) .collect(Collectors.toList())) .build()); assertNotEquals( @@ -255,7 +261,7 @@ public class SourceTest { SourceFixture.ValidSourceParams.DEBUG_KEY, SourceFixture.ValidSourceParams.ATTRIBUTION_MODE, SourceFixture.ValidSourceParams.buildAggregateSource(), - SourceFixture.ValidSourceParams.buildAggregateFilterData()); + SourceFixture.ValidSourceParams.buildFilterData()); assertInvalidSourceArguments( SourceFixture.ValidSourceParams.SOURCE_EVENT_ID, @@ -273,7 +279,7 @@ public class SourceTest { SourceFixture.ValidSourceParams.DEBUG_KEY, SourceFixture.ValidSourceParams.ATTRIBUTION_MODE, SourceFixture.ValidSourceParams.buildAggregateSource(), - SourceFixture.ValidSourceParams.buildAggregateFilterData()); + SourceFixture.ValidSourceParams.buildFilterData()); } @Test @@ -294,7 +300,7 @@ public class SourceTest { SourceFixture.ValidSourceParams.DEBUG_KEY, SourceFixture.ValidSourceParams.ATTRIBUTION_MODE, SourceFixture.ValidSourceParams.buildAggregateSource(), - SourceFixture.ValidSourceParams.buildAggregateFilterData()); + SourceFixture.ValidSourceParams.buildFilterData()); assertInvalidSourceArguments( SourceFixture.ValidSourceParams.SOURCE_EVENT_ID, @@ -312,7 +318,7 @@ public class SourceTest { SourceFixture.ValidSourceParams.DEBUG_KEY, SourceFixture.ValidSourceParams.ATTRIBUTION_MODE, SourceFixture.ValidSourceParams.buildAggregateSource(), - SourceFixture.ValidSourceParams.buildAggregateFilterData()); + SourceFixture.ValidSourceParams.buildFilterData()); assertInvalidSourceArguments( SourceFixture.ValidSourceParams.SOURCE_EVENT_ID, @@ -330,7 +336,7 @@ public class SourceTest { SourceFixture.ValidSourceParams.DEBUG_KEY, SourceFixture.ValidSourceParams.ATTRIBUTION_MODE, SourceFixture.ValidSourceParams.buildAggregateSource(), - SourceFixture.ValidSourceParams.buildAggregateFilterData()); + SourceFixture.ValidSourceParams.buildFilterData()); } @Test @@ -351,7 +357,7 @@ public class SourceTest { SourceFixture.ValidSourceParams.DEBUG_KEY, SourceFixture.ValidSourceParams.ATTRIBUTION_MODE, SourceFixture.ValidSourceParams.buildAggregateSource(), - SourceFixture.ValidSourceParams.buildAggregateFilterData()); + SourceFixture.ValidSourceParams.buildFilterData()); } @Test @@ -372,7 +378,7 @@ public class SourceTest { SourceFixture.ValidSourceParams.DEBUG_KEY, SourceFixture.ValidSourceParams.ATTRIBUTION_MODE, SourceFixture.ValidSourceParams.buildAggregateSource(), - SourceFixture.ValidSourceParams.buildAggregateFilterData()); + SourceFixture.ValidSourceParams.buildFilterData()); assertInvalidSourceArguments( SourceFixture.ValidSourceParams.SOURCE_EVENT_ID, @@ -390,7 +396,7 @@ public class SourceTest { SourceFixture.ValidSourceParams.DEBUG_KEY, SourceFixture.ValidSourceParams.ATTRIBUTION_MODE, SourceFixture.ValidSourceParams.buildAggregateSource(), - SourceFixture.ValidSourceParams.buildAggregateFilterData()); + SourceFixture.ValidSourceParams.buildFilterData()); } @Test @@ -411,7 +417,7 @@ public class SourceTest { SourceFixture.ValidSourceParams.DEBUG_KEY, SourceFixture.ValidSourceParams.ATTRIBUTION_MODE, SourceFixture.ValidSourceParams.buildAggregateSource(), - SourceFixture.ValidSourceParams.buildAggregateFilterData()); + SourceFixture.ValidSourceParams.buildFilterData()); } @Test @@ -591,8 +597,8 @@ public class SourceTest { final AggregatableAttributionSource attributionSource = new AggregatableAttributionSource.Builder() .setAggregatableSource(aggregatableSource) - .setAggregateFilterData( - new AggregateFilterData.Builder() + .setFilterData( + new FilterData.Builder() .setAttributionFilterMap(filterMap) .build()) .build(); @@ -604,14 +610,14 @@ public class SourceTest { assertNotNull(source.getAggregatableAttributionSource()); assertNotNull(source.getAggregatableAttributionSource().getAggregatableSource()); - assertNotNull(source.getAggregatableAttributionSource().getAggregateFilterData()); + assertNotNull(source.getAggregatableAttributionSource().getFilterData()); assertEquals( aggregatableSource, source.getAggregatableAttributionSource().getAggregatableSource()); assertEquals( filterMap, source.getAggregatableAttributionSource() - .getAggregateFilterData() + .getFilterData() .getAttributionFilterMap()); } @@ -627,7 +633,7 @@ public class SourceTest { .setSourceType(Source.SourceType.NAVIGATION) .build(); assertEquals( - PrivacyParams.NAVIGATION_TRIGGER_DATA_CARDINALITY, + PrivacyParams.getNavigationTriggerDataCardinality(), navigationSource.getTriggerDataCardinality()); } @@ -792,7 +798,7 @@ public class SourceTest { .setWebDestination(null) .setExpiryTime(expiry) .build()), - PrivacyParams.NAVIGATION_TRIGGER_DATA_CARDINALITY); + PrivacyParams.getNavigationTriggerDataCardinality()); // Single (Web) destination, EVENT type verifyAlgorithmicFakeReportGeneration( @@ -814,7 +820,7 @@ public class SourceTest { .setAppDestination(null) .setWebDestination(SourceFixture.ValidSourceParams.WEB_DESTINATION) .build()), - PrivacyParams.NAVIGATION_TRIGGER_DATA_CARDINALITY); + PrivacyParams.getNavigationTriggerDataCardinality()); // Both destinations set, EVENT type verifyAlgorithmicFakeReportGeneration( @@ -838,7 +844,7 @@ public class SourceTest { SourceFixture.ValidSourceParams.ATTRIBUTION_DESTINATION) .setWebDestination(SourceFixture.ValidSourceParams.WEB_DESTINATION) .build()), - PrivacyParams.NAVIGATION_TRIGGER_DATA_CARDINALITY); + PrivacyParams.getNavigationTriggerDataCardinality()); // App destination with cooldown window verifyAlgorithmicFakeReportGeneration( @@ -1258,16 +1264,52 @@ public class SourceTest { } @Test + public void testParseFilterData_nonEmpty() throws JSONException { + JSONObject filterDataJson = new JSONObject(); + filterDataJson.put("conversion", new JSONArray(Collections.singletonList("electronics"))); + filterDataJson.put("product", new JSONArray(Arrays.asList("1234", "2345"))); + Source source = SourceFixture.getValidSourceBuilder() + .setSourceType(Source.SourceType.NAVIGATION) + .setFilterData(filterDataJson.toString()) + .build(); + FilterData filterData = source.parseFilterData(); + assertEquals(filterData.getAttributionFilterMap().size(), 3); + assertEquals(Collections.singletonList("electronics"), + filterData.getAttributionFilterMap().get("conversion")); + assertEquals(Arrays.asList("1234", "2345"), + filterData.getAttributionFilterMap().get("product")); + assertEquals(Collections.singletonList("navigation"), + filterData.getAttributionFilterMap().get("source_type")); + } + + @Test + public void testParseFilterData_nullFilterData() throws JSONException { + Source source = SourceFixture.getValidSourceBuilder() + .setSourceType(Source.SourceType.EVENT) + .build(); + FilterData filterData = source.parseFilterData(); + assertEquals(filterData.getAttributionFilterMap().size(), 1); + assertEquals(Collections.singletonList("event"), + filterData.getAttributionFilterMap().get("source_type")); + } + + @Test + public void testParseFilterData_emptyFilterData() throws JSONException { + Source source = SourceFixture.getValidSourceBuilder() + .setSourceType(Source.SourceType.EVENT) + .setFilterData("") + .build(); + FilterData filterData = source.parseFilterData(); + assertEquals(filterData.getAttributionFilterMap().size(), 1); + assertEquals(Collections.singletonList("event"), + filterData.getAttributionFilterMap().get("source_type")); + } + + @Test public void testParseAggregateSource() throws JSONException { - JSONArray aggregatableSource = new JSONArray(); - JSONObject jsonObject1 = new JSONObject(); - jsonObject1.put("id", "campaignCounts"); - jsonObject1.put("key_piece", "0x159"); - JSONObject jsonObject2 = new JSONObject(); - jsonObject2.put("id", "geoValue"); - jsonObject2.put("key_piece", "0x5"); - aggregatableSource.put(jsonObject1); - aggregatableSource.put(jsonObject2); + JSONObject aggregatableSource = new JSONObject(); + aggregatableSource.put("campaignCounts", "0x159"); + aggregatableSource.put("geoValue", "0x5"); JSONObject filterData = new JSONObject(); filterData.put("conversion_subdomain", @@ -1275,8 +1317,9 @@ public class SourceTest { filterData.put("product", new JSONArray(Arrays.asList("1234", "2345"))); Source source = SourceFixture.getValidSourceBuilder() + .setSourceType(Source.SourceType.NAVIGATION) .setAggregateSource(aggregatableSource.toString()) - .setAggregateFilterData(filterData.toString()).build(); + .setFilterData(filterData.toString()).build(); Optional<AggregatableAttributionSource> aggregatableAttributionSource = source.parseAggregateSource(); assertTrue(aggregatableAttributionSource.isPresent()); @@ -1285,7 +1328,7 @@ public class SourceTest { assertEquals( aggregateSource.getAggregatableSource().get("campaignCounts").longValue(), 345L); assertEquals(aggregateSource.getAggregatableSource().get("geoValue").longValue(), 5L); - assertEquals(aggregateSource.getAggregateFilterData().getAttributionFilterMap().size(), 2); + assertEquals(aggregateSource.getFilterData().getAttributionFilterMap().size(), 3); } private void verifyAlgorithmicFakeReportGeneration(Source source, int expectedCardinality) { @@ -1304,7 +1347,8 @@ public class SourceTest { assertTrue( source.getExpiryTime() + TimeUnit.HOURS.toMillis(1) >= report.getReportingTime()); - assertTrue(report.getTriggerData() < expectedCardinality); + Long triggerData = report.getTriggerData().getValue(); + assertTrue(0 <= triggerData && triggerData < expectedCardinality); } } else if (source.getAttributionMode() == Source.AttributionMode.NEVER) { neverCount++; @@ -1331,7 +1375,7 @@ public class SourceTest { .map( reportState -> new Source.FakeReport( - reportState[0], + new UnsignedLong(Long.valueOf(reportState[0])), source.getReportingTimeForNoising(reportState[1]), reportState[2] == 0 ? source.getAppDestination() @@ -1340,7 +1384,7 @@ public class SourceTest { } private void assertInvalidSourceArguments( - Long sourceEventId, + UnsignedLong sourceEventId, Uri publisher, Uri appDestination, Uri webDestination, @@ -1352,10 +1396,10 @@ public class SourceTest { Source.SourceType sourceType, Long installAttributionWindow, Long installCooldownWindow, - @Nullable Long debugKey, + @Nullable UnsignedLong debugKey, @Source.AttributionMode int attributionMode, @Nullable String aggregateSource, - @Nullable String aggregateFilterData) { + @Nullable String filterData) { assertThrows( IllegalArgumentException.class, () -> @@ -1374,7 +1418,7 @@ public class SourceTest { .setInstallCooldownWindow(installCooldownWindow) .setAttributionMode(attributionMode) .setAggregateSource(aggregateSource) - .setAggregateFilterData(aggregateFilterData) + .setFilterData(filterData) .setDebugKey(debugKey) .build()); } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/TestObjectProvider.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/TestObjectProvider.java index 07974cfb43..288989b7a3 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/TestObjectProvider.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/TestObjectProvider.java @@ -24,12 +24,15 @@ import static org.mockito.Mockito.spy; import android.annotation.IntDef; import android.test.mock.MockContentResolver; +import com.android.adservices.data.enrollment.EnrollmentDao; import com.android.adservices.data.measurement.DatastoreManager; +import com.android.adservices.data.measurement.deletion.MeasurementDataDeleter; import com.android.adservices.service.Flags; import com.android.adservices.service.measurement.attribution.AttributionJobHandlerWrapper; import com.android.adservices.service.measurement.inputverification.ClickVerifier; -import com.android.adservices.service.measurement.registration.SourceFetcher; -import com.android.adservices.service.measurement.registration.TriggerFetcher; +import com.android.adservices.service.measurement.registration.AsyncSourceFetcher; +import com.android.adservices.service.measurement.registration.AsyncTriggerFetcher; +import com.android.adservices.service.measurement.util.UnsignedLong; import org.mockito.stubbing.Answer; @@ -57,35 +60,49 @@ class TestObjectProvider { } static MeasurementImpl getMeasurementImpl( - @Type int type, DatastoreManager datastoreManager, - SourceFetcher sourceFetcher, - TriggerFetcher triggerFetcher, ClickVerifier clickVerifier, - Flags flags) { + Flags flags, + MeasurementDataDeleter measurementDataDeleter, + EnrollmentDao enrollmentDao) { + return spy( + new MeasurementImpl( + null, + datastoreManager, + clickVerifier, + measurementDataDeleter, + enrollmentDao)); + } + + static AsyncRegistrationQueueRunner getAsyncRegistrationQueueRunner( + @Type int type, + DatastoreManager datastoreManager, + AsyncSourceFetcher asyncSourceFetcher, + AsyncTriggerFetcher asyncTriggerFetcher, + EnrollmentDao enrollmentDao) { if (type == Type.DENOISED) { - MeasurementImpl measurementImpl = + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = spy( - new MeasurementImpl( - null, + new AsyncRegistrationQueueRunner( new MockContentResolver(), - datastoreManager, - sourceFetcher, - triggerFetcher, - clickVerifier)); + asyncSourceFetcher, + asyncTriggerFetcher, + enrollmentDao, + datastoreManager)); // Disable Impression Noise - doReturn(Collections.emptyList()).when(measurementImpl).generateFakeEventReports(any()); - return measurementImpl; + doReturn(Collections.emptyList()) + .when(asyncRegistrationQueueRunner) + .generateFakeEventReports(any()); + return asyncRegistrationQueueRunner; } else if (type == Type.NOISY) { - MeasurementImpl measurementImpl = + AsyncRegistrationQueueRunner asyncRegistrationQueueRunner = spy( - new MeasurementImpl( - null, + new AsyncRegistrationQueueRunner( new MockContentResolver(), - datastoreManager, - sourceFetcher, - triggerFetcher, - clickVerifier)); + asyncSourceFetcher, + asyncTriggerFetcher, + enrollmentDao, + datastoreManager)); // Create impression noise with 100% probability Answer<?> answerSourceEventReports = invocation -> { @@ -93,9 +110,9 @@ class TestObjectProvider { source.setAttributionMode(Source.AttributionMode.FALSELY); return Collections.singletonList( new EventReport.Builder() - .setSourceId(source.getEventId()) + .setSourceEventId(source.getEventId()) .setReportTime(source.getExpiryTime() + ONE_HOUR_IN_MILLIS) - .setTriggerData(0) + .setTriggerData(new UnsignedLong(0L)) .setAttributionDestination(source.getAppDestination()) .setEnrollmentId(source.getEnrollmentId()) .setTriggerTime(0) @@ -106,17 +123,16 @@ class TestObjectProvider { .build()); }; doAnswer(answerSourceEventReports) - .when(measurementImpl) + .when(asyncRegistrationQueueRunner) .generateFakeEventReports(any()); - return measurementImpl; + return asyncRegistrationQueueRunner; } - return new MeasurementImpl( - null, + return new AsyncRegistrationQueueRunner( new MockContentResolver(), - datastoreManager, - sourceFetcher, - triggerFetcher, - clickVerifier); + asyncSourceFetcher, + asyncTriggerFetcher, + enrollmentDao, + datastoreManager); } } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/TriggerTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/TriggerTest.java index c563d9a421..d3250af480 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/TriggerTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/TriggerTest.java @@ -19,14 +19,15 @@ package com.android.adservices.service.measurement; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import android.net.Uri; import com.android.adservices.service.measurement.aggregation.AggregatableAttributionTrigger; -import com.android.adservices.service.measurement.aggregation.AggregateFilterData; import com.android.adservices.service.measurement.aggregation.AggregateTriggerData; +import com.android.adservices.service.measurement.util.UnsignedLong; import org.json.JSONArray; import org.json.JSONException; @@ -65,7 +66,17 @@ public class TriggerTest { + "}" + "]\n"; - private static final Long DEBUG_KEY = 2367372L; + private static final UnsignedLong DEBUG_KEY = new UnsignedLong(2367372L); + private static final Uri APP_DESTINATION = Uri.parse("android-app://com.android.app"); + private static final Uri APP_DESTINATION_WITH_PATH = + Uri.parse("android-app://com.android.app/with/path"); + private static final Uri WEB_DESTINATION = Uri.parse("https://example.com"); + private static final Uri WEB_DESTINATION_WITH_PATH = Uri.parse("https://example.com/with/path"); + private static final Uri WEB_DESTINATION_WITH_SUBDOMAIN = + Uri.parse("https://subdomain.example.com"); + private static final Uri WEB_DESTINATION_WITH_SUBDOMAIN_PATH_QUERY_FRAGMENT = + Uri.parse("https://subdomain.example.com/with/path?query=0#fragment"); + private static final Uri WEB_DESTINATION_INVALID = Uri.parse("https://example.notatld"); @Test public void testEqualsPass() throws JSONException { @@ -96,6 +107,7 @@ public class TriggerTest { .setAggregateTriggerData(aggregateTriggerDatas.toString()) .setAggregateValues(values.toString()) .setFilters(TOP_LEVEL_FILTERS_JSON_STRING) + .setNotFilters(TOP_LEVEL_FILTERS_JSON_STRING) .setDebugKey(DEBUG_KEY) .setAggregatableAttributionTrigger( TriggerFixture.getValidTrigger() @@ -113,6 +125,7 @@ public class TriggerTest { .setAggregateTriggerData(aggregateTriggerDatas.toString()) .setAggregateValues(values.toString()) .setFilters(TOP_LEVEL_FILTERS_JSON_STRING) + .setNotFilters(TOP_LEVEL_FILTERS_JSON_STRING) .setDebugKey(DEBUG_KEY) .setAggregatableAttributionTrigger( TriggerFixture.getValidTrigger() @@ -187,6 +200,11 @@ public class TriggerTest { .setFilters(TOP_LEVEL_FILTERS_JSON_STRING).build(), TriggerFixture.getValidTriggerBuilder() .setFilters(TOP_LEVEL_FILTERS_JSON_STRING_X).build()); + assertNotEquals( + TriggerFixture.getValidTriggerBuilder() + .setNotFilters(TOP_LEVEL_FILTERS_JSON_STRING).build(), + TriggerFixture.getValidTriggerBuilder() + .setNotFilters(TOP_LEVEL_FILTERS_JSON_STRING_X).build()); } @Test @@ -222,6 +240,7 @@ public class TriggerTest { TriggerFixture.ValidTriggerParams.AGGREGATE_TRIGGER_DATA, TriggerFixture.ValidTriggerParams.AGGREGATE_VALUES, TriggerFixture.ValidTriggerParams.TOP_LEVEL_FILTERS_JSON_STRING, + TriggerFixture.ValidTriggerParams.TOP_LEVEL_NOT_FILTERS_JSON_STRING, TriggerFixture.ValidTriggerParams.DEBUG_KEY); assertInvalidTriggerArguments( Uri.parse("com.destination"), @@ -232,6 +251,7 @@ public class TriggerTest { TriggerFixture.ValidTriggerParams.AGGREGATE_TRIGGER_DATA, TriggerFixture.ValidTriggerParams.AGGREGATE_VALUES, TriggerFixture.ValidTriggerParams.TOP_LEVEL_FILTERS_JSON_STRING, + TriggerFixture.ValidTriggerParams.TOP_LEVEL_NOT_FILTERS_JSON_STRING, TriggerFixture.ValidTriggerParams.DEBUG_KEY); } @@ -246,6 +266,7 @@ public class TriggerTest { TriggerFixture.ValidTriggerParams.AGGREGATE_TRIGGER_DATA, TriggerFixture.ValidTriggerParams.AGGREGATE_VALUES, TriggerFixture.ValidTriggerParams.TOP_LEVEL_FILTERS_JSON_STRING, + TriggerFixture.ValidTriggerParams.TOP_LEVEL_NOT_FILTERS_JSON_STRING, TriggerFixture.ValidTriggerParams.DEBUG_KEY); } @@ -260,6 +281,7 @@ public class TriggerTest { TriggerFixture.ValidTriggerParams.AGGREGATE_TRIGGER_DATA, TriggerFixture.ValidTriggerParams.AGGREGATE_VALUES, TriggerFixture.ValidTriggerParams.TOP_LEVEL_FILTERS_JSON_STRING, + TriggerFixture.ValidTriggerParams.TOP_LEVEL_NOT_FILTERS_JSON_STRING, TriggerFixture.ValidTriggerParams.DEBUG_KEY); assertInvalidTriggerArguments( TriggerFixture.ValidTriggerParams.ATTRIBUTION_DESTINATION, @@ -270,6 +292,7 @@ public class TriggerTest { TriggerFixture.ValidTriggerParams.AGGREGATE_TRIGGER_DATA, TriggerFixture.ValidTriggerParams.AGGREGATE_VALUES, TriggerFixture.ValidTriggerParams.TOP_LEVEL_FILTERS_JSON_STRING, + TriggerFixture.ValidTriggerParams.TOP_LEVEL_NOT_FILTERS_JSON_STRING, TriggerFixture.ValidTriggerParams.DEBUG_KEY); } @@ -343,6 +366,73 @@ public class TriggerTest { } @Test + public void testGetAttributionDestinationBaseUri_appDestination() throws JSONException { + Trigger trigger = TriggerFixture.getValidTriggerBuilder() + .setAttributionDestination(APP_DESTINATION) + .setDestinationType(EventSurfaceType.APP) + .build(); + assertEquals(APP_DESTINATION, trigger.getAttributionDestinationBaseUri()); + } + + @Test + public void testGetAttributionDestinationBaseUri_trimsAppDestination() throws JSONException { + Trigger trigger = TriggerFixture.getValidTriggerBuilder() + .setAttributionDestination(APP_DESTINATION_WITH_PATH) + .setDestinationType(EventSurfaceType.APP) + .build(); + assertEquals(APP_DESTINATION, trigger.getAttributionDestinationBaseUri()); + } + + @Test + public void testGetAttributionDestinationBaseUri_webDestination() throws JSONException { + Trigger trigger = TriggerFixture.getValidTriggerBuilder() + .setAttributionDestination(WEB_DESTINATION) + .setDestinationType(EventSurfaceType.WEB) + .build(); + assertEquals(WEB_DESTINATION, trigger.getAttributionDestinationBaseUri()); + } + + @Test + public void testGetAttributionDestinationBaseUri_trimsWebDestinationWithSubdomain() + throws JSONException { + Trigger trigger = TriggerFixture.getValidTriggerBuilder() + .setAttributionDestination(WEB_DESTINATION_WITH_SUBDOMAIN) + .setDestinationType(EventSurfaceType.WEB) + .build(); + assertEquals(WEB_DESTINATION, trigger.getAttributionDestinationBaseUri()); + } + + @Test + public void testGetAttributionDestinationBaseUri_trimsWebDestinationWithPath() + throws JSONException { + Trigger trigger = TriggerFixture.getValidTriggerBuilder() + .setAttributionDestination(WEB_DESTINATION_WITH_PATH) + .setDestinationType(EventSurfaceType.WEB) + .build(); + assertEquals(WEB_DESTINATION, trigger.getAttributionDestinationBaseUri()); + } + + @Test + public void testGetAttributionDestinationBaseUri_trimsWebDestinationWithSubdomainPathQueryFrag() + throws JSONException { + Trigger trigger = TriggerFixture.getValidTriggerBuilder() + .setAttributionDestination(WEB_DESTINATION_WITH_SUBDOMAIN_PATH_QUERY_FRAGMENT) + .setDestinationType(EventSurfaceType.WEB) + .build(); + assertEquals(WEB_DESTINATION, trigger.getAttributionDestinationBaseUri()); + } + + @Test + public void testGetAttributionDestinationBaseUri_invalidWebDestination() + throws JSONException { + Trigger trigger = TriggerFixture.getValidTriggerBuilder() + .setAttributionDestination(WEB_DESTINATION_INVALID) + .setDestinationType(EventSurfaceType.WEB) + .build(); + assertNull(trigger.getAttributionDestinationBaseUri()); + } + + @Test public void parseEventTriggers() throws JSONException { // setup JSONObject filters1 = @@ -392,25 +482,25 @@ public class TriggerTest { EventTrigger eventTrigger1 = new EventTrigger.Builder() .setTriggerPriority(2L) - .setTriggerData(2L) - .setDedupKey(2L) + .setTriggerData(new UnsignedLong(2L)) + .setDedupKey(new UnsignedLong(2L)) .setFilter( - new AggregateFilterData.Builder() - .buildAggregateFilterData(filters1) + new FilterData.Builder() + .buildFilterData(filters1) .build()) .setNotFilter( - new AggregateFilterData.Builder() - .buildAggregateFilterData(notFilters1) + new FilterData.Builder() + .buildFilterData(notFilters1) .build()) .build(); EventTrigger eventTrigger2 = new EventTrigger.Builder() .setTriggerPriority(3L) - .setTriggerData(3L) - .setDedupKey(3L) + .setTriggerData(new UnsignedLong(3L)) + .setDedupKey(new UnsignedLong(3L)) .setNotFilter( - new AggregateFilterData.Builder() - .buildAggregateFilterData(notFilters2) + new FilterData.Builder() + .buildFilterData(notFilters2) .build()) .build(); @@ -430,7 +520,8 @@ public class TriggerTest { String aggregateTriggerData, String aggregateValues, String filters, - Long debugKey) { + String notFilters, + UnsignedLong debugKey) { assertThrows( IllegalArgumentException.class, () -> @@ -443,6 +534,7 @@ public class TriggerTest { .setAggregateTriggerData(aggregateTriggerData) .setAggregateValues(aggregateValues) .setFilters(filters) + .setNotFilters(notFilters) .setDebugKey(debugKey) .build()); } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/access/ManifestBasedAdtechAccessResolverTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/access/ManifestBasedAdtechAccessResolverTest.java index 553cb029bb..02a7724e3b 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/access/ManifestBasedAdtechAccessResolverTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/access/ManifestBasedAdtechAccessResolverTest.java @@ -68,13 +68,14 @@ public class ManifestBasedAdtechAccessResolverTest { mMockEnrollmentDao = mock(EnrollmentDao.class); doReturn(EnrollmentFixture.getValidEnrollment()) .when(mMockEnrollmentDao) - .getEnrollmentDataFromMeasurementUrl(eq(ENROLLED_AD_TECH_URL.toString())); + .getEnrollmentDataFromMeasurementUrl(eq(ENROLLED_AD_TECH_URL)); mMockitoSession = ExtendedMockito.mockitoSession() .mockStatic(AppManifestConfigHelper.class) .strictness(Strictness.LENIENT) .initMocks(this) .startMocking(); + when(mFlags.isEnrollmentBlocklisted(any())).thenReturn(false); } @After @@ -193,6 +194,21 @@ public class ManifestBasedAdtechAccessResolverTest { } @Test + public void isNotAllowed_enrollmentInBlocklist() { + mClassUnderTest = + new ManifestBasedAdtechAccessResolver( + mMockEnrollmentDao, mFlags, PACKAGE, ENROLLED_AD_TECH_URL); + when(mFlags.isDisableMeasurementEnrollmentCheck()).thenReturn(false); + when(AppManifestConfigHelper.isAllowedAttributionAccess(any(), any(), any())) + .thenReturn(true); + + String enrollmentId = EnrollmentFixture.getValidEnrollment().getEnrollmentId(); + when(mFlags.isEnrollmentBlocklisted(enrollmentId)).thenReturn(true); + + assertFalse(mClassUnderTest.isAllowed(CONTEXT)); + } + + @Test public void getErrorMessage() { mClassUnderTest = new ManifestBasedAdtechAccessResolver( diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/access/UserConsentAccessResolverTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/access/UserConsentAccessResolverTest.java index c0a7cb654b..34af05bb30 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/access/UserConsentAccessResolverTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/access/UserConsentAccessResolverTest.java @@ -53,7 +53,7 @@ public class UserConsentAccessResolverTest { @Test public void isAllowed_consented_success() { // Setup - doReturn(AdServicesApiConsent.GIVEN).when(mConsentManager).getConsent(mPackageManager); + doReturn(AdServicesApiConsent.GIVEN).when(mConsentManager).getConsent(); // Execution assertTrue(mClassUnderTest.isAllowed(mContext)); @@ -62,7 +62,7 @@ public class UserConsentAccessResolverTest { @Test public void isAllowed_notConsented_success() { // Setup - doReturn(AdServicesApiConsent.REVOKED).when(mConsentManager).getConsent(mPackageManager); + doReturn(AdServicesApiConsent.REVOKED).when(mConsentManager).getConsent(); // Execution assertFalse(mClassUnderTest.isAllowed(mContext)); diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/AggregateReportingJob.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/AggregateReportingJob.java new file mode 100644 index 0000000000..19667858d5 --- /dev/null +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/AggregateReportingJob.java @@ -0,0 +1,44 @@ +/* + * 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.measurement.actions; + +import java.util.Objects; + +public final class AggregateReportingJob implements Action { + public final long mTimestamp; + + public AggregateReportingJob(long timestamp) { + mTimestamp = timestamp; + } + + public long getComparable() { + return mTimestamp; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof AggregateReportingJob)) return false; + AggregateReportingJob that = (AggregateReportingJob) o; + return mTimestamp == that.mTimestamp; + } + + @Override + public int hashCode() { + return Objects.hash(mTimestamp); + } +} diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/ReportingJob.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/EventReportingJob.java index 23f1e6b1c0..12b1551ea8 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/ReportingJob.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/EventReportingJob.java @@ -18,13 +18,14 @@ package com.android.adservices.service.measurement.actions; import java.util.Objects; -public final class ReportingJob implements Action { +public final class EventReportingJob implements Action { public final long mTimestamp; - public ReportingJob(long timestamp) { + public EventReportingJob(long timestamp) { mTimestamp = timestamp; } + @Override public long getComparable() { return mTimestamp; } @@ -32,8 +33,8 @@ public final class ReportingJob implements Action { @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof ReportingJob)) return false; - ReportingJob that = (ReportingJob) o; + if (!(o instanceof EventReportingJob)) return false; + EventReportingJob that = (EventReportingJob) o; return mTimestamp == that.mTimestamp; } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/InstallApp.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/InstallApp.java index 180d46e2a8..7de2213228 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/InstallApp.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/InstallApp.java @@ -33,6 +33,7 @@ public final class InstallApp implements Action { mTimestamp = obj.getLong(TestFormatJsonMapping.INSTALLS_TIMESTAMP_KEY); } + @Override public long getComparable() { return mTimestamp; } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/RegisterSource.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/RegisterSource.java index 4c17b47f4a..c81a9571d3 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/RegisterSource.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/RegisterSource.java @@ -36,21 +36,22 @@ public final class RegisterSource implements Action { public final RegistrationRequest mRegistrationRequest; public final Map<String, List<Map<String, List<String>>>> mUriToResponseHeadersMap; public final long mTimestamp; + // Used in interop tests + public final String mPublisher; public RegisterSource(JSONObject obj) throws JSONException { JSONObject regParamsJson = obj.getJSONObject( TestFormatJsonMapping.REGISTRATION_REQUEST_KEY); AttributionSource attributionSource = getAttributionSource( - regParamsJson.optString(TestFormatJsonMapping.ATTRIBUTION_SOURCE_KEY)); + regParamsJson.optString(TestFormatJsonMapping.ATTRIBUTION_SOURCE_KEY, + TestFormatJsonMapping.ATTRIBUTION_SOURCE_DEFAULT)); + + mPublisher = regParamsJson.optString(TestFormatJsonMapping.SOURCE_TOP_ORIGIN_URI_KEY); mRegistrationRequest = new RegistrationRequest.Builder() .setRegistrationType(RegistrationRequest.REGISTER_SOURCE) - .setTopOriginUri( - Uri.parse( - regParamsJson.getString( - TestFormatJsonMapping.SOURCE_TOP_ORIGIN_URI_KEY))) .setRegistrationUri( Uri.parse( regParamsJson.getString( @@ -61,6 +62,9 @@ public final class RegisterSource implements Action { .equals(TestFormatJsonMapping.SOURCE_VIEW_TYPE) ? null : getInputEvent()) + .setAdIdPermissionGranted( + regParamsJson.optBoolean( + TestFormatJsonMapping.IS_ADID_PERMISSION_GRANTED_KEY, true)) .setPackageName(attributionSource.getPackageName()) .build(); mUriToResponseHeadersMap = getUriToResponseHeadersMap(obj); @@ -71,4 +75,8 @@ public final class RegisterSource implements Action { public long getComparable() { return mTimestamp; } + + public String getPublisher() { + return mPublisher; + } } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/RegisterTrigger.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/RegisterTrigger.java index dd9f562deb..cdba2ac300 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/RegisterTrigger.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/RegisterTrigger.java @@ -35,25 +35,29 @@ public final class RegisterTrigger implements Action { public final RegistrationRequest mRegistrationRequest; public final Map<String, List<Map<String, List<String>>>> mUriToResponseHeadersMap; public final long mTimestamp; + // Used in interop tests + public final String mDestination; public RegisterTrigger(JSONObject obj) throws JSONException { JSONObject regParamsJson = obj.getJSONObject( TestFormatJsonMapping.REGISTRATION_REQUEST_KEY); AttributionSource attributionSource = getAttributionSource( - regParamsJson.optString(TestFormatJsonMapping.ATTRIBUTION_SOURCE_KEY)); + regParamsJson.optString(TestFormatJsonMapping.ATTRIBUTION_SOURCE_KEY, + TestFormatJsonMapping.ATTRIBUTION_SOURCE_DEFAULT)); + + mDestination = regParamsJson.optString(TestFormatJsonMapping.TRIGGER_TOP_ORIGIN_URI_KEY); mRegistrationRequest = new RegistrationRequest.Builder() .setRegistrationType(RegistrationRequest.REGISTER_TRIGGER) - .setTopOriginUri( - Uri.parse( - regParamsJson.getString( - TestFormatJsonMapping.TRIGGER_TOP_ORIGIN_URI_KEY))) .setRegistrationUri( Uri.parse( regParamsJson.getString( TestFormatJsonMapping.REGISTRATION_URI_KEY))) + .setAdIdPermissionGranted( + regParamsJson.optBoolean( + TestFormatJsonMapping.IS_ADID_PERMISSION_GRANTED_KEY, true)) .setPackageName(attributionSource.getPackageName()) .build(); @@ -61,7 +65,12 @@ public final class RegisterTrigger implements Action { mTimestamp = obj.getLong(TestFormatJsonMapping.TIMESTAMP_KEY); } + @Override public long getComparable() { return mTimestamp; } + + public String getDestination() { + return mDestination; + } } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/RegisterWebSource.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/RegisterWebSource.java index f65778126d..ac6c7660a1 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/RegisterWebSource.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/RegisterWebSource.java @@ -80,6 +80,9 @@ public final class RegisterWebSource implements Action { mRegistrationRequest = new WebSourceRegistrationRequestInternal.Builder( registrationRequest, attributionSource.getPackageName(), 2000L) + .setAdIdPermissionGranted( + regParamsJson.optBoolean( + TestFormatJsonMapping.IS_ADID_PERMISSION_GRANTED_KEY, true)) .build(); mUriToResponseHeadersMap = getUriToResponseHeadersMap(obj); @@ -102,8 +105,7 @@ public final class RegisterWebSource implements Action { Uri.parse( sourceParams.getString( TestFormatJsonMapping.REGISTRATION_URI_KEY))) - .setDebugKeyAllowed( - sourceParams.optBoolean(TestFormatJsonMapping.DEBUG_KEY, false)) + .setDebugKeyAllowed(true) .build()); } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/RegisterWebTrigger.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/RegisterWebTrigger.java index 2b86d30552..a52397486c 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/RegisterWebTrigger.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/RegisterWebTrigger.java @@ -61,6 +61,9 @@ public final class RegisterWebTrigger implements Action { mRegistrationRequest = new WebTriggerRegistrationRequestInternal.Builder( registrationRequest, attributionSource.getPackageName()) + .setAdIdPermissionGranted( + regParamsJson.optBoolean( + TestFormatJsonMapping.IS_ADID_PERMISSION_GRANTED_KEY, true)) .build(); mUriToResponseHeadersMap = getUriToResponseHeadersMap(obj); @@ -83,9 +86,7 @@ public final class RegisterWebTrigger implements Action { Uri.parse( triggerParams.getString( TestFormatJsonMapping.REGISTRATION_URI_KEY))) - .setDebugKeyAllowed( - triggerParams.optBoolean( - TestFormatJsonMapping.DEBUG_KEY, false)) + .setDebugKeyAllowed(true) .build()); } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/UninstallApp.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/UninstallApp.java index e73ad82d7b..ed11d75f86 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/UninstallApp.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/actions/UninstallApp.java @@ -33,6 +33,7 @@ public final class UninstallApp implements Action { mTimestamp = obj.getLong(TestFormatJsonMapping.INSTALLS_TIMESTAMP_KEY); } + @Override public long getComparable() { return mTimestamp; } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/aggregation/AggregateEncryptionKeyManagerIntegrationTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/aggregation/AggregateEncryptionKeyManagerIntegrationTest.java index 3599e1d054..e45de13395 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/aggregation/AggregateEncryptionKeyManagerIntegrationTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/aggregation/AggregateEncryptionKeyManagerIntegrationTest.java @@ -20,10 +20,11 @@ import static org.mockito.Mockito.when; import android.net.Uri; +import com.android.adservices.data.DbTestUtil; import com.android.adservices.data.measurement.AbstractDbIntegrationTest; import com.android.adservices.data.measurement.DatastoreManager; -import com.android.adservices.data.measurement.DatastoreManagerFactory; import com.android.adservices.data.measurement.DbState; +import com.android.adservices.data.measurement.SQLDatastoreManager; import org.json.JSONException; import org.junit.Assert; @@ -74,7 +75,8 @@ public class AggregateEncryptionKeyManagerIntegrationTest extends AbstractDbInte @Override public void runActionToTest() { - DatastoreManager datastoreManager = DatastoreManagerFactory.getDatastoreManager(sContext); + DatastoreManager datastoreManager = + new SQLDatastoreManager(DbTestUtil.getDbHelperForTest()); AggregateEncryptionKeyManager aggregateEncryptionKeyManager = new AggregateEncryptionKeyManager(datastoreManager, mFetcher, mClock, MEASUREMENT_AGGREGATE_ENCRYPTION_KEY_COORDINATOR_URL); diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/aggregation/AggregatePayloadGeneratorTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/aggregation/AggregatePayloadGeneratorTest.java index baeab5ff74..f55d7d28eb 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/aggregation/AggregatePayloadGeneratorTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/aggregation/AggregatePayloadGeneratorTest.java @@ -16,6 +16,8 @@ package com.android.adservices.service.measurement.aggregation; +import com.android.adservices.service.measurement.FilterData; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -48,12 +50,12 @@ public final class AggregatePayloadGeneratorTest { Collections.singletonList("electronics.megastore")); sourceFilterMap.put("product", Arrays.asList("1234", "234")); sourceFilterMap.put("ctid", Collections.singletonList("id")); - AggregateFilterData sourceFilter = new AggregateFilterData.Builder() + FilterData sourceFilter = new FilterData.Builder() .setAttributionFilterMap(sourceFilterMap).build(); AggregatableAttributionSource attributionSource = new AggregatableAttributionSource.Builder() .setAggregatableSource(aggregatableSource) - .setAggregateFilterData(sourceFilter).build(); + .setFilterData(sourceFilter).build(); // Build AggregatableAttributionTrigger. List<AggregateTriggerData> triggerDataList = new ArrayList<>(); @@ -67,9 +69,9 @@ public final class AggregatePayloadGeneratorTest { new AggregateTriggerData.Builder() .setKey(BigInteger.valueOf(1024L)) .setSourceKeys(new HashSet<>(Collections.singletonList("campaignCounts"))) - .setFilter(new AggregateFilterData.Builder() + .setFilter(new FilterData.Builder() .setAttributionFilterMap(triggerDataFilter1).build()) - .setNotFilter(new AggregateFilterData.Builder() + .setNotFilter(new FilterData.Builder() .setAttributionFilterMap(triggerDataNotFilter1).build()).build()); // Apply this key_piece to "geoValue". triggerDataList.add( @@ -107,18 +109,18 @@ public final class AggregatePayloadGeneratorTest { Map<String, BigInteger> aggregatableSource = new HashMap<>(); aggregatableSource.put("campaignCounts", BigInteger.valueOf(345L)); aggregatableSource.put("geoValue", BigInteger.valueOf(5L)); - aggregatableSource.put("thirdSource", BigInteger.valueOf(100L)); + aggregatableSource.put("thirdSource", BigInteger.valueOf(101L)); Map<String, List<String>> sourceFilterMap = new HashMap<>(); sourceFilterMap.put("conversion_subdomain", Collections.singletonList("electronics.megastore")); sourceFilterMap.put("product", Arrays.asList("1234", "234")); sourceFilterMap.put("ctid", Collections.singletonList("id")); - AggregateFilterData sourceFilter = new AggregateFilterData.Builder() + FilterData sourceFilter = new FilterData.Builder() .setAttributionFilterMap(sourceFilterMap).build(); AggregatableAttributionSource attributionSource = new AggregatableAttributionSource.Builder() .setAggregatableSource(aggregatableSource) - .setAggregateFilterData(sourceFilter).build(); + .setFilterData(sourceFilter).build(); // Build AggregatableAttributionTrigger. List<AggregateTriggerData> triggerDataList = new ArrayList<>(); // Apply this key_piece to "campaignCounts". @@ -131,9 +133,9 @@ public final class AggregatePayloadGeneratorTest { new AggregateTriggerData.Builder() .setKey(BigInteger.valueOf(1024L)) .setSourceKeys(new HashSet<>(Collections.singletonList("campaignCounts"))) - .setFilter(new AggregateFilterData.Builder() + .setFilter(new FilterData.Builder() .setAttributionFilterMap(triggerDataFilter1).build()) - .setNotFilter(new AggregateFilterData.Builder() + .setNotFilter(new FilterData.Builder() .setAttributionFilterMap(triggerDataNotFilter1).build()) .build()); // Apply this key_piece to "geoValue". @@ -158,13 +160,16 @@ public final class AggregatePayloadGeneratorTest { assertTrue(aggregateHistogramContributions.isPresent()); List<AggregateHistogramContribution> contributions = aggregateHistogramContributions.get(); - assertEquals(contributions.size(), 2); + assertEquals(contributions.size(), 3); assertTrue(contributions.contains( new AggregateHistogramContribution.Builder() .setKey(BigInteger.valueOf(1369L)).setValue(32768).build())); assertTrue(contributions.contains( new AggregateHistogramContribution.Builder() .setKey(BigInteger.valueOf(2693L)).setValue(1664).build())); + assertTrue(contributions.contains( + new AggregateHistogramContribution.Builder() + .setKey(BigInteger.valueOf(101L)).setValue(100).build())); } @Test @@ -178,12 +183,12 @@ public final class AggregatePayloadGeneratorTest { Collections.singletonList("electronics.megastore")); sourceFilterMap.put("product", Arrays.asList("1234", "234")); sourceFilterMap.put("ctid", Collections.singletonList("id")); - AggregateFilterData sourceFilter = new AggregateFilterData.Builder() + FilterData sourceFilter = new FilterData.Builder() .setAttributionFilterMap(sourceFilterMap).build(); AggregatableAttributionSource attributionSource = new AggregatableAttributionSource.Builder() .setAggregatableSource(aggregatableSource) - .setAggregateFilterData(sourceFilter).build(); + .setFilterData(sourceFilter).build(); // Build AggregatableAttributionTrigger. List<AggregateTriggerData> triggerDataList = new ArrayList<>(); // Apply this key_piece to "campaignCounts". @@ -196,9 +201,9 @@ public final class AggregatePayloadGeneratorTest { new AggregateTriggerData.Builder() .setKey(BigInteger.valueOf(1024L)) .setSourceKeys(new HashSet<>(Collections.singletonList("campaignCounts"))) - .setFilter(new AggregateFilterData.Builder() + .setFilter(new FilterData.Builder() .setAttributionFilterMap(triggerDataFilter1).build()) - .setNotFilter(new AggregateFilterData.Builder() + .setNotFilter(new FilterData.Builder() .setAttributionFilterMap(triggerDataNotFilter1).build()) .build()); // Apply this key_piece to "geoValue". @@ -220,7 +225,7 @@ public final class AggregatePayloadGeneratorTest { new AggregateTriggerData.Builder() .setKey(BigInteger.valueOf(200L)) .setSourceKeys(new HashSet<>(Arrays.asList("campaignCounts", "geoValue"))) - .setFilter(new AggregateFilterData.Builder() + .setFilter(new FilterData.Builder() .setAttributionFilterMap(triggerDataFilter2).build()) .build()); @@ -256,12 +261,12 @@ public final class AggregatePayloadGeneratorTest { sourceFilterMap.put("conversion_subdomain", Collections.singletonList("electronics.megastore")); sourceFilterMap.put("product", Arrays.asList("1234", "234")); - AggregateFilterData sourceFilter = new AggregateFilterData.Builder() + FilterData sourceFilter = new FilterData.Builder() .setAttributionFilterMap(sourceFilterMap).build(); AggregatableAttributionSource attributionSource = new AggregatableAttributionSource.Builder() .setAggregatableSource(aggregatableSource) - .setAggregateFilterData(sourceFilter).build(); + .setFilterData(sourceFilter).build(); // Build AggregatableAttributionTrigger. List<AggregateTriggerData> triggerDataList = new ArrayList<>(); @@ -273,7 +278,7 @@ public final class AggregatePayloadGeneratorTest { new AggregateTriggerData.Builder() .setKey(BigInteger.valueOf(2L).shiftLeft(63)) .setSourceKeys(new HashSet<>(Collections.singletonList("campaignCounts"))) - .setFilter(new AggregateFilterData.Builder() + .setFilter(new FilterData.Builder() .setAttributionFilterMap(triggerDataFilter1).build()).build()); Map<String, Integer> values = new HashMap<>(); @@ -305,12 +310,12 @@ public final class AggregatePayloadGeneratorTest { sourceFilterMap.put("conversion_subdomain", Collections.singletonList("electronics.megastore")); sourceFilterMap.put("product", Arrays.asList("1234", "234")); - AggregateFilterData sourceFilter = new AggregateFilterData.Builder() + FilterData sourceFilter = new FilterData.Builder() .setAttributionFilterMap(sourceFilterMap).build(); AggregatableAttributionSource attributionSource = new AggregatableAttributionSource.Builder() .setAggregatableSource(aggregatableSource) - .setAggregateFilterData(sourceFilter).build(); + .setFilterData(sourceFilter).build(); // Build AggregatableAttributionTrigger. List<AggregateTriggerData> triggerDataList = new ArrayList<>(); @@ -322,7 +327,7 @@ public final class AggregatePayloadGeneratorTest { new AggregateTriggerData.Builder() .setKey(BigInteger.valueOf(2L).shiftLeft(63).add(BigInteger.valueOf(4L))) .setSourceKeys(new HashSet<>(Collections.singletonList("campaignCounts"))) - .setFilter(new AggregateFilterData.Builder() + .setFilter(new FilterData.Builder() .setAttributionFilterMap(triggerDataFilter1).build()).build()); Map<String, Integer> values = new HashMap<>(); diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/aggregation/AggregateReportTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/aggregation/AggregateReportTest.java index 5dc25302c2..4cf1de7fc8 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/aggregation/AggregateReportTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/aggregation/AggregateReportTest.java @@ -25,16 +25,21 @@ import android.net.Uri; import androidx.test.filters.SmallTest; +import com.android.adservices.service.measurement.util.UnsignedLong; + import org.junit.Test; import java.util.Set; +import java.util.UUID; /** Unit tests for {@link AggregateReport} */ @SmallTest public final class AggregateReportTest { - private static final Long SOURCE_DEBUG_KEY = 237865L; - private static final Long TRIGGER_DEBUG_KEY = 928762L; + private static final UnsignedLong SOURCE_DEBUG_KEY = new UnsignedLong(237865L); + private static final UnsignedLong TRIGGER_DEBUG_KEY = new UnsignedLong(928762L); + private static final String SOURCE_ID = UUID.randomUUID().toString(); + private static final String TRIGGER_ID = UUID.randomUUID().toString(); private AggregateReport createAttributionReport() { return new AggregateReport.Builder() @@ -47,9 +52,12 @@ public final class AggregateReportTest { .setDebugCleartextPayload(" key: 1369, value: 32768; key: 3461, value: 1664;") .setAggregateAttributionData(new AggregateAttributionData.Builder().build()) .setStatus(AggregateReport.Status.PENDING) + .setDebugReportStatus(AggregateReport.DebugReportStatus.PENDING) .setApiVersion("1452") .setSourceDebugKey(SOURCE_DEBUG_KEY) .setTriggerDebugKey(TRIGGER_DEBUG_KEY) + .setSourceId(SOURCE_ID) + .setTriggerId(TRIGGER_ID) .build(); } @@ -64,8 +72,11 @@ public final class AggregateReportTest { .setDebugCleartextPayload(" key: 1369, value: 32768; key: 3461, value: 1664;") .setAggregateAttributionData(new AggregateAttributionData.Builder().build()) .setStatus(AggregateReport.Status.PENDING) + .setDebugReportStatus(AggregateReport.DebugReportStatus.PENDING) .setApiVersion("1452") .setTriggerDebugKey(TRIGGER_DEBUG_KEY) + .setSourceId(SOURCE_ID) + .setTriggerId(TRIGGER_ID) .build(); } @@ -80,8 +91,11 @@ public final class AggregateReportTest { .setDebugCleartextPayload(" key: 1369, value: 32768; key: 3461, value: 1664;") .setAggregateAttributionData(new AggregateAttributionData.Builder().build()) .setStatus(AggregateReport.Status.PENDING) + .setDebugReportStatus(AggregateReport.DebugReportStatus.PENDING) .setApiVersion("1452") .setSourceDebugKey(SOURCE_DEBUG_KEY) + .setSourceId(SOURCE_ID) + .setTriggerId(TRIGGER_ID) .build(); } @@ -99,13 +113,18 @@ public final class AggregateReportTest { attributionReport.getDebugCleartextPayload()); assertNotNull(attributionReport.getAggregateAttributionData()); assertEquals(AggregateReport.Status.PENDING, attributionReport.getStatus()); + assertEquals( + AggregateReport.DebugReportStatus.PENDING, + attributionReport.getDebugReportStatus()); assertEquals("1452", attributionReport.getApiVersion()); assertEquals(SOURCE_DEBUG_KEY, attributionReport.getSourceDebugKey()); assertEquals(TRIGGER_DEBUG_KEY, attributionReport.getTriggerDebugKey()); + assertEquals(SOURCE_ID, attributionReport.getSourceId()); + assertEquals(TRIGGER_ID, attributionReport.getTriggerId()); } @Test - public void testCreationSingleSourceDebugKey() throws Exception { + public void testCreationSingleSourceDebugKey() { AggregateReport attributionReport = createAttributionReportSingleSourceDebugKey(); assertEquals("1", attributionReport.getId()); assertEquals(Uri.parse("android-app://com.example.abc"), attributionReport.getPublisher()); @@ -119,13 +138,18 @@ public final class AggregateReportTest { attributionReport.getDebugCleartextPayload()); assertNotNull(attributionReport.getAggregateAttributionData()); assertEquals(AggregateReport.Status.PENDING, attributionReport.getStatus()); + assertEquals( + AggregateReport.DebugReportStatus.PENDING, + attributionReport.getDebugReportStatus()); assertEquals("1452", attributionReport.getApiVersion()); assertEquals(SOURCE_DEBUG_KEY, attributionReport.getSourceDebugKey()); assertNull(attributionReport.getTriggerDebugKey()); + assertEquals(SOURCE_ID, attributionReport.getSourceId()); + assertEquals(TRIGGER_ID, attributionReport.getTriggerId()); } @Test - public void testCreationSingleTriggerDebugKey() throws Exception { + public void testCreationSingleTriggerDebugKey() { AggregateReport attributionReport = createAttributionReportSingleTriggerDebugKey(); assertEquals("1", attributionReport.getId()); assertEquals(Uri.parse("android-app://com.example.abc"), attributionReport.getPublisher()); @@ -139,9 +163,14 @@ public final class AggregateReportTest { attributionReport.getDebugCleartextPayload()); assertNotNull(attributionReport.getAggregateAttributionData()); assertEquals(AggregateReport.Status.PENDING, attributionReport.getStatus()); + assertEquals( + AggregateReport.DebugReportStatus.PENDING, + attributionReport.getDebugReportStatus()); assertEquals("1452", attributionReport.getApiVersion()); assertNull(attributionReport.getSourceDebugKey()); assertEquals(TRIGGER_DEBUG_KEY, attributionReport.getTriggerDebugKey()); + assertEquals(SOURCE_ID, attributionReport.getSourceId()); + assertEquals(TRIGGER_ID, attributionReport.getTriggerId()); } @Test @@ -157,9 +186,13 @@ public final class AggregateReportTest { assertNull(attributionReport.getDebugCleartextPayload()); assertNull(attributionReport.getAggregateAttributionData()); assertEquals(AggregateReport.Status.PENDING, attributionReport.getStatus()); + assertEquals( + AggregateReport.DebugReportStatus.NONE, attributionReport.getDebugReportStatus()); assertNull(attributionReport.getApiVersion()); assertNull(attributionReport.getSourceDebugKey()); assertNull(attributionReport.getTriggerDebugKey()); + assertNull(attributionReport.getSourceId()); + assertNull(attributionReport.getTriggerId()); } @Test @@ -188,7 +221,10 @@ public final class AggregateReportTest { " key: 1369, value: 32768; key: 3461, value: 1664;") .setAggregateAttributionData(new AggregateAttributionData.Builder().build()) .setStatus(AggregateReport.Status.PENDING) + .setDebugReportStatus(AggregateReport.DebugReportStatus.PENDING) .setApiVersion("1452") + .setSourceId(SOURCE_ID) + .setTriggerId(TRIGGER_ID) .build(); Set<AggregateReport> attributionReportSet1 = Set.of(attributionReport1); Set<AggregateReport> attributionReportSet2 = Set.of(attributionReport2); diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/aggregation/AggregateTriggerDataTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/aggregation/AggregateTriggerDataTest.java index 4d4c920c8a..731f1554f8 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/aggregation/AggregateTriggerDataTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/aggregation/AggregateTriggerDataTest.java @@ -16,6 +16,8 @@ package com.android.adservices.service.measurement.aggregation; +import com.android.adservices.service.measurement.FilterData; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; @@ -45,8 +47,8 @@ public final class AggregateTriggerDataTest { assertEquals(attributionTriggerData.getKey().longValue(), 5L); assertEquals(attributionTriggerData.getSourceKeys().size(), 3); assertTrue(attributionTriggerData.getFilter().isPresent()); - AggregateFilterData filterData = attributionTriggerData.getFilter().get(); - AggregateFilterData nonFilteredData = attributionTriggerData.getNotFilter().get(); + FilterData filterData = attributionTriggerData.getFilter().get(); + FilterData nonFilteredData = attributionTriggerData.getNotFilter().get(); assertEquals(2, filterData.getAttributionFilterMap().get("ctid").size()); assertEquals(1, nonFilteredData.getAttributionFilterMap().get("nctid").size()); } @@ -78,15 +80,15 @@ public final class AggregateTriggerDataTest { Map<String, List<String>> attributionFilterMap = new HashMap<>(); attributionFilterMap.put("ctid", Arrays.asList("1")); - AggregateFilterData filterData = - new AggregateFilterData.Builder() + FilterData filterData = + new FilterData.Builder() .setAttributionFilterMap(attributionFilterMap) .build(); Map<String, List<String>> attributionNonFilterMap = new HashMap<>(); attributionNonFilterMap.put("other", Arrays.asList("1")); - AggregateFilterData nonFilterData = - new AggregateFilterData.Builder() + FilterData nonFilterData = + new FilterData.Builder() .setAttributionFilterMap(attributionNonFilterMap) .build(); @@ -110,15 +112,15 @@ public final class AggregateTriggerDataTest { private AggregateTriggerData createExample() { Map<String, List<String>> attributionFilterMap = new HashMap<>(); attributionFilterMap.put("ctid", Arrays.asList("1", "2")); - AggregateFilterData filterData = - new AggregateFilterData.Builder() + FilterData filterData = + new FilterData.Builder() .setAttributionFilterMap(attributionFilterMap) .build(); Map<String, List<String>> attributionNonFilterMap = new HashMap<>(); attributionNonFilterMap.put("nctid", Arrays.asList("3")); - AggregateFilterData nonFilterData = - new AggregateFilterData.Builder() + FilterData nonFilterData = + new FilterData.Builder() .setAttributionFilterMap(attributionNonFilterMap) .build(); diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/attribution/AttributionJobHandlerIntegrationTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/attribution/AttributionJobHandlerIntegrationTest.java index 8948dad655..936bb18df1 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/attribution/AttributionJobHandlerIntegrationTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/attribution/AttributionJobHandlerIntegrationTest.java @@ -16,10 +16,11 @@ package com.android.adservices.service.measurement.attribution; +import com.android.adservices.data.DbTestUtil; import com.android.adservices.data.measurement.AbstractDbIntegrationTest; import com.android.adservices.data.measurement.DatastoreManager; -import com.android.adservices.data.measurement.DatastoreManagerFactory; import com.android.adservices.data.measurement.DbState; +import com.android.adservices.data.measurement.SQLDatastoreManager; import org.json.JSONException; import org.junit.Assert; @@ -51,7 +52,8 @@ public class AttributionJobHandlerIntegrationTest extends AbstractDbIntegrationT @Override public void runActionToTest() { - DatastoreManager datastoreManager = DatastoreManagerFactory.getDatastoreManager(sContext); + DatastoreManager datastoreManager = + new SQLDatastoreManager(DbTestUtil.getDbHelperForTest()); Assert.assertTrue("Attribution failed.", (new AttributionJobHandler(datastoreManager)) .performPendingAttributions()); diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/attribution/AttributionJobHandlerTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/attribution/AttributionJobHandlerTest.java index 95b8388675..0edcefdc81 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/attribution/AttributionJobHandlerTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/attribution/AttributionJobHandlerTest.java @@ -16,6 +16,8 @@ package com.android.adservices.service.measurement.attribution; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; @@ -43,11 +45,14 @@ import com.android.adservices.service.measurement.Source; import com.android.adservices.service.measurement.SourceFixture; import com.android.adservices.service.measurement.Trigger; import com.android.adservices.service.measurement.TriggerFixture; +import com.android.adservices.service.measurement.aggregation.AggregateAttributionData; +import com.android.adservices.service.measurement.aggregation.AggregateHistogramContribution; +import com.android.adservices.service.measurement.aggregation.AggregateReport; +import com.android.adservices.service.measurement.util.UnsignedLong; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -55,6 +60,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -70,6 +76,8 @@ public class AttributionJobHandlerTest { private static final Context sContext = ApplicationProvider.getApplicationContext(); private static final Uri APP_DESTINATION = Uri.parse("android-app://com.example.app"); private static final Uri PUBLISHER = Uri.parse("android-app://publisher.app"); + private static final UnsignedLong SOURCE_DEBUG_KEY = new UnsignedLong(111111L); + private static final UnsignedLong TRIGGER_DEBUG_KEY = new UnsignedLong(222222L); private static final String EVENT_TRIGGERS = "[\n" + "{\n" @@ -128,7 +136,7 @@ public class AttributionJobHandlerTest { AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); attributionService.performPendingAttributions(); verify(mMeasurementDao).getTrigger(trigger.getId()); - verify(mMeasurementDao, never()).updateTriggerStatus(any()); + verify(mMeasurementDao, never()).updateTriggerStatus(any(), anyInt()); verify(mMeasurementDao, never()).insertEventReport(any()); verify(mTransaction, times(2)).begin(); verify(mTransaction, times(2)).end(); @@ -143,9 +151,9 @@ public class AttributionJobHandlerTest { when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(new ArrayList<>()); AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); attributionService.performPendingAttributions(); - ArgumentCaptor<Trigger> triggerArg = ArgumentCaptor.forClass(Trigger.class); - verify(mMeasurementDao).updateTriggerStatus(triggerArg.capture()); - Assert.assertEquals(Trigger.Status.IGNORED, triggerArg.getValue().getStatus()); + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); verify(mMeasurementDao, never()).insertEventReport(any()); verify(mTransaction, times(2)).begin(); verify(mTransaction, times(2)).end(); @@ -171,7 +179,7 @@ public class AttributionJobHandlerTest { + "]\n") .build(); Source source = SourceFixture.getValidSourceBuilder() - .setDedupKeys(Arrays.asList(1L, 2L)) + .setDedupKeys(Arrays.asList(new UnsignedLong(1L), new UnsignedLong(2L))) .setAttributionMode(Source.AttributionMode.TRUTHFULLY) .build(); List<Source> matchingSourceList = new ArrayList<>(); @@ -183,9 +191,9 @@ public class AttributionJobHandlerTest { when(mMeasurementDao.getAttributionsPerRateLimitWindow(any(), any())).thenReturn(5L); AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); attributionService.performPendingAttributions(); - ArgumentCaptor<Trigger> triggerArg = ArgumentCaptor.forClass(Trigger.class); - verify(mMeasurementDao).updateTriggerStatus(triggerArg.capture()); - Assert.assertEquals(Trigger.Status.IGNORED, triggerArg.getValue().getStatus()); + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); verify(mMeasurementDao, never()).insertEventReport(any()); verify(mTransaction, times(2)).begin(); verify(mTransaction, times(2)).end(); @@ -207,9 +215,9 @@ public class AttributionJobHandlerTest { AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); attributionService.performPendingAttributions(); verify(mMeasurementDao).getAttributionsPerRateLimitWindow(source, trigger); - ArgumentCaptor<Trigger> triggerArg = ArgumentCaptor.forClass(Trigger.class); - verify(mMeasurementDao).updateTriggerStatus(triggerArg.capture()); - Assert.assertEquals(Trigger.Status.IGNORED, triggerArg.getValue().getStatus()); + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); verify(mMeasurementDao, never()).insertEventReport(any()); verify(mTransaction, times(2)).begin(); verify(mTransaction, times(2)).end(); @@ -231,11 +239,12 @@ public class AttributionJobHandlerTest { any(), any(), any(), anyLong(), anyLong())).thenReturn(10); AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); attributionService.performPendingAttributions(); - verify(mMeasurementDao).countDistinctEnrollmentsPerPublisherXDestinationInAttribution( - any(), any(), any(), anyLong(), anyLong()); - ArgumentCaptor<Trigger> triggerArg = ArgumentCaptor.forClass(Trigger.class); - verify(mMeasurementDao).updateTriggerStatus(triggerArg.capture()); - Assert.assertEquals(Trigger.Status.IGNORED, triggerArg.getValue().getStatus()); + verify(mMeasurementDao) + .countDistinctEnrollmentsPerPublisherXDestinationInAttribution( + any(), any(), any(), anyLong(), anyLong()); + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); verify(mMeasurementDao, never()).insertEventReport(any()); verify(mTransaction, times(2)).begin(); verify(mTransaction, times(2)).end(); @@ -269,9 +278,9 @@ public class AttributionJobHandlerTest { when(mMeasurementDao.getAttributionsPerRateLimitWindow(any(), any())).thenReturn(5L); AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); attributionService.performPendingAttributions(); - ArgumentCaptor<Trigger> triggerArg = ArgumentCaptor.forClass(Trigger.class); - verify(mMeasurementDao).updateTriggerStatus(triggerArg.capture()); - Assert.assertEquals(Trigger.Status.IGNORED, triggerArg.getValue().getStatus()); + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); verify(mMeasurementDao, never()).insertEventReport(any()); verify(mTransaction, times(2)).begin(); verify(mTransaction, times(2)).end(); @@ -321,9 +330,9 @@ public class AttributionJobHandlerTest { when(mMeasurementDao.getAttributionsPerRateLimitWindow(any(), any())).thenReturn(5L); AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); attributionService.performPendingAttributions(); - ArgumentCaptor<Trigger> triggerArg = ArgumentCaptor.forClass(Trigger.class); - verify(mMeasurementDao).updateTriggerStatus(triggerArg.capture()); - Assert.assertEquals(Trigger.Status.IGNORED, triggerArg.getValue().getStatus()); + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); verify(mMeasurementDao, never()).insertEventReport(any()); verify(mTransaction, times(2)).begin(); verify(mTransaction, times(2)).end(); @@ -345,12 +354,15 @@ public class AttributionJobHandlerTest { when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); attributionService.performPendingAttributions(); - ArgumentCaptor<Trigger> triggerArg = ArgumentCaptor.forClass(Trigger.class); - verify(mMeasurementDao).updateTriggerStatus(triggerArg.capture()); - Assert.assertEquals(Trigger.Status.ATTRIBUTED, triggerArg.getValue().getStatus()); + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), + eq(Trigger.Status.ATTRIBUTED)); ArgumentCaptor<Source> sourceArg = ArgumentCaptor.forClass(Source.class); verify(mMeasurementDao).updateSourceDedupKeys(sourceArg.capture()); - Assert.assertEquals(sourceArg.getValue().getDedupKeys(), Collections.singletonList(2L)); + assertEquals( + sourceArg.getValue().getDedupKeys(), + Collections.singletonList(new UnsignedLong(2L))); verify(mMeasurementDao).insertEventReport(any()); verify(mTransaction, times(2)).begin(); verify(mTransaction, times(2)).end(); @@ -375,16 +387,20 @@ public class AttributionJobHandlerTest { .setStatus(Trigger.Status.PENDING) .setEventTriggers(eventTriggers) .build(); - Source source1 = SourceFixture.getValidSourceBuilder() - .setPriority(100L) - .setAttributionMode(Source.AttributionMode.TRUTHFULLY) - .setEventTime(1L) - .build(); - Source source2 = SourceFixture.getValidSourceBuilder() - .setPriority(200L) - .setAttributionMode(Source.AttributionMode.TRUTHFULLY) - .setEventTime(2L) - .build(); + Source source1 = + SourceFixture.getValidSourceBuilder() + .setId("source1") + .setPriority(100L) + .setAttributionMode(Source.AttributionMode.TRUTHFULLY) + .setEventTime(1L) + .build(); + Source source2 = + SourceFixture.getValidSourceBuilder() + .setId("source2") + .setPriority(200L) + .setAttributionMode(Source.AttributionMode.TRUTHFULLY) + .setEventTime(2L) + .build(); when(mMeasurementDao.getPendingTriggerIds()) .thenReturn(Collections.singletonList(trigger.getId())); when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); @@ -396,14 +412,18 @@ public class AttributionJobHandlerTest { AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); attributionService.performPendingAttributions(); trigger.setStatus(Trigger.Status.ATTRIBUTED); - verify(mMeasurementDao).updateSourceStatus(matchingSourceList, Source.Status.IGNORED); - Assert.assertEquals(1, matchingSourceList.size()); - ArgumentCaptor<Trigger> triggerArg = ArgumentCaptor.forClass(Trigger.class); - verify(mMeasurementDao).updateTriggerStatus(triggerArg.capture()); - Assert.assertEquals(Trigger.Status.ATTRIBUTED, triggerArg.getValue().getStatus()); + verify(mMeasurementDao) + .updateSourceStatus(eq(List.of(source1.getId())), eq(Source.Status.IGNORED)); + assertEquals(1, matchingSourceList.size()); + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), + eq(Trigger.Status.ATTRIBUTED)); ArgumentCaptor<Source> sourceArg = ArgumentCaptor.forClass(Source.class); verify(mMeasurementDao).updateSourceDedupKeys(sourceArg.capture()); - Assert.assertEquals(sourceArg.getValue().getDedupKeys(), Collections.singletonList(2L)); + assertEquals( + sourceArg.getValue().getDedupKeys(), + Collections.singletonList(new UnsignedLong(2L))); verify(mMeasurementDao).insertEventReport(any()); verify(mTransaction, times(2)).begin(); verify(mTransaction, times(2)).end(); @@ -466,9 +486,10 @@ public class AttributionJobHandlerTest { AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); attributionService.performPendingAttributions(); verify(mMeasurementDao).deleteEventReport(eventReport1); - ArgumentCaptor<Trigger> triggerArg = ArgumentCaptor.forClass(Trigger.class); - verify(mMeasurementDao).updateTriggerStatus(triggerArg.capture()); - Assert.assertEquals(Trigger.Status.ATTRIBUTED, triggerArg.getValue().getStatus()); + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), + eq(Trigger.Status.ATTRIBUTED)); verify(mMeasurementDao).insertEventReport(any()); verify(mTransaction, times(2)).begin(); verify(mTransaction, times(2)).end(); @@ -500,7 +521,7 @@ public class AttributionJobHandlerTest { verify(mMeasurementDao).getTrigger(anyString()); verify(mMeasurementDao).getMatchingActiveSources(any()); verify(mMeasurementDao).getAttributionsPerRateLimitWindow(any(), any()); - verify(mMeasurementDao, never()).updateTriggerStatus(any()); + verify(mMeasurementDao, never()).updateTriggerStatus(any(), anyInt()); verify(mMeasurementDao, never()).insertEventReport(any()); verify(mTransaction, times(2)).begin(); verify(mTransaction).rollback(); @@ -554,22 +575,16 @@ public class AttributionJobHandlerTest { when(mMeasurementDao.getMatchingActiveSources(trigger2)).thenReturn(matchingSourceList2); when(mMeasurementDao.getAttributionsPerRateLimitWindow(any(), any())).thenReturn(5L); AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); - Assert.assertTrue(attributionService.performPendingAttributions()); + assertTrue(attributionService.performPendingAttributions()); // Verify trigger status updates. - ArgumentCaptor<Trigger> triggerArg = ArgumentCaptor.forClass(Trigger.class); - verify(mMeasurementDao, times(2)).updateTriggerStatus(triggerArg.capture()); - List<Trigger> statusArgs = triggerArg.getAllValues(); - for (int i = 0; i < statusArgs.size(); i++) { - Assert.assertEquals(Trigger.Status.ATTRIBUTED, statusArgs.get(i).getStatus()); - Assert.assertEquals(triggers.get(i).getId(), statusArgs.get(i).getId()); - } + verify(mMeasurementDao, times(2)).updateTriggerStatus(any(), eq(Trigger.Status.ATTRIBUTED)); // Verify source dedup key updates. ArgumentCaptor<Source> sourceArg = ArgumentCaptor.forClass(Source.class); verify(mMeasurementDao, times(2)) .updateSourceDedupKeys(sourceArg.capture()); List<Source> dedupArgs = sourceArg.getAllValues(); for (int i = 0; i < dedupArgs.size(); i++) { - Assert.assertEquals( + assertEquals( dedupArgs.get(i).getDedupKeys(), Collections.singletonList( triggers.get(i).parseEventTriggers().get(0).getDedupKey())); @@ -580,7 +595,7 @@ public class AttributionJobHandlerTest { .insertEventReport(reportArg.capture()); List<EventReport> newReportArgs = reportArg.getAllValues(); for (int i = 0; i < newReportArgs.size(); i++) { - Assert.assertEquals( + assertEquals( newReportArgs.get(i).getTriggerDedupKey(), triggers.get(i).parseEventTriggers().get(0).getDedupKey()); } @@ -591,6 +606,7 @@ public class AttributionJobHandlerTest { long eventTime = System.currentTimeMillis(); Trigger trigger = TriggerFixture.getValidTriggerBuilder() + .setId("trigger1") .setStatus(Trigger.Status.PENDING) .setTriggerTime(eventTime + TimeUnit.DAYS.toMillis(5)) .setEventTriggers( @@ -603,20 +619,24 @@ public class AttributionJobHandlerTest { + "]\n") .build(); // Lower priority and older priority source. - Source source1 = SourceFixture.getValidSourceBuilder() - .setEventId(1) - .setPriority(100L) - .setAttributionMode(Source.AttributionMode.TRUTHFULLY) - .setInstallAttributed(true) - .setInstallCooldownWindow(TimeUnit.DAYS.toMillis(10)) - .setEventTime(eventTime - TimeUnit.DAYS.toMillis(2)) - .build(); - Source source2 = SourceFixture.getValidSourceBuilder() - .setEventId(2) - .setPriority(200L) - .setAttributionMode(Source.AttributionMode.TRUTHFULLY) - .setEventTime(eventTime) - .build(); + Source source1 = + SourceFixture.getValidSourceBuilder() + .setId("source1") + .setEventId(new UnsignedLong(1L)) + .setPriority(100L) + .setAttributionMode(Source.AttributionMode.TRUTHFULLY) + .setInstallAttributed(true) + .setInstallCooldownWindow(TimeUnit.DAYS.toMillis(10)) + .setEventTime(eventTime - TimeUnit.DAYS.toMillis(2)) + .build(); + Source source2 = + SourceFixture.getValidSourceBuilder() + .setId("source2") + .setEventId(new UnsignedLong(2L)) + .setPriority(200L) + .setAttributionMode(Source.AttributionMode.TRUTHFULLY) + .setEventTime(eventTime) + .build(); when(mMeasurementDao.getPendingTriggerIds()) .thenReturn(Collections.singletonList(trigger.getId())); when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); @@ -628,24 +648,30 @@ public class AttributionJobHandlerTest { AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); attributionService.performPendingAttributions(); trigger.setStatus(Trigger.Status.ATTRIBUTED); - verify(mMeasurementDao).updateSourceStatus(matchingSourceList, Source.Status.IGNORED); - Assert.assertEquals(1, matchingSourceList.size()); - Assert.assertEquals(source2.getEventId(), matchingSourceList.get(0).getEventId()); - ArgumentCaptor<Trigger> triggerArg = ArgumentCaptor.forClass(Trigger.class); - verify(mMeasurementDao).updateTriggerStatus(triggerArg.capture()); - Assert.assertEquals(Trigger.Status.ATTRIBUTED, triggerArg.getValue().getStatus()); + verify(mMeasurementDao) + .updateSourceStatus(eq(List.of(source2.getId())), eq(Source.Status.IGNORED)); + assertEquals(1, matchingSourceList.size()); + assertEquals(source2.getEventId(), matchingSourceList.get(0).getEventId()); + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), + eq(Trigger.Status.ATTRIBUTED)); ArgumentCaptor<Source> sourceArg = ArgumentCaptor.forClass(Source.class); verify(mMeasurementDao).updateSourceDedupKeys(sourceArg.capture()); - Assert.assertEquals(source1.getEventId(), sourceArg.getValue().getEventId()); - Assert.assertEquals(sourceArg.getValue().getDedupKeys(), Collections.singletonList(2L)); + assertEquals(source1.getEventId(), sourceArg.getValue().getEventId()); + assertEquals( + sourceArg.getValue().getDedupKeys(), + Collections.singletonList(new UnsignedLong(2L))); verify(mMeasurementDao).insertEventReport(any()); } @Test public void shouldNotAttributeToOldInstallAttributedSource() throws DatastoreException { + // Setup long eventTime = System.currentTimeMillis(); Trigger trigger = TriggerFixture.getValidTriggerBuilder() + .setId("trigger1") .setStatus(Trigger.Status.PENDING) .setTriggerTime(eventTime + TimeUnit.DAYS.toMillis(10)) .setEventTriggers( @@ -658,20 +684,24 @@ public class AttributionJobHandlerTest { + "]\n") .build(); // Lower Priority. Install cooldown Window passed. - Source source1 = SourceFixture.getValidSourceBuilder() - .setEventId(1) - .setPriority(100L) - .setAttributionMode(Source.AttributionMode.TRUTHFULLY) - .setInstallAttributed(true) - .setInstallCooldownWindow(TimeUnit.DAYS.toMillis(3)) - .setEventTime(eventTime - TimeUnit.DAYS.toMillis(2)) - .build(); - Source source2 = SourceFixture.getValidSourceBuilder() - .setEventId(2) - .setPriority(200L) - .setAttributionMode(Source.AttributionMode.TRUTHFULLY) - .setEventTime(eventTime) - .build(); + Source source1 = + SourceFixture.getValidSourceBuilder() + .setId("source1") + .setEventId(new UnsignedLong(1L)) + .setPriority(100L) + .setAttributionMode(Source.AttributionMode.TRUTHFULLY) + .setInstallAttributed(true) + .setInstallCooldownWindow(TimeUnit.DAYS.toMillis(3)) + .setEventTime(eventTime - TimeUnit.DAYS.toMillis(2)) + .build(); + Source source2 = + SourceFixture.getValidSourceBuilder() + .setId("source2") + .setEventId(new UnsignedLong(2L)) + .setPriority(200L) + .setAttributionMode(Source.AttributionMode.TRUTHFULLY) + .setEventTime(eventTime) + .build(); when(mMeasurementDao.getPendingTriggerIds()) .thenReturn(Collections.singletonList(trigger.getId())); when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); @@ -681,18 +711,25 @@ public class AttributionJobHandlerTest { when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); when(mMeasurementDao.getAttributionsPerRateLimitWindow(any(), any())).thenReturn(5L); AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); + // Execution attributionService.performPendingAttributions(); trigger.setStatus(Trigger.Status.ATTRIBUTED); - verify(mMeasurementDao).updateSourceStatus(matchingSourceList, Source.Status.IGNORED); - Assert.assertEquals(1, matchingSourceList.size()); - Assert.assertEquals(source1.getEventId(), matchingSourceList.get(0).getEventId()); - ArgumentCaptor<Trigger> triggerArg = ArgumentCaptor.forClass(Trigger.class); - verify(mMeasurementDao).updateTriggerStatus(triggerArg.capture()); - Assert.assertEquals(Trigger.Status.ATTRIBUTED, triggerArg.getValue().getStatus()); + + // Assertion + verify(mMeasurementDao) + .updateSourceStatus(eq(List.of(source1.getId())), eq(Source.Status.IGNORED)); + assertEquals(1, matchingSourceList.size()); + assertEquals(source1.getEventId(), matchingSourceList.get(0).getEventId()); + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), + eq(Trigger.Status.ATTRIBUTED)); ArgumentCaptor<Source> sourceArg = ArgumentCaptor.forClass(Source.class); verify(mMeasurementDao).updateSourceDedupKeys(sourceArg.capture()); - Assert.assertEquals(source2.getEventId(), sourceArg.getValue().getEventId()); - Assert.assertEquals(sourceArg.getValue().getDedupKeys(), Collections.singletonList(2L)); + assertEquals(source2.getEventId(), sourceArg.getValue().getEventId()); + assertEquals( + sourceArg.getValue().getDedupKeys(), + Collections.singletonList(new UnsignedLong(2L))); verify(mMeasurementDao).insertEventReport(any()); } @@ -724,9 +761,9 @@ public class AttributionJobHandlerTest { when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); attributionService.performPendingAttributions(); - ArgumentCaptor<Trigger> triggerArg = ArgumentCaptor.forClass(Trigger.class); - verify(mMeasurementDao).updateTriggerStatus(triggerArg.capture()); - Assert.assertEquals(Trigger.Status.IGNORED, triggerArg.getValue().getStatus()); + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); verify(mMeasurementDao, never()).updateSourceDedupKeys(any()); verify(mMeasurementDao, never()).insertEventReport(any()); verify(mTransaction, times(2)).begin(); @@ -761,9 +798,9 @@ public class AttributionJobHandlerTest { when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); attributionService.performPendingAttributions(); - ArgumentCaptor<Trigger> triggerArg = ArgumentCaptor.forClass(Trigger.class); - verify(mMeasurementDao).updateTriggerStatus(triggerArg.capture()); - Assert.assertEquals(Trigger.Status.IGNORED, triggerArg.getValue().getStatus()); + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); verify(mMeasurementDao, never()).updateSourceDedupKeys(any()); verify(mMeasurementDao, never()).insertEventReport(any()); verify(mTransaction, times(2)).begin(); @@ -799,12 +836,41 @@ public class AttributionJobHandlerTest { .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") .build(); - Source source = SourceFixture.getValidSourceBuilder() - .setAttributionMode(Source.AttributionMode.TRUTHFULLY) - .setAggregateSource("[{\"id\" : \"campaignCounts\", \"key_piece\" : \"0x159\"}," - + "{\"id\" : \"geoValue\", \"key_piece\" : \"0x5\"}]") - .setAggregateFilterData("{\"product\":[\"1234\",\"2345\"]}") - .build(); + Source source = + SourceFixture.getValidSourceBuilder() + .setId("sourceId1") + .setAttributionMode(Source.AttributionMode.TRUTHFULLY) + .setAggregateSource( + "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") + .setFilterData("{\"product\":[\"1234\",\"2345\"]}") + .build(); + AggregateReport expectedAggregateReport = + new AggregateReport.Builder() + .setApiVersion("0.1") + .setAttributionDestination(trigger.getAttributionDestination()) + .setDebugCleartextPayload( + "{\"operation\":\"histogram\"," + + "\"data\":[{\"bucket\":2693,\"value\":1644},{\"bucket\":1369," + + "\"value\":32768}]}") + .setEnrollmentId(source.getEnrollmentId()) + .setPublisher(source.getRegistrant()) + .setSourceId(source.getId()) + .setTriggerId(trigger.getId()) + .setAggregateAttributionData( + new AggregateAttributionData.Builder() + .setContributions( + Arrays.asList( + new AggregateHistogramContribution.Builder() + .setKey(new BigInteger("2693")) + .setValue(1644) + .build(), + new AggregateHistogramContribution.Builder() + .setKey(new BigInteger("1369")) + .setValue(32768) + .build())) + .build()) + .build(); + when(mMeasurementDao.getPendingTriggerIds()) .thenReturn(Collections.singletonList(trigger.getId())); when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); @@ -816,8 +882,12 @@ public class AttributionJobHandlerTest { attributionService.performPendingAttributions(); ArgumentCaptor<Source> sourceArg = ArgumentCaptor.forClass(Source.class); verify(mMeasurementDao).updateSourceAggregateContributions(sourceArg.capture()); - verify(mMeasurementDao).insertAggregateReport(any()); - Assert.assertEquals(sourceArg.getValue().getAggregateContributions(), 32768 + 1644); + ArgumentCaptor<AggregateReport> aggregateReportCaptor = + ArgumentCaptor.forClass(AggregateReport.class); + verify(mMeasurementDao).insertAggregateReport(aggregateReportCaptor.capture()); + + assertAggregateReportsEqual(expectedAggregateReport, aggregateReportCaptor.getValue()); + assertEquals(sourceArg.getValue().getAggregateContributions(), 32768 + 1644); } @Test @@ -842,13 +912,14 @@ public class AttributionJobHandlerTest { .setEventTriggers(EVENT_TRIGGERS) .build(); - Source source = SourceFixture.getValidSourceBuilder() - .setAttributionMode(Source.AttributionMode.TRUTHFULLY) - .setAggregateSource("[{\"id\" : \"campaignCounts\", \"key_piece\" : \"0x159\"}," - + "{\"id\" : \"geoValue\", \"key_piece\" : \"0x5\"}]") - .setAggregateFilterData("{\"product\":[\"1234\",\"2345\"]}") - .setAggregateContributions(65536 - 32768 - 1644 + 1) - .build(); + Source source = + SourceFixture.getValidSourceBuilder() + .setAttributionMode(Source.AttributionMode.TRUTHFULLY) + .setAggregateSource( + "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") + .setFilterData("{\"product\":[\"1234\",\"2345\"]}") + .setAggregateContributions(65536 - 32768 - 1644 + 1) + .build(); when(mMeasurementDao.getPendingTriggerIds()) .thenReturn(Collections.singletonList(trigger.getId())); when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); @@ -887,7 +958,7 @@ public class AttributionJobHandlerTest { Source source = SourceFixture.getValidSourceBuilder() .setAttributionMode(Source.AttributionMode.TRUTHFULLY) - .setAggregateFilterData( + .setFilterData( "{\n" + " \"key_1\": [\"value_1_x\", \"value_2_x\"],\n" + " \"key_2\": [\"value_1_x\", \"value_2_x\"]\n" @@ -907,9 +978,63 @@ public class AttributionJobHandlerTest { attributionService.performPendingAttributions(); // Assertions - ArgumentCaptor<Trigger> triggerArg = ArgumentCaptor.forClass(Trigger.class); - verify(mMeasurementDao).updateTriggerStatus(triggerArg.capture()); - Assert.assertEquals(Trigger.Status.IGNORED, triggerArg.getValue().getStatus()); + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); + verify(mMeasurementDao, never()).updateSourceDedupKeys(any()); + verify(mMeasurementDao, never()).insertEventReport(any()); + verify(mTransaction, times(2)).begin(); + verify(mTransaction, times(2)).end(); + } + + @Test + public void performAttributions_triggerNotFiltersWithCommonKeysIntersect_ignoreTrigger() + throws DatastoreException { + // Setup + Trigger trigger = + TriggerFixture.getValidTriggerBuilder() + .setId("triggerId1") + .setStatus(Trigger.Status.PENDING) + .setEventTriggers( + "[\n" + + "{\n" + + " \"trigger_data\": \"5\",\n" + + " \"priority\": \"123\",\n" + + " \"deduplication_key\": \"1\"\n" + + "}" + + "]\n") + .setNotFilters( + "{\n" + + " \"key_1\": [\"value_1\", \"value_2\"],\n" + + " \"key_2\": [\"value_1\", \"value_2\"]\n" + + "}\n") + .build(); + Source source = + SourceFixture.getValidSourceBuilder() + .setAttributionMode(Source.AttributionMode.TRUTHFULLY) + .setFilterData( + "{\n" + + " \"key_1\": [\"value_1\", \"value_2_x\"],\n" + + " \"key_2\": [\"value_1_x\", \"value_2\"]\n" + + "}\n") + .build(); + when(mMeasurementDao.getPendingTriggerIds()) + .thenReturn(Collections.singletonList(trigger.getId())); + when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); + List<Source> matchingSourceList = new ArrayList<>(); + matchingSourceList.add(source); + when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); + when(mMeasurementDao.getAttributionsPerRateLimitWindow(any(), any())).thenReturn(5L); + when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); + AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); + + // Execution + attributionService.performPendingAttributions(); + + // Assertions + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); verify(mMeasurementDao, never()).updateSourceDedupKeys(any()); verify(mMeasurementDao, never()).insertEventReport(any()); verify(mTransaction, times(2)).begin(); @@ -917,6 +1042,65 @@ public class AttributionJobHandlerTest { } @Test + public void performAttributions_triggerNotFiltersWithCommonKeysDontIntersect_attributeTrigger() + throws DatastoreException { + // Setup + Trigger trigger = + TriggerFixture.getValidTriggerBuilder() + .setId("triggerId1") + .setStatus(Trigger.Status.PENDING) + .setEventTriggers( + "[\n" + + "{\n" + + " \"trigger_data\": \"5\",\n" + + " \"priority\": \"123\",\n" + + " \"deduplication_key\": \"1\"\n" + + "}" + + "]\n") + .setNotFilters( + "{\n" + + " \"key_1\": [\"value_11_x\", \"value_12\"],\n" + + " \"key_2\": [\"value_21\", \"value_22_x\"]\n" + + "}\n") + .build(); + Source source = + SourceFixture.getValidSourceBuilder() + .setAttributionMode(Source.AttributionMode.TRUTHFULLY) + .setFilterData( + "{\n" + + " \"key_1\": [\"value_11\", \"value_12_x\"],\n" + + " \"key_2\": [\"value_21_x\", \"value_22\"]\n" + + "}\n") + .build(); + when(mMeasurementDao.getPendingTriggerIds()) + .thenReturn(Collections.singletonList(trigger.getId())); + when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); + List<Source> matchingSourceList = new ArrayList<>(); + matchingSourceList.add(source); + when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); + when(mMeasurementDao.getAttributionsPerRateLimitWindow(any(), any())).thenReturn(5L); + when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); + AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); + + // Execution + attributionService.performPendingAttributions(); + + // Assertions + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), + eq(Trigger.Status.ATTRIBUTED)); + ArgumentCaptor<Source> sourceArg = ArgumentCaptor.forClass(Source.class); + verify(mMeasurementDao).updateSourceDedupKeys(sourceArg.capture()); + assertEquals( + sourceArg.getValue().getDedupKeys(), + Collections.singletonList(new UnsignedLong(1L))); + verify(mMeasurementDao).insertEventReport(any()); + verify(mTransaction, times(2)).begin(); + verify(mTransaction, times(2)).end(); + } + + @Test public void performAttributions_triggerSourceFiltersWithCommonKeysIntersect_attributeTrigger() throws DatastoreException { // Setup @@ -941,7 +1125,7 @@ public class AttributionJobHandlerTest { Source source = SourceFixture.getValidSourceBuilder() .setAttributionMode(Source.AttributionMode.TRUTHFULLY) - .setAggregateFilterData( + .setFilterData( "{\n" + " \"key_1\": [\"value_11\", \"value_12_x\"],\n" + " \"key_2\": [\"value_21_x\", \"value_22\"]\n" @@ -961,12 +1145,198 @@ public class AttributionJobHandlerTest { attributionService.performPendingAttributions(); // Assertions - ArgumentCaptor<Trigger> triggerArg = ArgumentCaptor.forClass(Trigger.class); - verify(mMeasurementDao).updateTriggerStatus(triggerArg.capture()); - Assert.assertEquals(Trigger.Status.ATTRIBUTED, triggerArg.getValue().getStatus()); + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), + eq(Trigger.Status.ATTRIBUTED)); ArgumentCaptor<Source> sourceArg = ArgumentCaptor.forClass(Source.class); verify(mMeasurementDao).updateSourceDedupKeys(sourceArg.capture()); - Assert.assertEquals(sourceArg.getValue().getDedupKeys(), Collections.singletonList(1L)); + assertEquals( + sourceArg.getValue().getDedupKeys(), + Collections.singletonList(new UnsignedLong(1L))); + verify(mMeasurementDao).insertEventReport(any()); + verify(mTransaction, times(2)).begin(); + verify(mTransaction, times(2)).end(); + } + + @Test + public void performAttributions_commonKeysIntersect_attributeTrigger_debugApi() + throws DatastoreException { + // Setup + Trigger trigger = + TriggerFixture.getValidTriggerBuilder() + .setId("triggerId1") + .setStatus(Trigger.Status.PENDING) + .setEventTriggers( + "[\n" + + "{\n" + + " \"trigger_data\": \"5\",\n" + + " \"priority\": \"123\",\n" + + " \"deduplication_key\": \"1\"\n" + + "}" + + "]\n") + .setFilters( + "{\n" + + " \"key_1\": [\"value_11\", \"value_12\"],\n" + + " \"key_2\": [\"value_21\", \"value_22\"]\n" + + "}\n") + .setDebugKey(TRIGGER_DEBUG_KEY) + .build(); + Source source = + SourceFixture.getValidSourceBuilder() + .setAttributionMode(Source.AttributionMode.TRUTHFULLY) + .setFilterData( + "{\n" + + " \"key_1\": [\"value_11\", \"value_12_x\"],\n" + + " \"key_2\": [\"value_21_x\", \"value_22\"]\n" + + "}\n") + .setDebugKey(SOURCE_DEBUG_KEY) + .build(); + when(mMeasurementDao.getPendingTriggerIds()) + .thenReturn(Collections.singletonList(trigger.getId())); + when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); + List<Source> matchingSourceList = new ArrayList<>(); + matchingSourceList.add(source); + when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); + when(mMeasurementDao.getAttributionsPerRateLimitWindow(any(), any())).thenReturn(5L); + when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); + AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); + + // Execution + attributionService.performPendingAttributions(); + + // Assertions + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), + eq(Trigger.Status.ATTRIBUTED)); + ArgumentCaptor<Source> sourceArg = ArgumentCaptor.forClass(Source.class); + verify(mMeasurementDao).updateSourceDedupKeys(sourceArg.capture()); + assertEquals( + sourceArg.getValue().getDedupKeys(), + Collections.singletonList(new UnsignedLong(1L))); + assertEquals(sourceArg.getValue().getDebugKey(), SOURCE_DEBUG_KEY); + verify(mMeasurementDao).insertEventReport(any()); + verify(mTransaction, times(2)).begin(); + verify(mTransaction, times(2)).end(); + } + + @Test + public void performAttributions_commonKeysIntersect_attributeTrigger_debugApi_sourceKey() + throws DatastoreException { + // Setup + Trigger trigger = + TriggerFixture.getValidTriggerBuilder() + .setId("triggerId1") + .setStatus(Trigger.Status.PENDING) + .setEventTriggers( + "[\n" + + "{\n" + + " \"trigger_data\": \"5\",\n" + + " \"priority\": \"123\",\n" + + " \"deduplication_key\": \"1\"\n" + + "}" + + "]\n") + .setFilters( + "{\n" + + " \"key_1\": [\"value_11\", \"value_12\"],\n" + + " \"key_2\": [\"value_21\", \"value_22\"]\n" + + "}\n") + .build(); + Source source = + SourceFixture.getValidSourceBuilder() + .setAttributionMode(Source.AttributionMode.TRUTHFULLY) + .setFilterData( + "{\n" + + " \"key_1\": [\"value_11\", \"value_12_x\"],\n" + + " \"key_2\": [\"value_21_x\", \"value_22\"]\n" + + "}\n") + .setDebugKey(SOURCE_DEBUG_KEY) + .build(); + when(mMeasurementDao.getPendingTriggerIds()) + .thenReturn(Collections.singletonList(trigger.getId())); + when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); + List<Source> matchingSourceList = new ArrayList<>(); + matchingSourceList.add(source); + when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); + when(mMeasurementDao.getAttributionsPerRateLimitWindow(any(), any())).thenReturn(5L); + when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); + AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); + + // Execution + attributionService.performPendingAttributions(); + + // Assertions + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), + eq(Trigger.Status.ATTRIBUTED)); + ArgumentCaptor<Source> sourceArg = ArgumentCaptor.forClass(Source.class); + verify(mMeasurementDao).updateSourceDedupKeys(sourceArg.capture()); + assertEquals( + sourceArg.getValue().getDedupKeys(), + Collections.singletonList(new UnsignedLong(1L))); + assertEquals(sourceArg.getValue().getDebugKey(), SOURCE_DEBUG_KEY); + verify(mMeasurementDao).insertEventReport(any()); + verify(mTransaction, times(2)).begin(); + verify(mTransaction, times(2)).end(); + } + + @Test + public void performAttributions_commonKeysIntersect_attributeTrigger_debugApi_triggerKey() + throws DatastoreException { + // Setup + Trigger trigger = + TriggerFixture.getValidTriggerBuilder() + .setId("triggerId1") + .setStatus(Trigger.Status.PENDING) + .setEventTriggers( + "[\n" + + "{\n" + + " \"trigger_data\": \"5\",\n" + + " \"priority\": \"123\",\n" + + " \"deduplication_key\": \"1\"\n" + + "}" + + "]\n") + .setFilters( + "{\n" + + " \"key_1\": [\"value_11\", \"value_12\"],\n" + + " \"key_2\": [\"value_21\", \"value_22\"]\n" + + "}\n") + .setDebugKey(TRIGGER_DEBUG_KEY) + .build(); + Source source = + SourceFixture.getValidSourceBuilder() + .setAttributionMode(Source.AttributionMode.TRUTHFULLY) + .setFilterData( + "{\n" + + " \"key_1\": [\"value_11\", \"value_12_x\"],\n" + + " \"key_2\": [\"value_21_x\", \"value_22\"]\n" + + "}\n") + .build(); + when(mMeasurementDao.getPendingTriggerIds()) + .thenReturn(Collections.singletonList(trigger.getId())); + when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); + List<Source> matchingSourceList = new ArrayList<>(); + matchingSourceList.add(source); + when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); + when(mMeasurementDao.getAttributionsPerRateLimitWindow(any(), any())).thenReturn(5L); + when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); + AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); + + // Execution + attributionService.performPendingAttributions(); + + // Assertions + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), + eq(Trigger.Status.ATTRIBUTED)); + ArgumentCaptor<Source> sourceArg = ArgumentCaptor.forClass(Source.class); + verify(mMeasurementDao).updateSourceDedupKeys(sourceArg.capture()); + assertEquals( + sourceArg.getValue().getDedupKeys(), + Collections.singletonList(new UnsignedLong(1L))); verify(mMeasurementDao).insertEventReport(any()); verify(mTransaction, times(2)).begin(); verify(mTransaction, times(2)).end(); @@ -997,7 +1367,7 @@ public class AttributionJobHandlerTest { Source source = SourceFixture.getValidSourceBuilder() .setAttributionMode(Source.AttributionMode.TRUTHFULLY) - .setAggregateFilterData( + .setFilterData( "{\n" + " \"key_1x\": [\"value_11_x\", \"value_12_x\"],\n" + " \"key_2x\": [\"value_21_x\", \"value_22_x\"]\n" @@ -1017,12 +1387,15 @@ public class AttributionJobHandlerTest { attributionService.performPendingAttributions(); // Assertions - ArgumentCaptor<Trigger> triggerArg = ArgumentCaptor.forClass(Trigger.class); - verify(mMeasurementDao).updateTriggerStatus(triggerArg.capture()); - Assert.assertEquals(Trigger.Status.ATTRIBUTED, triggerArg.getValue().getStatus()); + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), + eq(Trigger.Status.ATTRIBUTED)); ArgumentCaptor<Source> sourceArg = ArgumentCaptor.forClass(Source.class); verify(mMeasurementDao).updateSourceDedupKeys(sourceArg.capture()); - Assert.assertEquals(sourceArg.getValue().getDedupKeys(), Collections.singletonList(1L)); + assertEquals( + sourceArg.getValue().getDedupKeys(), + Collections.singletonList(new UnsignedLong(1L))); verify(mMeasurementDao).insertEventReport(any()); verify(mTransaction, times(2)).begin(); verify(mTransaction, times(2)).end(); @@ -1060,7 +1433,7 @@ public class AttributionJobHandlerTest { Source source = SourceFixture.getValidSourceBuilder() .setAttributionMode(Source.AttributionMode.TRUTHFULLY) - .setAggregateFilterData( + .setFilterData( "{\n" + " \"key_1\": [\"value_1\", \"value_2\"],\n" + " \"key_2\": [\"value_1\", \"value_2\"]\n" @@ -1070,10 +1443,10 @@ public class AttributionJobHandlerTest { EventReport expectedEventReport = new EventReport.Builder() .setTriggerPriority(3L) - .setTriggerDedupKey(3L) - .setTriggerData(1L) + .setTriggerDedupKey(new UnsignedLong(3L)) + .setTriggerData(new UnsignedLong(1L)) .setTriggerTime(234324L) - .setSourceId(source.getEventId()) + .setSourceEventId(source.getEventId()) .setStatus(EventReport.Status.PENDING) .setAttributionDestination(source.getAppDestination()) .setEnrollmentId(source.getEnrollmentId()) @@ -1082,6 +1455,8 @@ public class AttributionJobHandlerTest { trigger.getTriggerTime(), trigger.getDestinationType())) .setSourceType(source.getSourceType()) .setRandomizedTriggerRate(source.getRandomAttributionProbability()) + .setSourceId(source.getId()) + .setTriggerId(trigger.getId()) .build(); when(mMeasurementDao.getPendingTriggerIds()) .thenReturn(Collections.singletonList(trigger.getId())); @@ -1097,9 +1472,10 @@ public class AttributionJobHandlerTest { attributionService.performPendingAttributions(); // Assertions - ArgumentCaptor<Trigger> triggerArg = ArgumentCaptor.forClass(Trigger.class); - verify(mMeasurementDao).updateTriggerStatus(triggerArg.capture()); - Assert.assertEquals(Trigger.Status.ATTRIBUTED, triggerArg.getValue().getStatus()); + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), + eq(Trigger.Status.ATTRIBUTED)); verify(mMeasurementDao).updateSourceDedupKeys(source); verify(mMeasurementDao).insertEventReport(eq(expectedEventReport)); verify(mTransaction, times(2)).begin(); @@ -1138,7 +1514,7 @@ public class AttributionJobHandlerTest { Source source = SourceFixture.getValidSourceBuilder() .setAttributionMode(Source.AttributionMode.TRUTHFULLY) - .setAggregateFilterData( + .setFilterData( "{\n" + " \"key_1\": [\"value_1\", \"value_2\"],\n" + " \"key_2\": [\"value_1\", \"value_2\"]\n" @@ -1148,10 +1524,10 @@ public class AttributionJobHandlerTest { EventReport expectedEventReport = new EventReport.Builder() .setTriggerPriority(3L) - .setTriggerDedupKey(3L) - .setTriggerData(1L) + .setTriggerDedupKey(new UnsignedLong(3L)) + .setTriggerData(new UnsignedLong(1L)) .setTriggerTime(234324L) - .setSourceId(source.getEventId()) + .setSourceEventId(source.getEventId()) .setStatus(EventReport.Status.PENDING) .setAttributionDestination(source.getAppDestination()) .setEnrollmentId(source.getEnrollmentId()) @@ -1160,6 +1536,8 @@ public class AttributionJobHandlerTest { trigger.getTriggerTime(), trigger.getDestinationType())) .setSourceType(source.getSourceType()) .setRandomizedTriggerRate(source.getRandomAttributionProbability()) + .setSourceId(source.getId()) + .setTriggerId(trigger.getId()) .build(); when(mMeasurementDao.getPendingTriggerIds()) .thenReturn(Collections.singletonList(trigger.getId())); @@ -1175,9 +1553,10 @@ public class AttributionJobHandlerTest { attributionService.performPendingAttributions(); // Assertions - ArgumentCaptor<Trigger> triggerArg = ArgumentCaptor.forClass(Trigger.class); - verify(mMeasurementDao).updateTriggerStatus(triggerArg.capture()); - Assert.assertEquals(Trigger.Status.ATTRIBUTED, triggerArg.getValue().getStatus()); + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), + eq(Trigger.Status.ATTRIBUTED)); verify(mMeasurementDao).updateSourceDedupKeys(source); verify(mMeasurementDao).insertEventReport(eq(expectedEventReport)); verify(mTransaction, times(2)).begin(); @@ -1218,7 +1597,7 @@ public class AttributionJobHandlerTest { Source source = SourceFixture.getValidSourceBuilder() .setAttributionMode(Source.AttributionMode.TRUTHFULLY) - .setAggregateFilterData( + .setFilterData( "{\n" + " \"key_1\": [\"value_1\", \"value_2\"],\n" + " \"key_2\": [\"value_1\", \"value_2\"]\n" @@ -1229,10 +1608,10 @@ public class AttributionJobHandlerTest { EventReport expectedEventReport = new EventReport.Builder() .setTriggerPriority(3L) - .setTriggerDedupKey(3L) - .setTriggerData(3L) + .setTriggerDedupKey(new UnsignedLong(3L)) + .setTriggerData(new UnsignedLong(3L)) .setTriggerTime(234324L) - .setSourceId(source.getEventId()) + .setSourceEventId(source.getEventId()) .setStatus(EventReport.Status.PENDING) .setAttributionDestination(source.getAppDestination()) .setEnrollmentId(source.getEnrollmentId()) @@ -1241,6 +1620,8 @@ public class AttributionJobHandlerTest { trigger.getTriggerTime(), trigger.getDestinationType())) .setSourceType(Source.SourceType.NAVIGATION) .setRandomizedTriggerRate(source.getRandomAttributionProbability()) + .setSourceId(source.getId()) + .setTriggerId(trigger.getId()) .build(); when(mMeasurementDao.getPendingTriggerIds()) .thenReturn(Collections.singletonList(trigger.getId())); @@ -1256,9 +1637,10 @@ public class AttributionJobHandlerTest { attributionService.performPendingAttributions(); // Assertions - ArgumentCaptor<Trigger> triggerArg = ArgumentCaptor.forClass(Trigger.class); - verify(mMeasurementDao).updateTriggerStatus(triggerArg.capture()); - Assert.assertEquals(Trigger.Status.ATTRIBUTED, triggerArg.getValue().getStatus()); + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), + eq(Trigger.Status.ATTRIBUTED)); verify(mMeasurementDao).updateSourceDedupKeys(source); verify(mMeasurementDao).insertEventReport(eq(expectedEventReport)); verify(mTransaction, times(2)).begin(); @@ -1299,15 +1681,14 @@ public class AttributionJobHandlerTest { Source source = SourceFixture.getValidSourceBuilder() .setAttributionMode(Source.AttributionMode.TRUTHFULLY) - .setAggregateFilterData( + .setFilterData( "{\n" + " \"key_1\": [\"value_1_y\", \"value_2_y\"],\n" + " \"key_2\": [\"value_1_y\", \"value_2_y\"]\n" + "}\n") .setAggregateSource( - "[{\"id\" : \"campaignCounts\", \"key_piece\" : \"0x159\"}," - + "{\"id\" : \"geoValue\", \"key_piece\" : \"0x5\"}]") - .setAggregateFilterData( + "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") + .setFilterData( "{\"product\":[\"1234\",\"2345\"], \"key_1\": " + "[\"value_1_y\", \"value_2_y\"]}") .setId("sourceId") @@ -1326,11 +1707,283 @@ public class AttributionJobHandlerTest { attributionService.performPendingAttributions(); // Assertions - ArgumentCaptor<Trigger> triggerArg = ArgumentCaptor.forClass(Trigger.class); - verify(mMeasurementDao).updateTriggerStatus(triggerArg.capture()); - Assert.assertEquals(Trigger.Status.ATTRIBUTED, triggerArg.getValue().getStatus()); + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), + eq(Trigger.Status.ATTRIBUTED)); + verify(mMeasurementDao, never()).insertEventReport(any()); + verify(mMeasurementDao).insertAggregateReport(any()); + verify(mTransaction, times(2)).begin(); + verify(mTransaction, times(2)).end(); + } + + @Test + public void performAttribution_aggregateReportsExceedsLimit_insertsOnlyEventReport() + throws DatastoreException, JSONException { + // Setup + Trigger trigger = + TriggerFixture.getValidTriggerBuilder() + .setId("triggerId1") + .setStatus(Trigger.Status.PENDING) + .setEventTriggers( + "[\n" + + "{\n" + + " \"trigger_data\": \"5\",\n" + + " \"priority\": \"123\",\n" + + " \"deduplication_key\": \"1\"\n" + + "}" + + "]\n") + .setFilters( + "{\n" + + " \"key_1\": [\"value_1\", \"value_2\"],\n" + + " \"key_2\": [\"value_1\", \"value_2\"]\n" + + "}\n") + .setTriggerTime(234324L) + .setAggregateTriggerData(buildAggregateTriggerData().toString()) + .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") + .build(); + Source source = + SourceFixture.getValidSourceBuilder() + .setAttributionMode(Source.AttributionMode.TRUTHFULLY) + .setAggregateSource( + "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") + .setFilterData( + "{\n" + + " \"key_1\": [\"value_1\", \"value_2\"],\n" + + " \"key_2\": [\"value_1\", \"value_2\"]\n" + + "}\n") + .build(); + when(mMeasurementDao.getPendingTriggerIds()) + .thenReturn(Collections.singletonList(trigger.getId())); + when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); + List<Source> matchingSourceList = new ArrayList<>(); + matchingSourceList.add(source); + when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); + when(mMeasurementDao.getAttributionsPerRateLimitWindow(any(), any())).thenReturn(5L); + when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); + int numAggregateReportPerDestination = 1024; + int numEventReportPerDestination = 1023; + when(mMeasurementDao.getNumAggregateReportsPerDestination( + trigger.getAttributionDestination(), trigger.getDestinationType())) + .thenReturn(numAggregateReportPerDestination); + when(mMeasurementDao.getNumEventReportsPerDestination( + trigger.getAttributionDestination(), trigger.getDestinationType())) + .thenReturn(numEventReportPerDestination); + AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); + + // Execution + attributionService.performPendingAttributions(); + + // Assertions + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), + eq(Trigger.Status.ATTRIBUTED)); + verify(mMeasurementDao, never()).insertAggregateReport(any()); + verify(mMeasurementDao).insertEventReport(any()); + verify(mTransaction, times(2)).begin(); + verify(mTransaction, times(2)).end(); + } + + @Test + public void performAttribution_eventReportsExceedsLimit_insertsOnlyAggregateReport() + throws DatastoreException, JSONException { + // Setup + Trigger trigger = + TriggerFixture.getValidTriggerBuilder() + .setId("triggerId1") + .setStatus(Trigger.Status.PENDING) + .setEventTriggers( + "[\n" + + "{\n" + + " \"trigger_data\": \"5\",\n" + + " \"priority\": \"123\",\n" + + " \"deduplication_key\": \"1\"\n" + + "}" + + "]\n") + .setFilters( + "{\n" + + " \"key_1\": [\"value_1\", \"value_2\"],\n" + + " \"key_2\": [\"value_1\", \"value_2\"]\n" + + "}\n") + .setTriggerTime(234324L) + .setAggregateTriggerData(buildAggregateTriggerData().toString()) + .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") + .build(); + Source source = + SourceFixture.getValidSourceBuilder() + .setAttributionMode(Source.AttributionMode.TRUTHFULLY) + .setAggregateSource( + "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") + .setFilterData( + "{\n" + + " \"key_1\": [\"value_1\", \"value_2\"],\n" + + " \"key_2\": [\"value_1\", \"value_2\"]\n" + + "}\n") + .build(); + when(mMeasurementDao.getPendingTriggerIds()) + .thenReturn(Collections.singletonList(trigger.getId())); + when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); + List<Source> matchingSourceList = new ArrayList<>(); + matchingSourceList.add(source); + when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); + when(mMeasurementDao.getAttributionsPerRateLimitWindow(any(), any())).thenReturn(5L); + when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); + int numAggregateReportPerDestination = 1023; + int numEventReportPerDestination = 1024; + when(mMeasurementDao.getNumAggregateReportsPerDestination( + trigger.getAttributionDestination(), trigger.getDestinationType())) + .thenReturn(numAggregateReportPerDestination); + when(mMeasurementDao.getNumEventReportsPerDestination( + trigger.getAttributionDestination(), trigger.getDestinationType())) + .thenReturn(numEventReportPerDestination); + AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); + + // Execution + attributionService.performPendingAttributions(); + + // Assertion + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), + eq(Trigger.Status.ATTRIBUTED)); + verify(mMeasurementDao).insertAggregateReport(any()); verify(mMeasurementDao, never()).insertEventReport(any()); + verify(mTransaction, times(2)).begin(); + verify(mTransaction, times(2)).end(); + } + + @Test + public void performAttribution_aggregateAndEventReportsExceedsLimit_noReportInsertion() + throws DatastoreException, JSONException { + // Setup + Trigger trigger = + TriggerFixture.getValidTriggerBuilder() + .setId("triggerId1") + .setStatus(Trigger.Status.PENDING) + .setEventTriggers( + "[\n" + + "{\n" + + " \"trigger_data\": \"5\",\n" + + " \"priority\": \"123\",\n" + + " \"deduplication_key\": \"1\"\n" + + "}" + + "]\n") + .setFilters( + "{\n" + + " \"key_1\": [\"value_1\", \"value_2\"],\n" + + " \"key_2\": [\"value_1\", \"value_2\"]\n" + + "}\n") + .setTriggerTime(234324L) + .setAggregateTriggerData(buildAggregateTriggerData().toString()) + .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") + .build(); + Source source = + SourceFixture.getValidSourceBuilder() + .setAttributionMode(Source.AttributionMode.TRUTHFULLY) + .setAggregateSource( + "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") + .setFilterData( + "{\n" + + " \"key_1\": [\"value_1\", \"value_2\"],\n" + + " \"key_2\": [\"value_1\", \"value_2\"]\n" + + "}\n") + .build(); + when(mMeasurementDao.getPendingTriggerIds()) + .thenReturn(Collections.singletonList(trigger.getId())); + when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); + List<Source> matchingSourceList = new ArrayList<>(); + matchingSourceList.add(source); + when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); + when(mMeasurementDao.getAttributionsPerRateLimitWindow(any(), any())).thenReturn(5L); + when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); + int numAggregateReportPerDestination = 1024; + int numEventReportPerDestination = 1024; + when(mMeasurementDao.getNumAggregateReportsPerDestination( + trigger.getAttributionDestination(), trigger.getDestinationType())) + .thenReturn(numAggregateReportPerDestination); + when(mMeasurementDao.getNumEventReportsPerDestination( + trigger.getAttributionDestination(), trigger.getDestinationType())) + .thenReturn(numEventReportPerDestination); + AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); + + // Execution + attributionService.performPendingAttributions(); + + // Assertion + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); + verify(mMeasurementDao, never()).insertAggregateReport(any()); + verify(mMeasurementDao, never()).insertEventReport(any()); + verify(mTransaction, times(2)).begin(); + verify(mTransaction, times(2)).end(); + } + + @Test + public void performAttribution_aggregateAndEventReportsDoNotExceedsLimit_ReportInsertion() + throws DatastoreException, JSONException { + // Setup + Trigger trigger = + TriggerFixture.getValidTriggerBuilder() + .setId("triggerId1") + .setStatus(Trigger.Status.PENDING) + .setEventTriggers( + "[\n" + + "{\n" + + " \"trigger_data\": \"5\",\n" + + " \"priority\": \"123\",\n" + + " \"deduplication_key\": \"1\"\n" + + "}" + + "]\n") + .setFilters( + "{\n" + + " \"key_1\": [\"value_1\", \"value_2\"],\n" + + " \"key_2\": [\"value_1\", \"value_2\"]\n" + + "}\n") + .setTriggerTime(234324L) + .setAggregateTriggerData(buildAggregateTriggerData().toString()) + .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") + .build(); + Source source = + SourceFixture.getValidSourceBuilder() + .setAttributionMode(Source.AttributionMode.TRUTHFULLY) + .setAggregateSource( + "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") + .setFilterData( + "{\n" + + " \"key_1\": [\"value_1\", \"value_2\"],\n" + + " \"key_2\": [\"value_1\", \"value_2\"]\n" + + "}\n") + .build(); + when(mMeasurementDao.getPendingTriggerIds()) + .thenReturn(Collections.singletonList(trigger.getId())); + when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); + List<Source> matchingSourceList = new ArrayList<>(); + matchingSourceList.add(source); + when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); + when(mMeasurementDao.getAttributionsPerRateLimitWindow(any(), any())).thenReturn(5L); + when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); + int numAggregateReportPerDestination = 1023; + int numEventReportPerDestination = 1023; + when(mMeasurementDao.getNumAggregateReportsPerDestination( + trigger.getAttributionDestination(), trigger.getDestinationType())) + .thenReturn(numAggregateReportPerDestination); + when(mMeasurementDao.getNumEventReportsPerDestination( + trigger.getAttributionDestination(), trigger.getDestinationType())) + .thenReturn(numEventReportPerDestination); + AttributionJobHandler attributionService = new AttributionJobHandler(mDatastoreManager); + + // Execution + attributionService.performPendingAttributions(); + + // Assertion + verify(mMeasurementDao) + .updateTriggerStatus( + eq(Collections.singletonList(trigger.getId())), + eq(Trigger.Status.ATTRIBUTED)); verify(mMeasurementDao).insertAggregateReport(any()); + verify(mMeasurementDao).insertEventReport(any()); verify(mTransaction, times(2)).begin(); verify(mTransaction, times(2)).end(); } @@ -1357,4 +2010,24 @@ public class AttributionJobHandlerTest { filterData.put("product", new JSONArray(Arrays.asList("1234", "2345"))); return filterData; } + + private void assertAggregateReportsEqual( + AggregateReport expectedReport, AggregateReport actualReport) { + // Avoids checking report time because there is randomization + assertEquals(expectedReport.getApiVersion(), actualReport.getApiVersion()); + assertEquals( + expectedReport.getAttributionDestination(), + actualReport.getAttributionDestination()); + assertEquals( + expectedReport.getDebugCleartextPayload(), actualReport.getDebugCleartextPayload()); + assertEquals(expectedReport.getEnrollmentId(), actualReport.getEnrollmentId()); + assertEquals(expectedReport.getPublisher(), actualReport.getPublisher()); + assertEquals(expectedReport.getSourceId(), actualReport.getSourceId()); + assertEquals(expectedReport.getTriggerId(), actualReport.getTriggerId()); + assertEquals( + expectedReport.getAggregateAttributionData(), + actualReport.getAggregateAttributionData()); + assertEquals(expectedReport.getSourceDebugKey(), actualReport.getSourceDebugKey()); + assertEquals(expectedReport.getTriggerDebugKey(), actualReport.getTriggerDebugKey()); + } } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/attribution/AttributionJobServiceTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/attribution/AttributionJobServiceTest.java index c1ced6759a..d32739f30c 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/attribution/AttributionJobServiceTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/attribution/AttributionJobServiceTest.java @@ -19,6 +19,7 @@ package com.android.adservices.service.measurement.attribution; import static com.android.adservices.service.AdServicesConfig.MEASUREMENT_ATTRIBUTION_JOB_ID; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -35,17 +36,17 @@ import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.content.Context; -import android.provider.DeviceConfig; import com.android.adservices.data.measurement.DatastoreManager; import com.android.adservices.data.measurement.DatastoreManagerFactory; +import com.android.adservices.service.Flags; +import com.android.adservices.service.FlagsFactory; import com.android.compatibility.common.util.TestUtils; import com.android.dx.mockito.inline.extended.ExtendedMockito; -import com.android.modules.utils.testing.TestableDeviceConfig; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; @@ -56,11 +57,6 @@ import java.util.Optional; * Unit test for {@link AttributionJobService */ public class AttributionJobServiceTest { - // This rule is used for configuring P/H flags - @Rule - public final TestableDeviceConfig.TestableDeviceConfigRule mDeviceConfigRule = - new TestableDeviceConfig.TestableDeviceConfigRule(); - private static final long WAIT_IN_MILLIS = 50L; private DatastoreManager mMockDatastoreManager; @@ -77,10 +73,11 @@ public class AttributionJobServiceTest { @Test public void onStartJob_killSwitchOn() throws Exception { - enableKillSwitch(); - runWithMocks( () -> { + // Setup + enableKillSwitch(); + // Execute boolean result = mSpyService.onStartJob(Mockito.mock(JobParameters.class)); @@ -99,11 +96,11 @@ public class AttributionJobServiceTest { @Test public void onStartJob_killSwitchOff() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { // Setup + disableKillSwitch(); + ExtendedMockito.doNothing() .when( () -> @@ -128,11 +125,11 @@ public class AttributionJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOn_dontSchedule() throws Exception { - enableKillSwitch(); - runWithMocks( () -> { // Setup + enableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -159,11 +156,11 @@ public class AttributionJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOff_previouslyExecuted_dontForceSchedule_dontSchedule() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { // Setup + disableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -190,11 +187,11 @@ public class AttributionJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOff_previouslyExecuted_forceSchedule_schedule() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { // Setup + disableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -220,11 +217,11 @@ public class AttributionJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOff_previouslyNotExecuted_dontForceSchedule_schedule() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { // Setup + disableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -246,11 +243,27 @@ public class AttributionJobServiceTest { }); } + @Test + public void testSchedule_jobInfoIsPersisted() { + // Setup + final JobScheduler jobScheduler = mock(JobScheduler.class); + final ArgumentCaptor<JobInfo> captor = ArgumentCaptor.forClass(JobInfo.class); + + // Execute + AttributionJobService.schedule(mock(Context.class), jobScheduler); + + // Validate + verify(jobScheduler, times(1)).schedule(captor.capture()); + assertNotNull(captor.getValue()); + assertFalse(captor.getValue().isPersisted()); + } + private void runWithMocks(TestUtils.RunnableWithThrow execute) throws Exception { MockitoSession session = ExtendedMockito.mockitoSession() - .spyStatic(DatastoreManagerFactory.class) .spyStatic(AttributionJobService.class) + .spyStatic(DatastoreManagerFactory.class) + .spyStatic(FlagsFactory.class) .strictness(Strictness.LENIENT) .startMocking(); try { @@ -282,10 +295,8 @@ public class AttributionJobServiceTest { } private void toggleKillSwitch(boolean value) { - DeviceConfig.setProperty( - DeviceConfig.NAMESPACE_ADSERVICES, - "measurement_job_attribution_kill_switch", - Boolean.toString(value), - /* makeDefault */ false); + Flags mockFlags = Mockito.mock(Flags.class); + ExtendedMockito.doReturn(mockFlags).when(FlagsFactory::getFlags); + ExtendedMockito.doReturn(value).when(mockFlags).getMeasurementJobAttributionKillSwitch(); } } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/AsyncSourceFetcherTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/AsyncSourceFetcherTest.java new file mode 100644 index 0000000000..2ddb62286a --- /dev/null +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/AsyncSourceFetcherTest.java @@ -0,0 +1,2635 @@ +/* + * 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.measurement.registration; + +import static com.android.adservices.service.measurement.PrivacyParams.MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS; +import static com.android.adservices.service.measurement.PrivacyParams.MIN_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS; +import static com.android.adservices.service.measurement.SystemHealthParams.MAX_AGGREGATE_KEYS_PER_REGISTRATION; +import static com.android.adservices.service.measurement.SystemHealthParams.MAX_ATTRIBUTION_FILTERS; +import static com.android.adservices.service.measurement.SystemHealthParams.MAX_REDIRECTS_PER_REGISTRATION; +import static com.android.adservices.service.measurement.SystemHealthParams.MAX_VALUES_PER_ATTRIBUTION_FILTER; +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_MEASUREMENT_REGISTRATIONS; +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_MEASUREMENT_REGISTRATIONS__TYPE__SOURCE; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.adservices.measurement.RegistrationRequest; +import android.adservices.measurement.WebSourceParams; +import android.adservices.measurement.WebSourceRegistrationRequest; +import android.content.Context; +import android.net.Uri; +import android.view.InputEvent; + +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.adservices.data.enrollment.EnrollmentDao; +import com.android.adservices.service.Flags; +import com.android.adservices.service.FlagsFactory; +import com.android.adservices.service.enrollment.EnrollmentData; +import com.android.adservices.service.measurement.AsyncRegistration; +import com.android.adservices.service.measurement.Source; +import com.android.adservices.service.measurement.util.AsyncFetchStatus; +import com.android.adservices.service.measurement.util.AsyncRedirect; +import com.android.adservices.service.measurement.util.UnsignedLong; +import com.android.adservices.service.stats.AdServicesLogger; +import com.android.adservices.service.stats.MeasurementRegistrationResponseStats; +import com.android.dx.mockito.inline.extended.ExtendedMockito; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.quality.Strictness; + +import java.io.IOException; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import javax.net.ssl.HttpsURLConnection; +/** Unit tests for {@link SourceFetcher} */ +@RunWith(MockitoJUnitRunner.class) +@SmallTest +public final class AsyncSourceFetcherTest { + private static final String ANDROID_APP_SCHEME = "android-app"; + private static final String ANDROID_APP_SCHEME_URI_PREFIX = ANDROID_APP_SCHEME + "://"; + private static final String DEFAULT_REGISTRATION = "https://foo.com"; + private static final String ENROLLMENT_ID = "enrollment-id"; + private static final EnrollmentData ENROLLMENT = + new EnrollmentData.Builder().setEnrollmentId("enrollment-id").build(); + private static final String DEFAULT_TOP_ORIGIN = + "https://com.android.adservices.servicecoretest"; + ; + private static final String DEFAULT_DESTINATION = "android-app://com.myapps"; + private static final String DEFAULT_DESTINATION_WITHOUT_SCHEME = "com.myapps"; + private static final long DEFAULT_PRIORITY = 123; + private static final long DEFAULT_EXPIRY = 456789; + private static final UnsignedLong DEFAULT_EVENT_ID = new UnsignedLong(987654321L); + private static final UnsignedLong EVENT_ID_1 = new UnsignedLong(987654321L); + private static final UnsignedLong DEBUG_KEY = new UnsignedLong(823523783L); + private static final String LIST_TYPE_REDIRECT_URI = "https://bar.com"; + private static final String LOCATION_TYPE_REDIRECT_URI = "https://example.com"; + private static final String ALT_DESTINATION = "android-app://com.yourapps"; + private static final long ALT_PRIORITY = 321; + private static final long ALT_EVENT_ID = 123456789; + private static final long ALT_EXPIRY = 456790; + private static final Uri REGISTRATION_URI_1 = Uri.parse("https://foo.com"); + private static final Uri REGISTRATION_URI_2 = Uri.parse("https://foo2.com"); + private static final Uri OS_DESTINATION = Uri.parse("android-app://com.os-destination"); + private static final String LONG_FILTER_STRING = "12345678901234567890123456"; + private static final String LONG_AGGREGATE_KEY_ID = "12345678901234567890123456"; + private static final String LONG_AGGREGATE_KEY_PIECE = "0x123456789012345678901234567890123"; + private static final Uri OS_DESTINATION_WITH_PATH = + Uri.parse("android-app://com.os-destination/my/path"); + private static final Uri WEB_DESTINATION = Uri.parse("https://web-destination.com"); + private static final Uri WEB_DESTINATION_WITH_SUBDOMAIN = + Uri.parse("https://subdomain.web-destination.com"); + private static final WebSourceParams SOURCE_REGISTRATION_1 = + new WebSourceParams.Builder(REGISTRATION_URI_1).setDebugKeyAllowed(true).build(); + private static final WebSourceParams SOURCE_REGISTRATION_2 = + new WebSourceParams.Builder(REGISTRATION_URI_2).setDebugKeyAllowed(false).build(); + private static final Context sContext = + InstrumentationRegistry.getInstrumentation().getContext(); + + AsyncSourceFetcher mFetcher; + + @Mock HttpsURLConnection mUrlConnection; + @Mock EnrollmentDao mEnrollmentDao; + @Mock Flags mFlags; + @Mock AdServicesLogger mLogger; + + private MockitoSession mStaticMockSession; + + @Before + public void setup() { + mStaticMockSession = + ExtendedMockito.mockitoSession() + .spyStatic(FlagsFactory.class) + .strictness(Strictness.WARN) + .startMocking(); + ExtendedMockito.doReturn(FlagsFactory.getFlagsForTest()).when(FlagsFactory::getFlags); + + mFetcher = spy(new AsyncSourceFetcher(mEnrollmentDao, mFlags, mLogger)); + // For convenience, return the same enrollment-ID since we're using many arbitrary + // registration URIs and not yet enforcing uniqueness of enrollment. + when(mEnrollmentDao.getEnrollmentDataFromMeasurementUrl(any())).thenReturn(ENROLLMENT); + } + + @After + public void cleanup() throws InterruptedException { + mStaticMockSession.finishMocking(); + } + + @Test + public void testBasicSourceRequest() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); + doReturn(5000L).when(mFlags).getMaxResponseBasedRegistrationPayloadSizeBytes(); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"" + + DEFAULT_DESTINATION + + "\",\n" + + " \"priority\": \"" + + DEFAULT_PRIORITY + + "\",\n" + + " \"expiry\": \"" + + DEFAULT_EXPIRY + + "\",\n" + + " \"source_event_id\": \"" + + DEFAULT_EVENT_ID + + "\",\n" + + " \"debug_key\": \"" + + DEBUG_KEY + + "\"\n" + + "}\n"))); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(DEFAULT_DESTINATION, result.getAppDestination().toString()); + assertEquals(DEFAULT_PRIORITY, result.getPriority()); + assertEquals( + result.getEventTime() + TimeUnit.SECONDS.toMillis(DEFAULT_EXPIRY), + result.getExpiryTime()); + assertEquals(DEFAULT_EVENT_ID, result.getEventId()); + assertEquals(DEBUG_KEY, result.getDebugKey()); + verify(mLogger) + .logMeasurementRegistrationsResponseSize( + eq( + new MeasurementRegistrationResponseStats.Builder( + AD_SERVICES_MEASUREMENT_REGISTRATIONS, + AD_SERVICES_MEASUREMENT_REGISTRATIONS__TYPE__SOURCE, + 190) + .setAdTechDomain(null) + .build())); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testBasicSourceRequest_failsWhenNotEnrolled() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + when(mEnrollmentDao.getEnrollmentDataFromMeasurementUrl(any())).thenReturn(null); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"" + + DEFAULT_DESTINATION + + "\",\n" + + " \"priority\": \"" + + DEFAULT_PRIORITY + + "\",\n" + + " \"expiry\": \"" + + DEFAULT_EXPIRY + + "\",\n" + + " \"source_event_id\": \"" + + DEFAULT_EVENT_ID + + "\",\n" + + " \"debug_key\": \"" + + DEBUG_KEY + + "\"\n" + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals( + AsyncFetchStatus.ResponseStatus.INVALID_ENROLLMENT, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + verify(mFetcher, never()).openUrl(any()); + } + + @Test + public void testBasicSourceRequestWithoutAdIdPermission() throws Exception { + RegistrationRequest request = buildRequestWithoutAdIdPermission(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"" + + DEFAULT_DESTINATION + + "\",\n" + + " \"priority\": \"" + + DEFAULT_PRIORITY + + "\",\n" + + " \"expiry\": \"" + + DEFAULT_EXPIRY + + "\",\n" + + " \"source_event_id\": \"" + + DEFAULT_EVENT_ID + + "\",\n" + + " \"debug_key\": \"" + + DEBUG_KEY + + "\"\n" + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(DEFAULT_DESTINATION, result.getAppDestination().toString()); + assertEquals(DEFAULT_PRIORITY, result.getPriority()); + assertEquals( + result.getEventTime() + TimeUnit.SECONDS.toMillis(DEFAULT_EXPIRY), + result.getExpiryTime()); + assertEquals(DEFAULT_EVENT_ID, result.getEventId()); + assertNull(result.getDebugKey()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testSourceRequestWithPostInstallAttributes() throws Exception { + RegistrationRequest request = + new RegistrationRequest.Builder() + .setRegistrationType(RegistrationRequest.REGISTER_SOURCE) + .setRegistrationUri(Uri.parse("https://foo.com")) + .setPackageName(sContext.getAttributionSource().getPackageName()) + .build(); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL("https://foo.com")); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"android-app://com.myapps\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\": \"987654321\",\n" + + " \"install_attribution_window\": \"272800\",\n" + + " \"post_install_exclusivity_window\": \"987654\"\n" + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals("android-app://com.myapps", result.getAppDestination().toString()); + assertEquals(123, result.getPriority()); + assertEquals( + result.getEventTime() + TimeUnit.SECONDS.toMillis(456789), result.getExpiryTime()); + assertEquals(new UnsignedLong(987654321L), result.getEventId()); + assertEquals(TimeUnit.SECONDS.toMillis(272800), result.getInstallAttributionWindow()); + assertEquals(TimeUnit.SECONDS.toMillis(987654L), result.getInstallCooldownWindow()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testSourceRequestWithPostInstallAttributesReceivedAsNull() throws Exception { + RegistrationRequest request = + new RegistrationRequest.Builder() + .setRegistrationType(RegistrationRequest.REGISTER_SOURCE) + .setRegistrationUri(Uri.parse("https://foo.com")) + .setPackageName(sContext.getAttributionSource().getPackageName()) + .build(); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL("https://foo.com")); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"android-app://com" + + ".myapps\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\": \"987654321\",\n" + + " \"install_attribution_window\": null,\n" + + " \"post_install_exclusivity_window\": null\n" + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals("android-app://com.myapps", result.getAppDestination().toString()); + assertEquals(123, result.getPriority()); + assertEquals( + result.getEventTime() + TimeUnit.SECONDS.toMillis(456789), result.getExpiryTime()); + assertEquals(new UnsignedLong(987654321L), result.getEventId()); + // fallback to default value - 30 days + assertEquals(TimeUnit.SECONDS.toMillis(2592000L), result.getInstallAttributionWindow()); + // fallback to default value - 0 days + assertEquals(0L, result.getInstallCooldownWindow()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testSourceRequestWithInstallAttributesOutofBounds() throws IOException { + RegistrationRequest request = + new RegistrationRequest.Builder() + .setRegistrationType(RegistrationRequest.REGISTER_SOURCE) + .setRegistrationUri(Uri.parse("https://foo.com")) + .setPackageName(sContext.getAttributionSource().getPackageName()) + .build(); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL("https://foo.com")); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"android-app://com.myapps\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\": \"987654321\",\n" + // Min value of attribution is 1 day or 86400 + // seconds + + " \"install_attribution_window\": \"86300\",\n" + // Max value of cooldown is 30 days or 2592000 + // seconds + + " \"post_install_exclusivity_window\":" + + " \"9876543210\"\n" + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals("android-app://com.myapps", result.getAppDestination().toString()); + assertEquals(123, result.getPriority()); + assertEquals( + result.getEventTime() + TimeUnit.SECONDS.toMillis(456789), result.getExpiryTime()); + assertEquals(new UnsignedLong(987654321L), result.getEventId()); + // Adjusted to minimum allowed value + assertEquals(TimeUnit.SECONDS.toMillis(86400), result.getInstallAttributionWindow()); + // Adjusted to maximum allowed value + assertEquals(TimeUnit.SECONDS.toMillis((2592000L)), result.getInstallCooldownWindow()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testBadSourceUrl() { + RegistrationRequest request = buildRequest(/* registrationUri = */ "bad-schema://foo.com"); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.PARSING_ERROR, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + } + + @Test + public void testBadSourceConnection() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doThrow(new IOException("Bad internet things")) + .when(mFetcher) + .openUrl(new URL(DEFAULT_REGISTRATION)); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.NETWORK_ERROR, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + } + + @Test + public void testBadSourceJson_missingSourceEventId() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + "\"source_event_id\": \"" + + DEFAULT_EVENT_ID + + "\""))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.PARSING_ERROR, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testBadSourceJson_missingHeader() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()).thenReturn(Collections.emptyMap()); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.PARSING_ERROR, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testBadSourceJson_missingDestination() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + "\"destination\": \"" + + DEFAULT_DESTINATION + + "\""))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.PARSING_ERROR, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testBasicSourceRequestMinimumFields() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + "\"destination\": \"" + + DEFAULT_DESTINATION + + "\",\n" + + "\"source_event_id\": \"" + + DEFAULT_EVENT_ID + + "\"\n" + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(DEFAULT_DESTINATION, result.getAppDestination().toString()); + assertEquals(DEFAULT_EVENT_ID, result.getEventId()); + assertEquals(0, result.getPriority()); + assertEquals( + result.getEventTime() + + TimeUnit.SECONDS.toMillis( + MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS), + result.getExpiryTime()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testBasicSourceRequest_sourceEventId_negative() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\"destination\":\"" + + DEFAULT_DESTINATION + + "\"," + + "\"source_event_id\":\"-35\"}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testBasicSourceRequest_sourceEventId_tooLarge() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\"destination\":\"" + + DEFAULT_DESTINATION + + "\",\"source_event_id\":\"" + + "18446744073709551616\"}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testBasicSourceRequest_sourceEventId_notAnInt() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\"destination\":\"" + + DEFAULT_DESTINATION + + "\"," + + "\"source_event_id\":\"8l2\"}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testBasicSourceRequest_sourceEventId_uses64thBit() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\"destination\":\"" + + DEFAULT_DESTINATION + + "\",\"source_event_id\":\"" + + "18446744073709551615\"}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + + // Assertion + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(DEFAULT_DESTINATION, result.getAppDestination().toString()); + assertEquals(0, result.getPriority()); + assertEquals(new UnsignedLong(-1L), result.getEventId()); + assertEquals( + result.getEventTime() + + TimeUnit.SECONDS.toMillis( + MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS), + result.getExpiryTime()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testBasicSourceRequest_debugKey_negative() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\"destination\":\"" + + DEFAULT_DESTINATION + + "\"," + + "\"source_event_id\":\"" + + DEFAULT_EVENT_ID + + "\"," + + "\"debug_key\":\"-18\"}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + + // Assertion + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(DEFAULT_DESTINATION, result.getAppDestination().toString()); + assertEquals(DEFAULT_EVENT_ID, result.getEventId()); + assertEquals(0, result.getPriority()); + assertNull(result.getDebugKey()); + assertEquals( + result.getEventTime() + + TimeUnit.SECONDS.toMillis( + MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS), + result.getExpiryTime()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testBasicSourceRequest_debugKey_tooLarge() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\"destination\":\"" + + DEFAULT_DESTINATION + + "\"," + + "\"source_event_id\":\"" + + DEFAULT_EVENT_ID + + "\"," + + "\"debug_key\":\"18446744073709551616\"}"))); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + + // Assertion + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(DEFAULT_DESTINATION, result.getAppDestination().toString()); + assertEquals(DEFAULT_EVENT_ID, result.getEventId()); + assertEquals(0, result.getPriority()); + assertNull(result.getDebugKey()); + assertEquals( + result.getEventTime() + + TimeUnit.SECONDS.toMillis( + MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS), + result.getExpiryTime()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testBasicSourceRequest_debugKey_notAnInt() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\"destination\":\"" + + DEFAULT_DESTINATION + + "\"," + + "\"source_event_id\":\"" + + DEFAULT_EVENT_ID + + "\"," + + "\"debug_key\":\"987fs\"}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + + // Assertion + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(DEFAULT_DESTINATION, result.getAppDestination().toString()); + assertEquals(DEFAULT_EVENT_ID, result.getEventId()); + assertEquals(0, result.getPriority()); + assertNull(result.getDebugKey()); + assertEquals( + result.getEventTime() + + TimeUnit.SECONDS.toMillis( + MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS), + result.getExpiryTime()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testBasicSourceRequest_debugKey_uses64thBit() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\"destination\":\"" + + DEFAULT_DESTINATION + + "\"," + + "\"source_event_id\":\"" + + DEFAULT_EVENT_ID + + "\"," + + "\"debug_key\":\"18446744073709551615\"}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + + // Assertion + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(DEFAULT_DESTINATION, result.getAppDestination().toString()); + assertEquals(DEFAULT_EVENT_ID, result.getEventId()); + assertEquals(0, result.getPriority()); + assertEquals(new UnsignedLong(-1L), result.getDebugKey()); + assertEquals( + result.getEventTime() + + TimeUnit.SECONDS.toMillis( + MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS), + result.getExpiryTime()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testBasicSourceRequestMinimumFieldsAndRestNull() throws Exception { + RegistrationRequest request = + new RegistrationRequest.Builder() + .setRegistrationType(RegistrationRequest.REGISTER_SOURCE) + .setRegistrationUri(Uri.parse("https://foo.com")) + .setPackageName(sContext.getAttributionSource().getPackageName()) + .build(); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL("https://foo.com")); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + "\"destination\": \"android-app://com.myapps\",\n" + + "\"source_event_id\": \"123\",\n" + + "\"priority\": null,\n" + + "\"expiry\": null\n" + + "}\n"))); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals("android-app://com.myapps", result.getAppDestination().toString()); + assertEquals(new UnsignedLong(123L), result.getEventId()); + assertEquals(0, result.getPriority()); + assertEquals( + result.getEventTime() + + TimeUnit.SECONDS.toMillis( + MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS), + result.getExpiryTime()); + verify(mUrlConnection).setRequestMethod("POST"); + } + @Test + public void testBasicSourceRequestWithExpiryLessThan2Days() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + "\"destination\": \"" + + DEFAULT_DESTINATION + + "\",\n" + + "\"source_event_id\": \"" + + DEFAULT_EVENT_ID + + "\",\n" + + "\"expiry\": 1" + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(DEFAULT_DESTINATION, result.getAppDestination().toString()); + assertEquals(DEFAULT_EVENT_ID, result.getEventId()); + assertEquals(0, result.getPriority()); + assertEquals( + result.getEventTime() + + TimeUnit.SECONDS.toMillis( + MIN_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS), + result.getExpiryTime()); + verify(mUrlConnection).setRequestMethod("POST"); + } + @Test + public void testBasicSourceRequestWithExpiryMoreThan30Days() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + "\"destination\": \"" + + DEFAULT_DESTINATION + + "\",\n" + + "\"source_event_id\": \"" + + DEFAULT_EVENT_ID + + "\",\n" + + "\"expiry\": 2678400" + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(DEFAULT_DESTINATION, result.getAppDestination().toString()); + assertEquals(DEFAULT_EVENT_ID, result.getEventId()); + assertEquals(0, result.getPriority()); + assertEquals( + result.getEventTime() + + TimeUnit.SECONDS.toMillis( + MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS), + result.getExpiryTime()); + verify(mUrlConnection).setRequestMethod("POST"); + } + @Test + public void testNotOverHttps() throws Exception { + RegistrationRequest request = buildRequest("http://foo.com"); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.PARSING_ERROR, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + verify(mFetcher, never()).openUrl(any()); + } + + @Test + public void test500_ignoreFailure() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(500); + Map<String, List<String>> headersSecondRequest = new HashMap<>(); + headersSecondRequest.put( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + "\"destination\": \"" + + DEFAULT_DESTINATION + + "\",\n" + + "\"source_event_id\": \"" + + ALT_EVENT_ID + + "\",\n" + + "\"expiry\": " + + ALT_EXPIRY + + "" + + "}\n")); + when(mUrlConnection.getHeaderFields()).thenReturn(headersSecondRequest); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals( + AsyncFetchStatus.ResponseStatus.SERVER_UNAVAILABLE, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + verify(mUrlConnection).setRequestMethod("POST"); + } + @Test + public void testFailedParsingButValidRedirect_returnFailure() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + Map<String, List<String>> headersFirstRequest = new HashMap<>(); + headersFirstRequest.put("Attribution-Reporting-Register-Source", List.of("{}")); + headersFirstRequest.put("Attribution-Reporting-Redirect", List.of("https://bar.com")); + Map<String, List<String>> headersSecondRequest = new HashMap<>(); + headersSecondRequest.put( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + "\"destination\": \"" + + DEFAULT_DESTINATION + + "\",\n" + + "\"source_event_id\": \"" + + ALT_EVENT_ID + + "\",\n" + + "\"expiry\": " + + ALT_EXPIRY + + "" + + "}\n")); + when(mUrlConnection.getHeaderFields()) + .thenReturn(headersFirstRequest) + .thenReturn(headersSecondRequest); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.PARSING_ERROR, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + } + @Test + public void testRedirectDifferentDestination_keepAllReturnSuccess() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + Map<String, List<String>> headersFirstRequest = new HashMap<>(); + headersFirstRequest.put( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + "\"destination\": \"" + + DEFAULT_DESTINATION + + "\",\n" + + "\"source_event_id\": \"" + + DEFAULT_EVENT_ID + + "\",\n" + + "\"priority\": \"" + + DEFAULT_PRIORITY + + "\",\n" + + "\"expiry\": " + + DEFAULT_EXPIRY + + "" + + "}\n")); + headersFirstRequest.put("Attribution-Reporting-Redirect", List.of(LIST_TYPE_REDIRECT_URI)); + Map<String, List<String>> headersSecondRequest = new HashMap<>(); + headersSecondRequest.put( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + "\"destination\": \"" + + ALT_DESTINATION + + "\",\n" + + "\"source_event_id\": \"" + + ALT_EVENT_ID + + "\",\n" + + "\"priority\": \"" + + ALT_PRIORITY + + "\",\n" + + "\"expiry\": " + + ALT_EXPIRY + + "" + + "}\n")); + when(mUrlConnection.getHeaderFields()) + .thenReturn(headersFirstRequest) + .thenReturn(headersSecondRequest); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(DEFAULT_DESTINATION, result.getAppDestination().toString()); + assertEquals(DEFAULT_EVENT_ID, result.getEventId()); + assertEquals(DEFAULT_PRIORITY, result.getPriority()); + assertEquals( + result.getEventTime() + TimeUnit.SECONDS.toMillis(DEFAULT_EXPIRY), + result.getExpiryTime()); + assertEquals(LIST_TYPE_REDIRECT_URI, asyncRedirect.getRedirects().get(0).toString()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + } + + // Tests for redirect types + + @Test + public void testRedirectType_bothRedirectHeaderTypes_choosesListType() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + Map<String, List<String>> headers = getDefaultHeaders(); + + // Populate both 'list' and 'location' type headers + headers.put("Attribution-Reporting-Redirect", List.of(LIST_TYPE_REDIRECT_URI)); + headers.put("Location", List.of(LOCATION_TYPE_REDIRECT_URI)); + + when(mUrlConnection.getHeaderFields()).thenReturn(headers); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertDefaultSourceRegistration(result); + + // Assert 'none' type redirects were chosen + assertEquals(AsyncRegistration.RedirectType.NONE, asyncRedirect.getRedirectType()); + assertEquals(1, asyncRedirect.getRedirects().size()); + assertEquals(LIST_TYPE_REDIRECT_URI, asyncRedirect.getRedirects().get(0).toString()); + + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + } + + @Test + public void testRedirectType_locationRedirectHeaderType_choosesLocationType() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(302); + Map<String, List<String>> headers = getDefaultHeaders(); + + // Populate only 'location' type header + headers.put("Location", List.of(LOCATION_TYPE_REDIRECT_URI)); + + when(mUrlConnection.getHeaderFields()).thenReturn(headers); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertDefaultSourceRegistration(result); + + // Assert 'location' type redirects were chosen + assertEquals(AsyncRegistration.RedirectType.DAISY_CHAIN, asyncRedirect.getRedirectType()); + assertEquals(1, asyncRedirect.getRedirects().size()); + assertEquals(LOCATION_TYPE_REDIRECT_URI, asyncRedirect.getRedirects().get(0).toString()); + + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + } + + @Test + public void testRedirectType_locationRedirectType_maxCount_noRedirectReturned() + throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(302); + Map<String, List<String>> headers = getDefaultHeaders(); + + // Populate only 'location' type header + headers.put("Location", List.of(LOCATION_TYPE_REDIRECT_URI)); + + when(mUrlConnection.getHeaderFields()).thenReturn(headers); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = mFetcher.fetchSource( + appSourceRegistrationRequest( + request, + AsyncRegistration.RedirectType.DAISY_CHAIN, + MAX_REDIRECTS_PER_REGISTRATION), + asyncFetchStatus, + asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertDefaultSourceRegistration(result); + + // Assert 'location' type redirect but no uri + assertEquals(AsyncRegistration.RedirectType.DAISY_CHAIN, asyncRedirect.getRedirectType()); + assertEquals(0, asyncRedirect.getRedirects().size()); + + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + } + + @Test + public void testRedirectType_locationRedirectType_count1_redirectReturned() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(302); + Map<String, List<String>> headers = getDefaultHeaders(); + + // Populate only 'location' type header + headers.put("Location", List.of(LOCATION_TYPE_REDIRECT_URI)); + + when(mUrlConnection.getHeaderFields()).thenReturn(headers); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest( + request, AsyncRegistration.RedirectType.DAISY_CHAIN, 1), + asyncFetchStatus, + asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertDefaultSourceRegistration(result); + + // Assert 'location' type redirect and uri + assertEquals(AsyncRegistration.RedirectType.DAISY_CHAIN, asyncRedirect.getRedirectType()); + assertEquals(1, asyncRedirect.getRedirects().size()); + assertEquals(LOCATION_TYPE_REDIRECT_URI, asyncRedirect.getRedirects().get(0).toString()); + + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + } + + @Test + public void testRedirectType_locationRedirectType_ignoresListType() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(302); + Map<String, List<String>> headers = getDefaultHeaders(); + + // Populate both 'list' and 'location' type headers + headers.put("Attribution-Reporting-Redirect", List.of(LIST_TYPE_REDIRECT_URI)); + headers.put("Location", List.of(LOCATION_TYPE_REDIRECT_URI)); + + when(mUrlConnection.getHeaderFields()).thenReturn(headers); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest( + request, AsyncRegistration.RedirectType.DAISY_CHAIN, 1), + asyncFetchStatus, + asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertDefaultSourceRegistration(result); + + // Assert 'location' type redirect and uri + assertEquals(AsyncRegistration.RedirectType.DAISY_CHAIN, asyncRedirect.getRedirectType()); + assertEquals(1, asyncRedirect.getRedirects().size()); + assertEquals(LOCATION_TYPE_REDIRECT_URI, asyncRedirect.getRedirects().get(0).toString()); + + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + } + + // End tests for redirect types + + @Test + public void testBasicSourceRequestWithFilterData() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + String filterData = + " \"filter_data\": {\"product\":[\"1234\",\"2345\"], \"ctid\":[\"id\"]} \n"; + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"android-app://com.myapps\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\": \"987654321\",\n" + + filterData + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals("android-app://com.myapps", result.getAppDestination().toString()); + assertEquals(123, result.getPriority()); + assertEquals( + result.getEventTime() + TimeUnit.SECONDS.toMillis(456789), result.getExpiryTime()); + assertEquals(new UnsignedLong(987654321L), result.getEventId()); + assertEquals( + "{\"product\":[\"1234\",\"2345\"],\"ctid\":[\"id\"]}", + result.getFilterData()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testSourceRequest_filterData_invalidJson() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + String filterData = + " \"filter_data\": {\"product\":\"1234\",\"2345\"], \"ctid\":[\"id\"]} \n"; + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"android-app://com.myapps\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\": \"987654321\",\n" + + filterData + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testSourceRequest_filterData_tooManyFilters() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + StringBuilder filters = new StringBuilder("{"); + filters.append( + IntStream.range(0, MAX_ATTRIBUTION_FILTERS + 1) + .mapToObj(i -> "\"filter-string-" + i + "\": [\"filter-value\"]") + .collect(Collectors.joining(","))); + filters.append("}"); + String filterData = " \"filter_data\": " + filters + "\n"; + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"android-app://com.myapps\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\": \"987654321\",\n" + + filterData + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testSourceRequest_filterData_keyTooLong() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + String filterData = + " \"filter_data\": {\"product\":[\"1234\",\"2345\"], \"" + + LONG_FILTER_STRING + + "\":[\"id\"]} \n"; + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"android-app://com.myapps\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\": \"987654321\",\n" + + filterData + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testSourceRequest_filterData_tooManyValues() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + StringBuilder filters = + new StringBuilder( + "{" + + "\"filter-string-1\": [\"filter-value-1\"]," + + "\"filter-string-2\": ["); + filters.append( + IntStream.range(0, MAX_VALUES_PER_ATTRIBUTION_FILTER + 1) + .mapToObj(i -> "\"filter-value-" + i + "\"") + .collect(Collectors.joining(","))); + filters.append("]}"); + String filterData = " \"filter_data\": " + filters + " \n"; + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"android-app://com.myapps\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\": \"987654321\",\n" + + filterData + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testSourceRequest_filterData_valueTooLong() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + String filterData = + " \"filter_data\": {\"product\":[\"1234\",\"" + + LONG_FILTER_STRING + + "\"], \"ctid\":[\"id\"]} \n"; + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"android-app://com.myapps\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\": \"987654321\",\n" + + filterData + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + @Test + public void testMissingHeaderButWithRedirect() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn(Map.of( + "Attribution-Reporting-Redirect", List.of(LIST_TYPE_REDIRECT_URI))) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"" + + DEFAULT_DESTINATION + + "\",\n" + + " \"priority\": \"" + + DEFAULT_PRIORITY + + "\",\n" + + " \"expiry\": \"" + + DEFAULT_EXPIRY + + "\",\n" + + " \"source_event_id\": \"" + + DEFAULT_EVENT_ID + + "\"\n" + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.PARSING_ERROR, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + } + + @Test + public void testBasicSourceRequestWithAggregateSource() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"android-app://com.myapps\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\": \"987654321\",\n" + + "\"aggregation_keys\": {\"campaignCounts\" :" + + " \"0x159\", \"geoValue\" : \"0x5\"}\n" + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals( + new JSONObject("{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") + .toString(), + result.getAggregateSource()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testBasicSourceRequestWithAggregateSource_rejectsTooManyKeys() throws Exception { + StringBuilder tooManyKeys = new StringBuilder("{"); + for (int i = 0; i < 51; i++) { + tooManyKeys.append(String.format("\"campaign-%1$s\": \"0x15%1$s\"", i)); + } + tooManyKeys.append("}"); + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"android-app://com.myapps\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\":" + + " \"987654321\",\"aggregation_keys\": " + + tooManyKeys))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.PARSING_ERROR, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + verify(mUrlConnection).setRequestMethod("POST"); + } + @Test + public void testSourceRequestWithAggregateSource_tooManyKeys() throws Exception { + StringBuilder tooManyKeys = new StringBuilder("{"); + for (int i = 0; i < MAX_AGGREGATE_KEYS_PER_REGISTRATION + 1; i++) { + tooManyKeys.append(String.format("\"campaign-%1$s\": \"0x15%1$s\"", i)); + } + tooManyKeys.append("}"); + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"android-app://com.myapps\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\":" + + " \"987654321\",\"aggregation_keys\": " + + tooManyKeys))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + @Test + public void testSourceRequestWithAggregateSource_keyIsNotAnObject() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"android-app://com.myapps\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\": \"987654321\",\n" + + "\"aggregation_keys\": [\"campaignCounts\"," + + " \"0x159\", \"geoValue\", \"0x5\"]\n" + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testSourceRequestWithAggregateSource_invalidKeyId() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"android-app://com.myapps\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\": \"987654321\",\n" + + "\"aggregation_keys\": {\"" + + LONG_AGGREGATE_KEY_ID + + "\": \"0x159\"," + + "\"geoValue\": \"0x5\"}\n" + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testSourceRequestWithAggregateSource_invalidKeyPiece_missingPrefix() + throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"android-app://com.myapps\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\": \"987654321\",\n" + + "\"aggregation_keys\": {\"campaignCounts\" :" + + " \"0159\", \"geoValue\" : \"0x5\"}\n" + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + @Test + public void testSourceRequestWithAggregateSource_invalidKeyPiece_tooLong() throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"android-app://com.myapps\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\": \"987654321\",\n" + + "\"aggregation_keys\": {\"campaignCounts\":" + + " \"0x159\", \"geoValue\": \"" + + LONG_AGGREGATE_KEY_PIECE + + "\"}\n" + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + @Test + public void fetchWebSources_basic_success() throws IOException { + // Setup + WebSourceRegistrationRequest request = + buildWebSourceRegistrationRequest( + Arrays.asList(SOURCE_REGISTRATION_1), + DEFAULT_TOP_ORIGIN, + Uri.parse(DEFAULT_DESTINATION), + null); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + "\"destination\": \"" + + DEFAULT_DESTINATION + + "\",\n" + + "\"source_event_id\": \"" + + EVENT_ID_1 + + "\",\n" + + " \"debug_key\": \"" + + DEBUG_KEY + + "\"\n" + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = mFetcher.fetchSource( + webSourceRegistrationRequest(request, true), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(0, asyncRedirect.getRedirects().size()); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(Uri.parse(DEFAULT_DESTINATION), result.getAppDestination()); + assertEquals(EVENT_ID_1, result.getEventId()); + assertEquals( + result.getEventTime() + TimeUnit.SECONDS.toMillis( + MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS), + result.getExpiryTime()); + verify(mUrlConnection).setRequestMethod("POST"); + } + @Test + public void fetchWebSourcesSuccessWithoutAdIdPermission() throws IOException { + // Setup + WebSourceRegistrationRequest request = + buildWebSourceRegistrationRequest( + Arrays.asList(SOURCE_REGISTRATION_1, SOURCE_REGISTRATION_2), + DEFAULT_TOP_ORIGIN, + Uri.parse(DEFAULT_DESTINATION), + null); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + "\"destination\": \"" + + DEFAULT_DESTINATION + + "\",\n" + + "\"source_event_id\": \"" + + EVENT_ID_1 + + "\",\n" + + " \"debug_key\": \"" + + DEBUG_KEY + + "\"\n" + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = mFetcher.fetchSource( + webSourceRegistrationRequest(request, false), asyncFetchStatus, asyncRedirect); + // Assertion + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(0, asyncRedirect.getRedirects().size()); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(Uri.parse(DEFAULT_DESTINATION), result.getAppDestination()); + assertEquals(EVENT_ID_1, result.getEventId()); + assertEquals( + result.getEventTime() + TimeUnit.SECONDS.toMillis( + MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS), + result.getExpiryTime()); + assertNull(result.getFilterData()); + assertNull(result.getAggregateSource()); + } + @Test + public void fetchWebSources_oneSuccessAndOneFailure_resultsIntoOneSourceFetched() + throws IOException { + // Setup + WebSourceRegistrationRequest request = + buildWebSourceRegistrationRequest( + Arrays.asList(SOURCE_REGISTRATION_1), + DEFAULT_TOP_ORIGIN, + Uri.parse(DEFAULT_DESTINATION), + null); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); + when(mUrlConnection.getResponseCode()).thenReturn(200); + // Its validation will fail due to destination mismatch + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + "\"destination\": \"" + /* wrong destination */ + + "android-app://com.wrongapp" + + "\",\n" + + "\"source_event_id\": \"" + + EVENT_ID_1 + + "\",\n" + + " \"debug_key\": \"" + + DEBUG_KEY + + "\"\n" + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = mFetcher.fetchSource( + webSourceRegistrationRequest(request, true), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.PARSING_ERROR, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + verify(mUrlConnection).setRequestMethod("POST"); + } + @Test + public void fetchWebSources_withExtendedHeaders_success() throws IOException, JSONException { + // Setup + WebSourceRegistrationRequest request = + buildWebSourceRegistrationRequest( + Collections.singletonList(SOURCE_REGISTRATION_1), + DEFAULT_TOP_ORIGIN, + OS_DESTINATION, + null); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); + when(mUrlConnection.getResponseCode()).thenReturn(200); + String aggregateSource = "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}"; + String filterData = "{\"product\":[\"1234\",\"2345\"]," + "\"ctid\":[\"id\"]}"; + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"" + + OS_DESTINATION + + "\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\": \"987654321\",\n" + + " \"filter_data\": " + + filterData + + ", " + + " \"aggregation_keys\": " + + aggregateSource + + "}"))); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = mFetcher.fetchSource( + webSourceRegistrationRequest(request, true), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(0, asyncRedirect.getRedirects().size()); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(OS_DESTINATION, result.getAppDestination()); + assertEquals(filterData, result.getFilterData()); + assertEquals(new UnsignedLong(987654321L), result.getEventId()); + assertEquals( + result.getEventTime() + TimeUnit.SECONDS.toMillis(456789L), + result.getExpiryTime()); + assertEquals(new JSONObject(aggregateSource).toString(), result.getAggregateSource()); + verify(mUrlConnection).setRequestMethod("POST"); + } + @Test + public void fetchWebSources_withRedirects_ignoresRedirects() throws IOException, JSONException { + // Setup + WebSourceRegistrationRequest request = + buildWebSourceRegistrationRequest( + Collections.singletonList(SOURCE_REGISTRATION_1), + DEFAULT_TOP_ORIGIN, + Uri.parse(DEFAULT_DESTINATION), + null); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); + when(mUrlConnection.getResponseCode()).thenReturn(200); + String aggregateSource = "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}"; + String filterData = "{\"product\":[\"1234\",\"2345\"]," + "\"ctid\":[\"id\"]}"; + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"" + + DEFAULT_DESTINATION + + "\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\": \"987654321\",\n" + + " \"filter_data\": " + + filterData + + " , " + + " \"aggregation_keys\": " + + aggregateSource + + "}"), + "Attribution-Reporting-Redirect", + List.of(LIST_TYPE_REDIRECT_URI), + "Location", + List.of(LOCATION_TYPE_REDIRECT_URI))); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = mFetcher.fetchSource( + webSourceRegistrationRequest(request, true), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(AsyncRegistration.RedirectType.NONE, asyncRedirect.getRedirectType()); + assertEquals(0, asyncRedirect.getRedirects().size()); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(Uri.parse(DEFAULT_DESTINATION), result.getAppDestination()); + assertEquals(filterData, result.getFilterData()); + assertEquals(new UnsignedLong(987654321L), result.getEventId()); + assertEquals( + result.getEventTime() + TimeUnit.SECONDS.toMillis(456789L), + result.getExpiryTime()); + assertEquals(new JSONObject(aggregateSource).toString(), result.getAggregateSource()); + verify(mUrlConnection).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + @Test + public void fetchWebSources_appDestinationDoNotMatch_failsDropsSource() throws IOException { + // Setup + WebSourceRegistrationRequest request = + buildWebSourceRegistrationRequest( + Collections.singletonList(SOURCE_REGISTRATION_1), + DEFAULT_TOP_ORIGIN, + OS_DESTINATION, + null); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); + when(mUrlConnection.getResponseCode()).thenReturn(200); + String filterData = "{\"product\":[\"1234\",\"2345\"]," + "\"ctid\":[\"id\"]}"; + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"android-app://com" + + ".myapps\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\": \"987654321\",\n" + + " \"destination\":" + + " android-app://wrong.os-destination,\n" + + " \"web_destination\": " + + WEB_DESTINATION + + ",\n" + + " \"filter_data\": " + + filterData + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = mFetcher.fetchSource( + webSourceRegistrationRequest(request, true), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.PARSING_ERROR, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + verify(mUrlConnection).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + @Test + public void fetchWebSources_webDestinationDoNotMatch_failsDropsSource() throws IOException { + // Setup + WebSourceRegistrationRequest request = + buildWebSourceRegistrationRequest( + Collections.singletonList(SOURCE_REGISTRATION_1), + DEFAULT_TOP_ORIGIN, + OS_DESTINATION, + WEB_DESTINATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); + when(mUrlConnection.getResponseCode()).thenReturn(200); + String filterData = "{\"product\":[\"1234\",\"2345\"]," + "\"ctid\":[\"id\"]}"; + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"android-app://com" + + ".myapps\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\": \"987654321\",\n" + + " \"destination\": " + + OS_DESTINATION + + ",\n" + + " \"web_destination\": " + + " https://wrong-web-destination.com,\n" + + " \"filter_data\": " + + filterData + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = mFetcher.fetchSource( + webSourceRegistrationRequest(request, true), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.PARSING_ERROR, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + verify(mUrlConnection).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + @Test + public void fetchWebSources_osAndWebDestinationMatch_recordSourceSuccess() throws IOException { + // Setup + WebSourceRegistrationRequest request = + buildWebSourceRegistrationRequest( + Collections.singletonList(SOURCE_REGISTRATION_1), + DEFAULT_TOP_ORIGIN, + OS_DESTINATION, + WEB_DESTINATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"android-app://com" + + ".myapps\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\": \"987654321\",\n" + + " \"destination\": \"" + + OS_DESTINATION + + "\",\n" + + "\"web_destination\": \"" + + WEB_DESTINATION + + "\"" + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = mFetcher.fetchSource( + webSourceRegistrationRequest(request, true), asyncFetchStatus, asyncRedirect); + + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(0, asyncRedirect.getRedirects().size()); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(OS_DESTINATION, result.getAppDestination()); + assertNull(result.getFilterData()); + assertEquals(new UnsignedLong(987654321L), result.getEventId()); + assertEquals( + result.getEventTime() + TimeUnit.SECONDS.toMillis(456789L), + result.getExpiryTime()); + assertNull(result.getAggregateSource()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void fetchWebSources_extractsTopPrivateDomain() throws IOException { + // Setup + WebSourceRegistrationRequest request = + buildWebSourceRegistrationRequest( + Collections.singletonList(SOURCE_REGISTRATION_1), + DEFAULT_TOP_ORIGIN, + OS_DESTINATION, + WEB_DESTINATION_WITH_SUBDOMAIN); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"android-app://com" + + ".myapps\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\": \"987654321\",\n" + + " \"destination\": \"" + + OS_DESTINATION + + "\",\n" + + "\"web_destination\": \"" + + WEB_DESTINATION_WITH_SUBDOMAIN + + "\"" + + "}"))); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = mFetcher.fetchSource( + webSourceRegistrationRequest(request, true), asyncFetchStatus, asyncRedirect); + + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(0, asyncRedirect.getRedirects().size()); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(OS_DESTINATION, result.getAppDestination()); + assertNull(result.getFilterData()); + assertEquals(new UnsignedLong(987654321L), result.getEventId()); + assertEquals( + result.getEventTime() + TimeUnit.SECONDS.toMillis(456789L), + result.getExpiryTime()); + assertNull(result.getAggregateSource()); + verify(mUrlConnection).setRequestMethod("POST"); + } + @Test + public void fetchWebSources_extractsDestinationBaseUri() throws IOException { + // Setup + WebSourceRegistrationRequest request = + buildWebSourceRegistrationRequest( + Collections.singletonList(SOURCE_REGISTRATION_1), + DEFAULT_TOP_ORIGIN, + OS_DESTINATION_WITH_PATH, + WEB_DESTINATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + " \"destination\": \"" + + OS_DESTINATION_WITH_PATH + + "\",\n" + + " \"priority\": \"123\",\n" + + " \"expiry\": \"456789\",\n" + + " \"source_event_id\": \"987654321\",\n" + + "\"web_destination\": \"" + + WEB_DESTINATION + + "\"" + + "}"))); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = mFetcher.fetchSource( + webSourceRegistrationRequest(request, true), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(0, asyncRedirect.getRedirects().size()); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(OS_DESTINATION, result.getAppDestination()); + assertNull(result.getFilterData()); + assertEquals(new UnsignedLong(987654321L), result.getEventId()); + assertEquals( + result.getEventTime() + TimeUnit.SECONDS.toMillis(456789L), + result.getExpiryTime()); + assertNull(result.getAggregateSource()); + verify(mUrlConnection).setRequestMethod("POST"); + } + @Test + public void fetchWebSources_missingDestinations_dropsSource() throws Exception { + // Setup + WebSourceRegistrationRequest request = + buildWebSourceRegistrationRequest( + Collections.singletonList(SOURCE_REGISTRATION_1), + DEFAULT_TOP_ORIGIN, + OS_DESTINATION, + WEB_DESTINATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + "\"source_event_id\": \"" + + DEFAULT_EVENT_ID + + "\"\n" + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = mFetcher.fetchSource( + webSourceRegistrationRequest(request, true), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.PARSING_ERROR, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + verify(mUrlConnection).setRequestMethod("POST"); + } + @Test + public void fetchWebSources_withDestinationUriNotHavingScheme_attachesAppScheme() + throws IOException { + // Setup + WebSourceRegistrationRequest request = + buildWebSourceRegistrationRequest( + Collections.singletonList(SOURCE_REGISTRATION_1), + DEFAULT_TOP_ORIGIN, + Uri.parse(DEFAULT_DESTINATION_WITHOUT_SCHEME), + null); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + "\"destination\": \"" + + DEFAULT_DESTINATION + + "\",\n" + + "\"source_event_id\": \"" + + EVENT_ID_1 + + "\"\n" + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = mFetcher.fetchSource( + webSourceRegistrationRequest(request, true), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Source result = fetch.get(); + assertEquals(0, asyncRedirect.getRedirects().size()); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(Uri.parse(DEFAULT_DESTINATION), result.getAppDestination()); + assertNull(result.getFilterData()); + assertEquals(EVENT_ID_1, result.getEventId()); + assertEquals( + result.getEventTime() + TimeUnit.SECONDS.toMillis( + MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS), + result.getExpiryTime()); + assertNull(result.getAggregateSource()); + verify(mUrlConnection).setRequestMethod("POST"); + } + @Test + public void fetchWebSources_withDestinationUriHavingHttpsScheme_dropsSource() + throws IOException { + // Setup + WebSourceRegistrationRequest request = + buildWebSourceRegistrationRequest( + Collections.singletonList(SOURCE_REGISTRATION_1), + DEFAULT_TOP_ORIGIN, + Uri.parse(DEFAULT_DESTINATION_WITHOUT_SCHEME), + null); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + "\"destination\": \"" + // Invalid (https) URI for app destination + + WEB_DESTINATION + + "\",\n" + + "\"source_event_id\": \"" + + EVENT_ID_1 + + "\"\n" + + "}\n"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = mFetcher.fetchSource( + webSourceRegistrationRequest(request, true), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.PARSING_ERROR, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + verify(mUrlConnection).setRequestMethod("POST"); + } + @Test + public void basicSourceRequest_headersMoreThanMaxResponseSize_emitsMetricsWithAdTechDomain() + throws Exception { + RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); + doReturn(5L).when(mFlags).getMaxResponseBasedRegistrationPayloadSizeBytes(); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + "\"destination\": \"" + + DEFAULT_DESTINATION + + "\",\n" + + "\"source_event_id\": \"" + + DEFAULT_EVENT_ID + + "\"\n" + + "}\n"))); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Source> fetch = + mFetcher.fetchSource( + appSourceRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + + assertTrue(fetch.isPresent()); + verify(mLogger) + .logMeasurementRegistrationsResponseSize( + eq( + new MeasurementRegistrationResponseStats.Builder( + AD_SERVICES_MEASUREMENT_REGISTRATIONS, + AD_SERVICES_MEASUREMENT_REGISTRATIONS__TYPE__SOURCE, + 115) + .setAdTechDomain(DEFAULT_REGISTRATION) + .build())); + } + private RegistrationRequest buildRequest(String registrationUri) { + return new RegistrationRequest.Builder() + .setRegistrationType(RegistrationRequest.REGISTER_SOURCE) + .setRegistrationUri(Uri.parse(registrationUri)) + .setPackageName(sContext.getAttributionSource().getPackageName()) + .setAdIdPermissionGranted(true) + .build(); + } + private RegistrationRequest buildRequestWithoutAdIdPermission(String registrationUri) { + return new RegistrationRequest.Builder() + .setRegistrationType(RegistrationRequest.REGISTER_SOURCE) + .setRegistrationUri(Uri.parse(registrationUri)) + .setPackageName(sContext.getAttributionSource().getPackageName()) + .setAdIdPermissionGranted(false) + .build(); + } + + public static AsyncRegistration appSourceRegistrationRequest( + RegistrationRequest registrationRequest) { + return appSourceRegistrationRequest( + registrationRequest, AsyncRegistration.RedirectType.ANY, 0); + } + + public static AsyncRegistration appSourceRegistrationRequest( + RegistrationRequest registrationRequest, + @AsyncRegistration.RedirectType int redirectType, + int redirectCount) { + // Necessary for testing + String enrollmentId = ""; + if (EnrollmentDao.getInstance(sContext) + .getEnrollmentDataFromMeasurementUrl( + registrationRequest + .getRegistrationUri() + .buildUpon() + .clearQuery() + .build()) + != null) { + enrollmentId = + EnrollmentDao.getInstance(sContext) + .getEnrollmentDataFromMeasurementUrl( + registrationRequest + .getRegistrationUri() + .buildUpon() + .clearQuery() + .build()) + .getEnrollmentId(); + } + return createAsyncRegistration( + UUID.randomUUID().toString(), + enrollmentId, + registrationRequest.getRegistrationUri(), + null, + null, + Uri.parse(ANDROID_APP_SCHEME_URI_PREFIX + sContext.getPackageName()), + null, + Uri.parse(ANDROID_APP_SCHEME_URI_PREFIX + sContext.getPackageName()), + registrationRequest.getRegistrationType() == RegistrationRequest.REGISTER_SOURCE + ? AsyncRegistration.RegistrationType.APP_SOURCE + : AsyncRegistration.RegistrationType.APP_TRIGGER, + getSourceType(registrationRequest.getInputEvent()), + System.currentTimeMillis(), + 0, + System.currentTimeMillis(), + redirectType, + redirectCount, + registrationRequest.isAdIdPermissionGranted()); + } + + private static AsyncRegistration webSourceRegistrationRequest( + WebSourceRegistrationRequest webSourceRegistrationRequest, + boolean adIdPermissionGranted) { + if (webSourceRegistrationRequest.getSourceParams().size() > 0) { + WebSourceParams webSourceParams = webSourceRegistrationRequest.getSourceParams().get(0); + // Necessary for testing + String enrollmentId = ""; + if (EnrollmentDao.getInstance(sContext) + .getEnrollmentDataFromMeasurementUrl( + webSourceRegistrationRequest + .getSourceParams() + .get(0) + .getRegistrationUri() + .buildUpon() + .clearQuery() + .build()) + != null) { + enrollmentId = + EnrollmentDao.getInstance(sContext) + .getEnrollmentDataFromMeasurementUrl( + webSourceParams + .getRegistrationUri() + .buildUpon() + .clearQuery() + .build()) + .getEnrollmentId(); + } + return createAsyncRegistration( + UUID.randomUUID().toString(), + enrollmentId, + webSourceParams.getRegistrationUri(), + webSourceRegistrationRequest.getWebDestination(), + webSourceRegistrationRequest.getAppDestination(), + Uri.parse(ANDROID_APP_SCHEME_URI_PREFIX + sContext.getPackageName()), + null, + webSourceRegistrationRequest.getTopOriginUri(), + AsyncRegistration.RegistrationType.WEB_SOURCE, + Source.SourceType.NAVIGATION, + System.currentTimeMillis(), + 0, + System.currentTimeMillis(), + AsyncRegistration.RedirectType.NONE, + 0, + adIdPermissionGranted); + } + return null; + } + + private static AsyncRegistration createAsyncRegistration( + String iD, + String enrollmentId, + Uri registrationUri, + Uri webDestination, + Uri osDestination, + Uri registrant, + Uri verifiedDestination, + Uri topOrigin, + AsyncRegistration.RegistrationType registrationType, + Source.SourceType sourceType, + long mRequestTime, + long mRetryCount, + long mLastProcessingTime, + @AsyncRegistration.RedirectType int redirectType, + int redirectCount, + boolean debugKeyAllowed) { + return new AsyncRegistration.Builder() + .setId(iD) + .setEnrollmentId(enrollmentId) + .setRegistrationUri(registrationUri) + .setWebDestination(webDestination) + .setOsDestination(osDestination) + .setRegistrant(registrant) + .setVerifiedDestination(verifiedDestination) + .setTopOrigin(topOrigin) + .setType(registrationType.ordinal()) + .setSourceType( + registrationType == AsyncRegistration.RegistrationType.APP_SOURCE + || registrationType + == AsyncRegistration.RegistrationType.WEB_SOURCE + ? sourceType + : null) + .setRequestTime(mRequestTime) + .setRetryCount(mRetryCount) + .setLastProcessingTime(mLastProcessingTime) + .setRedirectType(redirectType) + .setRedirectCount(redirectCount) + .setDebugKeyAllowed(debugKeyAllowed) + .build(); + } + + private WebSourceRegistrationRequest buildWebSourceRegistrationRequest( + List<WebSourceParams> sourceParamsList, + String topOrigin, + Uri appDestination, + Uri webDestination) { + WebSourceRegistrationRequest.Builder webSourceRegistrationRequestBuilder = + new WebSourceRegistrationRequest.Builder(sourceParamsList, Uri.parse(topOrigin)) + .setAppDestination(appDestination); + if (webDestination != null) { + webSourceRegistrationRequestBuilder.setWebDestination(webDestination); + } + return webSourceRegistrationRequestBuilder.build(); + } + + static Source.SourceType getSourceType(InputEvent inputEvent) { + return inputEvent == null ? Source.SourceType.EVENT : Source.SourceType.NAVIGATION; + } + + static Map<String, List<String>> getDefaultHeaders() { + Map<String, List<String>> headers = new HashMap<>(); + headers.put( + "Attribution-Reporting-Register-Source", + List.of( + "{\n" + + "\"destination\": \"" + + DEFAULT_DESTINATION + + "\",\n" + + "\"source_event_id\": \"" + + DEFAULT_EVENT_ID + + "\",\n" + + "\"priority\": \"" + + DEFAULT_PRIORITY + + "\",\n" + + "\"expiry\": " + + DEFAULT_EXPIRY + + "" + + "}\n")); + return headers; + } + + private static void assertDefaultSourceRegistration(Source result) { + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(DEFAULT_DESTINATION, result.getAppDestination().toString()); + assertEquals(DEFAULT_EVENT_ID, result.getEventId()); + assertEquals(DEFAULT_PRIORITY, result.getPriority()); + assertEquals( + result.getEventTime() + TimeUnit.SECONDS.toMillis(DEFAULT_EXPIRY), + result.getExpiryTime()); + } +} diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/AsyncTriggerFetcherTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/AsyncTriggerFetcherTest.java new file mode 100644 index 0000000000..e52eae5696 --- /dev/null +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/AsyncTriggerFetcherTest.java @@ -0,0 +1,2797 @@ +/* + * 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.measurement.registration; + +import static com.android.adservices.service.measurement.SystemHealthParams.MAX_AGGREGATABLE_TRIGGER_DATA; +import static com.android.adservices.service.measurement.SystemHealthParams.MAX_AGGREGATE_KEYS_PER_REGISTRATION; +import static com.android.adservices.service.measurement.SystemHealthParams.MAX_ATTRIBUTION_EVENT_TRIGGER_DATA; +import static com.android.adservices.service.measurement.SystemHealthParams.MAX_ATTRIBUTION_FILTERS; +import static com.android.adservices.service.measurement.SystemHealthParams.MAX_REDIRECTS_PER_REGISTRATION; +import static com.android.adservices.service.measurement.SystemHealthParams.MAX_VALUES_PER_ATTRIBUTION_FILTER; +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_MEASUREMENT_REGISTRATIONS; +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_MEASUREMENT_REGISTRATIONS__TYPE__TRIGGER; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.adservices.measurement.RegistrationRequest; +import android.adservices.measurement.WebTriggerParams; +import android.adservices.measurement.WebTriggerRegistrationRequest; +import android.content.Context; +import android.net.Uri; + +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.adservices.data.enrollment.EnrollmentDao; +import com.android.adservices.service.Flags; +import com.android.adservices.service.FlagsFactory; +import com.android.adservices.service.enrollment.EnrollmentData; +import com.android.adservices.service.measurement.AsyncRegistration; +import com.android.adservices.service.measurement.Source; +import com.android.adservices.service.measurement.Trigger; +import com.android.adservices.service.measurement.util.AsyncFetchStatus; +import com.android.adservices.service.measurement.util.AsyncRedirect; +import com.android.adservices.service.measurement.util.UnsignedLong; +import com.android.adservices.service.stats.AdServicesLogger; +import com.android.adservices.service.stats.MeasurementRegistrationResponseStats; +import com.android.dx.mockito.inline.extended.ExtendedMockito; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.quality.Strictness; + +import java.io.IOException; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import javax.net.ssl.HttpsURLConnection; +/** Unit tests for {@link AsyncTriggerFetcher} */ +@RunWith(MockitoJUnitRunner.class) +@SmallTest +public final class AsyncTriggerFetcherTest { + private static final String ANDROID_APP_SCHEME = "android-app"; + private static final String ANDROID_APP_SCHEME_URI_PREFIX = ANDROID_APP_SCHEME + "://"; + private static final String TRIGGER_URI = "https://foo.com"; + private static final String ENROLLMENT_ID = "enrollment-id"; + private static final EnrollmentData ENROLLMENT = + new EnrollmentData.Builder().setEnrollmentId("enrollment-id").build(); + private static final String TOP_ORIGIN = "https://baz.com"; + private static final long TRIGGER_DATA = 7; + private static final long PRIORITY = 1; + private static final String LONG_FILTER_STRING = "12345678901234567890123456"; + private static final String LONG_AGGREGATE_KEY_ID = "12345678901234567890123456"; + private static final String LONG_AGGREGATE_KEY_PIECE = "0x123456789012345678901234567890123"; + private static final long DEDUP_KEY = 100; + private static final UnsignedLong DEBUG_KEY = new UnsignedLong(34787843L); + private static final String DEFAULT_REDIRECT = "https://bar.com"; + private static final String EVENT_TRIGGERS_1 = + "[\n" + + "{\n" + + " \"trigger_data\": \"" + + TRIGGER_DATA + + "\",\n" + + " \"priority\": \"" + + PRIORITY + + "\",\n" + + " \"deduplication_key\": \"" + + DEDUP_KEY + + "\",\n" + + " \"filters\": {\n" + + " \"source_type\": [\"navigation\"],\n" + + " \"key_1\": [\"value_1\"] \n" + + " }\n" + + "}" + + "]\n"; + private static final String LIST_TYPE_REDIRECT_URI = "https://bar.com"; + private static final String LOCATION_TYPE_REDIRECT_URI = "https://example.com"; + private static final Uri REGISTRATION_URI_1 = Uri.parse("https://foo.com"); + private static final Uri REGISTRATION_URI_2 = Uri.parse("https://foo2.com"); + private static final WebTriggerParams TRIGGER_REGISTRATION_1 = + new WebTriggerParams.Builder(REGISTRATION_URI_1).setDebugKeyAllowed(true).build(); + private static final WebTriggerParams TRIGGER_REGISTRATION_2 = + new WebTriggerParams.Builder(REGISTRATION_URI_2).setDebugKeyAllowed(false).build(); + private static final Context CONTEXT = + InstrumentationRegistry.getInstrumentation().getContext(); + + AsyncTriggerFetcher mFetcher; + + @Mock HttpsURLConnection mUrlConnection; + @Mock HttpsURLConnection mUrlConnection1; + @Mock EnrollmentDao mEnrollmentDao; + @Mock Flags mFlags; + @Mock AdServicesLogger mLogger; + + private MockitoSession mStaticMockSession; + + @Before + public void setup() { + mStaticMockSession = + ExtendedMockito.mockitoSession() + .spyStatic(FlagsFactory.class) + .strictness(Strictness.WARN) + .startMocking(); + ExtendedMockito.doReturn(FlagsFactory.getFlagsForTest()).when(FlagsFactory::getFlags); + mFetcher = spy(new AsyncTriggerFetcher(mEnrollmentDao, mFlags, mLogger)); + // For convenience, return the same enrollment-ID since we're using many arbitrary + // registration URIs and not yet enforcing uniqueness of enrollment. + when(mEnrollmentDao.getEnrollmentDataFromMeasurementUrl(any())).thenReturn(ENROLLMENT); + } + + @After + public void cleanup() throws InterruptedException { + mStaticMockSession.finishMocking(); + } + + @Test + public void testBasicTriggerRequest() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + MeasurementRegistrationResponseStats expectedStats = + new MeasurementRegistrationResponseStats.Builder( + AD_SERVICES_MEASUREMENT_REGISTRATIONS, + AD_SERVICES_MEASUREMENT_REGISTRATIONS__TYPE__TRIGGER, + 221) + .setAdTechDomain(null) + .build(); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"event_trigger_data\":" + EVENT_TRIGGERS_1 + "}"))); + doReturn(5000L).when(mFlags).getMaxResponseBasedRegistrationPayloadSizeBytes(); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + AsyncRegistration asyncRegistration = appTriggerRegistrationRequest(request); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger(asyncRegistration, asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals( + asyncRegistration.getTopOrigin().toString(), + result.getAttributionDestination().toString()); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(new JSONArray(EVENT_TRIGGERS_1).toString(), result.getEventTriggers()); + verify(mUrlConnection).setRequestMethod("POST"); + verify(mLogger).logMeasurementRegistrationsResponseSize(eq(expectedStats)); + } + + // Tests for redirect types + + @Test + public void testRedirectType_bothRedirectHeaderTypes_choosesListType() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + Map<String, List<String>> headers = getDefaultHeaders(); + + // Populate both 'list' and 'location' type headers + headers.put("Attribution-Reporting-Redirect", List.of(LIST_TYPE_REDIRECT_URI)); + headers.put("Location", List.of(LOCATION_TYPE_REDIRECT_URI)); + + when(mUrlConnection.getHeaderFields()).thenReturn(headers); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + AsyncRegistration asyncRegistration = appTriggerRegistrationRequest(request); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger(asyncRegistration, asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertSourceRegistration(asyncRegistration, result); + + // Assert 'none' type redirects were chosen + assertEquals(AsyncRegistration.RedirectType.NONE, asyncRedirect.getRedirectType()); + assertEquals(1, asyncRedirect.getRedirects().size()); + assertEquals(LIST_TYPE_REDIRECT_URI, asyncRedirect.getRedirects().get(0).toString()); + + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + } + + @Test + public void testRedirectType_locationRedirectHeaderType_choosesLocationType() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(302); + Map<String, List<String>> headers = getDefaultHeaders(); + + // Populate only 'location' type header + headers.put("Location", List.of(LOCATION_TYPE_REDIRECT_URI)); + + when(mUrlConnection.getHeaderFields()).thenReturn(headers); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + AsyncRegistration asyncRegistration = appTriggerRegistrationRequest(request); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger(asyncRegistration, asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertSourceRegistration(asyncRegistration, result); + + // Assert 'location' type redirects were chosen + assertEquals(AsyncRegistration.RedirectType.DAISY_CHAIN, asyncRedirect.getRedirectType()); + assertEquals(1, asyncRedirect.getRedirects().size()); + assertEquals(LOCATION_TYPE_REDIRECT_URI, asyncRedirect.getRedirects().get(0).toString()); + + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + } + + @Test + public void testRedirectType_locationRedirectType_maxCount_noRedirectReturned() + throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(302); + Map<String, List<String>> headers = getDefaultHeaders(); + + // Populate only 'location' type header + headers.put("Location", List.of(LOCATION_TYPE_REDIRECT_URI)); + + when(mUrlConnection.getHeaderFields()).thenReturn(headers); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + AsyncRegistration asyncRegistration = appTriggerRegistrationRequest(request, + AsyncRegistration.RedirectType.DAISY_CHAIN, MAX_REDIRECTS_PER_REGISTRATION); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger(asyncRegistration, asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertSourceRegistration(asyncRegistration, result); + + // Assert 'location' type redirect but no uri + assertEquals(AsyncRegistration.RedirectType.DAISY_CHAIN, asyncRedirect.getRedirectType()); + assertEquals(0, asyncRedirect.getRedirects().size()); + + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + } + + @Test + public void testRedirectType_locationRedirectType_count1_redirectReturned() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(302); + Map<String, List<String>> headers = getDefaultHeaders(); + + // Populate only 'location' type header + headers.put("Location", List.of(LOCATION_TYPE_REDIRECT_URI)); + + when(mUrlConnection.getHeaderFields()).thenReturn(headers); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + AsyncRegistration asyncRegistration = appTriggerRegistrationRequest( + request, AsyncRegistration.RedirectType.DAISY_CHAIN, 1); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger(asyncRegistration, asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertSourceRegistration(asyncRegistration, result); + + // Assert 'location' type redirect and uri + assertEquals(AsyncRegistration.RedirectType.DAISY_CHAIN, asyncRedirect.getRedirectType()); + assertEquals(1, asyncRedirect.getRedirects().size()); + assertEquals(LOCATION_TYPE_REDIRECT_URI, asyncRedirect.getRedirects().get(0).toString()); + + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + } + + @Test + public void testRedirectType_locationRedirectType_ignoresListType() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(302); + Map<String, List<String>> headers = getDefaultHeaders(); + + // Populate both 'list' and 'location' type headers + headers.put("Attribution-Reporting-Redirect", List.of(LIST_TYPE_REDIRECT_URI)); + headers.put("Location", List.of(LOCATION_TYPE_REDIRECT_URI)); + + when(mUrlConnection.getHeaderFields()).thenReturn(headers); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + AsyncRegistration asyncRegistration = appTriggerRegistrationRequest( + request, AsyncRegistration.RedirectType.DAISY_CHAIN, 1); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger(asyncRegistration, asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertSourceRegistration(asyncRegistration, result); + + // Assert 'location' type redirect and uri + assertEquals(AsyncRegistration.RedirectType.DAISY_CHAIN, asyncRedirect.getRedirectType()); + assertEquals(1, asyncRedirect.getRedirects().size()); + assertEquals(LOCATION_TYPE_REDIRECT_URI, asyncRedirect.getRedirects().get(0).toString()); + + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + } + + // End tests for redirect types + + @Test + public void testTriggerRequest_eventTriggerData_tooManyEntries() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + StringBuilder tooManyEntries = new StringBuilder("["); + for (int i = 0; i < MAX_ATTRIBUTION_EVENT_TRIGGER_DATA + 1; i++) { + tooManyEntries.append("{\"trigger_data\": \"2\",\"priority\": \"101\"}"); + } + tooManyEntries.append("]"); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"event_trigger_data\":" + tooManyEntries + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_eventTriggerData_triggerData_negative() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String eventTriggerData = "[{\"trigger_data\":\"-2\",\"priority\":\"101\"}]"; + String expectedResult = "[{\"priority\":\"101\"}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{\"event_trigger_data\":" + eventTriggerData + "}"))); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(expectedResult, result.getEventTriggers()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_eventTriggerData_triggerData_tooLarge() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String eventTriggerData = + "[{\"trigger_data\":\"18446744073709551616\"," + "\"priority\":\"101\"}]"; + String expectedResult = "[{\"priority\":\"101\"}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{\"event_trigger_data\":" + eventTriggerData + "}"))); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(expectedResult, result.getEventTriggers()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_eventTriggerData_triggerData_notAnInt() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String eventTriggerData = "[{\"trigger_data\":\"101z\",\"priority\":\"101\"}]"; + String expectedResult = "[{\"priority\":\"101\"}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{\"event_trigger_data\":" + eventTriggerData + "}"))); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(expectedResult, result.getEventTriggers()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_eventTriggerData_triggerData_uses64thBit() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String eventTriggerData = + "[{\"trigger_data\":\"18446744073709551615\"," + "\"priority\":\"101\"}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{\"event_trigger_data\":" + eventTriggerData + "}"))); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(eventTriggerData, result.getEventTriggers()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_eventTriggerData_priority_negative() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String eventTriggerData = "[{\"trigger_data\":\"2\",\"priority\":\"-101\"}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{\"event_trigger_data\":" + eventTriggerData + "}"))); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(eventTriggerData, result.getEventTriggers()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_eventTriggerData_priority_tooLarge() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String eventTriggerData = + "[{\"trigger_data\":\"2\",\"priority\":" + "\"18446744073709551615\"}]"; + String expectedResult = "[{\"trigger_data\":\"2\"}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{\"event_trigger_data\":" + eventTriggerData + "}"))); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(expectedResult, result.getEventTriggers()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_eventTriggerData_priority_notAnInt() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String eventTriggerData = "[{\"trigger_data\":\"2\",\"priority\":\"a101\"}]"; + String expectedResult = "[{\"trigger_data\":\"2\"}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{\"event_trigger_data\":" + eventTriggerData + "}"))); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(expectedResult, result.getEventTriggers()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_eventTriggerData_deduplicationKey_negative() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String eventTriggerData = + "[{\"trigger_data\":\"2\",\"priority\":\"101\"," + + "\"deduplication_key\":\"-34\"}]"; + String expectedResult = "[{\"trigger_data\":\"2\",\"priority\":\"101\"}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{\"event_trigger_data\":" + eventTriggerData + "}"))); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(expectedResult, result.getEventTriggers()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_eventTriggerData_deduplicationKey_tooLarge() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String eventTriggerData = + "[{\"trigger_data\":\"2\",\"priority\":\"101\"," + + "\"deduplication_key\":\"18446744073709551616\"}]"; + String expectedResult = "[{\"trigger_data\":\"2\",\"priority\":\"101\"}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{\"event_trigger_data\":" + eventTriggerData + "}"))); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(expectedResult, result.getEventTriggers()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_eventTriggerData_deduplicationKey_notAnInt() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String eventTriggerData = + "[{\"trigger_data\":\"2\",\"priority\":\"101\"," + + "\"deduplication_key\":\"145l\"}]"; + String expectedResult = "[{\"trigger_data\":\"2\",\"priority\":\"101\"}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{\"event_trigger_data\":" + eventTriggerData + "}"))); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(expectedResult, result.getEventTriggers()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_eventTriggerData_deduplicationKey_uses64thBit() + throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String eventTriggerData = + "[{\"trigger_data\":\"2\",\"priority\":\"101\"," + + "\"deduplication_key\":\"18446744073709551615\"}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{\"event_trigger_data\":" + eventTriggerData + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(eventTriggerData, result.getEventTriggers()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_eventTriggerData_filters_invalidJson() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String filters = + "{\"product\":[\"1234\",\"2345\"], \"\"\":[\"id\"]}"; + String eventTriggerData = + "[{\"trigger_data\":\"2\",\"priority\":\"101\"," + "\"filters\":" + filters + "}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"event_trigger_data\":" + eventTriggerData + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_eventTriggerData_filters_tooManyFilters() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + StringBuilder filters = new StringBuilder("{"); + filters.append( + IntStream.range(0, MAX_ATTRIBUTION_FILTERS + 1) + .mapToObj(i -> "\"filter-string-" + i + "\": [\"filter-value\"]") + .collect(Collectors.joining(","))); + filters.append("}"); + String eventTriggerData = + "[{\"trigger_data\":\"2\",\"priority\":\"101\"," + "\"filters\":" + filters + "}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"event_trigger_data\":" + eventTriggerData + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_eventTriggerData_filters_keyTooLong() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String filters = + "{\"product\":[\"1234\",\"2345\"], \"" + LONG_FILTER_STRING + "\":[\"id\"]}"; + String eventTriggerData = + "[{\"trigger_data\":\"2\",\"priority\":\"101\"," + "\"filters\":" + filters + "}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"event_trigger_data\":" + eventTriggerData + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_eventTriggerData_filters_tooManyValues() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + StringBuilder filters = + new StringBuilder( + "{" + + "\"filter-string-1\": [\"filter-value-1\"]," + + "\"filter-string-2\": ["); + filters.append( + IntStream.range(0, MAX_VALUES_PER_ATTRIBUTION_FILTER + 1) + .mapToObj(i -> "\"filter-value-" + i + "\"") + .collect(Collectors.joining(","))); + filters.append("]}"); + String eventTriggerData = + "[{\"trigger_data\":\"2\",\"priority\":\"101\"," + "\"filters\":" + filters + "}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"event_trigger_data\":" + eventTriggerData + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_eventTriggerData_filters_valueTooLong() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String filters = + "{\"product\":[\"1234\",\"" + LONG_FILTER_STRING + "\"], \"ctid\":[\"id\"]}"; + String eventTriggerData = + "[{\"trigger_data\":\"2\",\"priority\":\"101\"," + "\"filters\":" + filters + "}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"event_trigger_data\":" + eventTriggerData + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_eventTriggerData_notFilters_invalidJson() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String notFilters = + "{\"product\":[\"1234\",\"2345\"], \"\"\":[\"id\"]}"; + String eventTriggerData = + "[{\"trigger_data\":\"2\",\"priority\":\"101\"," + + "\"not_filters\":" + + notFilters + + "}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"event_trigger_data\":" + eventTriggerData + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_eventTriggerData_notFilters_tooManyFilters() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + StringBuilder notFilters = new StringBuilder("{"); + notFilters.append( + IntStream.range(0, MAX_ATTRIBUTION_FILTERS + 1) + .mapToObj(i -> "\"filter-string-" + i + "\": [\"filter-value\"]") + .collect(Collectors.joining(","))); + notFilters.append("}"); + String eventTriggerData = + "[{\"trigger_data\":\"2\",\"priority\":\"101\"," + + "\"not_filters\":" + + notFilters + + "}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"event_trigger_data\":" + eventTriggerData + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_eventTriggerData_notFilters_keyTooLong() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String notFilters = + "{\"product\":[\"1234\",\"2345\"], \"" + LONG_FILTER_STRING + "\":[\"id\"]}"; + String eventTriggerData = + "[{\"trigger_data\":\"2\",\"priority\":\"101\"," + + "\"not_filters\":" + + notFilters + + "}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"event_trigger_data\":" + eventTriggerData + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_eventTriggerData_notFilters_tooManyValues() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + StringBuilder notFilters = + new StringBuilder( + "{" + + "\"filter-string-1\": [\"filter-value-1\"]," + + "\"filter-string-2\": ["); + notFilters.append( + IntStream.range(0, MAX_VALUES_PER_ATTRIBUTION_FILTER + 1) + .mapToObj(i -> "\"filter-value-" + i + "\"") + .collect(Collectors.joining(","))); + notFilters.append("]}"); + String eventTriggerData = + "[{\"trigger_data\":\"2\",\"priority\":\"101\"," + + "\"not_filters\":" + + notFilters + + "}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"event_trigger_data\":" + eventTriggerData + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_eventTriggerData_notFilters_valueTooLong() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String notFilters = + "{\"product\":[\"1234\",\"" + LONG_FILTER_STRING + "\"], \"ctid\":[\"id\"]}"; + String eventTriggerData = + "[{\"trigger_data\":\"2\",\"priority\":\"101\"," + + "\"not_filters\":" + + notFilters + + "}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"event_trigger_data\":" + eventTriggerData + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_filters_invalidJson() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String filters = + "{\"product\":[\"1234\",\"2345\"], \"\"\":[\"id\"]}"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"filters\":" + filters + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_filters_tooManyFilters() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + StringBuilder filters = new StringBuilder("{"); + filters.append( + IntStream.range(0, MAX_ATTRIBUTION_FILTERS + 1) + .mapToObj(i -> "\"filter-string-" + i + "\": [\"filter-value\"]") + .collect(Collectors.joining(","))); + filters.append("}"); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"filters\":" + filters + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_filters_keyTooLong() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String filters = + "{\"product\":[\"1234\",\"2345\"], \"" + LONG_FILTER_STRING + "\":[\"id\"]}"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"filters\":" + filters + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_filters_tooManyValues() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + StringBuilder filters = + new StringBuilder( + "{" + + "\"filter-string-1\": [\"filter-value-1\"]," + + "\"filter-string-2\": ["); + filters.append( + IntStream.range(0, MAX_VALUES_PER_ATTRIBUTION_FILTER + 1) + .mapToObj(i -> "\"filter-value-" + i + "\"") + .collect(Collectors.joining(","))); + filters.append("]}"); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"filters\":" + filters + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_filters_valueTooLong() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String filters = + "{\"product\":[\"1234\",\"" + LONG_FILTER_STRING + "\"], \"ctid\":[\"id\"]}"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"filters\":" + filters + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_notFilters_invalidJson() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String notFilters = + "{\"product\":[\"1234\",\"2345\"], \"\"\":[\"id\"]}"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"not_filters\":" + notFilters + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_notFilters_tooManyFilters() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + StringBuilder notFilters = new StringBuilder("{"); + notFilters.append( + IntStream.range(0, MAX_ATTRIBUTION_FILTERS + 1) + .mapToObj(i -> "\"filter-string-" + i + "\": [\"filter-value\"]") + .collect(Collectors.joining(","))); + notFilters.append("}"); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"not_filters\":" + notFilters + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_notFilters_keyTooLong() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String notFilters = + "{\"product\":[\"1234\",\"2345\"], \"" + LONG_FILTER_STRING + "\":[\"id\"]}"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"not_filters\":" + notFilters + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_notFilters_tooManyValues() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + StringBuilder notFilters = + new StringBuilder( + "{" + + "\"filter-string-1\": [\"filter-value-1\"]," + + "\"filter-string-2\": ["); + notFilters.append( + IntStream.range(0, MAX_VALUES_PER_ATTRIBUTION_FILTER + 1) + .mapToObj(i -> "\"filter-value-" + i + "\"") + .collect(Collectors.joining(","))); + notFilters.append("]}"); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"not_filters\":" + notFilters + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequest_notFilters_valueTooLong() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String notFilters = + "{\"product\":[\"1234\",\"" + LONG_FILTER_STRING + "\"], \"ctid\":[\"id\"]}"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"not_filters\":" + notFilters + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testBasicTriggerRequest_failsWhenNotEnrolled() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + when(mEnrollmentDao.getEnrollmentDataFromMeasurementUrl(any())).thenReturn(null); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"event_trigger_data\":" + EVENT_TRIGGERS_1 + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals( + AsyncFetchStatus.ResponseStatus.INVALID_ENROLLMENT, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + verify(mFetcher, never()).openUrl(any()); + } + + @Test + public void testBasicTriggerRequestWithDebugKey() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + Map<String, List<String>> headersRequest = new HashMap<>(); + headersRequest.put( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"event_trigger_data\": " + + EVENT_TRIGGERS_1 + + ", \"debug_key\": \"" + + DEBUG_KEY + + "\"" + + "}")); + when(mUrlConnection.getHeaderFields()).thenReturn(headersRequest); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + AsyncRegistration asyncRegistration = appTriggerRegistrationRequest(request); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger(asyncRegistration, asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals( + asyncRegistration.getTopOrigin().toString(), + result.getAttributionDestination().toString()); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(new JSONArray(EVENT_TRIGGERS_1).toString(), result.getEventTriggers()); + assertEquals(DEBUG_KEY, result.getDebugKey()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testBasicTriggerRequest_debugKey_negative() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + + Map<String, List<String>> headersRequest = new HashMap<>(); + headersRequest.put( + "Attribution-Reporting-Register-Trigger", + List.of( + "{\"event_trigger_data\":" + + EVENT_TRIGGERS_1 + + ",\"debug_key\":\"-376\"}")); + + when(mUrlConnection.getHeaderFields()).thenReturn(headersRequest); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + AsyncRegistration asyncRegistration = appTriggerRegistrationRequest(request); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger(asyncRegistration, asyncFetchStatus, asyncRedirect); + // Assertion + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(new JSONArray(EVENT_TRIGGERS_1).toString(), result.getEventTriggers()); + assertNull(result.getDebugKey()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testBasicTriggerRequest_debugKey_tooLarge() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + + Map<String, List<String>> headersRequest = new HashMap<>(); + headersRequest.put( + "Attribution-Reporting-Register-Trigger", + List.of( + "{\"event_trigger_data\":" + + EVENT_TRIGGERS_1 + + ",\"debug_key\":\"18446744073709551616\"}")); + + when(mUrlConnection.getHeaderFields()).thenReturn(headersRequest); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + AsyncRegistration asyncRegistration = appTriggerRegistrationRequest(request); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger(asyncRegistration, asyncFetchStatus, asyncRedirect); + // Assertion + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(new JSONArray(EVENT_TRIGGERS_1).toString(), result.getEventTriggers()); + assertNull(result.getDebugKey()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testBasicTriggerRequest_debugKey_notAnInt() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + + Map<String, List<String>> headersRequest = new HashMap<>(); + headersRequest.put( + "Attribution-Reporting-Register-Trigger", + List.of( + "{\"event_trigger_data\":" + + EVENT_TRIGGERS_1 + + ",\"debug_key\":\"65g43\"}")); + + when(mUrlConnection.getHeaderFields()).thenReturn(headersRequest); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + AsyncRegistration asyncRegistration = appTriggerRegistrationRequest(request); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger(asyncRegistration, asyncFetchStatus, asyncRedirect); + // Assertion + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(new JSONArray(EVENT_TRIGGERS_1).toString(), result.getEventTriggers()); + assertNull(result.getDebugKey()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testBasicTriggerRequest_debugKey_uses64thBit() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + + Map<String, List<String>> headersRequest = new HashMap<>(); + headersRequest.put( + "Attribution-Reporting-Register-Trigger", + List.of( + "{\"event_trigger_data\":" + + EVENT_TRIGGERS_1 + + ",\"debug_key\":\"18446744073709551615\"}")); + + when(mUrlConnection.getHeaderFields()).thenReturn(headersRequest); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + AsyncRegistration asyncRegistration = appTriggerRegistrationRequest(request); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger(asyncRegistration, asyncFetchStatus, asyncRedirect); + // Assertion + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(new JSONArray(EVENT_TRIGGERS_1).toString(), result.getEventTriggers()); + assertEquals(new UnsignedLong(-1L), result.getDebugKey()); + } + + @Test + public void testBasicTriggerRequestWithoutAdIdPermission() throws Exception { + RegistrationRequest request = buildRequestWithoutAdIdPermission(TRIGGER_URI); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + Map<String, List<String>> headersRequest = new HashMap<>(); + headersRequest.put( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"event_trigger_data\": " + + EVENT_TRIGGERS_1 + + ", \"debug_key\": \"" + + DEBUG_KEY + + "\"" + + "}")); + when(mUrlConnection.getHeaderFields()).thenReturn(headersRequest); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + AsyncRegistration asyncRegistration = appTriggerRegistrationRequest(request); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger(asyncRegistration, asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals( + asyncRegistration.getTopOrigin().toString(), + result.getAttributionDestination().toString()); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(new JSONArray(EVENT_TRIGGERS_1).toString(), result.getEventTriggers()); + assertNull(result.getDebugKey()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testBadTriggerUrl() throws Exception { + RegistrationRequest request = buildRequest("bad-schema://foo.com"); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.PARSING_ERROR, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + } + + @Test + public void testBadTriggerConnection() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + doThrow(new IOException("Bad internet things")) + .when(mFetcher) + .openUrl(new URL(TRIGGER_URI)); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.NETWORK_ERROR, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, never()).setRequestMethod("POST"); + } + + @Test + public void testBadRequestReturnFailure() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(400); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"event_trigger_data\":" + EVENT_TRIGGERS_1 + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals( + AsyncFetchStatus.ResponseStatus.SERVER_UNAVAILABLE, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testBasicTriggerRequestMinimumFields() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"event_trigger_data\": " + "[{}]" + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + AsyncRegistration asyncRegistration = appTriggerRegistrationRequest(request); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger(asyncRegistration, asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals( + asyncRegistration.getTopOrigin().toString(), + result.getAttributionDestination().toString()); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals("[{}]", result.getEventTriggers()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testNotOverHttps() throws Exception { + RegistrationRequest request = buildRequest("http://foo.com"); + // Non-https should fail. + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.PARSING_ERROR, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + } + + @Test + public void testFirst200Next500_ignoreFailureReturnSuccess() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(200).thenReturn(500); + Map<String, List<String>> headersFirstRequest = new HashMap<>(); + headersFirstRequest.put( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"event_trigger_data\":" + EVENT_TRIGGERS_1 + "}")); + headersFirstRequest.put("Attribution-Reporting-Redirect", List.of(DEFAULT_REDIRECT)); + when(mUrlConnection.getHeaderFields()).thenReturn(headersFirstRequest); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + AsyncRegistration asyncRegistration = appTriggerRegistrationRequest(request); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger(asyncRegistration, asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals( + asyncRegistration.getTopOrigin().toString(), + result.getAttributionDestination().toString()); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(new JSONArray(EVENT_TRIGGERS_1).toString(), result.getEventTriggers()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + } + + @Test + public void testMissingHeaderButWithRedirect() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn(Map.of("Attribution-Reporting-Redirect", List.of(DEFAULT_REDIRECT))) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"event_trigger_data\":" + EVENT_TRIGGERS_1 + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.PARSING_ERROR, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + } + + @Test + public void testBasicTriggerRequestWithAggregateTriggerData() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String aggregatable_trigger_data = + "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," + + "\"filters\":" + + "{\"conversion_subdomain\":[\"electronics.megastore\"]}," + + "\"not_filters\":{\"product\":[\"1\"]}}," + + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"aggregatable_trigger_data\": " + + aggregatable_trigger_data + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + AsyncRegistration asyncRegistration = appTriggerRegistrationRequest(request); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger(asyncRegistration, asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals( + asyncRegistration.getTopOrigin().toString(), + result.getAttributionDestination().toString()); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals( + new JSONArray(aggregatable_trigger_data).toString(), + result.getAggregateTriggerData()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testBasicTriggerRequestWithAggregateTriggerData_rejectsTooManyDataKeys() + throws Exception { + StringBuilder tooManyKeys = new StringBuilder("["); + for (int i = 0; i < 51; i++) { + tooManyKeys.append( + String.format( + "{\"key_piece\": \"0x15%1$s\",\"source_keys\":[\"campaign-%1$s\"]}", + i)); + } + tooManyKeys.append("]"); + RegistrationRequest request = buildRequest(TRIGGER_URI); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"aggregatable_trigger_data\": " + + tooManyKeys + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.PARSING_ERROR, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testBasicTriggerRequestWithAggregateValues() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String aggregatable_values = "{\"campaignCounts\":32768,\"geoValue\":1644}"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"aggregatable_values\": " + + aggregatable_values + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(new JSONObject(aggregatable_values).toString(), result.getAggregateValues()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void testBasicTriggerRequestWithAggregateTriggerData_rejectsTooManyValueKeys() + throws Exception { + StringBuilder tooManyKeys = new StringBuilder("{"); + int i = 0; + for (; i < 50; i++) { + tooManyKeys.append(String.format("\"key-%s\": 12345,", i)); + } + tooManyKeys.append(String.format("\"key-%s\": 12345}", i)); + RegistrationRequest request = buildRequest(TRIGGER_URI); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"aggregatable_values\": " + tooManyKeys + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.PARSING_ERROR, asyncFetchStatus.getStatus()); + assertFalse(fetch.isPresent()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void fetchTrigger_withReportingFilters_success() throws IOException, JSONException { + // Setup + String filters = + "{\n" + + " \"key_1\": [\"value_1\", \"value_2\"],\n" + + " \"key_2\": [\"value_1\", \"value_2\"]\n" + + "}"; + RegistrationRequest request = buildRequest(TRIGGER_URI); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"filters\": " + filters + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(new JSONObject(filters).toString(), result.getFilters()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void fetchWebTriggers_basic_success() throws IOException, JSONException { + // Setup + WebTriggerRegistrationRequest request = + buildWebTriggerRegistrationRequest( + Arrays.asList(TRIGGER_REGISTRATION_1), TOP_ORIGIN); + doReturn(mUrlConnection1).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); + when(mUrlConnection1.getResponseCode()).thenReturn(200); + Map<String, List<String>> headersRequest = new HashMap<>(); + headersRequest.put( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"event_trigger_data\": " + + EVENT_TRIGGERS_1 + + ", \"debug_key\": \"" + + DEBUG_KEY + + "\"" + + "}")); + when(mUrlConnection1.getHeaderFields()).thenReturn(headersRequest); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = mFetcher.fetchTrigger( + webTriggerRegistrationRequest(request, true), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(new JSONArray(EVENT_TRIGGERS_1).toString(), result.getEventTriggers()); + verify(mUrlConnection1).setRequestMethod("POST"); + } + + @Test + public void fetchWebTriggers_withExtendedHeaders_success() throws IOException, JSONException { + // Setup + WebTriggerRegistrationRequest request = + buildWebTriggerRegistrationRequest( + Collections.singletonList(TRIGGER_REGISTRATION_1), TOP_ORIGIN); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); + when(mUrlConnection.getResponseCode()).thenReturn(200); + String aggregatableValues = "{\"campaignCounts\":32768,\"geoValue\":1644}"; + String filters = + "{\n" + + " \"key_1\": [\"value_1\", \"value_2\"],\n" + + " \"key_2\": [\"value_1\", \"value_2\"]\n" + + "}"; + String aggregatableTriggerData = + "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," + + "\"filters\":" + + "{\"conversion_subdomain\":[\"electronics.megastore\"]}," + + "\"not_filters\":{\"product\":[\"1\"]}}," + + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"event_trigger_data\": " + + EVENT_TRIGGERS_1 + + ", \"filters\": " + + filters + + ", \"aggregatable_values\": " + + aggregatableValues + + ", \"aggregatable_trigger_data\": " + + aggregatableTriggerData + + "}"))); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = mFetcher.fetchTrigger( + webTriggerRegistrationRequest(request, true), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(new JSONArray(EVENT_TRIGGERS_1).toString(), result.getEventTriggers()); + assertEquals(new JSONArray(aggregatableTriggerData).toString(), + result.getAggregateTriggerData()); + assertEquals(new JSONObject(aggregatableValues).toString(), result.getAggregateValues()); + assertEquals(new JSONObject(filters).toString(), result.getFilters()); + verify(mUrlConnection).setRequestMethod("POST"); + } + + @Test + public void fetchWebTriggers_withRedirects_ignoresRedirects() + throws IOException, JSONException { + // Setup + WebTriggerRegistrationRequest request = + buildWebTriggerRegistrationRequest( + Collections.singletonList(TRIGGER_REGISTRATION_1), TOP_ORIGIN); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"event_trigger_data\": " + EVENT_TRIGGERS_1 + "}"), + "Attribution-Reporting-Redirect", + List.of(LIST_TYPE_REDIRECT_URI), + "Location", + List.of(LOCATION_TYPE_REDIRECT_URI))); + + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = mFetcher.fetchTrigger( + webTriggerRegistrationRequest(request, true), asyncFetchStatus, asyncRedirect); + // Assertion + assertEquals(AsyncFetchStatus.ResponseStatus.SUCCESS, asyncFetchStatus.getStatus()); + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(AsyncRegistration.RedirectType.NONE, asyncRedirect.getRedirectType()); + assertEquals(0, asyncRedirect.getRedirects().size()); + assertEquals(new JSONArray(EVENT_TRIGGERS_1).toString(), result.getEventTriggers()); + verify(mUrlConnection).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequestWithAggregateTriggerData_invalidJson() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String aggregatable_trigger_data = + "[{\"key_piece\":\"0x400\",\"source_keys\":\"campaignCounts\"]," + + "\"filters\":" + + "{\"conversion_subdomain\":[\"electronics.megastore\"]}," + + "\"not_filters\":{\"product\":[\"1\"]}}," + + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"aggregatable_trigger_data\": " + + aggregatable_trigger_data + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequestWithAggregateTriggerData_tooManyEntries() throws Exception { + StringBuilder tooManyEntries = new StringBuilder("["); + for (int i = 0; i < MAX_AGGREGATABLE_TRIGGER_DATA + 1; i++) { + tooManyEntries.append( + String.format( + "{\"key_piece\": \"0x15%1$s\",\"source_keys\":[\"campaign-%1$s\"]}", + i)); + } + tooManyEntries.append("]"); + RegistrationRequest request = buildRequest(TRIGGER_URI); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"aggregatable_trigger_data\": " + + tooManyEntries + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequestWithAggregateTriggerData_invalidKeyPiece_missingPrefix() + throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String aggregatable_trigger_data = + "[{\"key_piece\":\"0400\",\"source_keys\":[\"campaignCounts\"]," + + "\"filters\":" + + "{\"conversion_subdomain\":[\"electronics.megastore\"]}," + + "\"not_filters\":{\"product\":[\"1\"]}}," + + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"aggregatable_trigger_data\": " + + aggregatable_trigger_data + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequestWithAggregateTriggerData_invalidKeyPiece_tooLong() + throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String aggregatable_trigger_data = + "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," + + "\"filters\":" + + "{\"conversion_subdomain\":[\"electronics.megastore\"]}," + + "\"not_filters\":{\"product\":[\"1\"]}}," + + "{\"key_piece\":\"" + + LONG_AGGREGATE_KEY_PIECE + + "\",\"source_keys\":[\"geoValue\"]}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"aggregatable_trigger_data\": " + + aggregatable_trigger_data + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequestWithAggregateTriggerData_sourceKeys_notAnArray() + throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String aggregatable_trigger_data = + "[{\"key_piece\":\"0x400\",\"source_keys\":{\"campaignCounts\": true}," + + "\"filters\":" + + "{\"conversion_subdomain\":[\"electronics.megastore\"]}," + + "\"not_filters\":{\"product\":[\"1\"]}}," + + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"aggregatable_trigger_data\": " + + aggregatable_trigger_data + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequestWithAggregateTriggerData_sourceKeys_tooManyKeys() + throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + StringBuilder tooManyKeys = new StringBuilder("["); + tooManyKeys.append( + IntStream.range(0, MAX_AGGREGATE_KEYS_PER_REGISTRATION + 1) + .mapToObj(i -> "aggregate-key-" + i) + .collect(Collectors.joining(","))); + tooManyKeys.append("]"); + String aggregatable_trigger_data = + "[{\"key_piece\":\"0x400\",\"source_keys\": " + + tooManyKeys + + "," + + "\"filters\":" + + "{\"conversion_subdomain\":[\"electronics.megastore\"]}," + + "\"not_filters\":{\"product\":[\"1\"]}}," + + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"aggregatable_trigger_data\": " + + aggregatable_trigger_data + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + + @Test + public void testTriggerRequestWithAggregateTriggerData_sourceKeys_invalidKeyId() + throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String aggregatable_trigger_data = + "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\", \"" + + LONG_AGGREGATE_KEY_ID + + "\"]," + + "\"filters\":" + + "{\"conversion_subdomain\":[\"electronics.megastore\"]}," + + "\"not_filters\":{\"product\":[\"1\"]}}," + + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"aggregatable_trigger_data\": " + + aggregatable_trigger_data + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + @Test + public void testTriggerRequestWithAggregateTriggerData_filters_tooManyFilters() + throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + StringBuilder filters = new StringBuilder("{"); + filters.append( + IntStream.range(0, MAX_ATTRIBUTION_FILTERS + 1) + .mapToObj(i -> "\"filter-string-" + i + "\": [\"filter-value\"]") + .collect(Collectors.joining(","))); + filters.append("}"); + String aggregatable_trigger_data = + "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," + + "\"filters\": " + + filters + + "," + + "\"not_filters\":{\"product\":[\"1\"]}}," + + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"aggregatable_trigger_data\": " + + aggregatable_trigger_data + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + @Test + public void testTriggerRequestWithAggregateTriggerData_filters_keyTooLong() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String filters = + "{\"product\":[\"1234\",\"2345\"], \"" + LONG_FILTER_STRING + "\":[\"id\"]}"; + String aggregatable_trigger_data = + "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," + + "\"filters\": " + + filters + + "," + + "\"not_filters\":{\"product\":[\"1\"]}}," + + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"aggregatable_trigger_data\": " + + aggregatable_trigger_data + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + @Test + public void testTriggerRequestWithAggregateTriggerData_filters_tooManyValues() + throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + StringBuilder filters = + new StringBuilder( + "{" + + "\"filter-string-1\": [\"filter-value-1\"]," + + "\"filter-string-2\": ["); + filters.append( + IntStream.range(0, MAX_VALUES_PER_ATTRIBUTION_FILTER + 1) + .mapToObj(i -> "\"filter-value-" + i + "\"") + .collect(Collectors.joining(","))); + filters.append("]}"); + String aggregatable_trigger_data = + "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," + + "\"filters\": " + + filters + + "," + + "\"not_filters\":{\"product\":[\"1\"]}}," + + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"aggregatable_trigger_data\": " + + aggregatable_trigger_data + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + @Test + public void testTriggerRequestWithAggregateTriggerData_filters_valueTooLong() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String filters = + "{\"product\":[\"1234\",\"" + LONG_FILTER_STRING + "\"], \"ctid\":[\"id\"]}"; + String aggregatable_trigger_data = + "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," + + "\"filters\": " + + filters + + "," + + "\"not_filters\":{\"product\":[\"1\"]}}," + + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"aggregatable_trigger_data\": " + + aggregatable_trigger_data + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + @Test + public void testTriggerRequestWithAggregateTriggerData_notFilters_tooManyFilters() + throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + StringBuilder filters = new StringBuilder("{"); + filters.append( + IntStream.range(0, MAX_ATTRIBUTION_FILTERS + 1) + .mapToObj(i -> "\"filter-string-" + i + "\": [\"filter-value\"]") + .collect(Collectors.joining(","))); + filters.append("}"); + String aggregatable_trigger_data = + "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," + + "\"not_filters\": " + + filters + + "," + + "\"filters\":{\"product\":[\"1\"]}}," + + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"aggregatable_trigger_data\": " + + aggregatable_trigger_data + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + @Test + public void testTriggerRequestWithAggregateTriggerData_notFilters_keyTooLong() + throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String filters = + "{\"product\":[\"1234\",\"2345\"], \"" + LONG_FILTER_STRING + "\":[\"id\"]}"; + String aggregatable_trigger_data = + "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," + + "\"not_filters\": " + + filters + + "," + + "\"filters\":{\"product\":[\"1\"]}}," + + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"aggregatable_trigger_data\": " + + aggregatable_trigger_data + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + @Test + public void testTriggerRequestWithAggregateTriggerData_notFilters_tooManyValues() + throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + StringBuilder filters = + new StringBuilder( + "{" + + "\"filter-string-1\": [\"filter-value-1\"]," + + "\"filter-string-2\": ["); + filters.append( + IntStream.range(0, MAX_VALUES_PER_ATTRIBUTION_FILTER + 1) + .mapToObj(i -> "\"filter-value-" + i + "\"") + .collect(Collectors.joining(","))); + filters.append("]}"); + String aggregatable_trigger_data = + "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," + + "\"not_filters\": " + + filters + + "," + + "\"filters\":{\"product\":[\"1\"]}}," + + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"aggregatable_trigger_data\": " + + aggregatable_trigger_data + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + @Test + public void testTriggerRequestWithAggregateTriggerData_notFilters_valueTooLong() + throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String filters = + "{\"product\":[\"1234\",\"" + LONG_FILTER_STRING + "\"], \"ctid\":[\"id\"]}"; + String aggregatable_trigger_data = + "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," + + "\"not_filters\": " + + filters + + "," + + "\"filters\":{\"product\":[\"1\"]}}," + + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"aggregatable_trigger_data\": " + + aggregatable_trigger_data + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + @Test + public void testBasicTriggerRequestWithAggregatableValues() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String aggregatable_values = "{\"campaignCounts\":32768,\"geoValue\":1644}"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"aggregatable_values\": " + + aggregatable_values + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(new JSONObject(aggregatable_values).toString(), result.getAggregateValues()); + verify(mUrlConnection).setRequestMethod("POST"); + } + @Test + public void testTriggerRequestWithAggregatableValues_invalidJson() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String aggregatable_values = "{\"campaignCounts\":32768\"geoValue\":1644}"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"aggregatable_values\": " + + aggregatable_values + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + @Test + public void testTriggerRequestWithAggregatableValues_tooManyKeys() throws Exception { + StringBuilder tooManyKeys = new StringBuilder("{"); + tooManyKeys.append( + IntStream.range(0, MAX_AGGREGATE_KEYS_PER_REGISTRATION + 1) + .mapToObj(i -> String.format("\"key-%s\": 12345,", i)) + .collect(Collectors.joining(","))); + tooManyKeys.append("}"); + RegistrationRequest request = buildRequest(TRIGGER_URI); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"aggregatable_values\": " + tooManyKeys + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + @Test + public void testTriggerRequestWithAggregatableValues_invalidKeyId() throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + String aggregatable_values = + "{\"campaignCounts\":32768, \"" + LONG_AGGREGATE_KEY_ID + "\":1644}"; + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"aggregatable_values\": " + + aggregatable_values + + "}"))); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + assertFalse(fetch.isPresent()); + verify(mUrlConnection, times(1)).setRequestMethod("POST"); + verify(mFetcher, times(1)).openUrl(any()); + } + @Test + public void fetchWebTriggerSuccessWithoutAdIdPermission() throws IOException, JSONException { + // Setup + WebTriggerRegistrationRequest request = + buildWebTriggerRegistrationRequest( + Arrays.asList(TRIGGER_REGISTRATION_1, TRIGGER_REGISTRATION_2), TOP_ORIGIN); + doReturn(mUrlConnection1).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); + when(mUrlConnection1.getResponseCode()).thenReturn(200); + Map<String, List<String>> headersRequest = new HashMap<>(); + headersRequest.put( + "Attribution-Reporting-Register-Trigger", + List.of( + "{" + + "\"event_trigger_data\": " + + EVENT_TRIGGERS_1 + + ", \"debug_key\": \"" + + DEBUG_KEY + + "\"" + + "}")); + when(mUrlConnection1.getHeaderFields()).thenReturn(headersRequest); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = mFetcher.fetchTrigger( + webTriggerRegistrationRequest(request, false), asyncFetchStatus, asyncRedirect); + // Assertion + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(TOP_ORIGIN, result.getAttributionDestination().toString()); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(new JSONArray(EVENT_TRIGGERS_1).toString(), result.getEventTriggers()); + verify(mUrlConnection1).setRequestMethod("POST"); + } + @Test + public void basicTriggerRequest_headersMoreThanMaxResponseSize_emitsMetricsWithAdTechDomain() + throws Exception { + RegistrationRequest request = buildRequest(TRIGGER_URI); + MeasurementRegistrationResponseStats expectedStats = + new MeasurementRegistrationResponseStats.Builder( + AD_SERVICES_MEASUREMENT_REGISTRATIONS, + AD_SERVICES_MEASUREMENT_REGISTRATIONS__TYPE__TRIGGER, + 221) + .setAdTechDomain(TRIGGER_URI) + .build(); + doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); + when(mUrlConnection.getResponseCode()).thenReturn(200); + when(mUrlConnection.getHeaderFields()) + .thenReturn( + Map.of( + "Attribution-Reporting-Register-Trigger", + List.of("{" + "\"event_trigger_data\":" + EVENT_TRIGGERS_1 + "}"))); + doReturn(5L).when(mFlags).getMaxResponseBasedRegistrationPayloadSizeBytes(); + AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); + // Execution + Optional<Trigger> fetch = + mFetcher.fetchTrigger( + appTriggerRegistrationRequest(request), asyncFetchStatus, asyncRedirect); + assertTrue(fetch.isPresent()); + Trigger result = fetch.get(); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(new JSONArray(EVENT_TRIGGERS_1).toString(), result.getEventTriggers()); + verify(mUrlConnection).setRequestMethod("POST"); + verify(mLogger).logMeasurementRegistrationsResponseSize(eq(expectedStats)); + } + private RegistrationRequest buildRequest(String triggerUri) { + return new RegistrationRequest.Builder() + .setRegistrationType(RegistrationRequest.REGISTER_TRIGGER) + .setRegistrationUri(Uri.parse(triggerUri)) + .setPackageName(CONTEXT.getAttributionSource().getPackageName()) + .setAdIdPermissionGranted(true) + .build(); + } + private RegistrationRequest buildRequestWithoutAdIdPermission(String triggerUri) { + return new RegistrationRequest.Builder() + .setRegistrationType(RegistrationRequest.REGISTER_TRIGGER) + .setRegistrationUri(Uri.parse(triggerUri)) + .setPackageName(CONTEXT.getAttributionSource().getPackageName()) + .setAdIdPermissionGranted(false) + .build(); + } + private WebTriggerRegistrationRequest buildWebTriggerRegistrationRequest( + List<WebTriggerParams> triggerParams, String topOrigin) { + return new WebTriggerRegistrationRequest.Builder(triggerParams, Uri.parse(topOrigin)) + .build(); + } + + public static AsyncRegistration appTriggerRegistrationRequest( + RegistrationRequest registrationRequest) { + return appTriggerRegistrationRequest(registrationRequest, + AsyncRegistration.RedirectType.ANY, 0); + } + + public static AsyncRegistration appTriggerRegistrationRequest( + RegistrationRequest registrationRequest, + @AsyncRegistration.RedirectType int redirectType, + int redirectCount) { + // Necessary for testing + String enrollmentId = ""; + if (EnrollmentDao.getInstance(CONTEXT) + .getEnrollmentDataFromMeasurementUrl( + registrationRequest + .getRegistrationUri() + .buildUpon() + .clearQuery() + .build()) + != null) { + enrollmentId = + EnrollmentDao.getInstance(CONTEXT) + .getEnrollmentDataFromMeasurementUrl( + registrationRequest + .getRegistrationUri() + .buildUpon() + .clearQuery() + .build()) + .getEnrollmentId(); + } + return createAsyncRegistration( + UUID.randomUUID().toString(), + enrollmentId, + registrationRequest.getRegistrationUri(), + null, + null, + Uri.parse(ANDROID_APP_SCHEME_URI_PREFIX + CONTEXT.getPackageName()), + null, + Uri.parse(ANDROID_APP_SCHEME_URI_PREFIX + CONTEXT.getPackageName()), + registrationRequest.getRegistrationType() == RegistrationRequest.REGISTER_SOURCE + ? AsyncRegistration.RegistrationType.APP_SOURCE + : AsyncRegistration.RegistrationType.APP_TRIGGER, + null, + System.currentTimeMillis(), + 0, + System.currentTimeMillis(), + redirectType, + redirectCount, + registrationRequest.isAdIdPermissionGranted()); + } + + private static AsyncRegistration webTriggerRegistrationRequest( + WebTriggerRegistrationRequest webTriggerRegistrationRequest, + boolean adIdPermissionGranted) { + if (webTriggerRegistrationRequest.getTriggerParams().size() > 0) { + WebTriggerParams webTriggerParams = + webTriggerRegistrationRequest.getTriggerParams().get(0); + // Necessary for testing + String enrollmentId = ""; + if (EnrollmentDao.getInstance(CONTEXT) + .getEnrollmentDataFromMeasurementUrl( + webTriggerRegistrationRequest + .getTriggerParams() + .get(0) + .getRegistrationUri() + .buildUpon() + .clearQuery() + .build()) + != null) { + enrollmentId = + EnrollmentDao.getInstance(CONTEXT) + .getEnrollmentDataFromMeasurementUrl( + webTriggerParams + .getRegistrationUri() + .buildUpon() + .clearQuery() + .build()) + .getEnrollmentId(); + } + return createAsyncRegistration( + UUID.randomUUID().toString(), + enrollmentId, + webTriggerParams.getRegistrationUri(), + null, + null, + Uri.parse(ANDROID_APP_SCHEME_URI_PREFIX + CONTEXT.getPackageName()), + null, + webTriggerRegistrationRequest.getDestination(), + AsyncRegistration.RegistrationType.WEB_TRIGGER, + null, + System.currentTimeMillis(), + 0, + System.currentTimeMillis(), + AsyncRegistration.RedirectType.NONE, + 0, + adIdPermissionGranted); + } + return null; + } + private static AsyncRegistration createAsyncRegistration( + String iD, + String enrollmentId, + Uri registrationUri, + Uri webDestination, + Uri osDestination, + Uri registrant, + Uri verifiedDestination, + Uri topOrigin, + AsyncRegistration.RegistrationType registrationType, + Source.SourceType sourceType, + long mRequestTime, + long mRetryCount, + long mLastProcessingTime, + @AsyncRegistration.RedirectType int redirectType, + int redirectCount, + boolean debugKeyAllowed) { + return new AsyncRegistration.Builder() + .setId(iD) + .setEnrollmentId(enrollmentId) + .setRegistrationUri(registrationUri) + .setWebDestination(webDestination) + .setOsDestination(osDestination) + .setRegistrant(registrant) + .setVerifiedDestination(verifiedDestination) + .setTopOrigin(topOrigin) + .setType(registrationType.ordinal()) + .setSourceType(sourceType) + .setRequestTime(mRequestTime) + .setRetryCount(mRetryCount) + .setLastProcessingTime(mLastProcessingTime) + .setRedirectType(redirectType) + .setRedirectCount(redirectCount) + .setDebugKeyAllowed(debugKeyAllowed) + .build(); + } + + private static Map<String, List<String>> getDefaultHeaders() { + Map<String, List<String>> headers = new HashMap<>(); + headers.put("Attribution-Reporting-Register-Trigger", + List.of("{" + "\"event_trigger_data\":" + EVENT_TRIGGERS_1 + "}")); + return headers; + } + + private static void assertSourceRegistration(AsyncRegistration asyncRegistration, + Trigger result) throws JSONException { + assertEquals( + asyncRegistration.getRegistrant().toString(), + result.getAttributionDestination().toString()); + assertEquals(ENROLLMENT_ID, result.getEnrollmentId()); + assertEquals(new JSONArray(EVENT_TRIGGERS_1).toString(), result.getEventTriggers()); + } +} diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/FetcherUtilTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/FetcherUtilTest.java index 4a76028b79..07af7b36a5 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/FetcherUtilTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/FetcherUtilTest.java @@ -32,6 +32,8 @@ import android.net.Uri; import androidx.test.filters.SmallTest; import com.android.adservices.service.Flags; +import com.android.adservices.service.measurement.AsyncRegistration; +import com.android.adservices.service.measurement.util.AsyncRedirect; import com.android.adservices.service.stats.AdServicesLogger; import com.android.adservices.service.stats.MeasurementRegistrationResponseStats; @@ -82,27 +84,49 @@ public final class FetcherUtilTest { } @Test - public void testParseRedirectsNothingInitial() { - List<Uri> redirs = FetcherUtil.parseRedirects(Map.of()); - assertEquals(0, redirs.size()); + public void parseRedirects_noRedirectHeaders_returnsEmpty() { + AsyncRedirect asyncRedirect = FetcherUtil.parseRedirects( + Map.of(), AsyncRegistration.RedirectType.ANY); + assertEquals(AsyncRegistration.RedirectType.NONE, asyncRedirect.getRedirectType()); + assertEquals(0, asyncRedirect.getRedirects().size()); } @Test - public void testParseRedirectsARR() { - List<Uri> redirs = - FetcherUtil.parseRedirects( - Map.of("Attribution-Reporting-Redirect", List.of("foo.com", "bar.com"))); - assertEquals(2, redirs.size()); - assertEquals(Uri.parse("foo.com"), redirs.get(0)); - assertEquals(Uri.parse("bar.com"), redirs.get(1)); + public void parseRedirects_bothHeaderTypes_choosesListType() { + AsyncRedirect asyncRedirect = FetcherUtil.parseRedirects( + Map.of( + "Attribution-Reporting-Redirect", List.of("foo.test", "bar.test"), + "Location", List.of("baz.test")), + AsyncRegistration.RedirectType.ANY); + assertEquals(AsyncRegistration.RedirectType.NONE, asyncRedirect.getRedirectType()); + List<Uri> redirects = asyncRedirect.getRedirects(); + assertEquals(2, redirects.size()); + assertEquals(Uri.parse("foo.test"), redirects.get(0)); + assertEquals(Uri.parse("bar.test"), redirects.get(1)); } @Test - public void testParseRedirectsSingleElementARR() { - List<Uri> redirs = - FetcherUtil.parseRedirects( - Map.of("Attribution-Reporting-Redirect", List.of("foo.com"))); - assertEquals(1, redirs.size()); + public void parseRedirects_locationHeaderOnly_choosesLocationType() { + AsyncRedirect asyncRedirect = FetcherUtil.parseRedirects( + Map.of("Location", List.of("foo.test")), + AsyncRegistration.RedirectType.ANY); + assertEquals(AsyncRegistration.RedirectType.DAISY_CHAIN, asyncRedirect.getRedirectType()); + List<Uri> redirects = asyncRedirect.getRedirects(); + assertEquals(1, redirects.size()); + assertEquals(Uri.parse("foo.test"), redirects.get(0)); + } + + @Test + public void parseRedirects_bothHeaderTypes_providedLocationType_choosesLocationType() { + AsyncRedirect asyncRedirect = FetcherUtil.parseRedirects( + Map.of( + "Attribution-Reporting-Redirect", List.of("foo.test", "bar.test"), + "Location", List.of("baz.test")), + AsyncRegistration.RedirectType.DAISY_CHAIN); + assertEquals(AsyncRegistration.RedirectType.DAISY_CHAIN, asyncRedirect.getRedirectType()); + List<Uri> redirects = asyncRedirect.getRedirects(); + assertEquals(1, redirects.size()); + assertEquals(Uri.parse("baz.test"), redirects.get(0)); } @Test diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/SourceFetcherTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/SourceFetcherTest.java deleted file mode 100644 index fbc3c935d5..0000000000 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/SourceFetcherTest.java +++ /dev/null @@ -1,1937 +0,0 @@ -/* - * 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.measurement.registration; - -import static com.android.adservices.service.measurement.PrivacyParams.MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS; -import static com.android.adservices.service.measurement.PrivacyParams.MIN_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS; -import static com.android.adservices.service.measurement.SystemHealthParams.MAX_AGGREGATE_KEYS_PER_REGISTRATION; -import static com.android.adservices.service.measurement.SystemHealthParams.MAX_ATTRIBUTION_FILTERS; -import static com.android.adservices.service.measurement.SystemHealthParams.MAX_VALUES_PER_ATTRIBUTION_FILTER; -import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_MEASUREMENT_REGISTRATIONS; -import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_MEASUREMENT_REGISTRATIONS__TYPE__SOURCE; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.adservices.measurement.RegistrationRequest; -import android.adservices.measurement.WebSourceParams; -import android.adservices.measurement.WebSourceRegistrationRequest; -import android.content.Context; -import android.net.Uri; - -import androidx.test.filters.SmallTest; -import androidx.test.platform.app.InstrumentationRegistry; - -import com.android.adservices.data.enrollment.EnrollmentDao; -import com.android.adservices.service.Flags; -import com.android.adservices.service.enrollment.EnrollmentData; -import com.android.adservices.service.stats.AdServicesLogger; -import com.android.adservices.service.stats.MeasurementRegistrationResponseStats; - -import org.json.JSONArray; -import org.json.JSONException; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import java.io.IOException; -import java.net.URL; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import javax.net.ssl.HttpsURLConnection; - -/** Unit tests for {@link SourceFetcher} */ -@RunWith(MockitoJUnitRunner.class) -@SmallTest -public final class SourceFetcherTest { - private static final String DEFAULT_REGISTRATION = "https://foo.com"; - private static final String ENROLLMENT_ID = "enrollment-id"; - private static final EnrollmentData ENROLLMENT = new EnrollmentData.Builder() - .setEnrollmentId("enrollment-id") - .build(); - private static final String DEFAULT_TOP_ORIGIN = "https://baz.com"; - private static final String DEFAULT_DESTINATION = "android-app://com.myapps"; - private static final String DEFAULT_DESTINATION_WITHOUT_SCHEME = "com.myapps"; - private static final long DEFAULT_PRIORITY = 123; - private static final long DEFAULT_EXPIRY = 456789; - private static final long DEFAULT_EVENT_ID = 987654321; - private static final long EVENT_ID_1 = 987654321; - private static final long EVENT_ID_2 = 987654322; - private static final Long DEBUG_KEY = 823523783L; - private static final String ALT_REGISTRATION = "https://bar.com"; - private static final String ALT_DESTINATION = "android-app://com.yourapps"; - private static final long ALT_PRIORITY = 321; - private static final long ALT_EVENT_ID = 123456789; - private static final long ALT_EXPIRY = 456790; - private static final Uri REGISTRATION_URI_1 = Uri.parse("https://foo.com"); - private static final Uri REGISTRATION_URI_2 = Uri.parse("https://foo2.com"); - private static final Uri OS_DESTINATION = Uri.parse("android-app://com.os-destination"); - private static final Uri OS_DESTINATION_WITH_PATH = - Uri.parse("android-app://com.os-destination/my/path"); - private static final Uri WEB_DESTINATION = Uri.parse("https://web-destination.com"); - private static final Uri WEB_DESTINATION_WITH_SUBDOMAIN = - Uri.parse("https://subdomain.web-destination.com"); - private static final String LONG_FILTER_STRING = "12345678901234567890123456"; - private static final String LONG_AGGREGATE_KEY_ID = "12345678901234567890123456"; - private static final String LONG_AGGREGATE_KEY_PIECE = "0x123456789012345678901234567890123"; - private static final WebSourceParams SOURCE_REGISTRATION_1 = - new WebSourceParams.Builder(REGISTRATION_URI_1).setDebugKeyAllowed(true).build(); - private static final WebSourceParams SOURCE_REGISTRATION_2 = - new WebSourceParams.Builder(REGISTRATION_URI_2).setDebugKeyAllowed(false).build(); - - private static final Context sContext = - InstrumentationRegistry.getInstrumentation().getContext(); - - SourceFetcher mFetcher; - @Mock HttpsURLConnection mUrlConnection; - - @Mock HttpsURLConnection mUrlConnection1; - @Mock HttpsURLConnection mUrlConnection2; - @Mock EnrollmentDao mEnrollmentDao; - @Mock Flags mFlags; - @Mock AdServicesLogger mLogger; - - @Before - public void setup() { - mFetcher = spy(new SourceFetcher(mEnrollmentDao, mFlags, mLogger)); - // For convenience, return the same enrollment-ID since we're using many arbitrary - // registration URIs and not yet enforcing uniqueness of enrollment. - when(mEnrollmentDao.getEnrollmentDataFromMeasurementUrl(any())).thenReturn(ENROLLMENT); - } - - @Test - public void testBasicSourceRequest() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); - doReturn(5000L).when(mFlags).getMaxResponseBasedRegistrationPayloadSizeBytes(); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + " \"destination\": \"" - + DEFAULT_DESTINATION - + "\",\n" - + " \"priority\": \"" - + DEFAULT_PRIORITY - + "\",\n" - + " \"expiry\": \"" - + DEFAULT_EXPIRY - + "\",\n" - + " \"source_event_id\": \"" - + DEFAULT_EVENT_ID - + "\",\n" - + " \"debug_key\": \"" - + DEBUG_KEY - + "\"\n" - + "}\n"))); - - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals(DEFAULT_TOP_ORIGIN, result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals(DEFAULT_DESTINATION, result.get(0).getAppDestination().toString()); - assertEquals(DEFAULT_PRIORITY, result.get(0).getSourcePriority()); - assertEquals(DEFAULT_EXPIRY, result.get(0).getExpiry()); - assertEquals(DEFAULT_EVENT_ID, result.get(0).getSourceEventId()); - assertEquals(DEBUG_KEY, result.get(0).getDebugKey()); - verify(mLogger) - .logMeasurementRegistrationsResponseSize( - eq( - new MeasurementRegistrationResponseStats.Builder( - AD_SERVICES_MEASUREMENT_REGISTRATIONS, - AD_SERVICES_MEASUREMENT_REGISTRATIONS__TYPE__SOURCE, - 190) - .setAdTechDomain(null) - .build())); - - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void testBasicSourceRequest_failsWhenNotEnrolled() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - when(mEnrollmentDao.getEnrollmentDataFromMeasurementUrl(any())).thenReturn(null); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + " \"destination\": \"" - + DEFAULT_DESTINATION - + "\",\n" - + " \"priority\": \"" - + DEFAULT_PRIORITY - + "\",\n" - + " \"expiry\": \"" - + DEFAULT_EXPIRY - + "\",\n" - + " \"source_event_id\": \"" - + DEFAULT_EVENT_ID - + "\",\n" - + " \"debug_key\": \"" - + DEBUG_KEY - + "\"\n" - + "}\n"))); - - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertFalse(fetch.isPresent()); - verify(mFetcher, never()).openUrl(any()); - } - - @Test - public void testBasicSourceRequestWithoutAdIdPermission() throws Exception { - RegistrationRequest request = - buildRequestWithoutAdIdPermission(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + " \"destination\": \"" - + DEFAULT_DESTINATION - + "\",\n" - + " \"priority\": \"" - + DEFAULT_PRIORITY - + "\",\n" - + " \"expiry\": \"" - + DEFAULT_EXPIRY - + "\",\n" - + " \"source_event_id\": \"" - + DEFAULT_EVENT_ID - + "\",\n" - + " \"debug_key\": \"" - + DEBUG_KEY - + "\"\n" - + "}\n"))); - - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals(DEFAULT_TOP_ORIGIN, result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals(DEFAULT_DESTINATION, result.get(0).getAppDestination().toString()); - assertEquals(DEFAULT_PRIORITY, result.get(0).getSourcePriority()); - assertEquals(DEFAULT_EXPIRY, result.get(0).getExpiry()); - assertEquals(DEFAULT_EVENT_ID, result.get(0).getSourceEventId()); - assertNull(result.get(0).getDebugKey()); - - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void testSourceRequestWithPostInstallAttributes() throws Exception { - RegistrationRequest request = - new RegistrationRequest.Builder() - .setRegistrationType(RegistrationRequest.REGISTER_SOURCE) - .setRegistrationUri(Uri.parse("https://foo.com")) - .setTopOriginUri(Uri.parse("https://baz.com")) - .setPackageName(sContext.getAttributionSource().getPackageName()) - .build(); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL("https://foo.com")); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn(Map.of("Attribution-Reporting-Register-Source", - List.of("{\n" - + " \"destination\": \"android-app://com.myapps\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - + " \"install_attribution_window\": \"272800\",\n" - + " \"post_install_exclusivity_window\": \"987654\"\n" - + "}\n"))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals("https://baz.com", result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals("android-app://com.myapps", result.get(0).getAppDestination().toString()); - assertEquals(123, result.get(0).getSourcePriority()); - assertEquals(456789, result.get(0).getExpiry()); - assertEquals(987654321, result.get(0).getSourceEventId()); - assertEquals(272800, result.get(0).getInstallAttributionWindow()); - assertEquals(987654L, result.get(0).getInstallCooldownWindow()); - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void testSourceRequestWithPostInstallAttributesReceivedAsNull() throws Exception { - RegistrationRequest request = - new RegistrationRequest.Builder() - .setRegistrationType(RegistrationRequest.REGISTER_SOURCE) - .setRegistrationUri(Uri.parse("https://foo.com")) - .setTopOriginUri(Uri.parse("https://baz.com")) - .setPackageName(sContext.getAttributionSource().getPackageName()) - .build(); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL("https://foo.com")); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + " \"destination\": \"android-app://com" - + ".myapps\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - + " \"install_attribution_window\": null,\n" - + " \"post_install_exclusivity_window\": null\n" - + "}\n"))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals("https://baz.com", result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals("android-app://com.myapps", result.get(0).getAppDestination().toString()); - assertEquals(123, result.get(0).getSourcePriority()); - assertEquals(456789, result.get(0).getExpiry()); - assertEquals(987654321, result.get(0).getSourceEventId()); - // fallback to default value - 30 days - assertEquals(2592000L, result.get(0).getInstallAttributionWindow()); - // fallback to default value - 0 days - assertEquals(0L, result.get(0).getInstallCooldownWindow()); - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void testSourceRequestWithInstallAttributesOutofBounds() throws IOException { - RegistrationRequest request = - new RegistrationRequest.Builder() - .setRegistrationType(RegistrationRequest.REGISTER_SOURCE) - .setRegistrationUri(Uri.parse("https://foo.com")) - .setTopOriginUri(Uri.parse("https://baz.com")) - .setPackageName(sContext.getAttributionSource().getPackageName()) - .build(); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL("https://foo.com")); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn(Map.of("Attribution-Reporting-Register-Source", - List.of("{\n" - + " \"destination\": \"android-app://com.myapps\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - // Min value of attribution is 2 days or 172800 seconds - + " \"install_attribution_window\": \"172700\",\n" - // Max value of cooldown is 30 days or 2592000 seconds - + " \"post_install_exclusivity_window\": \"9876543210\"\n" - + "}\n"))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals("https://baz.com", result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals("android-app://com.myapps", result.get(0).getAppDestination().toString()); - assertEquals(123, result.get(0).getSourcePriority()); - assertEquals(456789, result.get(0).getExpiry()); - assertEquals(987654321, result.get(0).getSourceEventId()); - // Adjusted to minimum allowed value - assertEquals(172800, result.get(0).getInstallAttributionWindow()); - // Adjusted to maximum allowed value - assertEquals(2592000L, result.get(0).getInstallCooldownWindow()); - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void testBadSourceUrl() { - RegistrationRequest request = buildRequest( - /* registrationUri = */ "bad-schema://foo.com", DEFAULT_TOP_ORIGIN); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertFalse(fetch.isPresent()); - } - - @Test - public void testBadSourceConnection() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doThrow(new IOException("Bad internet things")).when(mFetcher).openUrl( - new URL(DEFAULT_REGISTRATION) - ); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertFalse(fetch.isPresent()); - } - - @Test - public void testBadSourceJson_missingSourceEventId() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn(Map.of("Attribution-Reporting-Register-Source", - List.of("{\n" - + "\"source_event_id\": \"" + DEFAULT_EVENT_ID + "\""))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testBadSourceJson_missingHeader() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()).thenReturn(Collections.emptyMap()); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testBadSourceJson_missingDestination() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn(Map.of("Attribution-Reporting-Register-Source", - List.of("{\n" - + "\"destination\": \"" + DEFAULT_DESTINATION + "\""))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testBasicSourceRequestMinimumFields() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn(Map.of("Attribution-Reporting-Register-Source", - List.of("{\n" - + "\"destination\": \"" + DEFAULT_DESTINATION + "\",\n" - + "\"source_event_id\": \"" + DEFAULT_EVENT_ID + "\"\n" - + "}\n"))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals(DEFAULT_TOP_ORIGIN, result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals(DEFAULT_DESTINATION, result.get(0).getAppDestination().toString()); - assertEquals(DEFAULT_EVENT_ID, result.get(0).getSourceEventId()); - assertEquals(0, result.get(0).getSourcePriority()); - assertEquals( - MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS, result.get(0).getExpiry()); - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void testBasicSourceRequestMinimumFieldsAndRestNull() throws Exception { - RegistrationRequest request = - new RegistrationRequest.Builder() - .setRegistrationType(RegistrationRequest.REGISTER_SOURCE) - .setRegistrationUri(Uri.parse("https://foo.com")) - .setTopOriginUri(Uri.parse("https://baz.com")) - .setPackageName(sContext.getAttributionSource().getPackageName()) - .build(); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL("https://foo.com")); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn(Map.of("Attribution-Reporting-Register-Source", - List.of("{\n" - + "\"destination\": \"android-app://com.myapps\",\n" - + "\"source_event_id\": \"123\",\n" - + "\"priority\": null,\n" - + "\"expiry\": null\n" - + "}\n"))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals("https://baz.com", result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals("android-app://com.myapps", result.get(0).getAppDestination().toString()); - assertEquals(123, result.get(0).getSourceEventId()); - assertEquals(0, result.get(0).getSourcePriority()); - assertEquals(MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS, - result.get(0).getExpiry()); - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void testBasicSourceRequestWithExpiryLessThan2Days() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn(Map.of("Attribution-Reporting-Register-Source", - List.of("{\n" - + "\"destination\": \"" + DEFAULT_DESTINATION + "\",\n" - + "\"source_event_id\": \"" + DEFAULT_EVENT_ID + "\",\n" - + "\"expiry\": 1" - + "}\n"))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals(DEFAULT_TOP_ORIGIN, result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals(DEFAULT_DESTINATION, result.get(0).getAppDestination().toString()); - assertEquals(DEFAULT_EVENT_ID, result.get(0).getSourceEventId()); - assertEquals(0, result.get(0).getSourcePriority()); - assertEquals( - MIN_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS, result.get(0).getExpiry()); - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void testBasicSourceRequestWithExpiryMoreThan30Days() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn(Map.of("Attribution-Reporting-Register-Source", - List.of("{\n" - + "\"destination\": \"" + DEFAULT_DESTINATION + "\",\n" - + "\"source_event_id\": \"" + DEFAULT_EVENT_ID + "\",\n" - + "\"expiry\": 2678400" - + "}\n"))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals(DEFAULT_TOP_ORIGIN, result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals(DEFAULT_DESTINATION, result.get(0).getAppDestination().toString()); - assertEquals(DEFAULT_EVENT_ID, result.get(0).getSourceEventId()); - assertEquals(0, result.get(0).getSourcePriority()); - assertEquals( - MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS, result.get(0).getExpiry()); - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void testNotOverHttps() throws Exception { - RegistrationRequest request = buildRequest("http://foo.com", DEFAULT_TOP_ORIGIN); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertFalse(fetch.isPresent()); - verify(mFetcher, never()).openUrl(any()); - } - - @Test - public void testFirst200Next500_ignoreFailureReturnSuccess() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); - when(mUrlConnection.getResponseCode()).thenReturn(200).thenReturn(500); - - Map<String, List<String>> headersFirstRequest = new HashMap<>(); - headersFirstRequest.put("Attribution-Reporting-Register-Source", List.of("{\n" - + "\"destination\": \"" + DEFAULT_DESTINATION + "\",\n" - + "\"source_event_id\": \"" + DEFAULT_EVENT_ID + "\",\n" - + "\"expiry\": " + DEFAULT_EXPIRY + "" - + "}\n")); - headersFirstRequest.put("Attribution-Reporting-Redirect", List.of("https://bar.com")); - - Map<String, List<String>> headersSecondRequest = new HashMap<>(); - headersSecondRequest.put("Attribution-Reporting-Register-Source", List.of("{\n" - + "\"destination\": \"" + DEFAULT_DESTINATION + "\",\n" - + "\"source_event_id\": \"" + ALT_EVENT_ID + "\",\n" - + "\"expiry\": " + ALT_EXPIRY + "" - + "}\n")); - - when(mUrlConnection.getHeaderFields()).thenReturn(headersFirstRequest).thenReturn( - headersSecondRequest); - - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals(DEFAULT_TOP_ORIGIN, result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals(DEFAULT_DESTINATION, result.get(0).getAppDestination().toString()); - assertEquals(DEFAULT_EVENT_ID, result.get(0).getSourceEventId()); - assertEquals(0, result.get(0).getSourcePriority()); - assertEquals(DEFAULT_EXPIRY, result.get(0).getExpiry()); - verify(mUrlConnection, times(2)).setRequestMethod("POST"); - } - - @Test - public void testFailedParsingButValidRedirect_returnFailure() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - - Map<String, List<String>> headersFirstRequest = new HashMap<>(); - headersFirstRequest.put("Attribution-Reporting-Register-Source", List.of("{}")); - headersFirstRequest.put("Attribution-Reporting-Redirect", List.of("https://bar.com")); - - Map<String, List<String>> headersSecondRequest = new HashMap<>(); - headersSecondRequest.put("Attribution-Reporting-Register-Source", List.of("{\n" - + "\"destination\": \"" + DEFAULT_DESTINATION + "\",\n" - + "\"source_event_id\": \"" + ALT_EVENT_ID + "\",\n" - + "\"expiry\": " + ALT_EXPIRY + "" - + "}\n")); - - when(mUrlConnection.getHeaderFields()).thenReturn(headersFirstRequest).thenReturn( - headersSecondRequest); - - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testRedirectDifferentDestination_keepAllReturnSuccess() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - - Map<String, List<String>> headersFirstRequest = new HashMap<>(); - headersFirstRequest.put("Attribution-Reporting-Register-Source", List.of("{\n" - + "\"destination\": \"" + DEFAULT_DESTINATION + "\",\n" - + "\"source_event_id\": \"" + DEFAULT_EVENT_ID + "\",\n" - + "\"priority\": \"" + DEFAULT_PRIORITY + "\",\n" - + "\"expiry\": " + DEFAULT_EXPIRY + "" - + "}\n")); - headersFirstRequest.put("Attribution-Reporting-Redirect", List.of(ALT_REGISTRATION)); - - Map<String, List<String>> headersSecondRequest = new HashMap<>(); - headersSecondRequest.put("Attribution-Reporting-Register-Source", List.of("{\n" - + "\"destination\": \"" + ALT_DESTINATION + "\",\n" - + "\"source_event_id\": \"" + ALT_EVENT_ID + "\",\n" - + "\"priority\": \"" + ALT_PRIORITY + "\",\n" - + "\"expiry\": " + ALT_EXPIRY + "" - + "}\n")); - - when(mUrlConnection.getHeaderFields()).thenReturn(headersFirstRequest).thenReturn( - headersSecondRequest); - - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(2, result.size()); - assertEquals(DEFAULT_TOP_ORIGIN, result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals(DEFAULT_DESTINATION, result.get(0).getAppDestination().toString()); - assertEquals(DEFAULT_EVENT_ID, result.get(0).getSourceEventId()); - assertEquals(DEFAULT_PRIORITY, result.get(0).getSourcePriority()); - assertEquals(DEFAULT_EXPIRY, result.get(0).getExpiry()); - assertEquals(DEFAULT_TOP_ORIGIN, result.get(1).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(1).getEnrollmentId()); - assertEquals(ALT_DESTINATION, result.get(1).getAppDestination().toString()); - assertEquals(ALT_EVENT_ID, result.get(1).getSourceEventId()); - assertEquals(ALT_PRIORITY, result.get(1).getSourcePriority()); - assertEquals(ALT_EXPIRY, result.get(1).getExpiry()); - verify(mUrlConnection, times(2)).setRequestMethod("POST"); - } - - @Test - public void testRedirectSameDestination_returnAllSuccessfully() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - - Map<String, List<String>> headersFirstRequest = new HashMap<>(); - headersFirstRequest.put("Attribution-Reporting-Register-Source", List.of("{\n" - + "\"destination\": \"" + DEFAULT_DESTINATION + "\",\n" - + "\"source_event_id\": \"" + DEFAULT_EVENT_ID + "\",\n" - + "\"priority\": 999,\n" - + "\"expiry\": " + DEFAULT_EXPIRY + "" - + "}\n")); - headersFirstRequest.put("Attribution-Reporting-Redirect", - List.of(ALT_REGISTRATION, ALT_REGISTRATION)); - - Map<String, List<String>> headersSecondRequest = new HashMap<>(); - headersSecondRequest.put("Attribution-Reporting-Register-Source", List.of("{\n" - + "\"destination\": \"" + DEFAULT_DESTINATION + "\",\n" - + "\"source_event_id\": \"" + ALT_EVENT_ID + "\",\n" - + "\"priority\": 888,\n" - + "\"expiry\": " + ALT_EXPIRY + "" - + "}\n")); - - Map<String, List<String>> headersThirdRequest = new HashMap<>(); - headersThirdRequest.put("Attribution-Reporting-Register-Source", List.of("{\n" - + "\"destination\": \"" + DEFAULT_DESTINATION + "\",\n" - + "\"source_event_id\": 777,\n" - + "\"priority\": 777,\n" - + "\"expiry\": 456791" - + "}\n")); - - when(mUrlConnection.getHeaderFields()).thenReturn(headersFirstRequest).thenReturn( - headersSecondRequest, headersThirdRequest); - - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(3, result.size()); - result.sort((o1, o2) -> (int) (o2.getSourcePriority() - o1.getSourcePriority())); - assertEquals(DEFAULT_TOP_ORIGIN, result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals(DEFAULT_DESTINATION, result.get(0).getAppDestination().toString()); - assertEquals(DEFAULT_EVENT_ID, result.get(0).getSourceEventId()); - assertEquals(999, result.get(0).getSourcePriority()); - assertEquals(DEFAULT_EXPIRY, result.get(0).getExpiry()); - assertEquals(DEFAULT_TOP_ORIGIN, result.get(1).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(1).getEnrollmentId()); - assertEquals(DEFAULT_DESTINATION, result.get(1).getAppDestination().toString()); - assertEquals(ALT_EVENT_ID, result.get(1).getSourceEventId()); - assertEquals(888, result.get(1).getSourcePriority()); - assertEquals(ALT_EXPIRY, result.get(1).getExpiry()); - assertEquals(DEFAULT_TOP_ORIGIN, result.get(2).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(2).getEnrollmentId()); - assertEquals(DEFAULT_DESTINATION, result.get(2).getAppDestination().toString()); - assertEquals(777, result.get(2).getSourceEventId()); - assertEquals(777, result.get(2).getSourcePriority()); - assertEquals(456791, result.get(2).getExpiry()); - verify(mUrlConnection, times(3)).setRequestMethod("POST"); - } - - @Test - public void testRedirectSameDestinationWithDelay_returnAllSuccessfully() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - - Map<String, List<String>> headersFirstRequest = - buildRegisterSourceDefaultHeader( - DEFAULT_DESTINATION, DEFAULT_EVENT_ID, /* priority = */ 1, DEFAULT_EXPIRY); - headersFirstRequest.put("Attribution-Reporting-Redirect", - List.of("https://bar2.com", - "https://bar3.com", - "https://bar4.com", - "https://bar5.com", - "https://bar6.com")); - - Map<String, List<String>> headersSecondRequest = buildRegisterSourceDefaultHeader( - DEFAULT_DESTINATION, DEFAULT_EVENT_ID, /* priority = */ 2, DEFAULT_EXPIRY); - - Map<String, List<String>> headersThirdRequest = buildRegisterSourceDefaultHeader( - DEFAULT_DESTINATION, DEFAULT_EVENT_ID, /* priority = */ 3, DEFAULT_EXPIRY); - - Map<String, List<String>> headersFourthRequest = buildRegisterSourceDefaultHeader( - DEFAULT_DESTINATION, DEFAULT_EVENT_ID, /* priority = */ 4, DEFAULT_EXPIRY); - - Map<String, List<String>> headersFifthRequest = buildRegisterSourceDefaultHeader( - DEFAULT_DESTINATION, DEFAULT_EVENT_ID, /* priority = */ 5, DEFAULT_EXPIRY); - - Map<String, List<String>> headersSixthRequest = buildRegisterSourceDefaultHeader( - DEFAULT_DESTINATION, DEFAULT_EVENT_ID, /* priority = */ 6, DEFAULT_EXPIRY); - - when(mUrlConnection.getHeaderFields()).thenReturn(headersFirstRequest) - .thenAnswer(invocation -> { - TimeUnit.MILLISECONDS.sleep(10); - return headersSecondRequest; - }).thenAnswer(invocation -> { - TimeUnit.MILLISECONDS.sleep(10); - return headersThirdRequest; - }).thenAnswer(invocation -> { - TimeUnit.MILLISECONDS.sleep(10); - return headersFourthRequest; - }).thenAnswer(invocation -> { - TimeUnit.MILLISECONDS.sleep(10); - return headersFifthRequest; - }).thenAnswer(invocation -> { - TimeUnit.MILLISECONDS.sleep(10); - return headersSixthRequest; - }); - - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(6, result.size()); - long expected = 1; - for (long priority : - result.stream() - .map(SourceRegistration::getSourcePriority) - .sorted() - .collect(Collectors.toList())) { - Assert.assertEquals(expected++, priority); - } - } - - @Test - public void testBasicSourceRequestWithAggregateFilterData() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - String filterData = - " \"filter_data\": {\"product\":[\"1234\",\"2345\"], \"ctid\":[\"id\"]} \n"; - doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn(Map.of("Attribution-Reporting-Register-Source", - List.of("{\n" - + " \"destination\": \"android-app://com.myapps\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - + filterData - + "}\n"))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals("https://baz.com", result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals("android-app://com.myapps", result.get(0).getAppDestination().toString()); - assertEquals(123, result.get(0).getSourcePriority()); - assertEquals(456789, result.get(0).getExpiry()); - assertEquals(987654321, result.get(0).getSourceEventId()); - assertEquals("{\"product\":[\"1234\",\"2345\"],\"ctid\":[\"id\"]}", - result.get(0).getAggregateFilterData()); - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void testSourceRequest_aggregateFilterData_invalidJson() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - String filterData = - " \"filter_data\": {\"product\":\"1234\",\"2345\"], \"ctid\":[\"id\"]} \n"; - doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn(Map.of("Attribution-Reporting-Register-Source", - List.of("{\n" - + " \"destination\": \"android-app://com.myapps\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - + filterData - + "}\n"))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testSourceRequest_aggregateFilterData_tooManyFilters() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - StringBuilder filters = new StringBuilder("{"); - filters.append(IntStream.range(0, MAX_ATTRIBUTION_FILTERS + 1) - .mapToObj(i -> "\"filter-string-" + i + "\": [\"filter-value\"]") - .collect(Collectors.joining(","))); - filters.append("}"); - String filterData = " \"filter_data\": " + filters + "\n"; - doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn(Map.of("Attribution-Reporting-Register-Source", - List.of("{\n" - + " \"destination\": \"android-app://com.myapps\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - + filterData - + "}\n"))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testSourceRequest_aggregateFilterData_keyTooLong() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - String filterData = - " \"filter_data\": {\"product\":[\"1234\",\"2345\"], \"" + LONG_FILTER_STRING - + "\":[\"id\"]} \n"; - doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn(Map.of("Attribution-Reporting-Register-Source", - List.of("{\n" - + " \"destination\": \"android-app://com.myapps\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - + filterData - + "}\n"))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testSourceRequest_aggregateFilterData_tooManyValues() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - StringBuilder filters = new StringBuilder("{" - + "\"filter-string-1\": [\"filter-value-1\"]," - + "\"filter-string-2\": ["); - filters.append(IntStream.range(0, MAX_VALUES_PER_ATTRIBUTION_FILTER + 1) - .mapToObj(i -> "\"filter-value-" + i + "\"") - .collect(Collectors.joining(","))); - filters.append("]}"); - String filterData = " \"filter_data\": " + filters + " \n"; - doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn(Map.of("Attribution-Reporting-Register-Source", - List.of("{\n" - + " \"destination\": \"android-app://com.myapps\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - + filterData - + "}\n"))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testSourceRequest_aggregateFilterData_valueTooLong() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - String filterData = - " \"filter_data\": {\"product\":[\"1234\",\"" + LONG_FILTER_STRING - + "\"], \"ctid\":[\"id\"]} \n"; - doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn(Map.of("Attribution-Reporting-Register-Source", - List.of("{\n" - + " \"destination\": \"android-app://com.myapps\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - + filterData - + "}\n"))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testMissingHeaderButWithRedirect() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn(Map.of("Attribution-Reporting-Redirect", List.of(ALT_REGISTRATION))) - .thenReturn(Map.of("Attribution-Reporting-Register-Source", - List.of("{\n" - + " \"destination\": \"" + DEFAULT_DESTINATION + "\",\n" - + " \"priority\": \"" + DEFAULT_PRIORITY + "\",\n" - + " \"expiry\": \"" + DEFAULT_EXPIRY + "\",\n" - + " \"source_event_id\": \"" + DEFAULT_EVENT_ID + "\"\n" - + "}\n"))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testBasicSourceRequestWithAggregateSource() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + " \"destination\": \"android-app://com.myapps\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - + "\"aggregation_keys\": [{\"id\" :" - + " \"campaignCounts\", \"key_piece\" :" - + " \"0x159\"},{\"id\" : \"geoValue\", \"key_piece\" :" - + " \"0x5\"}]\n" - + "}\n"))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals("https://baz.com", result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals( - new JSONArray( - "[{\"id\" : \"campaignCounts\", \"key_piece\" : \"0x159\"},{\"id\"" - + " : \"geoValue\", \"key_piece\" : \"0x5\"}]") - .toString(), - result.get(0).getAggregateSource()); - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void testSourceRequestWithAggregateSource_invalidJson() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + " \"destination\": \"android-app://com.myapps\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - + "\"aggregation_keys\": [\"id\" :" - + " \"campaignCounts\", \"key_piece\" :" - + " \"0x159\"},{\"id\" : \"geoValue\", \"key_piece\" :" - + " \"0x5\"}]\n" - + "}\n"))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testSourceRequestWithAggregateSource_tooManyKeys() throws Exception { - StringBuilder tooManyKeys = new StringBuilder("["); - for (int i = 0; i < MAX_AGGREGATE_KEYS_PER_REGISTRATION + 1; i++) { - tooManyKeys.append(String.format( - "{\"id\": \"campaign-%1$s\", \"key_piece\": \"0x15%1$s\"}", i)); - } - tooManyKeys.append("]"); - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + " \"destination\": \"android-app://com.myapps\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\":" - + " \"987654321\",'aggregation_keys': " - + tooManyKeys))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testSourceRequestWithAggregateSource_keyIsNotAnObject() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + " \"destination\": \"android-app://com.myapps\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - + "\"aggregation_keys\": [{\"id\" :" - + " \"campaignCounts\", \"key_piece\" :" - + " \"0x159\"},[\"id\", \"geoValue\", \"key_piece\" ," - + " \"0x5\"]\n" - + "}\n"))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testSourceRequestWithAggregateSource_invalidKeyId() throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + " \"destination\": \"android-app://com.myapps\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - + "\"aggregation_keys\": [{\"id\" :" - + " \"" + LONG_AGGREGATE_KEY_ID + "\", \"key_piece\" :" - + " \"0x159\"},{\"id\" : \"geoValue\", \"key_piece\" :" - + " \"0x5\"}]\n" - + "}\n"))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testSourceRequestWithAggregateSource_invalidKeyPiece_missingPrefix() - throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + " \"destination\": \"android-app://com.myapps\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - + "\"aggregation_keys\": [{\"id\" :" - + " \"campaignCounts\", \"key_piece\" :" - + " \"0159\"},{\"id\" : \"geoValue\", \"key_piece\" :" - + " \"0x5\"}]\n" - + "}\n"))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testSourceRequestWithAggregateSource_invalidKeyPiece_tooLong() - throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + " \"destination\": \"android-app://com.myapps\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - + "\"aggregation_keys\": [{\"id\" :" - + " \"campaignCounts\", \"key_piece\" :" - + " \"0x159\"},{\"id\" : \"geoValue\", \"key_piece\" :" - + " \"" + LONG_AGGREGATE_KEY_PIECE + "\"}]\n" - + "}\n"))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void fetchWebSources_basic_success() throws IOException { - // Setup - SourceRegistration expectedResult1 = - new SourceRegistration.Builder() - .setAppDestination(Uri.parse(DEFAULT_DESTINATION)) - .setExpiry(MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS) - .setTopOrigin(Uri.parse(DEFAULT_TOP_ORIGIN)) - .setEnrollmentId(ENROLLMENT_ID) - .setSourceEventId(EVENT_ID_1) - .setSourcePriority(0) - .setDebugKey(DEBUG_KEY) - .build(); - SourceRegistration expectedResult2 = - new SourceRegistration.Builder() - .setAppDestination(Uri.parse(DEFAULT_DESTINATION)) - .setExpiry(MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS) - .setTopOrigin(Uri.parse(DEFAULT_TOP_ORIGIN)) - .setEnrollmentId(ENROLLMENT_ID) - .setSourceEventId(EVENT_ID_2) - .setSourcePriority(0) - .build(); - - WebSourceRegistrationRequest request = - buildWebSourceRegistrationRequest( - Arrays.asList(SOURCE_REGISTRATION_1, SOURCE_REGISTRATION_2), - DEFAULT_TOP_ORIGIN, - Uri.parse(DEFAULT_DESTINATION), - null); - doReturn(mUrlConnection1).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); - doReturn(mUrlConnection2).when(mFetcher).openUrl(new URL(REGISTRATION_URI_2.toString())); - when(mUrlConnection1.getResponseCode()).thenReturn(200); - when(mUrlConnection2.getResponseCode()).thenReturn(200); - when(mUrlConnection1.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + "\"destination\": \"" - + DEFAULT_DESTINATION - + "\",\n" - + "\"source_event_id\": \"" - + EVENT_ID_1 - + "\",\n" - + " \"debug_key\": \"" - + DEBUG_KEY - + "\"\n" - + "}\n"))); - when(mUrlConnection2.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + "\"destination\": \"" - + DEFAULT_DESTINATION - + "\",\n" - + "\"source_event_id\": \"" - + EVENT_ID_2 - + "\"\n" - + "}\n"))); - - // Execution - Optional<List<SourceRegistration>> fetch = mFetcher.fetchWebSources(request, true); - - // Assertion - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(2, result.size()); - assertEquals( - new HashSet<>(Arrays.asList(expectedResult1, expectedResult2)), - new HashSet<>(result)); - verify(mUrlConnection1).setRequestMethod("POST"); - verify(mUrlConnection2).setRequestMethod("POST"); - } - - @Test - public void fetchWebSourcesSuccessWithoutAdIdPermission() throws IOException { - // Setup - SourceRegistration expectedResult1 = - new SourceRegistration.Builder() - .setAppDestination(Uri.parse(DEFAULT_DESTINATION)) - .setExpiry(MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS) - .setTopOrigin(Uri.parse(DEFAULT_TOP_ORIGIN)) - .setEnrollmentId(ENROLLMENT_ID) - .setSourceEventId(EVENT_ID_1) - .setSourcePriority(0) - .build(); - SourceRegistration expectedResult2 = - new SourceRegistration.Builder() - .setAppDestination(Uri.parse(DEFAULT_DESTINATION)) - .setExpiry(MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS) - .setTopOrigin(Uri.parse(DEFAULT_TOP_ORIGIN)) - .setEnrollmentId(ENROLLMENT_ID) - .setSourceEventId(EVENT_ID_2) - .setSourcePriority(0) - .build(); - - WebSourceRegistrationRequest request = - buildWebSourceRegistrationRequest( - Arrays.asList(SOURCE_REGISTRATION_1, SOURCE_REGISTRATION_2), - DEFAULT_TOP_ORIGIN, - Uri.parse(DEFAULT_DESTINATION), - null); - doReturn(mUrlConnection1).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); - doReturn(mUrlConnection2).when(mFetcher).openUrl(new URL(REGISTRATION_URI_2.toString())); - when(mUrlConnection1.getResponseCode()).thenReturn(200); - when(mUrlConnection2.getResponseCode()).thenReturn(200); - when(mUrlConnection1.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + "\"destination\": \"" - + DEFAULT_DESTINATION - + "\",\n" - + "\"source_event_id\": \"" - + EVENT_ID_1 - + "\",\n" - + " \"debug_key\": \"" - + DEBUG_KEY - + "\"\n" - + "}\n"))); - when(mUrlConnection2.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + "\"destination\": \"" - + DEFAULT_DESTINATION - + "\",\n" - + "\"source_event_id\": \"" - + EVENT_ID_2 - + "\"\n" - + "}\n"))); - - // Execution - Optional<List<SourceRegistration>> fetch = mFetcher.fetchWebSources(request, false); - - // Assertion - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(2, result.size()); - assertEquals( - new HashSet<>(Arrays.asList(expectedResult1, expectedResult2)), - new HashSet<>(result)); - verify(mUrlConnection1).setRequestMethod("POST"); - verify(mUrlConnection2).setRequestMethod("POST"); - } - - @Test - public void fetchWebSources_oneSuccessAndOneFailure_resultsIntoOneSourceFetched() - throws IOException { - // Setup - SourceRegistration expectedResult2 = - new SourceRegistration.Builder() - .setAppDestination(Uri.parse(DEFAULT_DESTINATION)) - .setExpiry(MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS) - .setTopOrigin(Uri.parse(DEFAULT_TOP_ORIGIN)) - .setEnrollmentId(ENROLLMENT_ID) - .setSourceEventId(EVENT_ID_2) - .setSourcePriority(0) - .build(); - - WebSourceRegistrationRequest request = - buildWebSourceRegistrationRequest( - Arrays.asList(SOURCE_REGISTRATION_1, SOURCE_REGISTRATION_2), - DEFAULT_TOP_ORIGIN, - Uri.parse(DEFAULT_DESTINATION), - null); - doReturn(mUrlConnection1).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); - doReturn(mUrlConnection2).when(mFetcher).openUrl(new URL(REGISTRATION_URI_2.toString())); - when(mUrlConnection1.getResponseCode()).thenReturn(200); - when(mUrlConnection2.getResponseCode()).thenReturn(200); - // Its validation will fail due to destination mismatch - when(mUrlConnection1.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + "\"destination\": \"" - /* wrong destination */ - + "android-app://com.wrongapp" - + "\",\n" - + "\"source_event_id\": \"" - + EVENT_ID_1 - + "\",\n" - + " \"debug_key\": \"" - + DEBUG_KEY - + "\"\n" - + "}\n"))); - - when(mUrlConnection2.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + "\"destination\": \"" - + DEFAULT_DESTINATION - + "\",\n" - + "\"source_event_id\": \"" - + EVENT_ID_2 - + "\"\n" - + "}\n"))); - - // Execution - Optional<List<SourceRegistration>> fetch = mFetcher.fetchWebSources(request, true); - - // Assertion - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals(expectedResult2, result.iterator().next()); - verify(mUrlConnection1).setRequestMethod("POST"); - verify(mUrlConnection2).setRequestMethod("POST"); - } - - @Test - public void fetchWebSources_withExtendedHeaders_success() throws IOException, JSONException { - // Setup - WebSourceRegistrationRequest request = - buildWebSourceRegistrationRequest( - Collections.singletonList(SOURCE_REGISTRATION_1), - DEFAULT_TOP_ORIGIN, - OS_DESTINATION, - null); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); - when(mUrlConnection.getResponseCode()).thenReturn(200); - String aggregateSource = - "[{\"id\" : \"campaignCounts\", \"key_piece\" : \"0x159\"}," - + "{\"id\" : \"geoValue\", \"key_piece\" : \"0x5\"}]"; - String filterData = "{\"product\":[\"1234\",\"2345\"]," + "\"ctid\":[\"id\"]}"; - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + " \"destination\": \"" - + OS_DESTINATION - + "\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - + " \"filter_data\": " - + filterData - + ", " - + " \"aggregation_keys\": " - + aggregateSource - + "}"))); - SourceRegistration expectedSourceRegistration = - new SourceRegistration.Builder() - .setAppDestination(OS_DESTINATION) - .setSourcePriority(123) - .setExpiry(456789) - .setSourceEventId(987654321) - .setAggregateFilterData(filterData) - .setAggregateSource(new JSONArray(aggregateSource).toString()) - .setTopOrigin(Uri.parse(DEFAULT_TOP_ORIGIN)) - .setEnrollmentId(ENROLLMENT_ID) - .build(); - - // Execution - Optional<List<SourceRegistration>> fetch = mFetcher.fetchWebSources(request, true); - - // Assertion - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals(expectedSourceRegistration, result.get(0)); - - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void fetchWebSources_withRedirects_ignoresRedirects() throws IOException, JSONException { - // Setup - WebSourceRegistrationRequest request = - buildWebSourceRegistrationRequest( - Collections.singletonList(SOURCE_REGISTRATION_1), - DEFAULT_TOP_ORIGIN, - Uri.parse(DEFAULT_DESTINATION), - null); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); - when(mUrlConnection.getResponseCode()).thenReturn(200); - String aggregateSource = - "[{\"id\" : \"campaignCounts\", \"key_piece\" : \"0x159\"}," - + "{\"id\" : \"geoValue\", \"key_piece\" : \"0x5\"}]"; - String filterData = "{\"product\":[\"1234\",\"2345\"]," + "\"ctid\":[\"id\"]}"; - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + " \"destination\": \"" - + DEFAULT_DESTINATION - + "\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - + " \"filter_data\": " - + filterData - + " , " - + " \"aggregation_keys\": " - + aggregateSource - + "}"), - "Attribution-Reporting-Redirect", - List.of(ALT_REGISTRATION))); - SourceRegistration expectedSourceRegistration = - new SourceRegistration.Builder() - .setAppDestination(Uri.parse(DEFAULT_DESTINATION)) - .setSourcePriority(123) - .setExpiry(456789) - .setSourceEventId(987654321) - .setAggregateFilterData(filterData) - .setAggregateSource(new JSONArray(aggregateSource).toString()) - .setTopOrigin(Uri.parse(DEFAULT_TOP_ORIGIN)) - .setEnrollmentId(ENROLLMENT_ID) - .build(); - - // Execution - Optional<List<SourceRegistration>> fetch = mFetcher.fetchWebSources(request, true); - - // Assertion - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals(expectedSourceRegistration, result.get(0)); - - verify(mUrlConnection).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void fetchWebSources_appDestinationDoNotMatch_failsDropsSource() throws IOException { - // Setup - WebSourceRegistrationRequest request = - buildWebSourceRegistrationRequest( - Collections.singletonList(SOURCE_REGISTRATION_1), - DEFAULT_TOP_ORIGIN, - OS_DESTINATION, - null); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); - when(mUrlConnection.getResponseCode()).thenReturn(200); - String filterData = "{\"product\":[\"1234\",\"2345\"]," + "\"ctid\":[\"id\"]}"; - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + " \"destination\": \"android-app://com" - + ".myapps\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - + " \"destination\":" - + " android-app://wrong.os-destination,\n" - + " \"web_destination\": " - + WEB_DESTINATION - + ",\n" - + " \"filter_data\": " - + filterData - + "}"))); - - // Execution - Optional<List<SourceRegistration>> fetch = mFetcher.fetchWebSources(request, true); - - // Assertion - assertFalse(fetch.isPresent()); - - verify(mUrlConnection).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void fetchWebSources_webDestinationDoNotMatch_failsDropsSource() throws IOException { - // Setup - WebSourceRegistrationRequest request = - buildWebSourceRegistrationRequest( - Collections.singletonList(SOURCE_REGISTRATION_1), - DEFAULT_TOP_ORIGIN, - OS_DESTINATION, - WEB_DESTINATION); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); - when(mUrlConnection.getResponseCode()).thenReturn(200); - String filterData = "{\"product\":[\"1234\",\"2345\"]," + "\"ctid\":[\"id\"]}"; - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + " \"destination\": \"android-app://com" - + ".myapps\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - + " \"destination\": " - + OS_DESTINATION - + ",\n" - + " \"web_destination\": " - + " https://wrong-web-destination.com,\n" - + " \"filter_data\": " - + filterData - + "}"))); - - // Execution - Optional<List<SourceRegistration>> fetch = mFetcher.fetchWebSources(request, true); - - // Assertion - assertFalse(fetch.isPresent()); - - verify(mUrlConnection).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void fetchWebSources_osAndWebDestinationMatch_recordSourceSuccess() throws IOException { - // Setup - WebSourceRegistrationRequest request = - buildWebSourceRegistrationRequest( - Collections.singletonList(SOURCE_REGISTRATION_1), - DEFAULT_TOP_ORIGIN, - OS_DESTINATION, - WEB_DESTINATION); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + " \"destination\": \"android-app://com" - + ".myapps\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - + " \"destination\": \"" - + OS_DESTINATION - + "\",\n" - + "\"web_destination\": \"" - + WEB_DESTINATION - + "\"" - + "}"))); - SourceRegistration expectedSourceRegistration = - new SourceRegistration.Builder() - .setAppDestination(OS_DESTINATION) - .setWebDestination(WEB_DESTINATION) - .setSourcePriority(123) - .setExpiry(456789) - .setSourceEventId(987654321) - .setTopOrigin(Uri.parse(DEFAULT_TOP_ORIGIN)) - .setEnrollmentId(ENROLLMENT_ID) - .build(); - - // Execution - Optional<List<SourceRegistration>> fetch = mFetcher.fetchWebSources(request, true); - - // Assertion - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals(expectedSourceRegistration, result.get(0)); - - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void fetchWebSources_extractsTopPrivateDomain() throws IOException { - // Setup - WebSourceRegistrationRequest request = - buildWebSourceRegistrationRequest( - Collections.singletonList(SOURCE_REGISTRATION_1), - DEFAULT_TOP_ORIGIN, - OS_DESTINATION, - WEB_DESTINATION_WITH_SUBDOMAIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + " \"destination\": \"android-app://com" - + ".myapps\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - + " \"destination\": \"" - + OS_DESTINATION - + "\",\n" - + "\"web_destination\": \"" - + WEB_DESTINATION_WITH_SUBDOMAIN - + "\"" - + "}"))); - SourceRegistration expectedSourceRegistration = - new SourceRegistration.Builder() - .setAppDestination(OS_DESTINATION) - .setWebDestination(WEB_DESTINATION) - .setSourcePriority(123) - .setExpiry(456789) - .setSourceEventId(987654321) - .setTopOrigin(Uri.parse(DEFAULT_TOP_ORIGIN)) - .setEnrollmentId(ENROLLMENT_ID) - .build(); - - // Execution - Optional<List<SourceRegistration>> fetch = mFetcher.fetchWebSources(request, true); - - // Assertion - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals(expectedSourceRegistration, result.get(0)); - - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void fetchWebSources_extractsDestinationBaseUri() throws IOException { - // Setup - WebSourceRegistrationRequest request = - buildWebSourceRegistrationRequest( - Collections.singletonList(SOURCE_REGISTRATION_1), - DEFAULT_TOP_ORIGIN, - OS_DESTINATION_WITH_PATH, - WEB_DESTINATION); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + " \"destination\": \"" - + OS_DESTINATION_WITH_PATH - + "\",\n" - + " \"priority\": \"123\",\n" - + " \"expiry\": \"456789\",\n" - + " \"source_event_id\": \"987654321\",\n" - + "\"web_destination\": \"" - + WEB_DESTINATION - + "\"" - + "}"))); - SourceRegistration expectedSourceRegistration = - new SourceRegistration.Builder() - .setAppDestination(OS_DESTINATION) - .setWebDestination(WEB_DESTINATION) - .setSourcePriority(123) - .setExpiry(456789) - .setSourceEventId(987654321) - .setTopOrigin(Uri.parse(DEFAULT_TOP_ORIGIN)) - .setEnrollmentId(ENROLLMENT_ID) - .build(); - - // Execution - Optional<List<SourceRegistration>> fetch = mFetcher.fetchWebSources(request, true); - - // Assertion - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals(expectedSourceRegistration, result.get(0)); - - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void fetchWebSources_missingDestinations_dropsSource() throws Exception { - // Setup - WebSourceRegistrationRequest request = - buildWebSourceRegistrationRequest( - Collections.singletonList(SOURCE_REGISTRATION_1), - DEFAULT_TOP_ORIGIN, - OS_DESTINATION, - WEB_DESTINATION); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + "\"source_event_id\": \"" - + DEFAULT_EVENT_ID - + "\"\n" - + "}\n"))); - - // Execution - Optional<List<SourceRegistration>> fetch = mFetcher.fetchWebSources(request, true); - - // Assertion - assertFalse(fetch.isPresent()); - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void fetchWebSources_withDestinationUriNotHavingScheme_attachesAppScheme() - throws IOException { - // Setup - SourceRegistration expectedResult = - new SourceRegistration.Builder() - .setAppDestination(Uri.parse(DEFAULT_DESTINATION)) - .setExpiry(MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS) - .setTopOrigin(Uri.parse(DEFAULT_TOP_ORIGIN)) - .setEnrollmentId(ENROLLMENT_ID) - .setSourceEventId(EVENT_ID_1) - .setSourcePriority(0) - .build(); - - WebSourceRegistrationRequest request = - buildWebSourceRegistrationRequest( - Collections.singletonList(SOURCE_REGISTRATION_1), - DEFAULT_TOP_ORIGIN, - Uri.parse(DEFAULT_DESTINATION_WITHOUT_SCHEME), - null); - doReturn(mUrlConnection1).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); - when(mUrlConnection1.getResponseCode()).thenReturn(200); - when(mUrlConnection1.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + "\"destination\": \"" - + DEFAULT_DESTINATION - + "\",\n" - + "\"source_event_id\": \"" - + EVENT_ID_1 - + "\"\n" - + "}\n"))); - - // Execution - Optional<List<SourceRegistration>> fetch = mFetcher.fetchWebSources(request, true); - - // Assertion - assertTrue(fetch.isPresent()); - List<SourceRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals( - new HashSet<>(Collections.singletonList(expectedResult)), new HashSet<>(result)); - verify(mUrlConnection1).setRequestMethod("POST"); - } - - @Test - public void fetchWebSources_withDestinationUriHavingHttpsScheme_dropsSource() - throws IOException { - // Setup - WebSourceRegistrationRequest request = - buildWebSourceRegistrationRequest( - Collections.singletonList(SOURCE_REGISTRATION_1), - DEFAULT_TOP_ORIGIN, - Uri.parse(DEFAULT_DESTINATION_WITHOUT_SCHEME), - null); - doReturn(mUrlConnection1).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); - when(mUrlConnection1.getResponseCode()).thenReturn(200); - when(mUrlConnection1.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + "\"destination\": \"" - // Invalid (https) URI for app destination - + WEB_DESTINATION - + "\",\n" - + "\"source_event_id\": \"" - + EVENT_ID_1 - + "\"\n" - + "}\n"))); - - // Execution - Optional<List<SourceRegistration>> fetch = mFetcher.fetchWebSources(request, true); - - // Assertion - assertFalse(fetch.isPresent()); - verify(mUrlConnection1).setRequestMethod("POST"); - } - - @Test - public void basicSourceRequest_headersMoreThanMaxResponseSize_emitsMetricsWithAdTechDomain() - throws Exception { - RegistrationRequest request = buildRequest(DEFAULT_REGISTRATION, DEFAULT_TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(DEFAULT_REGISTRATION)); - doReturn(5L).when(mFlags).getMaxResponseBasedRegistrationPayloadSizeBytes(); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Source", - List.of( - "{\n" - + "\"destination\": \"" - + DEFAULT_DESTINATION - + "\",\n" - + "\"source_event_id\": \"" - + DEFAULT_EVENT_ID - + "\"\n" - + "}\n"))); - Optional<List<SourceRegistration>> fetch = mFetcher.fetchSource(request); - assertTrue(fetch.isPresent()); - verify(mLogger) - .logMeasurementRegistrationsResponseSize( - eq( - new MeasurementRegistrationResponseStats.Builder( - AD_SERVICES_MEASUREMENT_REGISTRATIONS, - AD_SERVICES_MEASUREMENT_REGISTRATIONS__TYPE__SOURCE, - 115) - .setAdTechDomain(DEFAULT_REGISTRATION) - .build())); - } - - private RegistrationRequest buildRequest(String registrationUri, String topOrigin) { - return new RegistrationRequest.Builder() - .setRegistrationType(RegistrationRequest.REGISTER_SOURCE) - .setRegistrationUri(Uri.parse(registrationUri)) - .setTopOriginUri(Uri.parse(topOrigin)) - .setPackageName(sContext.getAttributionSource().getPackageName()) - .setAdIdPermissionGranted(true) - .build(); - } - - private RegistrationRequest buildRequestWithoutAdIdPermission( - String registrationUri, String topOrigin) { - return new RegistrationRequest.Builder() - .setRegistrationType(RegistrationRequest.REGISTER_SOURCE) - .setRegistrationUri(Uri.parse(registrationUri)) - .setTopOriginUri(Uri.parse(topOrigin)) - .setPackageName(sContext.getAttributionSource().getPackageName()) - .setAdIdPermissionGranted(false) - .build(); - } - - private WebSourceRegistrationRequest buildWebSourceRegistrationRequest( - List<WebSourceParams> sourceParamsList, - String topOrigin, - Uri appDestination, - Uri webDestination) { - WebSourceRegistrationRequest.Builder webSourceRegistrationRequestBuilder = - new WebSourceRegistrationRequest.Builder(sourceParamsList, Uri.parse(topOrigin)) - .setAppDestination(appDestination); - - if (webDestination != null) { - webSourceRegistrationRequestBuilder.setWebDestination(webDestination); - } - - return webSourceRegistrationRequestBuilder.build(); - } - - private Map<String, List<String>> buildRegisterSourceDefaultHeader( - String destination, long eventId, long priority, long expiry) { - Map<String, List<String>> headersFirstRequest = new HashMap<>(); - headersFirstRequest.put("Attribution-Reporting-Register-Source", List.of("{\n" - + "\"destination\": \"" + destination + "\",\n" - + "\"source_event_id\": \"" + eventId + "\",\n" - + "\"priority\": \"" + priority + "\",\n" - + "\"expiry\": " + expiry + "" - + "}\n")); - return headersFirstRequest; - } -} diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/SourceRegistrationTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/SourceRegistrationTest.java deleted file mode 100644 index e944fef627..0000000000 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/SourceRegistrationTest.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * 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.measurement.registration; - -import static com.android.adservices.service.measurement.PrivacyParams.MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThrows; - -import android.net.Uri; - -import androidx.test.filters.SmallTest; - -import org.junit.Test; - - -/** - * Unit tests for {@link SourceRegistration} - */ -@SmallTest -public final class SourceRegistrationTest { - private static final Uri TOP_ORIGIN = Uri.parse("https://foo.com"); - private static final String ENROLLMENT_ID = "enrollment-id"; - private static final Long DEBUG_KEY = 2376843L; - - private SourceRegistration createExampleResponse() { - - return new SourceRegistration.Builder() - .setTopOrigin(TOP_ORIGIN) - .setEnrollmentId(ENROLLMENT_ID) - .setAppDestination(Uri.parse("android-app://baz.com")) - .setSourceEventId(1234567) - .setExpiry(2345678) - .setSourcePriority(345678) - .setAggregateSource( - "[{\"id\" : \"campaignCounts\", \"key_piece\" : \"0x159\"}," - + "{\"id\" : \"geoValue\", \"key_piece\" : \"0x5\"}]") - .setAggregateFilterData("{\"product\":[\"1234\",\"2345\"],\"ctid\":[\"id\"]}") - .setDebugKey(DEBUG_KEY) - .build(); - } - - void verifyExampleResponse(SourceRegistration response) { - assertEquals("https://foo.com", response.getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, response.getEnrollmentId()); - assertEquals("android-app://baz.com", response.getAppDestination().toString()); - assertEquals(1234567, response.getSourceEventId()); - assertEquals(2345678, response.getExpiry()); - assertEquals(345678, response.getSourcePriority()); - assertEquals(DEBUG_KEY, response.getDebugKey()); - assertEquals( - "[{\"id\" : \"campaignCounts\", \"key_piece\" : \"0x159\"}," - + "{\"id\" : \"geoValue\", \"key_piece\" : \"0x5\"}]", - response.getAggregateSource()); - assertEquals("{\"product\":[\"1234\",\"2345\"],\"ctid\":[\"id\"]}", - response.getAggregateFilterData()); - } - - @Test - public void testCreation() throws Exception { - verifyExampleResponse(createExampleResponse()); - } - - @Test - public void sourceRegistration_onlyAppDestination_success() { - Uri destination = Uri.parse("android-app://baz.com"); - SourceRegistration response = - new SourceRegistration.Builder() - .setTopOrigin(TOP_ORIGIN) - .setEnrollmentId(ENROLLMENT_ID) - .setAppDestination(destination) - .build(); - assertEquals(TOP_ORIGIN, response.getTopOrigin()); - assertEquals(ENROLLMENT_ID, response.getEnrollmentId()); - assertEquals(destination, response.getAppDestination()); - assertNull(response.getWebDestination()); - assertEquals(0, response.getSourceEventId()); - assertEquals(MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS, response.getExpiry()); - assertEquals(0, response.getSourcePriority()); - assertNull(response.getAggregateSource()); - assertNull(response.getAggregateFilterData()); - } - - @Test - public void sourceRegistration_onlyWebDestination_success() { - Uri destination = Uri.parse("https://baz.com"); - SourceRegistration response = - new SourceRegistration.Builder() - .setTopOrigin(TOP_ORIGIN) - .setEnrollmentId(ENROLLMENT_ID) - .setWebDestination(destination) - .build(); - assertEquals(TOP_ORIGIN, response.getTopOrigin()); - assertEquals(ENROLLMENT_ID, response.getEnrollmentId()); - assertEquals(destination, response.getWebDestination()); - assertNull(response.getAppDestination()); - assertEquals(0, response.getSourceEventId()); - assertEquals(MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS, response.getExpiry()); - assertEquals(0, response.getSourcePriority()); - assertNull(response.getAggregateSource()); - assertNull(response.getAggregateFilterData()); - } - - @Test - public void sourceRegistration_bothOsAndWebDestination_success() { - Uri webDestination = Uri.parse("https://baz.com"); - Uri destination = Uri.parse("android-app://baz.com"); - SourceRegistration response = - new SourceRegistration.Builder() - .setTopOrigin(TOP_ORIGIN) - .setEnrollmentId(ENROLLMENT_ID) - .setAppDestination(destination) - .setWebDestination(webDestination) - .build(); - assertEquals(TOP_ORIGIN, response.getTopOrigin()); - assertEquals(ENROLLMENT_ID, response.getEnrollmentId()); - assertEquals(webDestination, response.getWebDestination()); - assertEquals(destination, response.getAppDestination()); - assertEquals(0, response.getSourceEventId()); - assertEquals(MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS, response.getExpiry()); - assertEquals(0, response.getSourcePriority()); - assertNull(response.getAggregateSource()); - assertNull(response.getAggregateFilterData()); - } - - @Test - public void sourceRegistration_bothDestinationsNull_throwsException() { - assertThrows( - IllegalArgumentException.class, - () -> - new SourceRegistration.Builder() - .setAppDestination(null) - .setWebDestination(null) - .build()); - } - - @Test - public void equals_success() { - assertEquals(createExampleResponse(), createExampleResponse()); - } -} diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/TriggerFetcherTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/TriggerFetcherTest.java deleted file mode 100644 index e7ae8f09b5..0000000000 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/TriggerFetcherTest.java +++ /dev/null @@ -1,1231 +0,0 @@ -/* - * 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.measurement.registration; - -import static com.android.adservices.service.measurement.SystemHealthParams.MAX_AGGREGATABLE_TRIGGER_DATA; -import static com.android.adservices.service.measurement.SystemHealthParams.MAX_AGGREGATE_KEYS_PER_REGISTRATION; -import static com.android.adservices.service.measurement.SystemHealthParams.MAX_ATTRIBUTION_EVENT_TRIGGER_DATA; -import static com.android.adservices.service.measurement.SystemHealthParams.MAX_ATTRIBUTION_FILTERS; -import static com.android.adservices.service.measurement.SystemHealthParams.MAX_VALUES_PER_ATTRIBUTION_FILTER; -import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_MEASUREMENT_REGISTRATIONS; -import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_MEASUREMENT_REGISTRATIONS__TYPE__TRIGGER; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.adservices.measurement.RegistrationRequest; -import android.adservices.measurement.WebTriggerParams; -import android.adservices.measurement.WebTriggerRegistrationRequest; -import android.content.Context; -import android.net.Uri; - -import androidx.test.filters.SmallTest; -import androidx.test.platform.app.InstrumentationRegistry; - -import com.android.adservices.data.enrollment.EnrollmentDao; -import com.android.adservices.service.Flags; -import com.android.adservices.service.enrollment.EnrollmentData; -import com.android.adservices.service.stats.AdServicesLogger; -import com.android.adservices.service.stats.MeasurementRegistrationResponseStats; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import java.io.IOException; -import java.net.URL; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import javax.net.ssl.HttpsURLConnection; - -/** Unit tests for {@link TriggerFetcher} */ -@RunWith(MockitoJUnitRunner.class) -@SmallTest -public final class TriggerFetcherTest { - private static final String TRIGGER_URI = "https://foo.com"; - private static final String ENROLLMENT_ID = "enrollment-id"; - private static final EnrollmentData ENROLLMENT = new EnrollmentData.Builder() - .setEnrollmentId("enrollment-id") - .build(); - private static final String TOP_ORIGIN = "https://baz.com"; - private static final long TRIGGER_DATA = 7; - private static final long PRIORITY = 1; - private static final String LONG_FILTER_STRING = "12345678901234567890123456"; - private static final String LONG_AGGREGATE_KEY_ID = "12345678901234567890123456"; - private static final String LONG_AGGREGATE_KEY_PIECE = "0x123456789012345678901234567890123"; - private static final long DEDUP_KEY = 100; - private static final Long DEBUG_KEY = 34787843L; - - private static final String DEFAULT_REDIRECT = "https://bar.com"; - - private static final String EVENT_TRIGGERS_1 = - "[\n" - + "{\n" - + " \"trigger_data\": \"" - + TRIGGER_DATA - + "\",\n" - + " \"priority\": \"" - + PRIORITY - + "\",\n" - + " \"deduplication_key\": \"" - + DEDUP_KEY - + "\",\n" - + " \"filters\": {\n" - + " \"source_type\": [\"navigation\"],\n" - + " \"key_1\": [\"value_1\"] \n" - + " }\n" - + "}" - + "]\n"; - private static final String EVENT_TRIGGERS_2 = - "[\n" - + "{\n" - + " \"trigger_data\": \"" - + 11 - + "\",\n" - + " \"priority\": \"" - + 21 - + "\",\n" - + " \"deduplication_key\": \"" - + 31 - + "\"}" - + "]\n"; - - private static final String ALT_REGISTRATION = "https://bar.com"; - private static final Uri REGISTRATION_URI_1 = Uri.parse("https://foo.com"); - private static final Uri REGISTRATION_URI_2 = Uri.parse("https://foo2.com"); - private static final WebTriggerParams TRIGGER_REGISTRATION_1 = - new WebTriggerParams.Builder(REGISTRATION_URI_1).setDebugKeyAllowed(true).build(); - private static final WebTriggerParams TRIGGER_REGISTRATION_2 = - new WebTriggerParams.Builder(REGISTRATION_URI_2).setDebugKeyAllowed(false).build(); - private static final Context CONTEXT = - InstrumentationRegistry.getInstrumentation().getContext(); - - TriggerFetcher mFetcher; - @Mock HttpsURLConnection mUrlConnection; - @Mock HttpsURLConnection mUrlConnection1; - @Mock HttpsURLConnection mUrlConnection2; - @Mock EnrollmentDao mEnrollmentDao; - @Mock Flags mFlags; - @Mock AdServicesLogger mLogger; - - @Before - public void setup() { - mFetcher = spy(new TriggerFetcher(mEnrollmentDao, mFlags, mLogger)); - // For convenience, return the same enrollment-ID since we're using many arbitrary - // registration URIs and not yet enforcing uniqueness of enrollment. - when(mEnrollmentDao.getEnrollmentDataFromMeasurementUrl(any())).thenReturn(ENROLLMENT); - } - - @Test - public void testBasicTriggerRequest() throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - MeasurementRegistrationResponseStats expectedStats = - new MeasurementRegistrationResponseStats.Builder( - AD_SERVICES_MEASUREMENT_REGISTRATIONS, - AD_SERVICES_MEASUREMENT_REGISTRATIONS__TYPE__TRIGGER, - 221) - .setAdTechDomain(null) - .build(); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of("{" + "'event_trigger_data':" + EVENT_TRIGGERS_1 + "}"))); - doReturn(5000L).when(mFlags).getMaxResponseBasedRegistrationPayloadSizeBytes(); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertTrue(fetch.isPresent()); - List<TriggerRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals(TOP_ORIGIN, result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals(new JSONArray(EVENT_TRIGGERS_1).toString(), result.get(0).getEventTriggers()); - verify(mUrlConnection).setRequestMethod("POST"); - verify(mLogger).logMeasurementRegistrationsResponseSize(eq(expectedStats)); - } - - @Test - public void testTriggerRequest_eventTriggerData_tooManyEntries() throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - StringBuilder tooManyEntries = new StringBuilder("["); - for (int i = 0; i < MAX_ATTRIBUTION_EVENT_TRIGGER_DATA + 1; i++) { - tooManyEntries.append("{\"trigger_data\": \"2\",\"priority\": \"101\"}"); - } - tooManyEntries.append("]"); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of("{" + "'event_trigger_data':" + tooManyEntries + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testBasicTriggerRequest_failsWhenNotEnrolled() throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - when(mEnrollmentDao.getEnrollmentDataFromMeasurementUrl(any())).thenReturn(null); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of("{" + "'event_trigger_data':" + EVENT_TRIGGERS_1 + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mFetcher, never()).openUrl(any()); - } - - @Test - public void testBasicTriggerRequestWithDebugKey() throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - - Map<String, List<String>> headersRequest = new HashMap<>(); - headersRequest.put( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'event_trigger_data': " - + EVENT_TRIGGERS_1 - + ", 'debug_key': '" - + DEBUG_KEY - + "'" - + "}")); - - when(mUrlConnection.getHeaderFields()).thenReturn(headersRequest); - - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertTrue(fetch.isPresent()); - List<TriggerRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals(TOP_ORIGIN, result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals(new JSONArray(EVENT_TRIGGERS_1).toString(), result.get(0).getEventTriggers()); - assertEquals(DEBUG_KEY, result.get(0).getDebugKey()); - - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void testBasicTriggerRequestWithoutAdIdPermission() throws Exception { - RegistrationRequest request = buildRequestWithoutAdIdPermission(TRIGGER_URI, TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - - Map<String, List<String>> headersRequest = new HashMap<>(); - headersRequest.put( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'event_trigger_data': " - + EVENT_TRIGGERS_1 - + ", 'debug_key': '" - + DEBUG_KEY - + "'" - + "}")); - - when(mUrlConnection.getHeaderFields()).thenReturn(headersRequest); - - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertTrue(fetch.isPresent()); - List<TriggerRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals(TOP_ORIGIN, result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals(new JSONArray(EVENT_TRIGGERS_1).toString(), result.get(0).getEventTriggers()); - assertNull(result.get(0).getDebugKey()); - - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void testBadTriggerUrl() throws Exception { - RegistrationRequest request = - buildRequest("bad-schema://foo.com", TOP_ORIGIN); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - } - - @Test - public void testBadTriggerConnection() throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - doThrow(new IOException("Bad internet things")) - .when(mFetcher).openUrl(new URL(TRIGGER_URI)); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, never()).setRequestMethod("POST"); - } - - @Test - public void testBadRequestReturnFailure() throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(400); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of("{" + "'event_trigger_data':" + EVENT_TRIGGERS_1 + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testBasicTriggerRequestMinimumFields() throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of("{" + "'event_trigger_data': " + "[{}]" + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertTrue(fetch.isPresent()); - List<TriggerRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals(TOP_ORIGIN, result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals("[{}]", result.get(0).getEventTriggers()); - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void testNotOverHttps() throws Exception { - RegistrationRequest request = buildRequest("http://foo.com", TOP_ORIGIN); - // Non-https should fail. - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - } - - @Test - public void testFirst200Next500_ignoreFailureReturnSuccess() throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); - when(mUrlConnection.getResponseCode()).thenReturn(200).thenReturn(500); - - Map<String, List<String>> headersFirstRequest = new HashMap<>(); - headersFirstRequest.put( - "Attribution-Reporting-Register-Trigger", - List.of("{" + "'event_trigger_data':" + EVENT_TRIGGERS_1 + "}")); - headersFirstRequest.put("Attribution-Reporting-Redirect", List.of(DEFAULT_REDIRECT)); - - when(mUrlConnection.getHeaderFields()).thenReturn(headersFirstRequest); - - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertTrue(fetch.isPresent()); - List<TriggerRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals(TOP_ORIGIN, result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals(new JSONArray(EVENT_TRIGGERS_1).toString(), result.get(0).getEventTriggers()); - verify(mUrlConnection, times(2)).setRequestMethod("POST"); - } - - @Test - public void testMissingHeaderButWithRedirect() throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(any(URL.class)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn(Map.of("Attribution-Reporting-Redirect", List.of(DEFAULT_REDIRECT))) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of("{" + "'event_trigger_data':" + EVENT_TRIGGERS_1 + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testBasicTriggerRequestWithAggregateTriggerData() throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - String aggregatable_trigger_data = - "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," - + "\"filters\":" - + "{\"conversion_subdomain\":[\"electronics.megastore\"]}," - + "\"not_filters\":{\"product\":[\"1\"]}}," - + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'aggregatable_trigger_data': " - + aggregatable_trigger_data - + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertTrue(fetch.isPresent()); - List<TriggerRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals("https://baz.com", result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals( - new JSONArray(aggregatable_trigger_data).toString(), - result.get(0).getAggregateTriggerData()); - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void testTriggerRequestWithAggregateTriggerData_invalidJson() throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - String aggregatable_trigger_data = - "[{\"key_piece\":\"0x400\",\"source_keys\":\"campaignCounts\"]," - + "\"filters\":" - + "{\"conversion_subdomain\":[\"electronics.megastore\"]}," - + "\"not_filters\":{\"product\":[\"1\"]}}," - + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'aggregatable_trigger_data': " - + aggregatable_trigger_data - + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testTriggerRequestWithAggregateTriggerData_tooManyEntries() throws Exception { - StringBuilder tooManyEntries = new StringBuilder("["); - for (int i = 0; i < MAX_AGGREGATABLE_TRIGGER_DATA + 1; i++) { - tooManyEntries.append(String.format( - "{\"key_piece\": \"0x15%1$s\",\"source_keys\":[\"campaign-%1$s\"]}", i)); - } - tooManyEntries.append("]"); - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'aggregatable_trigger_data': " - + tooManyEntries - + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testTriggerRequestWithAggregateTriggerData_invalidKeyPiece_missingPrefix() - throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - String aggregatable_trigger_data = - "[{\"key_piece\":\"0400\",\"source_keys\":[\"campaignCounts\"]," - + "\"filters\":" - + "{\"conversion_subdomain\":[\"electronics.megastore\"]}," - + "\"not_filters\":{\"product\":[\"1\"]}}," - + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'aggregatable_trigger_data': " - + aggregatable_trigger_data - + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testTriggerRequestWithAggregateTriggerData_invalidKeyPiece_tooLong() - throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - String aggregatable_trigger_data = - "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," - + "\"filters\":" - + "{\"conversion_subdomain\":[\"electronics.megastore\"]}," - + "\"not_filters\":{\"product\":[\"1\"]}}," - + "{\"key_piece\":\"" + LONG_AGGREGATE_KEY_PIECE - + "\",\"source_keys\":[\"geoValue\"]}]"; - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'aggregatable_trigger_data': " - + aggregatable_trigger_data - + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testTriggerRequestWithAggregateTriggerData_sourceKeys_notAnArray() - throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - String aggregatable_trigger_data = - "[{\"key_piece\":\"0x400\",\"source_keys\":{\"campaignCounts\": true}," - + "\"filters\":" - + "{\"conversion_subdomain\":[\"electronics.megastore\"]}," - + "\"not_filters\":{\"product\":[\"1\"]}}," - + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'aggregatable_trigger_data': " - + aggregatable_trigger_data - + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testTriggerRequestWithAggregateTriggerData_sourceKeys_tooManyKeys() - throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - StringBuilder tooManyKeys = new StringBuilder("["); - tooManyKeys.append(IntStream.range(0, MAX_AGGREGATE_KEYS_PER_REGISTRATION + 1) - .mapToObj(i -> "aggregate-key-" + i) - .collect(Collectors.joining(","))); - tooManyKeys.append("]"); - String aggregatable_trigger_data = - "[{\"key_piece\":\"0x400\",\"source_keys\": " + tooManyKeys + "," - + "\"filters\":" - + "{\"conversion_subdomain\":[\"electronics.megastore\"]}," - + "\"not_filters\":{\"product\":[\"1\"]}}," - + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'aggregatable_trigger_data': " - + aggregatable_trigger_data - + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testTriggerRequestWithAggregateTriggerData_sourceKeys_invalidKeyId() - throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - String aggregatable_trigger_data = - "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\", \"" - + LONG_AGGREGATE_KEY_ID + "\"]," - + "\"filters\":" - + "{\"conversion_subdomain\":[\"electronics.megastore\"]}," - + "\"not_filters\":{\"product\":[\"1\"]}}," - + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'aggregatable_trigger_data': " - + aggregatable_trigger_data - + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testTriggerRequestWithAggregateTriggerData_filters_tooManyFilters() - throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - StringBuilder filters = new StringBuilder("{"); - filters.append(IntStream.range(0, MAX_ATTRIBUTION_FILTERS + 1) - .mapToObj(i -> "\"filter-string-" + i + "\": [\"filter-value\"]") - .collect(Collectors.joining(","))); - filters.append("}"); - String aggregatable_trigger_data = - "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," - + "\"filters\": " + filters + "," - + "\"not_filters\":{\"product\":[\"1\"]}}," - + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'aggregatable_trigger_data': " - + aggregatable_trigger_data - + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testTriggerRequestWithAggregateTriggerData_filters_keyTooLong() throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - String filters = - "{\"product\":[\"1234\",\"2345\"], \"" + LONG_FILTER_STRING + "\":[\"id\"]}"; - String aggregatable_trigger_data = - "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," - + "\"filters\": " + filters + "," - + "\"not_filters\":{\"product\":[\"1\"]}}," - + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'aggregatable_trigger_data': " - + aggregatable_trigger_data - + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testTriggerRequestWithAggregateTriggerData_filters_tooManyValues() - throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - StringBuilder filters = new StringBuilder("{" - + "\"filter-string-1\": [\"filter-value-1\"]," - + "\"filter-string-2\": ["); - filters.append(IntStream.range(0, MAX_VALUES_PER_ATTRIBUTION_FILTER + 1) - .mapToObj(i -> "\"filter-value-" + i + "\"") - .collect(Collectors.joining(","))); - filters.append("]}"); - String aggregatable_trigger_data = - "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," - + "\"filters\": " + filters + "," - + "\"not_filters\":{\"product\":[\"1\"]}}," - + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'aggregatable_trigger_data': " - + aggregatable_trigger_data - + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testTriggerRequestWithAggregateTriggerData_filters_valueTooLong() - throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - String filters = - "{\"product\":[\"1234\",\"" + LONG_FILTER_STRING + "\"], \"ctid\":[\"id\"]}"; - String aggregatable_trigger_data = - "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," - + "\"filters\": " + filters + "," - + "\"not_filters\":{\"product\":[\"1\"]}}," - + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'aggregatable_trigger_data': " - + aggregatable_trigger_data - + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testTriggerRequestWithAggregateTriggerData_notFilters_tooManyFilters() - throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - StringBuilder filters = new StringBuilder("{"); - filters.append(IntStream.range(0, MAX_ATTRIBUTION_FILTERS + 1) - .mapToObj(i -> "\"filter-string-" + i + "\": [\"filter-value\"]") - .collect(Collectors.joining(","))); - filters.append("}"); - String aggregatable_trigger_data = - "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," - + "\"not_filters\": " + filters + "," - + "\"filters\":{\"product\":[\"1\"]}}," - + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'aggregatable_trigger_data': " - + aggregatable_trigger_data - + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testTriggerRequestWithAggregateTriggerData_notFilters_keyTooLong() - throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - String filters = - "{\"product\":[\"1234\",\"2345\"], \"" + LONG_FILTER_STRING + "\":[\"id\"]}"; - String aggregatable_trigger_data = - "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," - + "\"not_filters\": " + filters + "," - + "\"filters\":{\"product\":[\"1\"]}}," - + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'aggregatable_trigger_data': " - + aggregatable_trigger_data - + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testTriggerRequestWithAggregateTriggerData_notFilters_tooManyValues() - throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - StringBuilder filters = new StringBuilder("{" - + "\"filter-string-1\": [\"filter-value-1\"]," - + "\"filter-string-2\": ["); - filters.append(IntStream.range(0, MAX_VALUES_PER_ATTRIBUTION_FILTER + 1) - .mapToObj(i -> "\"filter-value-" + i + "\"") - .collect(Collectors.joining(","))); - filters.append("]}"); - String aggregatable_trigger_data = - "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," - + "\"not_filters\": " + filters + "," - + "\"filters\":{\"product\":[\"1\"]}}," - + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'aggregatable_trigger_data': " - + aggregatable_trigger_data - + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testTriggerRequestWithAggregateTriggerData_notFilters_valueTooLong() - throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - String filters = - "{\"product\":[\"1234\",\"" + LONG_FILTER_STRING + "\"], \"ctid\":[\"id\"]}"; - String aggregatable_trigger_data = - "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," - + "\"not_filters\": " + filters + "," - + "\"filters\":{\"product\":[\"1\"]}}," - + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'aggregatable_trigger_data': " - + aggregatable_trigger_data - + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testBasicTriggerRequestWithAggregatableValues() throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - String aggregatable_values = "{\"campaignCounts\":32768,\"geoValue\":1644}"; - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'aggregatable_values': " - + aggregatable_values - + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertTrue(fetch.isPresent()); - List<TriggerRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals("https://baz.com", result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals( - new JSONObject(aggregatable_values).toString(), result.get(0).getAggregateValues()); - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void testTriggerRequestWithAggregatableValues_invalidJson() throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - String aggregatable_values = "{\"campaignCounts\":32768\"geoValue\":1644}"; - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'aggregatable_values': " - + aggregatable_values - + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testTriggerRequestWithAggregatableValues_tooManyKeys() throws Exception { - StringBuilder tooManyKeys = new StringBuilder("{"); - tooManyKeys.append(IntStream.range(0, MAX_AGGREGATE_KEYS_PER_REGISTRATION + 1) - .mapToObj(i -> String.format("\"key-%s\": 12345,", i)) - .collect(Collectors.joining(","))); - tooManyKeys.append("}"); - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of("{" + "'aggregatable_values': " + tooManyKeys + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void testTriggerRequestWithAggregatableValues_invalidKeyId() throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - String aggregatable_values = "{\"campaignCounts\":32768, \"" + LONG_AGGREGATE_KEY_ID - + "\":1644}"; - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'aggregatable_values': " - + aggregatable_values - + "}"))); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertFalse(fetch.isPresent()); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void fetchTrigger_withReportingFilters_success() throws IOException, JSONException { - // Setup - String filters = - "{\n" - + " \"key_1\": [\"value_1\", \"value_2\"],\n" - + " \"key_2\": [\"value_1\", \"value_2\"]\n" - + "}"; - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of("{" + "'filters': " + filters + "}"))); - - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertTrue(fetch.isPresent()); - List<TriggerRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals("https://baz.com", result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals(new JSONObject(filters).toString(), result.get(0).getFilters()); - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void fetchWebTriggers_basic_success() throws IOException, JSONException { - // Setup - TriggerRegistration expectedResult1 = - new TriggerRegistration.Builder() - .setTopOrigin(Uri.parse(TOP_ORIGIN)) - .setEventTriggers(new JSONArray(EVENT_TRIGGERS_1).toString()) - .setEnrollmentId(ENROLLMENT_ID) - .setDebugKey(DEBUG_KEY) - .build(); - TriggerRegistration expectedResult2 = - new TriggerRegistration.Builder() - .setTopOrigin(Uri.parse(TOP_ORIGIN)) - .setEventTriggers(new JSONArray(EVENT_TRIGGERS_2).toString()) - .setEnrollmentId(ENROLLMENT_ID) - .build(); - - WebTriggerRegistrationRequest request = - buildWebTriggerRegistrationRequest( - Arrays.asList(TRIGGER_REGISTRATION_1, TRIGGER_REGISTRATION_2), TOP_ORIGIN); - doReturn(mUrlConnection1).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); - doReturn(mUrlConnection2).when(mFetcher).openUrl(new URL(REGISTRATION_URI_2.toString())); - when(mUrlConnection1.getResponseCode()).thenReturn(200); - when(mUrlConnection2.getResponseCode()).thenReturn(200); - - Map<String, List<String>> headersRequest = new HashMap<>(); - headersRequest.put( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'event_trigger_data': " - + EVENT_TRIGGERS_1 - + ", 'debug_key': '" - + DEBUG_KEY - + "'" - + "}")); - when(mUrlConnection1.getHeaderFields()).thenReturn(headersRequest); - when(mUrlConnection2.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of("{" + "'event_trigger_data': " + EVENT_TRIGGERS_2 + "}"))); - - // Execution - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchWebTriggers(request, true); - - // Assertion - assertTrue(fetch.isPresent()); - List<TriggerRegistration> result = fetch.get(); - assertEquals(2, result.size()); - assertEquals( - new HashSet<>(Arrays.asList(expectedResult1, expectedResult2)), - new HashSet<>(result)); - verify(mUrlConnection1).setRequestMethod("POST"); - verify(mUrlConnection2).setRequestMethod("POST"); - } - - @Test - public void fetchWebTriggerSuccessWithoutAdIdPermission() throws IOException, JSONException { - // Setup - TriggerRegistration expectedResult1 = - new TriggerRegistration.Builder() - .setTopOrigin(Uri.parse(TOP_ORIGIN)) - .setEventTriggers(new JSONArray(EVENT_TRIGGERS_1).toString()) - .setEnrollmentId(ENROLLMENT_ID) - .build(); - TriggerRegistration expectedResult2 = - new TriggerRegistration.Builder() - .setTopOrigin(Uri.parse(TOP_ORIGIN)) - .setEventTriggers(new JSONArray(EVENT_TRIGGERS_2).toString()) - .setEnrollmentId(ENROLLMENT_ID) - .build(); - - WebTriggerRegistrationRequest request = - buildWebTriggerRegistrationRequest( - Arrays.asList(TRIGGER_REGISTRATION_1, TRIGGER_REGISTRATION_2), TOP_ORIGIN); - doReturn(mUrlConnection1).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); - doReturn(mUrlConnection2).when(mFetcher).openUrl(new URL(REGISTRATION_URI_2.toString())); - when(mUrlConnection1.getResponseCode()).thenReturn(200); - when(mUrlConnection2.getResponseCode()).thenReturn(200); - - Map<String, List<String>> headersRequest = new HashMap<>(); - headersRequest.put( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'event_trigger_data': " - + EVENT_TRIGGERS_1 - + ", 'debug_key': '" - + DEBUG_KEY - + "'" - + "}")); - when(mUrlConnection1.getHeaderFields()).thenReturn(headersRequest); - when(mUrlConnection2.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of("{" + "'event_trigger_data': " + EVENT_TRIGGERS_2 + "}"))); - - // Execution - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchWebTriggers(request, false); - - // Assertion - assertTrue(fetch.isPresent()); - List<TriggerRegistration> result = fetch.get(); - assertEquals(2, result.size()); - assertEquals( - new HashSet<>(Arrays.asList(expectedResult1, expectedResult2)), - new HashSet<>(result)); - verify(mUrlConnection1).setRequestMethod("POST"); - verify(mUrlConnection2).setRequestMethod("POST"); - } - - @Test - public void fetchWebTriggers_withExtendedHeaders_success() throws IOException, JSONException { - // Setup - WebTriggerRegistrationRequest request = - buildWebTriggerRegistrationRequest( - Collections.singletonList(TRIGGER_REGISTRATION_1), TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); - when(mUrlConnection.getResponseCode()).thenReturn(200); - String aggregatableValues = "{\"campaignCounts\":32768,\"geoValue\":1644}"; - String filters = - "{\n" - + " \"key_1\": [\"value_1\", \"value_2\"],\n" - + " \"key_2\": [\"value_1\", \"value_2\"]\n" - + "}"; - String aggregatableTriggerData = - "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," - + "\"filters\":" - + "{\"conversion_subdomain\":[\"electronics.megastore\"]}," - + "\"not_filters\":{\"product\":[\"1\"]}}," - + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of( - "{" - + "'event_trigger_data': " - + EVENT_TRIGGERS_1 - + ", 'filters': " - + filters - + ", 'aggregatable_values': " - + aggregatableValues - + ", 'aggregatable_trigger_data': " - + aggregatableTriggerData - + "}"))); - TriggerRegistration expectedResult = - new TriggerRegistration.Builder() - .setTopOrigin(Uri.parse(TOP_ORIGIN)) - .setEventTriggers(new JSONArray(EVENT_TRIGGERS_1).toString()) - .setEnrollmentId(ENROLLMENT_ID) - .setFilters(new JSONObject(filters).toString()) - .setAggregateTriggerData(new JSONArray(aggregatableTriggerData).toString()) - .setAggregateValues(new JSONObject(aggregatableValues).toString()) - .build(); - - // Execution - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchWebTriggers(request, true); - - // Assertion - assertTrue(fetch.isPresent()); - List<TriggerRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals(expectedResult, result.get(0)); - verify(mUrlConnection).setRequestMethod("POST"); - } - - @Test - public void fetchWebTriggers_withRedirects_ignoresRedirects() - throws IOException, JSONException { - // Setup - WebTriggerRegistrationRequest request = - buildWebTriggerRegistrationRequest( - Collections.singletonList(TRIGGER_REGISTRATION_1), TOP_ORIGIN); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(REGISTRATION_URI_1.toString())); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of("{" + "'event_trigger_data': " + EVENT_TRIGGERS_1 + "}"), - "Attribution-Reporting-Redirect", - List.of(ALT_REGISTRATION))); - TriggerRegistration expectedResult = - new TriggerRegistration.Builder() - .setTopOrigin(Uri.parse(TOP_ORIGIN)) - .setEventTriggers(new JSONArray(EVENT_TRIGGERS_1).toString()) - .setEnrollmentId(ENROLLMENT_ID) - .build(); - - // Execution - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchWebTriggers(request, true); - - // Assertion - assertTrue(fetch.isPresent()); - List<TriggerRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals(expectedResult, result.get(0)); - verify(mUrlConnection, times(1)).setRequestMethod("POST"); - verify(mFetcher, times(1)).openUrl(any()); - } - - @Test - public void basicTriggerRequest_headersMoreThanMaxResponseSize_emitsMetricsWithAdTechDomain() - throws Exception { - RegistrationRequest request = buildRequest(TRIGGER_URI, TOP_ORIGIN); - MeasurementRegistrationResponseStats expectedStats = - new MeasurementRegistrationResponseStats.Builder( - AD_SERVICES_MEASUREMENT_REGISTRATIONS, - AD_SERVICES_MEASUREMENT_REGISTRATIONS__TYPE__TRIGGER, - 221) - .setAdTechDomain(TRIGGER_URI) - .build(); - doReturn(mUrlConnection).when(mFetcher).openUrl(new URL(TRIGGER_URI)); - when(mUrlConnection.getResponseCode()).thenReturn(200); - when(mUrlConnection.getHeaderFields()) - .thenReturn( - Map.of( - "Attribution-Reporting-Register-Trigger", - List.of("{" + "'event_trigger_data':" + EVENT_TRIGGERS_1 + "}"))); - doReturn(5L).when(mFlags).getMaxResponseBasedRegistrationPayloadSizeBytes(); - Optional<List<TriggerRegistration>> fetch = mFetcher.fetchTrigger(request); - assertTrue(fetch.isPresent()); - List<TriggerRegistration> result = fetch.get(); - assertEquals(1, result.size()); - assertEquals(TOP_ORIGIN, result.get(0).getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, result.get(0).getEnrollmentId()); - assertEquals(new JSONArray(EVENT_TRIGGERS_1).toString(), result.get(0).getEventTriggers()); - verify(mUrlConnection).setRequestMethod("POST"); - verify(mLogger).logMeasurementRegistrationsResponseSize(eq(expectedStats)); - } - - private RegistrationRequest buildRequest(String triggerUri, String topOriginUri) { - return new RegistrationRequest.Builder() - .setRegistrationType(RegistrationRequest.REGISTER_TRIGGER) - .setRegistrationUri(Uri.parse(triggerUri)) - .setTopOriginUri(Uri.parse(topOriginUri)) - .setPackageName(CONTEXT.getAttributionSource().getPackageName()) - .setAdIdPermissionGranted(true) - .build(); - } - - private RegistrationRequest buildRequestWithoutAdIdPermission( - String triggerUri, String topOriginUri) { - return new RegistrationRequest.Builder() - .setRegistrationType(RegistrationRequest.REGISTER_TRIGGER) - .setRegistrationUri(Uri.parse(triggerUri)) - .setTopOriginUri(Uri.parse(topOriginUri)) - .setPackageName(CONTEXT.getAttributionSource().getPackageName()) - .setAdIdPermissionGranted(false) - .build(); - } - - private WebTriggerRegistrationRequest buildWebTriggerRegistrationRequest( - List<WebTriggerParams> triggerParams, String topOrigin) { - return new WebTriggerRegistrationRequest.Builder(triggerParams, Uri.parse(topOrigin)) - .build(); - } -} diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/TriggerRegistrationTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/TriggerRegistrationTest.java deleted file mode 100644 index d42f96fc6d..0000000000 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/registration/TriggerRegistrationTest.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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.measurement.registration; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -import android.net.Uri; - -import androidx.test.filters.SmallTest; - -import org.junit.Test; - - -/** - * Unit tests for {@link TriggerRegistration} - */ -@SmallTest -public final class TriggerRegistrationTest { - private static final Uri TOP_ORIGIN = Uri.parse("https://foo.com"); - private static final String ENROLLMENT_ID = "enrollment-id"; - private static final String TOP_LEVEL_FILTERS_JSON_STRING = - "{\n" - + " \"key_1\": [\"value_1\", \"value_2\"],\n" - + " \"key_2\": [\"value_1\", \"value_2\"]\n" - + "}\n"; - private static final String EVENT_TRIGGERS = - "[\n" - + "{\n" - + " \"trigger_data\": \"1\",\n" - + " \"priority\": \"345678\",\n" - + " \"deduplication_key\": \"2345678\",\n" - + " \"filters\": {\n" - + " \"source_type\": [\"navigation\"],\n" - + " \"key_1\": [\"value_1\"] \n" - + " }\n" - + "}" - + "]\n"; - - private static final Long DEBUG_KEY = 23478951L; - - private static final String AGGREGATE_TRIGGER_DATA = - "[{\"key_piece\":\"0x400\",\"source_keys\":[\"campaignCounts\"]," - + "\"not_filters\":{\"product\":[\"1\"]}}," - + "{\"key_piece\":\"0xA80\",\"source_keys\":[\"geoValue\"]}]"; - - private TriggerRegistration createExampleResponse() { - return new TriggerRegistration.Builder() - .setTopOrigin(TOP_ORIGIN) - .setEnrollmentId(ENROLLMENT_ID) - .setEventTriggers(EVENT_TRIGGERS) - .setAggregateTriggerData(AGGREGATE_TRIGGER_DATA) - .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") - .setFilters(TOP_LEVEL_FILTERS_JSON_STRING) - .setDebugKey(DEBUG_KEY) - .build(); - } - - void verifyExampleResponse(TriggerRegistration triggerRegistration) { - assertEquals("https://foo.com", triggerRegistration.getTopOrigin().toString()); - assertEquals(ENROLLMENT_ID, triggerRegistration.getEnrollmentId()); - assertEquals(EVENT_TRIGGERS, triggerRegistration.getEventTriggers()); - assertEquals(AGGREGATE_TRIGGER_DATA, triggerRegistration.getAggregateTriggerData()); - assertEquals( - "{\"campaignCounts\":32768,\"geoValue\":1644}", - triggerRegistration.getAggregateValues()); - assertEquals(TOP_LEVEL_FILTERS_JSON_STRING, triggerRegistration.getFilters()); - assertEquals(DEBUG_KEY, triggerRegistration.getDebugKey()); - } - - @Test - public void testCreation() throws Exception { - verifyExampleResponse(createExampleResponse()); - } - - @Test - public void testDefaults() throws Exception { - TriggerRegistration response = - new TriggerRegistration.Builder() - .setTopOrigin(TOP_ORIGIN) - .setEnrollmentId(ENROLLMENT_ID) - .build(); - assertEquals(TOP_ORIGIN, response.getTopOrigin()); - assertEquals(ENROLLMENT_ID, response.getEnrollmentId()); - assertNull(response.getEventTriggers()); - assertNull(response.getAggregateTriggerData()); - assertNull(response.getAggregateValues()); - assertNull(response.getFilters()); - } - - @Test - public void equals_success() { - assertEquals(createExampleResponse(), createExampleResponse()); - } -} diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateFallbackReportingJobServiceTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateFallbackReportingJobServiceTest.java index 8757ab94d1..781224d349 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateFallbackReportingJobServiceTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateFallbackReportingJobServiceTest.java @@ -19,6 +19,7 @@ package com.android.adservices.service.measurement.reporting; import static com.android.adservices.service.AdServicesConfig.MEASUREMENT_AGGREGATE_FALLBACK_REPORTING_JOB_ID; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -35,33 +36,31 @@ import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.content.Context; -import android.provider.DeviceConfig; +import com.android.adservices.data.enrollment.EnrollmentDao; import com.android.adservices.data.measurement.DatastoreManager; import com.android.adservices.data.measurement.DatastoreManagerFactory; +import com.android.adservices.service.AdServicesConfig; +import com.android.adservices.service.Flags; +import com.android.adservices.service.FlagsFactory; import com.android.compatibility.common.util.TestUtils; import com.android.dx.mockito.inline.extended.ExtendedMockito; -import com.android.modules.utils.testing.TestableDeviceConfig; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; import java.util.Optional; +import java.util.concurrent.TimeUnit; /** * Unit test for {@link AggregateFallbackReportingJobService */ public class AggregateFallbackReportingJobServiceTest { - // This rule is used for configuring P/H flags - @Rule - public final TestableDeviceConfig.TestableDeviceConfigRule mDeviceConfigRule = - new TestableDeviceConfig.TestableDeviceConfigRule(); - - private static final long WAIT_IN_MILLIS = 50L; + private static final long WAIT_IN_MILLIS = 1_000L; private DatastoreManager mMockDatastoreManager; private JobScheduler mMockJobScheduler; @@ -77,10 +76,11 @@ public class AggregateFallbackReportingJobServiceTest { @Test public void onStartJob_killSwitchOn() throws Exception { - enableKillSwitch(); - runWithMocks( () -> { + // Setup + enableKillSwitch(); + // Execute boolean result = mSpyService.onStartJob(mock(JobParameters.class)); @@ -97,10 +97,11 @@ public class AggregateFallbackReportingJobServiceTest { @Test public void onStartJob_killSwitchOff() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { + // Setup + disableKillSwitch(); + // Execute boolean result = mSpyService.onStartJob(mock(JobParameters.class)); @@ -117,11 +118,11 @@ public class AggregateFallbackReportingJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOn_dontSchedule() throws Exception { - enableKillSwitch(); - runWithMocks( () -> { // Setup + enableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -149,11 +150,11 @@ public class AggregateFallbackReportingJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOff_previouslyExecuted_dontForceSchedule_dontSchedule() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { // Setup + disableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -181,11 +182,11 @@ public class AggregateFallbackReportingJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOff_previouslyExecuted_forceSchedule_schedule() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { // Setup + disableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -213,11 +214,11 @@ public class AggregateFallbackReportingJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOff_previouslyNotExecuted_dontForceSchedule_schedule() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { // Setup + disableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -240,11 +241,37 @@ public class AggregateFallbackReportingJobServiceTest { }); } + @Test + public void testSchedule_jobInfoIsPersisted() throws Exception { + runWithMocks( + () -> { + final JobScheduler jobScheduler = mock(JobScheduler.class); + final ArgumentCaptor<JobInfo> captor = ArgumentCaptor.forClass(JobInfo.class); + + // Execute + ExtendedMockito.doCallRealMethod() + .when( + () -> + AggregateFallbackReportingJobService.schedule( + any(), any())); + AggregateFallbackReportingJobService.schedule( + mock(Context.class), jobScheduler); + + // Validate + verify(jobScheduler, times(1)).schedule(captor.capture()); + assertNotNull(captor.getValue()); + assertTrue(captor.getValue().isPersisted()); + }); + } + private void runWithMocks(TestUtils.RunnableWithThrow execute) throws Exception { MockitoSession session = ExtendedMockito.mockitoSession() - .spyStatic(DatastoreManagerFactory.class) + .spyStatic(AdServicesConfig.class) .spyStatic(AggregateFallbackReportingJobService.class) + .spyStatic(DatastoreManagerFactory.class) + .spyStatic(EnrollmentDao.class) + .spyStatic(FlagsFactory.class) .strictness(Strictness.LENIENT) .startMocking(); try { @@ -256,6 +283,14 @@ public class AggregateFallbackReportingJobServiceTest { doNothing().when(mSpyService).jobFinished(any(), anyBoolean()); doReturn(mMockJobScheduler).when(mSpyService).getSystemService(JobScheduler.class); doReturn(Mockito.mock(Context.class)).when(mSpyService).getApplicationContext(); + ExtendedMockito.doReturn(TimeUnit.HOURS.toMillis(4)) + .when(AdServicesConfig::getMeasurementAggregateMainReportingJobPeriodMs); + ExtendedMockito.doReturn(TimeUnit.HOURS.toMillis(24)) + .when(AdServicesConfig::getMeasurementAggregateFallbackReportingJobPeriodMs); + ExtendedMockito.doReturn("http://example.com") + .when(AdServicesConfig::getMeasurementAggregateEncryptionKeyCoordinatorUrl); + ExtendedMockito.doReturn(mock(EnrollmentDao.class)) + .when(() -> EnrollmentDao.getInstance(any())); ExtendedMockito.doReturn(mMockDatastoreManager) .when(() -> DatastoreManagerFactory.getDatastoreManager(any())); ExtendedMockito.doNothing() @@ -277,10 +312,10 @@ public class AggregateFallbackReportingJobServiceTest { } private void toggleKillSwitch(boolean value) { - DeviceConfig.setProperty( - DeviceConfig.NAMESPACE_ADSERVICES, - "measurement_job_aggregate_fallback_reporting_kill_switch", - Boolean.toString(value), - /* makeDefault */ false); + Flags mockFlags = Mockito.mock(Flags.class); + ExtendedMockito.doReturn(mockFlags).when(FlagsFactory::getFlags); + ExtendedMockito.doReturn(value) + .when(mockFlags) + .getMeasurementJobAggregateFallbackReportingKillSwitch(); } } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateReportBodyTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateReportBodyTest.java index 3d79b2e089..d285a741ae 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateReportBodyTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateReportBodyTest.java @@ -22,8 +22,10 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import com.android.adservices.HpkeJni; +import com.android.adservices.service.measurement.aggregation.AggregateCryptoConverter; import com.android.adservices.service.measurement.aggregation.AggregateCryptoFixture; import com.android.adservices.service.measurement.aggregation.AggregateEncryptionKey; +import com.android.adservices.service.measurement.util.UnsignedLong; import org.json.JSONArray; import org.json.JSONException; @@ -50,8 +52,8 @@ public class AggregateReportBodyTest { private static final String SCHEDULED_REPORT_TIME = "1246174158155"; private static final String VERSION = "12"; private static final String REPORT_ID = "A1"; - private static final Long SOURCE_DEBUG_KEY = 27628792L; - private static final Long TRIGGER_DEBUG_KEY = 23443234L; + private static final UnsignedLong SOURCE_DEBUG_KEY = new UnsignedLong(27628792L); + private static final UnsignedLong TRIGGER_DEBUG_KEY = new UnsignedLong(23443234L); private static final String REPORTING_ORIGIN = "https://adtech.domain"; private static final String DEBUG_CLEARTEXT_PAYLOAD = "{\"operation\":\"histogram\"," + "\"data\":[{\"bucket\":1369,\"value\":32768},{\"bucket\":3461," @@ -123,10 +125,8 @@ public class AggregateReportBodyTest { assertEquals(REPORTING_ORIGIN, sharedInfoJson.get("reporting_origin")); assertEquals(ATTRIBUTION_DESTINATION, sharedInfoJson.get("attribution_destination")); assertEquals(SOURCE_REGISTRATION_TIME, sharedInfoJson.get("source_registration_time")); - assertEquals( - Long.toUnsignedString(SOURCE_DEBUG_KEY), aggregateJson.get("source_debug_key")); - assertEquals( - Long.toUnsignedString(TRIGGER_DEBUG_KEY), aggregateJson.get("trigger_debug_key")); + assertEquals(SOURCE_DEBUG_KEY.toString(), aggregateJson.get("source_debug_key")); + assertEquals(TRIGGER_DEBUG_KEY.toString(), aggregateJson.get("trigger_debug_key")); } @Test @@ -158,8 +158,7 @@ public class AggregateReportBodyTest { assertEquals(REPORTING_ORIGIN, sharedInfoJson.get("reporting_origin")); assertEquals(ATTRIBUTION_DESTINATION, sharedInfoJson.get("attribution_destination")); assertEquals(SOURCE_REGISTRATION_TIME, sharedInfoJson.get("source_registration_time")); - assertEquals( - Long.toUnsignedString(SOURCE_DEBUG_KEY), aggregateJson.get("source_debug_key")); + assertEquals(SOURCE_DEBUG_KEY.toString(), aggregateJson.get("source_debug_key")); assertNull(aggregateJson.opt("trigger_debug_key")); } @@ -177,8 +176,7 @@ public class AggregateReportBodyTest { assertEquals(ATTRIBUTION_DESTINATION, sharedInfoJson.get("attribution_destination")); assertEquals(SOURCE_REGISTRATION_TIME, sharedInfoJson.get("source_registration_time")); assertNull(aggregateJson.opt("source_debug_key")); - assertEquals( - Long.toUnsignedString(TRIGGER_DEBUG_KEY), aggregateJson.get("trigger_debug_key")); + assertEquals(TRIGGER_DEBUG_KEY.toString(), aggregateJson.get("trigger_debug_key")); } @Test @@ -196,13 +194,50 @@ public class AggregateReportBodyTest { assertEncryptedPayload(aggregateServicePayloads); } - private void assertEncodedDebugPayload(JSONObject aggregateServicePayloads) throws Exception { - final String encodedPayloadBase64 = - (String) aggregateServicePayloads.get("debug_cleartext_payload"); - assertNotNull(encodedPayloadBase64); + @Test + public void testAggregationServicePayloadsJsonSerializationWithDebugKey() throws Exception { + AggregateReportBody aggregateReport = + createAggregateReportBodyExampleWithSingleTriggerDebugKey(); + + AggregateEncryptionKey key = AggregateCryptoFixture.getKey(); + JSONArray aggregationServicePayloadsJson = + aggregateReport.aggregationServicePayloadsToJson(/* sharedInfo = */ null, key); + + JSONObject aggregateServicePayloads = aggregationServicePayloadsJson.getJSONObject(0); - final byte[] cborEncodedPayload = Base64.getDecoder().decode(encodedPayloadBase64); - assertCborEncoded(cborEncodedPayload); + assertEquals(key.getKeyId(), aggregateServicePayloads.get("key_id")); + assertEquals( + AggregateCryptoConverter.encode(DEBUG_CLEARTEXT_PAYLOAD), + aggregateServicePayloads.opt("debug_cleartext_payload")); + assertEncodedDebugPayload(aggregateServicePayloads); + assertEncryptedPayload(aggregateServicePayloads); + } + + @Test + public void testAggregationServicePayloadsJsonSerializationWithoutDebugKey() throws Exception { + AggregateReportBody aggregateReport = createAggregateReportBodyExampleWithNullDebugKeys(); + + AggregateEncryptionKey key = AggregateCryptoFixture.getKey(); + JSONArray aggregationServicePayloadsJson = + aggregateReport.aggregationServicePayloadsToJson(/* sharedInfo = */ null, key); + + JSONObject aggregateServicePayloads = aggregationServicePayloadsJson.getJSONObject(0); + + assertEquals(key.getKeyId(), aggregateServicePayloads.get("key_id")); + assertNull(aggregateServicePayloads.opt("debug_cleartext_payload")); + assertEncodedDebugPayload(aggregateServicePayloads); + assertEncryptedPayload(aggregateServicePayloads); + } + + private void assertEncodedDebugPayload(JSONObject aggregateServicePayloads) throws Exception { + if (!aggregateServicePayloads.isNull("debug_cleartext_payload")) { + final String encodedPayloadBase64 = + (String) aggregateServicePayloads.get("debug_cleartext_payload"); + assertNotNull(encodedPayloadBase64); + + final byte[] cborEncodedPayload = Base64.getDecoder().decode(encodedPayloadBase64); + assertCborEncoded(cborEncodedPayload); + } } private void assertEncryptedPayload(JSONObject aggregateServicePayloads) throws Exception { diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateReportSenderTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateReportSenderTest.java index 9ff8b9b544..5d0a1372b9 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateReportSenderTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateReportSenderTest.java @@ -23,6 +23,8 @@ import android.net.Uri; import com.android.adservices.service.measurement.aggregation.AggregateCryptoFixture; import com.android.modules.utils.testing.TestableDeviceConfig; +import com.google.common.truth.Truth; + import org.json.JSONException; import org.json.JSONObject; import org.junit.Rule; @@ -79,7 +81,7 @@ public class AggregateReportSenderTest { createAggregateReportBodyExample1().toJson(AggregateCryptoFixture.getKey()); Uri reportingOrigin = Uri.parse(REPORTING_ORIGIN); - AggregateReportSender aggregateReportSender = new AggregateReportSender(); + AggregateReportSender aggregateReportSender = new AggregateReportSender(false); AggregateReportSender spyAggregateReportSender = Mockito.spy(aggregateReportSender); Mockito.doReturn(httpUrlConnection).when(spyAggregateReportSender) @@ -98,8 +100,16 @@ public class AggregateReportSenderTest { URL spyUrl = Mockito.spy(new URL("https://foo")); Mockito.doReturn(mockConnection).when(spyUrl).openConnection(); - AggregateReportSender aggregateReportSender = new AggregateReportSender(); + AggregateReportSender aggregateReportSender = new AggregateReportSender(false); HttpURLConnection connection = aggregateReportSender.createHttpUrlConnection(spyUrl); assertEquals(mockConnection, connection); } + + @Test + public void testDebugReportUriPath() { + Truth.assertThat(new AggregateReportSender(false).getReportUriPath()) + .isEqualTo(AggregateReportSender.AGGREGATE_ATTRIBUTION_REPORT_URI_PATH); + Truth.assertThat(new AggregateReportSender(true).getReportUriPath()) + .isEqualTo(AggregateReportSender.DEBUG_AGGREGATE_ATTRIBUTION_REPORT_URI_PATH); + } } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateReportingJobHandlerIntegrationTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateReportingJobHandlerIntegrationTest.java index 09e4a71b5e..f0d3f8220a 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateReportingJobHandlerIntegrationTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateReportingJobHandlerIntegrationTest.java @@ -19,11 +19,12 @@ package com.android.adservices.service.measurement.reporting; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.android.adservices.data.DbTestUtil; import com.android.adservices.data.enrollment.EnrollmentDao; import com.android.adservices.data.measurement.AbstractDbIntegrationTest; import com.android.adservices.data.measurement.DatastoreManager; -import com.android.adservices.data.measurement.DatastoreManagerFactory; import com.android.adservices.data.measurement.DbState; +import com.android.adservices.data.measurement.SQLDatastoreManager; import com.android.adservices.service.enrollment.EnrollmentData; import com.android.adservices.service.measurement.aggregation.AggregateCryptoFixture; import com.android.adservices.service.measurement.aggregation.AggregateEncryptionKey; @@ -93,7 +94,8 @@ public class AggregateReportingJobHandlerIntegrationTest extends AbstractDbInteg }); Mockito.doReturn(isEnrolled ? ENROLLMENT : null) .when(mEnrollmentDao).getEnrollmentData(Mockito.any()); - DatastoreManager datastoreManager = DatastoreManagerFactory.getDatastoreManager(sContext); + DatastoreManager datastoreManager = + new SQLDatastoreManager(DbTestUtil.getDbHelperForTest()); AggregateReportingJobHandler spyReportingService = Mockito.spy(new AggregateReportingJobHandler( mEnrollmentDao, datastoreManager, mockKeyManager)); diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateReportingJobHandlerTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateReportingJobHandlerTest.java index 01a9da6dab..d77bc6d2f4 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateReportingJobHandlerTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateReportingJobHandlerTest.java @@ -42,6 +42,7 @@ import com.android.adservices.service.measurement.aggregation.AggregateCryptoFix import com.android.adservices.service.measurement.aggregation.AggregateEncryptionKey; import com.android.adservices.service.measurement.aggregation.AggregateEncryptionKeyManager; import com.android.adservices.service.measurement.aggregation.AggregateReport; +import com.android.adservices.service.measurement.util.UnsignedLong; import org.json.JSONException; import org.json.JSONObject; @@ -72,8 +73,8 @@ public class AggregateReportingJobHandlerTest { private static final String CLEARTEXT_PAYLOAD = "{\"operation\":\"histogram\",\"data\":[{\"bucket\":1,\"value\":2}]}"; - private static final Long SOURCE_DEBUG_KEY = 237865L; - private static final Long TRIGGER_DEBUG_KEY = 928762L; + private static final UnsignedLong SOURCE_DEBUG_KEY = new UnsignedLong(237865L); + private static final UnsignedLong TRIGGER_DEBUG_KEY = new UnsignedLong(928762L); protected static final Context sContext = ApplicationProvider.getApplicationContext(); DatastoreManager mDatastoreManager; @@ -86,6 +87,7 @@ public class AggregateReportingJobHandlerTest { AggregateReportingJobHandler mAggregateReportingJobHandler; AggregateReportingJobHandler mSpyAggregateReportingJobHandler; + AggregateReportingJobHandler mSpyDebugAggregateReportingJobHandler; class FakeDatasoreManager extends DatastoreManager { @@ -118,6 +120,11 @@ public class AggregateReportingJobHandlerTest { mAggregateReportingJobHandler = new AggregateReportingJobHandler( mEnrollmentDao, mDatastoreManager, mockKeyManager); mSpyAggregateReportingJobHandler = Mockito.spy(mAggregateReportingJobHandler); + mSpyDebugAggregateReportingJobHandler = + Mockito.spy( + new AggregateReportingJobHandler( + mEnrollmentDao, mDatastoreManager, mockKeyManager) + .setDebugReport(true)); } @Test @@ -147,13 +154,57 @@ public class AggregateReportingJobHandlerTest { .when(mSpyAggregateReportingJobHandler) .createReportJsonPayload(Mockito.any(), Mockito.any(), Mockito.any()); - doNothing().when(mMeasurementDao).markAggregateReportDelivered(aggregateReport.getId()); + doNothing() + .when(mMeasurementDao) + .markAggregateReportStatus( + aggregateReport.getId(), AggregateReport.Status.DELIVERED); Assert.assertEquals( AdServicesStatusUtils.STATUS_SUCCESS, mSpyAggregateReportingJobHandler.performReport( aggregateReport.getId(), AggregateCryptoFixture.getKey())); - verify(mMeasurementDao, times(1)).markAggregateReportDelivered(any()); + verify(mMeasurementDao, times(1)).markAggregateReportStatus(any(), anyInt()); + verify(mTransaction, times(2)).begin(); + verify(mTransaction, times(2)).end(); + } + + @Test + public void testSendReportForPendingDebugReportSuccess() + throws DatastoreException, IOException, JSONException { + AggregateReport aggregateReport = + new AggregateReport.Builder() + .setId("aggregateReportId") + .setStatus(AggregateReport.Status.PENDING) + .setDebugReportStatus(AggregateReport.DebugReportStatus.PENDING) + .setEnrollmentId(ENROLLMENT_ID) + .setSourceDebugKey(SOURCE_DEBUG_KEY) + .setTriggerDebugKey(TRIGGER_DEBUG_KEY) + .build(); + JSONObject aggregateReportBody = + new AggregateReportBody.Builder() + .setReportId(aggregateReport.getId()) + .setDebugCleartextPayload(CLEARTEXT_PAYLOAD) + .build() + .toJson(AggregateCryptoFixture.getKey()); + + when(mMeasurementDao.getAggregateReport(aggregateReport.getId())) + .thenReturn(aggregateReport); + doReturn(HttpURLConnection.HTTP_OK) + .when(mSpyDebugAggregateReportingJobHandler) + .makeHttpPostRequest(Mockito.any(), Mockito.any()); + doReturn(aggregateReportBody) + .when(mSpyDebugAggregateReportingJobHandler) + .createReportJsonPayload(Mockito.any(), Mockito.any(), Mockito.any()); + + doNothing() + .when(mMeasurementDao) + .markAggregateDebugReportDelivered(aggregateReport.getId()); + Assert.assertEquals( + AdServicesStatusUtils.STATUS_SUCCESS, + mSpyDebugAggregateReportingJobHandler.performReport( + aggregateReport.getId(), AggregateCryptoFixture.getKey())); + + verify(mMeasurementDao, times(1)).markAggregateDebugReportDelivered(any()); verify(mTransaction, times(2)).begin(); verify(mTransaction, times(2)).end(); } @@ -184,13 +235,16 @@ public class AggregateReportingJobHandlerTest { .when(mSpyAggregateReportingJobHandler) .createReportJsonPayload(Mockito.any(), Mockito.any(), Mockito.any()); - doNothing().when(mMeasurementDao).markAggregateReportDelivered(aggregateReport.getId()); + doNothing() + .when(mMeasurementDao) + .markAggregateReportStatus( + aggregateReport.getId(), AggregateReport.Status.DELIVERED); Assert.assertEquals( AdServicesStatusUtils.STATUS_SUCCESS, mSpyAggregateReportingJobHandler.performReport( aggregateReport.getId(), AggregateCryptoFixture.getKey())); - verify(mMeasurementDao, times(1)).markAggregateReportDelivered(any()); + verify(mMeasurementDao, times(1)).markAggregateReportStatus(any(), anyInt()); verify(mTransaction, times(2)).begin(); verify(mTransaction, times(2)).end(); } @@ -221,13 +275,16 @@ public class AggregateReportingJobHandlerTest { .when(mSpyAggregateReportingJobHandler) .createReportJsonPayload(Mockito.any(), Mockito.any(), Mockito.any()); - doNothing().when(mMeasurementDao).markAggregateReportDelivered(aggregateReport.getId()); + doNothing() + .when(mMeasurementDao) + .markAggregateReportStatus( + aggregateReport.getId(), AggregateReport.Status.DELIVERED); Assert.assertEquals( AdServicesStatusUtils.STATUS_SUCCESS, mSpyAggregateReportingJobHandler.performReport( aggregateReport.getId(), AggregateCryptoFixture.getKey())); - verify(mMeasurementDao, times(1)).markAggregateReportDelivered(any()); + verify(mMeasurementDao, times(1)).markAggregateReportStatus(any(), anyInt()); verify(mTransaction, times(2)).begin(); verify(mTransaction, times(2)).end(); } @@ -259,13 +316,16 @@ public class AggregateReportingJobHandlerTest { .when(mSpyAggregateReportingJobHandler) .createReportJsonPayload(Mockito.any(), Mockito.any(), Mockito.any()); - doNothing().when(mMeasurementDao).markAggregateReportDelivered(aggregateReport.getId()); + doNothing() + .when(mMeasurementDao) + .markAggregateReportStatus( + aggregateReport.getId(), AggregateReport.Status.DELIVERED); Assert.assertEquals( AdServicesStatusUtils.STATUS_SUCCESS, mSpyAggregateReportingJobHandler.performReport( aggregateReport.getId(), AggregateCryptoFixture.getKey())); - verify(mMeasurementDao, times(1)).markAggregateReportDelivered(any()); + verify(mMeasurementDao, times(1)).markAggregateReportStatus(any(), anyInt()); verify(mTransaction, times(2)).begin(); verify(mTransaction, times(2)).end(); } @@ -300,7 +360,7 @@ public class AggregateReportingJobHandlerTest { mSpyAggregateReportingJobHandler.performReport( aggregateReport.getId(), AggregateCryptoFixture.getKey())); - verify(mMeasurementDao, never()).markAggregateReportDelivered(any()); + verify(mMeasurementDao, never()).markAggregateReportStatus(any(), anyInt()); verify(mTransaction, times(1)).begin(); verify(mTransaction, times(1)).end(); } @@ -322,7 +382,7 @@ public class AggregateReportingJobHandlerTest { mSpyAggregateReportingJobHandler.performReport( aggregateReport.getId(), AggregateCryptoFixture.getKey())); - verify(mMeasurementDao, never()).markAggregateReportDelivered(any()); + verify(mMeasurementDao, never()).markAggregateReportStatus(any(), anyInt()); verify(mTransaction, times(1)).begin(); verify(mTransaction, times(1)).end(); } @@ -379,7 +439,7 @@ public class AggregateReportingJobHandlerTest { mSpyAggregateReportingJobHandler.performScheduledPendingReportsInWindow( 1000, 1100)); - verify(mMeasurementDao, times(2)).markAggregateReportDelivered(any()); + verify(mMeasurementDao, times(2)).markAggregateReportStatus(any(), anyInt()); verify(mTransaction, times(5)).begin(); verify(mTransaction, times(5)).end(); } @@ -424,7 +484,7 @@ public class AggregateReportingJobHandlerTest { mSpyAggregateReportingJobHandler.performScheduledPendingReportsInWindow( 1000, 1100)); - verify(mMeasurementDao, never()).markAggregateReportDelivered(any()); + verify(mMeasurementDao, never()).markAggregateReportStatus(any(), anyInt()); } @Test @@ -445,7 +505,7 @@ public class AggregateReportingJobHandlerTest { mSpyAggregateReportingJobHandler.performReport( aggregateReport.getId(), AggregateCryptoFixture.getKey())); - verify(mMeasurementDao, never()).markAggregateReportDelivered(any()); + verify(mMeasurementDao, never()).markAggregateReportStatus(any(), anyInt()); verify(mTransaction, times(1)).begin(); verify(mTransaction, times(1)).end(); } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateReportingJobServiceTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateReportingJobServiceTest.java index 9e17489156..d092a50f18 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateReportingJobServiceTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/AggregateReportingJobServiceTest.java @@ -19,6 +19,7 @@ package com.android.adservices.service.measurement.reporting; import static com.android.adservices.service.AdServicesConfig.MEASUREMENT_AGGREGATE_MAIN_REPORTING_JOB_ID; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -35,33 +36,31 @@ import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.content.Context; -import android.provider.DeviceConfig; +import com.android.adservices.data.enrollment.EnrollmentDao; import com.android.adservices.data.measurement.DatastoreManager; import com.android.adservices.data.measurement.DatastoreManagerFactory; +import com.android.adservices.service.AdServicesConfig; +import com.android.adservices.service.Flags; +import com.android.adservices.service.FlagsFactory; import com.android.compatibility.common.util.TestUtils; import com.android.dx.mockito.inline.extended.ExtendedMockito; -import com.android.modules.utils.testing.TestableDeviceConfig; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; import java.util.Optional; +import java.util.concurrent.TimeUnit; /** * Unit test for {@link AggregateReportingJobService */ public class AggregateReportingJobServiceTest { - // This rule is used for configuring P/H flags - @Rule - public final TestableDeviceConfig.TestableDeviceConfigRule mDeviceConfigRule = - new TestableDeviceConfig.TestableDeviceConfigRule(); - - private static final long WAIT_IN_MILLIS = 50L; + private static final long WAIT_IN_MILLIS = 1_000L; private DatastoreManager mMockDatastoreManager; private JobScheduler mMockJobScheduler; @@ -77,10 +76,11 @@ public class AggregateReportingJobServiceTest { @Test public void onStartJob_killSwitchOn() throws Exception { - enableKillSwitch(); - runWithMocks( () -> { + // Setup + enableKillSwitch(); + // Execute boolean result = mSpyService.onStartJob(mock(JobParameters.class)); @@ -97,10 +97,11 @@ public class AggregateReportingJobServiceTest { @Test public void onStartJob_killSwitchOff() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { + // Setup + disableKillSwitch(); + // Execute boolean result = mSpyService.onStartJob(mock(JobParameters.class)); @@ -117,11 +118,11 @@ public class AggregateReportingJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOn_dontSchedule() throws Exception { - enableKillSwitch(); - runWithMocks( () -> { // Setup + enableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -148,11 +149,11 @@ public class AggregateReportingJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOff_previouslyExecuted_dontForceSchedule_dontSchedule() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { // Setup + disableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -179,11 +180,11 @@ public class AggregateReportingJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOff_previouslyExecuted_forceSchedule_schedule() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { // Setup + disableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -210,11 +211,11 @@ public class AggregateReportingJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOff_previouslyNotExecuted_dontForceSchedule_schedule() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { // Setup + disableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -236,11 +237,34 @@ public class AggregateReportingJobServiceTest { }); } + @Test + public void testSchedule_jobInfoIsPersisted() throws Exception { + runWithMocks( + () -> { + // Setup + final JobScheduler jobScheduler = mock(JobScheduler.class); + final ArgumentCaptor<JobInfo> captor = ArgumentCaptor.forClass(JobInfo.class); + + // Execute + ExtendedMockito.doCallRealMethod() + .when(() -> AggregateReportingJobService.schedule(any(), any())); + AggregateReportingJobService.schedule(mock(Context.class), jobScheduler); + + // Validate + verify(jobScheduler, times(1)).schedule(captor.capture()); + assertNotNull(captor.getValue()); + assertTrue(captor.getValue().isPersisted()); + }); + } + private void runWithMocks(TestUtils.RunnableWithThrow execute) throws Exception { MockitoSession session = ExtendedMockito.mockitoSession() - .spyStatic(DatastoreManagerFactory.class) + .spyStatic(AdServicesConfig.class) .spyStatic(AggregateReportingJobService.class) + .spyStatic(DatastoreManagerFactory.class) + .spyStatic(EnrollmentDao.class) + .spyStatic(FlagsFactory.class) .strictness(Strictness.LENIENT) .startMocking(); try { @@ -252,6 +276,12 @@ public class AggregateReportingJobServiceTest { doNothing().when(mSpyService).jobFinished(any(), anyBoolean()); doReturn(mMockJobScheduler).when(mSpyService).getSystemService(JobScheduler.class); doReturn(Mockito.mock(Context.class)).when(mSpyService).getApplicationContext(); + ExtendedMockito.doReturn(TimeUnit.HOURS.toMillis(4)) + .when(AdServicesConfig::getMeasurementAggregateMainReportingJobPeriodMs); + ExtendedMockito.doReturn("http://example.com") + .when(AdServicesConfig::getMeasurementAggregateEncryptionKeyCoordinatorUrl); + ExtendedMockito.doReturn(mock(EnrollmentDao.class)) + .when(() -> EnrollmentDao.getInstance(any())); ExtendedMockito.doReturn(mMockDatastoreManager) .when(() -> DatastoreManagerFactory.getDatastoreManager(any())); ExtendedMockito.doNothing() @@ -273,10 +303,10 @@ public class AggregateReportingJobServiceTest { } private void toggleKillSwitch(boolean value) { - DeviceConfig.setProperty( - DeviceConfig.NAMESPACE_ADSERVICES, - "measurement_job_aggregate_reporting_kill_switch", - Boolean.toString(value), - /* makeDefault */ false); + Flags mockFlags = Mockito.mock(Flags.class); + ExtendedMockito.doReturn(mockFlags).when(FlagsFactory::getFlags); + ExtendedMockito.doReturn(value) + .when(mockFlags) + .getMeasurementJobAggregateReportingKillSwitch(); } } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/DebugReportingJobServiceTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/DebugReportingJobServiceTest.java new file mode 100644 index 0000000000..c18f231b17 --- /dev/null +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/DebugReportingJobServiceTest.java @@ -0,0 +1,287 @@ +/* + * 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.measurement.reporting; + +import static com.android.adservices.service.AdServicesConfig.MEASUREMENT_DEBUG_REPORT_JOB_ID; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.content.Context; + +import com.android.adservices.data.enrollment.EnrollmentDao; +import com.android.adservices.data.measurement.DatastoreManager; +import com.android.adservices.data.measurement.DatastoreManagerFactory; +import com.android.adservices.service.AdServicesConfig; +import com.android.adservices.service.Flags; +import com.android.adservices.service.FlagsFactory; +import com.android.compatibility.common.util.TestUtils; +import com.android.dx.mockito.inline.extended.ExtendedMockito; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +import java.util.Optional; + +/** + * Unit test for {@link DebugReportingJobService + */ +public class DebugReportingJobServiceTest { + + private static final long WAIT_IN_MILLIS = 1000L; + + private DatastoreManager mMockDatastoreManager; + private JobScheduler mMockJobScheduler; + + private DebugReportingJobService mSpyService; + + @Before + public void setUp() { + mSpyService = spy(new DebugReportingJobService()); + mMockDatastoreManager = mock(DatastoreManager.class); + mMockJobScheduler = mock(JobScheduler.class); + } + + @Test + public void onStartJob_killSwitchOn() throws Exception { + runWithMocks( + () -> { + // Setup + enableKillSwitch(); + + // Execute + boolean result = mSpyService.onStartJob(Mockito.mock(JobParameters.class)); + + // Validate + assertFalse(result); + // Allow background thread to execute + Thread.sleep(WAIT_IN_MILLIS); + verify(mMockDatastoreManager, never()).runInTransactionWithResult(any()); + verify(mSpyService, times(1)).jobFinished(any(), eq(false)); + verify(mMockJobScheduler, times(1)).cancel(eq(MEASUREMENT_DEBUG_REPORT_JOB_ID)); + }); + } + + @Test + public void onStartJob_killSwitchOff() throws Exception { + runWithMocks( + () -> { + // Setup + disableKillSwitch(); + + ExtendedMockito.doNothing() + .when( + () -> + DebugReportingJobService.scheduleIfNeeded( + any(), anyBoolean())); + + // Execute + boolean result = mSpyService.onStartJob(Mockito.mock(JobParameters.class)); + + // Validate + assertTrue(result); + // Allow background thread to execute + Thread.sleep(WAIT_IN_MILLIS); + verify(mMockDatastoreManager, times(2)).runInTransactionWithResult(any()); + verify(mSpyService, times(1)).jobFinished(any(), anyBoolean()); + verify(mMockJobScheduler, never()).cancel(eq(MEASUREMENT_DEBUG_REPORT_JOB_ID)); + }); + } + + @Test + public void scheduleIfNeeded_killSwitchOn_dontSchedule() throws Exception { + runWithMocks( + () -> { + // Setup + enableKillSwitch(); + + final Context mockContext = mock(Context.class); + doReturn(mMockJobScheduler) + .when(mockContext) + .getSystemService(JobScheduler.class); + final JobInfo mockJobInfo = mock(JobInfo.class); + doReturn(mockJobInfo) + .when(mMockJobScheduler) + .getPendingJob(eq(MEASUREMENT_DEBUG_REPORT_JOB_ID)); + + // Execute + DebugReportingJobService.scheduleIfNeeded( + mockContext, /* forceSchedule = */ false); + + // Validate + // Allow background thread to execute + Thread.sleep(WAIT_IN_MILLIS); + ExtendedMockito.verify( + () -> DebugReportingJobService.schedule(any(), any()), never()); + verify(mMockJobScheduler, never()) + .getPendingJob(eq(MEASUREMENT_DEBUG_REPORT_JOB_ID)); + }); + } + + @Test + public void scheduleIfNeeded_killSwitchOff_previouslyExecuted_dontForceSchedule_dontSchedule() + throws Exception { + runWithMocks( + () -> { + // Setup + disableKillSwitch(); + + final Context mockContext = mock(Context.class); + doReturn(mMockJobScheduler) + .when(mockContext) + .getSystemService(JobScheduler.class); + final JobInfo mockJobInfo = mock(JobInfo.class); + doReturn(mockJobInfo) + .when(mMockJobScheduler) + .getPendingJob(eq(MEASUREMENT_DEBUG_REPORT_JOB_ID)); + + // Execute + DebugReportingJobService.scheduleIfNeeded( + mockContext, /* forceSchedule = */ false); + + // Validate + // Allow background thread to execute + Thread.sleep(WAIT_IN_MILLIS); + ExtendedMockito.verify( + () -> DebugReportingJobService.schedule(any(), any()), never()); + verify(mMockJobScheduler, times(1)) + .getPendingJob(eq(MEASUREMENT_DEBUG_REPORT_JOB_ID)); + }); + } + + @Test + public void scheduleIfNeeded_killSwitchOff_previouslyExecuted_forceSchedule_schedule() + throws Exception { + runWithMocks( + () -> { + // Setup + disableKillSwitch(); + + final Context mockContext = mock(Context.class); + doReturn(mMockJobScheduler) + .when(mockContext) + .getSystemService(JobScheduler.class); + final JobInfo mockJobInfo = mock(JobInfo.class); + doReturn(mockJobInfo) + .when(mMockJobScheduler) + .getPendingJob(eq(MEASUREMENT_DEBUG_REPORT_JOB_ID)); + + // Execute + DebugReportingJobService.scheduleIfNeeded( + mockContext, /* forceSchedule = */ true); + + // Validate + // Allow background thread to execute + Thread.sleep(WAIT_IN_MILLIS); + ExtendedMockito.verify( + () -> DebugReportingJobService.schedule(any(), any()), times(1)); + verify(mMockJobScheduler, times(1)) + .getPendingJob(eq(MEASUREMENT_DEBUG_REPORT_JOB_ID)); + }); + } + + @Test + public void scheduleIfNeeded_killSwitchOff_previouslyNotExecuted_dontForceSchedule_schedule() + throws Exception { + runWithMocks( + () -> { + // Setup + disableKillSwitch(); + + final Context mockContext = mock(Context.class); + doReturn(mMockJobScheduler) + .when(mockContext) + .getSystemService(JobScheduler.class); + doReturn(/* noJobInfo = */ null) + .when(mMockJobScheduler) + .getPendingJob(eq(MEASUREMENT_DEBUG_REPORT_JOB_ID)); + + // Execute + DebugReportingJobService.scheduleIfNeeded(mockContext, false); + + // Validate + // Allow background thread to execute + Thread.sleep(WAIT_IN_MILLIS); + ExtendedMockito.verify( + () -> DebugReportingJobService.schedule(any(), any()), times(1)); + verify(mMockJobScheduler, times(1)) + .getPendingJob(eq(MEASUREMENT_DEBUG_REPORT_JOB_ID)); + }); + } + + private void runWithMocks(TestUtils.RunnableWithThrow execute) throws Exception { + MockitoSession session = + ExtendedMockito.mockitoSession() + .spyStatic(AdServicesConfig.class) + .spyStatic(DatastoreManagerFactory.class) + .spyStatic(EnrollmentDao.class) + .spyStatic(DebugReportingJobService.class) + .spyStatic(FlagsFactory.class) + .strictness(Strictness.LENIENT) + .startMocking(); + try { + // Setup mock everything in job + mMockDatastoreManager = mock(DatastoreManager.class); + doReturn(Optional.empty()) + .when(mMockDatastoreManager) + .runInTransactionWithResult(any()); + doNothing().when(mSpyService).jobFinished(any(), anyBoolean()); + doReturn(mMockJobScheduler).when(mSpyService).getSystemService(JobScheduler.class); + doReturn(Mockito.mock(Context.class)).when(mSpyService).getApplicationContext(); + ExtendedMockito.doReturn(mock(EnrollmentDao.class)) + .when(() -> EnrollmentDao.getInstance(any())); + ExtendedMockito.doReturn(mMockDatastoreManager) + .when(() -> DatastoreManagerFactory.getDatastoreManager(any())); + ExtendedMockito.doNothing().when(() -> DebugReportingJobService.schedule(any(), any())); + + // Execute + execute.run(); + } finally { + session.finishMocking(); + } + } + + private void enableKillSwitch() { + toggleKillSwitch(true); + } + + private void disableKillSwitch() { + toggleKillSwitch(false); + } + + private void toggleKillSwitch(boolean value) { + Flags mockFlags = Mockito.mock(Flags.class); + ExtendedMockito.doReturn(mockFlags).when(FlagsFactory::getFlags); + ExtendedMockito.doReturn(value).when(mockFlags).getMeasurementJobDebugReportingKillSwitch(); + } +} diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventFallbackReportingJobServiceTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventFallbackReportingJobServiceTest.java index 0fb5730cc3..84f2bbfa74 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventFallbackReportingJobServiceTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventFallbackReportingJobServiceTest.java @@ -19,6 +19,7 @@ package com.android.adservices.service.measurement.reporting; import static com.android.adservices.service.AdServicesConfig.MEASUREMENT_EVENT_FALLBACK_REPORTING_JOB_ID; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -35,33 +36,31 @@ import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.content.Context; -import android.provider.DeviceConfig; +import com.android.adservices.data.enrollment.EnrollmentDao; import com.android.adservices.data.measurement.DatastoreManager; import com.android.adservices.data.measurement.DatastoreManagerFactory; +import com.android.adservices.service.AdServicesConfig; +import com.android.adservices.service.Flags; +import com.android.adservices.service.FlagsFactory; import com.android.compatibility.common.util.TestUtils; import com.android.dx.mockito.inline.extended.ExtendedMockito; -import com.android.modules.utils.testing.TestableDeviceConfig; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; import java.util.Optional; +import java.util.concurrent.TimeUnit; /** * Unit test for {@link EventFallbackReportingJobService */ public class EventFallbackReportingJobServiceTest { - // This rule is used for configuring P/H flags - @Rule - public final TestableDeviceConfig.TestableDeviceConfigRule mDeviceConfigRule = - new TestableDeviceConfig.TestableDeviceConfigRule(); - - private static final long WAIT_IN_MILLIS = 50L; + private static final long WAIT_IN_MILLIS = 1_000L; private DatastoreManager mMockDatastoreManager; private JobScheduler mMockJobScheduler; @@ -77,10 +76,11 @@ public class EventFallbackReportingJobServiceTest { @Test public void onStartJob_killSwitchOn() throws Exception { - enableKillSwitch(); - runWithMocks( () -> { + // Setup + enableKillSwitch(); + // Execute boolean result = mSpyService.onStartJob(Mockito.mock(JobParameters.class)); @@ -97,11 +97,11 @@ public class EventFallbackReportingJobServiceTest { @Test public void onStartJob_killSwitchOff() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { // Setup + disableKillSwitch(); + ExtendedMockito.doNothing() .when( () -> @@ -124,11 +124,11 @@ public class EventFallbackReportingJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOn_dontSchedule() throws Exception { - enableKillSwitch(); - runWithMocks( () -> { // Setup + enableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -155,11 +155,11 @@ public class EventFallbackReportingJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOff_previouslyExecuted_dontForceSchedule_dontSchedule() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { // Setup + disableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -186,11 +186,11 @@ public class EventFallbackReportingJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOff_previouslyExecuted_forceSchedule_schedule() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { // Setup + disableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -218,11 +218,11 @@ public class EventFallbackReportingJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOff_previouslyNotExecuted_dontForceSchedule_schedule() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { // Setup + disableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -245,11 +245,34 @@ public class EventFallbackReportingJobServiceTest { }); } + @Test + public void testSchedule_jobInfoIsPersisted() throws Exception { + runWithMocks( + () -> { + // Setup + final JobScheduler jobScheduler = mock(JobScheduler.class); + final ArgumentCaptor<JobInfo> captor = ArgumentCaptor.forClass(JobInfo.class); + + // Execute + ExtendedMockito.doCallRealMethod() + .when(() -> EventFallbackReportingJobService.schedule(any(), any())); + EventFallbackReportingJobService.schedule(mock(Context.class), jobScheduler); + + // Validate + verify(jobScheduler, times(1)).schedule(captor.capture()); + assertNotNull(captor.getValue()); + assertTrue(captor.getValue().isPersisted()); + }); + } + private void runWithMocks(TestUtils.RunnableWithThrow execute) throws Exception { MockitoSession session = ExtendedMockito.mockitoSession() + .spyStatic(AdServicesConfig.class) .spyStatic(DatastoreManagerFactory.class) + .spyStatic(EnrollmentDao.class) .spyStatic(EventFallbackReportingJobService.class) + .spyStatic(FlagsFactory.class) .strictness(Strictness.LENIENT) .startMocking(); try { @@ -261,6 +284,12 @@ public class EventFallbackReportingJobServiceTest { doNothing().when(mSpyService).jobFinished(any(), anyBoolean()); doReturn(mMockJobScheduler).when(mSpyService).getSystemService(JobScheduler.class); doReturn(Mockito.mock(Context.class)).when(mSpyService).getApplicationContext(); + ExtendedMockito.doReturn(TimeUnit.HOURS.toMillis(4)) + .when(AdServicesConfig::getMeasurementEventMainReportingJobPeriodMs); + ExtendedMockito.doReturn(TimeUnit.HOURS.toMillis(24)) + .when(AdServicesConfig::getMeasurementEventFallbackReportingJobPeriodMs); + ExtendedMockito.doReturn(mock(EnrollmentDao.class)) + .when(() -> EnrollmentDao.getInstance(any())); ExtendedMockito.doReturn(mMockDatastoreManager) .when(() -> DatastoreManagerFactory.getDatastoreManager(any())); ExtendedMockito.doNothing() @@ -282,10 +311,10 @@ public class EventFallbackReportingJobServiceTest { } private void toggleKillSwitch(boolean value) { - DeviceConfig.setProperty( - DeviceConfig.NAMESPACE_ADSERVICES, - "measurement_job_event_fallback_reporting_kill_switch", - Boolean.toString(value), - /* makeDefault */ false); + Flags mockFlags = Mockito.mock(Flags.class); + ExtendedMockito.doReturn(mockFlags).when(FlagsFactory::getFlags); + ExtendedMockito.doReturn(value) + .when(mockFlags) + .getMeasurementJobEventFallbackReportingKillSwitch(); } } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventReportPayloadTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventReportPayloadTest.java index cd590525d1..c12ee6ea6b 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventReportPayloadTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventReportPayloadTest.java @@ -19,6 +19,8 @@ package com.android.adservices.service.measurement.reporting; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import com.android.adservices.service.measurement.util.UnsignedLong; + import org.json.JSONException; import org.json.JSONObject; import org.junit.Test; @@ -26,94 +28,72 @@ import org.junit.Test; public class EventReportPayloadTest { private static final String ATTRIBUTION_DESTINATION = "https://toasters.example"; - private static final String SOURCE_EVENT_ID = "12345"; - private static final String TRIGGER_DATA = "2"; + private static final UnsignedLong SOURCE_EVENT_ID = new UnsignedLong(12345L); + private static final UnsignedLong TRIGGER_DATA = new UnsignedLong(2L); private static final String REPORT_ID = "678"; private static final String SOURCE_TYPE = "event"; private static final double RANDOMIZED_TRIGGER_RATE = 0.0024; - private static final Long SOURCE_DEBUG_KEY = 3894783L; - private static final Long TRIGGER_DEBUG_KEY = 2387222L; + private static final UnsignedLong SOURCE_DEBUG_KEY = new UnsignedLong(3894783L); + private static final UnsignedLong TRIGGER_DEBUG_KEY = new UnsignedLong(2387222L); - private EventReportPayload createEventReportPayloadExample1() { + private static EventReportPayload createEventReportPayload(UnsignedLong triggerData, + UnsignedLong sourceDebugKey, UnsignedLong triggerDebugKey) { return new EventReportPayload.Builder() .setAttributionDestination(ATTRIBUTION_DESTINATION) .setSourceEventId(SOURCE_EVENT_ID) - .setTriggerData(TRIGGER_DATA) + .setTriggerData(triggerData) .setReportId(REPORT_ID) .setSourceType(SOURCE_TYPE) .setRandomizedTriggerRate(RANDOMIZED_TRIGGER_RATE) - .setSourceDebugKey(SOURCE_DEBUG_KEY) - .setTriggerDebugKey(TRIGGER_DEBUG_KEY) - .build(); - } - - private EventReportPayload createEventReportPayloadWithNullDebugKeys() { - return new EventReportPayload.Builder() - .setAttributionDestination(ATTRIBUTION_DESTINATION) - .setSourceEventId(SOURCE_EVENT_ID) - .setTriggerData(TRIGGER_DATA) - .setReportId(REPORT_ID) - .setSourceType(SOURCE_TYPE) - .setRandomizedTriggerRate(RANDOMIZED_TRIGGER_RATE) - .setSourceDebugKey(null) - .setTriggerDebugKey(null) - .build(); - } - - private EventReportPayload createEventReportPayloadWithSingleTriggerDebugKeys() { - return new EventReportPayload.Builder() - .setAttributionDestination(ATTRIBUTION_DESTINATION) - .setSourceEventId(SOURCE_EVENT_ID) - .setTriggerData(TRIGGER_DATA) - .setReportId(REPORT_ID) - .setSourceType(SOURCE_TYPE) - .setRandomizedTriggerRate(RANDOMIZED_TRIGGER_RATE) - .setTriggerDebugKey(TRIGGER_DEBUG_KEY) - .build(); - } - - private EventReportPayload createEventReportPayloadWithSingleSourceDebugKeys() { - return new EventReportPayload.Builder() - .setAttributionDestination(ATTRIBUTION_DESTINATION) - .setSourceEventId(SOURCE_EVENT_ID) - .setTriggerData(TRIGGER_DATA) - .setReportId(REPORT_ID) - .setSourceType(SOURCE_TYPE) - .setRandomizedTriggerRate(RANDOMIZED_TRIGGER_RATE) - .setSourceDebugKey(SOURCE_DEBUG_KEY) + .setSourceDebugKey(sourceDebugKey) + .setTriggerDebugKey(triggerDebugKey) .build(); } @Test public void testEventPayloadJsonSerialization() throws JSONException { - EventReportPayload eventReport = createEventReportPayloadExample1(); + EventReportPayload eventReport = + createEventReportPayload(TRIGGER_DATA, SOURCE_DEBUG_KEY, TRIGGER_DEBUG_KEY); JSONObject eventPayloadReportJson = eventReport.toJson(); assertEquals(ATTRIBUTION_DESTINATION, eventPayloadReportJson.get("attribution_destination")); - assertEquals(SOURCE_EVENT_ID, eventPayloadReportJson.get("source_event_id")); - assertEquals(TRIGGER_DATA, eventPayloadReportJson.get("trigger_data")); + assertEquals(SOURCE_EVENT_ID.toString(), eventPayloadReportJson.get("source_event_id")); + assertEquals(TRIGGER_DATA.toString(), eventPayloadReportJson.get("trigger_data")); assertEquals(REPORT_ID, eventPayloadReportJson.get("report_id")); assertEquals(SOURCE_TYPE, eventPayloadReportJson.get("source_type")); assertEquals(RANDOMIZED_TRIGGER_RATE, eventPayloadReportJson.get("randomized_trigger_rate")); + assertEquals(SOURCE_DEBUG_KEY.toString(), eventPayloadReportJson.get("source_debug_key")); + assertEquals(TRIGGER_DEBUG_KEY.toString(), eventPayloadReportJson.get("trigger_debug_key")); + } + + @Test + public void testEventPayloadJsonSerializationWithNullDebugKeys() throws JSONException { + EventReportPayload eventReport = createEventReportPayload(TRIGGER_DATA, null, null); + JSONObject eventPayloadReportJson = eventReport.toJson(); + assertEquals( - Long.toUnsignedString(SOURCE_DEBUG_KEY), - eventPayloadReportJson.get("source_debug_key")); + ATTRIBUTION_DESTINATION, eventPayloadReportJson.get("attribution_destination")); + assertEquals(SOURCE_EVENT_ID.toString(), eventPayloadReportJson.get("source_event_id")); + assertEquals(TRIGGER_DATA.toString(), eventPayloadReportJson.get("trigger_data")); + assertEquals(REPORT_ID, eventPayloadReportJson.get("report_id")); + assertEquals(SOURCE_TYPE, eventPayloadReportJson.get("source_type")); assertEquals( - Long.toUnsignedString(TRIGGER_DEBUG_KEY), - eventPayloadReportJson.get("trigger_debug_key")); + RANDOMIZED_TRIGGER_RATE, eventPayloadReportJson.get("randomized_trigger_rate")); + assertNull(eventPayloadReportJson.opt("source_debug_key")); + assertNull(eventPayloadReportJson.opt("trigger_debug_key")); } @Test - public void testEventPayloadJsonSerializationWithNullDebugKeys() throws JSONException { - EventReportPayload eventReport = createEventReportPayloadWithNullDebugKeys(); + public void testEventPayloadJsonSerializationWithNullTriggerData() throws JSONException { + EventReportPayload eventReport = createEventReportPayload(null, null, null); JSONObject eventPayloadReportJson = eventReport.toJson(); assertEquals( ATTRIBUTION_DESTINATION, eventPayloadReportJson.get("attribution_destination")); - assertEquals(SOURCE_EVENT_ID, eventPayloadReportJson.get("source_event_id")); - assertEquals(TRIGGER_DATA, eventPayloadReportJson.get("trigger_data")); + assertEquals(SOURCE_EVENT_ID.toString(), eventPayloadReportJson.get("source_event_id")); + assertEquals(new UnsignedLong(0L).toString(), eventPayloadReportJson.get("trigger_data")); assertEquals(REPORT_ID, eventPayloadReportJson.get("report_id")); assertEquals(SOURCE_TYPE, eventPayloadReportJson.get("source_type")); assertEquals( @@ -124,39 +104,66 @@ public class EventReportPayloadTest { @Test public void testEventPayloadJsonSerializationWithSingleTriggerDebugKeys() throws JSONException { - EventReportPayload eventReport = createEventReportPayloadWithSingleTriggerDebugKeys(); + EventReportPayload eventReport = + createEventReportPayload(TRIGGER_DATA, null, TRIGGER_DEBUG_KEY); JSONObject eventPayloadReportJson = eventReport.toJson(); assertEquals( ATTRIBUTION_DESTINATION, eventPayloadReportJson.get("attribution_destination")); - assertEquals(SOURCE_EVENT_ID, eventPayloadReportJson.get("source_event_id")); - assertEquals(TRIGGER_DATA, eventPayloadReportJson.get("trigger_data")); + assertEquals(SOURCE_EVENT_ID.toString(), eventPayloadReportJson.get("source_event_id")); + assertEquals(TRIGGER_DATA.toString(), eventPayloadReportJson.get("trigger_data")); assertEquals(REPORT_ID, eventPayloadReportJson.get("report_id")); assertEquals(SOURCE_TYPE, eventPayloadReportJson.get("source_type")); assertEquals( RANDOMIZED_TRIGGER_RATE, eventPayloadReportJson.get("randomized_trigger_rate")); assertNull(eventPayloadReportJson.opt("source_debug_key")); + assertEquals(TRIGGER_DEBUG_KEY.toString(), eventPayloadReportJson.get("trigger_debug_key")); + } + + @Test + public void testEventPayloadJsonSerialization_debugKeysSourceEventIdAndTriggerDataUse64thBit() + throws JSONException { + String unsigned64BitIntString = "18446744073709551615"; + UnsignedLong signed64BitInt = new UnsignedLong(-1L); + EventReportPayload eventReport = new EventReportPayload.Builder() + .setAttributionDestination(ATTRIBUTION_DESTINATION) + .setSourceEventId(signed64BitInt) + .setTriggerData(signed64BitInt) + .setReportId(REPORT_ID) + .setSourceType(SOURCE_TYPE) + .setRandomizedTriggerRate(RANDOMIZED_TRIGGER_RATE) + .setSourceDebugKey(signed64BitInt) + .setTriggerDebugKey(signed64BitInt) + .build(); + JSONObject eventPayloadReportJson = eventReport.toJson(); + assertEquals( - Long.toUnsignedString(TRIGGER_DEBUG_KEY), - eventPayloadReportJson.get("trigger_debug_key")); + ATTRIBUTION_DESTINATION, eventPayloadReportJson.get("attribution_destination")); + assertEquals(unsigned64BitIntString, eventPayloadReportJson.get("source_event_id")); + assertEquals(unsigned64BitIntString, eventPayloadReportJson.get("trigger_data")); + assertEquals(REPORT_ID, eventPayloadReportJson.get("report_id")); + assertEquals(SOURCE_TYPE, eventPayloadReportJson.get("source_type")); + assertEquals( + RANDOMIZED_TRIGGER_RATE, eventPayloadReportJson.get("randomized_trigger_rate")); + assertEquals(unsigned64BitIntString, eventPayloadReportJson.opt("source_debug_key")); + assertEquals(unsigned64BitIntString, eventPayloadReportJson.get("trigger_debug_key")); } @Test public void testEventPayloadJsonSerializationWithSingleSourceDebugKeys() throws JSONException { - EventReportPayload eventReport = createEventReportPayloadWithSingleSourceDebugKeys(); + EventReportPayload eventReport = + createEventReportPayload(TRIGGER_DATA, SOURCE_DEBUG_KEY, null); JSONObject eventPayloadReportJson = eventReport.toJson(); assertEquals( ATTRIBUTION_DESTINATION, eventPayloadReportJson.get("attribution_destination")); - assertEquals(SOURCE_EVENT_ID, eventPayloadReportJson.get("source_event_id")); - assertEquals(TRIGGER_DATA, eventPayloadReportJson.get("trigger_data")); + assertEquals(SOURCE_EVENT_ID.toString(), eventPayloadReportJson.get("source_event_id")); + assertEquals(TRIGGER_DATA.toString(), eventPayloadReportJson.get("trigger_data")); assertEquals(REPORT_ID, eventPayloadReportJson.get("report_id")); assertEquals(SOURCE_TYPE, eventPayloadReportJson.get("source_type")); assertEquals( RANDOMIZED_TRIGGER_RATE, eventPayloadReportJson.get("randomized_trigger_rate")); assertNull(eventPayloadReportJson.opt("trigger_debug_key")); - assertEquals( - Long.toUnsignedString(SOURCE_DEBUG_KEY), - eventPayloadReportJson.get("source_debug_key")); + assertEquals(SOURCE_DEBUG_KEY.toString(), eventPayloadReportJson.get("source_debug_key")); } } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventReportSenderTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventReportSenderTest.java index 6d96c9a95c..0fd9d1828d 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventReportSenderTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventReportSenderTest.java @@ -16,10 +16,14 @@ package com.android.adservices.service.measurement.reporting; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertEquals; import android.net.Uri; +import com.android.adservices.service.measurement.util.UnsignedLong; + import org.json.JSONException; import org.json.JSONObject; import org.junit.Test; @@ -33,8 +37,8 @@ import java.net.HttpURLConnection; public class EventReportSenderTest { private static final String ATTRIBUTION_DESTINATION = "https://toasters.example"; - private static final String SOURCE_EVENT_ID = "12345"; - private static final String TRIGGER_DATA = "2"; + private static final UnsignedLong SOURCE_EVENT_ID = new UnsignedLong(12345L); + private static final UnsignedLong TRIGGER_DATA = new UnsignedLong(2L); private static final String REPORT_ID = "678"; private static final String SOURCE_TYPE = "event"; private static final double RANDOMIZED_TRIGGER_RATE = 0.0024; @@ -68,7 +72,7 @@ public class EventReportSenderTest { Uri reportingOrigin = Uri.parse("https://ad-tech.example"); JSONObject eventReportJson = createEventReportPayloadExample1().toJson(); - EventReportSender eventReportSender = new EventReportSender(); + EventReportSender eventReportSender = new EventReportSender(false); EventReportSender spyEventReportSender = Mockito.spy(eventReportSender); Mockito.doReturn(httpUrlConnection).when(spyEventReportSender) @@ -80,4 +84,12 @@ public class EventReportSenderTest { assertEquals(outputStream.toString(), eventReportJson.toString()); assertEquals(responseCode, 200); } + + @Test + public void testDebugReportUriPath() { + assertThat(new EventReportSender(false).getReportUriPath()) + .isEqualTo(EventReportSender.EVENT_ATTRIBUTION_REPORT_URI_PATH); + assertThat(new EventReportSender(true).getReportUriPath()) + .isEqualTo(EventReportSender.DEBUG_EVENT_ATTRIBUTION_REPORT_URI_PATH); + } } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventReportingJobHandlerIntegrationTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventReportingJobHandlerIntegrationTest.java index 76075c2913..18d25f0fe0 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventReportingJobHandlerIntegrationTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventReportingJobHandlerIntegrationTest.java @@ -16,11 +16,12 @@ package com.android.adservices.service.measurement.reporting; +import com.android.adservices.data.DbTestUtil; import com.android.adservices.data.enrollment.EnrollmentDao; import com.android.adservices.data.measurement.AbstractDbIntegrationTest; import com.android.adservices.data.measurement.DatastoreManager; -import com.android.adservices.data.measurement.DatastoreManagerFactory; import com.android.adservices.data.measurement.DbState; +import com.android.adservices.data.measurement.SQLDatastoreManager; import com.android.adservices.service.enrollment.EnrollmentData; import org.json.JSONException; @@ -39,6 +40,7 @@ import java.util.Objects; /** Integration tests for {@link EventReportingJobHandler} */ @RunWith(Parameterized.class) public class EventReportingJobHandlerIntegrationTest extends AbstractDbIntegrationTest { + private static final EnrollmentData ENROLLMENT = new EnrollmentData.Builder() .setAttributionReportingUrl(List.of("https://ad-tech.com")) .build(); @@ -74,7 +76,8 @@ public class EventReportingJobHandlerIntegrationTest extends AbstractDbIntegrati Mockito.doReturn(isEnrolled ? ENROLLMENT : null) .when(mEnrollmentDao).getEnrollmentData(Mockito.any()); - DatastoreManager datastoreManager = DatastoreManagerFactory.getDatastoreManager(sContext); + DatastoreManager datastoreManager = + new SQLDatastoreManager(DbTestUtil.getDbHelperForTest()); EventReportingJobHandler spyReportingService = Mockito.spy(new EventReportingJobHandler(mEnrollmentDao, datastoreManager)); try { diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventReportingJobHandlerTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventReportingJobHandlerTest.java index 8f8fecdaa4..e4a3bc95bf 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventReportingJobHandlerTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventReportingJobHandlerTest.java @@ -17,6 +17,7 @@ package com.android.adservices.service.measurement.reporting; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; @@ -36,6 +37,8 @@ import com.android.adservices.data.measurement.IMeasurementDao; import com.android.adservices.data.measurement.ITransaction; import com.android.adservices.service.enrollment.EnrollmentData; import com.android.adservices.service.measurement.EventReport; +import com.android.adservices.service.measurement.aggregation.AggregateReport; +import com.android.adservices.service.measurement.util.UnsignedLong; import org.json.JSONException; import org.json.JSONObject; @@ -54,8 +57,8 @@ import java.util.List; /** Unit test for {@link EventReportingJobHandler} */ @RunWith(MockitoJUnitRunner.class) public class EventReportingJobHandlerTest { - private static final Long SOURCE_DEBUG_KEY = 237865L; - private static final Long TRIGGER_DEBUG_KEY = 928762L; + private static final UnsignedLong SOURCE_DEBUG_KEY = new UnsignedLong(237865L); + private static final UnsignedLong TRIGGER_DEBUG_KEY = new UnsignedLong(928762L); private static final EnrollmentData ENROLLMENT = new EnrollmentData.Builder() .setAttributionReportingUrl(List.of("https://ad-tech.com")) @@ -72,6 +75,7 @@ public class EventReportingJobHandlerTest { EventReportingJobHandler mEventReportingJobHandler; EventReportingJobHandler mSpyEventReportingJobHandler; + EventReportingJobHandler mSpyDebugEventReportingJobHandler; class FakeDatasoreManager extends DatastoreManager { @Override @@ -91,6 +95,10 @@ public class EventReportingJobHandlerTest { when(mEnrollmentDao.getEnrollmentData(any())).thenReturn(ENROLLMENT); mEventReportingJobHandler = new EventReportingJobHandler(mEnrollmentDao, mDatastoreManager); mSpyEventReportingJobHandler = Mockito.spy(mEventReportingJobHandler); + mSpyDebugEventReportingJobHandler = + Mockito.spy( + new EventReportingJobHandler(mEnrollmentDao, mDatastoreManager) + .setDebugReport(true)); } @Test @@ -99,12 +107,17 @@ public class EventReportingJobHandlerTest { EventReport eventReport = new EventReport.Builder() .setId("eventReportId") + .setSourceEventId(new UnsignedLong(1234L)) .setStatus(EventReport.Status.PENDING) .setSourceDebugKey(SOURCE_DEBUG_KEY) .setTriggerDebugKey(TRIGGER_DEBUG_KEY) .build(); JSONObject eventReportPayload = - new EventReportPayload.Builder().setReportId(eventReport.getId()).build().toJson(); + new EventReportPayload.Builder() + .setReportId(eventReport.getId()) + .setSourceEventId(eventReport.getSourceEventId()) + .build() + .toJson(); when(mMeasurementDao.getEventReport(eventReport.getId())).thenReturn(eventReport); doReturn(HttpURLConnection.HTTP_OK) @@ -114,13 +127,53 @@ public class EventReportingJobHandlerTest { .when(mSpyEventReportingJobHandler) .createReportJsonPayload(Mockito.any()); - doNothing().when(mMeasurementDao).markAggregateReportDelivered(eventReport.getId()); + doNothing() + .when(mMeasurementDao) + .markAggregateReportStatus(eventReport.getId(), AggregateReport.Status.DELIVERED); Assert.assertEquals( AdServicesStatusUtils.STATUS_SUCCESS, mSpyEventReportingJobHandler.performReport(eventReport.getId())); - verify(mMeasurementDao, times(1)).markEventReportDelivered(any()); + verify(mMeasurementDao, times(1)).markEventReportStatus(any(), anyInt()); + verify(mTransaction, times(2)).begin(); + verify(mTransaction, times(2)).end(); + } + + @Test + public void testSendReportForPendingDebugReportSuccess() + throws DatastoreException, IOException, JSONException { + EventReport eventReport = + new EventReport.Builder() + .setId("eventReportId") + .setSourceEventId(new UnsignedLong(1234L)) + .setStatus(EventReport.Status.PENDING) + .setDebugReportStatus(EventReport.DebugReportStatus.PENDING) + .setSourceDebugKey(SOURCE_DEBUG_KEY) + .setTriggerDebugKey(TRIGGER_DEBUG_KEY) + .build(); + JSONObject eventReportPayload = + new EventReportPayload.Builder() + .setReportId(eventReport.getId()) + .setSourceEventId(eventReport.getSourceEventId()) + .build() + .toJson(); + + when(mMeasurementDao.getEventReport(eventReport.getId())).thenReturn(eventReport); + doReturn(HttpURLConnection.HTTP_OK) + .when(mSpyDebugEventReportingJobHandler) + .makeHttpPostRequest(Mockito.any(), Mockito.any()); + doReturn(eventReportPayload) + .when(mSpyDebugEventReportingJobHandler) + .createReportJsonPayload(Mockito.any()); + + doNothing().when(mMeasurementDao).markEventDebugReportDelivered(eventReport.getId()); + + Assert.assertEquals( + AdServicesStatusUtils.STATUS_SUCCESS, + mSpyDebugEventReportingJobHandler.performReport(eventReport.getId())); + + verify(mMeasurementDao, times(1)).markEventDebugReportDelivered(any()); verify(mTransaction, times(2)).begin(); verify(mTransaction, times(2)).end(); } @@ -131,11 +184,16 @@ public class EventReportingJobHandlerTest { EventReport eventReport = new EventReport.Builder() .setId("eventReportId") + .setSourceEventId(new UnsignedLong(1234L)) .setStatus(EventReport.Status.PENDING) .setTriggerDebugKey(TRIGGER_DEBUG_KEY) .build(); JSONObject eventReportPayload = - new EventReportPayload.Builder().setReportId(eventReport.getId()).build().toJson(); + new EventReportPayload.Builder() + .setReportId(eventReport.getId()) + .setSourceEventId(eventReport.getSourceEventId()) + .build() + .toJson(); when(mMeasurementDao.getEventReport(eventReport.getId())).thenReturn(eventReport); doReturn(HttpURLConnection.HTTP_OK) @@ -145,13 +203,15 @@ public class EventReportingJobHandlerTest { .when(mSpyEventReportingJobHandler) .createReportJsonPayload(Mockito.any()); - doNothing().when(mMeasurementDao).markAggregateReportDelivered(eventReport.getId()); + doNothing() + .when(mMeasurementDao) + .markAggregateReportStatus(eventReport.getId(), AggregateReport.Status.DELIVERED); Assert.assertEquals( AdServicesStatusUtils.STATUS_SUCCESS, mSpyEventReportingJobHandler.performReport(eventReport.getId())); - verify(mMeasurementDao, times(1)).markEventReportDelivered(any()); + verify(mMeasurementDao, times(1)).markEventReportStatus(any(), anyInt()); verify(mTransaction, times(2)).begin(); verify(mTransaction, times(2)).end(); } @@ -162,11 +222,16 @@ public class EventReportingJobHandlerTest { EventReport eventReport = new EventReport.Builder() .setId("eventReportId") + .setSourceEventId(new UnsignedLong(1234L)) .setStatus(EventReport.Status.PENDING) .setSourceDebugKey(SOURCE_DEBUG_KEY) .build(); JSONObject eventReportPayload = - new EventReportPayload.Builder().setReportId(eventReport.getId()).build().toJson(); + new EventReportPayload.Builder() + .setReportId(eventReport.getId()) + .setSourceEventId(eventReport.getSourceEventId()) + .build() + .toJson(); when(mMeasurementDao.getEventReport(eventReport.getId())).thenReturn(eventReport); doReturn(HttpURLConnection.HTTP_OK) @@ -176,13 +241,15 @@ public class EventReportingJobHandlerTest { .when(mSpyEventReportingJobHandler) .createReportJsonPayload(Mockito.any()); - doNothing().when(mMeasurementDao).markAggregateReportDelivered(eventReport.getId()); + doNothing() + .when(mMeasurementDao) + .markAggregateReportStatus(eventReport.getId(), AggregateReport.Status.DELIVERED); Assert.assertEquals( AdServicesStatusUtils.STATUS_SUCCESS, mSpyEventReportingJobHandler.performReport(eventReport.getId())); - verify(mMeasurementDao, times(1)).markEventReportDelivered(any()); + verify(mMeasurementDao, times(1)).markEventReportStatus(any(), anyInt()); verify(mTransaction, times(2)).begin(); verify(mTransaction, times(2)).end(); } @@ -193,12 +260,17 @@ public class EventReportingJobHandlerTest { EventReport eventReport = new EventReport.Builder() .setId("eventReportId") + .setSourceEventId(new UnsignedLong(1234L)) .setStatus(EventReport.Status.PENDING) .setSourceDebugKey(null) .setTriggerDebugKey(null) .build(); JSONObject eventReportPayload = - new EventReportPayload.Builder().setReportId(eventReport.getId()).build().toJson(); + new EventReportPayload.Builder() + .setReportId(eventReport.getId()) + .setSourceEventId(eventReport.getSourceEventId()) + .build() + .toJson(); when(mMeasurementDao.getEventReport(eventReport.getId())).thenReturn(eventReport); doReturn(HttpURLConnection.HTTP_OK) @@ -208,12 +280,14 @@ public class EventReportingJobHandlerTest { .when(mSpyEventReportingJobHandler) .createReportJsonPayload(Mockito.any()); - doNothing().when(mMeasurementDao).markAggregateReportDelivered(eventReport.getId()); + doNothing() + .when(mMeasurementDao) + .markAggregateReportStatus(eventReport.getId(), AggregateReport.Status.DELIVERED); Assert.assertEquals( AdServicesStatusUtils.STATUS_SUCCESS, mSpyEventReportingJobHandler.performReport(eventReport.getId())); - verify(mMeasurementDao, times(1)).markEventReportDelivered(any()); + verify(mMeasurementDao, times(1)).markEventReportStatus(any(), anyInt()); verify(mTransaction, times(2)).begin(); verify(mTransaction, times(2)).end(); } @@ -224,10 +298,15 @@ public class EventReportingJobHandlerTest { EventReport eventReport = new EventReport.Builder() .setId("eventReportId") + .setSourceEventId(new UnsignedLong(1234L)) .setStatus(EventReport.Status.PENDING) .build(); JSONObject eventReportPayload = - new EventReportPayload.Builder().setReportId(eventReport.getId()).build().toJson(); + new EventReportPayload.Builder() + .setReportId(eventReport.getId()) + .setSourceEventId(eventReport.getSourceEventId()) + .build() + .toJson(); when(mMeasurementDao.getEventReport(eventReport.getId())).thenReturn(eventReport); doReturn(HttpURLConnection.HTTP_BAD_REQUEST) @@ -241,7 +320,7 @@ public class EventReportingJobHandlerTest { AdServicesStatusUtils.STATUS_IO_ERROR, mSpyEventReportingJobHandler.performReport(eventReport.getId())); - verify(mMeasurementDao, never()).markEventReportDelivered(any()); + verify(mMeasurementDao, never()).markEventReportStatus(any(), anyInt()); verify(mTransaction, times(1)).begin(); verify(mTransaction, times(1)).end(); } @@ -259,7 +338,7 @@ public class EventReportingJobHandlerTest { AdServicesStatusUtils.STATUS_INVALID_ARGUMENT, mSpyEventReportingJobHandler.performReport(eventReport.getId())); - verify(mMeasurementDao, never()).markEventReportDelivered(any()); + verify(mMeasurementDao, never()).markEventReportStatus(any(), anyInt()); verify(mTransaction, times(1)).begin(); verify(mTransaction, times(1)).end(); } @@ -270,19 +349,29 @@ public class EventReportingJobHandlerTest { EventReport eventReport1 = new EventReport.Builder() .setId("eventReport1") + .setSourceEventId(new UnsignedLong(1234L)) .setStatus(EventReport.Status.PENDING) .setReportTime(1000L) .build(); JSONObject eventReportPayload1 = - new EventReportPayload.Builder().setReportId(eventReport1.getId()).build().toJson(); + new EventReportPayload.Builder() + .setReportId(eventReport1.getId()) + .setSourceEventId(eventReport1.getSourceEventId()) + .build() + .toJson(); EventReport eventReport2 = new EventReport.Builder() .setId("eventReport2") + .setSourceEventId(new UnsignedLong(12345L)) .setStatus(EventReport.Status.PENDING) .setReportTime(1100L) .build(); JSONObject eventReportPayload2 = - new EventReportPayload.Builder().setReportId(eventReport2.getId()).build().toJson(); + new EventReportPayload.Builder() + .setReportId(eventReport2.getId()) + .setSourceEventId(eventReport2.getSourceEventId()) + .build() + .toJson(); when(mMeasurementDao.getPendingEventReportIdsInWindow(1000, 1100)) .thenReturn(List.of(eventReport1.getId(), eventReport2.getId())); @@ -301,7 +390,7 @@ public class EventReportingJobHandlerTest { Assert.assertTrue( mSpyEventReportingJobHandler.performScheduledPendingReportsInWindow(1000, 1100)); - verify(mMeasurementDao, times(2)).markEventReportDelivered(any()); + verify(mMeasurementDao, times(2)).markEventReportStatus(any(), anyInt()); verify(mTransaction, times(5)).begin(); verify(mTransaction, times(5)).end(); } @@ -320,7 +409,7 @@ public class EventReportingJobHandlerTest { AdServicesStatusUtils.STATUS_INTERNAL_ERROR, mSpyEventReportingJobHandler.performReport(eventReport.getId())); - verify(mMeasurementDao, never()).markEventReportDelivered(any()); + verify(mMeasurementDao, never()).markEventReportStatus(any(), anyInt()); verify(mTransaction, times(1)).begin(); verify(mTransaction, times(1)).end(); } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventReportingJobServiceTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventReportingJobServiceTest.java index bfae948a64..4cee88d060 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventReportingJobServiceTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/reporting/EventReportingJobServiceTest.java @@ -19,6 +19,7 @@ package com.android.adservices.service.measurement.reporting; import static com.android.adservices.service.AdServicesConfig.MEASUREMENT_EVENT_MAIN_REPORTING_JOB_ID; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -35,32 +36,30 @@ import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.content.Context; -import android.provider.DeviceConfig; +import com.android.adservices.data.enrollment.EnrollmentDao; import com.android.adservices.data.measurement.DatastoreManager; import com.android.adservices.data.measurement.DatastoreManagerFactory; +import com.android.adservices.service.AdServicesConfig; +import com.android.adservices.service.Flags; +import com.android.adservices.service.FlagsFactory; import com.android.compatibility.common.util.TestUtils; import com.android.dx.mockito.inline.extended.ExtendedMockito; -import com.android.modules.utils.testing.TestableDeviceConfig; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; import java.util.Optional; +import java.util.concurrent.TimeUnit; /** * Unit test for {@link EventReportingJobService */ public class EventReportingJobServiceTest { - // This rule is used for configuring P/H flags - @Rule - public final TestableDeviceConfig.TestableDeviceConfigRule mDeviceConfigRule = - new TestableDeviceConfig.TestableDeviceConfigRule(); - private static final long WAIT_IN_MILLIS = 50L; private DatastoreManager mMockDatastoreManager; @@ -77,10 +76,11 @@ public class EventReportingJobServiceTest { @Test public void onStartJob_killSwitchOn() throws Exception { - enableKillSwitch(); - runWithMocks( () -> { + // Setup + enableKillSwitch(); + // Execute boolean result = mSpyService.onStartJob(Mockito.mock(JobParameters.class)); @@ -97,11 +97,11 @@ public class EventReportingJobServiceTest { @Test public void onStartJob_killSwitchOff() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { // Setup + disableKillSwitch(); + ExtendedMockito.doNothing() .when( () -> @@ -124,11 +124,11 @@ public class EventReportingJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOn_dontSchedule() throws Exception { - enableKillSwitch(); - runWithMocks( () -> { // Setup + enableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -155,11 +155,11 @@ public class EventReportingJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOff_previouslyExecuted_dontForceSchedule_dontSchedule() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { // Setup + disableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -186,11 +186,11 @@ public class EventReportingJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOff_previouslyExecuted_forceSchedule_schedule() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { // Setup + disableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -217,11 +217,11 @@ public class EventReportingJobServiceTest { @Test public void scheduleIfNeeded_killSwitchOff_previouslyNotExecuted_dontForceSchedule_schedule() throws Exception { - disableKillSwitch(); - runWithMocks( () -> { // Setup + disableKillSwitch(); + final Context mockContext = mock(Context.class); doReturn(mMockJobScheduler) .when(mockContext) @@ -243,11 +243,34 @@ public class EventReportingJobServiceTest { }); } + @Test + public void testSchedule_jobInfoIsPersisted() throws Exception { + runWithMocks( + () -> { + // Setup + final JobScheduler jobScheduler = mock(JobScheduler.class); + final ArgumentCaptor<JobInfo> captor = ArgumentCaptor.forClass(JobInfo.class); + + // Execute + ExtendedMockito.doCallRealMethod() + .when(() -> EventReportingJobService.schedule(any(), any())); + EventReportingJobService.schedule(mock(Context.class), jobScheduler); + + // Validate + verify(jobScheduler, times(1)).schedule(captor.capture()); + assertNotNull(captor.getValue()); + assertTrue(captor.getValue().isPersisted()); + }); + } + private void runWithMocks(TestUtils.RunnableWithThrow execute) throws Exception { MockitoSession session = ExtendedMockito.mockitoSession() + .spyStatic(AdServicesConfig.class) .spyStatic(DatastoreManagerFactory.class) + .spyStatic(EnrollmentDao.class) .spyStatic(EventReportingJobService.class) + .spyStatic(FlagsFactory.class) .strictness(Strictness.LENIENT) .startMocking(); try { @@ -259,6 +282,10 @@ public class EventReportingJobServiceTest { doNothing().when(mSpyService).jobFinished(any(), anyBoolean()); doReturn(mMockJobScheduler).when(mSpyService).getSystemService(JobScheduler.class); doReturn(Mockito.mock(Context.class)).when(mSpyService).getApplicationContext(); + ExtendedMockito.doReturn(TimeUnit.HOURS.toMillis(4)) + .when(AdServicesConfig::getMeasurementEventMainReportingJobPeriodMs); + ExtendedMockito.doReturn(mock(EnrollmentDao.class)) + .when(() -> EnrollmentDao.getInstance(any())); ExtendedMockito.doReturn(mMockDatastoreManager) .when(() -> DatastoreManagerFactory.getDatastoreManager(any())); ExtendedMockito.doNothing().when(() -> EventReportingJobService.schedule(any(), any())); @@ -279,10 +306,8 @@ public class EventReportingJobServiceTest { } private void toggleKillSwitch(boolean value) { - DeviceConfig.setProperty( - DeviceConfig.NAMESPACE_ADSERVICES, - "measurement_job_event_reporting_kill_switch", - Boolean.toString(value), - /* makeDefault */ false); + Flags mockFlags = Mockito.mock(Flags.class); + ExtendedMockito.doReturn(mockFlags).when(FlagsFactory::getFlags); + ExtendedMockito.doReturn(value).when(mockFlags).getMeasurementJobEventReportingKillSwitch(); } } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/util/EnrollmentTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/util/EnrollmentTest.java index ffe8b06f02..72e21704ed 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/util/EnrollmentTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/util/EnrollmentTest.java @@ -52,7 +52,7 @@ public final class EnrollmentTest { @Test public void testMaybeGetEnrollmentId_enrollmentDataNull() { - when(mEnrollmentDao.getEnrollmentDataFromMeasurementUrl(eq(REGISTRATION_URI.toString()))) + when(mEnrollmentDao.getEnrollmentDataFromMeasurementUrl(eq(REGISTRATION_URI))) .thenReturn(null); assertEquals( Optional.empty(), @@ -61,7 +61,7 @@ public final class EnrollmentTest { @Test public void testMaybeGetEnrollmentId_enrollmentDataNonNull() { - when(mEnrollmentDao.getEnrollmentDataFromMeasurementUrl(eq(REGISTRATION_URI.toString()))) + when(mEnrollmentDao.getEnrollmentDataFromMeasurementUrl(eq(REGISTRATION_URI))) .thenReturn(ENROLLMENT); assertEquals( Optional.of(ENROLLMENT.getEnrollmentId()), @@ -87,7 +87,7 @@ public final class EnrollmentTest { @Test public void testMaybeGetReportingOrigin_attributionReportingUrlEmpty() { EnrollmentData enrollment = new EnrollmentData.Builder().build(); - when(mEnrollmentDao.getEnrollmentDataFromMeasurementUrl(eq(ENROLLMENT_ID))) + when(mEnrollmentDao.getEnrollmentDataFromMeasurementUrl(eq(Uri.parse(ENROLLMENT_ID)))) .thenReturn(enrollment); assertEquals( Optional.empty(), diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/util/FilterTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/util/FilterTest.java index cf07d13611..a5e7094a56 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/util/FilterTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/util/FilterTest.java @@ -19,7 +19,7 @@ package com.android.adservices.service.measurement.util; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import com.android.adservices.service.measurement.aggregation.AggregateFilterData; +import com.android.adservices.service.measurement.FilterData; import org.junit.Test; @@ -31,83 +31,170 @@ import java.util.Map; public class FilterTest { @Test - public void testIsFilterMatchReturnTrue() { + public void testIsFilterMatch_nonEmptyValues_returnTrue() { Map<String, List<String>> sourceFilterMap = new HashMap<>(); sourceFilterMap.put( "conversion_subdomain", Collections.singletonList("electronics.megastore")); sourceFilterMap.put("product", Arrays.asList("1234", "234")); sourceFilterMap.put("ctid", Collections.singletonList("id")); - AggregateFilterData sourceFilter = - new AggregateFilterData.Builder().setAttributionFilterMap(sourceFilterMap).build(); + FilterData sourceFilter = + new FilterData.Builder().setAttributionFilterMap(sourceFilterMap).build(); Map<String, List<String>> triggerFilterMap = new HashMap<>(); triggerFilterMap.put( "conversion_subdomain", Collections.singletonList("electronics.megastore")); triggerFilterMap.put("product", Arrays.asList("1234", "2345")); triggerFilterMap.put("id", Arrays.asList("1", "2")); - AggregateFilterData triggerFilter = - new AggregateFilterData.Builder().setAttributionFilterMap(triggerFilterMap).build(); + FilterData triggerFilter = + new FilterData.Builder().setAttributionFilterMap(triggerFilterMap).build(); assertTrue(Filter.isFilterMatch(sourceFilter, triggerFilter, true)); } @Test - public void testIsFilterMatchReturnFalse() { + public void testIsFilterMatch_nonEmptyValues_returnFalse() { Map<String, List<String>> sourceFilterMap = new HashMap<>(); sourceFilterMap.put( "conversion_subdomain", Collections.singletonList("electronics.megastore")); sourceFilterMap.put("product", Arrays.asList("1234", "234")); sourceFilterMap.put("ctid", Collections.singletonList("id")); - AggregateFilterData sourceFilter = - new AggregateFilterData.Builder().setAttributionFilterMap(sourceFilterMap).build(); + FilterData sourceFilter = + new FilterData.Builder().setAttributionFilterMap(sourceFilterMap).build(); Map<String, List<String>> triggerFilterMap = new HashMap<>(); triggerFilterMap.put( "conversion_subdomain", Collections.singletonList("electronics.megastore")); - triggerFilterMap.put("product", Arrays.asList("1", "2")); // doesn't match. + // Doesn't match + triggerFilterMap.put("product", Arrays.asList("1", "2")); triggerFilterMap.put("id", Arrays.asList("1", "2")); - AggregateFilterData triggerFilter = - new AggregateFilterData.Builder().setAttributionFilterMap(triggerFilterMap).build(); + FilterData triggerFilter = + new FilterData.Builder().setAttributionFilterMap(triggerFilterMap).build(); assertFalse(Filter.isFilterMatch(sourceFilter, triggerFilter, true)); } @Test - public void testIsNotFilterMatchReturnTrue() { + public void testIsFilterMatch_withEmptyValues_returnTrue() { + Map<String, List<String>> sourceFilterMap = new HashMap<>(); + sourceFilterMap.put( + "conversion_subdomain", Collections.singletonList("electronics.megastore")); + sourceFilterMap.put("product", Collections.emptyList()); + sourceFilterMap.put("ctid", Collections.singletonList("id")); + FilterData sourceFilter = + new FilterData.Builder().setAttributionFilterMap(sourceFilterMap).build(); + + Map<String, List<String>> triggerFilterMap = new HashMap<>(); + triggerFilterMap.put( + "conversion_subdomain", Collections.singletonList("electronics.megastore")); + triggerFilterMap.put("product", Collections.emptyList()); + triggerFilterMap.put("id", Arrays.asList("1", "2")); + FilterData triggerFilter = + new FilterData.Builder().setAttributionFilterMap(triggerFilterMap).build(); + + assertTrue(Filter.isFilterMatch(sourceFilter, triggerFilter, true)); + } + + @Test + public void testIsFilterMatch_withEmptyValues_returnFalse() { + Map<String, List<String>> sourceFilterMap = new HashMap<>(); + sourceFilterMap.put( + "conversion_subdomain", Collections.singletonList("electronics.megastore")); + sourceFilterMap.put("product", Arrays.asList("1234", "234")); + sourceFilterMap.put("ctid", Collections.singletonList("id")); + FilterData sourceFilter = + new FilterData.Builder().setAttributionFilterMap(sourceFilterMap).build(); + + Map<String, List<String>> triggerFilterMap = new HashMap<>(); + triggerFilterMap.put( + "conversion_subdomain", Collections.singletonList("electronics.megastore")); + // Doesn't match + triggerFilterMap.put("product", Collections.emptyList()); + triggerFilterMap.put("id", Arrays.asList("1", "2")); + FilterData triggerFilter = + new FilterData.Builder().setAttributionFilterMap(triggerFilterMap).build(); + + assertFalse(Filter.isFilterMatch(sourceFilter, triggerFilter, true)); + } + + @Test + public void testIsFilterMatch_withNegation_nonEmptyValues_returnTrue() { Map<String, List<String>> sourceFilterMap = new HashMap<>(); sourceFilterMap.put( "conversion_subdomain", Collections.singletonList("electronics.megastore")); sourceFilterMap.put("product", Arrays.asList("1234", "234")); sourceFilterMap.put("ctid", Collections.singletonList("id")); - AggregateFilterData sourceFilter = - new AggregateFilterData.Builder().setAttributionFilterMap(sourceFilterMap).build(); + FilterData sourceFilter = + new FilterData.Builder().setAttributionFilterMap(sourceFilterMap).build(); Map<String, List<String>> triggerFilterMap = new HashMap<>(); triggerFilterMap.put("conversion_subdomain", Collections.singletonList("electronics")); - triggerFilterMap.put("product", Arrays.asList("1", "2")); // doesn't match. + // Doesn't match + triggerFilterMap.put("product", Arrays.asList("1", "2")); triggerFilterMap.put("id", Arrays.asList("1", "2")); - AggregateFilterData triggerFilter = - new AggregateFilterData.Builder().setAttributionFilterMap(triggerFilterMap).build(); + FilterData triggerFilter = + new FilterData.Builder().setAttributionFilterMap(triggerFilterMap).build(); assertTrue(Filter.isFilterMatch(sourceFilter, triggerFilter, false)); } @Test - public void testIsNotFilterMatchReturnFalse() { + public void testIsFilterMatch_withNegation_nonEmptyValues_returnFalse() { Map<String, List<String>> sourceFilterMap = new HashMap<>(); sourceFilterMap.put( "conversion_subdomain", Collections.singletonList("electronics.megastore")); sourceFilterMap.put("product", Arrays.asList("1234", "234")); sourceFilterMap.put("ctid", Collections.singletonList("id")); - AggregateFilterData sourceFilter = - new AggregateFilterData.Builder().setAttributionFilterMap(sourceFilterMap).build(); + FilterData sourceFilter = + new FilterData.Builder().setAttributionFilterMap(sourceFilterMap).build(); Map<String, List<String>> triggerFilterMap = new HashMap<>(); triggerFilterMap.put( "conversion_subdomain", Collections.singletonList("electronics.megastore")); triggerFilterMap.put("product", Arrays.asList("1234", "2345")); triggerFilterMap.put("id", Arrays.asList("1", "2")); - AggregateFilterData triggerFilter = - new AggregateFilterData.Builder().setAttributionFilterMap(triggerFilterMap).build(); + FilterData triggerFilter = + new FilterData.Builder().setAttributionFilterMap(triggerFilterMap).build(); + + assertFalse(Filter.isFilterMatch(sourceFilter, triggerFilter, false)); + } + + @Test + public void testIsFilterMatch_withNegation_withEmptyValues_returnTrue() { + Map<String, List<String>> sourceFilterMap = new HashMap<>(); + sourceFilterMap.put( + "conversion_subdomain", Collections.singletonList("electronics.megastore")); + sourceFilterMap.put("product", Arrays.asList("1234", "234")); + sourceFilterMap.put("ctid", Collections.singletonList("id")); + FilterData sourceFilter = + new FilterData.Builder().setAttributionFilterMap(sourceFilterMap).build(); + + Map<String, List<String>> triggerFilterMap = new HashMap<>(); + triggerFilterMap.put("conversion_subdomain", Collections.singletonList("electronics")); + // Matches when negated + triggerFilterMap.put("product", Collections.emptyList()); + triggerFilterMap.put("id", Arrays.asList("1", "2")); + FilterData triggerFilter = + new FilterData.Builder().setAttributionFilterMap(triggerFilterMap).build(); + assertTrue(Filter.isFilterMatch(sourceFilter, triggerFilter, false)); + } + + @Test + public void testIsFilterMatch_withNegation_withEmptyValues_returnFalse() { + Map<String, List<String>> sourceFilterMap = new HashMap<>(); + sourceFilterMap.put( + "conversion_subdomain", Collections.singletonList("electronics.megastore")); + sourceFilterMap.put("product", Collections.emptyList()); + sourceFilterMap.put("ctid", Collections.singletonList("id")); + FilterData sourceFilter = + new FilterData.Builder().setAttributionFilterMap(sourceFilterMap).build(); + + Map<String, List<String>> triggerFilterMap = new HashMap<>(); + triggerFilterMap.put( + "conversion_subdomain", Collections.singletonList("electronics.megastore")); + // Doesn't match when negated + triggerFilterMap.put("product", Collections.emptyList()); + triggerFilterMap.put("id", Arrays.asList("1", "2")); + FilterData triggerFilter = + new FilterData.Builder().setAttributionFilterMap(triggerFilterMap).build(); assertFalse(Filter.isFilterMatch(sourceFilter, triggerFilter, false)); } diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/util/UnsignedLongTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/util/UnsignedLongTest.java new file mode 100644 index 0000000000..170b006802 --- /dev/null +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/util/UnsignedLongTest.java @@ -0,0 +1,105 @@ +/* + * 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.measurement.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class UnsignedLongTest { + private static final UnsignedLong ULONG_LONG = new UnsignedLong(2L); + private static final UnsignedLong ULONG_STR = new UnsignedLong("2"); + private static final UnsignedLong LARGE = new UnsignedLong("9223372036854775808"); + private static final UnsignedLong LARGER = new UnsignedLong("9223372036854775809"); + + @Test + public void testEqualsPass() { + assertEquals(ULONG_STR, ULONG_STR); + assertEquals(ULONG_STR, ULONG_LONG); + assertEquals(ULONG_LONG, ULONG_LONG); + } + + @Test + public void testEqualsFail() { + assertNotEquals(ULONG_LONG, new UnsignedLong(3L)); + assertNotEquals(ULONG_STR, new UnsignedLong(3L)); + } + + @Test + public void testCompareTo_equal() { + assertTrue(ULONG_LONG.compareTo(ULONG_STR) == 0); + assertTrue(LARGE.compareTo(new UnsignedLong("9223372036854775808")) == 0); + } + + @Test + public void testCompareTo_smaller() { + assertTrue(ULONG_LONG.compareTo(new UnsignedLong(3L)) < 0); + assertTrue(ULONG_LONG.compareTo(LARGE) < 0); + assertTrue(LARGE.compareTo(LARGER) < 0); + } + + @Test + public void testCompareTo_larger() { + assertTrue(ULONG_LONG.compareTo(new UnsignedLong(1L)) > 0); + assertTrue(LARGE.compareTo(ULONG_LONG) > 0); + assertTrue(LARGER.compareTo(LARGE) > 0); + } + + @Test + public void testHashCode_equals() { + assertEquals(ULONG_STR.hashCode(), ULONG_STR.hashCode()); + assertEquals(ULONG_STR.hashCode(), ULONG_LONG.hashCode()); + assertEquals(ULONG_LONG.hashCode(), ULONG_LONG.hashCode()); + } + + @Test + public void testHashCode_notEquals() { + assertNotEquals(ULONG_LONG.hashCode(), new UnsignedLong(3L).hashCode()); + assertNotEquals(ULONG_STR.hashCode(), new UnsignedLong(3L).hashCode()); + } + + @Test + public void testValidateConstructorArgument_long() { + Long arg = null; + assertThrows( + IllegalArgumentException.class, + () -> new UnsignedLong(arg)); + } + + @Test + public void testValidateConstructorArgument_string() { + String arg = null; + assertThrows( + IllegalArgumentException.class, + () -> new UnsignedLong(arg)); + } + + @Test + public void testMod() { + // uint64 18446744073709501613 = long -50003 + // 18446744073709501613 % 8 = 5 + assertEquals(new UnsignedLong(5L), new UnsignedLong(-50003L).mod(8)); + } + + @Test + public void testToString() { + assertEquals("18446744073709551615", new UnsignedLong(-1L).toString()); + } +} diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/util/WebTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/util/WebTest.java index 2b2bc00b7f..e16c1d8c3e 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/util/WebTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/measurement/util/WebTest.java @@ -36,13 +36,15 @@ public class WebTest { private static final String HTTPS_SCHEME = "https"; private static final String HTTP_SCHEME = "http"; private static final String INVALID_URL = "invalid url"; + private static final String PATH = "path"; + private static final String SECOND_PATH = "second_path"; @Test public void testTopPrivateDomainAndScheme_ValidPublicDomainAndHttpsScheme() { String inputUrl = String.format("%s://%s.%s", HTTPS_SCHEME, TOP_PRIVATE_DOMAIN, VALID_PUBLIC_DOMAIN); Uri expectedUri = Uri.parse(inputUrl); - Optional output = Web.topPrivateDomainAndScheme(inputUrl); + Optional<Uri> output = Web.topPrivateDomainAndScheme(inputUrl); assertTrue(output.isPresent()); assertEquals(expectedUri, output.get()); } @@ -52,7 +54,7 @@ public class WebTest { String inputUrl = String.format("%s://%s.%s", HTTPS_SCHEME, TOP_PRIVATE_DOMAIN, VALID_PRIVATE_DOMAIN); Uri expectedUri = Uri.parse(inputUrl); - Optional output = Web.topPrivateDomainAndScheme(inputUrl); + Optional<Uri> output = Web.topPrivateDomainAndScheme(inputUrl); assertTrue(output.isPresent()); assertEquals(expectedUri, output.get()); } @@ -63,7 +65,7 @@ public class WebTest { HTTPS_SCHEME, SUBDOMAIN, TOP_PRIVATE_DOMAIN, VALID_PUBLIC_DOMAIN); Uri expectedUri = Uri.parse(String.format("%s://%s.%s", HTTPS_SCHEME, TOP_PRIVATE_DOMAIN, VALID_PUBLIC_DOMAIN)); - Optional output = Web.topPrivateDomainAndScheme(inputUrl); + Optional<Uri> output = Web.topPrivateDomainAndScheme(inputUrl); assertTrue(output.isPresent()); assertEquals(expectedUri, output.get()); } @@ -74,7 +76,7 @@ public class WebTest { HTTPS_SCHEME, SUBDOMAIN, TOP_PRIVATE_DOMAIN, VALID_PRIVATE_DOMAIN); Uri expectedUri = Uri.parse(String.format("%s://%s.%s", HTTPS_SCHEME, TOP_PRIVATE_DOMAIN, VALID_PRIVATE_DOMAIN)); - Optional output = Web.topPrivateDomainAndScheme(inputUrl); + Optional<Uri> output = Web.topPrivateDomainAndScheme(inputUrl); assertTrue(output.isPresent()); assertEquals(expectedUri, output.get()); } @@ -84,7 +86,7 @@ public class WebTest { String inputUrl = String.format("%s://%s.%s", HTTP_SCHEME, TOP_PRIVATE_DOMAIN, VALID_PUBLIC_DOMAIN); Uri expectedUri = Uri.parse(inputUrl); - Optional output = Web.topPrivateDomainAndScheme(inputUrl); + Optional<Uri> output = Web.topPrivateDomainAndScheme(inputUrl); assertTrue(output.isPresent()); assertEquals(expectedUri, output.get()); } @@ -92,13 +94,68 @@ public class WebTest { @Test public void testTopPrivateDomainAndScheme_InvalidTldAndHttpsScheme() { String inputUrl = String.format("%s://%s.%s", HTTP_SCHEME, TOP_PRIVATE_DOMAIN, INVALID_TLD); - Optional output = Web.topPrivateDomainAndScheme(inputUrl); + Optional<Uri> output = Web.topPrivateDomainAndScheme(inputUrl); assertFalse(output.isPresent()); } @Test public void testTopPrivateDomainAndScheme_InvalidUrl() { - Optional output = Web.topPrivateDomainAndScheme(INVALID_URL); + Optional<Uri> output = Web.topPrivateDomainAndScheme(INVALID_URL); + assertFalse(output.isPresent()); + } + + @Test + public void topPrivateDomainAndPath_ForDomainAndPath_ReturnsDomainAndPath() { + String inputUrl = + String.format( + "%s://%s.%s/%s", + HTTPS_SCHEME, TOP_PRIVATE_DOMAIN, VALID_PUBLIC_DOMAIN, PATH); + Uri expectedUri = Uri.parse(inputUrl); + Optional<Uri> output = Web.topPrivateDomainSchemeAndPath(Uri.parse(inputUrl)); + assertTrue(output.isPresent()); + assertEquals(expectedUri, output.get()); + } + + @Test + public void topPrivateDomainAndPath_ForSubdomain_DoesNotReturnSubdomain() { + String inputUrl = + String.format( + "%s://%s.%s.%s/%s", + HTTPS_SCHEME, SUBDOMAIN, TOP_PRIVATE_DOMAIN, VALID_PUBLIC_DOMAIN, PATH); + Uri uri = Uri.parse(inputUrl); + String expectedUri = + String.format( + "%s://%s.%s/%s", + HTTPS_SCHEME, TOP_PRIVATE_DOMAIN, VALID_PUBLIC_DOMAIN, PATH); + Optional<Uri> output = Web.topPrivateDomainSchemeAndPath(uri); + assertTrue(output.isPresent()); + assertEquals(expectedUri, output.get().toString()); + } + + @Test + public void topPrivateDomainAndPath_ForMultiplePaths_ReturnsMultipleTokens() { + String inputUrl = + String.format( + "%s://%s.%s.%s/%s/%s", + HTTPS_SCHEME, + SUBDOMAIN, + TOP_PRIVATE_DOMAIN, + VALID_PUBLIC_DOMAIN, + PATH, + SECOND_PATH); + Uri uri = Uri.parse(inputUrl); + String expectedUri = + String.format( + "%s://%s.%s/%s/%s", + HTTPS_SCHEME, TOP_PRIVATE_DOMAIN, VALID_PUBLIC_DOMAIN, PATH, SECOND_PATH); + Optional<Uri> output = Web.topPrivateDomainSchemeAndPath(uri); + assertTrue(output.isPresent()); + assertEquals(expectedUri, output.get().toString()); + } + + @Test + public void topPrivateDomainAndPath_ForInvalidUri_ReturnsEmptyOptional() { + Optional<Uri> output = Web.topPrivateDomainAndScheme(INVALID_URL); assertFalse(output.isPresent()); } } |