|  | // Copyright 2018 The Chromium Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include "components/sync_sessions/local_session_event_handler_impl.h" | 
|  |  | 
|  | #include <utility> | 
|  | #include <vector> | 
|  |  | 
|  | #include "base/strings/stringprintf.h" | 
|  | #include "base/test/scoped_feature_list.h" | 
|  | #include "components/sessions/core/serialized_navigation_entry.h" | 
|  | #include "components/sessions/core/serialized_navigation_entry_test_helper.h" | 
|  | #include "components/sync/base/time.h" | 
|  | #include "components/sync/model/sync_change.h" | 
|  | #include "components/sync/protocol/sync.pb.h" | 
|  | #include "components/sync_sessions/mock_sync_sessions_client.h" | 
|  | #include "components/sync_sessions/synced_session_tracker.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 sessions::SerializedNavigationEntry; | 
|  | using sessions::SerializedNavigationEntryTestHelper; | 
|  | using testing::ByMove; | 
|  | using testing::Eq; | 
|  | using testing::IsEmpty; | 
|  | using testing::NiceMock; | 
|  | using testing::Pointee; | 
|  | using testing::Return; | 
|  | using testing::SizeIs; | 
|  | using testing::StrictMock; | 
|  | using testing::_; | 
|  |  | 
|  | const char kFoo1[] = "http://foo1/"; | 
|  | const char kBar1[] = "http://bar1/"; | 
|  | const char kBar2[] = "http://bar2/"; | 
|  | const char kBaz1[] = "http://baz1/"; | 
|  |  | 
|  | const char kSessionTag[] = "sessiontag1"; | 
|  | const char kSessionName[] = "Session Name 1"; | 
|  |  | 
|  | const base::Time kTime0 = base::Time::FromInternalValue(100); | 
|  | const base::Time kTime1 = base::Time::FromInternalValue(110); | 
|  | const base::Time kTime2 = base::Time::FromInternalValue(120); | 
|  | const base::Time kTime3 = base::Time::FromInternalValue(130); | 
|  |  | 
|  | const int kWindowId1 = 1000001; | 
|  | const int kWindowId2 = 1000002; | 
|  | const int kWindowId3 = 1000003; | 
|  | const int kTabId1 = 1000004; | 
|  | const int kTabId2 = 1000005; | 
|  | const int kTabId3 = 1000006; | 
|  |  | 
|  | class MockWriteBatch : public LocalSessionEventHandlerImpl::WriteBatch { | 
|  | public: | 
|  | MockWriteBatch() {} | 
|  | ~MockWriteBatch() override {} | 
|  |  | 
|  | MOCK_METHOD1(Delete, void(int tab_node_id)); | 
|  | MOCK_METHOD1(Put, void(std::unique_ptr<sync_pb::SessionSpecifics> specifics)); | 
|  | MOCK_METHOD0(Commit, void()); | 
|  | }; | 
|  |  | 
|  | class MockDelegate : public LocalSessionEventHandlerImpl::Delegate { | 
|  | public: | 
|  | ~MockDelegate() override {} | 
|  |  | 
|  | MOCK_METHOD0(CreateLocalSessionWriteBatch, | 
|  | std::unique_ptr<LocalSessionEventHandlerImpl::WriteBatch>()); | 
|  | MOCK_METHOD1(IsTabNodeUnsynced, bool(int tab_node_id)); | 
|  | MOCK_METHOD2(TrackLocalNavigationId, | 
|  | void(base::Time timestamp, int unique_id)); | 
|  | MOCK_METHOD1(OnPageFaviconUpdated, void(const GURL& page_url)); | 
|  | MOCK_METHOD2(OnFaviconVisited, | 
|  | void(const GURL& page_url, const GURL& favicon_url)); | 
|  | }; | 
|  |  | 
|  | class LocalSessionEventHandlerImplTest : public testing::Test { | 
|  | protected: | 
|  | LocalSessionEventHandlerImplTest() | 
|  | : session_tracker_(&mock_sync_sessions_client_) { | 
|  | ON_CALL(mock_sync_sessions_client_, GetSyncedWindowDelegatesGetter()) | 
|  | .WillByDefault(testing::Return(&window_getter_)); | 
|  | ON_CALL(mock_delegate_, CreateLocalSessionWriteBatch()) | 
|  | .WillByDefault( | 
|  | Return(ByMove(std::make_unique<NiceMock<MockWriteBatch>>()))); | 
|  |  | 
|  | session_tracker_.InitLocalSession(kSessionTag, kSessionName, | 
|  | sync_pb::SyncEnums_DeviceType_TYPE_PHONE); | 
|  | } | 
|  |  | 
|  | void InitHandler() { | 
|  | handler_ = std::make_unique<LocalSessionEventHandlerImpl>( | 
|  | &mock_delegate_, &mock_sync_sessions_client_, &session_tracker_); | 
|  | window_getter_.router()->StartRoutingTo(handler_.get()); | 
|  | } | 
|  |  | 
|  | TestSyncedWindowDelegate* AddWindow( | 
|  | int window_id, | 
|  | sync_pb::SessionWindow_BrowserType type = | 
|  | sync_pb::SessionWindow_BrowserType_TYPE_TABBED) { | 
|  | return window_getter_.AddWindow(type, | 
|  | SessionID::FromSerializedValue(window_id)); | 
|  | } | 
|  |  | 
|  | TestSyncedTabDelegate* AddTab(int window_id, | 
|  | const std::string& url, | 
|  | int tab_id = SessionID::NewUnique().id()) { | 
|  | TestSyncedTabDelegate* tab = | 
|  | window_getter_.AddTab(SessionID::FromSerializedValue(window_id), | 
|  | SessionID::FromSerializedValue(tab_id)); | 
|  | tab->Navigate(url, base::Time::Now()); | 
|  | return tab; | 
|  | } | 
|  |  | 
|  | TestSyncedTabDelegate* AddTabWithTime(int window_id, | 
|  | const std::string& url, | 
|  | base::Time time = base::Time::Now()) { | 
|  | TestSyncedTabDelegate* tab = | 
|  | window_getter_.AddTab(SessionID::FromSerializedValue(window_id)); | 
|  | tab->Navigate(url, time); | 
|  | return tab; | 
|  | } | 
|  |  | 
|  | testing::NiceMock<MockDelegate> mock_delegate_; | 
|  | testing::NiceMock<MockSyncSessionsClient> mock_sync_sessions_client_; | 
|  | SyncedSessionTracker session_tracker_; | 
|  | TestSyncedWindowDelegatesGetter window_getter_; | 
|  | std::unique_ptr<LocalSessionEventHandlerImpl> handler_; | 
|  | }; | 
|  |  | 
|  | // Populate the mock tab delegate with some data and navigation | 
|  | // entries and make sure that populating a SessionTab contains analgous | 
|  | // information. | 
|  | TEST_F(LocalSessionEventHandlerImplTest, GetTabSpecificsFromDelegate) { | 
|  | // Create a tab with three valid entries. | 
|  | AddWindow(kWindowId1); | 
|  | TestSyncedTabDelegate* tab = AddTabWithTime(kWindowId1, kFoo1, kTime1); | 
|  | tab->Navigate(kBar1, kTime2); | 
|  | tab->Navigate(kBaz1, kTime3); | 
|  | InitHandler(); | 
|  |  | 
|  | const sync_pb::SessionTab session_tab = | 
|  | handler_->GetTabSpecificsFromDelegateForTest(*tab); | 
|  |  | 
|  | EXPECT_EQ(tab->GetWindowId().id(), session_tab.window_id()); | 
|  | EXPECT_EQ(tab->GetSessionId().id(), session_tab.tab_id()); | 
|  | EXPECT_EQ(0, session_tab.tab_visual_index()); | 
|  | EXPECT_EQ(tab->GetCurrentEntryIndex(), | 
|  | session_tab.current_navigation_index()); | 
|  | EXPECT_FALSE(session_tab.pinned()); | 
|  | EXPECT_TRUE(session_tab.extension_app_id().empty()); | 
|  | ASSERT_EQ(3, session_tab.navigation_size()); | 
|  | EXPECT_EQ(GURL(kFoo1), session_tab.navigation(0).virtual_url()); | 
|  | EXPECT_EQ(GURL(kBar1), session_tab.navigation(1).virtual_url()); | 
|  | EXPECT_EQ(GURL(kBaz1), session_tab.navigation(2).virtual_url()); | 
|  | EXPECT_EQ(syncer::TimeToProtoTime(kTime1), | 
|  | session_tab.navigation(0).timestamp_msec()); | 
|  | EXPECT_EQ(syncer::TimeToProtoTime(kTime2), | 
|  | session_tab.navigation(1).timestamp_msec()); | 
|  | EXPECT_EQ(syncer::TimeToProtoTime(kTime3), | 
|  | session_tab.navigation(2).timestamp_msec()); | 
|  | EXPECT_EQ(200, session_tab.navigation(0).http_status_code()); | 
|  | EXPECT_EQ(200, session_tab.navigation(1).http_status_code()); | 
|  | EXPECT_EQ(200, session_tab.navigation(2).http_status_code()); | 
|  | EXPECT_FALSE(session_tab.navigation(0).has_blocked_state()); | 
|  | EXPECT_FALSE(session_tab.navigation(1).has_blocked_state()); | 
|  | EXPECT_FALSE(session_tab.navigation(2).has_blocked_state()); | 
|  | } | 
|  |  | 
|  | // Ensure the current_navigation_index gets set properly when the navigation | 
|  | // stack gets trucated to +/- 6 entries. | 
|  | TEST_F(LocalSessionEventHandlerImplTest, | 
|  | SetSessionTabFromDelegateNavigationIndex) { | 
|  | AddWindow(kWindowId1); | 
|  | TestSyncedTabDelegate* tab = AddTab(kWindowId1, kFoo1); | 
|  | const int kNavs = 10; | 
|  | for (int i = 1; i < kNavs; ++i) { | 
|  | tab->Navigate(base::StringPrintf("http://foo%i", i)); | 
|  | } | 
|  | tab->set_current_entry_index(kNavs - 2); | 
|  |  | 
|  | InitHandler(); | 
|  |  | 
|  | const sync_pb::SessionTab session_tab = | 
|  | handler_->GetTabSpecificsFromDelegateForTest(*tab); | 
|  |  | 
|  | EXPECT_EQ(6, session_tab.current_navigation_index()); | 
|  | ASSERT_EQ(8, session_tab.navigation_size()); | 
|  | EXPECT_EQ(GURL("http://foo2"), session_tab.navigation(0).virtual_url()); | 
|  | EXPECT_EQ(GURL("http://foo3"), session_tab.navigation(1).virtual_url()); | 
|  | EXPECT_EQ(GURL("http://foo4"), session_tab.navigation(2).virtual_url()); | 
|  | } | 
|  |  | 
|  | // Ensure the current_navigation_index gets set to the end of the navigation | 
|  | // stack if the current navigation is invalid. | 
|  | TEST_F(LocalSessionEventHandlerImplTest, | 
|  | SetSessionTabFromDelegateCurrentInvalid) { | 
|  | AddWindow(kWindowId1); | 
|  | TestSyncedTabDelegate* tab = AddTabWithTime(kWindowId1, kFoo1, kTime0); | 
|  | tab->Navigate(std::string(""), kTime1); | 
|  | tab->Navigate(kBar1, kTime2); | 
|  | tab->Navigate(kBar2, kTime3); | 
|  | tab->set_current_entry_index(1); | 
|  |  | 
|  | InitHandler(); | 
|  |  | 
|  | const sync_pb::SessionTab session_tab = | 
|  | handler_->GetTabSpecificsFromDelegateForTest(*tab); | 
|  |  | 
|  | EXPECT_EQ(2, session_tab.current_navigation_index()); | 
|  | ASSERT_EQ(3, session_tab.navigation_size()); | 
|  | } | 
|  |  | 
|  | // Tests that for supervised users blocked navigations are recorded and marked | 
|  | // as such, while regular navigations are marked as allowed. | 
|  | TEST_F(LocalSessionEventHandlerImplTest, BlockedNavigations) { | 
|  | AddWindow(kWindowId1); | 
|  | TestSyncedTabDelegate* tab = AddTabWithTime(kWindowId1, kFoo1, kTime1); | 
|  |  | 
|  | auto entry2 = std::make_unique<sessions::SerializedNavigationEntry>(); | 
|  | GURL url2("http://blocked.com/foo"); | 
|  | SerializedNavigationEntryTestHelper::SetVirtualURL(GURL(url2), entry2.get()); | 
|  | SerializedNavigationEntryTestHelper::SetTimestamp(kTime2, entry2.get()); | 
|  |  | 
|  | auto entry3 = std::make_unique<sessions::SerializedNavigationEntry>(); | 
|  | GURL url3("http://evil.com"); | 
|  | SerializedNavigationEntryTestHelper::SetVirtualURL(GURL(url3), entry3.get()); | 
|  | SerializedNavigationEntryTestHelper::SetTimestamp(kTime3, entry3.get()); | 
|  |  | 
|  | std::vector<std::unique_ptr<sessions::SerializedNavigationEntry>> | 
|  | blocked_navigations; | 
|  | blocked_navigations.push_back(std::move(entry2)); | 
|  | blocked_navigations.push_back(std::move(entry3)); | 
|  |  | 
|  | tab->set_is_supervised(true); | 
|  | tab->set_blocked_navigations(blocked_navigations); | 
|  |  | 
|  | InitHandler(); | 
|  | const sync_pb::SessionTab session_tab = | 
|  | handler_->GetTabSpecificsFromDelegateForTest(*tab); | 
|  |  | 
|  | EXPECT_EQ(tab->GetWindowId().id(), session_tab.window_id()); | 
|  | EXPECT_EQ(tab->GetSessionId().id(), session_tab.tab_id()); | 
|  | EXPECT_EQ(0, session_tab.tab_visual_index()); | 
|  | EXPECT_EQ(0, session_tab.current_navigation_index()); | 
|  | EXPECT_FALSE(session_tab.pinned()); | 
|  | ASSERT_EQ(3, session_tab.navigation_size()); | 
|  | EXPECT_EQ(GURL(kFoo1), session_tab.navigation(0).virtual_url()); | 
|  | EXPECT_EQ(url2, session_tab.navigation(1).virtual_url()); | 
|  | EXPECT_EQ(url3, session_tab.navigation(2).virtual_url()); | 
|  | EXPECT_EQ(syncer::TimeToProtoTime(kTime1), | 
|  | session_tab.navigation(0).timestamp_msec()); | 
|  | EXPECT_EQ(syncer::TimeToProtoTime(kTime2), | 
|  | session_tab.navigation(1).timestamp_msec()); | 
|  | EXPECT_EQ(syncer::TimeToProtoTime(kTime3), | 
|  | session_tab.navigation(2).timestamp_msec()); | 
|  | EXPECT_TRUE(session_tab.navigation(0).has_blocked_state()); | 
|  | EXPECT_TRUE(session_tab.navigation(1).has_blocked_state()); | 
|  | EXPECT_TRUE(session_tab.navigation(2).has_blocked_state()); | 
|  | EXPECT_EQ(sync_pb::TabNavigation_BlockedState_STATE_ALLOWED, | 
|  | session_tab.navigation(0).blocked_state()); | 
|  | EXPECT_EQ(sync_pb::TabNavigation_BlockedState_STATE_BLOCKED, | 
|  | session_tab.navigation(1).blocked_state()); | 
|  | EXPECT_EQ(sync_pb::TabNavigation_BlockedState_STATE_BLOCKED, | 
|  | session_tab.navigation(2).blocked_state()); | 
|  | } | 
|  |  | 
|  | // Tests that calling AssociateWindowsAndTabs() handles well the case with no | 
|  | // open tabs or windows. | 
|  | TEST_F(LocalSessionEventHandlerImplTest, AssociateWindowsAndTabsIfEmpty) { | 
|  | EXPECT_CALL(mock_delegate_, CreateLocalSessionWriteBatch()).Times(0); | 
|  | EXPECT_CALL(mock_delegate_, OnPageFaviconUpdated(_)).Times(0); | 
|  | EXPECT_CALL(mock_delegate_, OnFaviconVisited(_, _)).Times(0); | 
|  |  | 
|  | auto mock_batch = std::make_unique<StrictMock<MockWriteBatch>>(); | 
|  | EXPECT_CALL(*mock_batch, | 
|  | Put(Pointee(MatchesHeader(kSessionTag, /*window_ids=*/IsEmpty(), | 
|  | /*tabs_ids=*/IsEmpty())))); | 
|  | EXPECT_CALL(*mock_batch, Commit()); | 
|  | EXPECT_CALL(mock_delegate_, CreateLocalSessionWriteBatch()) | 
|  | .WillOnce(Return(ByMove(std::move(mock_batch)))); | 
|  |  | 
|  | InitHandler(); | 
|  | } | 
|  |  | 
|  | // Tests that calling AssociateWindowsAndTabs() reflects the open tabs in a) the | 
|  | // SyncSessionTracker and b) the delegate. | 
|  | TEST_F(LocalSessionEventHandlerImplTest, AssociateWindowsAndTabs) { | 
|  | AddWindow(kWindowId1); | 
|  | AddTab(kWindowId1, kFoo1, kTabId1); | 
|  | AddWindow(kWindowId2); | 
|  | AddTab(kWindowId2, kBar1, kTabId2); | 
|  | AddTab(kWindowId2, kBar2, kTabId3)->Navigate(kBaz1); | 
|  |  | 
|  | EXPECT_CALL(mock_delegate_, CreateLocalSessionWriteBatch()).Times(0); | 
|  | EXPECT_CALL(mock_delegate_, OnPageFaviconUpdated(_)).Times(0); | 
|  | EXPECT_CALL(mock_delegate_, OnFaviconVisited(GURL(kBar2), _)).Times(0); | 
|  |  | 
|  | EXPECT_CALL(mock_delegate_, OnFaviconVisited(GURL(kFoo1), _)); | 
|  | EXPECT_CALL(mock_delegate_, OnFaviconVisited(GURL(kBar1), _)); | 
|  | EXPECT_CALL(mock_delegate_, OnFaviconVisited(GURL(kBaz1), _)); | 
|  |  | 
|  | auto mock_batch = std::make_unique<StrictMock<MockWriteBatch>>(); | 
|  | EXPECT_CALL(*mock_batch, | 
|  | Put(Pointee(MatchesHeader(kSessionTag, {kWindowId1, kWindowId2}, | 
|  | {kTabId1, kTabId2, kTabId3})))); | 
|  | EXPECT_CALL(*mock_batch, | 
|  | Put(Pointee(MatchesTab(kSessionTag, kWindowId1, kTabId1, | 
|  | /*tab_node_id=*/_, | 
|  | /*urls=*/{kFoo1})))); | 
|  | EXPECT_CALL(*mock_batch, | 
|  | Put(Pointee(MatchesTab(kSessionTag, kWindowId2, kTabId2, | 
|  | /*tab_node_id=*/_, /*urls=*/{kBar1})))); | 
|  | EXPECT_CALL( | 
|  | *mock_batch, | 
|  | Put(Pointee(MatchesTab(kSessionTag, kWindowId2, kTabId3, | 
|  | /*tab_node_id=*/_, /*urls=*/{kBar2, kBaz1})))); | 
|  | EXPECT_CALL(*mock_batch, Commit()); | 
|  |  | 
|  | EXPECT_CALL(mock_delegate_, CreateLocalSessionWriteBatch()) | 
|  | .WillOnce(Return(ByMove(std::move(mock_batch)))); | 
|  |  | 
|  | InitHandler(); | 
|  | } | 
|  |  | 
|  | // Tests that association does not refresh window IDs for placeholder tabs, even | 
|  | // if the window ID changes across restarts. | 
|  | TEST_F(LocalSessionEventHandlerImplTest, DontUpdateWindowIdForPlaceholderTab) { | 
|  | const int kRegularTabNodeId = 1; | 
|  | const int kPlaceholderTabNodeId = 2; | 
|  |  | 
|  | // The tracker is initially restored from persisted state, containing a | 
|  | // regular tab and a placeholder tab. This mimics | 
|  | // SessionsSyncManager::InitFromSyncModel(). | 
|  | sync_pb::SessionSpecifics regular_tab; | 
|  | regular_tab.set_session_tag(kSessionTag); | 
|  | regular_tab.set_tab_node_id(kRegularTabNodeId); | 
|  | regular_tab.mutable_tab()->set_window_id(kWindowId1); | 
|  | regular_tab.mutable_tab()->set_tab_id(kTabId1); | 
|  | session_tracker_.ReassociateLocalTab(kRegularTabNodeId, | 
|  | SessionID::FromSerializedValue(kTabId1)); | 
|  | UpdateTrackerWithSpecifics(regular_tab, base::Time::Now(), &session_tracker_); | 
|  |  | 
|  | sync_pb::SessionSpecifics placeholder_tab; | 
|  | placeholder_tab.set_session_tag(kSessionTag); | 
|  | placeholder_tab.set_tab_node_id(kPlaceholderTabNodeId); | 
|  | placeholder_tab.mutable_tab()->set_window_id(kWindowId1); | 
|  | placeholder_tab.mutable_tab()->set_tab_id(kTabId2); | 
|  | session_tracker_.ReassociateLocalTab(kPlaceholderTabNodeId, | 
|  | SessionID::FromSerializedValue(kTabId2)); | 
|  | UpdateTrackerWithSpecifics(placeholder_tab, base::Time::Now(), | 
|  | &session_tracker_); | 
|  |  | 
|  | // Mimic the header being restored from peristence too. | 
|  | session_tracker_.PutWindowInSession( | 
|  | kSessionTag, SessionID::FromSerializedValue(kWindowId1)); | 
|  | session_tracker_.PutTabInWindow(kSessionTag, | 
|  | SessionID::FromSerializedValue(kWindowId1), | 
|  | SessionID::FromSerializedValue(kTabId1)); | 
|  | session_tracker_.PutTabInWindow(kSessionTag, | 
|  | SessionID::FromSerializedValue(kWindowId1), | 
|  | SessionID::FromSerializedValue(kTabId2)); | 
|  |  | 
|  | // Window ID has changed when the browser is started. | 
|  | TestSyncedWindowDelegate* window = AddWindow(kWindowId2); | 
|  | AddTab(kWindowId2, kFoo1, kTabId1); | 
|  | PlaceholderTabDelegate t1_override(SessionID::FromSerializedValue(kTabId2)); | 
|  | window->OverrideTabAt(1, &t1_override); | 
|  |  | 
|  | // Verify that window ID is updated for the regular tab, but not for the | 
|  | // placeholder tab. | 
|  | auto mock_batch = std::make_unique<StrictMock<MockWriteBatch>>(); | 
|  | EXPECT_CALL(*mock_batch, Put(Pointee(MatchesHeader(kSessionTag, {kWindowId2}, | 
|  | {kTabId1, kTabId2})))); | 
|  | EXPECT_CALL(*mock_batch, Put(Pointee(MatchesTab(kSessionTag, kWindowId2, | 
|  | kTabId1, kRegularTabNodeId, | 
|  | /*urls=*/{kFoo1})))); | 
|  | EXPECT_CALL(*mock_batch, Commit()); | 
|  |  | 
|  | EXPECT_CALL(mock_delegate_, CreateLocalSessionWriteBatch()) | 
|  | .WillOnce(Return(ByMove(std::move(mock_batch)))); | 
|  |  | 
|  | InitHandler(); | 
|  | } | 
|  |  | 
|  | // Tests that association of windows and tabs gets deferred due to ongoing | 
|  | // session restore during startup. | 
|  | TEST_F(LocalSessionEventHandlerImplTest, | 
|  | DeferAssociationDueToInitialSessionRestore) { | 
|  | AddWindow(kWindowId1)->SetIsSessionRestoreInProgress(true); | 
|  | AddTab(kWindowId1, kFoo1, kTabId1); | 
|  | AddWindow(kWindowId2); | 
|  | AddTab(kWindowId2, kBar1, kTabId2); | 
|  | AddTab(kWindowId2, kBar2, kTabId3)->Navigate(kBaz1); | 
|  |  | 
|  | EXPECT_CALL(mock_delegate_, CreateLocalSessionWriteBatch()).Times(0); | 
|  |  | 
|  | InitHandler(); | 
|  |  | 
|  | auto mock_batch = std::make_unique<StrictMock<MockWriteBatch>>(); | 
|  | EXPECT_CALL(*mock_batch, | 
|  | Put(Pointee(MatchesHeader(kSessionTag, {kWindowId1, kWindowId2}, | 
|  | {kTabId1, kTabId2, kTabId3})))); | 
|  | EXPECT_CALL(*mock_batch, | 
|  | Put(Pointee(MatchesTab(kSessionTag, kWindowId1, kTabId1, | 
|  | /*tab_node_id=*/_, | 
|  | /*urls=*/{kFoo1})))); | 
|  | EXPECT_CALL(*mock_batch, | 
|  | Put(Pointee(MatchesTab(kSessionTag, kWindowId2, kTabId2, | 
|  | /*tab_node_id=*/_, /*urls=*/{kBar1})))); | 
|  | EXPECT_CALL( | 
|  | *mock_batch, | 
|  | Put(Pointee(MatchesTab(kSessionTag, kWindowId2, kTabId3, | 
|  | /*tab_node_id=*/_, /*urls=*/{kBar2, kBaz1})))); | 
|  | EXPECT_CALL(*mock_batch, Commit()); | 
|  |  | 
|  | EXPECT_CALL(mock_delegate_, CreateLocalSessionWriteBatch()) | 
|  | .WillOnce(Return(ByMove(std::move(mock_batch)))); | 
|  |  | 
|  | window_getter_.SessionRestoreComplete(); | 
|  | } | 
|  |  | 
|  | // Tests that association of windows and tabs gets deferred due to ongoing | 
|  | // session restore happening at a late stage (e.g. CCT-only / no-tabbed-window | 
|  | // to tabbed-window transition). | 
|  | TEST_F(LocalSessionEventHandlerImplTest, | 
|  | DeferAssociationDueToLateSessionRestore) { | 
|  | AddWindow(kWindowId1); | 
|  | AddTab(kWindowId1, kFoo1, kTabId1); | 
|  |  | 
|  | InitHandler(); | 
|  |  | 
|  | // No updates expected during session restore. | 
|  | EXPECT_CALL(mock_delegate_, CreateLocalSessionWriteBatch()).Times(0); | 
|  |  | 
|  | AddWindow(kWindowId2)->SetIsSessionRestoreInProgress(true); | 
|  | AddTab(kWindowId2, kBar1, kTabId2); | 
|  | AddTab(kWindowId2, kBar2, kTabId3)->Navigate(kBaz1); | 
|  |  | 
|  | // As soon as session restore completes, we expect all updates. | 
|  | auto mock_batch = std::make_unique<StrictMock<MockWriteBatch>>(); | 
|  | EXPECT_CALL(*mock_batch, | 
|  | Put(Pointee(MatchesHeader(kSessionTag, {kWindowId1, kWindowId2}, | 
|  | {kTabId1, kTabId2, kTabId3})))); | 
|  | EXPECT_CALL(*mock_batch, | 
|  | Put(Pointee(MatchesTab(kSessionTag, kWindowId1, kTabId1, | 
|  | /*tab_node_id=*/_, | 
|  | /*urls=*/{kFoo1})))); | 
|  | EXPECT_CALL(*mock_batch, | 
|  | Put(Pointee(MatchesTab(kSessionTag, kWindowId2, kTabId2, | 
|  | /*tab_node_id=*/_, /*urls=*/{kBar1})))); | 
|  | EXPECT_CALL( | 
|  | *mock_batch, | 
|  | Put(Pointee(MatchesTab(kSessionTag, kWindowId2, kTabId3, | 
|  | /*tab_node_id=*/_, /*urls=*/{kBar2, kBaz1})))); | 
|  | EXPECT_CALL(*mock_batch, Commit()); | 
|  |  | 
|  | EXPECT_CALL(mock_delegate_, CreateLocalSessionWriteBatch()) | 
|  | .WillOnce(Return(ByMove(std::move(mock_batch)))); | 
|  |  | 
|  | window_getter_.SessionRestoreComplete(); | 
|  | } | 
|  |  | 
|  | // Tests that calling AssociateWindowsAndTabs() reflects the open tabs in a) the | 
|  | // SyncSessionTracker and b) the delegate, for the case where a custom tab | 
|  | // exists without native data (no tabbed window). | 
|  | TEST_F(LocalSessionEventHandlerImplTest, AssociateCustomTab) { | 
|  | const int kRegularTabNodeId = 1; | 
|  | const int kCustomTabNodeId = 2; | 
|  |  | 
|  | // The tracker is initially restored from persisted state, containing a | 
|  | // regular tab and a custom tab. This mimics | 
|  | // SessionsSyncManager::InitFromSyncModel(). | 
|  | sync_pb::SessionSpecifics regular_tab; | 
|  | regular_tab.set_session_tag(kSessionTag); | 
|  | regular_tab.set_tab_node_id(kRegularTabNodeId); | 
|  | regular_tab.mutable_tab()->set_window_id(kWindowId1); | 
|  | regular_tab.mutable_tab()->set_tab_id(kTabId1); | 
|  | session_tracker_.ReassociateLocalTab(kRegularTabNodeId, | 
|  | SessionID::FromSerializedValue(kTabId1)); | 
|  | UpdateTrackerWithSpecifics(regular_tab, base::Time::Now(), &session_tracker_); | 
|  |  | 
|  | sync_pb::SessionSpecifics custom_tab; | 
|  | custom_tab.set_session_tag(kSessionTag); | 
|  | custom_tab.set_tab_node_id(kCustomTabNodeId); | 
|  | custom_tab.mutable_tab()->set_window_id(kWindowId2); | 
|  | custom_tab.mutable_tab()->set_tab_id(kTabId2); | 
|  | session_tracker_.ReassociateLocalTab(kCustomTabNodeId, | 
|  | SessionID::FromSerializedValue(kTabId2)); | 
|  | UpdateTrackerWithSpecifics(custom_tab, base::Time::Now(), &session_tracker_); | 
|  |  | 
|  | sync_pb::SessionSpecifics header; | 
|  | header.set_session_tag(kSessionTag); | 
|  | header.mutable_header()->add_window()->set_window_id(kWindowId1); | 
|  | header.mutable_header()->mutable_window(0)->add_tab(kTabId1); | 
|  | header.mutable_header()->add_window()->set_window_id(kWindowId2); | 
|  | header.mutable_header()->mutable_window(1)->add_tab(kTabId2); | 
|  | UpdateTrackerWithSpecifics(header, base::Time::Now(), &session_tracker_); | 
|  |  | 
|  | ASSERT_THAT(session_tracker_.LookupSession(kSessionTag), | 
|  | MatchesSyncedSession(kSessionTag, | 
|  | {{kWindowId1, std::vector<int>{kTabId1}}, | 
|  | {kWindowId2, std::vector<int>{kTabId2}}})); | 
|  |  | 
|  | // In the current session, all we have is a custom tab. | 
|  | AddWindow(kWindowId3, sync_pb::SessionWindow_BrowserType_TYPE_CUSTOM_TAB); | 
|  | AddTab(kWindowId3, kFoo1, kTabId2); | 
|  |  | 
|  | auto mock_batch = std::make_unique<StrictMock<MockWriteBatch>>(); | 
|  | EXPECT_CALL(*mock_batch, Put(Pointee(MatchesTab(kSessionTag, kWindowId3, | 
|  | kTabId2, kCustomTabNodeId, | 
|  | /*urls=*/{kFoo1})))); | 
|  | EXPECT_CALL(*mock_batch, | 
|  | Put(Pointee(MatchesHeader(kSessionTag, | 
|  | {kWindowId1, kWindowId2, kWindowId3}, | 
|  | {kTabId1, kTabId2})))); | 
|  | EXPECT_CALL(*mock_batch, Commit()); | 
|  |  | 
|  | EXPECT_CALL(mock_delegate_, CreateLocalSessionWriteBatch()) | 
|  | .WillOnce(Return(ByMove(std::move(mock_batch)))); | 
|  |  | 
|  | InitHandler(); | 
|  |  | 
|  | EXPECT_THAT(session_tracker_.LookupSession(kSessionTag), | 
|  | MatchesSyncedSession(kSessionTag, | 
|  | {{kWindowId1, std::vector<int>{kTabId1}}, | 
|  | {kWindowId2, std::vector<int>()}, | 
|  | {kWindowId3, std::vector<int>{kTabId2}}})); | 
|  | } | 
|  |  | 
|  | TEST_F(LocalSessionEventHandlerImplTest, PropagateNewNavigation) { | 
|  | AddWindow(kWindowId1); | 
|  | TestSyncedTabDelegate* tab = AddTab(kWindowId1, kFoo1, kTabId1); | 
|  |  | 
|  | InitHandler(); | 
|  |  | 
|  | auto update_mock_batch = std::make_unique<StrictMock<MockWriteBatch>>(); | 
|  | // Note that the header is reported again, although it hasn't changed. This is | 
|  | // OK because sync will avoid updating an entity with identical content. | 
|  | EXPECT_CALL( | 
|  | *update_mock_batch, | 
|  | Put(Pointee(MatchesHeader(kSessionTag, {kWindowId1}, {kTabId1})))); | 
|  | EXPECT_CALL(*update_mock_batch, | 
|  | Put(Pointee(MatchesTab(kSessionTag, kWindowId1, kTabId1, | 
|  | /*tab_node_id=*/_, | 
|  | /*urls=*/{kFoo1, kBar1})))); | 
|  | EXPECT_CALL(*update_mock_batch, Commit()); | 
|  |  | 
|  | EXPECT_CALL(mock_delegate_, CreateLocalSessionWriteBatch()) | 
|  | .WillOnce(Return(ByMove(std::move(update_mock_batch)))); | 
|  |  | 
|  | tab->Navigate(kBar1); | 
|  | } | 
|  |  | 
|  | TEST_F(LocalSessionEventHandlerImplTest, PropagateNewTab) { | 
|  | AddWindow(kWindowId1); | 
|  | AddTab(kWindowId1, kFoo1, kTabId1); | 
|  |  | 
|  | InitHandler(); | 
|  |  | 
|  | // 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. | 
|  | auto tab_create_mock_batch = std::make_unique<StrictMock<MockWriteBatch>>(); | 
|  | EXPECT_CALL( | 
|  | *tab_create_mock_batch, | 
|  | Put(Pointee(MatchesHeader(kSessionTag, {kWindowId1}, {kTabId1})))); | 
|  | EXPECT_CALL(*tab_create_mock_batch, Commit()); | 
|  |  | 
|  | auto navigation_mock_batch = std::make_unique<StrictMock<MockWriteBatch>>(); | 
|  | EXPECT_CALL(*navigation_mock_batch, | 
|  | Put(Pointee(MatchesHeader(kSessionTag, {kWindowId1}, | 
|  | {kTabId1, kTabId2})))); | 
|  | EXPECT_CALL(*navigation_mock_batch, | 
|  | Put(Pointee(MatchesTab(kSessionTag, kWindowId1, kTabId2, | 
|  | /*tab_node_id=*/_, /*urls=*/{kBar1})))); | 
|  | EXPECT_CALL(*navigation_mock_batch, Commit()); | 
|  |  | 
|  | EXPECT_CALL(mock_delegate_, CreateLocalSessionWriteBatch()) | 
|  | .WillOnce(Return(ByMove(std::move(tab_create_mock_batch)))) | 
|  | .WillOnce(Return(ByMove(std::move(navigation_mock_batch)))); | 
|  |  | 
|  | AddTab(kWindowId1, kBar1, kTabId2); | 
|  | } | 
|  |  | 
|  | TEST_F(LocalSessionEventHandlerImplTest, PropagateClosedTab) { | 
|  | AddWindow(kWindowId1); | 
|  | AddTab(kWindowId1, kFoo1, kTabId1); | 
|  | TestSyncedTabDelegate* tab2 = AddTab(kWindowId1, kBar1, kTabId2); | 
|  |  | 
|  | InitHandler(); | 
|  |  | 
|  | // Closing a tab (later below) is expected to verify if the sync entity is | 
|  | // unsynced. | 
|  | EXPECT_CALL(mock_delegate_, IsTabNodeUnsynced(/*tab_node_id=*/0)); | 
|  |  | 
|  | // Closing a tab is expected to update the header and the remaining tab (this | 
|  | // test issues a navigation for it, but it would have been updated anyway). | 
|  | auto mock_batch = std::make_unique<StrictMock<MockWriteBatch>>(); | 
|  | EXPECT_CALL( | 
|  | *mock_batch, | 
|  | Put(Pointee(MatchesHeader(kSessionTag, {kWindowId1}, {kTabId2})))); | 
|  | EXPECT_CALL(*mock_batch, | 
|  | Put(Pointee(MatchesTab(kSessionTag, kWindowId1, kTabId2, | 
|  | /*tab_node_id=*/1, /*urls=*/{kBar1})))); | 
|  | EXPECT_CALL(*mock_batch, Commit()); | 
|  | EXPECT_CALL(mock_delegate_, CreateLocalSessionWriteBatch()) | 
|  | .WillOnce(Return(ByMove(std::move(mock_batch)))); | 
|  |  | 
|  | // Close tab and force reassociation. | 
|  | window_getter_.CloseTab(SessionID::FromSerializedValue(kTabId1)); | 
|  | handler_->OnLocalTabModified(tab2); | 
|  | } | 
|  |  | 
|  | TEST_F(LocalSessionEventHandlerImplTest, | 
|  | PropagateClosedTabWithDeferredRecyclingAndImmediateDeletion) { | 
|  | base::test::ScopedFeatureList feature_list; | 
|  | feature_list.InitWithFeatures( | 
|  | /*enabled_features=*/{kDeferRecyclingOfSyncTabNodesIfUnsynced, | 
|  | kTabNodePoolImmediateDeletion}, | 
|  | /*disabled_features=*/{}); | 
|  |  | 
|  | // We start with three tabs. | 
|  | AddWindow(kWindowId1); | 
|  | AddTab(kWindowId1, kFoo1, kTabId1); | 
|  | AddTab(kWindowId1, kBar1, kTabId2); | 
|  | TestSyncedTabDelegate* tab3 = AddTab(kWindowId1, kBaz1, kTabId3); | 
|  |  | 
|  | InitHandler(); | 
|  |  | 
|  | // |kTabId2| is unsynced, so it shouldn't be deleted even if it's closed. | 
|  | EXPECT_CALL(mock_delegate_, IsTabNodeUnsynced(/*tab_node_id=*/0)) | 
|  | .WillOnce(Return(false)); | 
|  | EXPECT_CALL(mock_delegate_, IsTabNodeUnsynced(/*tab_node_id=*/1)) | 
|  | .WillOnce(Return(true)); | 
|  |  | 
|  | // Closing two tabs (later below) is expected to update the header and the | 
|  | // remaining tab. In addition, one of the two closed tabs (the one that is | 
|  | // synced) should be deleted. | 
|  | auto mock_batch = std::make_unique<StrictMock<MockWriteBatch>>(); | 
|  | EXPECT_CALL( | 
|  | *mock_batch, | 
|  | Put(Pointee(MatchesHeader(kSessionTag, {kWindowId1}, {kTabId3})))); | 
|  | EXPECT_CALL(*mock_batch, | 
|  | Put(Pointee(MatchesTab(kSessionTag, kWindowId1, kTabId3, | 
|  | /*tab_node_id=*/2, /*urls=*/{kBaz1})))); | 
|  | EXPECT_CALL(*mock_batch, Delete(/*tab_node_id=*/0)); | 
|  | EXPECT_CALL(*mock_batch, Commit()); | 
|  | EXPECT_CALL(mock_delegate_, CreateLocalSessionWriteBatch()) | 
|  | .WillOnce(Return(ByMove(std::move(mock_batch)))); | 
|  |  | 
|  | // Close two tabs and force reassociation. | 
|  | window_getter_.CloseTab(SessionID::FromSerializedValue(kTabId1)); | 
|  | window_getter_.CloseTab(SessionID::FromSerializedValue(kTabId2)); | 
|  | handler_->OnLocalTabModified(tab3); | 
|  | } | 
|  |  | 
|  | TEST_F(LocalSessionEventHandlerImplTest, PropagateNewCustomTab) { | 
|  | InitHandler(); | 
|  |  | 
|  | // 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. | 
|  | auto tab_create_mock_batch = std::make_unique<StrictMock<MockWriteBatch>>(); | 
|  | EXPECT_CALL(*tab_create_mock_batch, | 
|  | Put(Pointee(MatchesHeader(kSessionTag, {}, {})))); | 
|  | EXPECT_CALL(*tab_create_mock_batch, Commit()); | 
|  |  | 
|  | auto navigation_mock_batch = std::make_unique<StrictMock<MockWriteBatch>>(); | 
|  | EXPECT_CALL( | 
|  | *navigation_mock_batch, | 
|  | Put(Pointee(MatchesHeader(kSessionTag, {kWindowId1}, {kTabId1})))); | 
|  | EXPECT_CALL(*navigation_mock_batch, | 
|  | Put(Pointee(MatchesTab(kSessionTag, kWindowId1, kTabId1, | 
|  | /*tab_node_id=*/0, /*urls=*/{kFoo1})))); | 
|  | EXPECT_CALL(*navigation_mock_batch, Commit()); | 
|  |  | 
|  | EXPECT_CALL(mock_delegate_, CreateLocalSessionWriteBatch()) | 
|  | .WillOnce(Return(ByMove(std::move(tab_create_mock_batch)))) | 
|  | .WillOnce(Return(ByMove(std::move(navigation_mock_batch)))); | 
|  |  | 
|  | AddWindow(kWindowId1, sync_pb::SessionWindow_BrowserType_TYPE_CUSTOM_TAB); | 
|  | AddTab(kWindowId1, kFoo1, kTabId1); | 
|  | } | 
|  |  | 
|  | TEST_F(LocalSessionEventHandlerImplTest, PropagateNewWindow) { | 
|  | AddWindow(kWindowId1); | 
|  | AddTab(kWindowId1, kFoo1, kTabId1); | 
|  | AddTab(kWindowId1, kBar1, kTabId2); | 
|  |  | 
|  | InitHandler(); | 
|  |  | 
|  | // Window creation triggers an update event due to the tab parented | 
|  | // notification, so the event handler issues two commits as well (one for | 
|  | // window creation, one for tab update). During the first update, however, | 
|  | // the window is not syncable and is hence skipped. | 
|  | auto tab_create_mock_batch = std::make_unique<StrictMock<MockWriteBatch>>(); | 
|  | EXPECT_CALL(*tab_create_mock_batch, | 
|  | Put(Pointee(MatchesHeader(kSessionTag, {kWindowId1}, | 
|  | {kTabId1, kTabId2})))); | 
|  | EXPECT_CALL(*tab_create_mock_batch, Commit()); | 
|  |  | 
|  | auto navigation_mock_batch = std::make_unique<StrictMock<MockWriteBatch>>(); | 
|  | EXPECT_CALL(*navigation_mock_batch, | 
|  | Put(Pointee(MatchesHeader(kSessionTag, {kWindowId1, kWindowId2}, | 
|  | {kTabId1, kTabId2, kTabId3})))); | 
|  | EXPECT_CALL(*navigation_mock_batch, | 
|  | Put(Pointee(MatchesTab(kSessionTag, kWindowId2, kTabId3, | 
|  | /*tab_node_id=*/_, /*urls=*/{kBaz1})))); | 
|  | EXPECT_CALL(*navigation_mock_batch, Commit()); | 
|  |  | 
|  | EXPECT_CALL(mock_delegate_, CreateLocalSessionWriteBatch()) | 
|  | .WillOnce(Return(ByMove(std::move(tab_create_mock_batch)))) | 
|  | .WillOnce(Return(ByMove(std::move(navigation_mock_batch)))); | 
|  |  | 
|  | AddWindow(kWindowId2); | 
|  | AddTab(kWindowId2, kBaz1, kTabId3); | 
|  | } | 
|  |  | 
|  | TEST_F(LocalSessionEventHandlerImplTest, | 
|  | PropagateNewNavigationWithoutTabbedWindows) { | 
|  | const int kTabNodeId1 = 0; | 
|  | const int kTabNodeId2 = 1; | 
|  |  | 
|  | // The tracker is initially restored from persisted state, containing two | 
|  | // custom tabs. | 
|  | sync_pb::SessionSpecifics custom_tab1; | 
|  | custom_tab1.set_session_tag(kSessionTag); | 
|  | custom_tab1.set_tab_node_id(kTabNodeId1); | 
|  | custom_tab1.mutable_tab()->set_window_id(kWindowId1); | 
|  | custom_tab1.mutable_tab()->set_tab_id(kTabId1); | 
|  | session_tracker_.ReassociateLocalTab(kTabNodeId1, | 
|  | SessionID::FromSerializedValue(kTabId1)); | 
|  | UpdateTrackerWithSpecifics(custom_tab1, base::Time::Now(), &session_tracker_); | 
|  |  | 
|  | sync_pb::SessionSpecifics custom_tab2; | 
|  | custom_tab2.set_session_tag(kSessionTag); | 
|  | custom_tab2.set_tab_node_id(kTabNodeId2); | 
|  | custom_tab2.mutable_tab()->set_window_id(kWindowId2); | 
|  | custom_tab2.mutable_tab()->set_tab_id(kTabId2); | 
|  | session_tracker_.ReassociateLocalTab(kTabNodeId2, | 
|  | SessionID::FromSerializedValue(kTabId2)); | 
|  | UpdateTrackerWithSpecifics(custom_tab2, base::Time::Now(), &session_tracker_); | 
|  |  | 
|  | sync_pb::SessionSpecifics header; | 
|  | header.set_session_tag(kSessionTag); | 
|  | header.mutable_header()->add_window()->set_window_id(kWindowId1); | 
|  | header.mutable_header()->mutable_window(0)->add_tab(kTabId1); | 
|  | header.mutable_header()->add_window()->set_window_id(kWindowId2); | 
|  | header.mutable_header()->mutable_window(1)->add_tab(kTabId2); | 
|  | UpdateTrackerWithSpecifics(header, base::Time::Now(), &session_tracker_); | 
|  |  | 
|  | ASSERT_THAT(session_tracker_.LookupSession(kSessionTag), | 
|  | MatchesSyncedSession(kSessionTag, | 
|  | {{kWindowId1, std::vector<int>{kTabId1}}, | 
|  | {kWindowId2, std::vector<int>{kTabId2}}})); | 
|  |  | 
|  | AddWindow(kWindowId1, sync_pb::SessionWindow_BrowserType_TYPE_CUSTOM_TAB); | 
|  | TestSyncedTabDelegate* tab1 = AddTab(kWindowId1, kFoo1, kTabId1); | 
|  |  | 
|  | AddWindow(kWindowId2, sync_pb::SessionWindow_BrowserType_TYPE_CUSTOM_TAB); | 
|  | AddTab(kWindowId2, kBar1, kTabId2); | 
|  |  | 
|  | InitHandler(); | 
|  |  | 
|  | auto update_mock_batch = std::make_unique<StrictMock<MockWriteBatch>>(); | 
|  | // Note that the header is reported again, although it hasn't changed. This is | 
|  | // OK because sync will avoid updating an entity with identical content. | 
|  | EXPECT_CALL(*update_mock_batch, | 
|  | Put(Pointee(MatchesHeader(kSessionTag, {kWindowId1, kWindowId2}, | 
|  | {kTabId1, kTabId2})))); | 
|  | EXPECT_CALL( | 
|  | *update_mock_batch, | 
|  | Put(Pointee(MatchesTab(kSessionTag, kWindowId1, kTabId1, kTabNodeId1, | 
|  | /*urls=*/{kFoo1, kBaz1})))); | 
|  | EXPECT_CALL(*update_mock_batch, Commit()); | 
|  |  | 
|  | EXPECT_CALL(mock_delegate_, CreateLocalSessionWriteBatch()) | 
|  | .WillOnce(Return(ByMove(std::move(update_mock_batch)))); | 
|  |  | 
|  | tab1->Navigate(kBaz1); | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  | }  // namespace sync_sessions |