| // 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/run_loop.h" |
| #include "base/test/scoped_task_environment.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>( |
| syncer::ModelTypeStoreTestUtil::MoveStoreToFactory(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 to be able to post tasks. |
| base::test::ScopedTaskEnvironment task_environment_; |
| |
| 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); |
| } |
| } |
| } |