[fetchCA] Handle request filter failures

Test: atest AdServicesServiceCoreUnitTests

Bug: 282017511

Fixes: 286671197

Change-Id: I69c438625a5b93d6653fadede87fec3e619b5519
diff --git a/adservices/service-core/java/com/android/adservices/service/common/AbstractFledgeServiceFilter.java b/adservices/service-core/java/com/android/adservices/service/common/AbstractFledgeServiceFilter.java
index 677f76b..e3135c2 100644
--- a/adservices/service-core/java/com/android/adservices/service/common/AbstractFledgeServiceFilter.java
+++ b/adservices/service-core/java/com/android/adservices/service/common/AbstractFledgeServiceFilter.java
@@ -94,6 +94,20 @@
     }
 
     /**
+     * Asserts caller has user consent to use FLEDGE APIs in the calling app and persists consent
+     * for.
+     *
+     * @throws ConsentManager.RevokedConsentException if FLEDGE or the Privacy Sandbox do not have
+     *     user consent
+     */
+    protected void assertAndPersistCallerHasUserConsentForApp(String callerPackageName)
+            throws ConsentManager.RevokedConsentException {
+        if (mConsentManager.isFledgeConsentRevokedForAppAfterSettingFledgeUse(callerPackageName)) {
+            throw new ConsentManager.RevokedConsentException();
+        }
+    }
+
+    /**
      * Asserts that the caller has the appropriate foreground status.
      *
      * @throws AppImportanceFilter.WrongCallingApplicationStateException if the foreground check
diff --git a/adservices/service-core/java/com/android/adservices/service/common/CustomAudienceServiceFilter.java b/adservices/service-core/java/com/android/adservices/service/common/CustomAudienceServiceFilter.java
index 5b7f924..c63d45c 100644
--- a/adservices/service-core/java/com/android/adservices/service/common/CustomAudienceServiceFilter.java
+++ b/adservices/service-core/java/com/android/adservices/service/common/CustomAudienceServiceFilter.java
@@ -63,7 +63,7 @@
      *     skipped.
      * @param callerPackageName caller package name to be validated
      * @param enforceForeground whether to enforce a foreground check
-     * @param enforceConsent currently unused in CustomAudienceServiceFilter
+     * @param enforceConsent whether to enforce per-app consent
      * @param callerUid caller's uid from the Binder thread
      * @param apiName the id of the api being called
      * @param apiKey api-specific throttler key
@@ -106,5 +106,10 @@
 
         sLogger.v("Validating caller package is in allow list.");
         assertAppInAllowList(callerPackageName, apiName);
+
+        if (enforceConsent) {
+            sLogger.v("Validating per-app user consent.");
+            assertAndPersistCallerHasUserConsentForApp(callerPackageName);
+        }
     }
 }
diff --git a/adservices/service-core/java/com/android/adservices/service/customaudience/FetchCustomAudienceImpl.java b/adservices/service-core/java/com/android/adservices/service/customaudience/FetchCustomAudienceImpl.java
index c126739..95e1792 100644
--- a/adservices/service-core/java/com/android/adservices/service/customaudience/FetchCustomAudienceImpl.java
+++ b/adservices/service-core/java/com/android/adservices/service/customaudience/FetchCustomAudienceImpl.java
@@ -27,7 +27,6 @@
 import android.annotation.NonNull;
 import android.net.Uri;
 import android.os.Build;
-import android.os.LimitExceededException;
 import android.os.RemoteException;
 
 import androidx.annotation.RequiresApi;
@@ -37,15 +36,13 @@
 import com.android.adservices.data.customaudience.DBCustomAudience;
 import com.android.adservices.service.Flags;
 import com.android.adservices.service.common.AdTechIdentifierValidator;
-import com.android.adservices.service.common.AppImportanceFilter;
 import com.android.adservices.service.common.CallingAppUidSupplier;
 import com.android.adservices.service.common.CustomAudienceServiceFilter;
-import com.android.adservices.service.common.FledgeAllowListsFilter;
-import com.android.adservices.service.common.FledgeAuthorizationFilter;
 import com.android.adservices.service.common.httpclient.AdServicesHttpClientRequest;
 import com.android.adservices.service.common.httpclient.AdServicesHttpClientResponse;
 import com.android.adservices.service.common.httpclient.AdServicesHttpsClient;
 import com.android.adservices.service.consent.ConsentManager;
+import com.android.adservices.service.exception.FilterException;
 import com.android.adservices.service.stats.AdServicesLogger;
 import com.android.internal.annotations.VisibleForTesting;
 
@@ -163,7 +160,16 @@
                                                 t,
                                                 "Error encountered in fetchCustomAudience"
                                                         + " execution");
-                                        if (t instanceof ConsentManager.RevokedConsentException) {
+                                        if (t instanceof FilterException
+                                                && t.getCause()
+                                                        instanceof
+                                                        ConsentManager.RevokedConsentException) {
+                                            // Skip logging if a FilterException occurs.
+                                            // AdSelectionServiceFilter ensures the failing
+                                            // assertion is logged
+                                            // internally.
+
+                                            // Fail Silently by notifying success to caller
                                             notifySuccess(callback);
                                         } else {
                                             notifyFailure(callback, t);
@@ -189,19 +195,17 @@
         mBuyer = AdTechIdentifier.fromString(host);
 
         // Filter request
-        mCustomAudienceServiceFilter.filterRequest(
-                mBuyer,
-                input.getCallerPackageName(),
-                mEnforceForegroundStatusForFledgeCustomAudience,
-                false,
-                mCallingAppUidSupplier.getCallingAppUid(),
-                API_NAME,
-                FLEDGE_API_FETCH_CUSTOM_AUDIENCE);
-
-        if (mConsentManager.isFledgeConsentRevokedForAppAfterSettingFledgeUse(
-                input.getCallerPackageName())) {
-            sLogger.v("Consent revoked");
-            throw new ConsentManager.RevokedConsentException();
+        try {
+            mCustomAudienceServiceFilter.filterRequest(
+                    mBuyer,
+                    input.getCallerPackageName(),
+                    mEnforceForegroundStatusForFledgeCustomAudience,
+                    true,
+                    mCallingAppUidSupplier.getCallingAppUid(),
+                    API_NAME,
+                    FLEDGE_API_FETCH_CUSTOM_AUDIENCE);
+        } catch (Throwable t) {
+            throw new FilterException(t);
         }
 
         // Validate request
@@ -337,35 +341,33 @@
     // TODO(b/283857101): Move DB handling to persistResponse using a common CustomAudienceReader.
     // private Void persistResponse (@NonNull DBCustomAudience customAudience) {}
 
-    private void notifyFailure(FetchAndJoinCustomAudienceCallback callback, Throwable exception) {
+    private void notifyFailure(FetchAndJoinCustomAudienceCallback callback, Throwable t) {
         try {
-            int resultCode = AdServicesStatusUtils.STATUS_UNSET;
+            int resultCode;
 
-            if (exception instanceof NullPointerException
-                    || exception instanceof IllegalArgumentException) {
+            boolean isFilterException = t instanceof FilterException;
+
+            if (isFilterException) {
+                resultCode = FilterException.getResultCode(t);
+            } else if (t instanceof IllegalArgumentException) {
                 resultCode = AdServicesStatusUtils.STATUS_INVALID_ARGUMENT;
-            } else if (exception
-                    instanceof AppImportanceFilter.WrongCallingApplicationStateException) {
-                resultCode = AdServicesStatusUtils.STATUS_BACKGROUND_CALLER;
-            } else if (exception instanceof FledgeAuthorizationFilter.CallerMismatchException) {
-                resultCode = AdServicesStatusUtils.STATUS_UNAUTHORIZED;
-            } else if (exception instanceof FledgeAuthorizationFilter.AdTechNotAllowedException
-                    || exception instanceof FledgeAllowListsFilter.AppNotAllowedException) {
-                resultCode = AdServicesStatusUtils.STATUS_CALLER_NOT_ALLOWED;
-            } else if (exception instanceof LimitExceededException) {
-                resultCode = AdServicesStatusUtils.STATUS_RATE_LIMIT_REACHED;
-            } else if (exception instanceof IllegalStateException) {
-                resultCode = AdServicesStatusUtils.STATUS_INTERNAL_ERROR;
             } else {
-                sLogger.e(exception, "Unexpected error during operation");
+                sLogger.d(t, "Unexpected error during operation");
                 resultCode = AdServicesStatusUtils.STATUS_INTERNAL_ERROR;
             }
+
+            // Skip logging if a FilterException occurs.
+            // AdSelectionServiceFilter ensures the failing assertion is logged internally.
+            // Note: Failure is logged before the callback to ensure deterministic testing.
+            if (!isFilterException) {
+                mAdServicesLogger.logFledgeApiCallStats(API_NAME, resultCode, 0);
+            }
+
             callback.onFailure(
                     new FledgeErrorResponse.Builder()
                             .setStatusCode(resultCode)
-                            .setErrorMessage(exception.getMessage())
+                            .setErrorMessage(t.getMessage())
                             .build());
-            mAdServicesLogger.logFledgeApiCallStats(API_NAME, resultCode, 0);
         } catch (RemoteException e) {
             sLogger.e(e, "Unable to send failed result to the callback");
             mAdServicesLogger.logFledgeApiCallStats(
@@ -377,9 +379,9 @@
     /** Invokes the onSuccess function from the callback and handles the exception. */
     private void notifySuccess(@NonNull FetchAndJoinCustomAudienceCallback callback) {
         try {
-            callback.onSuccess();
             mAdServicesLogger.logFledgeApiCallStats(
                     API_NAME, AdServicesStatusUtils.STATUS_SUCCESS, 0);
+            callback.onSuccess();
         } catch (RemoteException e) {
             sLogger.e(e, "Unable to send successful result to the callback");
             mAdServicesLogger.logFledgeApiCallStats(
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/common/CustomAudienceServiceFilterTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/common/CustomAudienceServiceFilterTest.java
index 971346e..f302b77 100644
--- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/common/CustomAudienceServiceFilterTest.java
+++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/common/CustomAudienceServiceFilterTest.java
@@ -16,16 +16,17 @@
 
 package com.android.adservices.service.common;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyString;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doThrow;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
 
 import static org.junit.Assert.assertThrows;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.never;
 
 import android.adservices.common.AdTechIdentifier;
 import android.adservices.common.CommonFixture;
@@ -260,4 +261,53 @@
                                 API_NAME,
                                 Throttler.ApiKey.UNKNOWN));
     }
