|  | // Copyright 2014 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/sessions_sync_manager.h" | 
|  |  | 
|  | #include <stdint.h> | 
|  |  | 
|  | #include "base/strings/string_util.h" | 
|  | #include "base/strings/stringprintf.h" | 
|  | #include "build/build_config.h" | 
|  | #include "components/prefs/testing_pref_service.h" | 
|  | #include "components/sync/model/sync_change_processor.h" | 
|  | #include "components/sync/model/sync_error_factory_mock.h" | 
|  | #include "components/sync_sessions/mock_sync_sessions_client.h" | 
|  | #include "components/sync_sessions/session_sync_prefs.h" | 
|  | #include "components/sync_sessions/session_sync_test_helper.h" | 
|  | #include "components/sync_sessions/sync_sessions_client.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" | 
|  |  | 
|  | using syncer::DeviceInfo; | 
|  | using syncer::SyncChange; | 
|  | using syncer::SyncChangeList; | 
|  | using syncer::SyncData; | 
|  | using syncer::SyncDataList; | 
|  | using syncer::SyncDataLocal; | 
|  | using syncer::SyncError; | 
|  | using testing::ElementsAre; | 
|  | using testing::Eq; | 
|  | using testing::IsNull; | 
|  |  | 
|  | namespace sync_sessions { | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | const char kCacheGuid[] = "cache_guid"; | 
|  | const char kFoo1[] = "http://foo1/"; | 
|  | const char kFoo2[] = "http://foo2/"; | 
|  | const char kBar1[] = "http://bar1/"; | 
|  | const char kBar2[] = "http://bar2/"; | 
|  | const char kBaz1[] = "http://baz1/"; | 
|  | const char kTag1[] = "tag1"; | 
|  | const char kTag2[] = "tag2"; | 
|  |  | 
|  | std::vector<SessionID> SessionIDs(const std::vector<SessionID::id_type>& ids) { | 
|  | std::vector<SessionID> result; | 
|  | for (SessionID::id_type id : ids) { | 
|  | result.push_back(SessionID::FromSerializedValue(id)); | 
|  | } | 
|  | return result; | 
|  | } | 
|  |  | 
|  | std::string TabNodeIdToTag(const std::string& machine_tag, int tab_node_id) { | 
|  | return base::StringPrintf("%s %d", machine_tag.c_str(), tab_node_id); | 
|  | } | 
|  |  | 
|  | size_t CountIfTagMatches(const SyncChangeList& changes, | 
|  | const std::string& tag) { | 
|  | return std::count_if( | 
|  | changes.begin(), changes.end(), [&tag](const SyncChange& change) { | 
|  | return change.sync_data().GetSpecifics().session().session_tag() == tag; | 
|  | }); | 
|  | } | 
|  |  | 
|  | size_t CountIfTagMatches(const std::vector<const SyncedSession*>& sessions, | 
|  | const std::string& tag) { | 
|  | return std::count_if(sessions.begin(), sessions.end(), | 
|  | [&tag](const SyncedSession* session) { | 
|  | return session->session_tag == tag; | 
|  | }); | 
|  | } | 
|  |  | 
|  | testing::AssertionResult AllOfChangesAreType( | 
|  | const SyncChangeList& changes, | 
|  | const SyncChange::SyncChangeType type) { | 
|  | auto invalid_change = std::find_if(changes.begin(), changes.end(), | 
|  | [&type](const SyncChange& change) { | 
|  | return change.change_type() != type; | 
|  | }); | 
|  | if (invalid_change != changes.end()) { | 
|  | return testing::AssertionFailure() << invalid_change->ToString() | 
|  | << " doesn't match " | 
|  | << SyncChange::ChangeTypeToString(type); | 
|  | } | 
|  | return testing::AssertionSuccess(); | 
|  | } | 
|  |  | 
|  | testing::AssertionResult ChangeTypeMatches( | 
|  | const SyncChangeList& changes, | 
|  | const std::vector<SyncChange::SyncChangeType>& types) { | 
|  | auto types_iter = types.begin(); | 
|  | if (changes.size() != types.size() || | 
|  | std::any_of(changes.begin(), changes.end(), | 
|  | [&types_iter](const SyncChange& change) { | 
|  | SCOPED_TRACE(change.ToString()); | 
|  | return change.change_type() != *types_iter++; | 
|  | })) { | 
|  | std::string type_string; | 
|  | std::for_each(types.begin(), types.end(), | 
|  | [&type_string](const SyncChange::SyncChangeType& type) { | 
|  | (type_string) += SyncChange::ChangeTypeToString(type) + " "; | 
|  | }); | 
|  | std::string change_string; | 
|  | std::for_each(changes.begin(), changes.end(), | 
|  | [&change_string](const SyncChange& change) { | 
|  | change_string += change.ToString(); | 
|  | }); | 
|  | return testing::AssertionFailure() | 
|  | << "Change type mismatch: " << type_string << " vs " | 
|  | << change_string; | 
|  | } | 
|  | return testing::AssertionSuccess(); | 
|  | } | 
|  |  | 
|  | class TestSyncChangeProcessor : public syncer::SyncChangeProcessor { | 
|  | public: | 
|  | explicit TestSyncChangeProcessor(SyncChangeList* output) : output_(output) {} | 
|  | SyncError ProcessSyncChanges(const base::Location& from_here, | 
|  | const SyncChangeList& change_list) override { | 
|  | if (error_.IsSet()) { | 
|  | SyncError error = error_; | 
|  | error_ = SyncError(); | 
|  | return error; | 
|  | } | 
|  |  | 
|  | if (output_) | 
|  | output_->insert(output_->end(), change_list.begin(), change_list.end()); | 
|  | NotifyLocalChangeObservers(); | 
|  |  | 
|  | return SyncError(); | 
|  | } | 
|  |  | 
|  | SyncDataList GetAllSyncData(syncer::ModelType type) const override { | 
|  | return SyncDataList(); | 
|  | } | 
|  |  | 
|  | void AddLocalChangeObserver(syncer::LocalChangeObserver* observer) override { | 
|  | local_change_observers_.AddObserver(observer); | 
|  | } | 
|  | void RemoveLocalChangeObserver( | 
|  | syncer::LocalChangeObserver* observer) override { | 
|  | local_change_observers_.RemoveObserver(observer); | 
|  | } | 
|  |  | 
|  | void NotifyLocalChangeObservers() { | 
|  | const SyncChange empty_change; | 
|  | for (syncer::LocalChangeObserver& observer : local_change_observers_) | 
|  | observer.OnLocalChange(nullptr, empty_change); | 
|  | } | 
|  |  | 
|  | void FailProcessSyncChangesWith(const SyncError& error) { error_ = error; } | 
|  |  | 
|  | private: | 
|  | SyncError error_; | 
|  | SyncChangeList* output_; | 
|  | base::ObserverList<syncer::LocalChangeObserver>::Unchecked | 
|  | local_change_observers_; | 
|  | }; | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | class SessionsSyncManagerTest : public testing::Test { | 
|  | protected: | 
|  | const SessionID kWindowId1 = SessionID::FromSerializedValue(1); | 
|  | const SessionID kWindowId2 = SessionID::FromSerializedValue(2); | 
|  | const std::vector<SessionID> kTabIds1 = SessionIDs({5, 10, 13, 17}); | 
|  | const std::vector<SessionID> kTabIds2 = SessionIDs({7, 15, 18, 20}); | 
|  |  | 
|  | SessionsSyncManagerTest() | 
|  | : local_device_info_(kCacheGuid, | 
|  | "Wayne Gretzky's Hacking Box", | 
|  | "Chromium 10k", | 
|  | "Chrome 10k", | 
|  | sync_pb::SyncEnums_DeviceType_TYPE_LINUX, | 
|  | "device_id"), | 
|  | session_sync_prefs_(&pref_service_) {} | 
|  |  | 
|  | void SetUp() override { | 
|  | SessionSyncPrefs::RegisterProfilePrefs(pref_service_.registry()); | 
|  |  | 
|  | ON_CALL(mock_sync_sessions_client_, GetSessionSyncPrefs()) | 
|  | .WillByDefault(testing::Return(&session_sync_prefs_)); | 
|  | ON_CALL(mock_sync_sessions_client_, GetLocalDeviceInfo()) | 
|  | .WillByDefault(testing::Return(&local_device_info_)); | 
|  | ON_CALL(mock_sync_sessions_client_, GetSyncedWindowDelegatesGetter()) | 
|  | .WillByDefault(testing::Return(&window_getter_)); | 
|  | ON_CALL(mock_sync_sessions_client_, GetLocalSessionEventRouter()) | 
|  | .WillByDefault(testing::Return(window_getter_.router())); | 
|  |  | 
|  | manager_ = | 
|  | std::make_unique<SessionsSyncManager>(&mock_sync_sessions_client_); | 
|  | } | 
|  |  | 
|  | void TearDown() override { | 
|  | test_processor_ = nullptr; | 
|  | helper()->Reset(); | 
|  | manager_.reset(); | 
|  | } | 
|  |  | 
|  | const DeviceInfo* GetLocalDeviceInfo() { return &local_device_info_; } | 
|  |  | 
|  | SessionsSyncManager* manager() { return manager_.get(); } | 
|  | SessionSyncTestHelper* helper() { return &helper_; } | 
|  | MockSyncSessionsClient* mock_sync_sessions_client() { | 
|  | return &mock_sync_sessions_client_; | 
|  | } | 
|  |  | 
|  | void InitWithSyncDataTakeOutput(const SyncDataList& initial_data, | 
|  | SyncChangeList* output) { | 
|  | test_processor_ = new TestSyncChangeProcessor(output); | 
|  | syncer::SyncMergeResult result = manager_->MergeDataAndStartSyncing( | 
|  | syncer::SESSIONS, initial_data, | 
|  | std::unique_ptr<syncer::SyncChangeProcessor>(test_processor_), | 
|  | std::unique_ptr<syncer::SyncErrorFactory>( | 
|  | new syncer::SyncErrorFactoryMock())); | 
|  | EXPECT_FALSE(result.error().IsSet()); | 
|  | } | 
|  |  | 
|  | void InitWithNoSyncData() { | 
|  | InitWithSyncDataTakeOutput(SyncDataList(), nullptr); | 
|  | } | 
|  |  | 
|  | void TriggerProcessSyncChangesError() { | 
|  | test_processor_->FailProcessSyncChangesWith(SyncError( | 
|  | FROM_HERE, SyncError::DATATYPE_ERROR, "Error", syncer::SESSIONS)); | 
|  | } | 
|  |  | 
|  | void VerifyLocalHeaderChange(const SyncChange& change, | 
|  | int num_windows, | 
|  | int num_tabs) { | 
|  | SCOPED_TRACE(change.ToString()); | 
|  | SyncDataLocal data(change.sync_data()); | 
|  | EXPECT_EQ(manager()->current_machine_tag(), data.GetTag()); | 
|  | ASSERT_TRUE(data.GetSpecifics().session().has_header()); | 
|  | EXPECT_FALSE(data.GetSpecifics().session().has_tab()); | 
|  | EXPECT_TRUE(data.GetSpecifics().session().header().has_device_type()); | 
|  | EXPECT_EQ(GetLocalDeviceInfo()->client_name(), | 
|  | data.GetSpecifics().session().header().client_name()); | 
|  | EXPECT_EQ(num_windows, | 
|  | data.GetSpecifics().session().header().window_size()); | 
|  | int tab_count = 0; | 
|  | for (auto& window : data.GetSpecifics().session().header().window()) { | 
|  | tab_count += window.tab_size(); | 
|  | } | 
|  | EXPECT_EQ(num_tabs, tab_count); | 
|  | } | 
|  |  | 
|  | void VerifyLocalTabChange(const SyncChange& change, | 
|  | int num_navigations, | 
|  | std::string final_url) { | 
|  | SCOPED_TRACE(change.ToString()); | 
|  | SyncDataLocal data(change.sync_data()); | 
|  | EXPECT_TRUE(base::StartsWith(data.GetTag(), | 
|  | manager()->current_machine_tag(), | 
|  | base::CompareCase::SENSITIVE)); | 
|  | EXPECT_FALSE(data.GetSpecifics().session().has_header()); | 
|  | ASSERT_TRUE(data.GetSpecifics().session().has_tab()); | 
|  | ASSERT_EQ(num_navigations, | 
|  | data.GetSpecifics().session().tab().navigation_size()); | 
|  | EXPECT_EQ(final_url, data.GetSpecifics() | 
|  | .session() | 
|  | .tab() | 
|  | .navigation(num_navigations - 1) | 
|  | .virtual_url()); | 
|  | } | 
|  |  | 
|  | SyncChangeList* FilterOutLocalHeaderChanges(SyncChangeList* list) { | 
|  | SyncChangeList::iterator it = list->begin(); | 
|  | bool found = false; | 
|  | while (it != list->end()) { | 
|  | if (it->sync_data().IsLocal() && | 
|  | SyncDataLocal(it->sync_data()).GetTag() == | 
|  | manager_->current_machine_tag()) { | 
|  | EXPECT_TRUE(SyncChange::ACTION_ADD == it->change_type() || | 
|  | SyncChange::ACTION_UPDATE == it->change_type()); | 
|  | it = list->erase(it); | 
|  | found = true; | 
|  | } else { | 
|  | ++it; | 
|  | } | 
|  | } | 
|  | EXPECT_TRUE(found); | 
|  | return list; | 
|  | } | 
|  |  | 
|  | SyncChange MakeRemoteChange(const sync_pb::SessionSpecifics& specifics, | 
|  | SyncChange::SyncChangeType type) const { | 
|  | return SyncChange(FROM_HERE, type, CreateRemoteData(specifics)); | 
|  | } | 
|  |  | 
|  | void AddTabsToChangeList(const std::vector<sync_pb::SessionSpecifics>& batch, | 
|  | SyncChange::SyncChangeType type, | 
|  | SyncChangeList* change_list) const { | 
|  | for (const auto& specifics : batch) { | 
|  | change_list->push_back( | 
|  | SyncChange(FROM_HERE, type, CreateRemoteData(specifics))); | 
|  | } | 
|  | } | 
|  |  | 
|  | void AddToSyncDataList(const sync_pb::SessionSpecifics& specifics, | 
|  | SyncDataList* list, | 
|  | base::Time mtime) const { | 
|  | list->push_back(CreateRemoteData(specifics, mtime)); | 
|  | } | 
|  |  | 
|  | void AddTabsToSyncDataList(const std::vector<sync_pb::SessionSpecifics>& tabs, | 
|  | SyncDataList* list) const { | 
|  | for (size_t i = 0; i < tabs.size(); ++i) { | 
|  | AddToSyncDataList(tabs[i], list, base::Time::FromInternalValue(i + 1)); | 
|  | } | 
|  | } | 
|  |  | 
|  | SyncData CreateRemoteData(const sync_pb::SessionSpecifics& specifics, | 
|  | base::Time mtime = base::Time()) const { | 
|  | sync_pb::EntitySpecifics entity; | 
|  | entity.mutable_session()->CopyFrom(specifics); | 
|  | return CreateRemoteData(entity, mtime); | 
|  | } | 
|  |  | 
|  | SyncData CreateRemoteData(const sync_pb::EntitySpecifics& entity, | 
|  | base::Time mtime = base::Time()) const { | 
|  | // The server ID is never relevant to these tests, so just use 1. | 
|  | return SyncData::CreateRemoteData( | 
|  | 1, entity, mtime, | 
|  | SessionsSyncManager::TagHashFromSpecifics(entity.session())); | 
|  | } | 
|  |  | 
|  | syncer::SyncDataList GetDataFromChanges( | 
|  | const syncer::SyncChangeList& changes) { | 
|  | syncer::SyncDataList data_list; | 
|  | for (auto& change : changes) { | 
|  | syncer::SyncDataLocal change_data(change.sync_data()); | 
|  | bool found = false; | 
|  | for (auto&& data : data_list) { | 
|  | syncer::SyncDataLocal local_data(data); | 
|  | if (local_data.GetTag() == change_data.GetTag()) { | 
|  | data = change.sync_data(); | 
|  | found = true; | 
|  | break; | 
|  | } | 
|  | } | 
|  | if (!found) | 
|  | data_list.push_back(change_data); | 
|  | } | 
|  | return data_list; | 
|  | } | 
|  |  | 
|  | syncer::SyncDataList ConvertToRemote(const syncer::SyncDataList& in) { | 
|  | syncer::SyncDataList out; | 
|  | for (auto& data : in) { | 
|  | out.push_back(CreateRemoteData(data.GetSpecifics())); | 
|  | } | 
|  | return out; | 
|  | } | 
|  |  | 
|  | void ResetWindows() { return window_getter_.ResetWindows(); } | 
|  |  | 
|  | TestSyncedWindowDelegate* AddWindow( | 
|  | sync_pb::SessionWindow_BrowserType type = | 
|  | sync_pb::SessionWindow_BrowserType_TYPE_TABBED) { | 
|  | return window_getter_.AddWindow(type); | 
|  | } | 
|  |  | 
|  | TestSyncedTabDelegate* AddTab(SessionID window_id, const std::string& url) { | 
|  | TestSyncedTabDelegate* tab = window_getter_.AddTab(window_id); | 
|  | tab->Navigate(url); | 
|  | return tab; | 
|  | } | 
|  |  | 
|  | private: | 
|  | const syncer::DeviceInfo local_device_info_; | 
|  | TestingPrefServiceSimple pref_service_; | 
|  | SessionSyncPrefs session_sync_prefs_; | 
|  | testing::NiceMock<MockSyncSessionsClient> mock_sync_sessions_client_; | 
|  | std::unique_ptr<SessionsSyncManager> manager_; | 
|  | SessionSyncTestHelper helper_; | 
|  | TestSyncChangeProcessor* test_processor_ = nullptr; | 
|  | TestSyncedWindowDelegatesGetter window_getter_; | 
|  | }; | 
|  |  | 
|  | // Tests that the local session header objects is created properly in | 
|  | // presence of no other session activity, once and only once. | 
|  | TEST_F(SessionsSyncManagerTest, MergeLocalSessionNoTabs) { | 
|  | // Add a single window with no tabs. | 
|  | AddWindow(); | 
|  |  | 
|  | SyncChangeList out; | 
|  | InitWithSyncDataTakeOutput(SyncDataList(), &out); | 
|  | EXPECT_FALSE(manager()->current_machine_tag().empty()); | 
|  |  | 
|  | // Header creation + update. | 
|  | ASSERT_TRUE(ChangeTypeMatches( | 
|  | out, {SyncChange::ACTION_ADD, SyncChange::ACTION_UPDATE})); | 
|  | EXPECT_EQ(out.size(), | 
|  | CountIfTagMatches(out, manager()->current_machine_tag())); | 
|  | VerifyLocalHeaderChange(out[0], 0, 0); | 
|  | VerifyLocalHeaderChange(out[1], 0, 0); | 
|  |  | 
|  | // Now take that header node and feed it in as input. | 
|  | SyncData d = CreateRemoteData(out[1].sync_data().GetSpecifics()); | 
|  | SyncDataList in = {d}; | 
|  | out.clear(); | 
|  | manager()->StopSyncing(syncer::SESSIONS); | 
|  | InitWithSyncDataTakeOutput(in, &out); | 
|  |  | 
|  | ASSERT_TRUE(ChangeTypeMatches(out, {SyncChange::ACTION_UPDATE})); | 
|  | EXPECT_TRUE(out[0].sync_data().GetSpecifics().session().has_header()); | 
|  | } | 
|  |  | 
|  | // Ensure that tabbed windows from a previous session are preserved if no | 
|  | // windows are present on startup. | 
|  | TEST_F(SessionsSyncManagerTest, PreserveTabbedDataNoWindows) { | 
|  | syncer::SyncDataList in; | 
|  | syncer::SyncChangeList out; | 
|  |  | 
|  | // Set up one tab and start sync with it. | 
|  | TestSyncedTabDelegate* tab = AddTab(AddWindow()->GetSessionId(), kFoo1); | 
|  | tab->Navigate(kFoo2); | 
|  | InitWithSyncDataTakeOutput(in, &out); | 
|  |  | 
|  | // There should be two entities, a header and a tab. | 
|  | in = GetDataFromChanges(out); | 
|  | out.clear(); | 
|  | ASSERT_EQ(2U, in.size()); | 
|  |  | 
|  | // Resync, using the previous sync data, but with no windows open now. | 
|  | manager()->StopSyncing(syncer::SESSIONS); | 
|  | ResetWindows(); | 
|  | InitWithSyncDataTakeOutput(ConvertToRemote(in), &out); | 
|  |  | 
|  | // There should be one change to the rewritten header. | 
|  | ASSERT_TRUE(ChangeTypeMatches(out, {SyncChange::ACTION_UPDATE})); | 
|  | VerifyLocalHeaderChange(out[0], 1, 1); | 
|  |  | 
|  | // SessionId should not be rewritten on restore. | 
|  | int restored_tab_id = | 
|  | out[0].sync_data().GetSpecifics().session().header().window(0).tab(0); | 
|  | EXPECT_EQ(tab->GetSessionId().id(), restored_tab_id); | 
|  | out.clear(); | 
|  |  | 
|  | // Now actually resurrect the native data, which will end up having different | 
|  | // native ids, but the tab has the same sync id as before. | 
|  | AddWindow()->OverrideTabAt(0, tab); | 
|  | tab->Navigate(kBar1); | 
|  |  | 
|  | ASSERT_TRUE(ChangeTypeMatches( | 
|  | out, {SyncChange::ACTION_UPDATE, SyncChange::ACTION_UPDATE})); | 
|  | VerifyLocalTabChange(out[0], 3, kBar1); | 
|  | VerifyLocalHeaderChange(out[1], 1, 1); | 
|  | } | 
|  |  | 
|  | // Ensure that tabbed windows from a previous session are preserved if only | 
|  | // transient windows are present at startup. | 
|  | TEST_F(SessionsSyncManagerTest, PreserveTabbedDataCustomTab) { | 
|  | syncer::SyncDataList in; | 
|  | syncer::SyncChangeList out; | 
|  |  | 
|  | // Set up one tab and start sync with it. | 
|  | TestSyncedWindowDelegate* window = AddWindow(); | 
|  | TestSyncedTabDelegate* tab = AddTab(window->GetSessionId(), kFoo1); | 
|  | tab->Navigate(kFoo2); | 
|  | InitWithSyncDataTakeOutput(in, &out); | 
|  |  | 
|  | // There should be two entities, a header and a tab. | 
|  | in = GetDataFromChanges(out); | 
|  | out.clear(); | 
|  | ASSERT_EQ(2U, in.size()); | 
|  |  | 
|  | // Resync, using the previous sync data, but with only a custom tab open. | 
|  | manager()->StopSyncing(syncer::SESSIONS); | 
|  | ResetWindows(); | 
|  | window = AddWindow(sync_pb::SessionWindow_BrowserType_TYPE_CUSTOM_TAB); | 
|  | AddTab(window->GetSessionId(), kBar1); | 
|  | InitWithSyncDataTakeOutput(ConvertToRemote(in), &out); | 
|  |  | 
|  | // The previous session should be preserved, together with the new custom tab. | 
|  | ASSERT_TRUE(ChangeTypeMatches( | 
|  | out, {SyncChange::ACTION_ADD, SyncChange::ACTION_UPDATE})); | 
|  | VerifyLocalTabChange(out[0], 1, kBar1); | 
|  | VerifyLocalHeaderChange(out[1], 2, 2); | 
|  | } | 
|  |  | 
|  | // Tests MergeDataAndStartSyncing with sync data but no local data. | 
|  | TEST_F(SessionsSyncManagerTest, MergeWithInitialForeignSession) { | 
|  | std::vector<sync_pb::SessionSpecifics> tabs1; | 
|  | sync_pb::SessionSpecifics meta( | 
|  | helper()->BuildForeignSession(kTag1, kTabIds1, &tabs1)); | 
|  | // Add a second window. | 
|  | helper()->AddWindowSpecifics(kWindowId2, kTabIds2, &meta); | 
|  |  | 
|  | // Set up initial data. | 
|  | SyncDataList initial_data; | 
|  | initial_data.push_back(CreateRemoteData(meta)); | 
|  | AddTabsToSyncDataList(tabs1, &initial_data); | 
|  | for (auto tab_id : kTabIds2) { | 
|  | sync_pb::EntitySpecifics entity; | 
|  | helper()->BuildTabSpecifics(kTag1, kWindowId1, tab_id, | 
|  | entity.mutable_session()); | 
|  | initial_data.push_back(CreateRemoteData(entity)); | 
|  | } | 
|  |  | 
|  | SyncChangeList output; | 
|  | InitWithSyncDataTakeOutput(initial_data, &output); | 
|  | EXPECT_TRUE(FilterOutLocalHeaderChanges(&output)->empty()); | 
|  |  | 
|  | std::vector<const SyncedSession*> foreign_sessions; | 
|  | ASSERT_TRUE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  | ASSERT_EQ(1U, foreign_sessions.size()); | 
|  | std::vector<std::vector<SessionID>> session_reference; | 
|  | session_reference.push_back(kTabIds1); | 
|  | session_reference.push_back(kTabIds2); | 
|  | helper()->VerifySyncedSession(kTag1, session_reference, | 
|  | *(foreign_sessions[0])); | 
|  | } | 
|  |  | 
|  | // Ensure model association associates the pre-existing tabs. | 
|  | TEST_F(SessionsSyncManagerTest, MergeLocalSessionExistingTabs) { | 
|  | TestSyncedWindowDelegate* window = AddWindow(); | 
|  | SessionID window_id = window->GetSessionId(); | 
|  | TestSyncedTabDelegate* tab = AddTab(window_id, kFoo1); | 
|  | tab->Navigate(kBar1);  // Adds back entry. | 
|  | tab->Navigate(kBaz1);  // Adds back entry. | 
|  | TestSyncedTabDelegate* tab2 = AddTab(window_id, kFoo2); | 
|  | tab2->Navigate(kBar2);  // Adds back entry. | 
|  |  | 
|  | SyncChangeList out; | 
|  | InitWithSyncDataTakeOutput(SyncDataList(), &out); | 
|  | // Header creation, add two tabs, header update. | 
|  | ASSERT_TRUE( | 
|  | ChangeTypeMatches(out, | 
|  | {SyncChange::ACTION_ADD, SyncChange::ACTION_ADD, | 
|  | SyncChange::ACTION_ADD, SyncChange::ACTION_UPDATE})); | 
|  | EXPECT_EQ(out.size(), | 
|  | CountIfTagMatches(out, manager()->current_machine_tag())); | 
|  |  | 
|  | // Check that this machine's data is not included in the foreign windows. | 
|  | std::vector<const SyncedSession*> foreign_sessions; | 
|  | ASSERT_FALSE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  |  | 
|  | VerifyLocalHeaderChange(out[0], 0, 0); | 
|  | VerifyLocalTabChange(out[1], tab->GetEntryCount(), kBaz1); | 
|  | VerifyLocalTabChange(out[2], tab2->GetEntryCount(), kBar2); | 
|  | VerifyLocalHeaderChange(out[3], 1, 2); | 
|  | } | 
|  |  | 
|  | // Ensure that the last known device name is reported. | 
|  | TEST_F(SessionsSyncManagerTest, MergeLocalSessionName) { | 
|  | const std::string kModifiedDeviceName = "New Device Name"; | 
|  |  | 
|  | SyncChangeList out; | 
|  | InitWithSyncDataTakeOutput(SyncDataList(), &out); | 
|  | syncer::SyncDataList initial_data = GetDataFromChanges(out); | 
|  | // Local header expected. | 
|  | ASSERT_EQ(1U, initial_data.size()); | 
|  |  | 
|  | // Change local device name to |kModifiedDeviceName|. | 
|  | const DeviceInfo new_device_info( | 
|  | kCacheGuid, kModifiedDeviceName, "Chromium 10k", "Chrome 10k", | 
|  | sync_pb::SyncEnums_DeviceType_TYPE_LINUX, "device_id"); | 
|  | ON_CALL(*mock_sync_sessions_client(), GetLocalDeviceInfo()) | 
|  | .WillByDefault(testing::Return(&new_device_info)); | 
|  |  | 
|  | // Restart the manager, now that the local device name has changed. | 
|  | manager()->StopSyncing(syncer::SESSIONS); | 
|  | out.clear(); | 
|  | InitWithSyncDataTakeOutput(ConvertToRemote(initial_data), &out); | 
|  |  | 
|  | EXPECT_EQ(kModifiedDeviceName, manager()->GetCurrentSessionNameForTest()); | 
|  | } | 
|  |  | 
|  | // This is a combination of MergeWithInitialForeignSession and | 
|  | // MergeLocalSessionExistingTabs. We repeat some checks performed in each of | 
|  | // those tests to ensure the common mixed scenario works. | 
|  | TEST_F(SessionsSyncManagerTest, MergeWithLocalAndForeignTabs) { | 
|  | // Local. | 
|  | TestSyncedTabDelegate* tab = AddTab(AddWindow()->GetSessionId(), kFoo1); | 
|  | tab->Navigate(kFoo2); | 
|  |  | 
|  | // Foreign. | 
|  | std::vector<sync_pb::SessionSpecifics> tabs1; | 
|  | sync_pb::SessionSpecifics meta( | 
|  | helper()->BuildForeignSession(kTag1, kTabIds1, &tabs1)); | 
|  | SyncDataList foreign_data; | 
|  | foreign_data.push_back(CreateRemoteData(meta)); | 
|  | AddTabsToSyncDataList(tabs1, &foreign_data); | 
|  |  | 
|  | SyncChangeList out; | 
|  | InitWithSyncDataTakeOutput(foreign_data, &out); | 
|  |  | 
|  | // Should be one header add, 1 tab add, and one header update. | 
|  | ASSERT_TRUE(ChangeTypeMatches(out, | 
|  | {SyncChange::ACTION_ADD, SyncChange::ACTION_ADD, | 
|  | SyncChange::ACTION_UPDATE})); | 
|  | EXPECT_EQ(out.size(), | 
|  | CountIfTagMatches(out, manager()->current_machine_tag())); | 
|  |  | 
|  | // Verify local data. | 
|  | VerifyLocalHeaderChange(out[0], 0, 0); | 
|  | VerifyLocalTabChange(out[1], tab->GetEntryCount(), kFoo2); | 
|  | VerifyLocalHeaderChange(out[2], 1, 1); | 
|  |  | 
|  | // Verify foreign data. | 
|  | std::vector<const SyncedSession*> foreign_sessions; | 
|  | ASSERT_TRUE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  | std::vector<std::vector<SessionID>> session_reference; | 
|  | session_reference.push_back(kTabIds1); | 
|  | helper()->VerifySyncedSession(kTag1, session_reference, | 
|  | *(foreign_sessions[0])); | 
|  | // There should be one and only one foreign session. If VerifySyncedSession | 
|  | // was successful above this EXPECT call ensures the local session didn't | 
|  | // get mistakenly added to foreign tracking (Similar to ExistingTabs test). | 
|  | EXPECT_EQ(1U, foreign_sessions.size()); | 
|  | } | 
|  |  | 
|  | // Tests the common scenario.  Merge with both local and foreign session data | 
|  | // followed by updates flowing from sync and local. | 
|  | TEST_F(SessionsSyncManagerTest, UpdatesAfterMixedMerge) { | 
|  | // Add local and foreign data. | 
|  | TestSyncedTabDelegate* tab = AddTab(AddWindow()->GetSessionId(), kFoo1); | 
|  | tab->Navigate(kFoo2); | 
|  | AddTab(AddWindow()->GetSessionId(), kBar1); | 
|  |  | 
|  | SyncDataList foreign_data1; | 
|  | std::vector<std::vector<SessionID>> meta1_reference; | 
|  | sync_pb::SessionSpecifics meta1; | 
|  |  | 
|  | meta1_reference.push_back(kTabIds1); | 
|  | std::vector<sync_pb::SessionSpecifics> tabs1; | 
|  | meta1 = helper()->BuildForeignSession(kTag1, kTabIds1, &tabs1); | 
|  | foreign_data1.push_back(CreateRemoteData(meta1)); | 
|  | AddTabsToSyncDataList(tabs1, &foreign_data1); | 
|  |  | 
|  | SyncChangeList out; | 
|  | InitWithSyncDataTakeOutput(foreign_data1, &out); | 
|  |  | 
|  | // 1 header add, two tab adds, one header update. | 
|  | ASSERT_TRUE( | 
|  | ChangeTypeMatches(out, | 
|  | {SyncChange::ACTION_ADD, SyncChange::ACTION_ADD, | 
|  | SyncChange::ACTION_ADD, SyncChange::ACTION_UPDATE})); | 
|  | EXPECT_EQ(out.size(), | 
|  | CountIfTagMatches(out, manager()->current_machine_tag())); | 
|  | VerifyLocalHeaderChange(out[3], 2, 2); | 
|  |  | 
|  | // Add a second window to the foreign session. | 
|  | meta1_reference.push_back(kTabIds2); | 
|  | helper()->AddWindowSpecifics(kWindowId2, kTabIds2, &meta1); | 
|  | std::vector<sync_pb::SessionSpecifics> tabs2; | 
|  | tabs2.resize(kTabIds2.size()); | 
|  | for (size_t i = 0; i < kTabIds2.size(); ++i) { | 
|  | helper()->BuildTabSpecifics(kTag1, kWindowId2, kTabIds2[i], &tabs2[i]); | 
|  | } | 
|  |  | 
|  | SyncChangeList changes; | 
|  | changes.push_back(MakeRemoteChange(meta1, SyncChange::ACTION_UPDATE)); | 
|  | AddTabsToChangeList(tabs2, SyncChange::ACTION_ADD, &changes); | 
|  | manager()->ProcessSyncChanges(FROM_HERE, changes); | 
|  | changes.clear(); | 
|  |  | 
|  | // Check that the foreign session was associated and retrieve the data. | 
|  | std::vector<const SyncedSession*> foreign_sessions; | 
|  | ASSERT_TRUE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  | ASSERT_EQ(1U, foreign_sessions.size()); | 
|  | ASSERT_EQ(4U, foreign_sessions[0] | 
|  | ->windows.find(kWindowId1) | 
|  | ->second->wrapped_window.tabs.size()); | 
|  | ASSERT_EQ(4U, foreign_sessions[0] | 
|  | ->windows.find(kWindowId2) | 
|  | ->second->wrapped_window.tabs.size()); | 
|  | helper()->VerifySyncedSession(kTag1, meta1_reference, *(foreign_sessions[0])); | 
|  |  | 
|  | // Add a new foreign session. | 
|  | const std::vector<SessionID> tag2_tab_list = SessionIDs({107, 115}); | 
|  | std::vector<sync_pb::SessionSpecifics> tag2_tabs; | 
|  | sync_pb::SessionSpecifics meta2( | 
|  | helper()->BuildForeignSession(kTag2, tag2_tab_list, &tag2_tabs)); | 
|  | changes.push_back(MakeRemoteChange(meta2, SyncChange::ACTION_ADD)); | 
|  | AddTabsToChangeList(tag2_tabs, SyncChange::ACTION_ADD, &changes); | 
|  |  | 
|  | manager()->ProcessSyncChanges(FROM_HERE, changes); | 
|  | changes.clear(); | 
|  |  | 
|  | ASSERT_TRUE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  | std::vector<std::vector<SessionID>> meta2_reference; | 
|  | meta2_reference.push_back(tag2_tab_list); | 
|  | ASSERT_EQ(2U, foreign_sessions.size()); | 
|  | ASSERT_EQ(2U, foreign_sessions[1] | 
|  | ->windows.find(kWindowId1) | 
|  | ->second->wrapped_window.tabs.size()); | 
|  | helper()->VerifySyncedSession(kTag2, meta2_reference, *(foreign_sessions[1])); | 
|  | foreign_sessions.clear(); | 
|  |  | 
|  | // Remove a tab from a window. | 
|  | meta1_reference[0].pop_back(); | 
|  | sync_pb::SessionWindow* win = meta1.mutable_header()->mutable_window(0); | 
|  | win->clear_tab(); | 
|  | for (std::vector<SessionID>::const_iterator iter = kTabIds1.begin(); | 
|  | iter + 1 != kTabIds1.end(); ++iter) { | 
|  | win->add_tab(iter->id()); | 
|  | } | 
|  | SyncChangeList removal; | 
|  | removal.push_back(MakeRemoteChange(meta1, SyncChange::ACTION_UPDATE)); | 
|  | AddTabsToChangeList(tabs1, SyncChange::ACTION_UPDATE, &removal); | 
|  | manager()->ProcessSyncChanges(FROM_HERE, removal); | 
|  |  | 
|  | ASSERT_TRUE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  | ASSERT_EQ(2U, foreign_sessions.size()); | 
|  | ASSERT_EQ(3U, foreign_sessions[0] | 
|  | ->windows.find(kWindowId1) | 
|  | ->second->wrapped_window.tabs.size()); | 
|  | helper()->VerifySyncedSession(kTag1, meta1_reference, *(foreign_sessions[0])); | 
|  | } | 
|  |  | 
|  | // Tests that this SyncSessionManager knows how to delete foreign sessions | 
|  | // if it wants to. | 
|  | TEST_F(SessionsSyncManagerTest, DeleteForeignSession) { | 
|  | InitWithNoSyncData(); | 
|  | SyncChangeList changes; | 
|  |  | 
|  | std::vector<const SyncedSession*> foreign_sessions; | 
|  | ASSERT_FALSE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  | manager()->DeleteForeignSessionInternal(kTag1, &changes); | 
|  | ASSERT_FALSE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  | EXPECT_TRUE(changes.empty()); | 
|  |  | 
|  | // Fill an instance of session specifics with a foreign session's data. | 
|  | std::vector<sync_pb::SessionSpecifics> tabs; | 
|  | sync_pb::SessionSpecifics meta( | 
|  | helper()->BuildForeignSession(kTag1, kTabIds1, &tabs)); | 
|  |  | 
|  | // Update associator with the session's meta node, window, and tabs. | 
|  | UpdateTrackerWithSpecifics(meta, base::Time(), &manager()->session_tracker_); | 
|  | for (std::vector<sync_pb::SessionSpecifics>::iterator iter = tabs.begin(); | 
|  | iter != tabs.end(); ++iter) { | 
|  | UpdateTrackerWithSpecifics(*iter, base::Time(), | 
|  | &manager()->session_tracker_); | 
|  | } | 
|  | ASSERT_TRUE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  | ASSERT_EQ(1U, foreign_sessions.size()); | 
|  |  | 
|  | // Now delete the foreign session. | 
|  | manager()->DeleteForeignSessionInternal(kTag1, &changes); | 
|  | EXPECT_FALSE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  |  | 
|  | EXPECT_EQ(5U, changes.size()); | 
|  | ASSERT_TRUE(AllOfChangesAreType(changes, SyncChange::ACTION_DELETE)); | 
|  | std::set<std::string> expected_tags(&kTag1, &kTag1 + 1); | 
|  | for (int i = 0; i < 5; ++i) | 
|  | expected_tags.insert(TabNodeIdToTag(kTag1, i)); | 
|  |  | 
|  | for (int i = 0; i < 5; ++i) { | 
|  | SCOPED_TRACE(changes[i].ToString()); | 
|  | EXPECT_TRUE(changes[i].IsValid()); | 
|  | EXPECT_TRUE(changes[i].sync_data().IsValid()); | 
|  | EXPECT_EQ(1U, expected_tags.erase( | 
|  | SyncDataLocal(changes[i].sync_data()).GetTag())); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Write a foreign session to a node, with the tabs arriving first, and then | 
|  | // retrieve it. | 
|  | TEST_F(SessionsSyncManagerTest, WriteForeignSessionToNodeTabsFirst) { | 
|  | InitWithNoSyncData(); | 
|  |  | 
|  | // Fill an instance of session specifics with a foreign session's data. | 
|  | std::string tag = "tag1"; | 
|  | std::vector<sync_pb::SessionSpecifics> tabs1; | 
|  | sync_pb::SessionSpecifics meta( | 
|  | helper()->BuildForeignSession(tag, kTabIds1, &tabs1)); | 
|  |  | 
|  | SyncChangeList adds; | 
|  | // Add tabs for first window, then the meta node. | 
|  | AddTabsToChangeList(tabs1, SyncChange::ACTION_ADD, &adds); | 
|  | adds.push_back(MakeRemoteChange(meta, SyncChange::ACTION_ADD)); | 
|  | manager()->ProcessSyncChanges(FROM_HERE, adds); | 
|  |  | 
|  | // Check that the foreign session was associated and retrieve the data. | 
|  | std::vector<const SyncedSession*> foreign_sessions; | 
|  | ASSERT_TRUE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  | ASSERT_EQ(1U, foreign_sessions.size()); | 
|  | std::vector<std::vector<SessionID>> session_reference; | 
|  | session_reference.push_back(kTabIds1); | 
|  | helper()->VerifySyncedSession(tag, session_reference, *(foreign_sessions[0])); | 
|  | } | 
|  |  | 
|  | // Write a foreign session to a node with some tabs that never arrive. | 
|  | TEST_F(SessionsSyncManagerTest, WriteForeignSessionToNodeMissingTabs) { | 
|  | InitWithNoSyncData(); | 
|  |  | 
|  | // Fill an instance of session specifics with a foreign session's data. | 
|  | std::string tag = "tag1"; | 
|  | std::vector<sync_pb::SessionSpecifics> tabs1; | 
|  | sync_pb::SessionSpecifics meta( | 
|  | helper()->BuildForeignSession(tag, kTabIds1, &tabs1)); | 
|  | // Add a second window, but this time only create two tab nodes, despite the | 
|  | // window expecting four tabs. | 
|  | helper()->AddWindowSpecifics(kWindowId2, kTabIds2, &meta); | 
|  | std::vector<sync_pb::SessionSpecifics> tabs2; | 
|  | tabs2.resize(2); | 
|  | for (size_t i = 0; i < 2; ++i) { | 
|  | helper()->BuildTabSpecifics(tag, kWindowId1, kTabIds2[i], &tabs2[i]); | 
|  | } | 
|  |  | 
|  | SyncChangeList changes; | 
|  | changes.push_back(MakeRemoteChange(meta, SyncChange::ACTION_ADD)); | 
|  | AddTabsToChangeList(tabs1, SyncChange::ACTION_ADD, &changes); | 
|  | AddTabsToChangeList(tabs2, SyncChange::ACTION_ADD, &changes); | 
|  | manager()->ProcessSyncChanges(FROM_HERE, changes); | 
|  | changes.clear(); | 
|  |  | 
|  | // Check that the foreign session was associated and retrieve the data. | 
|  | std::vector<const SyncedSession*> foreign_sessions; | 
|  | ASSERT_TRUE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  | ASSERT_EQ(1U, foreign_sessions.size()); | 
|  | ASSERT_EQ(2U, foreign_sessions[0]->windows.size()); | 
|  | ASSERT_EQ(4U, foreign_sessions[0] | 
|  | ->windows.find(kWindowId1) | 
|  | ->second->wrapped_window.tabs.size()); | 
|  | ASSERT_EQ(4U, foreign_sessions[0] | 
|  | ->windows.find(kWindowId2) | 
|  | ->second->wrapped_window.tabs.size()); | 
|  |  | 
|  | // Close the second window. | 
|  | meta.mutable_header()->clear_window(); | 
|  | helper()->AddWindowSpecifics(kWindowId1, kTabIds1, &meta); | 
|  | changes.push_back(MakeRemoteChange(meta, SyncChange::ACTION_UPDATE)); | 
|  | // Update associator with the session's meta node containing one window. | 
|  | manager()->ProcessSyncChanges(FROM_HERE, changes); | 
|  |  | 
|  | // Check that the foreign session was associated and retrieve the data. | 
|  | foreign_sessions.clear(); | 
|  | ASSERT_TRUE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  | ASSERT_EQ(1U, foreign_sessions.size()); | 
|  | ASSERT_EQ(1U, foreign_sessions[0]->windows.size()); | 
|  | std::vector<std::vector<SessionID>> session_reference; | 
|  | session_reference.push_back(kTabIds1); | 
|  | helper()->VerifySyncedSession(tag, session_reference, *(foreign_sessions[0])); | 
|  | } | 
|  |  | 
|  | // Tests that the SessionsSyncManager can handle a remote client deleting | 
|  | // sync nodes that belong to this local session. | 
|  | TEST_F(SessionsSyncManagerTest, ProcessRemoteDeleteOfLocalSession) { | 
|  | SessionID window_id = AddWindow()->GetSessionId(); | 
|  | SyncChangeList out; | 
|  | InitWithSyncDataTakeOutput(SyncDataList(), &out); | 
|  | ASSERT_EQ(2U, out.size()); | 
|  |  | 
|  | SyncChangeList changes; | 
|  | changes.push_back(MakeRemoteChange( | 
|  | out[1].sync_data().GetSpecifics().session(), SyncChange::ACTION_DELETE)); | 
|  | out.clear(); | 
|  | manager()->ProcessSyncChanges(FROM_HERE, changes); | 
|  | EXPECT_TRUE(manager()->local_tab_pool_out_of_sync_); | 
|  | EXPECT_TRUE(out.empty());  // ChangeProcessor shouldn't see any activity. | 
|  |  | 
|  | // This should trigger repair of the TabNodePool. | 
|  | AddTab(window_id, kFoo1); | 
|  | EXPECT_FALSE(manager()->local_tab_pool_out_of_sync_); | 
|  |  | 
|  | // Rebuilding associations will trigger an initial header add and update, | 
|  | // coupled with the tab creation and the header update to reflect the new tab. | 
|  | // In total, that means four changes. | 
|  | ASSERT_TRUE( | 
|  | ChangeTypeMatches(out, | 
|  | {SyncChange::ACTION_ADD, SyncChange::ACTION_UPDATE, | 
|  | SyncChange::ACTION_ADD, SyncChange::ACTION_UPDATE})); | 
|  |  | 
|  | // Verify the actual content. | 
|  | VerifyLocalTabChange(out[2], 1, kFoo1); | 
|  | VerifyLocalHeaderChange(out[3], 1, 1); | 
|  |  | 
|  | // Verify TabLinks. | 
|  | int tab_node_id = out[2].sync_data().GetSpecifics().session().tab_node_id(); | 
|  | int tab_id = out[2].sync_data().GetSpecifics().session().tab().tab_id(); | 
|  | EXPECT_EQ(tab_id, manager() | 
|  | ->session_tracker_ | 
|  | .LookupTabIdFromTabNodeId( | 
|  | manager()->current_machine_tag(), tab_node_id) | 
|  | .id()); | 
|  | } | 
|  |  | 
|  | // Test that receiving a session delete from sync removes the session | 
|  | // from tracking. | 
|  | TEST_F(SessionsSyncManagerTest, ProcessForeignDelete) { | 
|  | InitWithNoSyncData(); | 
|  | std::vector<sync_pb::SessionSpecifics> tabs1; | 
|  | sync_pb::SessionSpecifics meta( | 
|  | helper()->BuildForeignSession("tag1", kTabIds1, &tabs1)); | 
|  |  | 
|  | SyncChangeList changes; | 
|  | changes.push_back(MakeRemoteChange(meta, SyncChange::ACTION_ADD)); | 
|  | AddTabsToChangeList(tabs1, SyncChange::ACTION_ADD, &changes); | 
|  | manager()->ProcessSyncChanges(FROM_HERE, changes); | 
|  |  | 
|  | std::vector<const SyncedSession*> foreign_sessions; | 
|  | ASSERT_TRUE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  | ASSERT_EQ(1U, foreign_sessions.size()); | 
|  |  | 
|  | changes.clear(); | 
|  | foreign_sessions.clear(); | 
|  | changes.push_back(MakeRemoteChange(meta, SyncChange::ACTION_DELETE)); | 
|  | manager()->ProcessSyncChanges(FROM_HERE, changes); | 
|  |  | 
|  | EXPECT_FALSE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  | } | 
|  |  | 
|  | TEST_F(SessionsSyncManagerTest, ProcessForeignDeleteTabs) { | 
|  | SyncDataList foreign_data; | 
|  | base::Time stale_mtime = base::Time::Now() - base::TimeDelta::FromDays(15); | 
|  | std::string session_tag = "tag1"; | 
|  |  | 
|  | // 1 will not have ownership changed. | 
|  | // 2 will not be updated, but header will stop owning. | 
|  | // 3 will be deleted before header stops owning. | 
|  | // 4 will be deleted after header stops owning. | 
|  | // 5 will be deleted before header update, but header will still try to own. | 
|  | // 6 will be deleted after header update, but header will still try to own. | 
|  | // 7 starts orphaned and then deleted before header update. | 
|  | // 8 starts orphaned and then deleted after header update. | 
|  | const std::vector<SessionID> tab_list = SessionIDs({1, 2, 3, 4, 5, 6}); | 
|  | std::vector<sync_pb::SessionSpecifics> tabs; | 
|  | sync_pb::SessionSpecifics meta( | 
|  | helper()->BuildForeignSession(session_tag, tab_list, &tabs)); | 
|  | AddToSyncDataList(meta, &foreign_data, stale_mtime); | 
|  | AddTabsToSyncDataList(tabs, &foreign_data); | 
|  | sync_pb::SessionSpecifics orphan6; | 
|  | helper()->BuildTabSpecifics(session_tag, kWindowId1, | 
|  | SessionID::FromSerializedValue(6), &orphan6); | 
|  | AddToSyncDataList(orphan6, &foreign_data, stale_mtime); | 
|  | sync_pb::SessionSpecifics orphan7; | 
|  | helper()->BuildTabSpecifics(session_tag, kWindowId1, | 
|  | SessionID::FromSerializedValue(7), &orphan7); | 
|  | AddToSyncDataList(orphan7, &foreign_data, stale_mtime); | 
|  |  | 
|  | AddWindow(); | 
|  | SyncChangeList output; | 
|  | InitWithSyncDataTakeOutput(foreign_data, &output); | 
|  | ASSERT_EQ(2U, output.size()); | 
|  | output.clear(); | 
|  |  | 
|  | const std::vector<SessionID> update_list = SessionIDs({1, 5, 6}); | 
|  | sync_pb::SessionWindow* window = meta.mutable_header()->mutable_window(0); | 
|  | window->clear_tab(); | 
|  | for (SessionID i : update_list) { | 
|  | window->add_tab(i.id()); | 
|  | } | 
|  |  | 
|  | SyncChangeList changes; | 
|  | changes.push_back(MakeRemoteChange(tabs[2], SyncChange::ACTION_DELETE)); | 
|  | changes.push_back(MakeRemoteChange(tabs[4], SyncChange::ACTION_DELETE)); | 
|  | changes.push_back(MakeRemoteChange(orphan6, SyncChange::ACTION_DELETE)); | 
|  | changes.push_back(MakeRemoteChange(meta, SyncChange::ACTION_UPDATE)); | 
|  | changes.push_back(MakeRemoteChange(tabs[3], SyncChange::ACTION_DELETE)); | 
|  | changes.push_back(MakeRemoteChange(tabs[5], SyncChange::ACTION_DELETE)); | 
|  | changes.push_back(MakeRemoteChange(orphan7, SyncChange::ACTION_DELETE)); | 
|  | manager()->ProcessSyncChanges(FROM_HERE, changes); | 
|  |  | 
|  | std::vector<const SyncedSession*> foreign_sessions; | 
|  | ASSERT_TRUE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  | ASSERT_EQ(1U, foreign_sessions.size()); | 
|  | std::vector<std::vector<SessionID>> session_reference; | 
|  | session_reference.push_back(update_list); | 
|  | helper()->VerifySyncedSession(session_tag, session_reference, | 
|  | *(foreign_sessions[0])); | 
|  |  | 
|  | // Everything except for session, tab0, and tab1 will have no node_id, and | 
|  | // should get skipped by garbage collection. | 
|  | manager()->DoGarbageCollection(); | 
|  | ASSERT_EQ(3U, output.size()); | 
|  | } | 
|  |  | 
|  | TEST_F(SessionsSyncManagerTest, ProcessForeignDeleteTabsWithShadowing) { | 
|  | SyncDataList foreign_data; | 
|  | base::Time stale_mtime = base::Time::Now() - base::TimeDelta::FromDays(16); | 
|  | std::string session_tag = "tag1"; | 
|  |  | 
|  | // Add several tabs that shadow eachother, in that they share tab_ids. They | 
|  | // will, thanks to the helper, have unique tab_node_ids. | 
|  | sync_pb::SessionSpecifics tab1A; | 
|  | helper()->BuildTabSpecifics(session_tag, kWindowId1, kTabIds1[0], &tab1A); | 
|  | AddToSyncDataList(tab1A, &foreign_data, | 
|  | stale_mtime + base::TimeDelta::FromMinutes(1)); | 
|  |  | 
|  | sync_pb::SessionSpecifics tab1B; | 
|  | helper()->BuildTabSpecifics(session_tag, kWindowId1, kTabIds1[0], &tab1B); | 
|  | AddToSyncDataList(tab1B, &foreign_data, | 
|  | stale_mtime + base::TimeDelta::FromMinutes(2)); | 
|  |  | 
|  | sync_pb::SessionSpecifics tab1C; | 
|  | helper()->BuildTabSpecifics(session_tag, kWindowId1, kTabIds1[0], &tab1C); | 
|  | AddToSyncDataList(tab1C, &foreign_data, stale_mtime); | 
|  |  | 
|  | sync_pb::SessionSpecifics tab2A; | 
|  | helper()->BuildTabSpecifics(session_tag, kWindowId1, kTabIds1[1], &tab2A); | 
|  | AddToSyncDataList(tab2A, &foreign_data, | 
|  | stale_mtime + base::TimeDelta::FromMinutes(1)); | 
|  |  | 
|  | sync_pb::SessionSpecifics tab2B; | 
|  | helper()->BuildTabSpecifics(session_tag, kWindowId1, kTabIds1[1], &tab2B); | 
|  | AddToSyncDataList(tab2B, &foreign_data, | 
|  | stale_mtime + base::TimeDelta::FromMinutes(2)); | 
|  |  | 
|  | sync_pb::SessionSpecifics tab2C; | 
|  | helper()->BuildTabSpecifics(session_tag, kWindowId1, kTabIds1[1], &tab2C); | 
|  | AddToSyncDataList(tab2C, &foreign_data, stale_mtime); | 
|  |  | 
|  | AddWindow(); | 
|  | SyncChangeList output; | 
|  | InitWithSyncDataTakeOutput(foreign_data, &output); | 
|  | ASSERT_EQ(2U, output.size()); | 
|  | output.clear(); | 
|  |  | 
|  | // Verify that cleanup post-merge cleanup correctly removes all tabs objects. | 
|  | ASSERT_THAT( | 
|  | manager()->session_tracker_.LookupSessionTab(session_tag, kTabIds1[0]), | 
|  | IsNull()); | 
|  | ASSERT_THAT( | 
|  | manager()->session_tracker_.LookupSessionTab(session_tag, kTabIds1[1]), | 
|  | IsNull()); | 
|  |  | 
|  | EXPECT_THAT(manager()->session_tracker_.LookupTabNodeIds(session_tag), | 
|  | ElementsAre(tab1A.tab_node_id(), tab1B.tab_node_id(), | 
|  | tab1C.tab_node_id(), tab2A.tab_node_id(), | 
|  | tab2B.tab_node_id(), tab2C.tab_node_id())); | 
|  |  | 
|  | SyncChangeList changes; | 
|  | changes.push_back(MakeRemoteChange(tab1A, SyncChange::ACTION_DELETE)); | 
|  | changes.push_back(MakeRemoteChange(tab1B, SyncChange::ACTION_DELETE)); | 
|  | changes.push_back(MakeRemoteChange(tab2C, SyncChange::ACTION_DELETE)); | 
|  | manager()->ProcessSyncChanges(FROM_HERE, changes); | 
|  |  | 
|  | EXPECT_THAT(manager()->session_tracker_.LookupTabNodeIds(session_tag), | 
|  | ElementsAre(tab1C.tab_node_id(), tab2A.tab_node_id(), | 
|  | tab2B.tab_node_id())); | 
|  |  | 
|  | manager()->DoGarbageCollection(); | 
|  | ASSERT_EQ(3U, output.size()); | 
|  | } | 
|  |  | 
|  | TEST_F(SessionsSyncManagerTest, ProcessForeignDeleteTabsWithReusedNodeIds) { | 
|  | SyncDataList foreign_data; | 
|  | base::Time stale_mtime = base::Time::Now() - base::TimeDelta::FromDays(16); | 
|  | std::string session_tag = "tag1"; | 
|  | int tab_node_id_shared = 13; | 
|  | int tab_node_id_unique = 14; | 
|  |  | 
|  | sync_pb::SessionSpecifics tab1A; | 
|  | helper()->BuildTabSpecifics(session_tag, kWindowId1, kTabIds1[1], | 
|  | tab_node_id_shared, &tab1A); | 
|  | AddToSyncDataList(tab1A, &foreign_data, | 
|  | stale_mtime + base::TimeDelta::FromMinutes(1)); | 
|  |  | 
|  | sync_pb::SessionSpecifics tab1B; | 
|  | helper()->BuildTabSpecifics(session_tag, kWindowId1, kTabIds1[1], | 
|  | tab_node_id_unique, &tab1B); | 
|  | AddToSyncDataList(tab1B, &foreign_data, | 
|  | stale_mtime + base::TimeDelta::FromMinutes(2)); | 
|  |  | 
|  | sync_pb::SessionSpecifics tab2A; | 
|  | helper()->BuildTabSpecifics(session_tag, kWindowId1, kTabIds1[2], | 
|  | tab_node_id_shared, &tab2A); | 
|  | AddToSyncDataList(tab2A, &foreign_data, | 
|  | stale_mtime + base::TimeDelta::FromMinutes(1)); | 
|  |  | 
|  | AddWindow(); | 
|  | SyncChangeList output; | 
|  | InitWithSyncDataTakeOutput(foreign_data, &output); | 
|  | ASSERT_EQ(2U, output.size()); | 
|  | output.clear(); | 
|  |  | 
|  | EXPECT_THAT(manager()->session_tracker_.LookupTabNodeIds(session_tag), | 
|  | ElementsAre(tab_node_id_shared, tab_node_id_unique)); | 
|  |  | 
|  | SyncChangeList changes; | 
|  | changes.push_back(MakeRemoteChange(tab1A, SyncChange::ACTION_DELETE)); | 
|  | manager()->ProcessSyncChanges(FROM_HERE, changes); | 
|  |  | 
|  | EXPECT_THAT(manager()->session_tracker_.LookupTabNodeIds(session_tag), | 
|  | ElementsAre(tab_node_id_unique)); | 
|  |  | 
|  | manager()->DoGarbageCollection(); | 
|  | EXPECT_EQ(1U, output.size()); | 
|  | } | 
|  |  | 
|  | TEST_F(SessionsSyncManagerTest, AssociationReusesNodes) { | 
|  | SyncChangeList changes; | 
|  | TestSyncedWindowDelegate* window = AddWindow(); | 
|  | TestSyncedTabDelegate* tab = AddTab(window->GetSessionId(), kFoo1); | 
|  | InitWithSyncDataTakeOutput(SyncDataList(), &changes); | 
|  | ASSERT_TRUE(ChangeTypeMatches(changes, | 
|  | {SyncChange::ACTION_ADD, SyncChange::ACTION_ADD, | 
|  | SyncChange::ACTION_UPDATE})); | 
|  | ASSERT_TRUE(changes[1].sync_data().GetSpecifics().session().has_tab()); | 
|  | int tab_node_id = | 
|  | changes[1].sync_data().GetSpecifics().session().tab_node_id(); | 
|  |  | 
|  | // Pass back the previous tab and header nodes at association, along with a | 
|  | // second tab node (with rewritten tab IDs). | 
|  | SyncDataList in; | 
|  | in.push_back( | 
|  | CreateRemoteData(changes[2].sync_data().GetSpecifics()));  // Header node. | 
|  | sync_pb::SessionSpecifics new_tab( | 
|  | changes[1].sync_data().GetSpecifics().session()); | 
|  | new_tab.mutable_tab()->set_tab_id(new_tab.tab().tab_id() + 1); | 
|  | new_tab.set_tab_node_id(tab_node_id + 1); | 
|  | in.push_back(CreateRemoteData( | 
|  | changes[1].sync_data().GetSpecifics()));  // Old tab node. | 
|  | in.push_back(CreateRemoteData(new_tab));      // New tab node. | 
|  | changes.clear(); | 
|  |  | 
|  | // Reassociate (with the same single tab/window open). | 
|  | manager()->StopSyncing(syncer::SESSIONS); | 
|  | InitWithSyncDataTakeOutput(in, &changes); | 
|  |  | 
|  | // No tab entities should be deleted. The original (lower) tab node id should | 
|  | // be reused for association. | 
|  | FilterOutLocalHeaderChanges(&changes); | 
|  | ASSERT_TRUE(ChangeTypeMatches(changes, {SyncChange::ACTION_UPDATE})); | 
|  | VerifyLocalTabChange(changes[0], 1, kFoo1); | 
|  | EXPECT_EQ(tab_node_id, | 
|  | changes[0].sync_data().GetSpecifics().session().tab_node_id()); | 
|  | changes.clear(); | 
|  |  | 
|  | // Update the original tab. Ensure the same tab node is updated. | 
|  | tab->Navigate(kFoo2); | 
|  | FilterOutLocalHeaderChanges(&changes); | 
|  | ASSERT_TRUE(ChangeTypeMatches(changes, {SyncChange::ACTION_UPDATE})); | 
|  | VerifyLocalTabChange(changes[0], 2, kFoo2); | 
|  | EXPECT_EQ(tab_node_id, | 
|  | changes[0].sync_data().GetSpecifics().session().tab_node_id()); | 
|  | changes.clear(); | 
|  |  | 
|  | // Add a new tab. It should reuse the second tab node. | 
|  | AddTab(window->GetSessionId(), kBar1); | 
|  | FilterOutLocalHeaderChanges(&changes); | 
|  | ASSERT_TRUE(ChangeTypeMatches(changes, {SyncChange::ACTION_UPDATE})); | 
|  | VerifyLocalTabChange(changes[0], 1, kBar1); | 
|  | EXPECT_EQ(tab_node_id + 1, | 
|  | changes[0].sync_data().GetSpecifics().session().tab_node_id()); | 
|  | } | 
|  |  | 
|  | // Ensure that the merge process deletes a tab node without a tab id. | 
|  | TEST_F(SessionsSyncManagerTest, MergeDeletesTabMissingTabId) { | 
|  | SyncChangeList changes; | 
|  | InitWithNoSyncData(); | 
|  |  | 
|  | std::string local_tag = manager()->current_machine_tag(); | 
|  | int tab_node_id = 0; | 
|  | sync_pb::SessionSpecifics specifics; | 
|  | specifics.set_session_tag(local_tag); | 
|  | specifics.set_tab_node_id(tab_node_id); | 
|  | manager()->StopSyncing(syncer::SESSIONS); | 
|  | InitWithSyncDataTakeOutput({CreateRemoteData(specifics)}, &changes); | 
|  | EXPECT_EQ(1U, FilterOutLocalHeaderChanges(&changes)->size()); | 
|  | EXPECT_EQ(SyncChange::ACTION_DELETE, changes[0].change_type()); | 
|  | EXPECT_EQ(TabNodeIdToTag(local_tag, tab_node_id), | 
|  | SyncDataLocal(changes[0].sync_data()).GetTag()); | 
|  | } | 
|  |  | 
|  | // Verifies that we drop both headers and tabs during merge if their stored tag | 
|  | // hash doesn't match a computer tag hash. This mitigates potential failures | 
|  | // while cleaning up bad foreign data, see https://crbug.com/604657. | 
|  | TEST_F(SessionsSyncManagerTest, MergeDeletesBadHash) { | 
|  | SyncDataList foreign_data; | 
|  | std::vector<SessionID> empty_ids; | 
|  | std::vector<sync_pb::SessionSpecifics> empty_tabs; | 
|  | sync_pb::EntitySpecifics entity; | 
|  |  | 
|  | const std::string good_header_tag = "good_header_tag"; | 
|  | sync_pb::SessionSpecifics good_header( | 
|  | helper()->BuildForeignSession(good_header_tag, empty_ids, &empty_tabs)); | 
|  | foreign_data.push_back(CreateRemoteData(good_header)); | 
|  |  | 
|  | const std::string bad_header_tag = "bad_header_tag"; | 
|  | sync_pb::SessionSpecifics bad_header( | 
|  | helper()->BuildForeignSession(bad_header_tag, empty_ids, &empty_tabs)); | 
|  | entity.mutable_session()->CopyFrom(bad_header); | 
|  | foreign_data.push_back(SyncData::CreateRemoteData(1, entity, base::Time(), | 
|  | "bad_header_tag_hash")); | 
|  |  | 
|  | const std::string good_tag_tab = "good_tag_tab"; | 
|  | sync_pb::SessionSpecifics good_tab; | 
|  | helper()->BuildTabSpecifics(good_tag_tab, kWindowId1, kTabIds1[0], &good_tab); | 
|  | foreign_data.push_back(CreateRemoteData(good_tab)); | 
|  |  | 
|  | const std::string bad_tab_tag = "bad_tab_tag"; | 
|  | sync_pb::SessionSpecifics bad_tab; | 
|  | helper()->BuildTabSpecifics(bad_tab_tag, kWindowId1, kTabIds1[1], &bad_tab); | 
|  | entity.mutable_session()->CopyFrom(bad_tab); | 
|  | foreign_data.push_back( | 
|  | SyncData::CreateRemoteData(1, entity, base::Time(), "bad_tab_tag_hash")); | 
|  |  | 
|  | SyncChangeList output; | 
|  | InitWithSyncDataTakeOutput(foreign_data, &output); | 
|  | ASSERT_EQ(2U, FilterOutLocalHeaderChanges(&output)->size()); | 
|  | ASSERT_TRUE(AllOfChangesAreType(output, SyncChange::ACTION_DELETE)); | 
|  | EXPECT_EQ(1U, CountIfTagMatches(output, bad_header_tag)); | 
|  | EXPECT_EQ(1U, CountIfTagMatches(output, bad_tab_tag)); | 
|  |  | 
|  | const std::vector<const SyncedSession*> sessions = | 
|  | manager()->session_tracker_.LookupAllForeignSessions( | 
|  | SyncedSessionTracker::RAW); | 
|  | ASSERT_EQ(2U, sessions.size()); | 
|  | EXPECT_EQ(1U, CountIfTagMatches(sessions, good_header_tag)); | 
|  | EXPECT_EQ(1U, CountIfTagMatches(sessions, good_tag_tab)); | 
|  | } | 
|  |  | 
|  | // Test that things work if a tab is initially ignored. | 
|  | TEST_F(SessionsSyncManagerTest, AssociateWindowsDontReloadTabs) { | 
|  | SyncChangeList out; | 
|  | // Go to a URL that is ignored by session syncing. | 
|  | TestSyncedTabDelegate* tab = | 
|  | AddTab(AddWindow()->GetSessionId(), "chrome://preferences/"); | 
|  | InitWithSyncDataTakeOutput(SyncDataList(), &out); | 
|  | ASSERT_TRUE(ChangeTypeMatches( | 
|  | out, {SyncChange::ACTION_ADD, SyncChange::ACTION_UPDATE})); | 
|  | VerifyLocalHeaderChange(out[1], 0, 0); | 
|  | out.clear(); | 
|  |  | 
|  | // Go to a sync-interesting URL. | 
|  | tab->Navigate(kFoo1); | 
|  |  | 
|  | // The tab should be created, coupled with a header update. | 
|  | ASSERT_TRUE(ChangeTypeMatches( | 
|  | out, {SyncChange::ACTION_ADD, SyncChange::ACTION_UPDATE})); | 
|  | VerifyLocalTabChange(out[0], 2, kFoo1); | 
|  | VerifyLocalHeaderChange(out[1], 1, 1); | 
|  | } | 
|  |  | 
|  | // Tests that the SyncSessionManager responds to local tab events properly. | 
|  | TEST_F(SessionsSyncManagerTest, OnLocalTabModified) { | 
|  | SyncChangeList out; | 
|  | // Init with no local data, relies on MergeLocalSessionNoTabs. | 
|  | TestSyncedWindowDelegate* window = AddWindow(); | 
|  | SessionID window_id = window->GetSessionId(); | 
|  | InitWithSyncDataTakeOutput(SyncDataList(), &out); | 
|  | ASSERT_FALSE(manager()->current_machine_tag().empty()); | 
|  | ASSERT_EQ(2U, out.size()); | 
|  |  | 
|  | // Copy the original header. | 
|  | sync_pb::EntitySpecifics header(out[0].sync_data().GetSpecifics()); | 
|  | out.clear(); | 
|  |  | 
|  | AddTab(window_id, kFoo1)->Navigate(kFoo2); | 
|  | AddTab(window_id, kBar1)->Navigate(kBar2); | 
|  | std::vector<std::string> urls = {kFoo1, kFoo2, kBar1, kBar2}; | 
|  |  | 
|  | // Change type breakdown: | 
|  | // 1 tab add + 2 header updates. | 
|  | const size_t kChangesPerTabCreation = 3; | 
|  | // 1 tab update + 1 header update. | 
|  | const size_t kChangesPerTabNav = 2; | 
|  | const size_t kChangesPerTab = kChangesPerTabNav + kChangesPerTabCreation; | 
|  | const size_t kNumTabs = 2; | 
|  | const size_t kTotalUpdates = kChangesPerTab * kNumTabs; | 
|  |  | 
|  | std::vector<SyncChange::SyncChangeType> types = { | 
|  | // Tab 1 | 
|  | SyncChange::ACTION_UPDATE, SyncChange::ACTION_ADD, | 
|  | SyncChange::ACTION_UPDATE, SyncChange::ACTION_UPDATE, | 
|  | SyncChange::ACTION_UPDATE, | 
|  | // Tab 2 | 
|  | SyncChange::ACTION_UPDATE, SyncChange::ACTION_ADD, | 
|  | SyncChange::ACTION_UPDATE, SyncChange::ACTION_UPDATE, | 
|  | SyncChange::ACTION_UPDATE}; | 
|  | ASSERT_EQ(kTotalUpdates, types.size()); | 
|  |  | 
|  | // Verify the tab node creations and updates to ensure the SyncProcessor sees | 
|  | // the right operations. Do this by inspecting the set of changes for each | 
|  | // tab separately by iterating through the tabs. | 
|  | ASSERT_TRUE(ChangeTypeMatches(out, types)); | 
|  | for (size_t i = 0; i < kNumTabs; ++i) { | 
|  | int index = kChangesPerTab * i; | 
|  | int nav_per_tab_count = 0; | 
|  | { | 
|  | SCOPED_TRACE(index); | 
|  | // The initial tab parent event triggers a header update (which is in | 
|  | // effect a no-op). | 
|  | VerifyLocalHeaderChange(out[index++], (i == 0 ? 0 : 1), i); | 
|  | } | 
|  | { | 
|  | SCOPED_TRACE(index); | 
|  | nav_per_tab_count++; | 
|  | // Tab update after initial creation.. | 
|  | VerifyLocalTabChange(out[index++], nav_per_tab_count, | 
|  | urls[i * kChangesPerTabNav + nav_per_tab_count - 1]); | 
|  | } | 
|  | { | 
|  | SCOPED_TRACE(index); | 
|  | // The associate windows after the tab creation. | 
|  | VerifyLocalHeaderChange(out[index++], 1, i + 1); | 
|  | } | 
|  | { | 
|  | SCOPED_TRACE(index); | 
|  | nav_per_tab_count++; | 
|  | // Tab navigation. | 
|  | VerifyLocalTabChange(out[index++], nav_per_tab_count, | 
|  | urls[i * kChangesPerTabNav + nav_per_tab_count - 1]); | 
|  | } | 
|  | { | 
|  | SCOPED_TRACE(index); | 
|  | // The associate windows after the tab navigation. | 
|  | VerifyLocalHeaderChange(out[index++], 1, i + 1); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | TEST_F(SessionsSyncManagerTest, ForeignSessionModifiedTime) { | 
|  | SyncDataList foreign_data; | 
|  | base::Time newest_time = base::Time::Now() - base::TimeDelta::FromDays(1); | 
|  | base::Time middle_time = base::Time::Now() - base::TimeDelta::FromDays(2); | 
|  | base::Time oldest_time = base::Time::Now() - base::TimeDelta::FromDays(3); | 
|  |  | 
|  | { | 
|  | std::string session_tag = "tag1"; | 
|  | std::vector<sync_pb::SessionSpecifics> tabs; | 
|  | sync_pb::SessionSpecifics meta( | 
|  | helper()->BuildForeignSession(session_tag, SessionIDs({1, 2}), &tabs)); | 
|  | AddToSyncDataList(tabs[0], &foreign_data, newest_time); | 
|  | AddToSyncDataList(meta, &foreign_data, middle_time); | 
|  | AddToSyncDataList(tabs[1], &foreign_data, oldest_time); | 
|  | } | 
|  |  | 
|  | { | 
|  | std::string session_tag = "tag2"; | 
|  | std::vector<sync_pb::SessionSpecifics> tabs; | 
|  | sync_pb::SessionSpecifics meta( | 
|  | helper()->BuildForeignSession(session_tag, SessionIDs({3, 4}), &tabs)); | 
|  | AddToSyncDataList(tabs[0], &foreign_data, middle_time); | 
|  | AddToSyncDataList(meta, &foreign_data, newest_time); | 
|  | AddToSyncDataList(tabs[1], &foreign_data, oldest_time); | 
|  | } | 
|  |  | 
|  | { | 
|  | std::string session_tag = "tag3"; | 
|  | std::vector<sync_pb::SessionSpecifics> tabs; | 
|  | sync_pb::SessionSpecifics meta( | 
|  | helper()->BuildForeignSession(session_tag, SessionIDs({5, 6}), &tabs)); | 
|  | AddToSyncDataList(tabs[0], &foreign_data, oldest_time); | 
|  | AddToSyncDataList(meta, &foreign_data, middle_time); | 
|  | AddToSyncDataList(tabs[1], &foreign_data, newest_time); | 
|  | } | 
|  |  | 
|  | SyncChangeList output; | 
|  | AddWindow(); | 
|  | InitWithSyncDataTakeOutput(foreign_data, &output); | 
|  | ASSERT_EQ(2U, output.size()); | 
|  | output.clear(); | 
|  |  | 
|  | std::vector<const SyncedSession*> foreign_sessions; | 
|  | ASSERT_TRUE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  | ASSERT_EQ(3U, foreign_sessions.size()); | 
|  | EXPECT_EQ(newest_time, foreign_sessions[0]->modified_time); | 
|  | EXPECT_EQ(newest_time, foreign_sessions[1]->modified_time); | 
|  | EXPECT_EQ(newest_time, foreign_sessions[2]->modified_time); | 
|  | } | 
|  |  | 
|  | // Test garbage collection of stale foreign sessions. | 
|  | TEST_F(SessionsSyncManagerTest, DoGarbageCollection) { | 
|  | // Fill two instances of session specifics with a foreign session's data. | 
|  | std::vector<sync_pb::SessionSpecifics> tabs1; | 
|  | sync_pb::SessionSpecifics meta( | 
|  | helper()->BuildForeignSession(kTag1, kTabIds1, &tabs1)); | 
|  | std::vector<sync_pb::SessionSpecifics> tabs2; | 
|  | sync_pb::SessionSpecifics meta2( | 
|  | helper()->BuildForeignSession(kTag2, kTabIds2, &tabs2)); | 
|  | // Set the modification time for tag1 to be 21 days ago, tag2 to 5 days ago. | 
|  | base::Time tag1_time = base::Time::Now() - base::TimeDelta::FromDays(21); | 
|  | base::Time tag2_time = base::Time::Now() - base::TimeDelta::FromDays(5); | 
|  |  | 
|  | SyncDataList foreign_data; | 
|  | foreign_data.push_back(CreateRemoteData(meta, tag1_time)); | 
|  | foreign_data.push_back(CreateRemoteData(meta2, tag2_time)); | 
|  | AddTabsToSyncDataList(tabs1, &foreign_data); | 
|  | AddTabsToSyncDataList(tabs2, &foreign_data); | 
|  |  | 
|  | AddWindow(); | 
|  | SyncChangeList output; | 
|  | InitWithSyncDataTakeOutput(foreign_data, &output); | 
|  | ASSERT_EQ(2U, output.size()); | 
|  | output.clear(); | 
|  |  | 
|  | // Check that the foreign session was associated and retrieve the data. | 
|  | std::vector<const SyncedSession*> foreign_sessions; | 
|  | ASSERT_TRUE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  | ASSERT_EQ(2U, foreign_sessions.size()); | 
|  | foreign_sessions.clear(); | 
|  |  | 
|  | // Now garbage collect and verify the non-stale session is still there. | 
|  | manager()->DoGarbageCollection(); | 
|  | ASSERT_EQ(5U, output.size()); | 
|  | ASSERT_TRUE(AllOfChangesAreType(output, SyncChange::ACTION_DELETE)); | 
|  | EXPECT_EQ(kTag1, SyncDataLocal(output[0].sync_data()).GetTag()); | 
|  | for (int i = 1; i < 5; ++i) { | 
|  | EXPECT_EQ(TabNodeIdToTag(kTag1, i), | 
|  | SyncDataLocal(output[i].sync_data()).GetTag()); | 
|  | } | 
|  |  | 
|  | ASSERT_TRUE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  | ASSERT_EQ(1U, foreign_sessions.size()); | 
|  | std::vector<std::vector<SessionID>> session_reference; | 
|  | session_reference.push_back(kTabIds2); | 
|  | helper()->VerifySyncedSession(kTag2, session_reference, | 
|  | *(foreign_sessions[0])); | 
|  | } | 
|  |  | 
|  | TEST_F(SessionsSyncManagerTest, DoGarbageCollectionOrphans) { | 
|  | SyncDataList foreign_data; | 
|  | base::Time stale_mtime = base::Time::Now() - base::TimeDelta::FromDays(15); | 
|  |  | 
|  | { | 
|  | // A stale session with empty header | 
|  | std::string session_tag = "tag1"; | 
|  | std::vector<sync_pb::SessionSpecifics> tabs; | 
|  | sync_pb::SessionSpecifics meta( | 
|  | helper()->BuildForeignSession(session_tag, {}, &tabs)); | 
|  | AddToSyncDataList(meta, &foreign_data, stale_mtime); | 
|  | } | 
|  |  | 
|  | { | 
|  | // A stale session with orphans w/o header | 
|  | std::string session_tag = "tag2"; | 
|  | sync_pb::SessionSpecifics orphan; | 
|  | helper()->BuildTabSpecifics(session_tag, kWindowId1, kTabIds1[1], &orphan); | 
|  | AddToSyncDataList(orphan, &foreign_data, stale_mtime); | 
|  | } | 
|  |  | 
|  | { | 
|  | // A stale session with valid header/tab and an orphaned tab. | 
|  | std::string session_tag = "tag3"; | 
|  | std::vector<sync_pb::SessionSpecifics> tabs; | 
|  | sync_pb::SessionSpecifics meta( | 
|  | helper()->BuildForeignSession(session_tag, SessionIDs({2}), &tabs)); | 
|  |  | 
|  | // BuildForeignSession(...) will use a window id of 0, and we're also | 
|  | // passing a window id of 0 to BuildTabSpecifics(...) here.  It doesn't | 
|  | // really matter what window id we use for the orphaned tab, in the real | 
|  | // world orphans often reference real/still valid windows, but they're | 
|  | // orphans because the window/header doesn't reference back to them. | 
|  | sync_pb::SessionSpecifics orphan; | 
|  | helper()->BuildTabSpecifics(session_tag, kWindowId1, kTabIds1[1], &orphan); | 
|  | AddToSyncDataList(orphan, &foreign_data, stale_mtime); | 
|  |  | 
|  | AddToSyncDataList(tabs[0], &foreign_data, stale_mtime); | 
|  | AddToSyncDataList(orphan, &foreign_data, stale_mtime); | 
|  | AddToSyncDataList(meta, &foreign_data, stale_mtime); | 
|  | } | 
|  |  | 
|  | SyncChangeList output; | 
|  | AddWindow(); | 
|  | InitWithSyncDataTakeOutput(foreign_data, &output); | 
|  | ASSERT_EQ(2U, output.size()); | 
|  | output.clear(); | 
|  |  | 
|  | // Although we have 3 foreign sessions, only 1 is valid/clean enough. | 
|  | std::vector<const SyncedSession*> foreign_sessions; | 
|  | ASSERT_TRUE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  | ASSERT_EQ(1U, foreign_sessions.size()); | 
|  | foreign_sessions.clear(); | 
|  |  | 
|  | // Everything should get removed here. | 
|  | manager()->DoGarbageCollection(); | 
|  | // Expect 5 deletions. tag1 header only, tag2 tab only, tag3 header + 2x tabs. | 
|  | ASSERT_EQ(5U, output.size()); | 
|  | ASSERT_TRUE(AllOfChangesAreType(output, SyncChange::ACTION_DELETE)); | 
|  | } | 
|  |  | 
|  | // Test that an update to a previously considered "stale" session, | 
|  | // prior to garbage collection, will save the session from deletion. | 
|  | TEST_F(SessionsSyncManagerTest, GarbageCollectionHonoursUpdate) { | 
|  | std::vector<sync_pb::SessionSpecifics> tabs1; | 
|  | sync_pb::SessionSpecifics meta( | 
|  | helper()->BuildForeignSession(kTag1, kTabIds1, &tabs1)); | 
|  | SyncDataList foreign_data; | 
|  | base::Time tag1_time = base::Time::Now() - base::TimeDelta::FromDays(21); | 
|  | foreign_data.push_back(CreateRemoteData(meta, tag1_time)); | 
|  | AddTabsToSyncDataList(tabs1, &foreign_data); | 
|  | SyncChangeList output; | 
|  | AddWindow(); | 
|  | InitWithSyncDataTakeOutput(foreign_data, &output); | 
|  | ASSERT_EQ(2U, output.size()); | 
|  |  | 
|  | // Update to a non-stale time. | 
|  | sync_pb::EntitySpecifics update_entity; | 
|  | update_entity.mutable_session()->CopyFrom(tabs1[0]); | 
|  | SyncChangeList changes; | 
|  | changes.push_back(SyncChange(FROM_HERE, SyncChange::ACTION_UPDATE, | 
|  | CreateRemoteData(tabs1[0], base::Time::Now()))); | 
|  | manager()->ProcessSyncChanges(FROM_HERE, changes); | 
|  |  | 
|  | // Check that the foreign session was associated and retrieve the data. | 
|  | std::vector<const SyncedSession*> foreign_sessions; | 
|  | ASSERT_TRUE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  | ASSERT_EQ(1U, foreign_sessions.size()); | 
|  | foreign_sessions.clear(); | 
|  |  | 
|  | // Verify the now non-stale session does not get deleted. | 
|  | manager()->DoGarbageCollection(); | 
|  | ASSERT_TRUE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  | ASSERT_EQ(1U, foreign_sessions.size()); | 
|  | std::vector<std::vector<SessionID>> session_reference; | 
|  | session_reference.push_back(kTabIds1); | 
|  | helper()->VerifySyncedSession(kTag1, session_reference, | 
|  | *(foreign_sessions[0])); | 
|  | } | 
|  |  | 
|  | // Test that NOTIFICATION_FOREIGN_SESSION_UPDATED is sent when processing | 
|  | // sync changes. | 
|  | TEST_F(SessionsSyncManagerTest, NotifiedOfUpdates) { | 
|  | InitWithNoSyncData(); | 
|  |  | 
|  | std::vector<sync_pb::SessionSpecifics> tabs1; | 
|  | sync_pb::SessionSpecifics meta( | 
|  | helper()->BuildForeignSession("tag1", SessionIDs({5}), &tabs1)); | 
|  |  | 
|  | SyncChangeList changes; | 
|  | changes.push_back(MakeRemoteChange(meta, SyncChange::ACTION_ADD)); | 
|  | EXPECT_CALL(*mock_sync_sessions_client(), NotifyForeignSessionUpdated()); | 
|  | manager()->ProcessSyncChanges(FROM_HERE, changes); | 
|  |  | 
|  | changes.clear(); | 
|  | AddTabsToChangeList(tabs1, SyncChange::ACTION_ADD, &changes); | 
|  | EXPECT_CALL(*mock_sync_sessions_client(), NotifyForeignSessionUpdated()); | 
|  | manager()->ProcessSyncChanges(FROM_HERE, changes); | 
|  |  | 
|  | changes.clear(); | 
|  | changes.push_back(MakeRemoteChange(meta, SyncChange::ACTION_DELETE)); | 
|  | EXPECT_CALL(*mock_sync_sessions_client(), NotifyForeignSessionUpdated()); | 
|  | manager()->ProcessSyncChanges(FROM_HERE, changes); | 
|  | } | 
|  |  | 
|  | // Test that NOTIFICATION_FOREIGN_SESSION_UPDATED is sent when handling | 
|  | // local hide/removal of foreign session. | 
|  | TEST_F(SessionsSyncManagerTest, NotifiedOfLocalRemovalOfForeignSession) { | 
|  | InitWithNoSyncData(); | 
|  | const std::string tag("tag1"); | 
|  | std::vector<sync_pb::SessionSpecifics> tabs1; | 
|  | sync_pb::SessionSpecifics meta( | 
|  | helper()->BuildForeignSession(tag, SessionIDs({5}), &tabs1)); | 
|  |  | 
|  | SyncChangeList changes; | 
|  | changes.push_back(MakeRemoteChange(meta, SyncChange::ACTION_ADD)); | 
|  | manager()->ProcessSyncChanges(FROM_HERE, changes); | 
|  |  | 
|  | EXPECT_CALL(*mock_sync_sessions_client(), NotifyForeignSessionUpdated()); | 
|  | manager()->GetOpenTabsUIDelegate()->DeleteForeignSession(tag); | 
|  | } | 
|  |  | 
|  | // Tests receipt of duplicate tab IDs in the same window.  This should never | 
|  | // happen, but we want to make sure the client won't do anything bad if it does | 
|  | // receive such garbage input data. | 
|  | TEST_F(SessionsSyncManagerTest, ReceiveDuplicateTabInSameWindow) { | 
|  | std::string tag = "tag1"; | 
|  |  | 
|  | // Reuse tab ID 10 in an attempt to trigger bad behavior. | 
|  | std::vector<sync_pb::SessionSpecifics> tabs1; | 
|  | sync_pb::SessionSpecifics meta( | 
|  | helper()->BuildForeignSession(tag, kTabIds1, &tabs1)); | 
|  |  | 
|  | // Set up initial data. | 
|  | SyncDataList initial_data; | 
|  | sync_pb::EntitySpecifics entity; | 
|  | entity.mutable_session()->CopyFrom(meta); | 
|  | initial_data.push_back(CreateRemoteData(entity)); | 
|  | AddTabsToSyncDataList(tabs1, &initial_data); | 
|  |  | 
|  | SyncChangeList output; | 
|  | InitWithSyncDataTakeOutput(initial_data, &output); | 
|  | } | 
|  |  | 
|  | // Tests receipt of duplicate tab IDs for the same session.  The duplicate tab | 
|  | // ID is present in two different windows.  A client can't be expected to do | 
|  | // anything reasonable with this input, but we can expect that it doesn't | 
|  | // crash. | 
|  | TEST_F(SessionsSyncManagerTest, ReceiveDuplicateTabInOtherWindow) { | 
|  | // Tab ID 10 is a duplicate. | 
|  | const std::vector<SessionID> tab_list1 = SessionIDs({5, 10, 15}); | 
|  | const std::vector<SessionID> tab_list2 = SessionIDs({7, 10, 17}); | 
|  |  | 
|  | std::vector<sync_pb::SessionSpecifics> tabs1; | 
|  | sync_pb::SessionSpecifics meta( | 
|  | helper()->BuildForeignSession(kTag1, tab_list1, &tabs1)); | 
|  |  | 
|  | // Add a second window.  Tab ID 10 is a duplicate. | 
|  | helper()->AddWindowSpecifics(kWindowId2, tab_list2, &meta); | 
|  |  | 
|  | // Set up initial data. | 
|  | SyncDataList initial_data; | 
|  | sync_pb::EntitySpecifics entity; | 
|  | entity.mutable_session()->CopyFrom(meta); | 
|  | initial_data.push_back(CreateRemoteData(entity)); | 
|  | AddTabsToSyncDataList(tabs1, &initial_data); | 
|  |  | 
|  | for (SessionID tab_id : tab_list2) { | 
|  | sync_pb::EntitySpecifics entity; | 
|  | helper()->BuildTabSpecifics(kTag1, kWindowId1, tab_id, | 
|  | entity.mutable_session()); | 
|  | initial_data.push_back(CreateRemoteData(entity)); | 
|  | } | 
|  |  | 
|  | SyncChangeList output; | 
|  | InitWithSyncDataTakeOutput(initial_data, &output); | 
|  | } | 
|  |  | 
|  | // Tests receipt of multiple unassociated tabs and makes sure that | 
|  | // the ones with later timestamp win | 
|  | TEST_F(SessionsSyncManagerTest, ReceiveDuplicateUnassociatedTabs) { | 
|  | std::vector<sync_pb::SessionSpecifics> tabs1; | 
|  | sync_pb::SessionSpecifics meta( | 
|  | helper()->BuildForeignSession(kTag1, kTabIds1, &tabs1)); | 
|  |  | 
|  | // Set up initial data. | 
|  | SyncDataList initial_data; | 
|  | initial_data.push_back(CreateRemoteData(meta)); | 
|  |  | 
|  | sync_pb::EntitySpecifics entity; | 
|  |  | 
|  | for (size_t i = 0; i < tabs1.size(); ++i) { | 
|  | entity.mutable_session()->CopyFrom(tabs1[i]); | 
|  | initial_data.push_back( | 
|  | CreateRemoteData(entity, base::Time::FromDoubleT(2000))); | 
|  | } | 
|  |  | 
|  | // Add two more tabs with duplicating IDs but with different modification | 
|  | // times, one before and one after the tabs above. | 
|  | // These two tabs get a different visual indices to distinguish them from the | 
|  | // tabs above that get visual index 1 by default. | 
|  | sync_pb::SessionSpecifics duplicating_tab1; | 
|  | helper()->BuildTabSpecifics(kTag1, kWindowId1, kTabIds1[1], | 
|  | &duplicating_tab1); | 
|  | duplicating_tab1.mutable_tab()->set_tab_visual_index(2); | 
|  | entity.mutable_session()->CopyFrom(duplicating_tab1); | 
|  | initial_data.push_back( | 
|  | CreateRemoteData(entity, base::Time::FromDoubleT(1000))); | 
|  |  | 
|  | sync_pb::SessionSpecifics duplicating_tab2; | 
|  | helper()->BuildTabSpecifics(kTag1, kWindowId1, kTabIds1[2], | 
|  | &duplicating_tab2); | 
|  | duplicating_tab2.mutable_tab()->set_tab_visual_index(3); | 
|  | entity.mutable_session()->CopyFrom(duplicating_tab2); | 
|  | initial_data.push_back( | 
|  | CreateRemoteData(entity, base::Time::FromDoubleT(3000))); | 
|  |  | 
|  | SyncChangeList output; | 
|  | InitWithSyncDataTakeOutput(initial_data, &output); | 
|  |  | 
|  | std::vector<const SyncedSession*> foreign_sessions; | 
|  | ASSERT_TRUE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  |  | 
|  | const std::vector<std::unique_ptr<sessions::SessionTab>>& window_tabs = | 
|  | foreign_sessions[0] | 
|  | ->windows.find(kWindowId1) | 
|  | ->second->wrapped_window.tabs; | 
|  | ASSERT_EQ(4U, window_tabs.size()); | 
|  | // The first one is from the original set of tabs. | 
|  | ASSERT_EQ(1, window_tabs[0]->tab_visual_index); | 
|  | // The one from the original set of tabs wins over duplicating_tab1. | 
|  | ASSERT_EQ(1, window_tabs[1]->tab_visual_index); | 
|  | // duplicating_tab2 wins due to the later timestamp. | 
|  | ASSERT_EQ(3, window_tabs[2]->tab_visual_index); | 
|  | } | 
|  |  | 
|  | // Verify that GetAllForeignSessions returns all sessions sorted by recency. | 
|  | TEST_F(SessionsSyncManagerTest, GetAllForeignSessions) { | 
|  | std::vector<sync_pb::SessionSpecifics> tabs1; | 
|  | sync_pb::SessionSpecifics meta1( | 
|  | helper()->BuildForeignSession(kTag1, kTabIds1, &tabs1)); | 
|  |  | 
|  | std::vector<sync_pb::SessionSpecifics> tabs2; | 
|  | sync_pb::SessionSpecifics meta2( | 
|  | helper()->BuildForeignSession(kTag2, kTabIds1, &tabs2)); | 
|  |  | 
|  | SyncDataList initial_data; | 
|  | initial_data.push_back( | 
|  | CreateRemoteData(meta1, base::Time::FromInternalValue(10))); | 
|  | AddTabsToSyncDataList(tabs1, &initial_data); | 
|  | initial_data.push_back( | 
|  | CreateRemoteData(meta2, base::Time::FromInternalValue(200))); | 
|  | AddTabsToSyncDataList(tabs2, &initial_data); | 
|  |  | 
|  | SyncChangeList output; | 
|  | InitWithSyncDataTakeOutput(initial_data, &output); | 
|  |  | 
|  | std::vector<const SyncedSession*> foreign_sessions; | 
|  | ASSERT_TRUE(manager()->GetOpenTabsUIDelegate()->GetAllForeignSessions( | 
|  | &foreign_sessions)); | 
|  | ASSERT_EQ(2U, foreign_sessions.size()); | 
|  | ASSERT_GT(foreign_sessions[0]->modified_time, | 
|  | foreign_sessions[1]->modified_time); | 
|  | } | 
|  |  | 
|  | // Verify that GetForeignSessionTabs returns all tabs for a session sorted | 
|  | // by recency. | 
|  | TEST_F(SessionsSyncManagerTest, GetForeignSessionTabs) { | 
|  | std::vector<sync_pb::SessionSpecifics> tabs1; | 
|  | sync_pb::SessionSpecifics meta( | 
|  | helper()->BuildForeignSession(kTag1, kTabIds1, &tabs1)); | 
|  | // Add a second window. | 
|  | helper()->AddWindowSpecifics(kWindowId2, kTabIds2, &meta); | 
|  |  | 
|  | // Set up initial data. | 
|  | SyncDataList initial_data; | 
|  | initial_data.push_back(CreateRemoteData(meta)); | 
|  |  | 
|  | // Add the first window's tabs. | 
|  | AddTabsToSyncDataList(tabs1, &initial_data); | 
|  |  | 
|  | // Add the second window's tabs. | 
|  | for (size_t i = 0; i < kTabIds2.size(); ++i) { | 
|  | sync_pb::EntitySpecifics entity; | 
|  | helper()->BuildTabSpecifics(kTag1, kWindowId1, kTabIds2[i], | 
|  | entity.mutable_session()); | 
|  | // Order the tabs oldest to most recent and left to right visually. | 
|  | initial_data.push_back( | 
|  | CreateRemoteData(entity, base::Time::FromInternalValue(i + 1))); | 
|  | } | 
|  |  | 
|  | SyncChangeList output; | 
|  | InitWithSyncDataTakeOutput(initial_data, &output); | 
|  |  | 
|  | std::vector<const sessions::SessionTab*> tabs; | 
|  | ASSERT_TRUE( | 
|  | manager()->GetOpenTabsUIDelegate()->GetForeignSessionTabs(kTag1, &tabs)); | 
|  | // Assert that the size matches the total number of tabs and that the order | 
|  | // is from most recent to least. | 
|  | ASSERT_EQ(kTabIds1.size() + kTabIds2.size(), tabs.size()); | 
|  | base::Time last_time; | 
|  | for (size_t i = 0; i < tabs.size(); ++i) { | 
|  | base::Time this_time = tabs[i]->timestamp; | 
|  | if (i > 0) | 
|  | ASSERT_GE(last_time, this_time); | 
|  | last_time = tabs[i]->timestamp; | 
|  | } | 
|  | } | 
|  |  | 
|  | // Ensure model association associates the pre-existing tabs. | 
|  | TEST_F(SessionsSyncManagerTest, SwappedOutOnRestore) { | 
|  | // Start with three tabs in a window. | 
|  | TestSyncedWindowDelegate* window = AddWindow(); | 
|  | TestSyncedTabDelegate* tab1 = AddTab(window->GetSessionId(), kFoo1); | 
|  | tab1->Navigate(kFoo2); | 
|  | TestSyncedTabDelegate* tab2 = AddTab(window->GetSessionId(), kBar1); | 
|  | tab2->Navigate(kBar2); | 
|  |  | 
|  | SyncDataList in; | 
|  | SyncChangeList out; | 
|  | InitWithSyncDataTakeOutput(in, &out); | 
|  |  | 
|  | // Should be one header add, 2 tab adds/updates, one header update. | 
|  | ASSERT_EQ(4U, out.size()); | 
|  |  | 
|  | // Now update the sync data to be: | 
|  | // * one "normal" fully loaded tab | 
|  | // * one placeholder tab with no WebContents and no tab_id change | 
|  | sync_pb::EntitySpecifics t0_entity = out[1].sync_data().GetSpecifics(); | 
|  | sync_pb::EntitySpecifics t1_entity = out[2].sync_data().GetSpecifics(); | 
|  | in.push_back(CreateRemoteData(t0_entity)); | 
|  | in.push_back(CreateRemoteData(t1_entity)); | 
|  | out.clear(); | 
|  | manager()->StopSyncing(syncer::SESSIONS); | 
|  | ResetWindows(); | 
|  |  | 
|  | PlaceholderTabDelegate t1_override( | 
|  | SessionID::FromSerializedValue(t1_entity.session().tab().tab_id())); | 
|  | window = AddWindow(); | 
|  | window->OverrideTabAt(0, tab1); | 
|  | window->OverrideTabAt(1, &t1_override); | 
|  | InitWithSyncDataTakeOutput(in, &out); | 
|  |  | 
|  | // The last change should be the final header update, reflecting 1 window | 
|  | // and 2 tabs. | 
|  | VerifyLocalHeaderChange(out.back(), 1, 2); | 
|  |  | 
|  | // There should be one tab change, for the fully associated tab. The | 
|  | // window-ID change for the placeholder tab is not reported to avoid traffic, | 
|  | // since nothing relies on it. | 
|  | ASSERT_TRUE(AllOfChangesAreType(*FilterOutLocalHeaderChanges(&out), | 
|  | SyncChange::ACTION_UPDATE)); | 
|  | ASSERT_EQ(1U, out.size()); | 
|  | VerifyLocalTabChange(out[0], 2, kFoo2); | 
|  | } | 
|  |  | 
|  | // Ensure model association does not update the window ID for placeholder tabs. | 
|  | TEST_F(SessionsSyncManagerTest, WindowIdNotUpdatedOnRestoreForPlaceholderTab) { | 
|  | SyncDataList in; | 
|  | SyncChangeList out; | 
|  |  | 
|  | // Set up one tab and start sync with it. | 
|  | TestSyncedWindowDelegate* window = AddWindow(); | 
|  | AddTab(window->GetSessionId(), kFoo1); | 
|  | InitWithSyncDataTakeOutput(in, &out); | 
|  |  | 
|  | // Should be one header add, 1 tab add, and one header update. | 
|  | ASSERT_EQ(3U, out.size()); | 
|  | const sync_pb::EntitySpecifics t0_entity = out[1].sync_data().GetSpecifics(); | 
|  | ASSERT_TRUE(t0_entity.session().has_tab()); | 
|  |  | 
|  | in.push_back(CreateRemoteData(t0_entity)); | 
|  | out.clear(); | 
|  | manager()->StopSyncing(syncer::SESSIONS); | 
|  | ResetWindows(); | 
|  |  | 
|  | // Override the tab with a placeholder tab delegate. | 
|  | PlaceholderTabDelegate t0_override( | 
|  | SessionID::FromSerializedValue(t0_entity.session().tab().tab_id())); | 
|  |  | 
|  | // Set up the window with the new window ID and placeholder tab. | 
|  | window = AddWindow(); | 
|  | window->OverrideTabAt(0, &t0_override); | 
|  | InitWithSyncDataTakeOutput(in, &out); | 
|  |  | 
|  | // There should be no change other than the header update. | 
|  | ASSERT_EQ(0U, FilterOutLocalHeaderChanges(&out)->size()); | 
|  | } | 
|  |  | 
|  | // Ensure that the manager properly ignores a restored placeholder that refers | 
|  | // to a tab node that doesn't exist | 
|  | TEST_F(SessionsSyncManagerTest, RestoredPlacholderTabNodeDeleted) { | 
|  | syncer::SyncDataList in; | 
|  | syncer::SyncChangeList out; | 
|  |  | 
|  | // Set up one tab and start sync with it. | 
|  | TestSyncedWindowDelegate* window = AddWindow(); | 
|  | AddTab(window->GetSessionId(), kFoo1); | 
|  | InitWithSyncDataTakeOutput(in, &out); | 
|  |  | 
|  | // Should be one header add, 1 tab add, and one header update. | 
|  | ASSERT_EQ(3U, out.size()); | 
|  | const sync_pb::EntitySpecifics t0_entity = out[1].sync_data().GetSpecifics(); | 
|  | ASSERT_TRUE(t0_entity.session().has_tab()); | 
|  |  | 
|  | out.clear(); | 
|  | manager()->StopSyncing(syncer::SESSIONS); | 
|  |  | 
|  | // Override the tab with a placeholder tab delegate. | 
|  | PlaceholderTabDelegate t0_override( | 
|  | SessionID::FromSerializedValue(t0_entity.session().tab().tab_id())); | 
|  |  | 
|  | // Override the tab with a placeholder whose sync entity won't exist. | 
|  | window->OverrideTabAt(0, &t0_override); | 
|  | InitWithSyncDataTakeOutput(in, &out); | 
|  |  | 
|  | // Because no entities were passed in at associate time, there should be no | 
|  | // tab changes. | 
|  | ASSERT_EQ(0U, FilterOutLocalHeaderChanges(&out)->size()); | 
|  | } | 
|  |  | 
|  | // Tests that task ids are generated for navigations on local tabs. | 
|  | TEST_F(SessionsSyncManagerTest, TrackTasksOnLocalTabModified) { | 
|  | SyncChangeList changes; | 
|  | TestSyncedWindowDelegate* window = AddWindow(); | 
|  | InitWithSyncDataTakeOutput(SyncDataList(), &changes); | 
|  | SessionID window_id = window->GetSessionId(); | 
|  | ASSERT_FALSE(manager()->current_machine_tag().empty()); | 
|  | changes.clear(); | 
|  |  | 
|  | // Tab 1 | 
|  | AddTab(window_id, kFoo1) | 
|  | ->Navigate(kFoo2, base::Time::Now(), ui::PAGE_TRANSITION_TYPED); | 
|  | // Tab 2 | 
|  | AddTab(window_id, kBar1) | 
|  | ->Navigate(kBar2, base::Time::Now(), ui::PAGE_TRANSITION_LINK); | 
|  |  | 
|  | // We only test changes for tab add and tab update, and ignore header updates. | 
|  | FilterOutLocalHeaderChanges(&changes); | 
|  | // Sync data of adding Tab 1 change | 
|  | sync_pb::SessionTab tab = | 
|  | SyncDataLocal(changes[0].sync_data()).GetSpecifics().session().tab(); | 
|  | EXPECT_EQ(tab.navigation_size(), 1); | 
|  | EXPECT_EQ(tab.navigation(0).global_id(), tab.navigation(0).task_id()); | 
|  | EXPECT_TRUE(tab.navigation(0).ancestor_task_id().empty()); | 
|  |  | 
|  | // Sync data of updating Tab 1 change | 
|  | tab = SyncDataLocal(changes[1].sync_data()).GetSpecifics().session().tab(); | 
|  | EXPECT_EQ(tab.navigation_size(), 2); | 
|  | // navigation(0) and navigation(1) are two separated tasks. | 
|  | EXPECT_EQ(tab.navigation(0).global_id(), tab.navigation(0).task_id()); | 
|  | EXPECT_TRUE(tab.navigation(0).ancestor_task_id().empty()); | 
|  | EXPECT_EQ(tab.navigation(1).global_id(), tab.navigation(1).task_id()); | 
|  | EXPECT_TRUE(tab.navigation(1).ancestor_task_id().empty()); | 
|  |  | 
|  | // Sync data of adding Tab 2 change | 
|  | tab = SyncDataLocal(changes[2].sync_data()).GetSpecifics().session().tab(); | 
|  | EXPECT_EQ(tab.navigation_size(), 1); | 
|  | EXPECT_EQ(tab.navigation(0).global_id(), tab.navigation(0).task_id()); | 
|  | EXPECT_TRUE(tab.navigation(0).ancestor_task_id().empty()); | 
|  |  | 
|  | // Sync data of updating Tab 2 change | 
|  | tab = SyncDataLocal(changes[3].sync_data()).GetSpecifics().session().tab(); | 
|  | EXPECT_EQ(tab.navigation_size(), 2); | 
|  | EXPECT_EQ(tab.navigation(0).global_id(), tab.navigation(0).task_id()); | 
|  | EXPECT_TRUE(tab.navigation(0).ancestor_task_id().empty()); | 
|  | EXPECT_EQ(tab.navigation(1).global_id(), tab.navigation(1).task_id()); | 
|  | // navigation(1) is a subtask of navigation(0). | 
|  | EXPECT_EQ(tab.navigation(1).ancestor_task_id_size(), 1); | 
|  | EXPECT_EQ(tab.navigation(1).ancestor_task_id(0), tab.navigation(0).task_id()); | 
|  | } | 
|  |  | 
|  | }  // namespace sync_sessions |