1// Copyright 2014 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 "base/bind.h"
6#include "base/message_loop/message_loop.h"
7#include "base/run_loop.h"
8#include "components/dom_distiller/core/article_entry.h"
9#include "components/dom_distiller/core/distilled_content_store.h"
10#include "components/dom_distiller/core/proto/distilled_article.pb.h"
11#include "testing/gtest/include/gtest/gtest.h"
12
13namespace dom_distiller {
14
15namespace {
16
17ArticleEntry CreateEntry(std::string entry_id,
18                         std::string page_url1,
19                         std::string page_url2,
20                         std::string page_url3) {
21  ArticleEntry entry;
22  entry.set_entry_id(entry_id);
23  if (!page_url1.empty()) {
24    ArticleEntryPage* page = entry.add_pages();
25    page->set_url(page_url1);
26  }
27  if (!page_url2.empty()) {
28    ArticleEntryPage* page = entry.add_pages();
29    page->set_url(page_url2);
30  }
31  if (!page_url3.empty()) {
32    ArticleEntryPage* page = entry.add_pages();
33    page->set_url(page_url3);
34  }
35  return entry;
36}
37
38DistilledArticleProto CreateDistilledArticleForEntry(
39    const ArticleEntry& entry) {
40  DistilledArticleProto article;
41  for (int i = 0; i < entry.pages_size(); ++i) {
42    DistilledPageProto* page = article.add_pages();
43    page->set_url(entry.pages(i).url());
44    page->set_html("<div>" + entry.pages(i).url() + "</div>");
45  }
46  return article;
47}
48
49}  // namespace
50
51class InMemoryContentStoreTest : public testing::Test {
52 public:
53  void OnLoadCallback(bool success, scoped_ptr<DistilledArticleProto> proto) {
54    load_success_ = success;
55    loaded_proto_ = proto.Pass();
56  }
57
58  void OnSaveCallback(bool success) { save_success_ = success; }
59
60 protected:
61  // testing::Test implementation:
62  virtual void SetUp() OVERRIDE {
63    store_.reset(new InMemoryContentStore(kDefaultMaxNumCachedEntries));
64    save_success_ = false;
65    load_success_ = false;
66    loaded_proto_.reset();
67  }
68
69  scoped_ptr<InMemoryContentStore> store_;
70  bool save_success_;
71  bool load_success_;
72  scoped_ptr<DistilledArticleProto> loaded_proto_;
73};
74
75// Tests whether saving and then loading a single article works as expected.
76TEST_F(InMemoryContentStoreTest, SaveAndLoadSingleArticle) {
77  base::MessageLoop loop;
78  const ArticleEntry entry = CreateEntry("test-id", "url1", "url2", "url3");
79  const DistilledArticleProto stored_proto =
80      CreateDistilledArticleForEntry(entry);
81  store_->SaveContent(entry,
82                      stored_proto,
83                      base::Bind(&InMemoryContentStoreTest::OnSaveCallback,
84                                 base::Unretained(this)));
85  base::MessageLoop::current()->RunUntilIdle();
86  EXPECT_TRUE(save_success_);
87  save_success_ = false;
88
89  store_->LoadContent(entry,
90                      base::Bind(&InMemoryContentStoreTest::OnLoadCallback,
91                                 base::Unretained(this)));
92  base::MessageLoop::current()->RunUntilIdle();
93  EXPECT_TRUE(load_success_);
94  EXPECT_EQ(stored_proto.SerializeAsString(),
95            loaded_proto_->SerializeAsString());
96}
97
98// Tests that loading articles which have never been stored, yields a callback
99// where success is false.
100TEST_F(InMemoryContentStoreTest, LoadNonExistentArticle) {
101  base::MessageLoop loop;
102  const ArticleEntry entry = CreateEntry("bogus-id", "url1", "url2", "url3");
103  store_->LoadContent(entry,
104                      base::Bind(&InMemoryContentStoreTest::OnLoadCallback,
105                                 base::Unretained(this)));
106  base::MessageLoop::current()->RunUntilIdle();
107  EXPECT_FALSE(load_success_);
108}
109
110// Verifies that content store can store multiple articles, and that ordering
111// of save and store does not matter when the total number of articles does not
112// exceed |kDefaultMaxNumCachedEntries|.
113TEST_F(InMemoryContentStoreTest, SaveAndLoadMultipleArticles) {
114  base::MessageLoop loop;
115  // Store first article.
116  const ArticleEntry first_entry = CreateEntry("first", "url1", "url2", "url3");
117  const DistilledArticleProto first_stored_proto =
118      CreateDistilledArticleForEntry(first_entry);
119  store_->SaveContent(first_entry,
120                      first_stored_proto,
121                      base::Bind(&InMemoryContentStoreTest::OnSaveCallback,
122                                 base::Unretained(this)));
123  base::MessageLoop::current()->RunUntilIdle();
124  EXPECT_TRUE(save_success_);
125  save_success_ = false;
126
127  // Store second article.
128  const ArticleEntry second_entry =
129      CreateEntry("second", "url4", "url5", "url6");
130  const DistilledArticleProto second_stored_proto =
131      CreateDistilledArticleForEntry(second_entry);
132  store_->SaveContent(second_entry,
133                      second_stored_proto,
134                      base::Bind(&InMemoryContentStoreTest::OnSaveCallback,
135                                 base::Unretained(this)));
136  base::MessageLoop::current()->RunUntilIdle();
137  EXPECT_TRUE(save_success_);
138  save_success_ = false;
139
140  // Load second article.
141  store_->LoadContent(second_entry,
142                      base::Bind(&InMemoryContentStoreTest::OnLoadCallback,
143                                 base::Unretained(this)));
144  base::MessageLoop::current()->RunUntilIdle();
145  EXPECT_TRUE(load_success_);
146  load_success_ = false;
147  EXPECT_EQ(second_stored_proto.SerializeAsString(),
148            loaded_proto_->SerializeAsString());
149  loaded_proto_.reset();
150
151  // Load first article.
152  store_->LoadContent(first_entry,
153                      base::Bind(&InMemoryContentStoreTest::OnLoadCallback,
154                                 base::Unretained(this)));
155  base::MessageLoop::current()->RunUntilIdle();
156  EXPECT_TRUE(load_success_);
157  EXPECT_EQ(first_stored_proto.SerializeAsString(),
158            loaded_proto_->SerializeAsString());
159}
160
161// Verifies that the content store does not store unlimited number of articles,
162// but expires the oldest ones when the limit for number of articles is reached.
163TEST_F(InMemoryContentStoreTest, SaveAndLoadMoreThanMaxArticles) {
164  base::MessageLoop loop;
165
166  // Create a new store with only |kMaxNumArticles| articles as the limit.
167  const int kMaxNumArticles = 3;
168  store_.reset(new InMemoryContentStore(kMaxNumArticles));
169
170  // Store first article.
171  const ArticleEntry first_entry = CreateEntry("first", "url1", "url2", "url3");
172  const DistilledArticleProto first_stored_proto =
173      CreateDistilledArticleForEntry(first_entry);
174  store_->SaveContent(first_entry,
175                      first_stored_proto,
176                      base::Bind(&InMemoryContentStoreTest::OnSaveCallback,
177                                 base::Unretained(this)));
178  base::MessageLoop::current()->RunUntilIdle();
179  EXPECT_TRUE(save_success_);
180  save_success_ = false;
181
182  // Store second article.
183  const ArticleEntry second_entry =
184      CreateEntry("second", "url4", "url5", "url6");
185  const DistilledArticleProto second_stored_proto =
186      CreateDistilledArticleForEntry(second_entry);
187  store_->SaveContent(second_entry,
188                      second_stored_proto,
189                      base::Bind(&InMemoryContentStoreTest::OnSaveCallback,
190                                 base::Unretained(this)));
191  base::MessageLoop::current()->RunUntilIdle();
192  EXPECT_TRUE(save_success_);
193  save_success_ = false;
194
195  // Store third article.
196  const ArticleEntry third_entry = CreateEntry("third", "url7", "url8", "url9");
197  const DistilledArticleProto third_stored_proto =
198      CreateDistilledArticleForEntry(third_entry);
199  store_->SaveContent(third_entry,
200                      third_stored_proto,
201                      base::Bind(&InMemoryContentStoreTest::OnSaveCallback,
202                                 base::Unretained(this)));
203  base::MessageLoop::current()->RunUntilIdle();
204  EXPECT_TRUE(save_success_);
205  save_success_ = false;
206
207  // Load first article. This will make the first article the most recent
208  // accessed article.
209  store_->LoadContent(first_entry,
210                      base::Bind(&InMemoryContentStoreTest::OnLoadCallback,
211                                 base::Unretained(this)));
212  base::MessageLoop::current()->RunUntilIdle();
213  EXPECT_TRUE(load_success_);
214  load_success_ = false;
215  EXPECT_EQ(first_stored_proto.SerializeAsString(),
216            loaded_proto_->SerializeAsString());
217  loaded_proto_.reset();
218
219  // Store fourth article.
220  const ArticleEntry fourth_entry =
221      CreateEntry("fourth", "url10", "url11", "url12");
222  const DistilledArticleProto fourth_stored_proto =
223      CreateDistilledArticleForEntry(fourth_entry);
224  store_->SaveContent(fourth_entry,
225                      fourth_stored_proto,
226                      base::Bind(&InMemoryContentStoreTest::OnSaveCallback,
227                                 base::Unretained(this)));
228  base::MessageLoop::current()->RunUntilIdle();
229  EXPECT_TRUE(save_success_);
230  save_success_ = false;
231
232  // Load second article, which by now is the oldest accessed article, since
233  // the first article has been loaded once.
234  store_->LoadContent(second_entry,
235                      base::Bind(&InMemoryContentStoreTest::OnLoadCallback,
236                                 base::Unretained(this)));
237  base::MessageLoop::current()->RunUntilIdle();
238  // Since the store can only contain |kMaxNumArticles| entries, this load
239  // should fail.
240  EXPECT_FALSE(load_success_);
241}
242
243// Tests whether saving and then loading a single article works as expected.
244TEST_F(InMemoryContentStoreTest, LookupArticleByURL) {
245  base::MessageLoop loop;
246  const ArticleEntry entry = CreateEntry("test-id", "url1", "url2", "url3");
247  const DistilledArticleProto stored_proto =
248      CreateDistilledArticleForEntry(entry);
249  store_->SaveContent(entry,
250                      stored_proto,
251                      base::Bind(&InMemoryContentStoreTest::OnSaveCallback,
252                                 base::Unretained(this)));
253  base::MessageLoop::current()->RunUntilIdle();
254  EXPECT_TRUE(save_success_);
255  save_success_ = false;
256
257  // Create an entry where the entry ID does not match, but the first URL does.
258  const ArticleEntry lookup_entry1 = CreateEntry("lookup-id", "url1", "", "");
259  store_->LoadContent(lookup_entry1,
260                      base::Bind(&InMemoryContentStoreTest::OnLoadCallback,
261                                 base::Unretained(this)));
262  base::MessageLoop::current()->RunUntilIdle();
263  EXPECT_TRUE(load_success_);
264  EXPECT_EQ(stored_proto.SerializeAsString(),
265            loaded_proto_->SerializeAsString());
266
267  // Create an entry where the entry ID does not match, but the second URL does.
268  const ArticleEntry lookup_entry2 =
269      CreateEntry("lookup-id", "bogus", "url2", "");
270  store_->LoadContent(lookup_entry2,
271                      base::Bind(&InMemoryContentStoreTest::OnLoadCallback,
272                                 base::Unretained(this)));
273  base::MessageLoop::current()->RunUntilIdle();
274  EXPECT_TRUE(load_success_);
275  EXPECT_EQ(stored_proto.SerializeAsString(),
276            loaded_proto_->SerializeAsString());
277}
278
279// Verifies that the content store does not store unlimited number of articles,
280// but expires the oldest ones when the limit for number of articles is reached.
281TEST_F(InMemoryContentStoreTest, LoadArticleByURLAfterExpungedFromCache) {
282  base::MessageLoop loop;
283
284  // Create a new store with only |kMaxNumArticles| articles as the limit.
285  const int kMaxNumArticles = 1;
286  store_.reset(new InMemoryContentStore(kMaxNumArticles));
287
288  // Store an article.
289  const ArticleEntry first_entry = CreateEntry("first", "url1", "url2", "url3");
290  const DistilledArticleProto first_stored_proto =
291      CreateDistilledArticleForEntry(first_entry);
292  store_->SaveContent(first_entry,
293                      first_stored_proto,
294                      base::Bind(&InMemoryContentStoreTest::OnSaveCallback,
295                                 base::Unretained(this)));
296  base::MessageLoop::current()->RunUntilIdle();
297  EXPECT_TRUE(save_success_);
298  save_success_ = false;
299
300  // Looking up the first entry by URL should succeed when it is still in the
301  // cache.
302  const ArticleEntry first_entry_lookup =
303      CreateEntry("lookup-id", "url1", "", "");
304  store_->LoadContent(first_entry_lookup,
305                      base::Bind(&InMemoryContentStoreTest::OnLoadCallback,
306                                 base::Unretained(this)));
307  base::MessageLoop::current()->RunUntilIdle();
308  EXPECT_TRUE(load_success_);
309  EXPECT_EQ(first_stored_proto.SerializeAsString(),
310            loaded_proto_->SerializeAsString());
311
312  // Store second article. This will remove the first article from the cache.
313  const ArticleEntry second_entry =
314      CreateEntry("second", "url4", "url5", "url6");
315  const DistilledArticleProto second_stored_proto =
316      CreateDistilledArticleForEntry(second_entry);
317  store_->SaveContent(second_entry,
318                      second_stored_proto,
319                      base::Bind(&InMemoryContentStoreTest::OnSaveCallback,
320                                 base::Unretained(this)));
321  base::MessageLoop::current()->RunUntilIdle();
322  EXPECT_TRUE(save_success_);
323  save_success_ = false;
324
325  // Looking up the first entry by URL should fail when it is not in the cache.
326  store_->LoadContent(first_entry_lookup,
327                      base::Bind(&InMemoryContentStoreTest::OnLoadCallback,
328                                 base::Unretained(this)));
329  base::MessageLoop::current()->RunUntilIdle();
330  EXPECT_FALSE(load_success_);
331}
332
333}  // namespace dom_distiller
334