+
+    @Test
+    public void testAssertAndPersistCallerHasUserConsentForApp_enforceConsentTrue_succeeds() {
+        doReturn(false)
+                .when(mConsentManagerMock)
+                .isFledgeConsentRevokedForAppAfterSettingFledgeUse(CALLER_PACKAGE_NAME);
+        mCustomAudienceServiceFilter.filterRequest(
+                SELLER_VALID,
+                CALLER_PACKAGE_NAME,
+                false,
+                true,
+                MY_UID,
+                API_NAME,
+                Throttler.ApiKey.UNKNOWN);
+    }
+
+    @Test
+    public void testAssertAndPersistCallerHasUserConsentForApp_enforceConsentTrue_throws() {
+        doReturn(true)
+                .when(mConsentManagerMock)
+                .isFledgeConsentRevokedForAppAfterSettingFledgeUse(CALLER_PACKAGE_NAME);
+
+        assertThrows(
+                ConsentManager.RevokedConsentException.class,
+                () ->
+                        mCustomAudienceServiceFilter.filterRequest(
+                                SELLER_VALID,
+                                CALLER_PACKAGE_NAME,
+                                false,
+                                true,
+                                MY_UID,
+                                API_NAME,
+                                Throttler.ApiKey.UNKNOWN));
+    }
+
+    @Test
+    public void testAssertAndPersistCallerHasUserConsentForApp_enforceConsentFalse_succeeds() {
+        doReturn(true)
+                .when(mConsentManagerMock)
+                .isFledgeConsentRevokedForAppAfterSettingFledgeUse(CALLER_PACKAGE_NAME);
+        mCustomAudienceServiceFilter.filterRequest(
+                SELLER_VALID,
+                CALLER_PACKAGE_NAME,
+                false,
+                false,
+                MY_UID,
+                API_NAME,
+                Throttler.ApiKey.UNKNOWN);
+    }
 }
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/customaudience/FetchCustomAudienceImplTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/customaudience/FetchCustomAudienceImplTest.java
index 45734f5..c0c899c 100644
--- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/customaudience/FetchCustomAudienceImplTest.java
+++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/customaudience/FetchCustomAudienceImplTest.java
@@ -16,12 +16,23 @@
 
 package com.android.adservices.service.customaudience;
 
