// Copyright 2018 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "components/sync_sessions/session_store.h" #include #include #include #include "base/bind.h" #include "base/callback_helpers.h" #include "base/cancelable_callback.h" #include "base/run_loop.h" #include "base/test/mock_callback.h" #include "base/test/task_environment.h" #include "components/prefs/testing_pref_service.h" #include "components/sync/base/hash_util.h" #include "components/sync/protocol/entity_data.h" #include "components/sync/protocol/entity_metadata.pb.h" #include "components/sync/protocol/session_specifics.pb.h" #include "components/sync/test/model_type_store_test_util.h" #include "components/sync/test/test_matchers.h" #include "components/sync_device_info/local_device_info_util.h" #include "components/sync_sessions/mock_sync_sessions_client.h" #include "components/sync_sessions/session_sync_prefs.h" #include "components/sync_sessions/test_matchers.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" namespace sync_sessions { namespace { using sync_pb::SessionSpecifics; using syncer::DataBatch; using syncer::EntityData; using syncer::EntityMetadataMap; using syncer::HasEncryptionKeyName; using syncer::IsEmptyMetadataBatch; using syncer::MetadataBatch; using syncer::MetadataBatchContains; using syncer::ModelTypeStore; using syncer::NoModelError; using testing::_; using testing::ElementsAre; using testing::Eq; using testing::IsEmpty; using testing::Matcher; using testing::NiceMock; using testing::NotNull; using testing::Pair; using testing::Return; using testing::UnorderedElementsAre; const char kLocalCacheGuid[] = "SomeCacheGuid"; // A mock callback that a) can be used as mock to verify call expectations and // b) conveniently exposes the last instantiated session store. class MockOpenCallback { public: MOCK_METHOD(void, Run, (const absl::optional& error, SessionStore* store, MetadataBatch* metadata_batch), ()); SessionStore::OpenCallback Get() { return base::BindOnce( [](MockOpenCallback* callback, const absl::optional& error, std::unique_ptr store, std::unique_ptr metadata_batch) { // Store a copy of the pointer for GetResult(). callback->store_ = std::move(store); // Call mock method. callback->Run(error, callback->store_.get(), metadata_batch.get()); callback->loop_.Quit(); }, base::Unretained(this)); } // Waits until the callback gets triggered. void Wait() { loop_.Run(); } SessionStore* GetResult() { return store_.get(); } std::unique_ptr StealResult() { return std::move(store_); } private: base::RunLoop loop_; std::unique_ptr store_; }; MATCHER_P(EntityDataHasSpecifics, session_specifics_matcher, "") { return session_specifics_matcher.MatchAndExplain(arg.specifics.session(), result_listener); } std::map BatchToEntityDataMap( std::unique_ptr batch) { std::map storage_key_to_data; while (batch && batch->HasNext()) { auto [storage_key, entity_data] = batch->Next(); EXPECT_THAT(entity_data, NotNull()); if (entity_data) { storage_key_to_data.emplace(storage_key, std::move(*entity_data)); } } return storage_key_to_data; } std::unique_ptr ReadAllPersistedMetadataFrom( ModelTypeStore* store) { std::unique_ptr batch; base::RunLoop loop; store->ReadAllMetadata(base::BindOnce( [](std::unique_ptr* output_batch, base::RunLoop* loop, const absl::optional& error, std::unique_ptr input_batch) { EXPECT_FALSE(error) << error->ToString(); EXPECT_THAT(input_batch, NotNull()); *output_batch = std::move(input_batch); loop->Quit(); }, &batch, &loop)); loop.Run(); return batch; } std::map ReadAllPersistedDataFrom( ModelTypeStore* store) { std::unique_ptr records; base::RunLoop loop; store->ReadAllData(base::BindOnce( [](std::unique_ptr* output_records, base::RunLoop* loop, const absl::optional& error, std::unique_ptr input_records) { EXPECT_FALSE(error) << error->ToString(); EXPECT_THAT(input_records, NotNull()); *output_records = std::move(input_records); loop->Quit(); }, &records, &loop)); loop.Run(); std::map result; if (records) { for (const ModelTypeStore::Record& record : *records) { SessionSpecifics specifics; EXPECT_TRUE(specifics.ParseFromString(record.value)); result.emplace(record.id, specifics); } } return result; } class SessionStoreOpenTest : public ::testing::Test { protected: SessionStoreOpenTest() : session_sync_prefs_(&pref_service_), underlying_store_( syncer::ModelTypeStoreTestUtil::CreateInMemoryStoreForTest( syncer::SESSIONS)) { SessionSyncPrefs::RegisterProfilePrefs(pref_service_.registry()); mock_sync_sessions_client_ = std::make_unique>(); ON_CALL(*mock_sync_sessions_client_, GetSessionSyncPrefs()) .WillByDefault(Return(&session_sync_prefs_)); ON_CALL(*mock_sync_sessions_client_, GetStoreFactory()) .WillByDefault( Return(syncer::ModelTypeStoreTestUtil::FactoryForForwardingStore( underlying_store_.get()))); } ~SessionStoreOpenTest() override = default; base::test::SingleThreadTaskEnvironment task_environment_; TestingPrefServiceSimple pref_service_; SessionSyncPrefs session_sync_prefs_; std::unique_ptr mock_sync_sessions_client_; std::unique_ptr underlying_store_; }; TEST_F(SessionStoreOpenTest, ShouldCreateStore) { ASSERT_THAT(session_sync_prefs_.GetLegacySyncSessionsGUID(), IsEmpty()); MockOpenCallback completion; EXPECT_CALL(completion, Run(NoModelError(), /*store=*/NotNull(), MetadataBatchContains(_, IsEmpty()))); SessionStore::Open(kLocalCacheGuid, mock_sync_sessions_client_.get(), completion.Get()); completion.Wait(); ASSERT_THAT(completion.GetResult(), NotNull()); EXPECT_THAT(completion.GetResult()->local_session_info().client_name, Eq(syncer::GetPersonalizableDeviceNameBlocking())); EXPECT_THAT(completion.GetResult()->local_session_info().session_tag, Eq(kLocalCacheGuid)); EXPECT_THAT(session_sync_prefs_.GetLegacySyncSessionsGUID(), IsEmpty()); } TEST_F(SessionStoreOpenTest, ShouldReadLegacySessionsGuidFromPrefs) { const std::string kLegacySyncSessionsGUID = "sessiontag1"; session_sync_prefs_.SetLegacySyncSessionsGUIDForTesting( kLegacySyncSessionsGUID); NiceMock completion; SessionStore::Open(kLocalCacheGuid, mock_sync_sessions_client_.get(), completion.Get()); completion.Wait(); ASSERT_THAT(completion.GetResult(), NotNull()); EXPECT_THAT(completion.GetResult()->local_session_info().session_tag, Eq(kLegacySyncSessionsGUID)); EXPECT_THAT(session_sync_prefs_.GetLegacySyncSessionsGUID(), Eq(kLegacySyncSessionsGUID)); } TEST_F(SessionStoreOpenTest, ShouldNotUseClientIfCancelled) { // Mimics a caller that uses a weak pointer. class Caller { public: explicit Caller(SessionStore::OpenCallback cb) : cb_(std::move(cb)) {} SessionStore::OpenCallback GetCancelableCallback() { return base::BindOnce(&Caller::Completed, weak_ptr_factory_.GetWeakPtr()); } private: void Completed(const absl::optional& error, std::unique_ptr store, std::unique_ptr metadata_batch) { std::move(cb_).Run(error, std::move(store), std::move(metadata_batch)); } SessionStore::OpenCallback cb_; base::WeakPtrFactory weak_ptr_factory_{this}; }; NiceMock mock_completion; auto caller = std::make_unique(mock_completion.Get()); EXPECT_CALL(mock_completion, Run).Times(0); SessionStore::Open(kLocalCacheGuid, mock_sync_sessions_client_.get(), caller->GetCancelableCallback()); // The client gets destroyed before callback completion. mock_sync_sessions_client_.reset(); caller.reset(); // Run until idle to test for crashes due to use-after-free. base::RunLoop().RunUntilIdle(); } // Test fixture that creates an initial session store. class SessionStoreTest : public SessionStoreOpenTest { protected: SessionStoreTest() { session_store_ = CreateSessionStore(); } std::unique_ptr CreateSessionStore() { NiceMock completion; SessionStore::Open(kLocalCacheGuid, mock_sync_sessions_client_.get(), completion.Get()); completion.Wait(); EXPECT_THAT(completion.GetResult(), NotNull()); return completion.StealResult(); } SessionStore* session_store() { return session_store_.get(); } private: std::unique_ptr session_store_; }; TEST_F(SessionStoreTest, ShouldClearLegacySessionsGuidFromPrefs) { const std::string kLegacySyncSessionsGUID = "sessiontag1"; session_sync_prefs_.SetLegacySyncSessionsGUIDForTesting( kLegacySyncSessionsGUID); ASSERT_THAT(session_sync_prefs_.GetLegacySyncSessionsGUID(), Eq(kLegacySyncSessionsGUID)); session_store()->DeleteAllDataAndMetadata(); EXPECT_THAT(session_sync_prefs_.GetLegacySyncSessionsGUID(), IsEmpty()); } TEST_F(SessionStoreTest, ShouldCreateLocalSession) { const std::string header_storage_key = SessionStore::GetHeaderStorageKey(kLocalCacheGuid); EXPECT_THAT(BatchToEntityDataMap(session_store()->GetAllSessionData()), ElementsAre(Pair(header_storage_key, EntityDataHasSpecifics(MatchesHeader( kLocalCacheGuid, /*window_ids=*/{}, /*tab_ids=*/{}))))); // Verify that GetSessionDataForKeys() returns the header entity. EXPECT_THAT(BatchToEntityDataMap( session_store()->GetSessionDataForKeys({header_storage_key})), ElementsAre(Pair(header_storage_key, EntityDataHasSpecifics(MatchesHeader( kLocalCacheGuid, /*window_ids=*/{}, /*tab_ids=*/{}))))); // Verify the underlying storage does NOT contain the data. EXPECT_THAT(ReadAllPersistedDataFrom(underlying_store_.get()), IsEmpty()); // Verify the underlying storage does NOT contain metadata. EXPECT_THAT(ReadAllPersistedMetadataFrom(underlying_store_.get()), IsEmptyMetadataBatch()); } TEST_F(SessionStoreTest, ShouldWriteAndRestoreMetadata) { const std::string kStorageKey1 = "TestStorageKey1"; const std::string kServerId1 = "TestServerId1"; const std::string kEncryptionKeyName1 = "TestEncryptionKey1"; // Populate with metadata. std::unique_ptr batch = session_store()->CreateWriteBatch(/*error_handler=*/base::DoNothing()); ASSERT_THAT(batch, NotNull()); sync_pb::EntityMetadata metadata1; metadata1.set_server_id(kServerId1); batch->GetMetadataChangeList()->UpdateMetadata(kStorageKey1, metadata1); sync_pb::ModelTypeState model_type_state; model_type_state.set_encryption_key_name(kEncryptionKeyName1); batch->GetMetadataChangeList()->UpdateModelTypeState(model_type_state); SessionStore::WriteBatch::Commit(std::move(batch)); // Verify the underlying storage contains the metadata. EXPECT_THAT(ReadAllPersistedMetadataFrom(underlying_store_.get()), MetadataBatchContains(HasEncryptionKeyName(kEncryptionKeyName1), ElementsAre(Pair(kStorageKey1, _)))); // Create second session store. NiceMock completion; EXPECT_CALL(completion, Run(NoModelError(), /*store=*/NotNull(), MetadataBatchContains( HasEncryptionKeyName(kEncryptionKeyName1), ElementsAre(Pair(kStorageKey1, _))))); SessionStore::Open(kLocalCacheGuid, mock_sync_sessions_client_.get(), completion.Get()); completion.Wait(); EXPECT_THAT(completion.GetResult(), NotNull()); EXPECT_NE(session_store(), completion.GetResult()); } TEST_F(SessionStoreTest, ShouldUpdateTrackerWithForeignData) { const std::string kForeignSessionTag = "SomeForeignTag"; const int kWindowId = 5; const int kTabId1 = 7; const int kTabId2 = 8; const int kTabNodeId1 = 2; const int kTabNodeId2 = 3; ASSERT_THAT(session_store()->tracker()->LookupAllForeignSessions( SyncedSessionTracker::RAW), IsEmpty()); const std::string header_storage_key = SessionStore::GetHeaderStorageKey(kForeignSessionTag); const std::string tab_storage_key1 = SessionStore::GetTabStorageKey(kForeignSessionTag, kTabNodeId1); const std::string tab_storage_key2 = SessionStore::GetTabStorageKey(kForeignSessionTag, kTabNodeId2); ASSERT_THAT(BatchToEntityDataMap(session_store()->GetSessionDataForKeys( {header_storage_key, tab_storage_key1, tab_storage_key2})), IsEmpty()); // Populate with data. SessionSpecifics header; header.set_session_tag(kForeignSessionTag); header.mutable_header()->add_window()->set_window_id(kWindowId); header.mutable_header()->mutable_window(0)->add_tab(kTabId1); header.mutable_header()->mutable_window(0)->add_tab(kTabId2); ASSERT_TRUE(SessionStore::AreValidSpecifics(header)); SessionSpecifics tab1; tab1.set_session_tag(kForeignSessionTag); tab1.set_tab_node_id(kTabNodeId1); tab1.mutable_tab()->set_window_id(kWindowId); tab1.mutable_tab()->set_tab_id(kTabId1); ASSERT_TRUE(SessionStore::AreValidSpecifics(tab1)); SessionSpecifics tab2; tab2.set_session_tag(kForeignSessionTag); tab2.set_tab_node_id(kTabNodeId2); tab2.mutable_tab()->set_window_id(kWindowId); tab2.mutable_tab()->set_tab_id(kTabId2); ASSERT_TRUE(SessionStore::AreValidSpecifics(tab2)); std::unique_ptr batch = session_store()->CreateWriteBatch(/*error_handler=*/base::DoNothing()); ASSERT_THAT(batch, NotNull()); batch->PutAndUpdateTracker(header, base::Time::Now()); batch->PutAndUpdateTracker(tab1, base::Time::Now()); batch->PutAndUpdateTracker(tab2, base::Time::Now()); SessionStore::WriteBatch::Commit(std::move(batch)); EXPECT_THAT(session_store()->tracker()->LookupAllForeignSessions( SyncedSessionTracker::RAW), ElementsAre(MatchesSyncedSession( kForeignSessionTag, {{kWindowId, {kTabId1, kTabId2}}}))); EXPECT_THAT( BatchToEntityDataMap(session_store()->GetSessionDataForKeys( {header_storage_key, tab_storage_key1, tab_storage_key2})), UnorderedElementsAre( Pair(header_storage_key, EntityDataHasSpecifics(MatchesHeader( kForeignSessionTag, {kWindowId}, {kTabId1, kTabId2}))), Pair(tab_storage_key1, EntityDataHasSpecifics(MatchesTab(kForeignSessionTag, kWindowId, kTabId1, kTabNodeId1, /*urls=*/_))), Pair(tab_storage_key2, EntityDataHasSpecifics(MatchesTab(kForeignSessionTag, kWindowId, kTabId2, kTabNodeId2, /*urls=*/_))))); } TEST_F(SessionStoreTest, ShouldWriteAndRestoreForeignData) { const std::string kForeignSessionTag = "SomeForeignTag"; const int kWindowId = 5; const int kTabId1 = 7; const int kTabNodeId1 = 2; const std::string local_header_storage_key = SessionStore::GetHeaderStorageKey(kLocalCacheGuid); ASSERT_THAT(session_store()->tracker()->LookupAllForeignSessions( SyncedSessionTracker::RAW), IsEmpty()); // Local session is automatically created. ASSERT_THAT(BatchToEntityDataMap(session_store()->GetAllSessionData()), ElementsAre(Pair(local_header_storage_key, _))); ASSERT_THAT(ReadAllPersistedDataFrom(underlying_store_.get()), IsEmpty()); // Populate with data. SessionSpecifics header; header.set_session_tag(kForeignSessionTag); header.mutable_header()->add_window()->set_window_id(kWindowId); header.mutable_header()->mutable_window(0)->add_tab(kTabId1); ASSERT_TRUE(SessionStore::AreValidSpecifics(header)); SessionSpecifics tab1; tab1.set_session_tag(kForeignSessionTag); tab1.set_tab_node_id(kTabNodeId1); tab1.mutable_tab()->set_window_id(kWindowId); tab1.mutable_tab()->set_tab_id(kTabId1); ASSERT_TRUE(SessionStore::AreValidSpecifics(tab1)); std::unique_ptr batch = session_store()->CreateWriteBatch(/*error_handler=*/base::DoNothing()); ASSERT_THAT(batch, NotNull()); batch->PutAndUpdateTracker(header, base::Time::Now()); batch->PutAndUpdateTracker(tab1, base::Time::Now()); const std::string header_storage_key = SessionStore::GetHeaderStorageKey(kForeignSessionTag); const std::string tab_storage_key1 = SessionStore::GetTabStorageKey(kForeignSessionTag, kTabNodeId1); sync_pb::EntityMetadata header_metadata; header_metadata.set_server_id("someserverid1"); batch->GetMetadataChangeList()->UpdateMetadata(header_storage_key, header_metadata); sync_pb::EntityMetadata tab1_metadata; tab1_metadata.set_server_id("someserverid2"); batch->GetMetadataChangeList()->UpdateMetadata(tab_storage_key1, tab1_metadata); SessionStore::WriteBatch::Commit(std::move(batch)); // Verify the underlying storage contains the data. ASSERT_THAT( ReadAllPersistedDataFrom(underlying_store_.get()), UnorderedElementsAre( Pair(header_storage_key, MatchesHeader(kForeignSessionTag, {kWindowId}, {kTabId1})), Pair(tab_storage_key1, MatchesTab(kForeignSessionTag, kWindowId, kTabId1, kTabNodeId1, /*urls=*/_)))); // Verify tracker exposes the foreign tabs. ASSERT_THAT( session_store()->tracker()->LookupAllForeignSessions( SyncedSessionTracker::RAW), ElementsAre(MatchesSyncedSession( kForeignSessionTag, {{kWindowId, std::vector{kTabId1}}}))); // Create second session store to verify that the persisted state is restored, // by mimicing a Chrome restart and using |underlying_store_| (in-memory) as a // replacement for on-disk persistence. std::unique_ptr restored_store = CreateSessionStore(); ASSERT_THAT(restored_store, NotNull()); ASSERT_NE(session_store(), restored_store.get()); // Verify tracker was restored. EXPECT_THAT( restored_store->tracker()->LookupAllForeignSessions( SyncedSessionTracker::RAW), ElementsAre(MatchesSyncedSession( kForeignSessionTag, {{kWindowId, std::vector{kTabId1}}}))); EXPECT_THAT(BatchToEntityDataMap(restored_store->GetAllSessionData()), UnorderedElementsAre( Pair(local_header_storage_key, _), Pair(header_storage_key, EntityDataHasSpecifics(MatchesHeader( kForeignSessionTag, {kWindowId}, {kTabId1}))), Pair(tab_storage_key1, EntityDataHasSpecifics(MatchesTab( kForeignSessionTag, kWindowId, kTabId1, kTabNodeId1, /*urls=*/_))))); EXPECT_THAT(BatchToEntityDataMap(session_store()->GetSessionDataForKeys( {header_storage_key, tab_storage_key1})), UnorderedElementsAre( Pair(header_storage_key, EntityDataHasSpecifics(MatchesHeader( kForeignSessionTag, {kWindowId}, {kTabId1}))), Pair(tab_storage_key1, EntityDataHasSpecifics(MatchesTab( kForeignSessionTag, kWindowId, kTabId1, kTabNodeId1, /*urls=*/_))))); } TEST_F(SessionStoreTest, ShouldDeleteForeignData) { const std::string kForeignSessionTag = "SomeForeignTag"; const int kWindowId = 5; const int kTabId1 = 7; const int kTabId2 = 8; const int kTabNodeId1 = 1; const int kTabNodeId2 = 2; const std::string local_header_storage_key = SessionStore::GetHeaderStorageKey(kLocalCacheGuid); // Local session is automatically created. ASSERT_THAT(BatchToEntityDataMap(session_store()->GetAllSessionData()), ElementsAre(Pair(local_header_storage_key, _))); ASSERT_THAT(ReadAllPersistedDataFrom(underlying_store_.get()), IsEmpty()); // Populate with foreign data: one header entity and two tabs. SessionSpecifics header; header.set_session_tag(kForeignSessionTag); header.mutable_header()->add_window()->set_window_id(kWindowId); header.mutable_header()->mutable_window(0)->add_tab(kTabId1); header.mutable_header()->mutable_window(0)->add_tab(kTabId2); ASSERT_TRUE(SessionStore::AreValidSpecifics(header)); SessionSpecifics tab1; tab1.set_session_tag(kForeignSessionTag); tab1.set_tab_node_id(kTabNodeId1); tab1.mutable_tab()->set_window_id(kWindowId); tab1.mutable_tab()->set_tab_id(kTabId1); ASSERT_TRUE(SessionStore::AreValidSpecifics(tab1)); SessionSpecifics tab2; tab2.set_session_tag(kForeignSessionTag); tab2.set_tab_node_id(kTabNodeId2); tab2.mutable_tab()->set_window_id(kWindowId); tab2.mutable_tab()->set_tab_id(kTabId2); ASSERT_TRUE(SessionStore::AreValidSpecifics(tab2)); const std::string header_storage_key = SessionStore::GetHeaderStorageKey(kForeignSessionTag); const std::string tab_storage_key1 = SessionStore::GetTabStorageKey(kForeignSessionTag, kTabNodeId1); const std::string tab_storage_key2 = SessionStore::GetTabStorageKey(kForeignSessionTag, kTabNodeId2); // Write data and update the tracker. { std::unique_ptr batch = session_store()->CreateWriteBatch(/*error_handler=*/base::DoNothing()); ASSERT_THAT(batch, NotNull()); batch->PutAndUpdateTracker(header, base::Time::Now()); batch->PutAndUpdateTracker(tab1, base::Time::Now()); batch->PutAndUpdateTracker(tab2, base::Time::Now()); sync_pb::EntityMetadata header_metadata; header_metadata.set_server_id("someserverid1"); batch->GetMetadataChangeList()->UpdateMetadata(header_storage_key, header_metadata); sync_pb::EntityMetadata tab1_metadata; tab1_metadata.set_server_id("someserverid2"); batch->GetMetadataChangeList()->UpdateMetadata(tab_storage_key1, tab1_metadata); sync_pb::EntityMetadata tab2_metadata; tab2_metadata.set_server_id("someserverid3"); batch->GetMetadataChangeList()->UpdateMetadata(tab_storage_key2, tab2_metadata); SessionStore::WriteBatch::Commit(std::move(batch)); } // Verify the underlying storage contains the data. ASSERT_THAT( ReadAllPersistedDataFrom(underlying_store_.get()), UnorderedElementsAre( Pair(header_storage_key, MatchesHeader(kForeignSessionTag, {kWindowId}, {kTabId1, kTabId2})), Pair(tab_storage_key1, MatchesTab(kForeignSessionTag, kWindowId, kTabId1, kTabNodeId1, /*urls=*/_)), Pair(tab_storage_key2, MatchesTab(kForeignSessionTag, kWindowId, kTabId2, kTabNodeId2, /*urls=*/_)))); // Verify tracker exposes the foreign tabs. ASSERT_THAT(session_store()->tracker()->LookupAllForeignSessions( SyncedSessionTracker::RAW), ElementsAre(MatchesSyncedSession( kForeignSessionTag, {{kWindowId, std::vector{kTabId1, kTabId2}}}))); // Mimic receiving a tab deletion for |tab1|, which should only affect that // entity. { std::unique_ptr batch = session_store()->CreateWriteBatch(/*error_handler=*/base::DoNothing()); EXPECT_THAT(batch->DeleteForeignEntityAndUpdateTracker(tab_storage_key1), ElementsAre(tab_storage_key1)); SessionStore::WriteBatch::Commit(std::move(batch)); } EXPECT_THAT( ReadAllPersistedDataFrom(underlying_store_.get()), UnorderedElementsAre( Pair(header_storage_key, MatchesHeader(kForeignSessionTag, {kWindowId}, {kTabId1, kTabId2})), Pair(tab_storage_key2, MatchesTab(kForeignSessionTag, kWindowId, kTabId2, kTabNodeId2, /*urls=*/_)))); // Mimic receiving a header deletion (which should delete all remaining // entities for that session). { std::unique_ptr batch = session_store()->CreateWriteBatch(/*error_handler=*/base::DoNothing()); EXPECT_THAT(batch->DeleteForeignEntityAndUpdateTracker(header_storage_key), ElementsAre(header_storage_key, tab_storage_key2)); SessionStore::WriteBatch::Commit(std::move(batch)); } EXPECT_THAT(session_store()->tracker()->LookupAllForeignSessions( SyncedSessionTracker::RAW), IsEmpty()); EXPECT_THAT(ReadAllPersistedDataFrom(underlying_store_.get()), IsEmpty()); } TEST_F(SessionStoreTest, ShouldReturnForeignUnmappedTabs) { const std::string kForeignSessionTag = "SomeForeignTag"; const int kWindowId = 5; const int kTabId1 = 7; const int kTabNodeId1 = 2; const std::string local_header_storage_key = SessionStore::GetHeaderStorageKey(kLocalCacheGuid); const std::string foreign_header_storage_key = SessionStore::GetHeaderStorageKey(kForeignSessionTag); const std::string foreign_tab_storage_key = SessionStore::GetTabStorageKey(kForeignSessionTag, kTabNodeId1); // Local header entity is present initially. ASSERT_THAT(BatchToEntityDataMap(session_store()->GetAllSessionData()), ElementsAre(Pair(local_header_storage_key, _))); SessionSpecifics tab1; tab1.set_session_tag(kForeignSessionTag); tab1.set_tab_node_id(kTabNodeId1); tab1.mutable_tab()->set_window_id(kWindowId); tab1.mutable_tab()->set_tab_id(kTabId1); ASSERT_TRUE(SessionStore::AreValidSpecifics(tab1)); std::unique_ptr batch = session_store()->CreateWriteBatch(/*error_handler=*/base::DoNothing()); ASSERT_THAT(batch, NotNull()); batch->PutAndUpdateTracker(tab1, base::Time::Now()); SessionStore::WriteBatch::Commit(std::move(batch)); EXPECT_THAT(BatchToEntityDataMap(session_store()->GetAllSessionData()), UnorderedElementsAre( Pair(local_header_storage_key, _), Pair(foreign_header_storage_key, EntityDataHasSpecifics(MatchesHeader(kForeignSessionTag, /*window_ids=*/{}, /*tab_ids=*/{}))), Pair(foreign_tab_storage_key, EntityDataHasSpecifics(MatchesTab( kForeignSessionTag, kWindowId, kTabId1, kTabNodeId1, /*urls=*/_))))); } TEST_F(SessionStoreTest, ShouldIgnoreForeignOrphanTabs) { const std::string kForeignSessionTag = "SomeForeignTag"; const int kWindowId = 5; const int kTabId = 7; // Both tab nodes point to the same tab ID, so the second one should prevail. const int kTabNodeId1 = 2; const int kTabNodeId2 = 3; const std::string local_header_storage_key = SessionStore::GetHeaderStorageKey(kLocalCacheGuid); const std::string foreign_header_storage_key = SessionStore::GetHeaderStorageKey(kForeignSessionTag); const std::string foreign_tab_storage_key2 = SessionStore::GetTabStorageKey(kForeignSessionTag, kTabNodeId2); // Local header entity is present initially. ASSERT_THAT(BatchToEntityDataMap(session_store()->GetAllSessionData()), ElementsAre(Pair(local_header_storage_key, _))); SessionSpecifics tab1; tab1.set_session_tag(kForeignSessionTag); tab1.set_tab_node_id(kTabNodeId1); tab1.mutable_tab()->set_window_id(kWindowId); tab1.mutable_tab()->set_tab_id(kTabId); ASSERT_TRUE(SessionStore::AreValidSpecifics(tab1)); SessionSpecifics tab2; tab2.set_session_tag(kForeignSessionTag); tab2.set_tab_node_id(kTabNodeId2); tab2.mutable_tab()->set_window_id(kWindowId); tab2.mutable_tab()->set_tab_id(kTabId); ASSERT_TRUE(SessionStore::AreValidSpecifics(tab2)); // Store the two foreign tabs, in order. std::unique_ptr batch = session_store()->CreateWriteBatch(/*error_handler=*/base::DoNothing()); ASSERT_THAT(batch, NotNull()); batch->PutAndUpdateTracker(tab1, base::Time::Now()); batch->PutAndUpdateTracker(tab2, base::Time::Now()); SessionStore::WriteBatch::Commit(std::move(batch)); // The first foreign tab should have been overwritten by the second one, // because they shared a tab ID. EXPECT_THAT(BatchToEntityDataMap(session_store()->GetAllSessionData()), UnorderedElementsAre( Pair(local_header_storage_key, _), Pair(foreign_header_storage_key, EntityDataHasSpecifics(MatchesHeader(kForeignSessionTag, /*window_ids=*/{}, /*tab_ids=*/{}))), Pair(foreign_tab_storage_key2, EntityDataHasSpecifics(MatchesTab( kForeignSessionTag, kWindowId, kTabId, kTabNodeId2, /*urls=*/_))))); } } // namespace } // namespace sync_sessions