chromium/chrome/android/java/src/org/chromium/chrome/browser/auxiliary_search/AuxiliarySearchProviderTest.java

// Copyright 2023 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.chrome.browser.auxiliary_search;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
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.doAnswer;
import static org.mockito.Mockito.doReturn;

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.annotation.Config;

import org.chromium.base.Callback;
import org.chromium.base.FeatureList;
import org.chromium.base.FeatureList.TestValues;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.auxiliary_search.AuxiliarySearchGroupProto.AuxiliarySearchBookmarkGroup;
import org.chromium.chrome.browser.auxiliary_search.AuxiliarySearchGroupProto.AuxiliarySearchEntry;
import org.chromium.chrome.browser.auxiliary_search.AuxiliarySearchGroupProto.AuxiliarySearchTabGroup;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.MockTab;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.test.util.browser.tabmodel.MockTabModel;
import org.chromium.url.GURL;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

/** Unit tests for {@link AuxiliarySearchProvider} */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
@EnableFeatures({ChromeFeatureList.ANDROID_APP_INTEGRATION})
public class AuxiliarySearchProviderTest {
    private static final String TAB_TITLE = "tab";
    private static final String TAB_URL = "https://tab.google.com/";
    private static final String BOOKMARK_TITLE = "bookmark";
    private static final String BOOKMARK_URL = "https://bookmark.google.com";
    private static final String NEW_TAB_PAGE_URL = "chrome-native://newtab";
    private static final long FAKE_NATIVE_PROVIDER = 1;

    public @Rule JniMocker mJniMocker = new JniMocker();
    public @Rule MockitoRule mMockitoRule = MockitoJUnit.rule();

    private @Mock AuxiliarySearchBridge.Natives mMockAuxiliarySearchBridgeJni;
    private @Mock Profile mProfile;
    private @Mock TabModelSelector mTabModelSelector;

    private AuxiliarySearchProvider mAuxiliarySearchProvider;
    private MockTabModel mMockNormalTabModel;

    @Before
    public void setUp() {
        mJniMocker.mock(AuxiliarySearchBridgeJni.TEST_HOOKS, mMockAuxiliarySearchBridgeJni);
        doReturn(FAKE_NATIVE_PROVIDER).when(mMockAuxiliarySearchBridgeJni).getForProfile(mProfile);
        mAuxiliarySearchProvider = new AuxiliarySearchProvider(mProfile, mTabModelSelector);
        mMockNormalTabModel = new MockTabModel(mProfile, null);
        doReturn(mMockNormalTabModel).when(mTabModelSelector).getModel(false);
    }

    private Tab createTab(int index, long timestamp) {
        MockTab tab = mMockNormalTabModel.addTab(index);
        tab.setGurlOverrideForTesting(new GURL(TAB_URL + Integer.toString(index)));
        tab.setTitle(TAB_TITLE + Integer.toString(index));
        tab.setTimestampMillis(timestamp);

        return tab;
    }

    private void compareTabs(List<Tab> expectedTabs, List<Tab> returnedTabs) {
        HashSet<Integer> returnedTabsNumbers = new HashSet<Integer>();
        for (Tab returnedTab : returnedTabs) {
            int returnedNumber =
                    Integer.valueOf(returnedTab.getUrl().getSpec().substring(TAB_URL.length()));

            Tab expectedTab = null;
            for (Tab tab : expectedTabs) {
                int expectedNumber =
                        Integer.valueOf(tab.getUrl().getSpec().substring(TAB_URL.length()));
                if (expectedNumber == returnedNumber) {
                    expectedTab = tab;
                    break;
                }
            }
            assertNotNull(expectedTab);
            assertEquals(returnedTab.getTitle(), expectedTab.getTitle());
            assertEquals(returnedTab.getTitle(), expectedTab.getTitle());
            assertEquals(returnedTab.getTimestampMillis(), expectedTab.getTimestampMillis());

            returnedTabsNumbers.add(returnedNumber);
        }
        assertEquals(expectedTabs.size(), returnedTabsNumbers.size());
    }

    @Test
    @SmallTest
    public void testGetBookmarksSearchableDataProto() {
        var bookmark =
                AuxiliarySearchEntry.newBuilder()
                        .setTitle(BOOKMARK_TITLE)
                        .setUrl(BOOKMARK_URL)
                        .setCreationTimestamp(1)
                        .build();
        var proto = AuxiliarySearchBookmarkGroup.newBuilder().addBookmark(bookmark).build();

        doReturn(proto.toByteArray())
                .when(mMockAuxiliarySearchBridgeJni)
                .getBookmarksSearchableData(FAKE_NATIVE_PROVIDER);

        AuxiliarySearchBookmarkGroup bookmarksList =
                mAuxiliarySearchProvider.getBookmarksSearchableDataProto();

        assertEquals(bookmarksList.getBookmarkCount(), 1);
        assertEquals(bookmarksList.getBookmark(0).getTitle(), BOOKMARK_TITLE);
        assertEquals(bookmarksList.getBookmark(0).getUrl(), BOOKMARK_URL);
        assertEquals(bookmarksList.getBookmark(0).getCreationTimestamp(), 1);
        assertFalse(bookmarksList.getBookmark(0).hasLastModificationTimestamp());
        assertFalse(bookmarksList.getBookmark(0).hasLastAccessTimestamp());
    }

