// Copyright 2019 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/send_tab_to_self/send_tab_to_self_bridge.h" #include #include #include #include "base/bind.h" #include "base/run_loop.h" #include "base/strings/stringprintf.h" #include "base/test/simple_test_clock.h" #include "base/test/task_environment.h" #include "components/history/core/browser/history_service.h" #include "components/send_tab_to_self/features.h" #include "components/send_tab_to_self/proto/send_tab_to_self.pb.h" #include "components/send_tab_to_self/target_device_info.h" #include "components/sync/model/entity_change.h" #include "components/sync/model/in_memory_metadata_change_list.h" #include "components/sync/model/metadata_batch.h" #include "components/sync/protocol/entity_data.h" #include "components/sync/protocol/model_type_state.pb.h" #include "components/sync/test/mock_model_type_change_processor.h" #include "components/sync/test/model_type_store_test_util.h" #include "components/sync/test/test_matchers.h" #include "components/sync_device_info/device_info.h" #include "components/sync_device_info/device_info_util.h" #include "components/sync_device_info/fake_device_info_tracker.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" namespace send_tab_to_self { namespace { using testing::_; using testing::AllOf; using testing::ElementsAre; using testing::IsEmpty; using testing::Return; using testing::SizeIs; using testing::UnorderedElementsAre; const char kGuidFormat[] = "guid %d"; const char kURLFormat[] = "https://www.url%d.com/"; const char kTitleFormat[] = "title %d"; const char kDeviceFormat[] = "device %d"; const char kLocalDeviceCacheGuid[] = "local_device_guid"; const char kLocalDeviceName[] = "local_device_name"; // Action SaveArgPointeeMove(pointer) saves the value pointed to by the k-th // (0-based) argument of the mock function by moving it to *pointer. ACTION_TEMPLATE(SaveArgPointeeMove, HAS_1_TEMPLATE_PARAMS(int, k), AND_1_VALUE_PARAMS(pointer)) { *pointer = std::move(*testing::get(args)); } sync_pb::SendTabToSelfSpecifics CreateSpecifics( int suffix, base::Time shared_time = base::Time::Now()) { sync_pb::SendTabToSelfSpecifics specifics; specifics.set_guid(base::StringPrintf(kGuidFormat, suffix)); specifics.set_url(base::StringPrintf(kURLFormat, suffix)); specifics.set_device_name(base::StringPrintf(kDeviceFormat, suffix)); specifics.set_title(base::StringPrintf(kTitleFormat, suffix)); specifics.set_target_device_sync_cache_guid(kLocalDeviceCacheGuid); specifics.set_shared_time_usec( shared_time.ToDeltaSinceWindowsEpoch().InMicroseconds()); return specifics; } std::unique_ptr CreateDevice( const std::string& guid, const std::string& name, base::Time last_updated_timestamp, bool send_tab_to_self_receiving_enabled = true) { return std::make_unique( guid, name, "chrome_version", "user_agent", sync_pb::SyncEnums_DeviceType_TYPE_LINUX, "scoped_id", "manufacturer", "model", "full_hardware_class", last_updated_timestamp, syncer::DeviceInfoUtil::GetPulseInterval(), send_tab_to_self_receiving_enabled, /*sharing_info=*/absl::nullopt, /*paask_info=*/absl::nullopt, /*fcm_registration_token=*/std::string(), /*interested_data_types=*/syncer::ModelTypeSet()); } sync_pb::ModelTypeState StateWithEncryption( const std::string& encryption_key_name) { sync_pb::ModelTypeState state; state.set_encryption_key_name(encryption_key_name); return state; } class MockSendTabToSelfModelObserver : public SendTabToSelfModelObserver { public: MOCK_METHOD0(SendTabToSelfModelLoaded, void()); MOCK_METHOD1(EntriesAddedRemotely, void(const std::vector&)); MOCK_METHOD1(EntriesOpenedRemotely, void(const std::vector&)); MOCK_METHOD1(EntriesRemovedRemotely, void(const std::vector&)); }; MATCHER_P(GuidIs, e, "") { return testing::ExplainMatchResult(e, arg->GetGUID(), result_listener); } class SendTabToSelfBridgeTest : public testing::Test { public: SendTabToSelfBridgeTest(const SendTabToSelfBridgeTest&) = delete; SendTabToSelfBridgeTest& operator=(const SendTabToSelfBridgeTest&) = delete; protected: SendTabToSelfBridgeTest() : store_(syncer::ModelTypeStoreTestUtil::CreateInMemoryStoreForTest()) {} void InitializeLocalDeviceIfNeeded() { if (local_device_) { return; } SetLocalDeviceCacheGuid(kLocalDeviceCacheGuid); local_device_ = CreateDevice(kLocalDeviceCacheGuid, "device", clock()->Now() - base::Days(1)); AddTestDevice(local_device_.get(), /*local=*/true); } // Initialized the bridge based on the current local device and store. Can // only be called once per run, as it passes |store_|. void InitializeBridge() { InitializeLocalDeviceIfNeeded(); InitializeBridgeWithoutDevice(); } // Initializes only the bridge without creating local device. This is useful // to test the case when the device info tracker is not initialized yet. void InitializeBridgeWithoutDevice() { ON_CALL(mock_processor_, IsTrackingMetadata()).WillByDefault(Return(true)); bridge_ = std::make_unique( mock_processor_.CreateForwardingProcessor(), &clock_, syncer::ModelTypeStoreTestUtil::MoveStoreToFactory(std::move(store_)), /*history_service=*/nullptr, &device_info_tracker_); bridge_->AddObserver(&mock_observer_); base::RunLoop().RunUntilIdle(); } void ShutdownBridge() { bridge_->RemoveObserver(&mock_observer_); store_ = SendTabToSelfBridge::DestroyAndStealStoreForTest(std::move(bridge_)); base::RunLoop().RunUntilIdle(); } base::Time AdvanceAndGetTime(base::TimeDelta delta = base::Milliseconds(10)) { clock_.Advance(delta); return clock_.Now(); } void DisableBridge() { ON_CALL(mock_processor_, IsTrackingMetadata()).WillByDefault(Return(false)); } syncer::EntityData MakeEntityData(const SendTabToSelfEntry& entry) { SendTabToSelfLocal specifics = entry.AsLocalProto(); syncer::EntityData entity_data; *entity_data.specifics.mutable_send_tab_to_self() = specifics.specifics(); entity_data.name = entry.GetURL().spec(); return entity_data; } // Helper method to reduce duplicated code between tests. Wraps the given // specifics objects in an EntityData and EntityChange of type ACTION_ADD, and // returns an EntityChangeList containing them all. Order is maintained. syncer::EntityChangeList EntityAddList( const std::vector& specifics_list) { syncer::EntityChangeList changes; for (const auto& specifics : specifics_list) { syncer::EntityData entity_data; *entity_data.specifics.mutable_send_tab_to_self() = specifics; entity_data.name = specifics.url(); changes.push_back(syncer::EntityChange::CreateAdd( specifics.guid(), std::move(entity_data))); } return changes; } // For Model Tests. void AddSampleEntries() { // Adds timer to avoid having two entries with the same shared timestamp. bridge_->AddEntry(GURL("http://a.com"), "a", kLocalDeviceCacheGuid); bridge_->AddEntry(GURL("http://b.com"), "b", kLocalDeviceCacheGuid); bridge_->AddEntry(GURL("http://c.com"), "c", kLocalDeviceCacheGuid); bridge_->AddEntry(GURL("http://d.com"), "d", kLocalDeviceCacheGuid); } void SetLocalDeviceCacheGuid(const std::string& cache_guid) { ON_CALL(mock_processor_, TrackedCacheGuid()) .WillByDefault(Return(cache_guid)); } void AddTestDevice(const syncer::DeviceInfo* device, bool local = false) { device_info_tracker_.Add(device); if (local) { device_info_tracker_.SetLocalCacheGuid(device->guid()); } } syncer::MockModelTypeChangeProcessor* processor() { return &mock_processor_; } SendTabToSelfBridge* bridge() { return bridge_.get(); } MockSendTabToSelfModelObserver* mock_observer() { return &mock_observer_; } base::SimpleTestClock* clock() { return &clock_; } syncer::FakeDeviceInfoTracker* device_info_tracker() { return &device_info_tracker_; } private: base::SimpleTestClock clock_; // In memory model type store needs to be able to post tasks. base::test::TaskEnvironment task_environment_; std::unique_ptr store_; testing::NiceMock mock_processor_; syncer::FakeDeviceInfoTracker device_info_tracker_; std::unique_ptr bridge_; testing::NiceMock mock_observer_; std::unique_ptr local_device_; }; TEST_F(SendTabToSelfBridgeTest, CheckEmpties) { InitializeBridge(); EXPECT_CALL(*mock_observer(), EntriesAddedRemotely(_)).Times(0); EXPECT_EQ(0ul, bridge()->GetAllGuids().size()); AddSampleEntries(); EXPECT_EQ(4ul, bridge()->GetAllGuids().size()); } TEST_F(SendTabToSelfBridgeTest, SyncAddOneEntry) { InitializeBridge(); syncer::EntityChangeList remote_input; SendTabToSelfEntry entry("guid1", GURL("http://www.example.com/"), "title", AdvanceAndGetTime(), "device", kLocalDeviceCacheGuid); remote_input.push_back( syncer::EntityChange::CreateAdd("guid1", MakeEntityData(entry))); auto metadata_change_list = std::make_unique(); EXPECT_CALL(*mock_observer(), EntriesAddedRemotely(SizeIs(1))); bridge()->MergeSyncData(std::move(metadata_change_list), std::move(remote_input)); EXPECT_EQ(1ul, bridge()->GetAllGuids().size()); } TEST_F(SendTabToSelfBridgeTest, ApplySyncChangesAddTwoSpecifics) { InitializeBridge(); const sync_pb::SendTabToSelfSpecifics specifics1 = CreateSpecifics(1); const sync_pb::SendTabToSelfSpecifics specifics2 = CreateSpecifics(2); sync_pb::ModelTypeState state = StateWithEncryption("ekn"); std::unique_ptr metadata_changes = bridge()->CreateMetadataChangeList(); metadata_changes->UpdateModelTypeState(state); EXPECT_CALL(*mock_observer(), EntriesAddedRemotely(SizeIs(2))); auto error = bridge()->ApplySyncChanges( std::move(metadata_changes), EntityAddList({specifics1, specifics2})); EXPECT_FALSE(error); } TEST_F(SendTabToSelfBridgeTest, ApplySyncChangesOneAdd) { InitializeBridge(); SendTabToSelfEntry entry("guid1", GURL("http://www.example.com/"), "title", AdvanceAndGetTime(), "device", kLocalDeviceCacheGuid); syncer::EntityChangeList add_changes; add_changes.push_back( syncer::EntityChange::CreateAdd("guid1", MakeEntityData(entry))); auto metadata_change_list = std::make_unique(); EXPECT_CALL(*mock_observer(), EntriesAddedRemotely(SizeIs(1))); bridge()->ApplySyncChanges(std::move(metadata_change_list), std::move(add_changes)); EXPECT_EQ(1ul, bridge()->GetAllGuids().size()); } // Tests that the send tab to self entry is correctly removed. TEST_F(SendTabToSelfBridgeTest, ApplySyncChangesOneDeletion) { InitializeBridge(); SendTabToSelfEntry entry("guid1", GURL("http://www.example.com/"), "title", AdvanceAndGetTime(), "device", kLocalDeviceCacheGuid); syncer::EntityChangeList add_changes; add_changes.push_back( syncer::EntityChange::CreateAdd("guid1", MakeEntityData(entry))); EXPECT_CALL(*mock_observer(), EntriesAddedRemotely(SizeIs(1))); bridge()->ApplySyncChanges(bridge()->CreateMetadataChangeList(), std::move(add_changes)); EXPECT_EQ(1ul, bridge()->GetAllGuids().size()); syncer::EntityChangeList delete_changes; delete_changes.push_back(syncer::EntityChange::CreateDelete("guid1")); EXPECT_CALL(*mock_observer(), EntriesRemovedRemotely(SizeIs(1))); bridge()->ApplySyncChanges(bridge()->CreateMetadataChangeList(), std::move(delete_changes)); EXPECT_EQ(0ul, bridge()->GetAllGuids().size()); } // Tests that the send tab to self entry is correctly removed. TEST_F(SendTabToSelfBridgeTest, LocalHistoryDeletion) { InitializeBridge(); SendTabToSelfEntry entry1("guid1", GURL("http://www.example.com/"), "title", AdvanceAndGetTime(), "device", kLocalDeviceCacheGuid); SendTabToSelfEntry entry2("guid2", GURL("http://www.example2.com/"), "title2", AdvanceAndGetTime(), "device2", kLocalDeviceCacheGuid); SendTabToSelfEntry entry3("guid3", GURL("http://www.example3.com/"), "title3", AdvanceAndGetTime(), "device3", kLocalDeviceCacheGuid); syncer::EntityChangeList add_changes; add_changes.push_back( syncer::EntityChange::CreateAdd("guid1", MakeEntityData(entry1))); add_changes.push_back( syncer::EntityChange::CreateAdd("guid2", MakeEntityData(entry2))); add_changes.push_back( syncer::EntityChange::CreateAdd("guid3", MakeEntityData(entry3))); bridge()->ApplySyncChanges(bridge()->CreateMetadataChangeList(), std::move(add_changes)); ASSERT_EQ(3ul, bridge()->GetAllGuids().size()); history::URLRows urls_to_remove; urls_to_remove.push_back(history::URLRow(GURL("http://www.example.com/"))); urls_to_remove.push_back(history::URLRow(GURL("http://www.example2.com/"))); EXPECT_CALL(*mock_observer(), EntriesRemovedRemotely(SizeIs(2))); EXPECT_CALL(*processor(), Delete("guid1", _)); EXPECT_CALL(*processor(), Delete("guid2", _)); bridge()->OnURLsDeleted(nullptr, history::DeletionInfo::ForUrls( urls_to_remove, std::set())); EXPECT_EQ(1ul, bridge()->GetAllGuids().size()); } TEST_F(SendTabToSelfBridgeTest, ApplySyncChangesEmpty) { InitializeBridge(); EXPECT_CALL(*mock_observer(), EntriesAddedRemotely(_)).Times(0); auto error = bridge()->ApplySyncChanges(bridge()->CreateMetadataChangeList(), syncer::EntityChangeList()); EXPECT_FALSE(error); } TEST_F(SendTabToSelfBridgeTest, AddEntryAndRestartBridge) { InitializeBridge(); const sync_pb::SendTabToSelfSpecifics specifics = CreateSpecifics(1); sync_pb::ModelTypeState state = StateWithEncryption("ekn"); std::unique_ptr metadata_changes = bridge()->CreateMetadataChangeList(); metadata_changes->UpdateModelTypeState(state); auto error = bridge()->ApplySyncChanges(std::move(metadata_changes), EntityAddList({specifics})); ASSERT_FALSE(error); ShutdownBridge(); EXPECT_CALL(*processor(), ModelReadyToSync(MetadataBatchContains( syncer::HasEncryptionKeyName(state.encryption_key_name()), /*entities=*/IsEmpty()))); EXPECT_CALL(*mock_observer(), EntriesAddedRemotely(_)).Times(0); InitializeBridge(); std::vector guids = bridge()->GetAllGuids(); ASSERT_EQ(1ul, guids.size()); EXPECT_EQ(specifics.url(), bridge()->GetEntryByGUID(guids[0])->GetURL().spec()); } TEST_F(SendTabToSelfBridgeTest, ApplySyncChangesInMemory) { InitializeBridge(); const sync_pb::SendTabToSelfSpecifics specifics = CreateSpecifics(1); std::unique_ptr metadata_changes = bridge()->CreateMetadataChangeList(); EXPECT_CALL(*mock_observer(), EntriesAddedRemotely(SizeIs(1))); auto error_on_add = bridge()->ApplySyncChanges( bridge()->CreateMetadataChangeList(), EntityAddList({specifics})); EXPECT_FALSE(error_on_add); EXPECT_EQ(1ul, bridge()->GetAllGuids().size()); EXPECT_CALL(*mock_observer(), EntriesRemovedRemotely(SizeIs(1))); syncer::EntityChangeList entity_change_list; entity_change_list.push_back( syncer::EntityChange::CreateDelete(specifics.guid())); auto error_on_delete = bridge()->ApplySyncChanges( bridge()->CreateMetadataChangeList(), std::move(entity_change_list)); EXPECT_FALSE(error_on_delete); EXPECT_EQ(0ul, bridge()->GetAllGuids().size()); } TEST_F(SendTabToSelfBridgeTest, ApplyDeleteNonexistent) { InitializeBridge(); EXPECT_CALL(*mock_observer(), EntriesAddedRemotely(_)).Times(0); std::unique_ptr metadata_changes = bridge()->CreateMetadataChangeList(); EXPECT_CALL(*processor(), Delete(_, _)).Times(0); syncer::EntityChangeList entity_change_list; entity_change_list.push_back(syncer::EntityChange::CreateDelete("guid")); auto error = bridge()->ApplySyncChanges(std::move(metadata_changes), std::move(entity_change_list)); EXPECT_FALSE(error); } TEST_F(SendTabToSelfBridgeTest, MarkEntryOpenedInformsServer) { InitializeBridge(); SendTabToSelfEntry entry("guid", GURL("http://g.com/"), "title", AdvanceAndGetTime(), "remote", "remote"); syncer::EntityChangeList remote_data; remote_data.push_back( syncer::EntityChange::CreateAdd("guid", MakeEntityData(entry))); bridge()->MergeSyncData(bridge()->CreateMetadataChangeList(), std::move(remote_data)); ASSERT_THAT(bridge()->GetAllGuids(), UnorderedElementsAre("guid")); syncer::EntityData uploaded_opened_entity; EXPECT_CALL(*processor(), Put("guid", _, _)) .WillOnce(SaveArgPointeeMove<1>(&uploaded_opened_entity)); bridge()->MarkEntryOpened("guid"); EXPECT_TRUE(uploaded_opened_entity.specifics.send_tab_to_self().opened()); } TEST_F(SendTabToSelfBridgeTest, DismissEntryInformsServer) { InitializeBridge(); SendTabToSelfEntry entry("guid", GURL("http://g.com/"), "title", AdvanceAndGetTime(), "remote", "remote"); syncer::EntityChangeList remote_data; remote_data.push_back( syncer::EntityChange::CreateAdd("guid", MakeEntityData(entry))); bridge()->MergeSyncData(bridge()->CreateMetadataChangeList(), std::move(remote_data)); ASSERT_THAT(bridge()->GetAllGuids(), UnorderedElementsAre("guid")); syncer::EntityData uploaded_dismissed_entity; EXPECT_CALL(*processor(), Put("guid", _, _)) .WillOnce(SaveArgPointeeMove<1>(&uploaded_dismissed_entity)); bridge()->DismissEntry("guid"); EXPECT_TRUE(uploaded_dismissed_entity.specifics.send_tab_to_self() .notification_dismissed()); } TEST_F(SendTabToSelfBridgeTest, PreserveDissmissalAfterRestartBridge) { InitializeBridge(); const sync_pb::SendTabToSelfSpecifics specifics = CreateSpecifics(1); std::unique_ptr metadata_changes = bridge()->CreateMetadataChangeList(); auto error = bridge()->ApplySyncChanges(std::move(metadata_changes), EntityAddList({specifics})); ASSERT_FALSE(error); EXPECT_CALL(*processor(), Put(_, _, _)); bridge()->DismissEntry(specifics.guid()); ShutdownBridge(); InitializeBridge(); std::vector guids = bridge()->GetAllGuids(); ASSERT_EQ(1ul, guids.size()); EXPECT_TRUE(bridge()->GetEntryByGUID(guids[0])->GetNotificationDismissed()); } TEST_F(SendTabToSelfBridgeTest, ExpireEntryDuringInit) { InitializeBridge(); const sync_pb::SendTabToSelfSpecifics expired_specifics = CreateSpecifics(1, AdvanceAndGetTime()); AdvanceAndGetTime(kExpiryTime / 2.0); const sync_pb::SendTabToSelfSpecifics not_expired_specifics = CreateSpecifics(2, AdvanceAndGetTime()); sync_pb::ModelTypeState state = StateWithEncryption("ekn"); std::unique_ptr metadata_changes = bridge()->CreateMetadataChangeList(); metadata_changes->UpdateModelTypeState(state); auto error = bridge()->ApplySyncChanges( std::move(metadata_changes), EntityAddList({expired_specifics, not_expired_specifics})); ASSERT_FALSE(error); ShutdownBridge(); AdvanceAndGetTime(kExpiryTime / 2.0); EXPECT_CALL(*mock_observer(), EntriesRemovedRemotely(SizeIs(1))); EXPECT_CALL(*processor(), Delete(_, _)); InitializeBridge(); std::vector guids = bridge()->GetAllGuids(); EXPECT_EQ(1ul, guids.size()); EXPECT_EQ(not_expired_specifics.url(), bridge()->GetEntryByGUID(guids[0])->GetURL().spec()); } TEST_F(SendTabToSelfBridgeTest, AddExpiredEntry) { InitializeBridge(); sync_pb::ModelTypeState state = StateWithEncryption("ekn"); std::unique_ptr metadata_changes = bridge()->CreateMetadataChangeList(); metadata_changes->UpdateModelTypeState(state); const sync_pb::SendTabToSelfSpecifics expired_specifics = CreateSpecifics(1, AdvanceAndGetTime()); AdvanceAndGetTime(kExpiryTime); const sync_pb::SendTabToSelfSpecifics not_expired_specifics = CreateSpecifics(2, AdvanceAndGetTime()); EXPECT_CALL(*processor(), Delete(_, _)); auto error = bridge()->ApplySyncChanges( std::move(metadata_changes), EntityAddList({expired_specifics, not_expired_specifics})); ASSERT_FALSE(error); std::vector guids = bridge()->GetAllGuids(); EXPECT_EQ(1ul, guids.size()); EXPECT_EQ(not_expired_specifics.url(), bridge()->GetEntryByGUID(guids[0])->GetURL().spec()); } TEST_F(SendTabToSelfBridgeTest, AddInvalidEntries) { InitializeBridge(); EXPECT_CALL(*mock_observer(), EntriesAddedRemotely(_)).Times(0); // Add Entry should succeed in this case. EXPECT_CALL(*processor(), Put(_, _, _)); EXPECT_NE(nullptr, bridge()->AddEntry(GURL("http://www.example.com/"), "d", kLocalDeviceCacheGuid)); // Add Entry should fail on invalid URLs. EXPECT_CALL(*processor(), Put(_, _, _)).Times(0); EXPECT_EQ(nullptr, bridge()->AddEntry(GURL(), "d", kLocalDeviceCacheGuid)); EXPECT_EQ(nullptr, bridge()->AddEntry(GURL("http://?k=v"), "d", kLocalDeviceCacheGuid)); EXPECT_EQ(nullptr, bridge()->AddEntry(GURL("http//google.com"), "d", kLocalDeviceCacheGuid)); } TEST_F(SendTabToSelfBridgeTest, IsBridgeReady) { InitializeBridge(); ASSERT_TRUE(bridge()->IsReady()); DisableBridge(); ASSERT_FALSE(bridge()->IsReady()); } TEST_F(SendTabToSelfBridgeTest, AddDuplicateEntries) { InitializeBridge(); EXPECT_CALL(*mock_observer(), EntriesAddedRemotely(_)).Times(0); // The de-duplication code does not use the title as a comparator. // So they are intentionally different here. EXPECT_CALL(*processor(), Put(_, _, _)).Times(1); bridge()->AddEntry(GURL("http://a.com"), "a", kLocalDeviceCacheGuid); bridge()->AddEntry(GURL("http://a.com"), "b", kLocalDeviceCacheGuid); EXPECT_EQ(1ul, bridge()->GetAllGuids().size()); // Wait for more than the current dedupe time (5 seconds). AdvanceAndGetTime(base::Seconds(6)); EXPECT_CALL(*processor(), Put(_, _, _)).Times(2); bridge()->AddEntry(GURL("http://a.com"), "a", kLocalDeviceCacheGuid); bridge()->AddEntry(GURL("http://b.com"), "b", kLocalDeviceCacheGuid); EXPECT_EQ(3ul, bridge()->GetAllGuids().size()); } TEST_F(SendTabToSelfBridgeTest, NotifyRemoteSendTabToSelfEntryAdded) { const std::string kRemoteGuid = "RemoteDevice"; InitializeBridge(); // Add on entry targeting this device and another targeting another device. syncer::EntityChangeList remote_input; SendTabToSelfEntry entry1("guid1", GURL("http://www.example.com/"), "title", AdvanceAndGetTime(), "device", kLocalDeviceCacheGuid); SendTabToSelfEntry entry2("guid2", GURL("http://www.example.com/"), "title", AdvanceAndGetTime(), "device", kRemoteGuid); remote_input.push_back( syncer::EntityChange::CreateAdd("guid1", MakeEntityData(entry1))); remote_input.push_back( syncer::EntityChange::CreateAdd("guid2", MakeEntityData(entry2))); auto metadata_change_list = std::make_unique(); // There should only be one entry sent to the observers. EXPECT_CALL(*mock_observer(), EntriesAddedRemotely(SizeIs(1))); bridge()->MergeSyncData(std::move(metadata_change_list), std::move(remote_input)); EXPECT_EQ(2ul, bridge()->GetAllGuids().size()); } // Tests that only the most recent device's guid is returned when multiple // devices have the same name. TEST_F(SendTabToSelfBridgeTest, GetTargetDeviceInfoSortedList_OneDevicePerName) { const std::string kRecentGuid = "guid1"; const std::string kOldGuid = "guid2"; const std::string kOlderGuid = "guid3"; InitializeBridge(); // Create multiple DeviceInfo objects with the same name but different guids. std::unique_ptr recent_device = CreateDevice(kRecentGuid, "device_name", clock()->Now() - base::Days(1)); AddTestDevice(recent_device.get()); std::unique_ptr old_device = CreateDevice(kOldGuid, "device_name", clock()->Now() - base::Days(3)); AddTestDevice(old_device.get()); std::unique_ptr older_device = CreateDevice(kOlderGuid, "device_name", clock()->Now() - base::Days(5)); AddTestDevice(older_device.get()); TargetDeviceInfo target_device_info( recent_device->client_name(), recent_device->client_name(), recent_device->guid(), recent_device->device_type(), recent_device->last_updated_timestamp()); EXPECT_THAT(bridge()->GetTargetDeviceInfoSortedList(), ElementsAre(target_device_info)); } // Tests that only devices that have the send tab to self receiving feature // enabled are returned. TEST_F(SendTabToSelfBridgeTest, GetTargetDeviceInfoSortedList_OnlyReceivingEnabled) { InitializeBridge(); std::unique_ptr enabled_device = CreateDevice("enabled_guid", "enabled_device_name", clock()->Now()); AddTestDevice(enabled_device.get()); std::unique_ptr disabled_device = CreateDevice("disabled_guid", "disabled_device_name", clock()->Now(), /*send_tab_to_self_receiving_enabled=*/false); AddTestDevice(disabled_device.get()); TargetDeviceInfo target_device_info( enabled_device->client_name(), enabled_device->client_name(), enabled_device->guid(), enabled_device->device_type(), enabled_device->last_updated_timestamp()); EXPECT_THAT(bridge()->GetTargetDeviceInfoSortedList(), ElementsAre(target_device_info)); } // Tests that only devices that are not expired are returned. TEST_F(SendTabToSelfBridgeTest, GetTargetDeviceInfoSortedList_NoExpiredDevices) { InitializeBridge(); std::unique_ptr expired_device = CreateDevice( "expired_guid", "expired_device_name", clock()->Now() - base::Days(11)); AddTestDevice(expired_device.get()); std::unique_ptr valid_device = CreateDevice( "valid_guid", "valid_device_name", clock()->Now() - base::Days(1)); AddTestDevice(valid_device.get()); TargetDeviceInfo target_device_info( valid_device->client_name(), valid_device->client_name(), valid_device->guid(), valid_device->device_type(), valid_device->last_updated_timestamp()); EXPECT_THAT(bridge()->GetTargetDeviceInfoSortedList(), ElementsAre(target_device_info)); } // Tests that the local device is not returned. TEST_F(SendTabToSelfBridgeTest, GetTargetDeviceInfoSortedList_NoLocalDevice) { InitializeBridge(); bridge()->SetLocalDeviceNameForTest(kLocalDeviceName); std::unique_ptr local_device = CreateDevice(kLocalDeviceCacheGuid, kLocalDeviceName, clock()->Now()); AddTestDevice(local_device.get()); std::unique_ptr other_local_device = CreateDevice("other_local_guid", kLocalDeviceName, clock()->Now()); AddTestDevice(other_local_device.get()); std::unique_ptr other_device = CreateDevice("other_guid", "other_device_name", clock()->Now()); AddTestDevice(other_device.get()); TargetDeviceInfo target_device_info( other_device->client_name(), other_device->client_name(), other_device->guid(), other_device->device_type(), other_device->last_updated_timestamp()); EXPECT_THAT(bridge()->GetTargetDeviceInfoSortedList(), ElementsAre(target_device_info)); } // Tests that a device is no longer returned after time advances and it expires. TEST_F(SendTabToSelfBridgeTest, GetTargetDeviceInfoSortedList_Updated_DeviceExpired) { InitializeBridge(); // Set a device that is about to expire and a more recent device. std::unique_ptr older_device = CreateDevice("older_guid", "older_name", clock()->Now() - base::Days(9)); AddTestDevice(older_device.get()); std::unique_ptr recent_device = CreateDevice( "recent_guid", "recent_name", clock()->Now() - base::Days(1)); AddTestDevice(recent_device.get()); TargetDeviceInfo older_device_info( older_device->client_name(), older_device->client_name(), older_device->guid(), older_device->device_type(), older_device->last_updated_timestamp()); TargetDeviceInfo recent_device_info( recent_device->client_name(), recent_device->client_name(), recent_device->guid(), recent_device->device_type(), recent_device->last_updated_timestamp()); // Make sure the list has the 2 devices. EXPECT_THAT(bridge()->GetTargetDeviceInfoSortedList(), ElementsAre(recent_device_info, older_device_info)); // Advance the time so that the older device expires. clock()->Advance(base::Days(5)); // Make sure only the recent device is in the list. EXPECT_THAT(bridge()->GetTargetDeviceInfoSortedList(), ElementsAre(recent_device_info)); } // Tests that a new device is also returned after it is added. TEST_F(SendTabToSelfBridgeTest, GetTargetDeviceInfoSortedList_Updated_NewEntries) { InitializeBridge(); // Set a valid device. std::unique_ptr device = CreateDevice("guid", "name", clock()->Now()); AddTestDevice(device.get()); // Make sure the list has the device. TargetDeviceInfo device_info(device->client_name(), device->client_name(), device->guid(), device->device_type(), device->last_updated_timestamp()); EXPECT_THAT(bridge()->GetTargetDeviceInfoSortedList(), ElementsAre(device_info)); // Add a new device. std::unique_ptr new_device = CreateDevice("new_guid", "new_name", clock()->Now()); AddTestDevice(new_device.get()); // Make sure both devices are in the list. TargetDeviceInfo new_device_info( new_device->client_name(), new_device->client_name(), new_device->guid(), new_device->device_type(), new_device->last_updated_timestamp()); EXPECT_THAT(bridge()->GetTargetDeviceInfoSortedList(), ElementsAre(device_info, new_device_info)); } // Tests the device list is updated if the last_updated_timestamp of one of // them changes. Regression test for crbug.com/1257573. TEST_F(SendTabToSelfBridgeTest, GetTargetDeviceInfoSortedList_Updated_LastUpdatedTimestampChanged) { InitializeBridge(); std::unique_ptr device1 = CreateDevice("guid1", "name1", clock()->Now() - base::Days(1)); AddTestDevice(device1.get()); std::unique_ptr device2_old = CreateDevice("guid2", "name2", clock()->Now() - base::Days(2)); AddTestDevice(device2_old.get()); EXPECT_THAT( bridge()->GetTargetDeviceInfoSortedList(), ElementsAre( TargetDeviceInfo(device1->client_name(), device1->client_name(), device1->guid(), device1->device_type(), device1->last_updated_timestamp()), TargetDeviceInfo(device2_old->client_name(), device2_old->client_name(), device2_old->guid(), device2_old->device_type(), device2_old->last_updated_timestamp()))); // Simulate device 2 being used today. std::unique_ptr device2_new = CreateDevice( device2_old->guid(), device2_old->client_name(), clock()->Now()); device_info_tracker()->Replace(device2_old.get(), device2_new.get()); // Device 2 is now the most recently used and should be the first on the list. EXPECT_THAT( bridge()->GetTargetDeviceInfoSortedList(), ElementsAre( TargetDeviceInfo(device2_new->client_name(), device2_new->client_name(), device2_new->guid(), device2_new->device_type(), device2_new->last_updated_timestamp()), TargetDeviceInfo(device1->client_name(), device1->client_name(), device1->guid(), device1->device_type(), device1->last_updated_timestamp()))); } TEST_F(SendTabToSelfBridgeTest, NotifyRemoteSendTabToSelfEntryOpened) { InitializeBridge(); SetLocalDeviceCacheGuid("Device1"); // Add on entry targeting this device and another targeting another device. syncer::EntityChangeList remote_input; SendTabToSelfEntry entry1("guid1", GURL("http://www.example.com/"), "title", AdvanceAndGetTime(), "device", "Device1"); SendTabToSelfEntry entry2("guid2", GURL("http://www.example.com/"), "title", AdvanceAndGetTime(), "device", "Device2"); remote_input.push_back( syncer::EntityChange::CreateAdd("guid1", MakeEntityData(entry1))); remote_input.push_back( syncer::EntityChange::CreateAdd("guid2", MakeEntityData(entry2))); entry1.MarkOpened(); remote_input.push_back( syncer::EntityChange::CreateUpdate("guid1", MakeEntityData(entry1))); auto metadata_change_list = std::make_unique(); // an entry with "guid1" should be sent to the observers. EXPECT_CALL(*mock_observer(), EntriesOpenedRemotely( AllOf(SizeIs(1), UnorderedElementsAre(GuidIs("guid1"))))); bridge()->MergeSyncData(std::move(metadata_change_list), std::move(remote_input)); EXPECT_EQ(2ul, bridge()->GetAllGuids().size()); } TEST_F(SendTabToSelfBridgeTest, ShouldHaveEmptyTargetDeviceInfoListWhileEmptyDeviceInfo) { InitializeBridgeWithoutDevice(); SetLocalDeviceCacheGuid("cache_guid"); ASSERT_FALSE(bridge()->change_processor()->TrackedCacheGuid().empty()); ASSERT_FALSE(device_info_tracker()->IsSyncing()); EXPECT_FALSE(bridge()->HasValidTargetDevice()); } TEST_F(SendTabToSelfBridgeTest, CollapseWhitespacesOfEntryTitle) { InitializeBridge(); const SendTabToSelfEntry* result = bridge()->AddEntry(GURL("http://a.com"), " a b ", kLocalDeviceCacheGuid); EXPECT_EQ("a b", result->GetTitle()); result = bridge()->AddEntry(GURL("http://b.com"), "입", kLocalDeviceCacheGuid); EXPECT_EQ("입", result->GetTitle()); } } // namespace } // namespace send_tab_to_self