blob: adb0fa60e15ee2e060614bd6294a67f37533833f [file] [log] [blame]
// 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 <map>
#include <set>
#include <utility>
#include "base/bind.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/scoped_task_environment.h"
#include "base/test/simple_test_clock.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/sync/model/entity_change.h"
#include "components/sync/model/metadata_batch.h"
#include "components/sync/model/mock_model_type_change_processor.h"
#include "components/sync/model/model_type_store_test_util.h"
#include "components/sync/model_impl/in_memory_metadata_change_list.h"
#include "components/sync/protocol/model_type_state.pb.h"
#include "components/sync/test/test_matchers.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::IsEmpty;
using testing::Return;
using testing::SizeIs;
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 kTargetDeviceCacheGuid[] = "target_device";
sync_pb::SendTabToSelfSpecifics CreateSpecifics(
int suffix,
base::Time shared_time = base::Time::Now(),
base::Time navigation_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(kTargetDeviceCacheGuid);
specifics.set_shared_time_usec(
shared_time.ToDeltaSinceWindowsEpoch().InMicroseconds());
specifics.set_navigation_time_usec(
navigation_time.ToDeltaSinceWindowsEpoch().InMicroseconds());
return specifics;
}
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<const SendTabToSelfEntry*>&));
MOCK_METHOD1(EntriesRemovedRemotely, void(const std::vector<std::string>&));
};
class SendTabToSelfBridgeTest : public testing::Test {
protected:
SendTabToSelfBridgeTest()
: store_(syncer::ModelTypeStoreTestUtil::CreateInMemoryStoreForTest()) {
scoped_feature_list_.InitAndEnableFeature(kSendTabToSelfShowSendingUI);
SetLocalDeviceCacheGuid("target_device");
}
// Initialized the bridge based on the current local device and store. Can
// only be called once per run, as it passes |store_|.
void InitializeBridge() {
ON_CALL(mock_processor_, IsTrackingMetadata()).WillByDefault(Return(true));
bridge_ = std::make_unique<SendTabToSelfBridge>(
mock_processor_.CreateForwardingProcessor(), &clock_,
syncer::ModelTypeStoreTestUtil::MoveStoreToFactory(std::move(store_)),
nullptr);
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::TimeDelta::FromMilliseconds(10)) {
clock_.Advance(delta);
return clock_.Now();
}
void DisableBridge() {
ON_CALL(mock_processor_, IsTrackingMetadata()).WillByDefault(Return(false));
}
std::unique_ptr<syncer::EntityData> MakeEntityData(
const SendTabToSelfEntry& entry) {
SendTabToSelfLocal specifics = entry.AsLocalProto();
auto entity_data = std::make_unique<syncer::EntityData>();
*(entity_data->specifics.mutable_send_tab_to_self()) =
specifics.specifics();
entity_data->non_unique_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<sync_pb::SendTabToSelfSpecifics>& specifics_list) {
syncer::EntityChangeList changes;
for (const auto& specifics : specifics_list) {
auto entity_data = std::make_unique<syncer::EntityData>();
*(entity_data->specifics.mutable_send_tab_to_self()) = specifics;
entity_data->non_unique_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", AdvanceAndGetTime(),
"target_device");
bridge_->AddEntry(GURL("http://b.com"), "b", AdvanceAndGetTime(),
"target_device");
bridge_->AddEntry(GURL("http://c.com"), "c", AdvanceAndGetTime(),
"target_device");
bridge_->AddEntry(GURL("http://d.com"), "d", AdvanceAndGetTime(),
"target_device");
}
void SetLocalDeviceCacheGuid(const std::string& cache_guid) {
ON_CALL(mock_processor_, TrackedCacheGuid())
.WillByDefault(Return(cache_guid));
}
syncer::MockModelTypeChangeProcessor* processor() { return &mock_processor_; }
SendTabToSelfBridge* bridge() { return bridge_.get(); }
MockSendTabToSelfModelObserver* mock_observer() { return &mock_observer_; }
private:
base::SimpleTestClock clock_;
// In memory model type store needs to be able to post tasks.
base::test::ScopedTaskEnvironment task_environment_;
std::unique_ptr<syncer::ModelTypeStore> store_;
testing::NiceMock<syncer::MockModelTypeChangeProcessor> mock_processor_;
std::unique_ptr<SendTabToSelfBridge> bridge_;
testing::NiceMock<MockSendTabToSelfModelObserver> mock_observer_;
base::test::ScopedFeatureList scoped_feature_list_;
DISALLOW_COPY_AND_ASSIGN(SendTabToSelfBridgeTest);
};
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(), AdvanceAndGetTime(), "device",
"target_device");
remote_input.push_back(
syncer::EntityChange::CreateAdd("guid1", MakeEntityData(entry)));
auto metadata_change_list =
std::make_unique<syncer::InMemoryMetadataChangeList>();
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<syncer::MetadataChangeList> 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(), AdvanceAndGetTime(), "device",
"target_device");
syncer::EntityChangeList add_changes;
add_changes.push_back(
syncer::EntityChange::CreateAdd("guid1", MakeEntityData(entry)));
auto metadata_change_list =
std::make_unique<syncer::InMemoryMetadataChangeList>();
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(), AdvanceAndGetTime(), "device",
"target_device");
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());
}
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<syncer::MetadataChangeList> 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<std::string> 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<syncer::MetadataChangeList> 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<syncer::MetadataChangeList> 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, PreserveDissmissalAfterRestartBridge) {
InitializeBridge();
const sync_pb::SendTabToSelfSpecifics specifics = CreateSpecifics(1);
std::unique_ptr<syncer::MetadataChangeList> metadata_changes =
bridge()->CreateMetadataChangeList();
auto error = bridge()->ApplySyncChanges(std::move(metadata_changes),
EntityAddList({specifics}));
ASSERT_FALSE(error);
EXPECT_CALL(*processor(), Put(_, _, _)).Times(0);
EXPECT_CALL(*processor(), Delete(_, _)).Times(0);
bridge()->DismissEntry(specifics.guid());
ShutdownBridge();
InitializeBridge();
std::vector<std::string> 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());
AdvanceAndGetTime(kExpiryTime / 2.0);
const sync_pb::SendTabToSelfSpecifics not_expired_specifics =
CreateSpecifics(2, AdvanceAndGetTime(), AdvanceAndGetTime());
sync_pb::ModelTypeState state = StateWithEncryption("ekn");
std::unique_ptr<syncer::MetadataChangeList> 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);
AdvanceAndGetTime(kExpiryTime / 2.0);
EXPECT_CALL(*mock_observer(), EntriesRemovedRemotely(SizeIs(1)));
ShutdownBridge();
InitializeBridge();
std::vector<std::string> 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<syncer::MetadataChangeList> metadata_changes =
bridge()->CreateMetadataChangeList();
metadata_changes->UpdateModelTypeState(state);
const sync_pb::SendTabToSelfSpecifics expired_specifics =
CreateSpecifics(1, AdvanceAndGetTime(), AdvanceAndGetTime());
AdvanceAndGetTime(kExpiryTime);
const sync_pb::SendTabToSelfSpecifics not_expired_specifics =
CreateSpecifics(2, AdvanceAndGetTime(), AdvanceAndGetTime());
auto error = bridge()->ApplySyncChanges(
std::move(metadata_changes),
EntityAddList({expired_specifics, not_expired_specifics}));
ASSERT_FALSE(error);
std::vector<std::string> 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_NE(nullptr, bridge()->AddEntry(GURL("http://www.example.com/"), "d",
AdvanceAndGetTime(), "target_device"));
// Add Entry should fail on invalid URLs.
EXPECT_EQ(nullptr, bridge()->AddEntry(GURL(), "d", AdvanceAndGetTime(),
"target_device"));
EXPECT_EQ(nullptr, bridge()->AddEntry(GURL("http://?k=v"), "d",
AdvanceAndGetTime(), "target_device"));
EXPECT_EQ(nullptr, bridge()->AddEntry(GURL("http//google.com"), "d",
AdvanceAndGetTime(), "target_device"));
// Add Entry should succeed on an invalid navigation_time, since that is the
// case for sending links.
EXPECT_NE(nullptr, bridge()->AddEntry(GURL("http://www.example.com/"), "d",
base::Time(), "target_device"));
}
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);
base::Time navigation_time = AdvanceAndGetTime();
// The de-duplication code does not use the title as a comparator.
// So they are intentionally different here.
bridge()->AddEntry(GURL("http://a.com"), "a", navigation_time,
"target_device");
bridge()->AddEntry(GURL("http://a.com"), "b", navigation_time,
"target_device");
EXPECT_EQ(1ul, bridge()->GetAllGuids().size());
bridge()->AddEntry(GURL("http://a.com"), "a", AdvanceAndGetTime(),
"target_device");
bridge()->AddEntry(GURL("http://b.com"), "b", AdvanceAndGetTime(),
"target_device");
EXPECT_EQ(3ul, bridge()->GetAllGuids().size());
}
TEST_F(SendTabToSelfBridgeTest,
NotifyRemoteSendTabToSelfEntryAdded_BroadcastDisabled) {
base::test::ScopedFeatureList scoped_features;
scoped_features.InitWithFeatures(
/*enabled_features=*/{kSendTabToSelfShowSendingUI},
/*disabled_features=*/{kSendTabToSelfBroadcast});
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(), AdvanceAndGetTime(), "device",
"Device1");
SendTabToSelfEntry entry2("guid2", GURL("http://www.example.com/"), "title",
AdvanceAndGetTime(), AdvanceAndGetTime(), "device",
"Device2");
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<syncer::InMemoryMetadataChangeList>();
// 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());
}
TEST_F(SendTabToSelfBridgeTest,
NotifyRemoteSendTabToSelfEntryAdded_BroadcastEnabled) {
base::test::ScopedFeatureList scoped_features;
scoped_features.InitWithFeatures(
/*enabled_features=*/{kSendTabToSelfShowSendingUI,
kSendTabToSelfBroadcast},
/*disabled_features=*/{});
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(), AdvanceAndGetTime(), "device",
"Device1");
SendTabToSelfEntry entry2("guid2", GURL("http://www.example.com/"), "title",
AdvanceAndGetTime(), AdvanceAndGetTime(), "device",
"Device2");
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<syncer::InMemoryMetadataChangeList>();
// The 2 entries should be sent to the observers.
EXPECT_CALL(*mock_observer(), EntriesAddedRemotely(SizeIs(2)));
bridge()->MergeSyncData(std::move(metadata_change_list),
std::move(remote_input));
EXPECT_EQ(2ul, bridge()->GetAllGuids().size());
}
} // namespace
} // namespace send_tab_to_self