blob: fd5a3470cb4a6d2bd7b7ceb7f179df1e33d794cf [file] [log] [blame]
// Copyright 2016 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/reading_list/core/reading_list_store.h"
#include <map>
#include <set>
#include <utility>
#include "base/bind.h"
#include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
#include "base/test/simple_test_clock.h"
#include "components/reading_list/core/reading_list_model_impl.h"
#include "components/sync/model/mock_model_type_change_processor.h"
#include "components/sync/model/model_type_store_test_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
using testing::_;
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;
}
// 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) {
EXPECT_EQ(ReadingListStore::CompareEntriesForSync(entryA, entryB), possible);
std::unique_ptr<ReadingListEntry> a =
ReadingListEntry::FromReadingListSpecifics(entryA,
base::Time::FromTimeT(10));
std::unique_ptr<ReadingListEntry> b =
ReadingListEntry::FromReadingListSpecifics(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(ReadingListStore::CompareEntriesForSync(entryA, *mergedEntry));
EXPECT_TRUE(ReadingListStore::CompareEntriesForSync(entryB, *mergedEntry));
}
}
base::Time AdvanceAndGetTime(base::SimpleTestClock* clock) {
clock->Advance(base::TimeDelta::FromMilliseconds(10));
return clock->Now();
}
} // namespace
class FakeModelTypeChangeProcessorObserver {
public:
virtual void Put(const std::string& client_tag,
std::unique_ptr<syncer::EntityData> entity_data,
syncer::MetadataChangeList* metadata_change_list) = 0;
virtual void Delete(const std::string& client_tag,
syncer::MetadataChangeList* metadata_change_list) = 0;
};
class ReadingListStoreTest : public testing::Test,
public ReadingListStoreDelegate {
protected:
ReadingListStoreTest()
: store_(syncer::ModelTypeStoreTestUtil::CreateInMemoryStoreForTest()) {
ON_CALL(processor_, IsTrackingMetadata())
.WillByDefault(testing::Return(true));
ClearState();
reading_list_store_ = std::make_unique<ReadingListStore>(
base::BindOnce(&syncer::ModelTypeStoreTestUtil::MoveStoreToCallback,
std::move(store_)),
processor_.CreateForwardingProcessor());
model_ = std::make_unique<ReadingListModelImpl>(nullptr, nullptr, &clock_);
reading_list_store_->SetReadingListModel(model_.get(), this, &clock_);
base::RunLoop().RunUntilIdle();
}
void AssertCounts(int sync_add_called,
int sync_remove_called,
int sync_merge_called) {
EXPECT_EQ(sync_add_called, sync_add_called_);
EXPECT_EQ(sync_remove_called, sync_remove_called_);
EXPECT_EQ(sync_merge_called, sync_merge_called_);
}
void ClearState() {
sync_add_called_ = 0;
sync_remove_called_ = 0;
sync_merge_called_ = 0;
sync_added_.clear();
sync_removed_.clear();
sync_merged_.clear();
}
// These three mathods handle callbacks from a ReadingListStore.
void StoreLoaded(std::unique_ptr<ReadingListEntries> entries) override {}
// Handle sync events.
void SyncAddEntry(std::unique_ptr<ReadingListEntry> entry) override {
sync_add_called_++;
sync_added_[entry->URL().spec()] = entry->IsRead();
}
void SyncRemoveEntry(const GURL& gurl) override {
sync_remove_called_++;
sync_removed_.insert(gurl.spec());
}
ReadingListEntry* SyncMergeEntry(
std::unique_ptr<ReadingListEntry> entry) override {
sync_merge_called_++;
sync_merged_[entry->URL().spec()] = entry->IsRead();
return model_->SyncMergeEntry(std::move(entry));
}
// In memory model type store needs a MessageLoop.
base::MessageLoop message_loop_;
testing::NiceMock<syncer::MockModelTypeChangeProcessor> processor_;
std::unique_ptr<syncer::ModelTypeStore> store_;
std::unique_ptr<ReadingListModelImpl> model_;
base::SimpleTestClock clock_;
std::unique_ptr<ReadingListStore> reading_list_store_;
int sync_add_called_;
int sync_remove_called_;
int sync_merge_called_;
std::map<std::string, bool> sync_added_;
std::set<std::string> sync_removed_;
std::map<std::string, bool> sync_merged_;
};
TEST_F(ReadingListStoreTest, CheckEmpties) {
EXPECT_EQ(0ul, model_->size());
}
TEST_F(ReadingListStoreTest, SaveOneRead) {
ReadingListEntry entry(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),
_));
reading_list_store_->SaveEntry(entry);
AssertCounts(0, 0, 0);
}
TEST_F(ReadingListStoreTest, SaveOneUnread) {
ReadingListEntry entry(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),
_));
reading_list_store_->SaveEntry(entry);
AssertCounts(0, 0, 0);
}
TEST_F(ReadingListStoreTest, SyncMergeOneEntry) {
EXPECT_CALL(processor_, Put(_, _, _)).Times(0);
syncer::EntityChangeList remote_input;
ReadingListEntry entry(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.client_tag_hash = "http://read.example.com/";
*data.specifics.mutable_reading_list() = *specifics;
remote_input.push_back(syncer::EntityChange::CreateAdd(
"http://read.example.com/", data.PassToPtr()));
std::unique_ptr<syncer::MetadataChangeList> metadata_changes(
reading_list_store_->CreateMetadataChangeList());
auto error = reading_list_store_->MergeSyncData(std::move(metadata_changes),
remote_input);
AssertCounts(1, 0, 0);
EXPECT_EQ(sync_added_.size(), 1u);
EXPECT_EQ(sync_added_.count("http://read.example.com/"), 1u);
EXPECT_EQ(sync_added_["http://read.example.com/"], true);
}
TEST_F(ReadingListStoreTest, ApplySyncChangesOneAdd) {
EXPECT_CALL(processor_, Put(_, _, _)).Times(0);
ReadingListEntry entry(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.client_tag_hash = "http://read.example.com/";
*data.specifics.mutable_reading_list() = *specifics;
syncer::EntityChangeList add_changes;
add_changes.push_back(syncer::EntityChange::CreateAdd(
"http://read.example.com/", data.PassToPtr()));
auto error = reading_list_store_->ApplySyncChanges(
reading_list_store_->CreateMetadataChangeList(), add_changes);
AssertCounts(1, 0, 0);
EXPECT_EQ(sync_added_.size(), 1u);
EXPECT_EQ(sync_added_.count("http://read.example.com/"), 1u);
EXPECT_EQ(sync_added_["http://read.example.com/"], true);
}
TEST_F(ReadingListStoreTest, ApplySyncChangesOneMerge) {
AdvanceAndGetTime(&clock_);
model_->AddEntry(GURL("http://unread.example.com/"), "unread title",
reading_list::ADDED_VIA_CURRENT_APP);
ReadingListEntry new_entry(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.client_tag_hash = "http://unread.example.com/";
*data.specifics.mutable_reading_list() = *specifics;
EXPECT_CALL(processor_, Put("http://unread.example.com/", _, _));
syncer::EntityChangeList add_changes;
add_changes.push_back(syncer::EntityChange::CreateAdd(
"http://unread.example.com/", data.PassToPtr()));
auto error = reading_list_store_->ApplySyncChanges(
reading_list_store_->CreateMetadataChangeList(), add_changes);
AssertCounts(0, 0, 1);
EXPECT_EQ(sync_merged_.size(), 1u);
EXPECT_EQ(sync_merged_.count("http://unread.example.com/"), 1u);
EXPECT_EQ(sync_merged_["http://unread.example.com/"], true);
}
TEST_F(ReadingListStoreTest, ApplySyncChangesOneIgnored) {
// Read entry but with unread URL as it must update the other one.
ReadingListEntry old_entry(GURL("http://unread.example.com/"),
"old unread title", AdvanceAndGetTime(&clock_));
old_entry.SetRead(true, AdvanceAndGetTime(&clock_));
AdvanceAndGetTime(&clock_);
model_->AddEntry(GURL("http://unread.example.com/"), "new unread title",
reading_list::ADDED_VIA_CURRENT_APP);
AssertCounts(0, 0, 0);
std::unique_ptr<sync_pb::ReadingListSpecifics> specifics =
old_entry.AsReadingListSpecifics();
syncer::EntityData data;
data.client_tag_hash = "http://unread.example.com/";
*data.specifics.mutable_reading_list() = *specifics;
EXPECT_CALL(processor_, Put("http://unread.example.com/", _, _));
syncer::EntityChangeList add_changes;
add_changes.push_back(syncer::EntityChange::CreateAdd(
"http://unread.example.com/", data.PassToPtr()));
auto error = reading_list_store_->ApplySyncChanges(
reading_list_store_->CreateMetadataChangeList(), add_changes);
AssertCounts(0, 0, 1);
EXPECT_EQ(sync_merged_.size(), 1u);
}
TEST_F(ReadingListStoreTest, ApplySyncChangesOneRemove) {
syncer::EntityChangeList delete_changes;
delete_changes.push_back(
syncer::EntityChange::CreateDelete("http://read.example.com/"));
auto error = reading_list_store_->ApplySyncChanges(
reading_list_store_->CreateMetadataChangeList(), delete_changes);
AssertCounts(0, 1, 0);
EXPECT_EQ(sync_removed_.size(), 1u);
EXPECT_EQ(sync_removed_.count("http://read.example.com/"), 1u);
}
TEST_F(ReadingListStoreTest, 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_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(ReadingListStore::CompareEntriesForSync(entryA, entryB));
EXPECT_FALSE(ReadingListStore::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_update_time_us(99);
ExpectAB(entryA, entryB, true);
ExpectAB(entryB, entryA, false);
sync_pb::ReadingListSpecifics::ReadingListEntryStatus status_oder[3] = {
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);
}
}
}