+import static android.adservices.common.AdServicesStatusUtils.ILLEGAL_STATE_BACKGROUND_CALLER_ERROR_MESSAGE;
+import static android.adservices.common.AdServicesStatusUtils.RATE_LIMIT_REACHED_ERROR_MESSAGE;
+import static android.adservices.common.AdServicesStatusUtils.SECURITY_EXCEPTION_CALLER_NOT_ALLOWED_ERROR_MESSAGE;
+import static android.adservices.common.AdServicesStatusUtils.SECURITY_EXCEPTION_CALLER_NOT_ALLOWED_ON_BEHALF_ERROR_MESSAGE;
+import static android.adservices.common.AdServicesStatusUtils.STATUS_BACKGROUND_CALLER;
+import static android.adservices.common.AdServicesStatusUtils.STATUS_CALLER_NOT_ALLOWED;
 import static android.adservices.common.AdServicesStatusUtils.STATUS_INTERNAL_ERROR;
+import static android.adservices.common.AdServicesStatusUtils.STATUS_RATE_LIMIT_REACHED;
 import static android.adservices.common.AdServicesStatusUtils.STATUS_SUCCESS;
+import static android.adservices.common.AdServicesStatusUtils.STATUS_UNAUTHORIZED;
+import static android.adservices.common.AdServicesStatusUtils.STATUS_USER_CONSENT_REVOKED;
 
 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doThrow;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
 import static org.junit.Assert.assertEquals;
