chromium/chrome/android/javatests/src/org/chromium/chrome/browser/sync/BookmarksTest.java

// Copyright 2015 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.sync;

import android.util.Pair;

import androidx.test.filters.LargeTest;

import com.google.protobuf.InvalidProtocolBufferException;

import org.hamcrest.Matchers;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaNotSatisfiedException;
import org.chromium.base.test.util.Feature;
import org.chromium.chrome.browser.bookmarks.BookmarkModel;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.util.browser.sync.SyncTestUtil;
import org.chromium.components.bookmarks.BookmarkId;
import org.chromium.components.sync.DataType;
import org.chromium.components.sync.UserSelectableType;
import org.chromium.components.sync.protocol.BookmarkSpecifics;
import org.chromium.components.sync.protocol.SyncEntity;
import org.chromium.url.GURL;

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

/** Test suite for the bookmarks sync data type. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class BookmarksTest {
    @Rule public SyncTestRule mSyncTestRule = new SyncTestRule();

    private static final String BOOKMARKS_TYPE_STRING = "Bookmarks";

    private static final GURL URL = new GURL("http://chromium.org/");
    private static final String TITLE = "Chromium";
    private static final String MODIFIED_TITLE = "Chromium2";
    private static final String FOLDER_TITLE = "Tech";

    private BookmarkModel mBookmarkModel;

    // A container to store bookmark information for data verification.
    private static class Bookmark {
        public final String id;
        public final String guid;
        public final String title;
        public final String url;
        public final String parentId;

        private Bookmark(
                String id,
                String guid,
                String title,
                String url,
                String parentId,
                String parentGuid) {
            this.id = id;
            this.guid = guid;
            this.title = title;
            this.url = url;
            this.parentId = parentId;
        }

        public boolean isFolder() {
            return url == null;
        }
    }

    @Before
    public void setUp() throws Exception {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mBookmarkModel =
                            BookmarkModel.getForProfile(ProfileManager.getLastUsedRegularProfile());
                    // The BookmarkModel needs to know how to handle partner bookmarks.
                    // Without this call to fake that knowledge for testing, it crashes.
                    mBookmarkModel.loadEmptyPartnerBookmarkShimForTesting();
                });
        mSyncTestRule.setUpAccountAndEnableSyncForTesting();
        // Make sure initial state is clean.
        assertClientBookmarkCount(0);
        assertServerBookmarkCountWithName(0, TITLE);
        assertServerBookmarkCountWithName(0, MODIFIED_TITLE);
        assertServerBookmarkCountWithName(0, FOLDER_TITLE);
    }

    // Test syncing a new bookmark from server to client.
    @Test
    @LargeTest
    @Feature({"Sync"})
    public void testDownloadBookmark() throws Exception {
        addServerBookmark(TITLE, URL);
        SyncTestUtil.triggerSync();
        waitForClientBookmarkCount(1);

        List<Bookmark> bookmarks = getClientBookmarks();
        Assert.assertEquals(
                "Only the injected bookmark should exist on the client.", 1, bookmarks.size());
        Bookmark bookmark = bookmarks.get(0);
        Assert.assertEquals("The wrong title was found for the bookmark.", TITLE, bookmark.title);
        Assert.assertEquals(
                "The wrong URL was found for the bookmark.", URL.getSpec(), bookmark.url);
    }

    // Test syncing a bookmark modification from server to client.
    @Test
    @LargeTest
    @Feature({"Sync"})
    public void testDownloadBookmarkModification() throws Exception {
        // Add the entity to test modifying.
        addServerBookmark(TITLE, URL);
        SyncTestUtil.triggerSync();
        waitForClientBookmarkCount(1);

        // Modify on server, sync, and verify the modification locally.
        Bookmark bookmark = getClientBookmarks().get(0);
        modifyServerBookmark(bookmark.id, bookmark.guid, MODIFIED_TITLE, URL);
        SyncTestUtil.triggerSync();
        mSyncTestRule.pollInstrumentationThread(
                () -> {
                    Bookmark modifiedBookmark = getClientBookmarks().get(0);
                    Criteria.checkThat(modifiedBookmark.title, Matchers.is(MODIFIED_TITLE));
                });
    }

    // Test syncing a bookmark tombstone from server to client.
    @Test
    @LargeTest
    @Feature({"Sync"})
    public void testDownloadBookmarkTombstone() throws Exception {
        // Add the entity to test deleting.
        addServerBookmark(TITLE, URL);
        SyncTestUtil.triggerSync();
        waitForClientBookmarkCount(1);

        // Delete on server, sync, and verify deleted locally.
        Bookmark bookmark = getClientBookmarks().get(0);
        mSyncTestRule.getFakeServerHelper().deleteEntity(bookmark.id);
        SyncTestUtil.triggerSync();
        waitForClientBookmarkCount(0);
    }

    // Test syncing a bookmark modification from server to client.
    @Test
    @LargeTest
    @Feature({"Sync"})
    public void testDownloadMovedBookmark() throws Exception {
        // Add the entity to test moving.
        addServerBookmark(TITLE, URL);
        SyncTestUtil.triggerSync();
        waitForClientBookmarkCount(1);

        // Add the folder to move to.
        addServerBookmarkFolder(FOLDER_TITLE);
        SyncTestUtil.triggerSync();
        waitForClientBookmarkCount(2);

        // We should have exactly two (2) bookmark items: one a folder and the
        // other a bookmark. We need to figure out which is which because the
        // order is not being explicitly set on creation.
        //
        // See http://crbug/642128 - Explicitly set order on bookmark creation
        // and verify the order here.
        List<Bookmark> clientBookmarks = getClientBookmarks();
        Assert.assertEquals(2, clientBookmarks.size());
        Bookmark item0 = clientBookmarks.get(0);
        Bookmark item1 = clientBookmarks.get(1);
        Assert.assertTrue(item0.isFolder() != item1.isFolder());
        final int bookmarkIndex = item0.isFolder() ? 1 : 0;
        final Bookmark folder = item0.isFolder() ? item0 : item1;
        final Bookmark bookmark = item0.isFolder() ? item1 : item0;

        // On the server, move the bookmark into the folder then sync, and
        // verify the move locally.
        mSyncTestRule
                .getFakeServerHelper()
                .modifyBookmarkEntity(
                        bookmark.id, bookmark.guid, TITLE, URL, folder.id, folder.guid);
        SyncTestUtil.triggerSync();
        mSyncTestRule.pollInstrumentationThread(
                () -> {
                    List<Bookmark> bookmarks = getClientBookmarks();
                    Bookmark modifiedBookmark = bookmarks.get(bookmarks.get(0).isFolder() ? 1 : 0);
                    // The "s" is prepended because the server adds one to the parentId.
                    Criteria.checkThat(modifiedBookmark.parentId, Matchers.is("s" + folder.id));
                });
    }

    // Test syncing a new bookmark folder from server to client.
    @Test
    @LargeTest
    @Feature({"Sync"})
    public void testDownloadBookmarkFolder() throws Exception {
        addServerBookmarkFolder(TITLE);
        SyncTestUtil.triggerSync();
        waitForClientBookmarkCount(1);

        List<Bookmark> bookmarks = getClientBookmarks();
        Assert.assertEquals(
                "Only the injected bookmark folder should exist on the client.",
                1,
                bookmarks.size());
        Bookmark folder = bookmarks.get(0);
        Assert.assertEquals("The wrong title was found for the folder.", TITLE, folder.title);
        Assert.assertEquals("Bookmark folders do not have a URL.", null, folder.url);
    }

    // Test syncing a bookmark folder modification from server to client.
    @Test
    @LargeTest
    @Feature({"Sync"})
    public void testDownloadBookmarkFolderModification() throws Exception {
        // Add the entity to test modifying.
        addServerBookmarkFolder(TITLE);
        SyncTestUtil.triggerSync();
        waitForClientBookmarkCount(1);

        // Modify on server, sync, and verify the modification locally.
        Bookmark folder = getClientBookmarks().get(0);
        Assert.assertTrue(folder.isFolder());
        modifyServerBookmarkFolder(folder.id, folder.guid, MODIFIED_TITLE);
        SyncTestUtil.triggerSync();

        mSyncTestRule.pollInstrumentationThread(
                () -> {
                    Bookmark modifiedFolder = getClientBookmarks().get(0);
                    Criteria.checkThat(modifiedFolder.isFolder(), Matchers.is(true));
                    Criteria.checkThat(modifiedFolder.title, Matchers.is(MODIFIED_TITLE));
                });
    }

    // Test syncing a bookmark folder tombstone from server to client.
    @Test
    @LargeTest
    @Feature({"Sync"})
    public void testDownloadBookmarkFolderTombstone() throws Exception {
        // Add the entity to test deleting.
        addServerBookmarkFolder(TITLE);
        assertServerBookmarkCountWithName(1, TITLE);
        SyncTestUtil.triggerSync();
        waitForClientBookmarkCount(1);

        // Delete on server, sync, and verify deleted locally.
        Bookmark folder = getClientBookmarks().get(0);
        Assert.assertTrue(folder.isFolder());

        mSyncTestRule.getFakeServerHelper().deleteEntity(folder.id);
        assertServerBookmarkCountWithName(0, TITLE);
        SyncTestUtil.triggerSync();
        waitForClientBookmarkCount(0);
    }

    // Test syncing a new bookmark from client to server.
    @Test
    @LargeTest
    @Feature({"Sync"})
    public void testUploadBookmark() {
        addClientBookmark(TITLE, URL);
        waitForClientBookmarkCount(1);
        waitForServerBookmarkCountWithName(1, TITLE);
    }

    // Test syncing a bookmark modification from client to server.
    @Test
    @LargeTest
    @Feature({"Sync"})
    public void testUploadBookmarkModification() {
        // Add the entity to test modifying.
        BookmarkId bookmarkId = addClientBookmark(TITLE, URL);
        waitForClientBookmarkCount(1);
        waitForServerBookmarkCountWithName(1, TITLE);

        setClientBookmarkTitle(bookmarkId, MODIFIED_TITLE);
        waitForServerBookmarkCountWithName(1, MODIFIED_TITLE);
        assertServerBookmarkCountWithName(0, TITLE);
    }

    // Test syncing a bookmark tombstone from client to server.
    @Test
    @LargeTest
    @Feature({"Sync"})
    public void testUploadBookmarkTombstone() {
        // Add the entity to test deleting.
        BookmarkId bookmarkId = addClientBookmark(TITLE, URL);
        waitForClientBookmarkCount(1);
        waitForServerBookmarkCountWithName(1, TITLE);

        deleteClientBookmark(bookmarkId);
        waitForClientBookmarkCount(0);
        waitForServerBookmarkCountWithName(0, TITLE);
        assertServerBookmarkCountWithName(0, MODIFIED_TITLE);
    }

    // Test syncing a bookmark modification from client to server.
    @Test
    @LargeTest
    @Feature({"Sync"})
    public void testUploadMovedBookmark() throws Exception {
        // Add the entity to test moving.
        BookmarkId bookmarkId = addClientBookmark(TITLE, URL);
        SyncTestUtil.triggerSync();
        waitForServerBookmarkCountWithName(1, TITLE);

        // Add the folder to move to.
        BookmarkId folderId = addClientBookmarkFolder(FOLDER_TITLE);
        SyncTestUtil.triggerSync();
        waitForServerBookmarkCountWithName(1, FOLDER_TITLE);

        List<Bookmark> bookmarks = getServerBookmarks();
        Assert.assertEquals("Wrong number of bookmarks.", 2, bookmarks.size());
        final Bookmark folder = bookmarks.get(bookmarks.get(0).isFolder() ? 0 : 1);

        // Move on client, sync, and verify the move on the server.
        moveClientBookmark(bookmarkId, folderId);
        SyncTestUtil.triggerSync();
        mSyncTestRule.pollInstrumentationThread(
                () -> {
                    List<Bookmark> serverBookmarks = getServerBookmarks();
                    Bookmark modifiedBookmark =
                            serverBookmarks.get(serverBookmarks.get(0).isFolder() ? 1 : 0);
                    Criteria.checkThat(modifiedBookmark.parentId, Matchers.is(folder.id));
                });
    }

    // Test syncing a new bookmark folder from client to server.
    @Test
    @LargeTest
    @Feature({"Sync"})
    public void testUploadBookmarkFolder() {
        addClientBookmarkFolder(TITLE);
        waitForClientBookmarkCount(1);
        waitForServerBookmarkCountWithName(1, TITLE);
    }

    // Test syncing a bookmark folder modification from client to server.
    @Test
    @LargeTest
    @Feature({"Sync"})
    public void testUploadBookmarkFolderModification() {
        // Add the entity to test modifying.
        BookmarkId bookmarkId = addClientBookmarkFolder(TITLE);
        waitForClientBookmarkCount(1);
        waitForServerBookmarkCountWithName(1, TITLE);

        setClientBookmarkTitle(bookmarkId, MODIFIED_TITLE);
        waitForServerBookmarkCountWithName(1, MODIFIED_TITLE);
        assertServerBookmarkCountWithName(0, TITLE);
    }

    // Test syncing a bookmark folder tombstone from client to server.
    @Test
    @LargeTest
    @Feature({"Sync"})
    public void testUploadBookmarkFolderTombstone() {
        // Add the entity to test deleting.
        BookmarkId bookmarkId = addClientBookmarkFolder(TITLE);
        waitForClientBookmarkCount(1);
        waitForServerBookmarkCountWithName(1, TITLE);

        deleteClientBookmark(bookmarkId);
        waitForClientBookmarkCount(0);
        waitForServerBookmarkCountWithName(0, TITLE);
    }

    // Test that bookmarks don't get downloaded if the data type is disabled.
    @Test
    @LargeTest
    @Feature({"Sync"})
    public void testDisabledNoDownloadBookmark() throws Exception {
        mSyncTestRule.disableDataType(UserSelectableType.BOOKMARKS);
        addServerBookmark(TITLE, URL);
        SyncTestUtil.triggerSyncAndWaitForCompletion();
        assertClientBookmarkCount(0);
    }

    // Test that bookmarks don't get uploaded if the data type is disabled.
    @Test
    @LargeTest
    @Feature({"Sync"})
    public void testDisabledNoUploadBookmark() {
        mSyncTestRule.disableDataType(UserSelectableType.BOOKMARKS);
        addClientBookmark(TITLE, URL);
        SyncTestUtil.triggerSyncAndWaitForCompletion();
        assertServerBookmarkCountWithName(0, TITLE);
    }

    private BookmarkId addClientBookmark(final String title, final GURL url) {
        BookmarkId id =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            BookmarkId parentId = mBookmarkModel.getMobileFolderId();
                            return mBookmarkModel.addBookmark(parentId, 0, title, url);
                        });
        Assert.assertNotNull("Failed to create bookmark.", id);
        return id;
    }

    private BookmarkId addClientBookmarkFolder(final String title) {
        BookmarkId id =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            BookmarkId parentId = mBookmarkModel.getMobileFolderId();
                            return mBookmarkModel.addFolder(parentId, 0, title);
                        });
        Assert.assertNotNull("Failed to create bookmark folder.", id);
        return id;
    }

    private String getBookmarkBarGuid() {
        return ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    return mBookmarkModel.getBookmarkGuidByIdForTesting(
                            mBookmarkModel.getDesktopFolderId());
                });
    }

    private void addServerBookmark(String title, GURL url) {
        mSyncTestRule
                .getFakeServerHelper()
                .injectBookmarkEntity(
                        title,
                        url,
                        mSyncTestRule.getFakeServerHelper().getBookmarkBarFolderId(),
                        getBookmarkBarGuid());
    }

    private void addServerBookmarkFolder(String title) {
        mSyncTestRule
                .getFakeServerHelper()
                .injectBookmarkFolderEntity(
                        title,
                        mSyncTestRule.getFakeServerHelper().getBookmarkBarFolderId(),
                        getBookmarkBarGuid());
    }

    private void modifyServerBookmark(
            String bookmarkId, String bookmarkGuid, String title, GURL url) {
        mSyncTestRule
                .getFakeServerHelper()
                .modifyBookmarkEntity(
                        bookmarkId,
                        bookmarkGuid,
                        title,
                        url,
                        mSyncTestRule.getFakeServerHelper().getBookmarkBarFolderId(),
                        getBookmarkBarGuid());
    }

    private void modifyServerBookmarkFolder(String folderId, String folderGuid, String title) {
        mSyncTestRule
                .getFakeServerHelper()
                .modifyBookmarkFolderEntity(
                        folderId,
                        folderGuid,
                        title,
                        mSyncTestRule.getFakeServerHelper().getBookmarkBarFolderId(),
                        getBookmarkBarGuid());
    }

    private void deleteClientBookmark(final BookmarkId id) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mBookmarkModel.deleteBookmark(id);
                });
    }

    private void setClientBookmarkTitle(final BookmarkId id, final String title) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mBookmarkModel.setBookmarkTitle(id, title);
                });
    }

    private void moveClientBookmark(final BookmarkId id, final BookmarkId newParentId) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mBookmarkModel.moveBookmark(id, newParentId, 0 /* new index */);
                });
    }

    private List<Bookmark> getClientBookmarks() {
        try {
            List<Pair<String, JSONObject>> rawBookmarks =
                    SyncTestUtil.getLocalData(
                            mSyncTestRule.getTargetContext(), BOOKMARKS_TYPE_STRING);
            List<Bookmark> bookmarks = new ArrayList<Bookmark>(rawBookmarks.size());
            for (Pair<String, JSONObject> rawBookmark : rawBookmarks) {
                String id = rawBookmark.first;
                JSONObject json = rawBookmark.second;
                bookmarks.add(
                        new Bookmark(
                                id,
                                json.getString("guid"),
                                json.getString("legacy_canonicalized_title"),
                                json.optString("url", null),
                                json.getString("parent_id"),
                                json.getString("parent_guid")));
            }
            return bookmarks;
        } catch (JSONException ex) {
            Assert.fail(ex.toString());
            return null;
        }
    }

    private List<Bookmark> getServerBookmarks() {
        try {
            List<SyncEntity> entities =
                    mSyncTestRule
                            .getFakeServerHelper()
                            .getSyncEntitiesByDataType(DataType.BOOKMARKS);
            List<Bookmark> bookmarks = new ArrayList<Bookmark>(entities.size());
            for (SyncEntity entity : entities) {
                String id = entity.getIdString();
                String parentId = entity.getParentIdString();
                BookmarkSpecifics specifics = entity.getSpecifics().getBookmark();
                bookmarks.add(
                        new Bookmark(
                                id,
                                specifics.getGuid(),
                                specifics.getLegacyCanonicalizedTitle(),
                                entity.getFolder() ? null : specifics.getUrl(),
                                parentId,
                                specifics.getParentGuid()));
            }
            return bookmarks;
        } catch (InvalidProtocolBufferException ex) {
            Assert.fail(ex.toString());
            return null;
        }
    }

    private void assertClientBookmarkCount(int count) throws JSONException {
        Assert.assertEquals(
                "There should be " + count + " local bookmarks.",
                count,
                SyncTestUtil.getLocalData(mSyncTestRule.getTargetContext(), BOOKMARKS_TYPE_STRING)
                        .size());
    }

    private void assertServerBookmarkCountWithName(int count, String name) {
        Assert.assertTrue(
                "There should be " + count + " remote bookmarks with name " + name + ".",
                mSyncTestRule
                        .getFakeServerHelper()
                        .verifyEntityCountByTypeAndName(count, DataType.BOOKMARKS, name));
    }

    private void waitForClientBookmarkCount(int n) {
        mSyncTestRule.pollInstrumentationThread(
                () -> {
                    try {
                        Criteria.checkThat(
                                SyncTestUtil.getLocalData(
                                                mSyncTestRule.getTargetContext(),
                                                BOOKMARKS_TYPE_STRING)
                                        .size(),
                                Matchers.is(n));
                    } catch (JSONException ex) {
                        throw new CriteriaNotSatisfiedException(ex);
                    }
                });
    }

    private void waitForServerBookmarkCountWithName(final int count, final String name) {
        mSyncTestRule.pollInstrumentationThread(
                () ->
                        mSyncTestRule
                                .getFakeServerHelper()
                                .verifyEntityCountByTypeAndName(count, DataType.BOOKMARKS, name),
                "Expected " + count + " remote bookmarks with name " + name + ".");
    }
}