// 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 #include #include #include "base/callback_helpers.h" #include "base/run_loop.h" #include "base/test/bind.h" #include "base/test/mock_callback.h" #include "base/test/task_environment.h" #include "components/prefs/testing_pref_service.h" #include "components/sync/base/client_tag_hash.h" #include "components/sync/base/sync_prefs.h" #include "components/sync/engine/commit_and_get_updates_types.h" #include "components/sync/engine/data_type_activation_response.h" #include "components/sync/model/client_tag_based_model_type_processor.h" #include "components/sync/model/data_batch.h" #include "components/sync/model/data_type_activation_request.h" #include "components/sync/model/metadata_batch.h" #include "components/sync/model/metadata_change_list.h" #include "components/sync/model/model_type_sync_bridge.h" #include "components/sync/protocol/entity_metadata.pb.h" #include "components/sync/protocol/model_type_state.pb.h" #include "components/sync/protocol/session_specifics.pb.h" #include "components/sync/protocol/sync_enums.pb.h" #include "components/sync/test/mock_model_type_change_processor.h" #include "components/sync/test/model_type_store_test_util.h" #include "components/sync/test/test_matchers.h" #include "components/sync_sessions/mock_sync_sessions_client.h" #include "components/sync_sessions/session_sync_prefs.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::SessionSpecifics; using syncer::CommitResponseDataList; using syncer::DataBatch; using syncer::EntityChangeList; using syncer::EntityData; using syncer::FailedCommitResponseDataList; using syncer::IsEmptyMetadataBatch; using syncer::MetadataBatch; using syncer::MockModelTypeChangeProcessor; using testing::_; using testing::AtLeast; using testing::ElementsAre; using testing::Eq; using testing::IsEmpty; using testing::IsNull; 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 kLocalCacheGuid[] = "TestLocalCacheGuid"; MATCHER_P(EntityDataHasSpecifics, session_specifics_matcher, "") { return session_specifics_matcher.MatchAndExplain(arg->specifics.session(), result_listener); } syncer::EntityData SpecificsToEntity(const sync_pb::SessionSpecifics& specifics, base::Time mtime = base::Time::Now()) { syncer::EntityData data; data.client_tag_hash = syncer::ClientTagHash::FromUnhashed( syncer::SESSIONS, SessionStore::GetClientTag(specifics)); *data.specifics.mutable_session() = specifics; data.modification_time = mtime; return data; } 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> BatchToEntityDataMap( std::unique_ptr batch) { std::map> 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) { syncer::EntityData tombstone; tombstone.client_tag_hash = syncer::ClientTagHash::FromUnhashed(syncer::SESSIONS, client_tag); syncer::UpdateResponseData data; data.entity = std::move(tombstone); data.response_version = 2; return data; } syncer::CommitResponseData CreateSuccessResponse( const std::string& client_tag) { syncer::CommitResponseData response; response.client_tag_hash = syncer::ClientTagHash::FromUnhashed(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::SyncEnums_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)), session_sync_prefs_(&pref_service_) { SessionSyncPrefs::RegisterProfilePrefs(pref_service_.registry()); ON_CALL(mock_sync_sessions_client_, GetSessionSyncPrefs()) .WillByDefault(Return(&session_sync_prefs_)); ON_CALL(mock_sync_sessions_client_, GetStoreFactory()) .WillByDefault( Return(syncer::ModelTypeStoreTestUtil::FactoryForForwardingStore( store_.get()))); ON_CALL(mock_sync_sessions_client_, GetSyncedWindowDelegatesGetter()) .WillByDefault(Return(&window_getter_)); ON_CALL(mock_sync_sessions_client_, GetLocalSessionEventRouter()) .WillByDefault(Return(window_getter_.router())); // 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 = default; void InitializeBridge() { real_processor_ = std::make_unique( syncer::SESSIONS, /*dump_stack=*/base::DoNothing()); mock_processor_.DelegateCallsByDefaultTo(real_processor_.get()); // Instantiate the bridge. bridge_ = std::make_unique( mock_foreign_session_updated_cb_.Get(), &mock_sync_sessions_client_, 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& remote_data = {}) { syncer::DataTypeActivationRequest request; request.error_handler = base::DoNothing(); request.cache_guid = kLocalCacheGuid; request.authenticated_account_id = CoreAccountId("SomeAccountId"); base::RunLoop loop; real_processor_->OnSyncStarting( request, base::BindLambdaForTesting( [&loop](std::unique_ptr) { loop.Quit(); })); loop.Run(); sync_pb::ModelTypeState state; state.set_initial_sync_done(true); state.set_cache_guid(request.cache_guid); state.mutable_progress_marker()->set_data_type_id( GetSpecificsFieldNumberFromModelType(syncer::SESSIONS)); state.set_authenticated_account_id("SomeAccountId"); syncer::UpdateResponseDataList initial_updates; for (const SessionSpecifics& specifics : remote_data) { initial_updates.push_back(SpecificsToUpdateResponse(specifics)); } real_processor_->OnUpdateReceived(state, std::move(initial_updates)); } std::map> GetAllData() { base::RunLoop loop; std::unique_ptr batch; bridge_->GetAllDataForDebugging(base::BindLambdaForTesting( [&loop, &batch](std::unique_ptr input_batch) { batch = std::move(input_batch); loop.Quit(); })); loop.Run(); EXPECT_NE(nullptr, batch); return BatchToEntityDataMap(std::move(batch)); } std::map> GetData( const std::vector& storage_keys) { base::RunLoop loop; std::unique_ptr batch; bridge_->GetData( storage_keys, base::BindLambdaForTesting( [&loop, &batch](std::unique_ptr input_batch) { batch = std::move(input_batch); loop.Quit(); })); loop.Run(); EXPECT_NE(nullptr, batch); return BatchToEntityDataMap(std::move(batch)); } std::unique_ptr GetData(const std::string& storage_key) { std::map> entity_data_map = GetData(std::vector{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::SyncEnums_BrowserType type = sync_pb::SyncEnums_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; } void CloseTab(int tab_id) { window_getter_.CloseTab(SessionID::FromSerializedValue(tab_id)); } void SessionRestoreComplete() { window_getter_.SessionRestoreComplete(); } MockSyncSessionsClient& mock_sync_sessions_client() { return mock_sync_sessions_client_; } base::MockCallback& mock_foreign_session_updated_cb() { return mock_foreign_session_updated_cb_; } SessionSyncBridge* bridge() { return bridge_.get(); } syncer::MockModelTypeChangeProcessor& mock_processor() { return mock_processor_; } syncer::ClientTagBasedModelTypeProcessor* real_processor() { return real_processor_.get(); } syncer::ModelTypeStore* underlying_store() { return store_.get(); } private: base::test::SingleThreadTaskEnvironment task_environment_; const std::unique_ptr store_; // Dependencies. TestingPrefServiceSimple pref_service_; SessionSyncPrefs session_sync_prefs_; testing::NiceMock> mock_foreign_session_updated_cb_; testing::NiceMock mock_sync_sessions_client_; testing::NiceMock mock_processor_; TestSyncedWindowDelegatesGetter window_getter_; std::unique_ptr bridge_; std::unique_ptr 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(kLocalCacheGuid, /*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); SessionRestoreComplete(); 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(kLocalCacheGuid); const std::string tab_storage_key1 = SessionStore::GetTabStorageKey(kLocalCacheGuid, 0); const std::string tab_storage_key2 = SessionStore::GetTabStorageKey(kLocalCacheGuid, 1); EXPECT_CALL(mock_processor(), Put(header_storage_key, EntityDataHasSpecifics(MatchesHeader( kLocalCacheGuid, {kWindowId}, {kTabId1, kTabId2})), _)); EXPECT_CALL(mock_processor(), Put(tab_storage_key1, EntityDataHasSpecifics( MatchesTab(kLocalCacheGuid, kWindowId, kTabId1, /*tab_node_id=*/_, {"http://foo.com/"})), _)); EXPECT_CALL(mock_processor(), Put(tab_storage_key2, EntityDataHasSpecifics( MatchesTab(kLocalCacheGuid, kWindowId, kTabId2, /*tab_node_id=*/_, {"http://bar.com/"})), _)); StartSyncing(); EXPECT_THAT(GetData(header_storage_key), EntityDataHasSpecifics(MatchesHeader(kLocalCacheGuid, {kWindowId}, {kTabId1, kTabId2}))); EXPECT_THAT( GetAllData(), UnorderedElementsAre( Pair(header_storage_key, EntityDataHasSpecifics(MatchesHeader( kLocalCacheGuid, {kWindowId}, {kTabId1, kTabId2}))), Pair(tab_storage_key1, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, kWindowId, kTabId1, /*tab_node_id=*/_, {"http://foo.com/"}))), Pair(tab_storage_key2, EntityDataHasSpecifics( MatchesTab(kLocalCacheGuid, 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_session_updated_cb(), 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( kLocalCacheGuid, {kWindowId}, {kTabId1})), _)); EXPECT_CALL(mock_processor(), Put(_, EntityDataHasSpecifics(MatchesHeader( kLocalCacheGuid, {kWindowId}, {kTabId1, kTabId2})), _)) .After(put_transient_header) .WillOnce(WithArg<0>(SaveArg<0>(&header_storage_key))); EXPECT_CALL(mock_processor(), Put(_, EntityDataHasSpecifics( MatchesTab(kLocalCacheGuid, 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(kLocalCacheGuid))); 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( kLocalCacheGuid, {kWindowId}, {kTabId1, kTabId2}))), Pair(_, EntityDataHasSpecifics( MatchesTab(kLocalCacheGuid, kWindowId, kTabId1, /*tab_node_id=*/_, {"http://foo.com/"}))), Pair(tab_storage_key, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, kWindowId, kTabId2, /*tab_node_id=*/_, {"http://bar.com/"}))))); EXPECT_THAT(GetData(header_storage_key), EntityDataHasSpecifics(MatchesHeader(kLocalCacheGuid, {kWindowId}, {kTabId1, kTabId2}))); EXPECT_THAT(GetData(tab_storage_key), EntityDataHasSpecifics( MatchesTab(kLocalCacheGuid, kWindowId, kTabId2, /*tab_node_id=*/_, {"http://bar.com/"}))); } TEST_F(SessionSyncBridgeTest, ShouldNotUpdatePlaceholderTabsDuringRestore) { const int kWindowId1 = 1000001; const int kWindowId2 = 1000002; const int kTabId1 = 1000003; const int kTabId2 = 1000004; // Zero is the first assigned tab node ID. const int kTabNodeId1 = 0; const int kTabNodeId2 = 1; AddWindow(kWindowId1); AddTab(kWindowId1, "http://foo.com/", kTabId1); AddTab(kWindowId1, "http://bar.com/", kTabId2); const std::string header_storage_key = SessionStore::GetHeaderStorageKey(kLocalCacheGuid); const std::string tab_storage_key1 = SessionStore::GetTabStorageKey(kLocalCacheGuid, kTabNodeId1); const std::string tab_storage_key2 = SessionStore::GetTabStorageKey(kLocalCacheGuid, kTabNodeId2); InitializeBridge(); StartSyncing(); ASSERT_THAT(GetData(header_storage_key), EntityDataHasSpecifics(MatchesHeader( kLocalCacheGuid, {kWindowId1}, {kTabId1, kTabId2}))); ASSERT_THAT( GetData(tab_storage_key1), EntityDataHasSpecifics(MatchesTab(kLocalCacheGuid, kWindowId1, kTabId1, kTabNodeId1, {"http://foo.com/"}))); ASSERT_THAT( GetData(tab_storage_key2), EntityDataHasSpecifics(MatchesTab(kLocalCacheGuid, kWindowId1, kTabId2, kTabNodeId2, {"http://bar.com/"}))); ShutdownBridge(); // Override tabs with placeholder tab delegates. Note that, on Android, tab // IDs are persisted by session restore across browser restarts. PlaceholderTabDelegate placeholder_tab1( SessionID::FromSerializedValue(kTabId1)); PlaceholderTabDelegate placeholder_tab2( SessionID::FromSerializedValue(kTabId2)); ResetWindows(); TestSyncedWindowDelegate* window = AddWindow(kWindowId2); window->OverrideTabAt(0, &placeholder_tab1); window->OverrideTabAt(1, &placeholder_tab2); // When the bridge gets restarted, we only expect the header to be updated, // and placeholder tabs stay unchanged with a stale window ID. EXPECT_CALL(mock_processor(), Put(header_storage_key, EntityDataHasSpecifics(MatchesHeader( kLocalCacheGuid, {kWindowId2}, {kTabId1, kTabId2})), _)); // Start the bridge again. InitializeBridge(); StartSyncing(); // Although we haven't notified the processor about the window-ID change, if // it hypothetically asked for these entities, the returned entities are // up-to-date. EXPECT_THAT(GetData(header_storage_key), EntityDataHasSpecifics(MatchesHeader( kLocalCacheGuid, {kWindowId2}, {kTabId1, kTabId2}))); EXPECT_THAT( GetData(tab_storage_key1), EntityDataHasSpecifics(MatchesTab(kLocalCacheGuid, kWindowId2, kTabId1, kTabNodeId1, {"http://foo.com/"}))); EXPECT_THAT( GetData(tab_storage_key2), EntityDataHasSpecifics(MatchesTab(kLocalCacheGuid, kWindowId2, kTabId2, kTabNodeId2, {"http://bar.com/"}))); EXPECT_THAT( GetAllData(), UnorderedElementsAre( Pair(header_storage_key, EntityDataHasSpecifics(MatchesHeader( kLocalCacheGuid, {kWindowId2}, {kTabId1, kTabId2}))), Pair(tab_storage_key1, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, kWindowId2, kTabId1, kTabNodeId1, {"http://foo.com/"}))), Pair(tab_storage_key2, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, kWindowId2, kTabId2, kTabNodeId2, {"http://bar.com/"}))))); } TEST_F(SessionSyncBridgeTest, ShouldIgnoreUnsyncablePlaceholderTabDuringRestore) { const int kWindowId1 = 1000001; const int kWindowId2 = 1000002; const int kTabId1 = 1000002; const int kTabId2 = 1000003; // Zero is the first assigned tab node ID. const int kTabNodeId1 = 0; AddWindow(kWindowId1); AddTab(kWindowId1, "http://foo.com/", kTabId1); // Tab 2 is unsyncable because of the URL scheme. AddTab(kWindowId1, "about:blank", kTabId2); const std::string header_storage_key = SessionStore::GetHeaderStorageKey(kLocalCacheGuid); const std::string tab_storage_key1 = SessionStore::GetTabStorageKey(kLocalCacheGuid, kTabNodeId1); InitializeBridge(); StartSyncing(); ASSERT_THAT( GetAllData(), UnorderedElementsAre( Pair(header_storage_key, EntityDataHasSpecifics( MatchesHeader(kLocalCacheGuid, {kWindowId1}, {kTabId1}))), Pair(tab_storage_key1, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, kWindowId1, kTabId1, kTabNodeId1, {"http://foo.com/"}))))); ShutdownBridge(); // Override tabs with placeholder tab delegates. Note that, on Android, tab // IDs are persisted by session restore across browser restarts. PlaceholderTabDelegate placeholder_tab1( SessionID::FromSerializedValue(kTabId1)); PlaceholderTabDelegate placeholder_tab2( SessionID::FromSerializedValue(kTabId2)); ResetWindows(); TestSyncedWindowDelegate* window = AddWindow(kWindowId2); window->OverrideTabAt(0, &placeholder_tab1); window->OverrideTabAt(1, &placeholder_tab2); // Start the bridge again. InitializeBridge(); StartSyncing(); EXPECT_THAT( GetAllData(), UnorderedElementsAre( Pair(header_storage_key, EntityDataHasSpecifics( MatchesHeader(kLocalCacheGuid, {kWindowId2}, {kTabId1}))), Pair(tab_storage_key1, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, kWindowId2, kTabId1, 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 kWindowId1 = 1000001; const int kWindowId2 = 1000002; const int kTabNodeId = 0; AddWindow(kWindowId1); TestSyncedTabDelegate* tab = AddTab(kWindowId1, "http://foo.com/"); const std::string header_storage_key = SessionStore::GetHeaderStorageKey(kLocalCacheGuid); const std::string tab_storage_key = SessionStore::GetTabStorageKey(kLocalCacheGuid, kTabNodeId); InitializeBridge(); StartSyncing(); ASSERT_THAT( GetAllData(), UnorderedElementsAre( Pair(header_storage_key, EntityDataHasSpecifics(MatchesHeader(kLocalCacheGuid, _, _))), Pair(tab_storage_key, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, _, _, 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(kLocalCacheGuid, _, _))), Pair(tab_storage_key, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, _, _, 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(kLocalCacheGuid, _, _)), _)); EXPECT_CALL(mock_processor(), Put(tab_storage_key, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, /*window_id=*/_, /*tab_id=*/_, kTabNodeId, {"http://foo.com/", "http://bar.com/"})), _)); AddWindow(kWindowId2)->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(kLocalCacheGuid, _, _))), Pair(_, EntityDataHasSpecifics(MatchesTab(kLocalCacheGuid, _, _, /*tab_node_id=*/0, {"http://foo.com/"}))))); ShutdownBridge(); // Start the bridge with only a custom tab open. ResetWindows(); AddWindow(kWindowId2, sync_pb::SyncEnums_BrowserType_TYPE_CUSTOM_TAB); AddTab(kWindowId2, "http://bar.com/"); InitializeBridge(); StartSyncing(); // The previous session should be preserved, together with the new custom tab. EXPECT_THAT( GetAllData(), UnorderedElementsAre( Pair(_, EntityDataHasSpecifics(MatchesHeader(kLocalCacheGuid, _, _))), Pair(_, EntityDataHasSpecifics(MatchesTab(kLocalCacheGuid, _, _, /*tab_node_id=*/0, {"http://foo.com/"}))), Pair(_, EntityDataHasSpecifics(MatchesTab(kLocalCacheGuid, _, _, /*tab_node_id=*/1, {"http://bar.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( kLocalCacheGuid, {kWindowId1}, {kTabId1}))), Pair(_, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, kWindowId1, kTabId1, /*tab_node_id=*/0, {"http://foo.com/"}))))); ShutdownBridge(); // Start the bridge with an additional local custom tab. AddWindow(kWindowId2, sync_pb::SyncEnums_BrowserType_TYPE_CUSTOM_TAB); AddTab(kWindowId2, "http://bar.com/", kTabId2); InitializeBridge(); StartSyncing(); EXPECT_THAT(GetAllData(), UnorderedElementsAre( Pair(_, EntityDataHasSpecifics(MatchesHeader( kLocalCacheGuid, {kWindowId1, kWindowId2}, {kTabId1, kTabId2}))), Pair(_, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, kWindowId1, kTabId1, /*tab_node_id=*/0, {"http://foo.com/"}))), Pair(_, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, 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) starts syncing that tab. TEST_F(SessionSyncBridgeTest, ShouldAssociateIfCustomTabOnlyOnStartup) { const int kWindowId = 1000001; const int kTabId = 1000002; AddWindow(kWindowId, sync_pb::SyncEnums_BrowserType_TYPE_CUSTOM_TAB); AddTab(kWindowId, "http://foo.com/", kTabId); InitializeBridge(); StartSyncing(); EXPECT_THAT(GetAllData(), UnorderedElementsAre( Pair(_, EntityDataHasSpecifics(MatchesHeader( kLocalCacheGuid, {kWindowId}, {kTabId}))), Pair(_, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, kWindowId, kTabId, /*tab_node_id=*/0, {"http://foo.com/"}))))); } // 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::SyncEnums_BrowserType_TYPE_CUSTOM_TAB); AddTab(kWindowId1, "http://foo.com/", kTabId1); InitializeBridge(); StartSyncing(); ASSERT_THAT(GetAllData(), UnorderedElementsAre( Pair(_, EntityDataHasSpecifics(MatchesHeader( kLocalCacheGuid, {kWindowId1}, {kTabId1}))), Pair(_, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, kWindowId1, kTabId1, /*tab_node_id=*/0, {"http://foo.com/"}))))); // 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. EXPECT_THAT(GetAllData(), UnorderedElementsAre( Pair(_, EntityDataHasSpecifics(MatchesHeader( kLocalCacheGuid, {kWindowId1, kWindowId2}, {kTabId1, kTabId2}))), Pair(_, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, kWindowId1, kTabId1, /*tab_node_id=*/0, {"http://foo.com/"}))), Pair(_, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, kWindowId2, kTabId2, /*tab_node_id=*/1, {"http://bar.com/"}))))); } TEST_F(SessionSyncBridgeTest, ShouldRecycleTabNodeAfterCommitCompleted) { const int kWindowId = 1000001; const int kTabId1 = 1000003; const int kTabId2 = 1000004; const int kTabId3 = 1000005; const int kTabId4 = 1000006; // Zero is the first assigned tab node ID. const int kTabNodeId1 = 0; const int kTabNodeId2 = 1; const int kTabNodeId3 = 2; AddWindow(kWindowId); TestSyncedTabDelegate* tab1 = AddTab(kWindowId, "http://foo.com/", kTabId1); const std::string header_storage_key = SessionStore::GetHeaderStorageKey(kLocalCacheGuid); const std::string tab_storage_key1 = SessionStore::GetTabStorageKey(kLocalCacheGuid, kTabNodeId1); const std::string tab_storage_key2 = SessionStore::GetTabStorageKey(kLocalCacheGuid, kTabNodeId2); const std::string tab_storage_key3 = SessionStore::GetTabStorageKey(kLocalCacheGuid, kTabNodeId3); const std::string tab_client_tag1 = SessionStore::GetTabClientTagForTest(kLocalCacheGuid, kTabNodeId1); const std::string tab_client_tag2 = SessionStore::GetTabClientTagForTest(kLocalCacheGuid, kTabNodeId2); const std::string tab_client_tag3 = SessionStore::GetTabClientTagForTest(kLocalCacheGuid, kTabNodeId3); InitializeBridge(); StartSyncing(); // Mimic a commit completing for the initial sync. ASSERT_TRUE(real_processor()->HasLocalChangesForTest()); sync_pb::ModelTypeState state; state.set_initial_sync_done(true); real_processor()->OnCommitCompleted( state, {CreateSuccessResponse(kLocalCacheGuid), CreateSuccessResponse(tab_client_tag1)}, /*error_response_list=*/FailedCommitResponseDataList()); ASSERT_FALSE(real_processor()->HasLocalChangesForTest()); // Open a second tab. AddTab(kWindowId, "http://bar.com/", kTabId2); ASSERT_TRUE(real_processor()->HasLocalChangesForTest()); // Close |kTabId2| and force reassociation by navigating in the remaining open // tab, leading to a freed tab entity. However, while there are pending // changes to commit, the entity shouldn't be deleted (to prevent history // loss). EXPECT_CALL(mock_processor(), Delete).Times(0); CloseTab(kTabId2); tab1->Navigate("http://foo2.com/"); EXPECT_TRUE(real_processor()->HasLocalChangesForTest()); EXPECT_THAT( GetAllData(), UnorderedElementsAre( Pair(header_storage_key, EntityDataHasSpecifics( MatchesHeader(kLocalCacheGuid, {kWindowId}, {kTabId1}))), Pair(tab_storage_key1, EntityDataHasSpecifics( MatchesTab(kLocalCacheGuid, kWindowId, kTabId1, kTabNodeId1, {"http://foo.com/", "http://foo2.com/"}))), Pair(tab_storage_key2, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, kWindowId, kTabId2, kTabNodeId2, {"http://bar.com/"}))))); // If a new tab is opened, the entity with unsynced changes should not be // recycled. AddTab(kWindowId, "http://baz.com/", kTabId3); EXPECT_THAT(GetAllData(), UnorderedElementsAre(Pair(header_storage_key, _), Pair(tab_storage_key1, _), Pair(tab_storage_key2, _), Pair(tab_storage_key3, _))); // Completing the commit for the previously closed tab should issue a // deletion. For that to trigger, we need to trigger the next association, // which we do by navigating in one of the open tabs. EXPECT_CALL(mock_processor(), Delete(tab_storage_key2, _)); real_processor()->OnCommitCompleted( state, {CreateSuccessResponse(tab_client_tag2)}, /*error_response_list=*/FailedCommitResponseDataList()); tab1->Navigate("http://foo3.com/"); EXPECT_THAT(GetAllData(), UnorderedElementsAre(Pair(header_storage_key, _), Pair(tab_storage_key1, _), Pair(tab_storage_key3, _))); // If yet anothertab is opened, the entity for the closed tab should be // recycled. AddTab(kWindowId, "http://qux.com/", kTabId4); EXPECT_THAT( GetAllData(), UnorderedElementsAre( Pair(header_storage_key, _), Pair(tab_storage_key1, _), Pair(tab_storage_key2, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, kWindowId, kTabId4, kTabNodeId2, {"http://qux.com/"}))), Pair(tab_storage_key3, _))); } TEST_F(SessionSyncBridgeTest, ShouldRestoreLocalSessionWithFreedTab) { const int kWindowId1 = 1000001; const int kWindowId2 = 1000002; const int kTabId1 = 1000003; const int kTabId2 = 1000004; const int kTabId3 = 1000005; // Zero is the first assigned tab node ID. const int kTabNodeId1 = 0; const int kTabNodeId2 = 1; const int kTabNodeId3 = 2; AddWindow(kWindowId1); TestSyncedTabDelegate* tab1 = AddTab(kWindowId1, "http://foo.com/", kTabId1); AddTab(kWindowId1, "http://bar.com/", kTabId2); const std::string header_storage_key = SessionStore::GetHeaderStorageKey(kLocalCacheGuid); const std::string tab_storage_key1 = SessionStore::GetTabStorageKey(kLocalCacheGuid, kTabNodeId1); const std::string tab_storage_key2 = SessionStore::GetTabStorageKey(kLocalCacheGuid, kTabNodeId2); const std::string tab_storage_key3 = SessionStore::GetTabStorageKey(kLocalCacheGuid, kTabNodeId3); InitializeBridge(); StartSyncing(); ASSERT_THAT(GetData(header_storage_key), EntityDataHasSpecifics(MatchesHeader( kLocalCacheGuid, {kWindowId1}, {kTabId1, kTabId2}))); // Close |kTabId2| and force reassociation by navigating in the remaining open // tab, leading to a freed tab entity. CloseTab(kTabId2); tab1->Navigate("http://foo2.com/"); ASSERT_THAT(GetData(header_storage_key), EntityDataHasSpecifics( MatchesHeader(kLocalCacheGuid, {kWindowId1}, {kTabId1}))); ShutdownBridge(); ResetWindows(); // The browser gets restarted with a new initial tab, for example because the // user chose "Continue where you left off". AddWindow(kWindowId2); AddTab(kWindowId2, "http://baz.com/", kTabId3); // Start the bridge again. InitializeBridge(); StartSyncing(); // Two tab nodes should be free at this point, because both tabs have been // closed. However, they are also unsynced (the commit hasn't completed), // which prevents their recycling, so a new tab node should be created. ASSERT_THAT( GetAllData(), UnorderedElementsAre( Pair(header_storage_key, EntityDataHasSpecifics( MatchesHeader(kLocalCacheGuid, {kWindowId2}, {kTabId3}))), Pair(tab_storage_key1, EntityDataHasSpecifics( MatchesTab(kLocalCacheGuid, kWindowId1, kTabId1, kTabNodeId1, {"http://foo.com/", "http://foo2.com/"}))), Pair(tab_storage_key2, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, kWindowId1, kTabId2, kTabNodeId2, {"http://bar.com/"}))), Pair(tab_storage_key3, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, kWindowId2, kTabId3, kTabNodeId3, {"http://baz.com/"}))))); } 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(kLocalCacheGuid); ASSERT_THAT(GetData(header_storage_key), EntityDataHasSpecifics( MatchesHeader(kLocalCacheGuid, {kWindowId}, {kTabId}))); ASSERT_THAT(GetAllData(), Not(IsEmpty())); EXPECT_CALL(mock_processor(), ModelReadyToSync).Times(0); real_processor()->OnSyncStopping(syncer::CLEAR_METADATA); StartSyncing(); ASSERT_THAT(GetData(header_storage_key), EntityDataHasSpecifics( MatchesHeader(kLocalCacheGuid, {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(kLocalCacheGuid, _, _)), _)); EXPECT_CALL(mock_foreign_session_updated_cb(), Run()); StartSyncing({foreign_header, foreign_tab}); std::vector foreign_sessions; EXPECT_TRUE(bridge()->GetOpenTabsUIDelegate()->GetAllForeignSessions( &foreign_sessions)); EXPECT_THAT(foreign_sessions, ElementsAre(MatchesSyncedSession( kForeignSessionTag, {{kForeignWindowId, std::vector{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(kLocalCacheGuid, _, _)), _)); StartSyncing({foreign_header}); ASSERT_THAT(GetData(foreign_header_storage_key), NotNull()); std::vector 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(kLocalCacheGuid); 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}); sync_pb::ModelTypeState state; state.set_initial_sync_done(true); // Mimic receiving a commit ack for the local header entity, to later be able // to verify HasLocalChangesForTest() without interferences from the local // session. ASSERT_TRUE(real_processor()->HasLocalChangesForTest()); real_processor()->OnCommitCompleted( state, {CreateSuccessResponse(kLocalCacheGuid)}, /*error_response_list=*/FailedCommitResponseDataList()); ASSERT_FALSE(real_processor()->HasLocalChangesForTest()); 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 foreign_sessions; ASSERT_TRUE(bridge()->GetOpenTabsUIDelegate()->GetAllForeignSessions( &foreign_sessions)); ASSERT_THAT(foreign_sessions, ElementsAre(MatchesSyncedSession( kForeignSessionTag, {{kForeignWindowId, std::vector{kForeignTabId}}}))); ASSERT_TRUE(real_processor()->IsTrackingMetadata()); ASSERT_TRUE(real_processor()->IsTrackingEntityForTest( SessionStore::GetHeaderStorageKey(kForeignSessionTag))); ASSERT_TRUE(real_processor()->IsTrackingEntityForTest( SessionStore::GetTabStorageKey(kForeignSessionTag, kForeignTabNodeId))); ASSERT_FALSE(real_processor()->HasLocalChangesForTest()); // Mimic receiving a remote deletion of the foreign session. EXPECT_CALL(mock_foreign_session_updated_cb(), Run()); syncer::UpdateResponseDataList updates; updates.push_back( CreateTombstone(SessionStore::GetClientTag(foreign_header))); real_processor()->OnUpdateReceived(state, std::move(updates)); foreign_session_tab = nullptr; EXPECT_FALSE(bridge()->GetOpenTabsUIDelegate()->GetForeignTab( kForeignSessionTag, SessionID::FromSerializedValue(kForeignTabId), &foreign_session_tab)); EXPECT_FALSE(bridge()->GetOpenTabsUIDelegate()->GetAllForeignSessions( &foreign_sessions)); EXPECT_FALSE(real_processor()->HasLocalChangesForTest()); const std::string header_storage_key = SessionStore::GetHeaderStorageKey(kForeignSessionTag); const std::string tab_storage_key = SessionStore::GetTabStorageKey(kForeignSessionTag, kForeignTabNodeId); EXPECT_FALSE(real_processor()->IsTrackingEntityForTest(header_storage_key)); EXPECT_FALSE(real_processor()->IsTrackingEntityForTest(tab_storage_key)); // Verify that both entities have been deleted from storage. { base::RunLoop loop; underlying_store()->ReadData( {header_storage_key, tab_storage_key}, base::BindLambdaForTesting( [&](const absl::optional& error, std::unique_ptr data_records, std::unique_ptr missing_id_list) { EXPECT_THAT(data_records, Pointee(IsEmpty())); EXPECT_THAT( missing_id_list, Pointee(ElementsAre(header_storage_key, tab_storage_key))); loop.Quit(); })); loop.Run(); } // Verify that the sync metadata for both entities have been deleted too. { base::RunLoop loop; underlying_store()->ReadAllMetadata(base::BindLambdaForTesting( [&](const absl::optional& error, std::unique_ptr metadata_batch) { syncer::EntityMetadataMap entity_metadata_map = metadata_batch->TakeAllMetadata(); EXPECT_EQ(0U, entity_metadata_map.count(header_storage_key)); EXPECT_EQ(0U, entity_metadata_map.count(tab_storage_key)); loop.Quit(); })); loop.Run(); } } 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(kLocalCacheGuid); const std::string tab_storage_key1 = SessionStore::GetTabStorageKey(kLocalCacheGuid, kTabNodeId1); const std::string tab_client_tag1 = SessionStore::GetTabClientTagForTest(kLocalCacheGuid, kTabNodeId1); ASSERT_THAT( GetAllData(), UnorderedElementsAre( Pair(header_storage_key, EntityDataHasSpecifics(MatchesHeader(kLocalCacheGuid, _, _))), Pair(tab_storage_key1, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, 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(kLocalCacheGuid)}, /*error_response_list=*/FailedCommitResponseDataList()); ASSERT_FALSE(real_processor()->HasLocalChangesForTest()); // Mimic receiving a remote deletion of both entities. EXPECT_CALL(mock_processor(), Put).Times(0); syncer::UpdateResponseDataList updates; updates.push_back(CreateTombstone(kLocalCacheGuid)); updates.push_back(CreateTombstone(tab_client_tag1)); real_processor()->OnUpdateReceived(state, std::move(updates)); // State should remain unchanged (deletions ignored). EXPECT_THAT( GetAllData(), UnorderedElementsAre( Pair(header_storage_key, EntityDataHasSpecifics(MatchesHeader(kLocalCacheGuid, _, _))), Pair(tab_storage_key1, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, 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(kLocalCacheGuid, 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( kLocalCacheGuid, ElementsAre(kWindowId1, kWindowId2), ElementsAre(kTabId1, kTabId2))), _)) .Times(2) .After(put_transient_header); EXPECT_CALL(mock_processor(), Put(tab_storage_key1, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, kWindowId1, kTabId1, kTabNodeId1, {"http://foo.com/"})), _)); EXPECT_CALL(mock_processor(), Put(tab_storage_key2, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, kWindowId2, kTabId2, kTabNodeId2, {"http://bar.com/"})), _)) .Times(2); AddTab(kWindowId2, "http://bar.com/", kTabId2); EXPECT_THAT( GetAllData(), UnorderedElementsAre( Pair(header_storage_key, EntityDataHasSpecifics(MatchesHeader( kLocalCacheGuid, ElementsAre(kWindowId1, kWindowId2), ElementsAre(kTabId1, kTabId2)))), Pair(tab_storage_key1, EntityDataHasSpecifics( MatchesTab(kLocalCacheGuid, /*window_id=*/_, /*tab_id=*/_, kTabNodeId1, {"http://foo.com/"}))), Pair(tab_storage_key2, EntityDataHasSpecifics(MatchesTab( kLocalCacheGuid, 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 foreign_sessions; ASSERT_TRUE(bridge()->GetOpenTabsUIDelegate()->GetAllForeignSessions( &foreign_sessions)); ASSERT_THAT(foreign_sessions, ElementsAre(MatchesSyncedSession( kForeignSessionTag, {{kForeignWindowId, std::vector{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_session_updated_cb(), 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_session_updated_cb(), Run()).Times(0); EXPECT_CALL(mock_processor(), Delete).Times(0); bridge()->GetOpenTabsUIDelegate()->DeleteForeignSession(kLocalCacheGuid); const SyncedSession* session = nullptr; EXPECT_TRUE(bridge()->GetOpenTabsUIDelegate()->GetLocalSession(&session)); EXPECT_THAT(session, NotNull()); EXPECT_THAT(GetData(SessionStore::GetHeaderStorageKey(kLocalCacheGuid)), NotNull()); } // Verifies that receiving an empty update list does not broadcast a foreign // session change via the corresponding callback. TEST_F(SessionSyncBridgeTest, ShouldNotBroadcastUpdatesIfEmpty) { InitializeBridge(); StartSyncing(); EXPECT_CALL(mock_foreign_session_updated_cb(), Run()).Times(0); // Mimic receiving an empty list of remote updates. sync_pb::ModelTypeState state; state.set_initial_sync_done(true); real_processor()->OnUpdateReceived(state, {}); } 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::Days(15); const base::Time recent_mtime = base::Time::Now() - base::Days(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)); // 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_session_updated_cb(), Run()).Times(AtLeast(1)); real_processor()->OnUpdateReceived(state, std::move(updates)); } TEST_F(SessionSyncBridgeTest, ShouldReturnBrowserTypeInGetData) { const int kWindowId = 1000001; const int kTabId = 1000002; AddWindow(kWindowId, sync_pb::SyncEnums_BrowserType_TYPE_CUSTOM_TAB); AddTab(kWindowId, "http://foo.com/", kTabId); InitializeBridge(); StartSyncing(); std::unique_ptr tab_data = GetData( SessionStore::GetTabStorageKey(kLocalCacheGuid, /*tab_node_id=*/0)); ASSERT_THAT(tab_data, NotNull()); EXPECT_EQ(sync_pb::SyncEnums_BrowserType_TYPE_CUSTOM_TAB, tab_data->specifics.session().tab().browser_type()); } } // namespace } // namespace sync_sessions