@@ -37,6 +48,8 @@
 import android.adservices.customaudience.FetchAndJoinCustomAudienceInput;
 import android.adservices.http.MockWebServerRule;
 import android.net.Uri;
+import android.os.LimitExceededException;
+import android.os.Process;
 import android.os.RemoteException;
 
 import com.android.adservices.LogUtil;
@@ -44,7 +57,11 @@
 import com.android.adservices.concurrency.AdServicesExecutors;
 import com.android.adservices.data.customaudience.CustomAudienceDao;
 import com.android.adservices.service.Flags;
+import com.android.adservices.service.common.AppImportanceFilter;
 import com.android.adservices.service.common.CustomAudienceServiceFilter;
+import com.android.adservices.service.common.FledgeAllowListsFilter;
+import com.android.adservices.service.common.FledgeAuthorizationFilter;
+import com.android.adservices.service.common.Throttler;
 import com.android.adservices.service.common.cache.CacheProviderFactory;
 import com.android.adservices.service.common.httpclient.AdServicesHttpsClient;
 import com.android.adservices.service.consent.ConsentManager;
@@ -81,17 +98,16 @@
     @Mock private ConsentManager mConsentManagerMock;
     @Mock private CustomAudienceServiceFilter mCustomAudienceServiceFilterMock;
     @Mock private CustomAudienceDao mCustomAudienceDaoMock;
