summaryrefslogtreecommitdiff
path: root/chromium/components/sync_sessions/session_sync_bridge_unittest.cc
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/components/sync_sessions/session_sync_bridge_unittest.cc')
-rw-r--r--chromium/components/sync_sessions/session_sync_bridge_unittest.cc1429
1 files changed, 1429 insertions, 0 deletions
diff --git a/chromium/components/sync_sessions/session_sync_bridge_unittest.cc b/chromium/components/sync_sessions/session_sync_bridge_unittest.cc
new file mode 100644
index 00000000000..25902c79227
--- /dev/null
+++ b/chromium/components/sync_sessions/session_sync_bridge_unittest.cc
@@ -0,0 +1,1429 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/sync_sessions/session_sync_bridge.h"
+
+#include <map>
+#include <utility>
+#include <vector>
+
+#include "base/bind_helpers.h"
+#include "base/json/json_writer.h"
+#include "base/memory/weak_ptr.h"
+#include "base/message_loop/message_loop.h"
+#include "base/run_loop.h"
+#include "base/strings/stringprintf.h"
+#include "base/test/bind_test_util.h"
+#include "base/test/mock_callback.h"
+#include "components/sync/base/hash_util.h"
+#include "components/sync/base/sync_prefs.h"
+#include "components/sync/device_info/local_device_info_provider_mock.h"
+#include "components/sync/model/data_batch.h"
+#include "components/sync/model/metadata_batch.h"
+#include "components/sync/model/metadata_change_list.h"
+#include "components/sync/model/mock_model_type_change_processor.h"
+#include "components/sync/model/model_type_store_test_util.h"
+#include "components/sync/model/model_type_sync_bridge.h"
+#include "components/sync/model/sync_metadata_store.h"
+#include "components/sync/model_impl/client_tag_based_model_type_processor.h"
+#include "components/sync/protocol/proto_value_conversions.h"
+#include "components/sync/protocol/sync.pb.h"
+#include "components/sync/test/test_matchers.h"
+#include "components/sync_sessions/favicon_cache.h"
+#include "components/sync_sessions/mock_sync_sessions_client.h"
+#include "components/sync_sessions/tab_node_pool.h"
+#include "components/sync_sessions/test_matchers.h"
+#include "components/sync_sessions/test_synced_window_delegates_getter.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace sync_sessions {
+namespace {
+
+using sync_pb::EntityMetadata;
+using sync_pb::SessionSpecifics;
+using syncer::DataBatch;
+using syncer::EntityChangeList;
+using syncer::EntityData;
+using syncer::IsEmptyMetadataBatch;
+using syncer::MetadataBatch;
+using syncer::MetadataChangeList;
+using syncer::MockModelTypeChangeProcessor;
+using testing::_;
+using testing::Contains;
+using testing::ElementsAre;
+using testing::Eq;
+using testing::InSequence;
+using testing::IsEmpty;
+using testing::IsNull;
+using testing::Matcher;
+using testing::Not;
+using testing::NotNull;
+using testing::Pair;
+using testing::Pointee;
+using testing::Return;
+using testing::SaveArg;
+using testing::SizeIs;
+using testing::UnorderedElementsAre;
+using testing::WithArg;
+
+const char kLocalSessionTag[] = "sessiontag1";
+
+class MockSessionSyncPrefs : public syncer::SessionSyncPrefs {
+ public:
+ MockSessionSyncPrefs() = default;
+ ~MockSessionSyncPrefs() override = default;
+
+ MOCK_CONST_METHOD0(GetSyncSessionsGUID, std::string());
+ MOCK_METHOD1(SetSyncSessionsGUID, void(const std::string& guid));
+};
+
+MATCHER_P(EntityDataHasSpecifics, session_specifics_matcher, "") {
+ return session_specifics_matcher.MatchAndExplain(arg->specifics.session(),
+ result_listener);
+}
+
+syncer::EntityDataPtr SpecificsToEntity(
+ const sync_pb::SessionSpecifics& specifics,
+ base::Time mtime = base::Time::Now()) {
+ syncer::EntityData data;
+ data.client_tag_hash = syncer::GenerateSyncableHash(
+ syncer::SESSIONS, SessionStore::GetClientTag(specifics));
+ *data.specifics.mutable_session() = specifics;
+ data.modification_time = mtime;
+ return data.PassToPtr();
+}
+
+syncer::UpdateResponseData SpecificsToUpdateResponse(
+ const sync_pb::SessionSpecifics& specifics,
+ base::Time mtime = base::Time::Now()) {
+ syncer::UpdateResponseData data;
+ data.entity = SpecificsToEntity(specifics, mtime);
+ return data;
+}
+
+std::map<std::string, std::unique_ptr<EntityData>> BatchToEntityDataMap(
+ std::unique_ptr<DataBatch> batch) {
+ std::map<std::string, std::unique_ptr<EntityData>> storage_key_to_data;
+ while (batch && batch->HasNext()) {
+ storage_key_to_data.insert(batch->Next());
+ }
+ return storage_key_to_data;
+}
+
+syncer::UpdateResponseData CreateTombstone(const std::string& client_tag) {
+ EntityData tombstone;
+ tombstone.client_tag_hash =
+ syncer::GenerateSyncableHash(syncer::SESSIONS, client_tag);
+
+ syncer::UpdateResponseData data;
+ data.entity = tombstone.PassToPtr();
+ data.response_version = 2;
+ return data;
+}
+
+syncer::CommitResponseData CreateSuccessResponse(
+ const std::string& client_tag) {
+ syncer::CommitResponseData response;
+ response.client_tag_hash =
+ syncer::GenerateSyncableHash(syncer::SESSIONS, client_tag);
+ response.sequence_number = 1;
+ return response;
+}
+
+sync_pb::SessionSpecifics CreateHeaderSpecificsWithOneTab(
+ const std::string& session_tag,
+ int window_id,
+ int tab_id) {
+ sync_pb::SessionSpecifics specifics;
+ specifics.set_session_tag(session_tag);
+ specifics.mutable_header()->set_client_name("Some client name");
+ specifics.mutable_header()->set_device_type(
+ sync_pb::SyncEnums_DeviceType_TYPE_LINUX);
+ sync_pb::SessionWindow* window = specifics.mutable_header()->add_window();
+ window->set_browser_type(sync_pb::SessionWindow_BrowserType_TYPE_TABBED);
+ window->set_window_id(window_id);
+ window->add_tab(tab_id);
+ return specifics;
+}
+
+sync_pb::SessionSpecifics CreateTabSpecifics(const std::string& session_tag,
+ int window_id,
+ int tab_id,
+ int tab_node_id,
+ const std::string& url) {
+ sync_pb::SessionSpecifics specifics;
+ specifics.set_session_tag(session_tag);
+ specifics.set_tab_node_id(tab_node_id);
+ specifics.mutable_tab()->add_navigation()->set_virtual_url(url);
+ specifics.mutable_tab()->set_window_id(window_id);
+ specifics.mutable_tab()->set_tab_id(tab_id);
+ return specifics;
+}
+
+class SessionSyncBridgeTest : public ::testing::Test {
+ protected:
+ SessionSyncBridgeTest()
+ : store_(syncer::ModelTypeStoreTestUtil::CreateInMemoryStoreForTest(
+ syncer::SESSIONS)),
+ favicon_cache_(/*favicon_service=*/nullptr,
+ /*history_service=*/nullptr,
+ /*max_sync_favicon_limit=*/0) {
+ ON_CALL(mock_sync_sessions_client_, GetSyncedWindowDelegatesGetter())
+ .WillByDefault(Return(&window_getter_));
+ ON_CALL(mock_sync_sessions_client_, GetLocalSessionEventRouter())
+ .WillByDefault(Return(window_getter_.router()));
+ ON_CALL(mock_sync_prefs_, GetSyncSessionsGUID())
+ .WillByDefault(Return(kLocalSessionTag));
+
+ // Even if we use NiceMock, let's be strict about errors and let tests
+ // explicitly list them.
+ EXPECT_CALL(mock_processor_, ReportError(_)).Times(0);
+ }
+
+ ~SessionSyncBridgeTest() override {}
+
+ void InitializeBridge() {
+ real_processor_ =
+ std::make_unique<syncer::ClientTagBasedModelTypeProcessor>(
+ syncer::SESSIONS, /*dump_stack=*/base::DoNothing(),
+ /*commit_only=*/false);
+ mock_processor_.DelegateCallsByDefaultTo(real_processor_.get());
+ // Instantiate the bridge.
+ bridge_ = std::make_unique<SessionSyncBridge>(
+ &mock_sync_sessions_client_, &mock_sync_prefs_,
+ &mock_device_info_provider_,
+ /*store_factory=*/
+ syncer::ModelTypeStoreTestUtil::FactoryForForwardingStore(store_.get()),
+ mock_foreign_sessions_updated_callback_.Get(),
+ mock_processor_.CreateForwardingProcessor());
+ }
+
+ void ShutdownBridge() {
+ bridge_.reset();
+ // The mock is still delegating to |real_processor_|, so we reset it too.
+ ASSERT_TRUE(testing::Mock::VerifyAndClear(&mock_processor_));
+ real_processor_.reset();
+ }
+
+ void StartSyncing(const std::vector<SessionSpecifics>& remote_data = {}) {
+ // DeviceInfo is provided when sync is being enabled, which should lead to
+ // ModelReadyToSync().
+ mock_device_info_provider_.Initialize(std::make_unique<syncer::DeviceInfo>(
+ "cache_guid", "Wayne Gretzky's Hacking Box", "Chromium 10k",
+ "Chrome 10k", sync_pb::SyncEnums_DeviceType_TYPE_LINUX, "device_id"));
+
+ base::RunLoop loop;
+ real_processor_->OnSyncStarting(
+ /*error_handler=*/base::DoNothing(),
+ base::BindLambdaForTesting(
+ [&loop](std::unique_ptr<syncer::ActivationContext>) {
+ loop.Quit();
+ }));
+ loop.Run();
+
+ sync_pb::ModelTypeState state;
+ state.set_initial_sync_done(true);
+ syncer::UpdateResponseDataList initial_updates;
+ for (const SessionSpecifics& specifics : remote_data) {
+ initial_updates.push_back(SpecificsToUpdateResponse(specifics));
+ }
+ real_processor_->OnUpdateReceived(state, initial_updates);
+ }
+
+ std::map<std::string, std::unique_ptr<EntityData>> GetAllData() {
+ base::RunLoop loop;
+ std::unique_ptr<DataBatch> batch;
+ bridge_->GetAllData(base::BindLambdaForTesting(
+ [&loop, &batch](std::unique_ptr<DataBatch> input_batch) {
+ batch = std::move(input_batch);
+ loop.Quit();
+ }));
+ loop.Run();
+ EXPECT_NE(nullptr, batch);
+ return BatchToEntityDataMap(std::move(batch));
+ }
+
+ std::map<std::string, std::unique_ptr<EntityData>> GetData(
+ const std::vector<std::string>& storage_keys) {
+ base::RunLoop loop;
+ std::unique_ptr<DataBatch> batch;
+ bridge_->GetData(
+ storage_keys,
+ base::BindLambdaForTesting(
+ [&loop, &batch](std::unique_ptr<DataBatch> input_batch) {
+ batch = std::move(input_batch);
+ loop.Quit();
+ }));
+ loop.Run();
+ EXPECT_NE(nullptr, batch);
+ return BatchToEntityDataMap(std::move(batch));
+ }
+
+ std::unique_ptr<EntityData> GetData(const std::string& storage_key) {
+ std::map<std::string, std::unique_ptr<EntityData>> entity_data_map =
+ GetData(std::vector<std::string>{storage_key});
+ EXPECT_LE(entity_data_map.size(), 1U);
+ if (entity_data_map.empty()) {
+ return nullptr;
+ }
+ EXPECT_EQ(storage_key, entity_data_map.begin()->first);
+ return std::move(entity_data_map.begin()->second);
+ }
+
+ void ResetWindows() { window_getter_.ResetWindows(); }
+
+ TestSyncedWindowDelegate* AddWindow(
+ int window_id,
+ sync_pb::SessionWindow_BrowserType type =
+ sync_pb::SessionWindow_BrowserType_TYPE_TABBED) {
+ return window_getter_.AddWindow(type,
+ SessionID::FromSerializedValue(window_id));
+ }
+
+ TestSyncedTabDelegate* AddTab(int window_id,
+ const std::string& url,
+ int tab_id = SessionID::NewUnique().id()) {
+ TestSyncedTabDelegate* tab =
+ window_getter_.AddTab(SessionID::FromSerializedValue(window_id),
+ SessionID::FromSerializedValue(tab_id));
+ tab->Navigate(url, base::Time::Now());
+ return tab;
+ }
+
+ SessionSyncBridge* bridge() { return bridge_.get(); }
+
+ syncer::MockModelTypeChangeProcessor& mock_processor() {
+ return mock_processor_;
+ }
+
+ syncer::ClientTagBasedModelTypeProcessor* real_processor() {
+ return real_processor_.get();
+ }
+
+ base::MockCallback<base::RepeatingClosure>&
+ mock_foreign_sessions_updated_callback() {
+ return mock_foreign_sessions_updated_callback_;
+ }
+
+ private:
+ base::MessageLoop message_loop_;
+ const std::unique_ptr<syncer::ModelTypeStore> store_;
+
+ // Dependencies.
+ testing::NiceMock<MockSyncSessionsClient> mock_sync_sessions_client_;
+ testing::NiceMock<MockSessionSyncPrefs> mock_sync_prefs_;
+ syncer::LocalDeviceInfoProviderMock mock_device_info_provider_;
+ testing::NiceMock<MockModelTypeChangeProcessor> mock_processor_;
+ testing::NiceMock<base::MockCallback<base::RepeatingClosure>>
+ mock_foreign_sessions_updated_callback_;
+ TestSyncedWindowDelegatesGetter window_getter_;
+ FaviconCache favicon_cache_;
+
+ std::unique_ptr<SessionSyncBridge> bridge_;
+ std::unique_ptr<syncer::ClientTagBasedModelTypeProcessor> real_processor_;
+};
+
+TEST_F(SessionSyncBridgeTest, ShouldCallModelReadyToSyncWhenSyncEnabled) {
+ EXPECT_CALL(mock_processor(), ModelReadyToSync(_)).Times(0);
+ InitializeBridge();
+ EXPECT_CALL(mock_processor(), ModelReadyToSync(IsEmptyMetadataBatch()));
+ StartSyncing();
+}
+
+// Test that handling of local events (i.e. propagating the local state to
+// sync) does not start while a session restore is in progress.
+TEST_F(SessionSyncBridgeTest, ShouldDeferLocalEventDueToSessionRestore) {
+ const int kWindowId = 1000001;
+ const int kTabId1 = 1000002;
+ const int kTabId2 = 1000003;
+
+ // No notifications expected until OnSessionRestoreComplete().
+ EXPECT_CALL(mock_processor(), Put(_, _, _)).Times(0);
+
+ AddWindow(kWindowId)->SetIsSessionRestoreInProgress(true);
+ // Initial tab should be ignored (not exposed to processor) while session
+ // restore is in progress.
+ AddTab(kWindowId, "http://foo.com/", kTabId1);
+
+ InitializeBridge();
+ StartSyncing();
+ EXPECT_THAT(GetAllData(),
+ ElementsAre(Pair(
+ _, EntityDataHasSpecifics(MatchesHeader(kLocalSessionTag,
+ /*window_ids=*/{},
+ /*tab_ids=*/{})))));
+
+ // Create the actual tab, which should be ignored because session restore
+ // is in progress.
+ AddTab(kWindowId, "http://bar.com/", kTabId2);
+ EXPECT_THAT(GetAllData(), SizeIs(1));
+
+ // OnSessionRestoreComplete() should issue three Put() calls, one updating the
+ // header and one for each of the two added tabs.
+ EXPECT_CALL(mock_processor(), Put(_, _, _)).Times(3);
+ bridge()->OnSessionRestoreComplete();
+ EXPECT_THAT(GetAllData(), SizeIs(3));
+}
+
+TEST_F(SessionSyncBridgeTest, ShouldCreateHeaderByDefault) {
+ InitializeBridge();
+
+ EXPECT_CALL(mock_processor(), ModelReadyToSync(IsEmptyMetadataBatch()));
+ StartSyncing();
+
+ EXPECT_THAT(GetAllData(), SizeIs(1));
+}
+
+// Tests that local windows and tabs that exist at the time the bridge is
+// started (e.g. after a Chrome restart) are properly exposed via the bridge's
+// GetData() and GetAllData() methods, as well as notified via Put().
+TEST_F(SessionSyncBridgeTest, ShouldExposeInitialLocalTabsToProcessor) {
+ const int kWindowId = 1000001;
+ const int kTabId1 = 1000002;
+ const int kTabId2 = 1000003;
+
+ AddWindow(kWindowId);
+ AddTab(kWindowId, "http://foo.com/", kTabId1);
+ AddTab(kWindowId, "http://bar.com/", kTabId2);
+
+ InitializeBridge();
+
+ const std::string header_storage_key =
+ SessionStore::GetHeaderStorageKey(kLocalSessionTag);
+ const std::string tab_storage_key1 =
+ SessionStore::GetTabStorageKey(kLocalSessionTag, 0);
+ const std::string tab_storage_key2 =
+ SessionStore::GetTabStorageKey(kLocalSessionTag, 1);
+
+ EXPECT_CALL(mock_processor(),
+ Put(header_storage_key,
+ EntityDataHasSpecifics(MatchesHeader(
+ kLocalSessionTag, {kWindowId}, {kTabId1, kTabId2})),
+ _));
+ EXPECT_CALL(mock_processor(),
+ Put(tab_storage_key1,
+ EntityDataHasSpecifics(
+ MatchesTab(kLocalSessionTag, kWindowId, kTabId1,
+ /*tab_node_id=*/_, {"http://foo.com/"})),
+ _));
+ EXPECT_CALL(mock_processor(),
+ Put(tab_storage_key2,
+ EntityDataHasSpecifics(
+ MatchesTab(kLocalSessionTag, kWindowId, kTabId2,
+ /*tab_node_id=*/_, {"http://bar.com/"})),
+ _));
+
+ StartSyncing();
+
+ EXPECT_THAT(GetData(header_storage_key),
+ EntityDataHasSpecifics(MatchesHeader(
+ kLocalSessionTag, {kWindowId}, {kTabId1, kTabId2})));
+ EXPECT_THAT(
+ GetAllData(),
+ UnorderedElementsAre(
+ Pair(header_storage_key,
+ EntityDataHasSpecifics(MatchesHeader(
+ kLocalSessionTag, {kWindowId}, {kTabId1, kTabId2}))),
+ Pair(tab_storage_key1, EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, kWindowId, kTabId1,
+ /*tab_node_id=*/_, {"http://foo.com/"}))),
+ Pair(tab_storage_key2,
+ EntityDataHasSpecifics(
+ MatchesTab(kLocalSessionTag, kWindowId, kTabId2,
+ /*tab_node_id=*/_, {"http://bar.com/"})))));
+}
+
+// Tests that the creation of a new tab while sync is enabled is propagated to:
+// 1) The processor, via Put().
+// 2) The in-memory representation exposed via GetData().
+// 3) The persisted store, exposed via GetAllData().
+TEST_F(SessionSyncBridgeTest, ShouldReportLocalTabCreation) {
+ const int kWindowId = 1000001;
+ const int kTabId1 = 1000002;
+ const int kTabId2 = 1000003;
+
+ AddWindow(kWindowId);
+ AddTab(kWindowId, "http://foo.com/", kTabId1);
+
+ InitializeBridge();
+ StartSyncing();
+
+ ASSERT_THAT(GetAllData(), SizeIs(2));
+ EXPECT_CALL(mock_foreign_sessions_updated_callback(), Run()).Times(0);
+
+ // Expectations for the processor.
+ std::string header_storage_key;
+ std::string tab_storage_key;
+ // Tab creation triggers an update event due to the tab parented notification,
+ // so the event handler issues two commits as well (one for tab creation, one
+ // for tab update). During the first update, however, the tab is not syncable
+ // and is hence skipped.
+ testing::Expectation put_transient_header = EXPECT_CALL(
+ mock_processor(), Put(_,
+ EntityDataHasSpecifics(MatchesHeader(
+ kLocalSessionTag, {kWindowId}, {kTabId1})),
+ _));
+ EXPECT_CALL(mock_processor(),
+ Put(_,
+ EntityDataHasSpecifics(MatchesHeader(
+ kLocalSessionTag, {kWindowId}, {kTabId1, kTabId2})),
+ _))
+ .After(put_transient_header)
+ .WillOnce(WithArg<0>(SaveArg<0>(&header_storage_key)));
+ EXPECT_CALL(mock_processor(),
+ Put(_,
+ EntityDataHasSpecifics(
+ MatchesTab(kLocalSessionTag, kWindowId, kTabId2,
+ /*tab_node_id=*/_, {"http://bar.com/"})),
+ _))
+ .WillOnce(WithArg<0>(SaveArg<0>(&tab_storage_key)));
+
+ // Create the actual tab, now that we're syncing.
+ AddTab(kWindowId, "http://bar.com/", kTabId2);
+
+ ASSERT_THAT(header_storage_key,
+ Eq(SessionStore::GetHeaderStorageKey(kLocalSessionTag)));
+ ASSERT_THAT(tab_storage_key, Not(IsEmpty()));
+
+ // Verify the bridge's state exposed via the getters.
+ EXPECT_THAT(
+ GetAllData(),
+ UnorderedElementsAre(
+ Pair(header_storage_key,
+ EntityDataHasSpecifics(MatchesHeader(
+ kLocalSessionTag, {kWindowId}, {kTabId1, kTabId2}))),
+ Pair(_, EntityDataHasSpecifics(
+ MatchesTab(kLocalSessionTag, kWindowId, kTabId1,
+ /*tab_node_id=*/_, {"http://foo.com/"}))),
+ Pair(tab_storage_key, EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, kWindowId, kTabId2,
+ /*tab_node_id=*/_, {"http://bar.com/"})))));
+ EXPECT_THAT(GetData(header_storage_key),
+ EntityDataHasSpecifics(MatchesHeader(
+ kLocalSessionTag, {kWindowId}, {kTabId1, kTabId2})));
+ EXPECT_THAT(GetData(tab_storage_key),
+ EntityDataHasSpecifics(
+ MatchesTab(kLocalSessionTag, kWindowId, kTabId2,
+ /*tab_node_id=*/_, {"http://bar.com/"})));
+}
+
+TEST_F(SessionSyncBridgeTest, ShouldUpdateIdsDuringRestore) {
+ const int kWindowId = 1000001;
+ const int kTabIdBeforeRestore1 = 1000002;
+ const int kTabIdBeforeRestore2 = 1000003;
+ const int kTabIdAfterRestore1 = 1000004;
+ const int kTabIdAfterRestore2 = 1000005;
+ // Zero is the first assigned tab node ID.
+ const int kTabNodeId1 = 0;
+ const int kTabNodeId2 = 1;
+
+ AddWindow(kWindowId);
+ TestSyncedTabDelegate* tab1 =
+ AddTab(kWindowId, "http://foo.com/", kTabIdBeforeRestore1);
+ TestSyncedTabDelegate* tab2 =
+ AddTab(kWindowId, "http://bar.com/", kTabIdBeforeRestore2);
+
+ const std::string header_storage_key =
+ SessionStore::GetHeaderStorageKey(kLocalSessionTag);
+ const std::string tab_storage_key1 =
+ SessionStore::GetTabStorageKey(kLocalSessionTag, kTabNodeId1);
+ const std::string tab_storage_key2 =
+ SessionStore::GetTabStorageKey(kLocalSessionTag, kTabNodeId2);
+
+ InitializeBridge();
+ StartSyncing();
+
+ ASSERT_THAT(tab1->GetSyncId(), Eq(kTabNodeId1));
+ ASSERT_THAT(tab2->GetSyncId(), Eq(kTabNodeId2));
+
+ ASSERT_THAT(GetData(header_storage_key),
+ EntityDataHasSpecifics(
+ MatchesHeader(kLocalSessionTag, {kWindowId},
+ {kTabIdBeforeRestore1, kTabIdBeforeRestore2})));
+ ASSERT_THAT(GetData(tab_storage_key1),
+ EntityDataHasSpecifics(
+ MatchesTab(kLocalSessionTag, kWindowId, kTabIdBeforeRestore1,
+ kTabNodeId1, {"http://foo.com/"})));
+ ASSERT_THAT(GetData(tab_storage_key2),
+ EntityDataHasSpecifics(
+ MatchesTab(kLocalSessionTag, kWindowId, kTabIdBeforeRestore2,
+ kTabNodeId2, {"http://bar.com/"})));
+
+ ShutdownBridge();
+
+ // Override tabs with placeholder tab delegates.
+ PlaceholderTabDelegate placeholder_tab1(
+ SessionID::FromSerializedValue(kTabIdAfterRestore1), tab1->GetSyncId());
+ PlaceholderTabDelegate placeholder_tab2(
+ SessionID::FromSerializedValue(kTabIdAfterRestore2), tab2->GetSyncId());
+ ResetWindows();
+ TestSyncedWindowDelegate* window = AddWindow(kWindowId);
+ window->OverrideTabAt(0, &placeholder_tab1);
+ window->OverrideTabAt(1, &placeholder_tab2);
+
+ // When the bridge gets restarted, we expected tab IDs being updated, but the
+ // rest of the information such as navigation URLs should be reused.
+ EXPECT_CALL(mock_processor(),
+ Put(header_storage_key,
+ EntityDataHasSpecifics(MatchesHeader(
+ kLocalSessionTag, {kWindowId},
+ {kTabIdAfterRestore1, kTabIdAfterRestore2})),
+ _));
+ EXPECT_CALL(mock_processor(),
+ Put(tab_storage_key1,
+ EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, kWindowId, kTabIdAfterRestore1,
+ kTabNodeId1, {"http://foo.com/"})),
+ _));
+ EXPECT_CALL(mock_processor(),
+ Put(tab_storage_key2,
+ EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, kWindowId, kTabIdAfterRestore2,
+ kTabNodeId2, {"http://bar.com/"})),
+ _));
+
+ // Start the bridge again.
+ InitializeBridge();
+ StartSyncing();
+
+ EXPECT_THAT(placeholder_tab1.GetSyncId(), Eq(kTabNodeId1));
+ EXPECT_THAT(placeholder_tab2.GetSyncId(), Eq(kTabNodeId2));
+
+ EXPECT_THAT(GetData(header_storage_key),
+ EntityDataHasSpecifics(
+ MatchesHeader(kLocalSessionTag, {kWindowId},
+ {kTabIdAfterRestore1, kTabIdAfterRestore2})));
+ EXPECT_THAT(GetData(tab_storage_key1),
+ EntityDataHasSpecifics(
+ MatchesTab(kLocalSessionTag, kWindowId, kTabIdAfterRestore1,
+ kTabNodeId1, {"http://foo.com/"})));
+ EXPECT_THAT(GetData(tab_storage_key2),
+ EntityDataHasSpecifics(
+ MatchesTab(kLocalSessionTag, kWindowId, kTabIdAfterRestore2,
+ kTabNodeId2, {"http://bar.com/"})));
+
+ EXPECT_THAT(GetAllData(),
+ UnorderedElementsAre(
+ Pair(header_storage_key,
+ EntityDataHasSpecifics(MatchesHeader(
+ kLocalSessionTag, {kWindowId},
+ {kTabIdAfterRestore1, kTabIdAfterRestore2}))),
+ Pair(tab_storage_key1,
+ EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, kWindowId, kTabIdAfterRestore1,
+ kTabNodeId1, {"http://foo.com/"}))),
+ Pair(tab_storage_key2,
+ EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, kWindowId, kTabIdAfterRestore2,
+ kTabNodeId2, {"http://bar.com/"})))));
+}
+
+TEST_F(SessionSyncBridgeTest,
+ ShouldIgnoreUnsyncablePlaceholderTabDuringRestore) {
+ const int kWindowId = 1000001;
+ const int kTabIdBeforeRestore1 = 1000002;
+ const int kTabIdBeforeRestore2 = 1000003;
+ const int kTabIdAfterRestore1 = 1000004;
+ const int kTabIdAfterRestore2 = 1000005;
+ // Zero is the first assigned tab node ID.
+ const int kTabNodeId1 = 0;
+
+ AddWindow(kWindowId);
+ TestSyncedTabDelegate* tab1 =
+ AddTab(kWindowId, "http://foo.com/", kTabIdBeforeRestore1);
+ // Tab 2 is unsyncable because of the URL scheme.
+ TestSyncedTabDelegate* tab2 =
+ AddTab(kWindowId, "about:blank", kTabIdBeforeRestore2);
+
+ const std::string header_storage_key =
+ SessionStore::GetHeaderStorageKey(kLocalSessionTag);
+ const std::string tab_storage_key1 =
+ SessionStore::GetTabStorageKey(kLocalSessionTag, kTabNodeId1);
+
+ InitializeBridge();
+ StartSyncing();
+
+ ASSERT_THAT(tab1->GetSyncId(), Eq(kTabNodeId1));
+ ASSERT_THAT(tab2->GetSyncId(), Eq(TabNodePool::kInvalidTabNodeID));
+
+ ASSERT_THAT(GetAllData(),
+ UnorderedElementsAre(
+ Pair(header_storage_key, EntityDataHasSpecifics(MatchesHeader(
+ kLocalSessionTag, {kWindowId},
+ {kTabIdBeforeRestore1}))),
+ Pair(tab_storage_key1,
+ EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, kWindowId, kTabIdBeforeRestore1,
+ kTabNodeId1, {"http://foo.com/"})))));
+
+ ShutdownBridge();
+
+ // Override tabs with placeholder tab delegates.
+ PlaceholderTabDelegate placeholder_tab1(
+ SessionID::FromSerializedValue(kTabIdAfterRestore1), tab1->GetSyncId());
+ PlaceholderTabDelegate placeholder_tab2(
+ SessionID::FromSerializedValue(kTabIdAfterRestore2), tab2->GetSyncId());
+ ResetWindows();
+ TestSyncedWindowDelegate* window = AddWindow(kWindowId);
+ window->OverrideTabAt(0, &placeholder_tab1);
+ window->OverrideTabAt(1, &placeholder_tab2);
+
+ // Start the bridge again.
+ InitializeBridge();
+ StartSyncing();
+
+ EXPECT_THAT(placeholder_tab1.GetSyncId(), Eq(kTabNodeId1));
+ EXPECT_THAT(placeholder_tab2.GetSyncId(), Eq(TabNodePool::kInvalidTabNodeID));
+
+ EXPECT_THAT(GetAllData(),
+ UnorderedElementsAre(
+ Pair(header_storage_key, EntityDataHasSpecifics(MatchesHeader(
+ kLocalSessionTag, {kWindowId},
+ {kTabIdAfterRestore1}))),
+ Pair(tab_storage_key1,
+ EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, kWindowId, kTabIdAfterRestore1,
+ kTabNodeId1, {"http://foo.com/"})))));
+}
+
+// Ensure that tabbed windows from a previous session are preserved if no
+// windows are present on startup.
+TEST_F(SessionSyncBridgeTest, ShouldRestoreTabbedDataIfNoWindowsDuringStartup) {
+ const int kWindowId = 1000001;
+ const int kTabNodeId = 0;
+
+ AddWindow(kWindowId);
+ TestSyncedTabDelegate* tab = AddTab(kWindowId, "http://foo.com/");
+
+ const std::string header_storage_key =
+ SessionStore::GetHeaderStorageKey(kLocalSessionTag);
+ const std::string tab_storage_key =
+ SessionStore::GetTabStorageKey(kLocalSessionTag, kTabNodeId);
+
+ InitializeBridge();
+ StartSyncing();
+
+ ASSERT_THAT(
+ GetAllData(),
+ UnorderedElementsAre(
+ Pair(header_storage_key,
+ EntityDataHasSpecifics(MatchesHeader(kLocalSessionTag, _, _))),
+ Pair(tab_storage_key,
+ EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, _, _, kTabNodeId, {"http://foo.com/"})))));
+
+ ShutdownBridge();
+
+ // Start the bridge with no local windows/tabs.
+ ResetWindows();
+ InitializeBridge();
+ StartSyncing();
+
+ EXPECT_THAT(
+ GetAllData(),
+ UnorderedElementsAre(
+ Pair(header_storage_key,
+ EntityDataHasSpecifics(MatchesHeader(kLocalSessionTag, _, _))),
+ Pair(tab_storage_key,
+ EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, _, _, kTabNodeId, {"http://foo.com/"})))));
+
+ // Now actually resurrect the native data, which will end up having different
+ // native ids, but the tab has the same sync id as before.
+ EXPECT_CALL(
+ mock_processor(),
+ Put(header_storage_key,
+ EntityDataHasSpecifics(MatchesHeader(kLocalSessionTag, _, _)), _));
+ EXPECT_CALL(mock_processor(),
+ Put(tab_storage_key,
+ EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, /*window_id=*/_, /*tab_id=*/_,
+ kTabNodeId, {"http://foo.com/", "http://bar.com/"})),
+ _));
+ AddWindow(kWindowId)->OverrideTabAt(0, tab);
+ tab->Navigate("http://bar.com/");
+}
+
+// Ensure that tabbed windows from a previous session are preserved if only
+// a custom tab is present at startup.
+TEST_F(SessionSyncBridgeTest, ShouldPreserveTabbedDataIfCustomTabOnlyFound) {
+ const int kWindowId1 = 1000001;
+ const int kWindowId2 = 1000002;
+
+ AddWindow(kWindowId1);
+ AddTab(kWindowId1, "http://foo.com/");
+
+ InitializeBridge();
+ StartSyncing();
+
+ ASSERT_THAT(GetAllData(),
+ UnorderedElementsAre(
+ Pair(_, EntityDataHasSpecifics(
+ MatchesHeader(kLocalSessionTag, _, _))),
+ Pair(_, EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, _, _,
+ /*tab_node_id=*/0, {"http://foo.com/"})))));
+
+ ShutdownBridge();
+
+ // Start the bridge with only a custom tab open.
+ ResetWindows();
+ AddWindow(kWindowId2, sync_pb::SessionWindow_BrowserType_TYPE_CUSTOM_TAB);
+ AddTab(kWindowId2, "http://bar.com/");
+ InitializeBridge();
+ StartSyncing();
+
+ // The previous session should be preserved. The transient window cannot be
+ // synced because we do not have enough local data to ensure that we wouldn't
+ // vend the same sync ID if our persistent storage didn't match upon the last
+ // shutdown.
+ EXPECT_THAT(GetAllData(),
+ UnorderedElementsAre(
+ Pair(_, EntityDataHasSpecifics(
+ MatchesHeader(kLocalSessionTag, _, _))),
+ Pair(_, EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, _, _,
+ /*tab_node_id=*/0, {"http://foo.com/"})))));
+}
+
+// Ensure that tabbed windows from a previous session are preserved and combined
+// with a custom tab that was newly found during startup.
+TEST_F(SessionSyncBridgeTest, ShouldPreserveTabbedDataIfNewCustomTabAlsoFound) {
+ const int kWindowId1 = 1000001;
+ const int kWindowId2 = 1000002;
+ const int kTabId1 = 1000003;
+ const int kTabId2 = 1000004;
+
+ AddWindow(kWindowId1);
+ AddTab(kWindowId1, "http://foo.com/", kTabId1);
+
+ InitializeBridge();
+ StartSyncing();
+
+ ASSERT_THAT(GetAllData(),
+ UnorderedElementsAre(
+ Pair(_, EntityDataHasSpecifics(MatchesHeader(
+ kLocalSessionTag, {kWindowId1}, {kTabId1}))),
+ Pair(_, EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, kWindowId1, kTabId1,
+ /*tab_node_id=*/0, {"http://foo.com/"})))));
+
+ ShutdownBridge();
+
+ // Start the bridge with an additional local custom tab.
+ AddWindow(kWindowId2, sync_pb::SessionWindow_BrowserType_TYPE_CUSTOM_TAB);
+ AddTab(kWindowId2, "http://bar.com/", kTabId2);
+ InitializeBridge();
+ StartSyncing();
+
+ EXPECT_THAT(GetAllData(),
+ UnorderedElementsAre(
+ Pair(_, EntityDataHasSpecifics(MatchesHeader(
+ kLocalSessionTag, {kWindowId1, kWindowId2},
+ {kTabId1, kTabId2}))),
+ Pair(_, EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, kWindowId1, kTabId1,
+ /*tab_node_id=*/0, {"http://foo.com/"}))),
+ Pair(_, EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, kWindowId2, kTabId2,
+ /*tab_node_id=*/1, {"http://bar.com/"})))));
+}
+
+// Ensure that, in a scenario without prior sync data, encountering a custom
+// tab only ( no tabbed window) does not vend new sync IDs.
+TEST_F(SessionSyncBridgeTest, ShouldIgnoreIfCustomTabOnlyOnStartup) {
+ const int kWindowId = 1000001;
+
+ AddWindow(kWindowId, sync_pb::SessionWindow_BrowserType_TYPE_CUSTOM_TAB);
+ AddTab(kWindowId, "http://foo.com/");
+
+ InitializeBridge();
+ StartSyncing();
+
+ EXPECT_THAT(GetAllData(),
+ UnorderedElementsAre(Pair(_, EntityDataHasSpecifics(MatchesHeader(
+ kLocalSessionTag, _, _)))));
+}
+
+// Ensure that all tabs are exposed in a scenario where only a custom tab
+// (without tabbed windows) was present during startup, and later tabbed windows
+// appear (browser started).
+TEST_F(SessionSyncBridgeTest, ShouldExposeTabbedWindowAfterCustomTabOnly) {
+ const int kWindowId1 = 1000001;
+ const int kWindowId2 = 1000002;
+ const int kTabId1 = 1000003;
+ const int kTabId2 = 1000004;
+
+ AddWindow(kWindowId1, sync_pb::SessionWindow_BrowserType_TYPE_CUSTOM_TAB);
+ TestSyncedTabDelegate* custom_tab =
+ AddTab(kWindowId1, "http://foo.com/", kTabId1);
+
+ InitializeBridge();
+ StartSyncing();
+
+ ASSERT_THAT(GetAllData(),
+ UnorderedElementsAre(Pair(_, EntityDataHasSpecifics(MatchesHeader(
+ kLocalSessionTag, _, _)))));
+
+ // Load the actual tabbed window, now that we're syncing.
+ AddWindow(kWindowId2);
+ AddTab(kWindowId2, "http://bar.com/", kTabId2);
+
+ // The local change should be created and tracked correctly. This doesn't
+ // actually start syncing the custom tab yet, because the tab itself isn't
+ // associated yet.
+ EXPECT_THAT(GetAllData(),
+ UnorderedElementsAre(
+ Pair(_, EntityDataHasSpecifics(MatchesHeader(
+ kLocalSessionTag, {kWindowId2}, {kTabId2}))),
+ Pair(_, EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, kWindowId2, kTabId2,
+ /*tab_node_id=*/0, {"http://bar.com/"})))));
+
+ // Now trigger OnLocalTabModified() for the custom tab again, it should sync.
+ custom_tab->Navigate("http://baz.com/");
+ EXPECT_THAT(GetAllData(),
+ UnorderedElementsAre(
+ Pair(_, EntityDataHasSpecifics(MatchesHeader(
+ kLocalSessionTag, {kWindowId1, kWindowId2},
+ {kTabId1, kTabId2}))),
+ Pair(_, EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, kWindowId2, kTabId2,
+ /*tab_node_id=*/0, {"http://bar.com/"}))),
+ Pair(_, EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, kWindowId1, kTabId1,
+ /*tab_node_id=*/1,
+ {"http://foo.com/", "http://baz.com/"})))));
+}
+
+// Ensure that newly assigned tab node IDs do not conflict with IDs provided
+// by the delegate, for IDs the tracker might not know about. This is possible
+// for example if an Android client gets killed after Android's tab restore
+// has flushed the sync ID to disk, but before the sync database has been
+// written.
+TEST_F(SessionSyncBridgeTest, ShouldHonorUnknownSyncIdsFromDelegate) {
+ const int kWindowId = 1000001;
+ const int kTabNodeId = 0;
+
+ // In a previous run of a browser, we associated sync ID 0 to the second tab
+ // below, which the Android tab restore database succeeded to flush to disk.
+ // The sync_sessions counterpart (SessionStore) however didn't, because writes
+ // are not atomic.
+ AddWindow(kWindowId);
+ AddTab(kWindowId, "http://foo.com/");
+ AddTab(kWindowId, "http://bar.com/")->SetSyncId(kTabNodeId);
+
+ InitializeBridge();
+ StartSyncing();
+
+ EXPECT_THAT(
+ GetAllData(),
+ UnorderedElementsAre(
+ Pair(_,
+ EntityDataHasSpecifics(MatchesHeader(kLocalSessionTag, _, _))),
+ Pair(_,
+ EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, _, _, kTabNodeId, {"http://bar.com/"}))),
+ Pair(_, EntityDataHasSpecifics(MatchesTab(kLocalSessionTag, _, _,
+ /*tab_node_id=*/1,
+ {"http://foo.com/"})))));
+}
+
+// Ensure that unsyncable tabs are ignored even if the delegate reports a sync
+// ID (because the tab used to be syncable).
+TEST_F(SessionSyncBridgeTest,
+ ShouldHonorUnknownSyncIdsFromDelegateWithUnsyncableTab) {
+ const int kWindowId = 1000001;
+ const int kTabId = 1000002;
+ const int kTabNodeId = 0;
+
+ AddWindow(kWindowId);
+ AddTab(kWindowId, "about:blank", kTabId)->SetSyncId(kTabNodeId);
+
+ InitializeBridge();
+
+ EXPECT_CALL(mock_processor(),
+ Put(_,
+ EntityDataHasSpecifics(
+ MatchesHeader(kLocalSessionTag, {kWindowId}, {kTabId})),
+ _));
+
+ StartSyncing();
+
+ // As a regression test for crbug.com/846480, we verify that restarting the
+ // bridge without tabbed windows doesn't crash or issue more calls to Put().
+ // This is only problematic because, in the current implementation and as
+ // reflected in the assertion below, the unsyncable tab is still stored in
+ // the local model.
+ ASSERT_THAT(GetAllData(),
+ UnorderedElementsAre(
+ Pair(_, EntityDataHasSpecifics(MatchesHeader(
+ kLocalSessionTag, {kWindowId}, {kTabId}))),
+ Pair(_, EntityDataHasSpecifics(
+ MatchesTab(kLocalSessionTag, kWindowId, kTabId,
+ kTabNodeId, /*urls=*/{})))));
+
+ ShutdownBridge();
+ ResetWindows();
+ InitializeBridge();
+ StartSyncing();
+ EXPECT_THAT(
+ GetAllData(),
+ ElementsAre(Pair(_, EntityDataHasSpecifics(MatchesHeader(
+ kLocalSessionTag, /*window_ids=*/SizeIs(1),
+ /*tab_ids=*/SizeIs(1))))));
+}
+
+TEST_F(SessionSyncBridgeTest, ShouldDisableSyncAndReenable) {
+ const int kWindowId = 1000001;
+ const int kTabId = 1000002;
+
+ AddWindow(kWindowId);
+ AddTab(kWindowId, "http://foo.com/", kTabId);
+
+ InitializeBridge();
+ StartSyncing();
+
+ const std::string header_storage_key =
+ SessionStore::GetHeaderStorageKey(kLocalSessionTag);
+ ASSERT_THAT(GetData(header_storage_key),
+ EntityDataHasSpecifics(
+ MatchesHeader(kLocalSessionTag, {kWindowId}, {kTabId})));
+ ASSERT_THAT(GetAllData(), Not(IsEmpty()));
+
+ EXPECT_CALL(mock_processor(), ModelReadyToSync(_)).Times(0);
+ real_processor()->DisableSync();
+
+ EXPECT_CALL(mock_processor(), ModelReadyToSync(IsEmptyMetadataBatch()));
+ StartSyncing();
+ ASSERT_THAT(GetData(header_storage_key),
+ EntityDataHasSpecifics(
+ MatchesHeader(kLocalSessionTag, {kWindowId}, {kTabId})));
+}
+
+// Starting sync with no local data should just store the foreign entities in
+// the store and expose them via OpenTabsUIDelegate.
+TEST_F(SessionSyncBridgeTest, ShouldMergeForeignSession) {
+ const std::string kForeignSessionTag = "foreignsessiontag";
+ const int kForeignWindowId = 2000001;
+ const int kForeignTabId = 2000002;
+ const int kForeignTabNodeId = 2003;
+
+ EXPECT_CALL(mock_processor(), UpdateStorageKey(_, _, _)).Times(0);
+ EXPECT_CALL(mock_processor(), Put(_, _, _)).Times(0);
+ InitializeBridge();
+
+ const sync_pb::SessionSpecifics foreign_header =
+ CreateHeaderSpecificsWithOneTab(kForeignSessionTag, kForeignWindowId,
+ kForeignTabId);
+ const sync_pb::SessionSpecifics foreign_tab =
+ CreateTabSpecifics(kForeignSessionTag, kForeignWindowId, kForeignTabId,
+ kForeignTabNodeId, "http://baz.com/");
+
+ EXPECT_CALL(
+ mock_processor(),
+ Put(_, EntityDataHasSpecifics(MatchesHeader(kLocalSessionTag, _, _)), _));
+ EXPECT_CALL(mock_foreign_sessions_updated_callback(), Run());
+
+ StartSyncing({foreign_header, foreign_tab});
+
+ std::vector<const SyncedSession*> foreign_sessions;
+ EXPECT_TRUE(bridge()->GetOpenTabsUIDelegate()->GetAllForeignSessions(
+ &foreign_sessions));
+ EXPECT_THAT(foreign_sessions,
+ ElementsAre(MatchesSyncedSession(
+ kForeignSessionTag,
+ {{kForeignWindowId, std::vector<int>{kForeignTabId}}})));
+}
+
+TEST_F(SessionSyncBridgeTest, ShouldNotExposeForeignHeaderWithoutTabs) {
+ const std::string kForeignSessionTag = "foreignsessiontag";
+ const int kForeignWindowId = 2000001;
+ const int kForeignTabId = 2000002;
+
+ EXPECT_CALL(mock_processor(), UpdateStorageKey(_, _, _)).Times(0);
+ EXPECT_CALL(mock_processor(), Put(_, _, _)).Times(0);
+ InitializeBridge();
+
+ const sync_pb::SessionSpecifics foreign_header =
+ CreateHeaderSpecificsWithOneTab(kForeignSessionTag, kForeignWindowId,
+ kForeignTabId);
+ const std::string foreign_header_storage_key =
+ SessionStore::GetHeaderStorageKey(kForeignSessionTag);
+
+ EXPECT_CALL(
+ mock_processor(),
+ Put(_, EntityDataHasSpecifics(MatchesHeader(kLocalSessionTag, _, _)), _));
+
+ StartSyncing({foreign_header});
+ ASSERT_THAT(GetData(foreign_header_storage_key), NotNull());
+
+ std::vector<const SyncedSession*> foreign_sessions;
+ EXPECT_FALSE(bridge()->GetOpenTabsUIDelegate()->GetAllForeignSessions(
+ &foreign_sessions));
+
+ // Restart bridge to verify the state doesn't change.
+ ShutdownBridge();
+ InitializeBridge();
+ StartSyncing();
+ ASSERT_THAT(GetData(foreign_header_storage_key), NotNull());
+
+ EXPECT_FALSE(bridge()->GetOpenTabsUIDelegate()->GetAllForeignSessions(
+ &foreign_sessions));
+}
+
+// Regression test for crbug.com/837517: Ensure that the bridge doesn't crash
+// and closed foreign tabs (|kForeignTabId2| in the test) are not exposed after
+// restarting the browser.
+TEST_F(SessionSyncBridgeTest, ShouldNotExposeClosedTabsAfterRestart) {
+ const std::string kForeignSessionTag = "foreignsessiontag";
+ const int kForeignWindowId = 2000001;
+ const int kForeignTabId1 = 2000002;
+ const int kForeignTabId2 = 2000003;
+ const int kForeignTabNodeId1 = 2004;
+ const int kForeignTabNodeId2 = 2005;
+
+ // The header only lists a single tab |kForeignTabId1|, which becomes a mapped
+ // tab.
+ const sync_pb::SessionSpecifics foreign_header =
+ CreateHeaderSpecificsWithOneTab(kForeignSessionTag, kForeignWindowId,
+ kForeignTabId1);
+ const sync_pb::SessionSpecifics foreign_tab1 =
+ CreateTabSpecifics(kForeignSessionTag, kForeignWindowId, kForeignTabId1,
+ kForeignTabNodeId1, "http://foo.com/");
+ // |kForeignTabId2| is not present in the header, leading to an unmapped tab.
+ const sync_pb::SessionSpecifics foreign_tab2 =
+ CreateTabSpecifics(kForeignSessionTag, kForeignWindowId, kForeignTabId2,
+ kForeignTabNodeId2, "http://bar.com/");
+
+ InitializeBridge();
+ StartSyncing({foreign_header, foreign_tab1, foreign_tab2});
+
+ const std::string local_header_storage_key =
+ SessionStore::GetHeaderStorageKey(kLocalSessionTag);
+ const std::string foreign_header_storage_key =
+ SessionStore::GetHeaderStorageKey(kForeignSessionTag);
+ const std::string foreign_tab_storage_key1 =
+ SessionStore::GetTabStorageKey(kForeignSessionTag, kForeignTabNodeId1);
+ const std::string foreign_tab_storage_key2 =
+ SessionStore::GetTabStorageKey(kForeignSessionTag, kForeignTabNodeId2);
+
+ ASSERT_THAT(
+ GetAllData(),
+ UnorderedElementsAre(
+ Pair(local_header_storage_key, _),
+ Pair(foreign_header_storage_key,
+ EntityDataHasSpecifics(MatchesHeader(
+ kForeignSessionTag, {kForeignWindowId}, {kForeignTabId1}))),
+ Pair(foreign_tab_storage_key1,
+ EntityDataHasSpecifics(MatchesTab(
+ kForeignSessionTag, kForeignWindowId, kForeignTabId1,
+ kForeignTabNodeId1, {"http://foo.com/"}))),
+ Pair(foreign_tab_storage_key2,
+ EntityDataHasSpecifics(MatchesTab(
+ kForeignSessionTag, kForeignWindowId, kForeignTabId2,
+ kForeignTabNodeId2, {"http://bar.com/"})))));
+
+ // Mimic a browser restart, which should restore the very same state (and not
+ // crash!).
+ ShutdownBridge();
+ InitializeBridge();
+ StartSyncing();
+
+ EXPECT_THAT(GetAllData(),
+ UnorderedElementsAre(Pair(local_header_storage_key, _),
+ Pair(foreign_header_storage_key, _),
+ Pair(foreign_tab_storage_key1, _),
+ Pair(foreign_tab_storage_key2, _)));
+}
+
+TEST_F(SessionSyncBridgeTest, ShouldHandleRemoteDeletion) {
+ const std::string kForeignSessionTag = "foreignsessiontag";
+ const int kForeignWindowId = 2000001;
+ const int kForeignTabId = 2000002;
+ const int kForeignTabNodeId = 2003;
+
+ InitializeBridge();
+
+ const sync_pb::SessionSpecifics foreign_header =
+ CreateHeaderSpecificsWithOneTab(kForeignSessionTag, kForeignWindowId,
+ kForeignTabId);
+ const sync_pb::SessionSpecifics foreign_tab =
+ CreateTabSpecifics(kForeignSessionTag, kForeignWindowId, kForeignTabId,
+ kForeignTabNodeId, "http://baz.com/");
+ StartSyncing({foreign_header, foreign_tab});
+
+ const sessions::SessionTab* foreign_session_tab = nullptr;
+ ASSERT_TRUE(bridge()->GetOpenTabsUIDelegate()->GetForeignTab(
+ kForeignSessionTag, SessionID::FromSerializedValue(kForeignTabId),
+ &foreign_session_tab));
+ ASSERT_THAT(foreign_session_tab, NotNull());
+ std::vector<const SyncedSession*> foreign_sessions;
+ ASSERT_TRUE(bridge()->GetOpenTabsUIDelegate()->GetAllForeignSessions(
+ &foreign_sessions));
+ ASSERT_THAT(foreign_sessions,
+ ElementsAre(MatchesSyncedSession(
+ kForeignSessionTag,
+ {{kForeignWindowId, std::vector<int>{kForeignTabId}}})));
+ ASSERT_TRUE(real_processor()->IsTrackingMetadata());
+
+ // Mimic receiving a remote deletion of the foreign session.
+ sync_pb::ModelTypeState state;
+ state.set_initial_sync_done(true);
+
+ EXPECT_CALL(mock_foreign_sessions_updated_callback(), Run());
+ real_processor()->OnUpdateReceived(
+ state, {CreateTombstone(SessionStore::GetClientTag(foreign_header))});
+
+ foreign_session_tab = nullptr;
+ EXPECT_FALSE(bridge()->GetOpenTabsUIDelegate()->GetForeignTab(
+ kForeignSessionTag, SessionID::FromSerializedValue(kForeignTabId),
+ &foreign_session_tab));
+
+ EXPECT_FALSE(bridge()->GetOpenTabsUIDelegate()->GetAllForeignSessions(
+ &foreign_sessions));
+}
+
+TEST_F(SessionSyncBridgeTest, ShouldIgnoreRemoteDeletionOfLocalTab) {
+ const int kWindowId1 = 1000001;
+ const int kTabId1 = 1000002;
+ const int kTabNodeId1 = 0;
+
+ AddWindow(kWindowId1);
+ AddTab(kWindowId1, "http://foo.com/", kTabId1);
+
+ InitializeBridge();
+ StartSyncing();
+
+ const std::string header_storage_key =
+ SessionStore::GetHeaderStorageKey(kLocalSessionTag);
+ const std::string tab_storage_key1 =
+ SessionStore::GetTabStorageKey(kLocalSessionTag, kTabNodeId1);
+ const std::string tab_client_tag1 =
+ SessionStore::GetTabClientTagForTest(kLocalSessionTag, kTabNodeId1);
+
+ ASSERT_THAT(
+ GetAllData(),
+ UnorderedElementsAre(
+ Pair(header_storage_key,
+ EntityDataHasSpecifics(MatchesHeader(kLocalSessionTag, _, _))),
+ Pair(tab_storage_key1, EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, kWindowId1, kTabId1,
+ kTabNodeId1, {"http://foo.com/"})))));
+ ASSERT_TRUE(real_processor()->IsTrackingMetadata());
+ ASSERT_TRUE(real_processor()->HasLocalChangesForTest());
+
+ // Mimic receiving a commit ack for both the tab and the header entity,
+ // because otherwise it will be treated as conflict, and then local wins.
+ sync_pb::ModelTypeState state;
+ state.set_initial_sync_done(true);
+ real_processor()->OnCommitCompleted(
+ state, {CreateSuccessResponse(tab_client_tag1),
+ CreateSuccessResponse(kLocalSessionTag)});
+ ASSERT_FALSE(real_processor()->HasLocalChangesForTest());
+
+ // Mimic receiving a remote deletion of both entities.
+ EXPECT_CALL(mock_processor(), Put(_, _, _)).Times(0);
+ real_processor()->OnUpdateReceived(state, {CreateTombstone(kLocalSessionTag),
+ CreateTombstone(tab_client_tag1)});
+
+ // State should remain unchanged (deletions ignored).
+ EXPECT_THAT(
+ GetAllData(),
+ UnorderedElementsAre(
+ Pair(header_storage_key,
+ EntityDataHasSpecifics(MatchesHeader(kLocalSessionTag, _, _))),
+ Pair(tab_storage_key1, EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, kWindowId1, kTabId1,
+ kTabNodeId1, {"http://foo.com/"})))));
+
+ // Creating a new tab locally should trigger Put() calls for *all* entities
+ // (because the local data was out of sync).
+ const int kWindowId2 = 2000001;
+ const int kTabId2 = 2000002;
+ const int kTabNodeId2 = 1;
+
+ const std::string tab_storage_key2 =
+ SessionStore::GetTabStorageKey(kLocalSessionTag, kTabNodeId2);
+
+ // Window creation already triggers a header update, which will be overriden
+ // later below.
+ testing::Expectation put_transient_header =
+ EXPECT_CALL(mock_processor(), Put(header_storage_key, _, _));
+ AddWindow(kWindowId2);
+
+ // In the current implementation, some of the updates are reported to the
+ // processor twice, but that's OK because the processor can detect it.
+ EXPECT_CALL(mock_processor(),
+ Put(header_storage_key,
+ EntityDataHasSpecifics(MatchesHeader(
+ kLocalSessionTag, ElementsAre(kWindowId1, kWindowId2),
+ ElementsAre(kTabId1, kTabId2))),
+ _))
+ .Times(2)
+ .After(put_transient_header);
+ EXPECT_CALL(mock_processor(), Put(tab_storage_key1,
+ EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, kWindowId1, kTabId1,
+ kTabNodeId1, {"http://foo.com/"})),
+ _));
+ EXPECT_CALL(mock_processor(), Put(tab_storage_key2,
+ EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, kWindowId2, kTabId2,
+ kTabNodeId2, {"http://bar.com/"})),
+ _))
+ .Times(2);
+
+ AddTab(kWindowId2, "http://bar.com/", kTabId2);
+
+ EXPECT_THAT(
+ GetAllData(),
+ UnorderedElementsAre(
+ Pair(header_storage_key,
+ EntityDataHasSpecifics(MatchesHeader(
+ kLocalSessionTag, ElementsAre(kWindowId1, kWindowId2),
+ ElementsAre(kTabId1, kTabId2)))),
+ Pair(tab_storage_key1,
+ EntityDataHasSpecifics(
+ MatchesTab(kLocalSessionTag, /*window_id=*/_, /*tab_id=*/_,
+ kTabNodeId1, {"http://foo.com/"}))),
+ Pair(tab_storage_key2, EntityDataHasSpecifics(MatchesTab(
+ kLocalSessionTag, kWindowId2, kTabId2,
+ kTabNodeId2, {"http://bar.com/"})))));
+
+ // Run until idle because PostTask() is used to invoke ResubmitLocalSession().
+ base::RunLoop().RunUntilIdle();
+}
+
+// Verifies that a foreign session can be deleted by the user from the history
+// UI (via OpenTabsUIDelegate).
+TEST_F(SessionSyncBridgeTest, ShouldDeleteForeignSessionFromUI) {
+ const std::string kForeignSessionTag = "foreignsessiontag";
+ const int kForeignWindowId = 2000001;
+ const int kForeignTabId = 2000002;
+ const int kForeignTabNodeId = 2003;
+
+ InitializeBridge();
+
+ const sync_pb::SessionSpecifics foreign_header =
+ CreateHeaderSpecificsWithOneTab(kForeignSessionTag, kForeignWindowId,
+ kForeignTabId);
+ const sync_pb::SessionSpecifics foreign_tab =
+ CreateTabSpecifics(kForeignSessionTag, kForeignWindowId, kForeignTabId,
+ kForeignTabNodeId, "http://baz.com/");
+ StartSyncing({foreign_header, foreign_tab});
+
+ const std::string foreign_header_storage_key =
+ SessionStore::GetHeaderStorageKey(kForeignSessionTag);
+ const std::string foreign_tab_storage_key =
+ SessionStore::GetTabStorageKey(kForeignSessionTag, kForeignTabNodeId);
+
+ // Test fixture expects the two foreign entities in the model as well as the
+ // underlying store.
+ ASSERT_THAT(GetData(foreign_header_storage_key), NotNull());
+ ASSERT_THAT(GetData(foreign_tab_storage_key), NotNull());
+
+ const sessions::SessionTab* foreign_session_tab = nullptr;
+ ASSERT_TRUE(bridge()->GetOpenTabsUIDelegate()->GetForeignTab(
+ kForeignSessionTag, SessionID::FromSerializedValue(kForeignTabId),
+ &foreign_session_tab));
+ ASSERT_THAT(foreign_session_tab, NotNull());
+ std::vector<const SyncedSession*> foreign_sessions;
+ ASSERT_TRUE(bridge()->GetOpenTabsUIDelegate()->GetAllForeignSessions(
+ &foreign_sessions));
+ ASSERT_THAT(foreign_sessions,
+ ElementsAre(MatchesSyncedSession(
+ kForeignSessionTag,
+ {{kForeignWindowId, std::vector<int>{kForeignTabId}}})));
+ ASSERT_TRUE(real_processor()->IsTrackingMetadata());
+
+ // Mimic the user requesting a session deletion from the UI.
+ EXPECT_CALL(mock_processor(), Delete(foreign_header_storage_key, _));
+ EXPECT_CALL(mock_processor(), Delete(foreign_tab_storage_key, _));
+ EXPECT_CALL(mock_foreign_sessions_updated_callback(), Run());
+ bridge()->GetOpenTabsUIDelegate()->DeleteForeignSession(kForeignSessionTag);
+
+ // Verify what gets exposed to the UI.
+ foreign_session_tab = nullptr;
+ EXPECT_FALSE(bridge()->GetOpenTabsUIDelegate()->GetForeignTab(
+ kForeignSessionTag, SessionID::FromSerializedValue(kForeignTabId),
+ &foreign_session_tab));
+ EXPECT_FALSE(bridge()->GetOpenTabsUIDelegate()->GetAllForeignSessions(
+ &foreign_sessions));
+
+ // Verify store.
+ EXPECT_THAT(GetData(foreign_header_storage_key), IsNull());
+ EXPECT_THAT(GetData(foreign_tab_storage_key), IsNull());
+}
+
+// Verifies that attempts to delete the local session from the UI are ignored,
+// although the UI sholdn't really be offering that option.
+TEST_F(SessionSyncBridgeTest, ShouldIgnoreLocalSessionDeletionFromUI) {
+ InitializeBridge();
+ StartSyncing();
+
+ EXPECT_CALL(mock_foreign_sessions_updated_callback(), Run()).Times(0);
+ EXPECT_CALL(mock_processor(), Delete(_, _)).Times(0);
+
+ bridge()->GetOpenTabsUIDelegate()->DeleteForeignSession(kLocalSessionTag);
+
+ const SyncedSession* session = nullptr;
+ EXPECT_TRUE(bridge()->GetOpenTabsUIDelegate()->GetLocalSession(&session));
+ EXPECT_THAT(session, NotNull());
+ EXPECT_THAT(GetData(SessionStore::GetHeaderStorageKey(kLocalSessionTag)),
+ NotNull());
+}
+
+TEST_F(SessionSyncBridgeTest, ShouldDoGarbageCollection) {
+ // We construct two identical sessions, one modified recently, one modified
+ // more than |kStaleSessionThreshold| ago (14 days ago).
+ const base::Time stale_mtime =
+ base::Time::Now() - base::TimeDelta::FromDays(15);
+ const base::Time recent_mtime =
+ base::Time::Now() - base::TimeDelta::FromDays(13);
+ const std::string kStaleSessionTag = "stalesessiontag";
+ const std::string kRecentSessionTag = "recentsessiontag";
+ const int kWindowId = 2000001;
+ const int kTabId = 2000002;
+ const int kTabNodeId = 2003;
+
+ InitializeBridge();
+ StartSyncing();
+
+ // Construct a remote update.
+ sync_pb::ModelTypeState state;
+ state.set_initial_sync_done(true);
+ syncer::UpdateResponseDataList updates;
+ // Two entities belong to a recent session.
+ updates.push_back(SpecificsToUpdateResponse(
+ CreateHeaderSpecificsWithOneTab(kStaleSessionTag, kWindowId, kTabId),
+ stale_mtime));
+ updates.push_back(SpecificsToUpdateResponse(
+ CreateTabSpecifics(kStaleSessionTag, kWindowId, kTabId, kTabNodeId,
+ "http://baz.com/"),
+ stale_mtime));
+ updates.push_back(SpecificsToUpdateResponse(
+ CreateHeaderSpecificsWithOneTab(kRecentSessionTag, kWindowId, kTabId),
+ recent_mtime));
+ updates.push_back(SpecificsToUpdateResponse(
+ CreateTabSpecifics(kRecentSessionTag, kWindowId, kTabId, kTabNodeId,
+ "http://baz.com/"),
+ recent_mtime));
+ real_processor()->OnUpdateReceived(state, updates);
+
+ // During garbage collection, we expect |kStaleSessionTag| to be deleted.
+ EXPECT_CALL(mock_processor(),
+ Delete(SessionStore::GetHeaderStorageKey(kStaleSessionTag), _));
+ EXPECT_CALL(
+ mock_processor(),
+ Delete(SessionStore::GetTabStorageKey(kStaleSessionTag, kTabNodeId), _));
+ EXPECT_CALL(mock_foreign_sessions_updated_callback(), Run());
+
+ bridge()->ScheduleGarbageCollection();
+ base::RunLoop().RunUntilIdle();
+}
+
+} // namespace
+} // namespace sync_sessions