| // Copyright 2018 The Chromium Authors |
| // 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/functional/callback_helpers.h" |
| #include "base/memory/raw_ptr.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/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/service/sync_prefs.h" |
| #include "components/sync/test/mock_commit_queue.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 kAccountId[] = "TestAccountId"; |
| const char kLocalCacheGuid[] = "TestLocalCacheGuid"; |
| |
| MATCHER_P(EntityDataHasSpecifics, session_specifics_matcher, "") { |
| return session_specifics_matcher.MatchAndExplain(arg->specifics.session(), |
| result_listener); |
| } |
| |
| sync_pb::ModelTypeState GetModelTypeStateWithInitialSyncDone() { |
| sync_pb::ModelTypeState state; |
| state.set_initial_sync_state( |
| sync_pb::ModelTypeState_InitialSyncState_INITIAL_SYNC_DONE); |
| state.set_cache_guid(kLocalCacheGuid); |
| state.set_authenticated_account_id(kAccountId); |
| state.mutable_progress_marker()->set_data_type_id( |
| GetSpecificsFieldNumberFromModelType(syncer::SESSIONS)); |
| return state; |
| } |
| |
| 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<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) { |
| 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::ClientTagBasedModelTypeProcessor>( |
| syncer::SESSIONS, /*dump_stack=*/base::DoNothing()); |
| mock_processor_.DelegateCallsByDefaultTo(real_processor_.get()); |
| // Instantiate the bridge. |
| bridge_ = std::make_unique<SessionSyncBridge>( |
| 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<SessionSpecifics>& remote_data = {}) { |
| syncer::DataTypeActivationRequest request; |
| request.error_handler = base::DoNothing(); |
| request.cache_guid = kLocalCacheGuid; |
| request.authenticated_account_id = CoreAccountId::FromGaiaId(kAccountId); |
| |
| base::RunLoop loop; |
| real_processor_->OnSyncStarting( |
| request, |
| base::BindLambdaForTesting( |
| [&loop](std::unique_ptr<syncer::DataTypeActivationResponse>) { |
| loop.Quit(); |
| })); |
| loop.Run(); |
| |
| // ClientTagBasedModelTypeProcessor requires connecting before other |
| // interactions with the worker happen. |
| real_processor_->ConnectSync( |
| std::make_unique<testing::NiceMock<syncer::MockCommitQueue>>()); |
| |
| sync_pb::ModelTypeState state = GetModelTypeStateWithInitialSyncDone(); |
| syncer::UpdateResponseDataList initial_updates; |
| for (const SessionSpecifics& specifics : remote_data) { |
| initial_updates.push_back(SpecificsToUpdateResponse(specifics)); |
| } |
| real_processor_->OnUpdateReceived(state, std::move(initial_updates), |
| /*gc_directive=*/std::nullopt); |
| } |
| |
| std::map<std::string, std::unique_ptr<EntityData>> GetAllData() { |
| base::RunLoop loop; |
| std::unique_ptr<DataBatch> batch; |
| bridge_->GetAllDataForDebugging(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::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<base::RepeatingClosure>& |
| 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<syncer::ModelTypeStore> store_; |
| |
| // Dependencies. |
| TestingPrefServiceSimple pref_service_; |
| SessionSyncPrefs session_sync_prefs_; |
| testing::NiceMock<base::MockCallback<base::RepeatingClosure>> |
| mock_foreign_session_updated_cb_; |
| testing::NiceMock<MockSyncSessionsClient> mock_sync_sessions_client_; |
| testing::NiceMock<MockModelTypeChangeProcessor> mock_processor_; |
| TestSyncedWindowDelegatesGetter window_getter_; |
| |
| 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(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)); |
| auto snapshot1 = std::make_unique<TestSyncedTabDelegate>( |
| SessionID::FromSerializedValue(kWindowId1), |
| SessionID::FromSerializedValue(kTabId1), base::DoNothing()); |
| placeholder_tab1.SetPlaceholderTabSyncedTabDelegate(std::move(snapshot1)); |
| |
| PlaceholderTabDelegate placeholder_tab2( |
| SessionID::FromSerializedValue(kTabId2)); |
| auto snapshot2 = std::make_unique<TestSyncedTabDelegate>( |
| SessionID::FromSerializedValue(kWindowId2), |
| SessionID::FromSerializedValue(kTabId2), base::DoNothing()); |
| placeholder_tab2.SetPlaceholderTabSyncedTabDelegate(std::move(snapshot2)); |
| |
| 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()); |
| real_processor()->OnCommitCompleted( |
| GetModelTypeStateWithInitialSyncDone(), |
| {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( |
| GetModelTypeStateWithInitialSyncDone(), |
| {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()).Times(AtLeast(1)); |
| StartSyncing({foreign_header, foreign_tab}); |
| |
| std::vector<raw_ptr<const SyncedSession, VectorExperimental>> |
| foreign_sessions; |
| EXPECT_TRUE(bridge()->GetOpenTabsUIDelegate()->GetAllForeignSessions( |
| &foreign_sessions)); |
| EXPECT_THAT(foreign_sessions, |
| ElementsAre(MatchesSyncedSession( |
| kForeignSessionTag, |
| {{kForeignWindowId, std::vector<int>{kForeignTabId}}}))); |
| } |
| |
| // Starting sync even without remote data should trigger a notification for |
| // updated foreign session. |
| TEST_F(SessionSyncBridgeTest, ShouldTriggerNotificationWithoutRemoteData) { |
| InitializeBridge(); |
| ASSERT_THAT(bridge()->GetOpenTabsUIDelegate(), IsNull()); |
| // Starting sync, even without remote data, should trigger a notification that |
| // foreign sessions got updated, because `GetOpenTabsUIDelegate()`'s behavior |
| // changed and it no longer returns null. |
| EXPECT_CALL(mock_foreign_session_updated_cb(), Run()); |
| StartSyncing(); |
| |
| EXPECT_THAT(bridge()->GetOpenTabsUIDelegate(), NotNull()); |
| } |
| |
| 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<raw_ptr<const SyncedSession, VectorExperimental>> |
| 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}); |
| |
| // 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( |
| GetModelTypeStateWithInitialSyncDone(), |
| {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<raw_ptr<const SyncedSession, VectorExperimental>> |
| 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()); |
| 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(GetModelTypeStateWithInitialSyncDone(), |
| std::move(updates), |
| /*gc_directive=*/std::nullopt); |
| |
| 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 std::optional<syncer::ModelError>& error, |
| std::unique_ptr<syncer::ModelTypeStore::RecordList> |
| data_records, |
| std::unique_ptr<syncer::ModelTypeStore::IdList> |
| 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 std::optional<syncer::ModelError>& error, |
| std::unique_ptr<syncer::MetadataBatch> 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. |
| real_processor()->OnCommitCompleted( |
| GetModelTypeStateWithInitialSyncDone(), |
| {CreateSuccessResponse(tab_client_tag1), |
| CreateSuccessResponse(kLocalCacheGuid)}, |
| /*error_response_list=*/FailedCommitResponseDataList()); |
| ASSERT_FALSE(real_processor()->HasLocalChangesForTest()); |
| |
| // Mimic receiving a remote deletion of both entities. |
| ASSERT_FALSE(bridge()->IsLocalDataOutOfSyncForTest()); |
| 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(GetModelTypeStateWithInitialSyncDone(), |
| std::move(updates), |
| /*gc_directive=*/std::nullopt); |
| |
| // State should remain unchanged (deletions ignored). |
| EXPECT_TRUE(bridge()->IsLocalDataOutOfSyncForTest()); |
| 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(); |
| } |
| |
| // Test that remote deletion of local data will not result in a placeholder tab |
| // being lost. |
| TEST_F(SessionSyncBridgeTest, ShouldIgnoreRemoteDeletionOfLocalPlaceholderTab) { |
| const int kWindowId1 = 1000001; |
| const int kTabId1 = 1000003; |
| // Zero is the first assigned tab node ID. |
| 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()->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. |
| real_processor()->OnCommitCompleted( |
| GetModelTypeStateWithInitialSyncDone(), |
| {CreateSuccessResponse(tab_client_tag1), |
| CreateSuccessResponse(kLocalCacheGuid)}, |
| /*error_response_list=*/FailedCommitResponseDataList()); |
| ASSERT_FALSE(real_processor()->HasLocalChangesForTest()); |
| |
| ShutdownBridge(); |
| |
| // Override the tab with a placeholder tab delegate. Note that, on Android, |
| // tab IDs are persisted by session restore across browser restarts. |
| PlaceholderTabDelegate placeholder_tab1( |
| SessionID::FromSerializedValue(kTabId1)); |
| ResetWindows(); |
| TestSyncedWindowDelegate* window = AddWindow(kWindowId1); |
| window->OverrideTabAt(0, &placeholder_tab1); |
| |
| // Start the bridge again. |
| InitializeBridge(); |
| StartSyncing(); |
| ASSERT_TRUE(real_processor()->IsTrackingMetadata()); |
| ASSERT_FALSE(real_processor()->HasLocalChangesForTest()); |
| |
| // Mimic receiving a remote deletion of both entities (header and tab). |
| ASSERT_FALSE(bridge()->IsLocalDataOutOfSyncForTest()); |
| syncer::UpdateResponseDataList updates; |
| updates.push_back(CreateTombstone(kLocalCacheGuid)); |
| updates.push_back(CreateTombstone(tab_client_tag1)); |
| real_processor()->OnUpdateReceived(GetModelTypeStateWithInitialSyncDone(), |
| std::move(updates), |
| /*gc_directive=*/std::nullopt); |
| |
| // State should remain unchanged (deletions ignored), but the local data |
| // should be marked as "out of sync". |
| EXPECT_TRUE(bridge()->IsLocalDataOutOfSyncForTest()); |
| EXPECT_THAT( |
| GetAllData(), |
| UnorderedElementsAre( |
| Pair(header_storage_key, |
| EntityDataHasSpecifics(MatchesHeader(kLocalCacheGuid, _, _))), |
| Pair(tab_storage_key1, EntityDataHasSpecifics(MatchesTab( |
| kLocalCacheGuid, kWindowId1, kTabId1, |
| kTabNodeId1, {"http://foo.com/"}))))); |
| |
| ShutdownBridge(); |
| |
| // Start the bridge once again. The state should remain unchanged. |
| InitializeBridge(); |
| EXPECT_TRUE(bridge()->IsLocalDataOutOfSyncForTest()); |
| StartSyncing(); |
| EXPECT_THAT( |
| GetAllData(), |
| UnorderedElementsAre( |
| Pair(header_storage_key, |
| EntityDataHasSpecifics(MatchesHeader(kLocalCacheGuid, _, _))), |
| Pair(tab_storage_key1, EntityDataHasSpecifics(MatchesTab( |
| kLocalCacheGuid, kWindowId1, kTabId1, |
| kTabNodeId1, {"http://foo.com/"}))))); |
| |
| // ResubmitLocalSession() should have been posted and the LocalDataOutOfSync |
| // state cleared. However, the data has not yet been sent to the processor. |
| EXPECT_FALSE(bridge()->IsLocalDataOutOfSyncForTest()); |
| ASSERT_FALSE(real_processor()->HasLocalChangesForTest()); |
| |
| // Run until idle because PostTask() is used to invoke ResubmitLocalSession(). |
| base::RunLoop().RunUntilIdle(); |
| |
| // The local data should have been sent for reupload, but no commit has |
| // occurred yet. |
| ASSERT_TRUE(real_processor()->HasLocalChangesForTest()); |
| } |
| |
| // Test that while remote deletion will not delete data, local metadata deletion |
| // from an action such as toggling sync will properly delete metadata and not |
| // reupload it. |
| TEST_F(SessionSyncBridgeTest, ShouldNotRestoreLocalSessionWithoutMetadata) { |
| const int kWindowId1 = 1000001; |
| 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); |
| |
| 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); |
| const std::string tab_storage_key2 = |
| SessionStore::GetTabStorageKey(kLocalCacheGuid, kTabNodeId2); |
| const std::string tab_client_tag2 = |
| SessionStore::GetTabClientTagForTest(kLocalCacheGuid, kTabNodeId2); |
| |
| ASSERT_THAT( |
| GetAllData(), |
| UnorderedElementsAre( |
| Pair(header_storage_key, |
| EntityDataHasSpecifics(MatchesHeader(kLocalCacheGuid, _, _))), |
| Pair(tab_storage_key1, EntityDataHasSpecifics(MatchesTab( |
| kLocalCacheGuid, kWindowId1, kTabId1, |
| kTabNodeId1, {"http://foo.com/"}))), |
| Pair(tab_storage_key2, EntityDataHasSpecifics(MatchesTab( |
| kLocalCacheGuid, kWindowId1, kTabId2, |
| kTabNodeId2, {"http://bar.com/"}))))); |
| |
| ASSERT_TRUE(real_processor()->HasLocalChangesForTest()); |
| |
| // Mimic receiving a commit ack for both the tabs and the header entity, |
| // because otherwise it will be treated as conflict, and then local wins. |
| real_processor()->OnCommitCompleted( |
| GetModelTypeStateWithInitialSyncDone(), |
| {CreateSuccessResponse(tab_client_tag1), |
| CreateSuccessResponse(tab_client_tag2), |
| CreateSuccessResponse(kLocalCacheGuid)}, |
| /*error_response_list=*/FailedCommitResponseDataList()); |
| |
| ASSERT_FALSE(real_processor()->HasLocalChangesForTest()); |
| ASSERT_TRUE(real_processor()->IsTrackingMetadata()); |
| |
| // Mimic receiving a remote deletion of all entities. |
| ASSERT_FALSE(bridge()->IsLocalDataOutOfSyncForTest()); |
| syncer::UpdateResponseDataList updates; |
| updates.push_back(CreateTombstone(kLocalCacheGuid)); |
| updates.push_back(CreateTombstone(tab_client_tag1)); |
| updates.push_back(CreateTombstone(tab_client_tag2)); |
| real_processor()->OnUpdateReceived(GetModelTypeStateWithInitialSyncDone(), |
| std::move(updates), |
| /*gc_directive=*/std::nullopt); |
| |
| // State should remain unchanged (deletions ignored). |
| EXPECT_TRUE(bridge()->IsLocalDataOutOfSyncForTest()); |
| EXPECT_THAT( |
| GetAllData(), |
| UnorderedElementsAre( |
| Pair(header_storage_key, |
| EntityDataHasSpecifics(MatchesHeader(kLocalCacheGuid, _, _))), |
| Pair(tab_storage_key1, EntityDataHasSpecifics(MatchesTab( |
| kLocalCacheGuid, kWindowId1, kTabId1, |
| kTabNodeId1, {"http://foo.com/"}))), |
| Pair(tab_storage_key2, EntityDataHasSpecifics(MatchesTab( |
| kLocalCacheGuid, kWindowId1, kTabId2, |
| kTabNodeId2, {"http://bar.com/"}))))); |
| |
| ShutdownBridge(); |
| |
| // Override tab1 with a placeholder tab delegate. Note that, on Android, |
| // tab IDs are persisted by session restore across browser restarts. |
| PlaceholderTabDelegate placeholder_tab1( |
| SessionID::FromSerializedValue(kTabId1)); |
| auto snapshot1 = std::make_unique<TestSyncedTabDelegate>( |
| SessionID::FromSerializedValue(kWindowId1), |
| SessionID::FromSerializedValue(kTabId1), base::DoNothing()); |
| placeholder_tab1.SetPlaceholderTabSyncedTabDelegate(std::move(snapshot1)); |
| |
| ResetWindows(); |
| TestSyncedWindowDelegate* window = AddWindow(kWindowId1); |
| window->OverrideTabAt(0, &placeholder_tab1); |
| AddTab(kWindowId1, "http://bar.com/", kTabId2); |
| |
| // Start the bridge again. |
| InitializeBridge(); |
| |
| // Clear the metadata. This will lose the placeholder tab permanently. |
| real_processor()->ClearMetadataIfStopped(); |
| EXPECT_TRUE(bridge()->IsLocalDataOutOfSyncForTest()); |
| |
| StartSyncing(); |
| ASSERT_TRUE(real_processor()->IsTrackingMetadata()); |
| |
| // The TabNodeId gets re-used now that tab1 is gone. |
| const std::string tab_storage_key2_reused = |
| SessionStore::GetTabStorageKey(kLocalCacheGuid, kTabNodeId1); |
| const std::string tab_client_tag2_reused = |
| SessionStore::GetTabClientTagForTest(kLocalCacheGuid, kTabNodeId1); |
| |
| // The header and non-placeholder tab should both be restored, but the |
| // placeholder tab got lost. |
| // TODO(crbug/1462305): on Android it should be possible to reupload |
| // placeholder tabs. |
| EXPECT_FALSE(bridge()->IsLocalDataOutOfSyncForTest()); |
| EXPECT_THAT(GetAllData(), |
| UnorderedElementsAre( |
| Pair(header_storage_key, EntityDataHasSpecifics(MatchesHeader( |
| kLocalCacheGuid, _, _))), |
| Pair(tab_storage_key2_reused, |
| EntityDataHasSpecifics( |
| MatchesTab(kLocalCacheGuid, kWindowId1, kTabId2, |
| kTabNodeId1, {"http://bar.com/"}))))); |
| } |
| |
| // 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<raw_ptr<const SyncedSession, VectorExperimental>> |
| 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_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. |
| real_processor()->OnUpdateReceived(GetModelTypeStateWithInitialSyncDone(), {}, |
| /*gc_directive=*/std::nullopt); |
| } |
| |
| TEST_F(SessionSyncBridgeTest, ShouldDoGarbageCollection) { |
| // We construct two identical sessions, one modified recently, one modified |
| // more than |kStaleSessionThreshold| ago (28 days ago). |
| const base::Time stale_mtime = base::Time::Now() - base::Days(29); |
| const base::Time recent_mtime = base::Time::Now() - base::Days(27); |
| 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. |
| 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(GetModelTypeStateWithInitialSyncDone(), |
| std::move(updates), |
| /*gc_directive=*/std::nullopt); |
| } |
| |
| 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<EntityData> 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 |