chromium/components/search_engines/android/java/src/org/chromium/components/search_engines/SearchEngineChoiceServiceUnitTest.java

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.search_engines;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;

import android.content.Context;

import androidx.test.filters.SmallTest;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.ParameterizedRobolectricTestRunner;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.FeatureList;
import org.chromium.base.Promise;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.components.search_engines.SearchEngineCountryDelegate.DeviceChoiceEventType;

import java.util.Arrays;
import java.util.Collection;

@SmallTest
@RunWith(ParameterizedRobolectricTestRunner.class)
public class SearchEngineChoiceServiceUnitTest {
    @Parameters
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][] {{true}, {false}});
    }

    public @Rule MockitoRule mockitoRule = MockitoJUnit.rule();

    private @Mock Context mContext;
    private @Mock SearchEngineCountryDelegate mDelegate;

    private final boolean mIsClayBlockingEnabled;

    public SearchEngineChoiceServiceUnitTest(boolean isClayBlockingEnabled) {
        this.mIsClayBlockingEnabled = isClayBlockingEnabled;
    }

    @Before
    public void setUp() {
        configureClayBlockingFeature(mIsClayBlockingEnabled, /* isDarkLaunchEnabled= */ false);

        doReturn(Promise.rejected()).when(mDelegate).getDeviceCountry();
    }

    @Test
    public void testAbstractDelegate() {
        var service = new SearchEngineChoiceService(new SearchEngineCountryDelegate(mContext) {});

        // The default implementation should be set to not trigger anything disruptive.
        assertTrue(service.getDeviceCountry().isRejected());

        assertFalse(service.isDeviceChoiceDialogEligible());
        assertFalse(service.getIsDeviceChoiceRequiredSupplier().get());

        var shouldShowDeviceDialogPromise = service.shouldShowDeviceChoiceDialog();
        ShadowLooper.runUiThreadTasks();
        if (mIsClayBlockingEnabled) {
            assertTrue(shouldShowDeviceDialogPromise.isFulfilled());
            assertFalse(shouldShowDeviceDialogPromise.getResult());
        } else {
            assertTrue(shouldShowDeviceDialogPromise.isRejected());
        }
    }

    @Test
    public void testFakeDelegate() {
        var service =
                new SearchEngineChoiceService(
                        new FakeSearchEngineCountryDelegate(mContext, /* enableLogging= */ true));

        if (mIsClayBlockingEnabled) {
            // It should have generally sensible values and make the dialog be shown.
            assertTrue(service.getDeviceCountry().isFulfilled());

            assertTrue(service.isDeviceChoiceDialogEligible());
            assertTrue(service.getIsDeviceChoiceRequiredSupplier().get());

            var shouldShowDeviceDialogPromise = service.shouldShowDeviceChoiceDialog();
            ShadowLooper.runUiThreadTasks();
            assertTrue(shouldShowDeviceDialogPromise.isFulfilled());
            assertTrue(shouldShowDeviceDialogPromise.getResult());
        } else {
            // Same as the abstract delegate.
            assertTrue(service.getDeviceCountry().isRejected());

            assertFalse(service.isDeviceChoiceDialogEligible());
            assertFalse(service.getIsDeviceChoiceRequiredSupplier().get());

            var shouldShowDeviceDialogPromise = service.shouldShowDeviceChoiceDialog();
            ShadowLooper.runUiThreadTasks();

            assertTrue(shouldShowDeviceDialogPromise.isRejected());
        }

        // The calls below should be fine to run without triggering anything.
        service.launchDeviceChoiceScreens();
        service.notifyDeviceChoiceBlockCleared();
        service.notifyDeviceChoiceBlockShown();
        ShadowLooper.runUiThreadTasks();
    }

    @Test
    public void testGetDeviceCountry_rejected() {
        reset(mDelegate);
        doReturn(Promise.rejected()).when(mDelegate).getDeviceCountry();

        var service = new SearchEngineChoiceService(mDelegate);

        assertTrue(service.getDeviceCountry().isRejected());
        verify(mDelegate, times(1)).getDeviceCountry();

        // Even if it changes, the device country is not fetched again afterwards.
        reset(mDelegate);
        assertTrue(service.getDeviceCountry().isRejected());
        verifyNoInteractions(mDelegate);
    }

    @Test
    public void testGetDeviceCountry_fulfilled() {
        reset(mDelegate);
        doReturn(Promise.fulfilled("countryCode")).when(mDelegate).getDeviceCountry();

        var service = new SearchEngineChoiceService(mDelegate);

        var deviceCountryPromise = service.getDeviceCountry();
        assertTrue(deviceCountryPromise.isFulfilled());
        assertEquals("countryCode", deviceCountryPromise.getResult());
        verify(mDelegate, times(1)).getDeviceCountry();

        // Even if it changes, the device country is not fetched again afterwards.
        reset(mDelegate);
        assertEquals("countryCode", service.getDeviceCountry().getResult());
        verifyNoInteractions(mDelegate);
    }

    @Test
    public void testIsDeviceDialogChoiceEligible() {
        var service = new SearchEngineChoiceService(mDelegate);

        doReturn(false).when(mDelegate).isDeviceChoiceDialogEligible();
        assertFalse(service.isDeviceChoiceDialogEligible());
        verify(mDelegate, times(mIsClayBlockingEnabled ? 1 : 0)).isDeviceChoiceDialogEligible();

        doReturn(true).when(mDelegate).isDeviceChoiceDialogEligible();
        if (mIsClayBlockingEnabled) {
            assertTrue(service.isDeviceChoiceDialogEligible());
            verify(mDelegate, times(2)).isDeviceChoiceDialogEligible();
        } else {
            assertFalse(service.isDeviceChoiceDialogEligible());
            verify(mDelegate, never()).isDeviceChoiceDialogEligible();
        }
    }

    @Test
    public void testGetIsDeviceChoiceRequiredSupplier() {
        var service = new SearchEngineChoiceService(mDelegate);

        ObservableSupplier<Boolean> fakeSupplier = new ObservableSupplierImpl<>();

        doReturn(fakeSupplier).when(mDelegate).getIsDeviceChoiceRequiredSupplier();
        var actualSupplier = service.getIsDeviceChoiceRequiredSupplier();

        if (mIsClayBlockingEnabled) {
            assertSame(fakeSupplier, actualSupplier);
            verify(mDelegate).getIsDeviceChoiceRequiredSupplier();
        } else {
            assertNotSame(fakeSupplier, actualSupplier);
            assertFalse(actualSupplier.get());
            verify(mDelegate, never()).getIsDeviceChoiceRequiredSupplier();
        }
    }

    @Test
    public void testGetIsDeviceChoiceRequiredSupplier_darkLaunch() {
        configureClayBlockingFeature(mIsClayBlockingEnabled, /* isDarkLaunchEnabled= */ true);

        var service = new SearchEngineChoiceService(mDelegate);

        ObservableSupplierImpl<Boolean> fakeSupplier = new ObservableSupplierImpl<>();

        doReturn(fakeSupplier).when(mDelegate).getIsDeviceChoiceRequiredSupplier();
        var actualSupplier = service.getIsDeviceChoiceRequiredSupplier();

        if (mIsClayBlockingEnabled) {
            // For dark launch, we do call into the delegate, but we don't return its values
            // directly.
            assertNotSame(fakeSupplier, actualSupplier);
            assertNull(actualSupplier.get());
            verify(mDelegate).getIsDeviceChoiceRequiredSupplier();

            // We match behaviour for the pending states, but when we get a value from the delegate,
            // we ignore it and always return false.
            fakeSupplier.set(true);
            assertFalse(actualSupplier.get());
        } else {
            assertNotSame(fakeSupplier, actualSupplier);
            assertFalse(actualSupplier.get());
            verify(mDelegate, never()).getIsDeviceChoiceRequiredSupplier();
        }
    }

    @Test
    public void testShouldShowDeviceChoiceDialog() {
        var service = new SearchEngineChoiceService(mDelegate);

        ObservableSupplierImpl<Boolean> fakeSupplier = new ObservableSupplierImpl<>();

        doReturn(fakeSupplier).when(mDelegate).getIsDeviceChoiceRequiredSupplier();
        var promise = service.shouldShowDeviceChoiceDialog();
        ShadowLooper.runUiThreadTasks();

        if (mIsClayBlockingEnabled) {
            assertTrue(promise.isPending());
            verify(mDelegate).getIsDeviceChoiceRequiredSupplier();

            fakeSupplier.set(true);
            ShadowLooper.runUiThreadTasks();
            assertTrue(promise.isFulfilled());
            assertTrue(promise.getResult());
        } else {
            assertTrue(promise.isRejected());
            verify(mDelegate, never()).getIsDeviceChoiceRequiredSupplier();
        }
    }

    @Test
    public void testNotifyDeviceChoiceBlockShown() {
        var service = new SearchEngineChoiceService(mDelegate);

        service.notifyDeviceChoiceBlockShown();
        verify(mDelegate, times(mIsClayBlockingEnabled ? 1 : 0))
                .log(DeviceChoiceEventType.BLOCK_SHOWN);
    }

    @Test
    public void testNotifyDeviceChoiceBlockCleared() {
        var service = new SearchEngineChoiceService(mDelegate);

        service.notifyDeviceChoiceBlockCleared();
        verify(mDelegate, times(mIsClayBlockingEnabled ? 1 : 0))
                .log(DeviceChoiceEventType.BLOCK_CLEARED);
    }

    private static void configureClayBlockingFeature(
            boolean isClayBlockingEnabled, boolean isDarkLaunchEnabled) {
        var testFeatures = new FeatureList.TestValues();
        testFeatures.addFeatureFlagOverride(
                SearchEnginesFeatures.CLAY_BLOCKING, isClayBlockingEnabled);
        testFeatures.addFieldTrialParamOverride(
                SearchEnginesFeatures.CLAY_BLOCKING,
                "is_dark_launch",
                isDarkLaunchEnabled ? "true" : "");
        FeatureList.setTestValues(testFeatures);
    }
}