1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "components/dom_distiller/core/dom_distiller_store.h"
6
7#include "base/bind.h"
8#include "base/files/file_util.h"
9#include "base/files/scoped_temp_dir.h"
10#include "base/message_loop/message_loop.h"
11#include "base/run_loop.h"
12#include "base/time/time.h"
13#include "components/dom_distiller/core/article_entry.h"
14#include "components/dom_distiller/core/dom_distiller_test_util.h"
15#include "components/leveldb_proto/testing/fake_db.h"
16#include "sync/api/attachments/attachment_id.h"
17#include "sync/internal_api/public/attachments/attachment_service_proxy_for_test.h"
18#include "sync/protocol/sync.pb.h"
19#include "testing/gmock/include/gmock/gmock.h"
20#include "testing/gtest/include/gtest/gtest.h"
21
22using base::Time;
23using leveldb_proto::test::FakeDB;
24using sync_pb::EntitySpecifics;
25using syncer::ModelType;
26using syncer::SyncChange;
27using syncer::SyncChangeList;
28using syncer::SyncChangeProcessor;
29using syncer::SyncData;
30using syncer::SyncDataList;
31using syncer::SyncError;
32using syncer::SyncErrorFactory;
33using testing::AssertionFailure;
34using testing::AssertionResult;
35using testing::AssertionSuccess;
36
37namespace dom_distiller {
38
39namespace {
40
41const ModelType kDomDistillerModelType = syncer::ARTICLES;
42
43typedef base::hash_map<std::string, ArticleEntry> EntryMap;
44
45void AddEntry(const ArticleEntry& e, EntryMap* map) {
46  (*map)[e.entry_id()] = e;
47}
48
49class FakeSyncErrorFactory : public syncer::SyncErrorFactory {
50 public:
51  virtual syncer::SyncError CreateAndUploadError(
52      const tracked_objects::Location& location,
53      const std::string& message) OVERRIDE {
54    return syncer::SyncError();
55  }
56};
57
58class FakeSyncChangeProcessor : public syncer::SyncChangeProcessor {
59 public:
60  explicit FakeSyncChangeProcessor(EntryMap* model) : model_(model) {}
61
62  virtual syncer::SyncDataList GetAllSyncData(
63      syncer::ModelType type) const OVERRIDE {
64    ADD_FAILURE() << "FakeSyncChangeProcessor::GetAllSyncData not implemented.";
65    return syncer::SyncDataList();
66  }
67
68  virtual SyncError ProcessSyncChanges(
69      const tracked_objects::Location&,
70      const syncer::SyncChangeList& changes) OVERRIDE {
71    for (SyncChangeList::const_iterator it = changes.begin();
72         it != changes.end(); ++it) {
73      AddEntry(GetEntryFromChange(*it), model_);
74    }
75    return SyncError();
76  }
77
78 private:
79  EntryMap* model_;
80};
81
82ArticleEntry CreateEntry(std::string entry_id, std::string page_url1,
83                         std::string page_url2, std::string page_url3) {
84  ArticleEntry entry;
85  entry.set_entry_id(entry_id);
86  if (!page_url1.empty()) {
87    ArticleEntryPage* page = entry.add_pages();
88    page->set_url(page_url1);
89  }
90  if (!page_url2.empty()) {
91    ArticleEntryPage* page = entry.add_pages();
92    page->set_url(page_url2);
93  }
94  if (!page_url3.empty()) {
95    ArticleEntryPage* page = entry.add_pages();
96    page->set_url(page_url3);
97  }
98  return entry;
99}
100
101ArticleEntry GetSampleEntry(int id) {
102  static ArticleEntry entries[] = {
103      CreateEntry("entry0", "example.com/1", "example.com/2", "example.com/3"),
104      CreateEntry("entry1", "example.com/1", "", ""),
105      CreateEntry("entry2", "example.com/p1", "example.com/p2", ""),
106      CreateEntry("entry3", "example.com/something/all", "", ""),
107      CreateEntry("entry4", "example.com/somethingelse/1", "", ""),
108      CreateEntry("entry5", "rock.example.com/p1", "rock.example.com/p2", ""),
109      CreateEntry("entry7", "example.com/entry7/1", "example.com/entry7/2", ""),
110      CreateEntry("entry8", "example.com/entry8/1", "", ""),
111      CreateEntry("entry9", "example.com/entry9/all", "", ""),
112  };
113  EXPECT_LT(id, 9);
114  return entries[id % 9];
115}
116
117class MockDistillerObserver : public DomDistillerObserver {
118 public:
119  MOCK_METHOD1(ArticleEntriesUpdated, void(const std::vector<ArticleUpdate>&));
120  virtual ~MockDistillerObserver() {}
121};
122
123}  // namespace
124
125class DomDistillerStoreTest : public testing::Test {
126 public:
127  virtual void SetUp() {
128    db_model_.clear();
129    sync_model_.clear();
130    store_model_.clear();
131    next_sync_id_ = 1;
132  }
133
134  virtual void TearDown() {
135    store_.reset();
136    fake_db_ = NULL;
137    fake_sync_processor_ = NULL;
138  }
139
140  // Creates a simple DomDistillerStore initialized with |store_model_| and
141  // with a FakeDB backed by |db_model_|.
142  void CreateStore() {
143    fake_db_ = new FakeDB<ArticleEntry>(&db_model_);
144    store_.reset(test::util::CreateStoreWithFakeDB(fake_db_, store_model_));
145  }
146
147  void StartSyncing() {
148    fake_sync_processor_ = new FakeSyncChangeProcessor(&sync_model_);
149
150    store_->MergeDataAndStartSyncing(
151        kDomDistillerModelType, SyncDataFromEntryMap(sync_model_),
152        make_scoped_ptr<SyncChangeProcessor>(fake_sync_processor_),
153        scoped_ptr<SyncErrorFactory>(new FakeSyncErrorFactory()));
154  }
155
156 protected:
157  SyncData CreateSyncData(const ArticleEntry& entry) {
158    EntitySpecifics specifics = SpecificsFromEntry(entry);
159    return SyncData::CreateRemoteData(
160        next_sync_id_++, specifics, Time::UnixEpoch(),
161        syncer::AttachmentIdList(),
162        syncer::AttachmentServiceProxyForTest::Create());
163  }
164
165  SyncDataList SyncDataFromEntryMap(const EntryMap& model) {
166    SyncDataList data;
167    for (EntryMap::const_iterator it = model.begin(); it != model.end(); ++it) {
168      data.push_back(CreateSyncData(it->second));
169    }
170    return data;
171  }
172
173  base::MessageLoop message_loop_;
174
175  EntryMap db_model_;
176  EntryMap sync_model_;
177  FakeDB<ArticleEntry>::EntryMap store_model_;
178
179  scoped_ptr<DomDistillerStore> store_;
180
181  // Both owned by |store_|.
182  FakeDB<ArticleEntry>* fake_db_;
183  FakeSyncChangeProcessor* fake_sync_processor_;
184
185  int64 next_sync_id_;
186};
187
188AssertionResult AreEntriesEqual(const DomDistillerStore::EntryVector& entries,
189                                EntryMap expected_entries) {
190  if (entries.size() != expected_entries.size())
191    return AssertionFailure() << "Expected " << expected_entries.size()
192                              << " entries but found " << entries.size();
193
194  for (DomDistillerStore::EntryVector::const_iterator it = entries.begin();
195       it != entries.end(); ++it) {
196    EntryMap::iterator expected_it = expected_entries.find(it->entry_id());
197    if (expected_it == expected_entries.end()) {
198      return AssertionFailure() << "Found unexpected entry with id <"
199                                << it->entry_id() << ">";
200    }
201    if (!AreEntriesEqual(expected_it->second, *it)) {
202      return AssertionFailure() << "Mismatched entry with id <"
203                                << it->entry_id() << ">";
204    }
205    expected_entries.erase(expected_it);
206  }
207  return AssertionSuccess();
208}
209
210AssertionResult AreEntryMapsEqual(const EntryMap& left, const EntryMap& right) {
211  DomDistillerStore::EntryVector entries;
212  for (EntryMap::const_iterator it = left.begin(); it != left.end(); ++it) {
213    entries.push_back(it->second);
214  }
215  return AreEntriesEqual(entries, right);
216}
217
218TEST_F(DomDistillerStoreTest, TestDatabaseLoad) {
219  AddEntry(GetSampleEntry(0), &db_model_);
220  AddEntry(GetSampleEntry(1), &db_model_);
221  AddEntry(GetSampleEntry(2), &db_model_);
222
223  CreateStore();
224
225  fake_db_->InitCallback(true);
226  EXPECT_EQ(fake_db_->GetDirectory(),
227            FakeDB<ArticleEntry>::DirectoryForTestDB());
228
229  fake_db_->LoadCallback(true);
230  EXPECT_TRUE(AreEntriesEqual(store_->GetEntries(), db_model_));
231}
232
233TEST_F(DomDistillerStoreTest, TestDatabaseLoadMerge) {
234  AddEntry(GetSampleEntry(0), &db_model_);
235  AddEntry(GetSampleEntry(1), &db_model_);
236  AddEntry(GetSampleEntry(2), &db_model_);
237
238  AddEntry(GetSampleEntry(2), &store_model_);
239  AddEntry(GetSampleEntry(3), &store_model_);
240  AddEntry(GetSampleEntry(4), &store_model_);
241
242  EntryMap expected_model(db_model_);
243  AddEntry(GetSampleEntry(3), &expected_model);
244  AddEntry(GetSampleEntry(4), &expected_model);
245
246  CreateStore();
247  fake_db_->InitCallback(true);
248  fake_db_->LoadCallback(true);
249
250  EXPECT_TRUE(AreEntriesEqual(store_->GetEntries(), expected_model));
251  EXPECT_TRUE(AreEntryMapsEqual(db_model_, expected_model));
252}
253
254TEST_F(DomDistillerStoreTest, TestAddAndRemoveEntry) {
255  CreateStore();
256  fake_db_->InitCallback(true);
257  fake_db_->LoadCallback(true);
258
259  EXPECT_TRUE(store_->GetEntries().empty());
260  EXPECT_TRUE(db_model_.empty());
261
262  store_->AddEntry(GetSampleEntry(0));
263
264  EntryMap expected_model;
265  AddEntry(GetSampleEntry(0), &expected_model);
266
267  EXPECT_TRUE(AreEntriesEqual(store_->GetEntries(), expected_model));
268  EXPECT_TRUE(AreEntryMapsEqual(db_model_, expected_model));
269
270  store_->RemoveEntry(GetSampleEntry(0));
271  expected_model.clear();
272
273  EXPECT_TRUE(AreEntriesEqual(store_->GetEntries(), expected_model));
274  EXPECT_TRUE(AreEntryMapsEqual(db_model_, expected_model));
275}
276
277TEST_F(DomDistillerStoreTest, TestAddAndUpdateEntry) {
278  CreateStore();
279  fake_db_->InitCallback(true);
280  fake_db_->LoadCallback(true);
281
282  EXPECT_TRUE(store_->GetEntries().empty());
283  EXPECT_TRUE(db_model_.empty());
284
285  store_->AddEntry(GetSampleEntry(0));
286
287  EntryMap expected_model;
288  AddEntry(GetSampleEntry(0), &expected_model);
289
290  EXPECT_TRUE(AreEntriesEqual(store_->GetEntries(), expected_model));
291  EXPECT_TRUE(AreEntryMapsEqual(db_model_, expected_model));
292
293  EXPECT_FALSE(store_->UpdateEntry(GetSampleEntry(0)));
294
295  ArticleEntry updated_entry(GetSampleEntry(0));
296  updated_entry.set_title("updated title.");
297  EXPECT_TRUE(store_->UpdateEntry(updated_entry));
298  expected_model.clear();
299  AddEntry(updated_entry, &expected_model);
300
301  EXPECT_TRUE(AreEntriesEqual(store_->GetEntries(), expected_model));
302  EXPECT_TRUE(AreEntryMapsEqual(db_model_, expected_model));
303
304  store_->RemoveEntry(updated_entry);
305  EXPECT_FALSE(store_->UpdateEntry(updated_entry));
306  EXPECT_FALSE(store_->UpdateEntry(GetSampleEntry(0)));
307}
308
309TEST_F(DomDistillerStoreTest, TestSyncMergeWithEmptyDatabase) {
310  AddEntry(GetSampleEntry(0), &sync_model_);
311  AddEntry(GetSampleEntry(1), &sync_model_);
312  AddEntry(GetSampleEntry(2), &sync_model_);
313
314  CreateStore();
315  fake_db_->InitCallback(true);
316  fake_db_->LoadCallback(true);
317
318  StartSyncing();
319
320  EXPECT_TRUE(AreEntriesEqual(store_->GetEntries(), sync_model_));
321  EXPECT_TRUE(AreEntryMapsEqual(db_model_, sync_model_));
322}
323
324TEST_F(DomDistillerStoreTest, TestSyncMergeAfterDatabaseLoad) {
325  AddEntry(GetSampleEntry(0), &db_model_);
326  AddEntry(GetSampleEntry(1), &db_model_);
327  AddEntry(GetSampleEntry(2), &db_model_);
328
329  AddEntry(GetSampleEntry(2), &sync_model_);
330  AddEntry(GetSampleEntry(3), &sync_model_);
331  AddEntry(GetSampleEntry(4), &sync_model_);
332
333  EntryMap expected_model(db_model_);
334  AddEntry(GetSampleEntry(3), &expected_model);
335  AddEntry(GetSampleEntry(4), &expected_model);
336
337  CreateStore();
338  fake_db_->InitCallback(true);
339  fake_db_->LoadCallback(true);
340
341  EXPECT_TRUE(AreEntriesEqual(store_->GetEntries(), db_model_));
342
343  StartSyncing();
344
345  EXPECT_TRUE(AreEntriesEqual(store_->GetEntries(), expected_model));
346  EXPECT_TRUE(AreEntryMapsEqual(db_model_, expected_model));
347  EXPECT_TRUE(AreEntryMapsEqual(sync_model_, expected_model));
348}
349
350TEST_F(DomDistillerStoreTest, TestGetAllSyncData) {
351  AddEntry(GetSampleEntry(0), &db_model_);
352  AddEntry(GetSampleEntry(1), &db_model_);
353  AddEntry(GetSampleEntry(2), &db_model_);
354
355  AddEntry(GetSampleEntry(2), &sync_model_);
356  AddEntry(GetSampleEntry(3), &sync_model_);
357  AddEntry(GetSampleEntry(4), &sync_model_);
358
359  EntryMap expected_model(db_model_);
360  AddEntry(GetSampleEntry(3), &expected_model);
361  AddEntry(GetSampleEntry(4), &expected_model);
362
363  CreateStore();
364
365  fake_db_->InitCallback(true);
366  fake_db_->LoadCallback(true);
367
368  StartSyncing();
369
370  SyncDataList data = store_->GetAllSyncData(kDomDistillerModelType);
371  DomDistillerStore::EntryVector entries;
372  for (SyncDataList::iterator it = data.begin(); it != data.end(); ++it) {
373    entries.push_back(EntryFromSpecifics(it->GetSpecifics()));
374  }
375  EXPECT_TRUE(AreEntriesEqual(entries, expected_model));
376}
377
378TEST_F(DomDistillerStoreTest, TestProcessSyncChanges) {
379  AddEntry(GetSampleEntry(0), &db_model_);
380  AddEntry(GetSampleEntry(1), &db_model_);
381  sync_model_ = db_model_;
382
383  EntryMap expected_model(db_model_);
384  AddEntry(GetSampleEntry(2), &expected_model);
385  AddEntry(GetSampleEntry(3), &expected_model);
386
387  CreateStore();
388
389  fake_db_->InitCallback(true);
390  fake_db_->LoadCallback(true);
391
392  StartSyncing();
393
394  SyncChangeList changes;
395  changes.push_back(SyncChange(FROM_HERE, SyncChange::ACTION_ADD,
396                               CreateSyncData(GetSampleEntry(2))));
397  changes.push_back(SyncChange(FROM_HERE, SyncChange::ACTION_ADD,
398                               CreateSyncData(GetSampleEntry(3))));
399
400  store_->ProcessSyncChanges(FROM_HERE, changes);
401
402  EXPECT_TRUE(AreEntriesEqual(store_->GetEntries(), expected_model));
403  EXPECT_TRUE(AreEntryMapsEqual(db_model_, expected_model));
404}
405
406TEST_F(DomDistillerStoreTest, TestSyncMergeWithSecondDomDistillerStore) {
407  AddEntry(GetSampleEntry(0), &db_model_);
408  AddEntry(GetSampleEntry(1), &db_model_);
409  AddEntry(GetSampleEntry(2), &db_model_);
410
411  EntryMap other_db_model;
412  AddEntry(GetSampleEntry(2), &other_db_model);
413  AddEntry(GetSampleEntry(3), &other_db_model);
414  AddEntry(GetSampleEntry(4), &other_db_model);
415
416  EntryMap expected_model(db_model_);
417  AddEntry(GetSampleEntry(3), &expected_model);
418  AddEntry(GetSampleEntry(4), &expected_model);
419
420  CreateStore();
421
422  fake_db_->InitCallback(true);
423  fake_db_->LoadCallback(true);
424
425  FakeDB<ArticleEntry>* other_fake_db =
426      new FakeDB<ArticleEntry>(&other_db_model);
427  scoped_ptr<DomDistillerStore> owned_other_store(new DomDistillerStore(
428      scoped_ptr<leveldb_proto::ProtoDatabase<ArticleEntry> >(other_fake_db),
429      std::vector<ArticleEntry>(),
430      base::FilePath(FILE_PATH_LITERAL("/fake/other/path"))));
431  DomDistillerStore* other_store = owned_other_store.get();
432  other_fake_db->InitCallback(true);
433  other_fake_db->LoadCallback(true);
434
435  EXPECT_FALSE(AreEntriesEqual(store_->GetEntries(), expected_model));
436  EXPECT_FALSE(AreEntriesEqual(other_store->GetEntries(), expected_model));
437  ASSERT_TRUE(AreEntriesEqual(other_store->GetEntries(), other_db_model));
438
439  FakeSyncErrorFactory* other_error_factory = new FakeSyncErrorFactory();
440  store_->MergeDataAndStartSyncing(
441      kDomDistillerModelType, SyncDataFromEntryMap(other_db_model),
442      owned_other_store.PassAs<SyncChangeProcessor>(),
443      make_scoped_ptr<SyncErrorFactory>(other_error_factory));
444
445  EXPECT_TRUE(AreEntriesEqual(store_->GetEntries(), expected_model));
446  EXPECT_TRUE(AreEntriesEqual(other_store->GetEntries(), expected_model));
447}
448
449TEST_F(DomDistillerStoreTest, TestObserver) {
450  CreateStore();
451  MockDistillerObserver observer;
452  store_->AddObserver(&observer);
453  fake_db_->InitCallback(true);
454  fake_db_->LoadCallback(true);
455  std::vector<DomDistillerObserver::ArticleUpdate> expected_updates;
456  DomDistillerObserver::ArticleUpdate update;
457  update.entry_id = GetSampleEntry(0).entry_id();
458  update.update_type = DomDistillerObserver::ArticleUpdate::ADD;
459  expected_updates.push_back(update);
460  EXPECT_CALL(observer, ArticleEntriesUpdated(
461                            test::util::HasExpectedUpdates(expected_updates)));
462  store_->AddEntry(GetSampleEntry(0));
463
464  expected_updates.clear();
465  update.entry_id = GetSampleEntry(1).entry_id();
466  update.update_type = DomDistillerObserver::ArticleUpdate::ADD;
467  expected_updates.push_back(update);
468  EXPECT_CALL(observer, ArticleEntriesUpdated(
469                            test::util::HasExpectedUpdates(expected_updates)));
470  store_->AddEntry(GetSampleEntry(1));
471
472  expected_updates.clear();
473  update.entry_id = GetSampleEntry(0).entry_id();
474  update.update_type = DomDistillerObserver::ArticleUpdate::REMOVE;
475  expected_updates.clear();
476  expected_updates.push_back(update);
477  EXPECT_CALL(observer, ArticleEntriesUpdated(
478                            test::util::HasExpectedUpdates(expected_updates)));
479  store_->RemoveEntry(GetSampleEntry(0));
480
481  // Add entry_id = 3 and update entry_id = 1.
482  expected_updates.clear();
483  SyncDataList change_data;
484  change_data.push_back(CreateSyncData(GetSampleEntry(3)));
485  ArticleEntry updated_entry(GetSampleEntry(1));
486  updated_entry.set_title("changed_title");
487  change_data.push_back(CreateSyncData(updated_entry));
488  update.entry_id = GetSampleEntry(3).entry_id();
489  update.update_type = DomDistillerObserver::ArticleUpdate::ADD;
490  expected_updates.push_back(update);
491  update.entry_id = GetSampleEntry(1).entry_id();
492  update.update_type = DomDistillerObserver::ArticleUpdate::UPDATE;
493  expected_updates.push_back(update);
494  EXPECT_CALL(observer, ArticleEntriesUpdated(
495                            test::util::HasExpectedUpdates(expected_updates)));
496
497  FakeSyncErrorFactory* fake_error_factory = new FakeSyncErrorFactory();
498  EntryMap fake_model;
499  FakeSyncChangeProcessor* fake_sync_change_processor =
500      new FakeSyncChangeProcessor(&fake_model);
501  store_->MergeDataAndStartSyncing(
502      kDomDistillerModelType, change_data,
503      make_scoped_ptr<SyncChangeProcessor>(fake_sync_change_processor),
504      make_scoped_ptr<SyncErrorFactory>(fake_error_factory));
505}
506
507}  // namespace dom_distiller
508