-
     @Spy
     private final AdServicesHttpsClient mHttpClientSpy =
             new AdServicesHttpsClient(
                     AdServicesExecutors.getBlockingExecutor(),
                     CacheProviderFactory.createNoOpCache());
-
     @Rule public MockWebServerRule mMockWebServerRule = MockWebServerRuleFactory.createForHttps();
+    private static final AdTechIdentifier BUYER = AdTechIdentifier.fromString("localhost");
     private Uri mFetchUri;
     private FetchCustomAudienceImpl mFetchCustomAudienceImpl;
-    private FetchAndJoinCustomAudienceInput mValidInput;
+    private FetchAndJoinCustomAudienceInput.Builder mInputBuilder;
     private MockitoSession mStaticMockSession = null;
 
     @Before
@@ -105,14 +121,13 @@
 
         mFetchUri = mMockWebServerRule.uriForPath("/fetch");
 
-        mValidInput =
+        mInputBuilder =
                 new FetchAndJoinCustomAudienceInput.Builder(
                                 mFetchUri, CustomAudienceFixture.VALID_OWNER)
                         .setName(CustomAudienceFixture.VALID_NAME)
                         .setActivationTime(CustomAudienceFixture.VALID_ACTIVATION_TIME)
                         .setExpirationTime(CustomAudienceFixture.VALID_EXPIRATION_TIME)
-                        .setUserBiddingSignals(CustomAudienceFixture.VALID_USER_BIDDING_SIGNALS)
-                        .build();
+                        .setUserBiddingSignals(CustomAudienceFixture.VALID_USER_BIDDING_SIGNALS);
 
         mFetchCustomAudienceImpl =
                 new FetchCustomAudienceImpl(
@@ -156,8 +171,17 @@
                         mCustomAudienceServiceFilterMock,
                         mHttpClientSpy);
 
-        FetchCustomAudienceTestCallback callback = callFetchCustomAudience(mValidInput);
+        MockWebServer mockWebServer =
+                mMockWebServerRule.startMockWebServer(
+                        List.of(
+                                new MockResponse()
+                                        .setBody(
+                                                FetchCustomAudienceFixture
+                                                        .getFullSuccessfulJsonResponseString())));
 
+        FetchCustomAudienceTestCallback callback = callFetchCustomAudience(mInputBuilder.build());
+
+        assertEquals(0, mockWebServer.getRequestCount());
         assertFalse(callback.mIsSuccess);
         verify(mAdServicesLoggerMock)
                 .logFledgeApiCallStats(
@@ -176,15 +200,14 @@
                                                 FetchCustomAudienceFixture
                                                         .getFullSuccessfulJsonResponseString())));
 
-        FetchCustomAudienceTestCallback callback = callFetchCustomAudience(mValidInput);
+        FetchCustomAudienceTestCallback callback = callFetchCustomAudience(mInputBuilder.build());
 
         assertEquals(1, mockWebServer.getRequestCount());
         assertTrue(callback.mIsSuccess);
         verify(mCustomAudienceDaoMock)
                 .insertOrOverwriteCustomAudience(
                         FetchCustomAudienceFixture.getFullSuccessfulDBCustomAudience(),
-                        CustomAudienceFixture.getValidDailyUpdateUriByBuyer(
-                                AdTechIdentifier.fromString("localhost")));
+                        CustomAudienceFixture.getValidDailyUpdateUriByBuyer(BUYER));
         verify(mAdServicesLoggerMock)
                 .logFledgeApiCallStats(
                         eq(AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN),
@@ -192,6 +215,185 @@
                         anyInt());
     }
 
