blob: 5235b6b16105450629179f03c1962192b74cba6c [file]
// Copyright 2016 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/reading_list/core/reading_list_sync_bridge.h"
#include <array>
#include <map>
#include <set>
#include <utility>
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/test/bind.h"
#include "base/test/simple_test_clock.h"
#include "base/test/task_environment.h"
#include "components/reading_list/core/reading_list_model_impl.h"
#include "components/reading_list/core/reading_list_model_storage_impl.h"
#include "components/sync/base/deletion_origin.h"
#include "components/sync/base/storage_type.h"
#include "components/sync/test/data_type_store_test_util.h"
#include "components/sync/test/mock_data_type_local_change_processor.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
using testing::_;
using testing::SizeIs;
constexpr char kCacheGuid[] = "test_cache_guid";
MATCHER_P3(MatchesSpecifics,
expected_title,
expected_url,
expected_status,
"") {
const sync_pb::ReadingListSpecifics& specifics =
arg->specifics.reading_list();
if (specifics.title() != expected_title) {
*result_listener << "which has title \"" << specifics.title();
return false;
}
if (specifics.url() != expected_url) {
*result_listener << "which has URL " << specifics.url();
return false;
}
if (specifics.status() != expected_status) {
*result_listener << "which has unexpected status";
return false;
}
return true;
}
MATCHER_P2(MatchesEntry, url_matcher, is_read_matcher, "") {
if (!arg) {
*result_listener << "which is null";
return false;
}
return testing::SafeMatcherCast<GURL>(url_matcher)
.MatchAndExplain(arg->URL(), result_listener) &&
testing::SafeMatcherCast<bool>(is_read_matcher)
.MatchAndExplain(arg->IsRead(), result_listener);
}
MATCHER_P(DeletionOriginMatchesLocation, expected_location, "") {
return arg.is_specified() &&
*arg.GetLocationForTesting() == expected_location;
}
// Tests that the transition from |entryA| to |entryB| is possible (|possible|
// is true) or not.
void ExpectAB(const sync_pb::ReadingListSpecifics& entryA,
const sync_pb::ReadingListSpecifics& entryB,
bool possible) {
ASSERT_TRUE(ReadingListEntry::IsSpecificsValid(entryA));
ASSERT_TRUE(ReadingListEntry::IsSpecificsValid(entryB));
EXPECT_EQ(ReadingListSyncBridge::CompareEntriesForSync(entryA, entryB),
possible);
scoped_refptr<ReadingListEntry> a =
ReadingListEntry::FromReadingListValidSpecifics(
entryA, base::Time::FromTimeT(10));
scoped_refptr<ReadingListEntry> b =
ReadingListEntry::FromReadingListValidSpecifics(
entryB, base::Time::FromTimeT(10));
a->MergeWithEntry(*b);
std::unique_ptr<sync_pb::ReadingListSpecifics> mergedEntry =
a->AsReadingListSpecifics();
if (possible) {
// If transition is possible, the merge should be B.
EXPECT_EQ(entryB.SerializeAsString(), mergedEntry->SerializeAsString());
} else {
// If transition is not possible, the transition shold be possible to the
// merged state.
EXPECT_TRUE(
ReadingListSyncBridge::CompareEntriesForSync(entryA, *mergedEntry));
EXPECT_TRUE(
ReadingListSyncBridge::CompareEntriesForSync(entryB, *mergedEntry));
}
}
base::Time AdvanceAndGetTime(base::SimpleTestClock* clock) {
clock->Advance(base::Milliseconds(10));
return clock->Now();
}
} // namespace
class ReadingListSyncBridgeTest : public testing::Test {
protected:
ReadingListSyncBridgeTest() {
ResetModelAndBridge(syncer::StorageType::kUnspecified,
syncer::WipeModelUponSyncDisabledBehavior::kNever,
/*initial_sync_done=*/true);
}
void ResetModelAndBridge(syncer::StorageType storage_type,
syncer::WipeModelUponSyncDisabledBehavior
wipe_model_upon_sync_disabled_behavior,
bool initial_sync_done) {
std::unique_ptr<syncer::DataTypeStore> data_type_store =
syncer::DataTypeStoreTestUtil::CreateInMemoryStoreForTest();
underlying_in_memory_store_ = data_type_store.get();
if (initial_sync_done) {
// Mimic initial sync having been done earlier.
sync_pb::DataTypeState data_type_state;
data_type_state.set_cache_guid(kCacheGuid);
data_type_state.set_initial_sync_state(
sync_pb::DataTypeState_InitialSyncState_INITIAL_SYNC_DONE);
std::unique_ptr<syncer::DataTypeStore::WriteBatch> write_batch =
underlying_in_memory_store_->CreateWriteBatch();
write_batch->GetMetadataChangeList()->UpdateDataTypeState(
data_type_state);
underlying_in_memory_store_->CommitWriteBatch(std::move(write_batch),
base::DoNothing());
}
model_ = ReadingListModelImpl::BuildNewForTest(
std::make_unique<ReadingListModelStorageImpl>(
syncer::DataTypeStoreTestUtil::MoveStoreToFactory(
std::move(data_type_store))),
storage_type, wipe_model_upon_sync_disabled_behavior, &clock_,
processor_.CreateForwardingProcessor());
// Wait until the model loads.
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(model_->loaded());
ON_CALL(processor_, IsTrackingMetadata())
.WillByDefault(testing::Return(initial_sync_done));
}
ReadingListSyncBridge* bridge() { return model_->GetSyncBridgeForTest(); }
// In memory data type store needs to be able to post tasks.
base::test::SingleThreadTaskEnvironment task_environment_;
base::SimpleTestClock clock_;
testing::NiceMock<syncer::MockDataTypeLocalChangeProcessor> processor_;
std::unique_ptr<ReadingListModelImpl> model_;
// DataTypeStore is owned by |model_|.
raw_ptr<syncer::DataTypeStore> underlying_in_memory_store_ = nullptr;
};
TEST_F(ReadingListSyncBridgeTest, SaveOneRead) {
auto entry = base::MakeRefCounted<ReadingListEntry>(
GURL("http://read.example.com/"), "read title",
AdvanceAndGetTime(&clock_));
entry->SetRead(true, AdvanceAndGetTime(&clock_));
AdvanceAndGetTime(&clock_);
EXPECT_CALL(processor_,
Put("http://read.example.com/",
MatchesSpecifics("read title", "http://read.example.com/",
sync_pb::ReadingListSpecifics::READ),
_));
auto batch = model_->BeginBatchUpdatesWithSyncMetadata();
bridge()->DidAddOrUpdateEntry(*entry, batch->GetSyncMetadataChangeList());
}
TEST_F(ReadingListSyncBridgeTest, SaveOneUnread) {
auto entry = base::MakeRefCounted<ReadingListEntry>(
GURL("http://unread.example.com/"), "unread title",
AdvanceAndGetTime(&clock_));
EXPECT_CALL(processor_,
Put("http://unread.example.com/",
MatchesSpecifics("unread title", "http://unread.example.com/",
sync_pb::ReadingListSpecifics::UNSEEN),
_));
auto batch = model_->BeginBatchUpdatesWithSyncMetadata();
bridge()->DidAddOrUpdateEntry(*entry, batch->GetSyncMetadataChangeList());
}
TEST_F(ReadingListSyncBridgeTest, DeleteOneEntry) {
const base::Location kLocation = FROM_HERE;
auto entry = MakeRefCounted<ReadingListEntry>(
GURL("http://unread.example.com/"), "unread title",
AdvanceAndGetTime(&clock_));
EXPECT_CALL(processor_, Delete("http://unread.example.com/",
DeletionOriginMatchesLocation(kLocation), _));
auto batch = model_->BeginBatchUpdatesWithSyncMetadata();
bridge()->DidRemoveEntry(*entry, kLocation,
batch->GetSyncMetadataChangeList());
}
TEST_F(ReadingListSyncBridgeTest, SyncMergeOneEntry) {
EXPECT_CALL(processor_, Put(_, _, _)).Times(0);
syncer::EntityChangeList remote_input;
auto entry = base::MakeRefCounted<ReadingListEntry>(
GURL("http://read.example.com/"), "read title",
AdvanceAndGetTime(&clock_));
entry->SetRead(true, AdvanceAndGetTime(&clock_));
std::unique_ptr<sync_pb::ReadingListSpecifics> specifics =
entry->AsReadingListSpecifics();
syncer::EntityData data;
*data.specifics.mutable_reading_list() = *specifics;
remote_input.push_back(syncer::EntityChange::CreateAdd(
"http://read.example.com/", std::move(data)));
std::unique_ptr<syncer::MetadataChangeList> metadata_changes(
bridge()->CreateMetadataChangeList());
ASSERT_EQ(0ul, model_->size());
auto error = bridge()->MergeFullSyncData(std::move(metadata_changes),
std::move(remote_input));
EXPECT_FALSE(error.has_value());
EXPECT_EQ(1ul, model_->size());
EXPECT_THAT(model_->GetEntryByURL(GURL("http://read.example.com/")),
MatchesEntry("http://read.example.com/",
/*is_read=*/true));
}
TEST_F(ReadingListSyncBridgeTest, ApplyIncrementalSyncChangesOneAdd) {
EXPECT_CALL(processor_, Put(_, _, _)).Times(0);
auto entry = base::MakeRefCounted<ReadingListEntry>(
GURL("http://read.example.com/"), "read title",
AdvanceAndGetTime(&clock_));
entry->SetRead(true, AdvanceAndGetTime(&clock_));
std::unique_ptr<sync_pb::ReadingListSpecifics> specifics =
entry->AsReadingListSpecifics();
syncer::EntityData data;
*data.specifics.mutable_reading_list() = *specifics;
syncer::EntityChangeList add_changes;
add_changes.push_back(syncer::EntityChange::CreateAdd(
"http://read.example.com/", std::move(data)));
ASSERT_EQ(0ul, model_->size());
auto error = bridge()->ApplyIncrementalSyncChanges(
bridge()->CreateMetadataChangeList(), std::move(add_changes));
EXPECT_FALSE(error.has_value());
EXPECT_EQ(1ul, model_->size());
EXPECT_THAT(model_->GetEntryByURL(GURL("http://read.example.com/")),
MatchesEntry("http://read.example.com/",
/*is_read=*/true));
}
TEST_F(ReadingListSyncBridgeTest, ApplyIncrementalSyncChangesOneMerge) {
AdvanceAndGetTime(&clock_);
model_->AddOrReplaceEntry(GURL("http://unread.example.com/"), "unread title",
reading_list::ADDED_VIA_CURRENT_APP,
/*estimated_read_time=*/std::nullopt,
/*creation_time=*/std::nullopt);
auto new_entry = base::MakeRefCounted<ReadingListEntry>(
GURL("http://unread.example.com/"), "unread title",
AdvanceAndGetTime(&clock_));
new_entry->SetRead(true, AdvanceAndGetTime(&clock_));
std::unique_ptr<sync_pb::ReadingListSpecifics> specifics =
new_entry->AsReadingListSpecifics();
syncer::EntityData data;
*data.specifics.mutable_reading_list() = *specifics;
// ApplyIncrementalSyncChanges() must *not* result in any Put() calls - that
// would risk triggering ping-pong between two syncing devices.
EXPECT_CALL(processor_, Put(_, _, _)).Times(0);
syncer::EntityChangeList add_changes;
add_changes.push_back(syncer::EntityChange::CreateAdd(
"http://unread.example.com/", std::move(data)));
ASSERT_EQ(1ul, model_->size());
auto error = bridge()->ApplyIncrementalSyncChanges(
bridge()->CreateMetadataChangeList(), std::move(add_changes));
EXPECT_FALSE(error.has_value());
EXPECT_EQ(1ul, model_->size());
EXPECT_THAT(model_->GetEntryByURL(GURL("http://unread.example.com/")),
MatchesEntry("http://unread.example.com/", /*is_read=*/true));
}
TEST_F(ReadingListSyncBridgeTest, ApplyIncrementalSyncChangesOneIgnored) {
// Read entry but with unread URL as it must update the other one.
auto old_entry = base::MakeRefCounted<ReadingListEntry>(
GURL("http://unread.example.com/"), "old unread title",
AdvanceAndGetTime(&clock_));
old_entry->SetRead(true, AdvanceAndGetTime(&clock_));
AdvanceAndGetTime(&clock_);
model_->AddOrReplaceEntry(GURL("http://unread.example.com/"),
"new unread title",
reading_list::ADDED_VIA_CURRENT_APP,
/*estimated_read_time=*/std::nullopt,
/*creation_time=*/std::nullopt);
std::unique_ptr<sync_pb::ReadingListSpecifics> specifics =
old_entry->AsReadingListSpecifics();
syncer::EntityData data;
*data.specifics.mutable_reading_list() = *specifics;
// ApplyIncrementalSyncChanges() must *not* result in any Put() calls - that
// would risk triggering ping-pong between two syncing devices.
EXPECT_CALL(processor_, Put(_, _, _)).Times(0);
syncer::EntityChangeList add_changes;
add_changes.push_back(syncer::EntityChange::CreateAdd(
"http://unread.example.com/", std::move(data)));
ASSERT_EQ(1ul, model_->size());
auto error = bridge()->ApplyIncrementalSyncChanges(
bridge()->CreateMetadataChangeList(), std::move(add_changes));
EXPECT_FALSE(error.has_value());
EXPECT_EQ(1ul, model_->size());
EXPECT_THAT(model_->GetEntryByURL(GURL("http://unread.example.com/")),
MatchesEntry("http://unread.example.com/", /*is_read=*/false));
}
TEST_F(ReadingListSyncBridgeTest, ApplyIncrementalSyncChangesOneRemove) {
model_->AddOrReplaceEntry(GURL("http://read.example.com/"), "read title",
reading_list::ADDED_VIA_CURRENT_APP,
/*estimated_read_time=*/std::nullopt,
/*creation_time=*/std::nullopt);
syncer::EntityChangeList delete_changes;
delete_changes.push_back(syncer::EntityChange::CreateDelete(
"http://read.example.com/", syncer::EntityData()));
ASSERT_EQ(1ul, model_->size());
auto error = bridge()->ApplyIncrementalSyncChanges(
bridge()->CreateMetadataChangeList(), std::move(delete_changes));
EXPECT_FALSE(error.has_value());
EXPECT_EQ(0ul, model_->size());
}
TEST_F(ReadingListSyncBridgeTest, DisableSyncWithUnspecifiedStorage) {
ResetModelAndBridge(syncer::StorageType::kUnspecified,
syncer::WipeModelUponSyncDisabledBehavior::kNever,
/*initial_sync_done=*/true);
model_->AddOrReplaceEntry(GURL("http://read.example.com/"), "read title",
reading_list::ADDED_VIA_CURRENT_APP,
/*estimated_read_time=*/std::nullopt,
/*creation_time=*/std::nullopt);
ASSERT_EQ(1ul, model_->size());
bridge()->ApplyDisableSyncChanges(bridge()->CreateMetadataChangeList());
EXPECT_EQ(1ul, model_->size());
}
TEST_F(ReadingListSyncBridgeTest, DisableSyncWithAccountStorage) {
ResetModelAndBridge(syncer::StorageType::kAccount,
syncer::WipeModelUponSyncDisabledBehavior::kAlways,
/*initial_sync_done=*/true);
model_->AddOrReplaceEntry(GURL("http://read.example.com/"), "read title",
reading_list::ADDED_VIA_CURRENT_APP,
/*estimated_read_time=*/std::nullopt,
/*creation_time=*/std::nullopt);
ASSERT_EQ(1ul, model_->size());
bridge()->ApplyDisableSyncChanges(bridge()->CreateMetadataChangeList());
EXPECT_EQ(0ul, model_->size());
}
TEST_F(ReadingListSyncBridgeTest, DisableSyncWithAccountStorageAndOrphanData) {
ResetModelAndBridge(syncer::StorageType::kAccount,
syncer::WipeModelUponSyncDisabledBehavior::kAlways,
/*initial_sync_done=*/true);
// Write some orphan or unexpected data directly onto the underlying
// DataTypeStore, which should be rare but may be possible due to bugs or
// edge cases.
std::unique_ptr<syncer::DataTypeStore::WriteBatch> write_batch =
underlying_in_memory_store_->CreateWriteBatch();
write_batch->WriteData("orphan-data-key", "orphan-data-value");
std::optional<syncer::ModelError> error;
base::RunLoop loop;
underlying_in_memory_store_->CommitWriteBatch(
std::move(write_batch),
base::BindLambdaForTesting(
[&loop](const std::optional<syncer::ModelError>& error) {
EXPECT_FALSE(error.has_value()) << error->ToString();
loop.Quit();
}));
loop.Run();
ASSERT_THAT(syncer::DataTypeStoreTestUtil::ReadAllDataAndWait(
*underlying_in_memory_store_),
SizeIs(1));
bridge()->ApplyDisableSyncChanges(bridge()->CreateMetadataChangeList());
EXPECT_THAT(syncer::DataTypeStoreTestUtil::ReadAllDataAndWait(
*underlying_in_memory_store_),
SizeIs(0));
}
TEST_F(ReadingListSyncBridgeTest, CompareEntriesForSync) {
sync_pb::ReadingListSpecifics entryA;
sync_pb::ReadingListSpecifics entryB;
entryA.set_entry_id("http://foo.bar/");
entryB.set_entry_id("http://foo.bar/");
entryA.set_url("http://foo.bar/");
entryB.set_url("http://foo.bar/");
entryA.set_title("Foo Bar");
entryB.set_title("Foo Bar");
entryA.set_status(sync_pb::ReadingListSpecifics::UNREAD);
entryB.set_status(sync_pb::ReadingListSpecifics::UNREAD);
entryA.set_creation_time_us(10);
entryB.set_creation_time_us(10);
entryA.set_estimated_read_time_seconds(420);
entryB.set_estimated_read_time_seconds(420);
entryA.set_first_read_time_us(50);
entryB.set_first_read_time_us(50);
entryA.set_update_time_us(100);
entryB.set_update_time_us(100);
entryA.set_update_title_time_us(110);
entryB.set_update_title_time_us(110);
// Equal entries can be submitted.
ExpectAB(entryA, entryB, true);
ExpectAB(entryB, entryA, true);
// Try to update each field.
// You cannot change the URL of an entry.
entryA.set_url("http://foo.foo/");
EXPECT_FALSE(ReadingListSyncBridge::CompareEntriesForSync(entryA, entryB));
EXPECT_FALSE(ReadingListSyncBridge::CompareEntriesForSync(entryB, entryA));
entryA.set_url("http://foo.bar/");
// You can set a title to a title later in alphabetical order if the
// update_title_time is the same. If a title has been more recently updated,
// the only possible transition is to this one.
entryA.set_title("");
ExpectAB(entryA, entryB, true);
ExpectAB(entryB, entryA, false);
entryA.set_update_title_time_us(109);
ExpectAB(entryA, entryB, true);
ExpectAB(entryB, entryA, false);
entryA.set_update_title_time_us(110);
entryA.set_title("Foo Aar");
ExpectAB(entryA, entryB, true);
ExpectAB(entryB, entryA, false);
entryA.set_update_title_time_us(109);
ExpectAB(entryA, entryB, true);
ExpectAB(entryB, entryA, false);
entryA.set_update_title_time_us(110);
entryA.set_title("Foo Ba");
ExpectAB(entryA, entryB, true);
ExpectAB(entryB, entryA, false);
entryA.set_update_title_time_us(109);
ExpectAB(entryA, entryB, true);
ExpectAB(entryB, entryA, false);
entryA.set_update_title_time_us(110);
entryA.set_title("Foo Bas");
ExpectAB(entryA, entryB, false);
ExpectAB(entryB, entryA, true);
entryA.set_update_title_time_us(109);
ExpectAB(entryA, entryB, true);
ExpectAB(entryB, entryA, false);
entryA.set_update_title_time_us(110);
entryA.set_title("Foo Bar");
// Update times.
entryA.set_creation_time_us(9);
ExpectAB(entryA, entryB, true);
ExpectAB(entryB, entryA, false);
entryA.set_first_read_time_us(51);
ExpectAB(entryA, entryB, true);
ExpectAB(entryB, entryA, false);
entryA.set_first_read_time_us(49);
ExpectAB(entryA, entryB, true);
ExpectAB(entryB, entryA, false);
entryA.set_first_read_time_us(0);
ExpectAB(entryA, entryB, true);
ExpectAB(entryB, entryA, false);
entryA.set_first_read_time_us(50);
entryB.set_first_read_time_us(0);
ExpectAB(entryA, entryB, true);
ExpectAB(entryB, entryA, false);
entryB.set_first_read_time_us(50);
entryA.set_creation_time_us(10);
entryA.set_first_read_time_us(51);
ExpectAB(entryA, entryB, true);
ExpectAB(entryB, entryA, false);
entryA.set_first_read_time_us(0);
ExpectAB(entryA, entryB, true);
ExpectAB(entryB, entryA, false);
entryA.set_first_read_time_us(50);
entryA.set_creation_time_us(11);
entryA.set_estimated_read_time_seconds(400);
ExpectAB(entryB, entryA, true);
ExpectAB(entryA, entryB, false);
entryA.set_estimated_read_time_seconds(420);
entryA.set_creation_time_us(10);
entryA.set_update_time_us(99);
ExpectAB(entryA, entryB, true);
ExpectAB(entryB, entryA, false);
std::array<sync_pb::ReadingListSpecifics::ReadingListEntryStatus, 3>
status_oder = {
sync_pb::ReadingListSpecifics::UNSEEN,
sync_pb::ReadingListSpecifics::UNREAD,
sync_pb::ReadingListSpecifics::READ,
};
for (int index_a = 0; index_a < 3; index_a++) {
entryA.set_status(status_oder[index_a]);
for (int index_b = 0; index_b < 3; index_b++) {
entryB.set_status(status_oder[index_b]);
ExpectAB(entryA, entryB, true);
ExpectAB(entryB, entryA, false);
}
}
entryA.set_update_time_us(100);
for (int index_a = 0; index_a < 3; index_a++) {
entryA.set_status(status_oder[index_a]);
entryB.set_status(status_oder[index_a]);
ExpectAB(entryA, entryB, true);
ExpectAB(entryB, entryA, true);
for (int index_b = index_a + 1; index_b < 3; index_b++) {
entryB.set_status(status_oder[index_b]);
ExpectAB(entryA, entryB, true);
ExpectAB(entryB, entryA, false);
}
}
}
TEST_F(ReadingListSyncBridgeTest, EntityDataShouldBeValid) {
auto entry = base::MakeRefCounted<ReadingListEntry>(
GURL("http://example.com/"), "example title", AdvanceAndGetTime(&clock_));
std::unique_ptr<sync_pb::ReadingListSpecifics> specifics =
entry->AsReadingListSpecifics();
syncer::EntityData data;
*data.specifics.mutable_reading_list() = *specifics;
EXPECT_TRUE(bridge()->IsEntityDataValid(data));
}
TEST_F(ReadingListSyncBridgeTest,
EntityDataShouldBeNotValidIfItHasEmptyEntryId) {
auto entry = base::MakeRefCounted<ReadingListEntry>(
GURL("http://example.com/"), "example title", AdvanceAndGetTime(&clock_));
std::unique_ptr<sync_pb::ReadingListSpecifics> specifics =
entry->AsReadingListSpecifics();
*specifics->mutable_entry_id() = "";
syncer::EntityData data;
*data.specifics.mutable_reading_list() = *specifics;
EXPECT_FALSE(bridge()->IsEntityDataValid(data));
}
TEST_F(ReadingListSyncBridgeTest,
EntityDataShouldBeNotValidIfItHasUnequalEntryIdAndUrl) {
auto entry = base::MakeRefCounted<ReadingListEntry>(
GURL("http://EntryUrl.com/"), "example title",
AdvanceAndGetTime(&clock_));
std::unique_ptr<sync_pb::ReadingListSpecifics> specifics =
entry->AsReadingListSpecifics();
*specifics->mutable_entry_id() = "http://UnequalEntryIdAndUrl.com/";
syncer::EntityData data;
*data.specifics.mutable_reading_list() = *specifics;
EXPECT_FALSE(bridge()->IsEntityDataValid(data));
}
TEST_F(ReadingListSyncBridgeTest, EntityDataShouldBeNotValidIfItHasEmptyUrl) {
auto entry = base::MakeRefCounted<ReadingListEntry>(
GURL("http://example.com/"), "example title", AdvanceAndGetTime(&clock_));
std::unique_ptr<sync_pb::ReadingListSpecifics> specifics =
entry->AsReadingListSpecifics();
*specifics->mutable_url() = "";
syncer::EntityData data;
*data.specifics.mutable_reading_list() = *specifics;
EXPECT_FALSE(bridge()->IsEntityDataValid(data));
}
TEST_F(ReadingListSyncBridgeTest, EntityDataShouldBeNotValidIfItHasInvalidUrl) {
auto entry = base::MakeRefCounted<ReadingListEntry>(
GURL("http://example.com/"), "example title", AdvanceAndGetTime(&clock_));
std::unique_ptr<sync_pb::ReadingListSpecifics> specifics =
entry->AsReadingListSpecifics();
*specifics->mutable_url() = "InvalidUrl";
syncer::EntityData data;
*data.specifics.mutable_reading_list() = *specifics;
EXPECT_FALSE(bridge()->IsEntityDataValid(data));
}
TEST_F(ReadingListSyncBridgeTest,
EntityDataShouldBeNotValidIfTitleContainsNonUTF8) {
auto entry = base::MakeRefCounted<ReadingListEntry>(
GURL("http://example.com/"), "example title", AdvanceAndGetTime(&clock_));
std::unique_ptr<sync_pb::ReadingListSpecifics> specifics =
entry->AsReadingListSpecifics();
*specifics->mutable_title() = "\xFC\x9C\xBF\x80\xBF\x80";
syncer::EntityData data;
*data.specifics.mutable_reading_list() = *specifics;
EXPECT_FALSE(bridge()->IsEntityDataValid(data));
}