    @Test
    @SmallTest
    public void testGetBookmarksSearchableDataProto_failureToParse() {
        // Return a random array which cannot been parsed to proto.
        doReturn(new byte[] {1, 2, 3})
                .when(mMockAuxiliarySearchBridgeJni)
                .getBookmarksSearchableData(FAKE_NATIVE_PROVIDER);

        AuxiliarySearchBookmarkGroup bookmarksList =
                mAuxiliarySearchProvider.getBookmarksSearchableDataProto();
        assertNull(bookmarksList);
    }

    @Test
    @SmallTest
    public void testGetTabsSearchableDataProtoAsync() {
        ArrayList<Tab> tabList = new ArrayList<>();
        long now = System.currentTimeMillis();
        // Create 200 tabs with different timestamps(from 0 to 199), and only the newest 100 tabs
        // should be returned from 'getTabsSearchableDataProtoAsync'.
        for (int i = 0; i < 200; i++) {
            MockTab tab = mMockNormalTabModel.addTab(i);
            tab.setGurlOverrideForTesting(new GURL(TAB_URL + Integer.toString(i)));
            tab.setTitle(TAB_TITLE + Integer.toString(i));
            tab.setTimestampMillis(now + i);
            if (i >= 100) {
                tabList.add(tab);
            }
        }

        Object[] tabObject = new Object[tabList.size()];
        tabList.toArray(tabObject);
        doAnswer(
                        invocation -> {
                            invocation.<Callback<Object[]>>getArgument(2).onResult(tabObject);
                            return null;
                        })
                .when(mMockAuxiliarySearchBridgeJni)
                .getNonSensitiveTabs(eq(FAKE_NATIVE_PROVIDER), any(), any(Callback.class));

        mAuxiliarySearchProvider.getTabsSearchableDataProtoAsync(
                new Callback<AuxiliarySearchTabGroup>() {
                    @Override
                    public void onResult(AuxiliarySearchTabGroup tabGroup) {
                        assertEquals(100, tabGroup.getTabCount());
                        HashSet<Integer> returnedTabsNumbers = new HashSet<Integer>();
                        for (int i = 0; i < tabGroup.getTabCount(); i++) {
                            AuxiliarySearchEntry tab = tabGroup.getTab(i);
                            assertTrue(tab.hasTitle());
                            assertTrue(tab.hasUrl());
                            assertTrue(tab.hasLastAccessTimestamp());
                            assertFalse(tab.hasCreationTimestamp());
                            assertFalse(tab.hasLastModificationTimestamp());

                            int number = Integer.valueOf(tab.getUrl().substring(TAB_URL.length()));
                            assertTrue(
                                    "Only the newest 100 tabs should be received",
                                    number >= 100 && number <= 199);
                            assertEquals(now + (long) number, tab.getLastAccessTimestamp());
                            returnedTabsNumbers.add(number);
                        }
                        assertEquals(returnedTabsNumbers.size(), 100);
                    }
                });
    }

    @Test
    @SmallTest
    public void testGetTabsByMinimalAccessTime() {
        long now = System.currentTimeMillis();
        List<Tab> tabList =
                List.of(
                        createTab(1, now - 50),
                        createTab(2, now - 100),
                        createTab(3, now - 150),
                        createTab(4, now - 200),
                        createTab(5, now - 250));

        var tabs = mAuxiliarySearchProvider.getTabsByMinimalAccessTime(now - 150);
        assertEquals(3, tabs.size());
        compareTabs(tabList.subList(0, 3), tabs);

        tabs = mAuxiliarySearchProvider.getTabsByMinimalAccessTime(now);
        assertEquals(0, tabs.size());

        tabs = mAuxiliarySearchProvider.getTabsByMinimalAccessTime(0);
        assertEquals(5, tabs.size());
        compareTabs(tabList, tabs);
    }

    @Test
    @SmallTest
    public void testTabToAuxiliarySearchEntry_nullTab() {
        assertNull(AuxiliarySearchProvider.tabToAuxiliarySearchEntry(null));
    }

    @Test
    @SmallTest
    public void configuredTabsAgeCannotBeZero() {
        var config = new TestValues();
        config.addFieldTrialParamOverride(
                ChromeFeatureList.ANDROID_APP_INTEGRATION,
                AuxiliarySearchProvider.TAB_AGE_HOURS_PARAM,
                "0");
        FeatureList.setTestValues(config);
        // Recreate provider to update the finch parameter.
        mAuxiliarySearchProvider = new AuxiliarySearchProvider(mProfile, mTabModelSelector);

        assertNotEquals(0L, mAuxiliarySearchProvider.getTabsMaxAgeMs());
        assertEquals(
                AuxiliarySearchProvider.DEFAULT_TAB_AGE_HOURS * 60 * 60 * 1000,
                mAuxiliarySearchProvider.getTabsMaxAgeMs());
    }

    @Test
    @SmallTest
    public void configuredTabsAge() {
        var config = new TestValues();
        config.addFieldTrialParamOverride(
                ChromeFeatureList.ANDROID_APP_INTEGRATION,
                AuxiliarySearchProvider.TAB_AGE_HOURS_PARAM,
                "10");
        FeatureList.setTestValues(config);
        // Recreate provider to update the finch parameter.
        mAuxiliarySearchProvider = new AuxiliarySearchProvider(mProfile, mTabModelSelector);
        assertEquals(10 * 60 * 60 * 1000, mAuxiliarySearchProvider.getTabsMaxAgeMs());
    }
}