+    @Test
+    public void testFetchCustomAudience_throwsWithInvalidPackageName() throws Exception {
+        String otherPackageName = CustomAudienceFixture.VALID_OWNER + "incorrectPackage";
+
+        doThrow(new FledgeAuthorizationFilter.CallerMismatchException())
+                .when(mCustomAudienceServiceFilterMock)
+                .filterRequest(
+                        BUYER,
+                        otherPackageName,
+                        true,
+                        true,
+                        Process.myUid(),
+                        AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN,
+                        Throttler.ApiKey.FLEDGE_API_FETCH_CUSTOM_AUDIENCE);
+
+        FetchCustomAudienceTestCallback callback =
+                callFetchCustomAudience(
+                        mInputBuilder.setCallerPackageName(otherPackageName).build());
+
+        assertFalse(callback.mIsSuccess);
+        assertEquals(STATUS_UNAUTHORIZED, callback.mFledgeErrorResponse.getStatusCode());
+        assertEquals(
+                SECURITY_EXCEPTION_CALLER_NOT_ALLOWED_ON_BEHALF_ERROR_MESSAGE,
+                callback.mFledgeErrorResponse.getErrorMessage());
+
+        // Confirm a duplicate log entry does not exist.
+        // CustomAudienceServiceFilter ensures the failing assertion is logged internally.
+        verify(mAdServicesLoggerMock, never())
+                .logFledgeApiCallStats(
+                        eq(AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN),
+                        eq(STATUS_UNAUTHORIZED),
+                        anyInt());
+    }
+
+    @Test
+    public void testFetchCustomAudience_throwsWhenThrottled() throws Exception {
+        doThrow(new LimitExceededException(RATE_LIMIT_REACHED_ERROR_MESSAGE))
+                .when(mCustomAudienceServiceFilterMock)
+                .filterRequest(
+                        BUYER,
+                        CustomAudienceFixture.VALID_OWNER,
+                        true,
+                        true,
+                        Process.myUid(),
+                        AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN,
+                        Throttler.ApiKey.FLEDGE_API_FETCH_CUSTOM_AUDIENCE);
+
+        FetchCustomAudienceTestCallback callback = callFetchCustomAudience(mInputBuilder.build());
+
+        assertFalse(callback.mIsSuccess);
+        assertEquals(STATUS_RATE_LIMIT_REACHED, callback.mFledgeErrorResponse.getStatusCode());
+        assertEquals(
+                RATE_LIMIT_REACHED_ERROR_MESSAGE, callback.mFledgeErrorResponse.getErrorMessage());
+
+        // Confirm a duplicate log entry does not exist.
+        // CustomAudienceServiceFilter ensures the failing assertion is logged internally.
+        verify(mAdServicesLoggerMock, never())
+                .logFledgeApiCallStats(
+                        eq(AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN),
+                        eq(STATUS_RATE_LIMIT_REACHED),
+                        anyInt());
+    }
+
+    @Test
+    public void testFetchCustomAudience_throwsWithFailedForegroundCheck() throws Exception {
+        doThrow(new AppImportanceFilter.WrongCallingApplicationStateException())
+                .when(mCustomAudienceServiceFilterMock)
+                .filterRequest(
+                        BUYER,
+                        CustomAudienceFixture.VALID_OWNER,
+                        true,
+                        true,
+                        Process.myUid(),
+                        AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN,
+                        Throttler.ApiKey.FLEDGE_API_FETCH_CUSTOM_AUDIENCE);
+
+        FetchCustomAudienceTestCallback callback = callFetchCustomAudience(mInputBuilder.build());
+
+        assertFalse(callback.mIsSuccess);
+        assertEquals(STATUS_BACKGROUND_CALLER, callback.mFledgeErrorResponse.getStatusCode());
+        assertEquals(
+                ILLEGAL_STATE_BACKGROUND_CALLER_ERROR_MESSAGE,
+                callback.mFledgeErrorResponse.getErrorMessage());
+
+        // Confirm a duplicate log entry does not exist.
+        // CustomAudienceServiceFilter ensures the failing assertion is logged internally.
+        verify(mAdServicesLoggerMock, never())
+                .logFledgeApiCallStats(
+                        eq(AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN),
+                        eq(STATUS_BACKGROUND_CALLER),
+                        anyInt());
+    }
+
+    @Test
+    public void testFetchCustomAudience_throwsWithFailedEnrollmentCheck() throws Exception {
+        doThrow(new FledgeAuthorizationFilter.AdTechNotAllowedException())
+                .when(mCustomAudienceServiceFilterMock)
+                .filterRequest(
+                        BUYER,
+                        CustomAudienceFixture.VALID_OWNER,
+                        true,
+                        true,
+                        Process.myUid(),
+                        AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN,
+                        Throttler.ApiKey.FLEDGE_API_FETCH_CUSTOM_AUDIENCE);
+
+        FetchCustomAudienceTestCallback callback = callFetchCustomAudience(mInputBuilder.build());
+
+        assertFalse(callback.mIsSuccess);
+        assertEquals(STATUS_CALLER_NOT_ALLOWED, callback.mFledgeErrorResponse.getStatusCode());
+        assertEquals(
+                SECURITY_EXCEPTION_CALLER_NOT_ALLOWED_ERROR_MESSAGE,
+                callback.mFledgeErrorResponse.getErrorMessage());
+
+        // Confirm a duplicate log entry does not exist.
+        // CustomAudienceServiceFilter ensures the failing assertion is logged internally.
+        verify(mAdServicesLoggerMock, never())
+                .logFledgeApiCallStats(
+                        eq(AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN),
+                        eq(STATUS_CALLER_NOT_ALLOWED),
+                        anyInt());
+    }
+
+    @Test
+    public void testFetchCustomAudience_throwsWhenAppCannotUsePPAPI() throws Exception {
+        doThrow(new FledgeAllowListsFilter.AppNotAllowedException())
+                .when(mCustomAudienceServiceFilterMock)
+                .filterRequest(
+                        BUYER,
+                        CustomAudienceFixture.VALID_OWNER,
+                        true,
+                        true,
+                        Process.myUid(),
+                        AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN,
+                        Throttler.ApiKey.FLEDGE_API_FETCH_CUSTOM_AUDIENCE);
+
+        FetchCustomAudienceTestCallback callback = callFetchCustomAudience(mInputBuilder.build());
+
+        assertFalse(callback.mIsSuccess);
+        assertEquals(STATUS_CALLER_NOT_ALLOWED, callback.mFledgeErrorResponse.getStatusCode());
+        assertEquals(
+                SECURITY_EXCEPTION_CALLER_NOT_ALLOWED_ERROR_MESSAGE,
+                callback.mFledgeErrorResponse.getErrorMessage());
+
+        // Confirm a duplicate log entry does not exist.
+        // CustomAudienceServiceFilter ensures the failing assertion is logged internally.
+        verify(mAdServicesLoggerMock, never())
+                .logFledgeApiCallStats(
+                        eq(AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN),
+                        eq(STATUS_CALLER_NOT_ALLOWED),
+                        anyInt());
+    }
+
+    @Test
+    public void testFetchCustomAudience_failsSilentlyWithRevokeConsent() throws Exception {
+        doThrow(new ConsentManager.RevokedConsentException())
+                .when(mCustomAudienceServiceFilterMock)
+                .filterRequest(
+                        BUYER,
+                        CustomAudienceFixture.VALID_OWNER,
+                        true,
+                        true,
+                        Process.myUid(),
+                        AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN,
+                        Throttler.ApiKey.FLEDGE_API_FETCH_CUSTOM_AUDIENCE);
+
+        FetchCustomAudienceTestCallback callback = callFetchCustomAudience(mInputBuilder.build());
+
+        assertTrue(callback.mIsSuccess);
+
+        // Confirm a duplicate log entry does not exist.
+        // CustomAudienceServiceFilter ensures the failing assertion is logged internally.
+        verify(mAdServicesLoggerMock, never())
+                .logFledgeApiCallStats(
+                        eq(AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN),
+                        eq(STATUS_USER_CONSENT_REVOKED),
+                        anyInt());
+    }
+
     private FetchCustomAudienceTestCallback callFetchCustomAudience(
             FetchAndJoinCustomAudienceInput input) throws Exception {
         CountDownLatch resultLatch = new CountDownLatch(1);