chromium/chrome/browser/tab_group_sync/android/java/src/org/chromium/chrome/browser/tab_group_sync/TabGroupSyncUtilsUnitTest.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.chrome.browser.tab_group_sync;

import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.util.Pair;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.annotation.Config;

import org.chromium.base.Token;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.chrome.test.util.browser.tabmodel.MockTabModel;
import org.chromium.components.tab_group_sync.ClosingSource;
import org.chromium.components.tab_group_sync.EventDetails;
import org.chromium.components.tab_group_sync.LocalTabGroupId;
import org.chromium.components.tab_group_sync.SavedTabGroup;
import org.chromium.components.tab_group_sync.SavedTabGroupTab;
import org.chromium.components.tab_group_sync.TabGroupEvent;
import org.chromium.components.tab_group_sync.TabGroupSyncService;
import org.chromium.url.GURL;

import java.util.List;

/** Unit tests for the {@link TabGroupSyncUtils}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class TabGroupSyncUtilsUnitTest {
    private static final int TAB_ID_1 = 1;
    private static final int TAB_ID_2 = 2;
    private static final int TAB_ID_3 = 2;
    private static final int ROOT_ID_1 = 1;
    private static final Token TOKEN_1 = new Token(2, 3);
    private static final Token TOKEN_2 = new Token(5, 8);
    private static final String SYNC_GROUP_ID1 = "remote one";
    private static final String SYNC_GROUP_ID2 = "remote two";
    private static final LocalTabGroupId LOCAL_TAB_GROUP_ID_1 = new LocalTabGroupId(TOKEN_1);
    private static final LocalTabGroupId LOCAL_TAB_GROUP_ID_2 = new LocalTabGroupId(TOKEN_2);

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

    @Spy private TabGroupSyncService mTabGroupSyncService;
    @Mock private Profile mProfile;
    @Mock private TabGroupModelFilter mTabGroupModelFilter;
    private MockTabModel mTabModel;
    private Tab mTab1;
    private Tab mTab2;

    @Before
    public void setUp() {
        mTabModel = spy(new MockTabModel(mProfile, null));
        when(mTabGroupModelFilter.getTabModel()).thenReturn(mTabModel);
        when(mTabGroupModelFilter.isIncognito()).thenReturn(false);

        mTab1 = mTabModel.addTab(TAB_ID_1);
        mTab2 = mTabModel.addTab(TAB_ID_2);
        createTabGroup(List.of(mTab1, mTab2), ROOT_ID_1, TOKEN_1);
    }

    @Test
    public void testUnmapAllTabGroupIdsNotInCurrentFilter() {
        SavedTabGroup group1 = new SavedTabGroup();
        group1.syncId = SYNC_GROUP_ID1;
        SavedTabGroupTab savedTabGroup1Tab1 = new SavedTabGroupTab();
        SavedTabGroupTab savedTabGroup1Tab2 = new SavedTabGroupTab();
        savedTabGroup1Tab1.localId = TAB_ID_1;
        savedTabGroup1Tab2.localId = TAB_ID_2;
        group1.savedTabs = List.of(savedTabGroup1Tab1, savedTabGroup1Tab2);
        group1.localId = LOCAL_TAB_GROUP_ID_1;

        SavedTabGroup group2 = new SavedTabGroup();
        group2.syncId = SYNC_GROUP_ID2;
        SavedTabGroupTab savedTabGroup2Tab1 = new SavedTabGroupTab();
        savedTabGroup2Tab1.localId = TAB_ID_3;
        group2.savedTabs = List.of(savedTabGroup2Tab1);
        group2.localId = LOCAL_TAB_GROUP_ID_2;

        when(mTabGroupSyncService.getAllGroupIds())
                .thenReturn(new String[] {SYNC_GROUP_ID1, SYNC_GROUP_ID2});
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID1)).thenReturn(group1);
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID2)).thenReturn(group2);
        when(mTabGroupModelFilter.getRootIdFromStableId(TOKEN_2)).thenReturn(Tab.INVALID_TAB_ID);

        TabGroupSyncUtils.unmapLocalIdsNotInTabGroupModelFilter(
                mTabGroupSyncService, mTabGroupModelFilter);

        verify(mTabGroupSyncService, never()).removeLocalTabGroupMapping(LOCAL_TAB_GROUP_ID_1);
        verify(mTabGroupSyncService).removeLocalTabGroupMapping(LOCAL_TAB_GROUP_ID_2);

        // Verify metrics.
        ArgumentCaptor<EventDetails> eventDetailsCaptor =
                ArgumentCaptor.forClass(EventDetails.class);
        verify(mTabGroupSyncService).recordTabGroupEvent(eventDetailsCaptor.capture());
        EventDetails eventDetails = eventDetailsCaptor.getValue();
        assertEquals(TabGroupEvent.TAB_GROUP_CLOSED, eventDetails.eventType);
        assertEquals(LOCAL_TAB_GROUP_ID_2, eventDetails.localGroupId);
        assertEquals(ClosingSource.CLEANED_UP_ON_LAST_INSTANCE_CLOSURE, eventDetails.closingSource);
    }

    @Test
    public void testUnmapAllTabGroupIdsNotInCurrentFilter_NullLocalId() {
        SavedTabGroup group1 = new SavedTabGroup();
        group1.syncId = SYNC_GROUP_ID1;
        SavedTabGroupTab savedTabGroup1Tab1 = new SavedTabGroupTab();
        SavedTabGroupTab savedTabGroup1Tab2 = new SavedTabGroupTab();
        group1.savedTabs = List.of(savedTabGroup1Tab1, savedTabGroup1Tab2);
        group1.localId = null;

        when(mTabGroupSyncService.getAllGroupIds()).thenReturn(new String[] {SYNC_GROUP_ID1});
        when(mTabGroupSyncService.getGroup(SYNC_GROUP_ID1)).thenReturn(group1);

        TabGroupSyncUtils.unmapLocalIdsNotInTabGroupModelFilter(
                mTabGroupSyncService, mTabGroupModelFilter);

        // Shouldn't crash and never called.
        verify(mTabGroupModelFilter, never()).getRootIdFromStableId(any());
        verify(mTabGroupSyncService, never()).removeLocalTabGroupMapping(LOCAL_TAB_GROUP_ID_1);
    }

    @Test
    public void testGetFilteredUrl_NewTab() {
        // All types of NTP URLs.
        expectFilteredUrlAndTitle(
                "chrome://newtab", "New tab", "chrome-native://newtab", "New Tab");
        expectFilteredUrlAndTitle(
                "chrome://newtab", "New tab", "chrome-native://newtab/", "New tab");
        expectFilteredUrlAndTitle("chrome://newtab", "New tab", "chrome://newtab", "New tab");
        expectFilteredUrlAndTitle("chrome://newtab", "New tab", "chrome://newtab/", "New tab");
        expectFilteredUrlAndTitle("chrome://newtab", "New tab", "chrome://new-tab-page", "New Tab");
    }

    @Test
    public void testGetFilteredUrl_HttpHttpsChromeFile() {
        // HTTP / HTTPS URLs.
        expectFilteredUrlAndTitle("https://google.com", "Google", "https://google.com", "Google");
        expectFilteredUrlAndTitle("http://google.com", "Google", "http://google.com", "Google");

        // These URLs are not syncable.
        expectFilteredUrlAndTitle("chrome://newtab", "Unsavable tab", "ftp://foo.com", "Foo");
        expectFilteredUrlAndTitle(
                "chrome://newtab", "Unsavable tab", "chrome://flags", "Experiments");
        expectFilteredUrlAndTitle(
                "chrome://newtab", "Unsavable tab", "chrome-untrusted://foo", "Foo");
        expectFilteredUrlAndTitle("chrome://newtab", "Unsavable tab", "www.foo.com", "Foo");
        expectFilteredUrlAndTitle("chrome://newtab", "Unsavable tab", "file://sdcard/foo", "Foo");
    }

    private void expectFilteredUrlAndTitle(
            String filteredUrl, String filteredTitle, String inputUrl, String inputTitle) {
        Assert.assertEquals(
                "Failed expectation for " + inputUrl,
                new Pair<>(new GURL(filteredUrl), filteredTitle),
                TabGroupSyncUtils.getFilteredUrlAndTitle(new GURL(inputUrl), inputTitle));
    }

    private void createTabGroup(List<Tab> tabs, int rootId, Token tabGroupId) {
        for (Tab tab : tabs) {
            tab.setRootId(rootId);
            tab.setTabGroupId(tabGroupId);
        }
        when(mTabGroupModelFilter.getRelatedTabListForRootId(eq(rootId))).thenReturn(tabs);
        when(mTabGroupModelFilter.getRootIdFromStableId(eq(tabGroupId))).thenReturn(rootId);
        when(mTabGroupModelFilter.getStableIdFromRootId(eq(rootId))).thenReturn(tabGroupId);
    }
}