1// Copyright (c) 2011 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 <string>
6#include <utility>
7
8#include "base/basictypes.h"
9#include "base/compiler_specific.h"
10#include "base/file_path.h"
11#include "base/file_util.h"
12#include "base/memory/scoped_ptr.h"
13#include "base/memory/scoped_temp_dir.h"
14#include "base/path_service.h"
15#include "base/string16.h"
16#include "base/utf_string_conversions.h"
17#include "chrome/browser/bookmarks/bookmark_model.h"
18#include "chrome/browser/history/archived_database.h"
19#include "chrome/browser/history/expire_history_backend.h"
20#include "chrome/browser/history/history_database.h"
21#include "chrome/browser/history/history_notifications.h"
22#include "chrome/browser/history/text_database_manager.h"
23#include "chrome/browser/history/thumbnail_database.h"
24#include "chrome/browser/history/top_sites.h"
25#include "chrome/common/thumbnail_score.h"
26#include "chrome/test/testing_profile.h"
27#include "chrome/tools/profiles/thumbnail-inl.h"
28#include "content/browser/browser_thread.h"
29#include "testing/gtest/include/gtest/gtest.h"
30#include "third_party/skia/include/core/SkBitmap.h"
31#include "ui/gfx/codec/jpeg_codec.h"
32
33using base::Time;
34using base::TimeDelta;
35using base::TimeTicks;
36
37// Filename constants.
38static const FilePath::CharType kHistoryFile[] = FILE_PATH_LITERAL("History");
39static const FilePath::CharType kArchivedHistoryFile[] =
40    FILE_PATH_LITERAL("Archived History");
41static const FilePath::CharType kThumbnailFile[] =
42    FILE_PATH_LITERAL("Thumbnails");
43
44// The test must be in the history namespace for the gtest forward declarations
45// to work. It also eliminates a bunch of ugly "history::".
46namespace history {
47
48// ExpireHistoryTest -----------------------------------------------------------
49
50class ExpireHistoryTest : public testing::Test,
51                          public BroadcastNotificationDelegate {
52 public:
53  ExpireHistoryTest()
54      : bookmark_model_(NULL),
55        ui_thread_(BrowserThread::UI, &message_loop_),
56        db_thread_(BrowserThread::DB, &message_loop_),
57        ALLOW_THIS_IN_INITIALIZER_LIST(expirer_(this, &bookmark_model_)),
58        now_(Time::Now()) {
59  }
60
61 protected:
62  // Called by individual tests when they want data populated.
63  void AddExampleData(URLID url_ids[3], Time visit_times[4]);
64  // Add visits with source information.
65  void AddExampleSourceData(const GURL& url, URLID* id);
66
67  // Returns true if the given favicon/thumanil has an entry in the DB.
68  bool HasFavicon(FaviconID favicon_id);
69  bool HasThumbnail(URLID url_id);
70
71  FaviconID GetFavicon(const GURL& page_url, IconType icon_type);
72
73  // Returns the number of text matches for the given URL in the example data
74  // added by AddExampleData.
75  int CountTextMatchesForURL(const GURL& url);
76
77  // EXPECTs that each URL-specific history thing (basically, everything but
78  // favicons) is gone.
79  void EnsureURLInfoGone(const URLRow& row);
80
81  // Clears the list of notifications received.
82  void ClearLastNotifications() {
83    for (size_t i = 0; i < notifications_.size(); i++)
84      delete notifications_[i].second;
85    notifications_.clear();
86  }
87
88  void StarURL(const GURL& url) {
89    bookmark_model_.AddURL(
90        bookmark_model_.GetBookmarkBarNode(), 0, string16(), url);
91  }
92
93  static bool IsStringInFile(const FilePath& filename, const char* str);
94
95  // Returns the path the db files are created in.
96  const FilePath& path() const { return tmp_dir_.path(); }
97
98  // This must be destroyed last.
99  ScopedTempDir tmp_dir_;
100
101  BookmarkModel bookmark_model_;
102
103  MessageLoopForUI message_loop_;
104  BrowserThread ui_thread_;
105  BrowserThread db_thread_;
106
107  ExpireHistoryBackend expirer_;
108
109  scoped_ptr<HistoryDatabase> main_db_;
110  scoped_ptr<ArchivedDatabase> archived_db_;
111  scoped_ptr<ThumbnailDatabase> thumb_db_;
112  scoped_ptr<TextDatabaseManager> text_db_;
113  TestingProfile profile_;
114  scoped_refptr<TopSites> top_sites_;
115
116  // Time at the beginning of the test, so everybody agrees what "now" is.
117  const Time now_;
118
119  // Notifications intended to be broadcast, we can check these values to make
120  // sure that the deletor is doing the correct broadcasts. We own the details
121  // pointers.
122  typedef std::vector< std::pair<NotificationType, HistoryDetails*> >
123      NotificationList;
124  NotificationList notifications_;
125
126 private:
127  void SetUp() {
128    ASSERT_TRUE(tmp_dir_.CreateUniqueTempDir());
129
130    FilePath history_name = path().Append(kHistoryFile);
131    main_db_.reset(new HistoryDatabase);
132    if (main_db_->Init(history_name, FilePath()) != sql::INIT_OK)
133      main_db_.reset();
134
135    FilePath archived_name = path().Append(kArchivedHistoryFile);
136    archived_db_.reset(new ArchivedDatabase);
137    if (!archived_db_->Init(archived_name))
138      archived_db_.reset();
139
140    FilePath thumb_name = path().Append(kThumbnailFile);
141    thumb_db_.reset(new ThumbnailDatabase);
142    if (thumb_db_->Init(thumb_name, NULL, main_db_.get()) != sql::INIT_OK)
143      thumb_db_.reset();
144
145    text_db_.reset(new TextDatabaseManager(path(),
146                                           main_db_.get(), main_db_.get()));
147    if (!text_db_->Init(NULL))
148      text_db_.reset();
149
150    expirer_.SetDatabases(main_db_.get(), archived_db_.get(), thumb_db_.get(),
151                          text_db_.get());
152    profile_.CreateTopSites();
153    profile_.BlockUntilTopSitesLoaded();
154    top_sites_ = profile_.GetTopSites();
155  }
156
157  void TearDown() {
158    top_sites_ = NULL;
159
160    ClearLastNotifications();
161
162    expirer_.SetDatabases(NULL, NULL, NULL, NULL);
163
164    main_db_.reset();
165    archived_db_.reset();
166    thumb_db_.reset();
167    text_db_.reset();
168  }
169
170  // BroadcastNotificationDelegate implementation.
171  void BroadcastNotifications(NotificationType type,
172                              HistoryDetails* details_deleted) {
173    // This gets called when there are notifications to broadcast. Instead, we
174    // store them so we can tell that the correct notifications were sent.
175    notifications_.push_back(std::make_pair(type, details_deleted));
176  }
177};
178
179// The example data consists of 4 visits. The middle two visits are to the
180// same URL, while the first and last are for unique ones. This allows a test
181// for the oldest or newest to include both a URL that should get totally
182// deleted (the one on the end) with one that should only get a visit deleted
183// (with the one in the middle) when it picks the proper threshold time.
184//
185// Each visit has indexed data, each URL has thumbnail. The first two URLs will
186// share the same favicon, while the last one will have a unique favicon. The
187// second visit for the middle URL is typed.
188//
189// The IDs of the added URLs, and the times of the four added visits will be
190// added to the given arrays.
191void ExpireHistoryTest::AddExampleData(URLID url_ids[3], Time visit_times[4]) {
192  if (!main_db_.get() || !text_db_.get())
193    return;
194
195  // Four times for each visit.
196  visit_times[3] = Time::Now();
197  visit_times[2] = visit_times[3] - TimeDelta::FromDays(1);
198  visit_times[1] = visit_times[3] - TimeDelta::FromDays(2);
199  visit_times[0] = visit_times[3] - TimeDelta::FromDays(3);
200
201  // Two favicons. The first two URLs will share the same one, while the last
202  // one will have a unique favicon.
203  FaviconID favicon1 = thumb_db_->AddFavicon(GURL("http://favicon/url1"),
204                                             FAVICON);
205  FaviconID favicon2 = thumb_db_->AddFavicon(GURL("http://favicon/url2"),
206                                             FAVICON);
207
208  // Three URLs.
209  URLRow url_row1(GURL("http://www.google.com/1"));
210  url_row1.set_last_visit(visit_times[0]);
211  url_row1.set_visit_count(1);
212  url_ids[0] = main_db_->AddURL(url_row1);
213  thumb_db_->AddIconMapping(url_row1.url(), favicon1);
214
215  URLRow url_row2(GURL("http://www.google.com/2"));
216  url_row2.set_last_visit(visit_times[2]);
217  url_row2.set_visit_count(2);
218  url_row2.set_typed_count(1);
219  url_ids[1] = main_db_->AddURL(url_row2);
220  thumb_db_->AddIconMapping(url_row2.url(), favicon1);
221
222  URLRow url_row3(GURL("http://www.google.com/3"));
223  url_row3.set_last_visit(visit_times[3]);
224  url_row3.set_visit_count(1);
225  url_ids[2] = main_db_->AddURL(url_row3);
226  thumb_db_->AddIconMapping(url_row3.url(), favicon2);
227
228  // Thumbnails for each URL.
229  scoped_ptr<SkBitmap> thumbnail(
230      gfx::JPEGCodec::Decode(kGoogleThumbnail, sizeof(kGoogleThumbnail)));
231  ThumbnailScore score(0.25, true, true, Time::Now());
232
233  Time time;
234  GURL gurl;
235  top_sites_->SetPageThumbnail(url_row1.url(), *thumbnail, score);
236  top_sites_->SetPageThumbnail(url_row2.url(), *thumbnail, score);
237  top_sites_->SetPageThumbnail(url_row3.url(), *thumbnail, score);
238
239  // Four visits.
240  VisitRow visit_row1;
241  visit_row1.url_id = url_ids[0];
242  visit_row1.visit_time = visit_times[0];
243  visit_row1.is_indexed = true;
244  main_db_->AddVisit(&visit_row1, SOURCE_BROWSED);
245
246  VisitRow visit_row2;
247  visit_row2.url_id = url_ids[1];
248  visit_row2.visit_time = visit_times[1];
249  visit_row2.is_indexed = true;
250  main_db_->AddVisit(&visit_row2, SOURCE_BROWSED);
251
252  VisitRow visit_row3;
253  visit_row3.url_id = url_ids[1];
254  visit_row3.visit_time = visit_times[2];
255  visit_row3.is_indexed = true;
256  visit_row3.transition = PageTransition::TYPED;
257  main_db_->AddVisit(&visit_row3, SOURCE_BROWSED);
258
259  VisitRow visit_row4;
260  visit_row4.url_id = url_ids[2];
261  visit_row4.visit_time = visit_times[3];
262  visit_row4.is_indexed = true;
263  main_db_->AddVisit(&visit_row4, SOURCE_BROWSED);
264
265  // Full text index for each visit.
266  text_db_->AddPageData(url_row1.url(), visit_row1.url_id, visit_row1.visit_id,
267                        visit_row1.visit_time, UTF8ToUTF16("title"),
268                        UTF8ToUTF16("body"));
269
270  text_db_->AddPageData(url_row2.url(), visit_row2.url_id, visit_row2.visit_id,
271                        visit_row2.visit_time, UTF8ToUTF16("title"),
272                        UTF8ToUTF16("body"));
273  text_db_->AddPageData(url_row2.url(), visit_row3.url_id, visit_row3.visit_id,
274                        visit_row3.visit_time, UTF8ToUTF16("title"),
275                        UTF8ToUTF16("body"));
276
277  // Note the special text in this URL. We'll search the file for this string
278  // to make sure it doesn't hang around after the delete.
279  text_db_->AddPageData(url_row3.url(), visit_row4.url_id, visit_row4.visit_id,
280                        visit_row4.visit_time, UTF8ToUTF16("title"),
281                        UTF8ToUTF16("goats body"));
282}
283
284void ExpireHistoryTest::AddExampleSourceData(const GURL& url, URLID* id) {
285  if (!main_db_.get())
286    return;
287
288  Time last_visit_time = Time::Now();
289  // Add one URL.
290  URLRow url_row1(url);
291  url_row1.set_last_visit(last_visit_time);
292  url_row1.set_visit_count(4);
293  URLID url_id = main_db_->AddURL(url_row1);
294  *id = url_id;
295
296  // Four times for each visit.
297  VisitRow visit_row1(url_id, last_visit_time - TimeDelta::FromDays(4), 0,
298                      PageTransition::TYPED, 0);
299  main_db_->AddVisit(&visit_row1, SOURCE_SYNCED);
300
301  VisitRow visit_row2(url_id, last_visit_time - TimeDelta::FromDays(3), 0,
302                      PageTransition::TYPED, 0);
303  main_db_->AddVisit(&visit_row2, SOURCE_BROWSED);
304
305  VisitRow visit_row3(url_id, last_visit_time - TimeDelta::FromDays(2), 0,
306                      PageTransition::TYPED, 0);
307  main_db_->AddVisit(&visit_row3, SOURCE_EXTENSION);
308
309  VisitRow visit_row4(url_id, last_visit_time, 0, PageTransition::TYPED, 0);
310  main_db_->AddVisit(&visit_row4, SOURCE_FIREFOX_IMPORTED);
311}
312
313bool ExpireHistoryTest::HasFavicon(FaviconID favicon_id) {
314  if (!thumb_db_.get() || favicon_id == 0)
315    return false;
316  Time last_updated;
317  std::vector<unsigned char> icon_data_unused;
318  GURL icon_url;
319  return thumb_db_->GetFavicon(favicon_id, &last_updated, &icon_data_unused,
320                               &icon_url);
321}
322
323FaviconID ExpireHistoryTest::GetFavicon(const GURL& page_url,
324                                        IconType icon_type) {
325  IconMapping icon_mapping;
326  thumb_db_->GetIconMappingForPageURL(page_url, icon_type, &icon_mapping);
327  return icon_mapping.icon_id;
328}
329
330bool ExpireHistoryTest::HasThumbnail(URLID url_id) {
331  // TODO(sky): fix this. This test isn't really valid for TopSites. For
332  // TopSites we should be checking URL always, not the id.
333  URLRow info;
334  if (!main_db_->GetURLRow(url_id, &info))
335    return false;
336  GURL url = info.url();
337  scoped_refptr<RefCountedBytes> data;
338  return top_sites_->GetPageThumbnail(url, &data);
339}
340
341int ExpireHistoryTest::CountTextMatchesForURL(const GURL& url) {
342  if (!text_db_.get())
343    return 0;
344
345  // "body" should match all pages in the example data.
346  std::vector<TextDatabase::Match> results;
347  QueryOptions options;
348  Time first_time;
349  text_db_->GetTextMatches(UTF8ToUTF16("body"), options,
350                           &results, &first_time);
351
352  int count = 0;
353  for (size_t i = 0; i < results.size(); i++) {
354    if (results[i].url == url)
355      count++;
356  }
357  return count;
358}
359
360void ExpireHistoryTest::EnsureURLInfoGone(const URLRow& row) {
361  // Verify the URL no longer exists.
362  URLRow temp_row;
363  EXPECT_FALSE(main_db_->GetURLRow(row.id(), &temp_row));
364
365  // The indexed data should be gone.
366  EXPECT_EQ(0, CountTextMatchesForURL(row.url()));
367
368  // There should be no visits.
369  VisitVector visits;
370  main_db_->GetVisitsForURL(row.id(), &visits);
371  EXPECT_EQ(0U, visits.size());
372
373  // Thumbnail should be gone.
374  // TODO(sky): fix this, see comment in HasThumbnail.
375  // EXPECT_FALSE(HasThumbnail(row.id()));
376
377  // Check the notifications. There should be a delete notification with this
378  // URL in it. There should also be a "typed URL changed" notification if the
379  // row is marked typed.
380  bool found_delete_notification = false;
381  bool found_typed_changed_notification = false;
382  for (size_t i = 0; i < notifications_.size(); i++) {
383    if (notifications_[i].first == NotificationType::HISTORY_URLS_DELETED) {
384      const URLsDeletedDetails* deleted_details =
385          reinterpret_cast<URLsDeletedDetails*>(notifications_[i].second);
386      if (deleted_details->urls.find(row.url()) !=
387          deleted_details->urls.end()) {
388        found_delete_notification = true;
389      }
390    } else if (notifications_[i].first ==
391               NotificationType::HISTORY_TYPED_URLS_MODIFIED) {
392      // See if we got a typed URL changed notification.
393      const URLsModifiedDetails* modified_details =
394          reinterpret_cast<URLsModifiedDetails*>(notifications_[i].second);
395      for (size_t cur_url = 0; cur_url < modified_details->changed_urls.size();
396           cur_url++) {
397        if (modified_details->changed_urls[cur_url].url() == row.url())
398          found_typed_changed_notification = true;
399      }
400    } else if (notifications_[i].first ==
401               NotificationType::HISTORY_URL_VISITED) {
402      // See if we got a visited URL notification.
403      const URLVisitedDetails* visited_details =
404          reinterpret_cast<URLVisitedDetails*>(notifications_[i].second);
405      if (visited_details->row.url() == row.url())
406          found_typed_changed_notification = true;
407    }
408  }
409  EXPECT_TRUE(found_delete_notification);
410  EXPECT_EQ(row.typed_count() > 0, found_typed_changed_notification);
411}
412
413TEST_F(ExpireHistoryTest, DeleteFaviconsIfPossible) {
414  // Add a favicon record.
415  const GURL favicon_url("http://www.google.com/favicon.ico");
416  FaviconID icon_id = thumb_db_->AddFavicon(favicon_url, FAVICON);
417  EXPECT_TRUE(icon_id);
418  EXPECT_TRUE(HasFavicon(icon_id));
419
420  // The favicon should be deletable with no users.
421  std::set<FaviconID> favicon_set;
422  favicon_set.insert(icon_id);
423  expirer_.DeleteFaviconsIfPossible(favicon_set);
424  EXPECT_FALSE(HasFavicon(icon_id));
425
426  // Add back the favicon.
427  icon_id = thumb_db_->AddFavicon(favicon_url, TOUCH_ICON);
428  EXPECT_TRUE(icon_id);
429  EXPECT_TRUE(HasFavicon(icon_id));
430
431  // Add a page that references the favicon.
432  URLRow row(GURL("http://www.google.com/2"));
433  row.set_visit_count(1);
434  EXPECT_TRUE(main_db_->AddURL(row));
435  thumb_db_->AddIconMapping(row.url(), icon_id);
436
437  // Favicon should not be deletable.
438  favicon_set.clear();
439  favicon_set.insert(icon_id);
440  expirer_.DeleteFaviconsIfPossible(favicon_set);
441  EXPECT_TRUE(HasFavicon(icon_id));
442}
443
444// static
445bool ExpireHistoryTest::IsStringInFile(const FilePath& filename,
446                                       const char* str) {
447  std::string contents;
448  EXPECT_TRUE(file_util::ReadFileToString(filename, &contents));
449  return contents.find(str) != std::string::npos;
450}
451
452// Deletes a URL with a favicon that it is the last referencer of, so that it
453// should also get deleted.
454// Fails near end of month. http://crbug.com/43586
455TEST_F(ExpireHistoryTest, FLAKY_DeleteURLAndFavicon) {
456  URLID url_ids[3];
457  Time visit_times[4];
458  AddExampleData(url_ids, visit_times);
459
460  // Verify things are the way we expect with a URL row, favicon, thumbnail.
461  URLRow last_row;
462  ASSERT_TRUE(main_db_->GetURLRow(url_ids[2], &last_row));
463  FaviconID favicon_id = GetFavicon(last_row.url(), FAVICON);
464  EXPECT_TRUE(HasFavicon(favicon_id));
465  // TODO(sky): fix this, see comment in HasThumbnail.
466  // EXPECT_TRUE(HasThumbnail(url_ids[2]));
467
468  VisitVector visits;
469  main_db_->GetVisitsForURL(url_ids[2], &visits);
470  ASSERT_EQ(1U, visits.size());
471  EXPECT_EQ(1, CountTextMatchesForURL(last_row.url()));
472
473  // In this test we also make sure that any pending entries in the text
474  // database manager are removed.
475  text_db_->AddPageURL(last_row.url(), last_row.id(), visits[0].visit_id,
476                       visits[0].visit_time);
477
478  // Compute the text DB filename.
479  FilePath fts_filename = path().Append(
480      TextDatabase::IDToFileName(text_db_->TimeToID(visit_times[3])));
481
482  // When checking the file, the database must be closed. We then re-initialize
483  // it just like the test set-up did.
484  text_db_.reset();
485  EXPECT_TRUE(IsStringInFile(fts_filename, "goats"));
486  text_db_.reset(new TextDatabaseManager(path(),
487                                         main_db_.get(), main_db_.get()));
488  ASSERT_TRUE(text_db_->Init(NULL));
489  expirer_.SetDatabases(main_db_.get(), archived_db_.get(), thumb_db_.get(),
490                        text_db_.get());
491
492  // Delete the URL and its dependencies.
493  expirer_.DeleteURL(last_row.url());
494
495  // The string should be removed from the file. FTS can mark it as gone but
496  // doesn't remove it from the file, we want to be sure we're doing the latter.
497  text_db_.reset();
498  EXPECT_FALSE(IsStringInFile(fts_filename, "goats"));
499  text_db_.reset(new TextDatabaseManager(path(),
500                                         main_db_.get(), main_db_.get()));
501  ASSERT_TRUE(text_db_->Init(NULL));
502  expirer_.SetDatabases(main_db_.get(), archived_db_.get(), thumb_db_.get(),
503                        text_db_.get());
504
505  // Run the text database expirer. This will flush any pending entries so we
506  // can check that nothing was committed. We use a time far in the future so
507  // that anything added recently will get flushed.
508  TimeTicks expiration_time = TimeTicks::Now() + TimeDelta::FromDays(1);
509  text_db_->FlushOldChangesForTime(expiration_time);
510
511  // All the normal data + the favicon should be gone.
512  EnsureURLInfoGone(last_row);
513  EXPECT_FALSE(GetFavicon(last_row.url(), FAVICON));
514  EXPECT_FALSE(HasFavicon(favicon_id));
515}
516
517// Deletes a URL with a favicon that other URLs reference, so that the favicon
518// should not get deleted. This also tests deleting more than one visit.
519TEST_F(ExpireHistoryTest, DeleteURLWithoutFavicon) {
520  URLID url_ids[3];
521  Time visit_times[4];
522  AddExampleData(url_ids, visit_times);
523
524  // Verify things are the way we expect with a URL row, favicon, thumbnail.
525  URLRow last_row;
526  ASSERT_TRUE(main_db_->GetURLRow(url_ids[1], &last_row));
527  FaviconID favicon_id = GetFavicon(last_row.url(), FAVICON);
528  EXPECT_TRUE(HasFavicon(favicon_id));
529  // TODO(sky): fix this, see comment in HasThumbnail.
530  // EXPECT_TRUE(HasThumbnail(url_ids[1]));
531
532  VisitVector visits;
533  main_db_->GetVisitsForURL(url_ids[1], &visits);
534  EXPECT_EQ(2U, visits.size());
535  EXPECT_EQ(1, CountTextMatchesForURL(last_row.url()));
536
537  // Delete the URL and its dependencies.
538  expirer_.DeleteURL(last_row.url());
539
540  // All the normal data + the favicon should be gone.
541  EnsureURLInfoGone(last_row);
542  EXPECT_TRUE(HasFavicon(favicon_id));
543}
544
545// DeleteURL should not delete starred urls.
546TEST_F(ExpireHistoryTest, DontDeleteStarredURL) {
547  URLID url_ids[3];
548  Time visit_times[4];
549  AddExampleData(url_ids, visit_times);
550
551  URLRow url_row;
552  ASSERT_TRUE(main_db_->GetURLRow(url_ids[2], &url_row));
553
554  // Star the last URL.
555  StarURL(url_row.url());
556
557  // Attempt to delete the url.
558  expirer_.DeleteURL(url_row.url());
559
560  // Because the url is starred, it shouldn't be deleted.
561  GURL url = url_row.url();
562  ASSERT_TRUE(main_db_->GetRowForURL(url, &url_row));
563
564  // And the favicon should exist.
565  FaviconID favicon_id = GetFavicon(url_row.url(), FAVICON);
566  EXPECT_TRUE(HasFavicon(favicon_id));
567
568  // But there should be no fts.
569  ASSERT_EQ(0, CountTextMatchesForURL(url_row.url()));
570
571  // And no visits.
572  VisitVector visits;
573  main_db_->GetVisitsForURL(url_row.id(), &visits);
574  ASSERT_EQ(0U, visits.size());
575
576  // Should still have the thumbnail.
577  // TODO(sky): fix this, see comment in HasThumbnail.
578  // ASSERT_TRUE(HasThumbnail(url_row.id()));
579
580  // Unstar the URL and delete again.
581  bookmark_model_.SetURLStarred(url, string16(), false);
582  expirer_.DeleteURL(url);
583
584  // Now it should be completely deleted.
585  EnsureURLInfoGone(url_row);
586}
587
588// Expires all URLs more recent than a given time, with no starred items.
589// Our time threshold is such that one URL should be updated (we delete one of
590// the two visits) and one is deleted.
591TEST_F(ExpireHistoryTest, FlushRecentURLsUnstarred) {
592  URLID url_ids[3];
593  Time visit_times[4];
594  AddExampleData(url_ids, visit_times);
595
596  URLRow url_row1, url_row2;
597  ASSERT_TRUE(main_db_->GetURLRow(url_ids[1], &url_row1));
598  ASSERT_TRUE(main_db_->GetURLRow(url_ids[2], &url_row2));
599
600  // In this test we also make sure that any pending entries in the text
601  // database manager are removed.
602  VisitVector visits;
603  main_db_->GetVisitsForURL(url_ids[2], &visits);
604  ASSERT_EQ(1U, visits.size());
605  text_db_->AddPageURL(url_row2.url(), url_row2.id(), visits[0].visit_id,
606                       visits[0].visit_time);
607
608  // This should delete the last two visits.
609  std::set<GURL> restrict_urls;
610  expirer_.ExpireHistoryBetween(restrict_urls, visit_times[2], Time());
611
612  // Run the text database expirer. This will flush any pending entries so we
613  // can check that nothing was committed. We use a time far in the future so
614  // that anything added recently will get flushed.
615  TimeTicks expiration_time = TimeTicks::Now() + TimeDelta::FromDays(1);
616  text_db_->FlushOldChangesForTime(expiration_time);
617
618  // Verify that the middle URL had its last visit deleted only.
619  visits.clear();
620  main_db_->GetVisitsForURL(url_ids[1], &visits);
621  EXPECT_EQ(1U, visits.size());
622  EXPECT_EQ(0, CountTextMatchesForURL(url_row1.url()));
623
624  // Verify that the middle URL visit time and visit counts were updated.
625  URLRow temp_row;
626  ASSERT_TRUE(main_db_->GetURLRow(url_ids[1], &temp_row));
627  EXPECT_TRUE(visit_times[2] == url_row1.last_visit());  // Previous value.
628  EXPECT_TRUE(visit_times[1] == temp_row.last_visit());  // New value.
629  EXPECT_EQ(2, url_row1.visit_count());
630  EXPECT_EQ(1, temp_row.visit_count());
631  EXPECT_EQ(1, url_row1.typed_count());
632  EXPECT_EQ(0, temp_row.typed_count());
633
634  // Verify that the middle URL's favicon and thumbnail is still there.
635  FaviconID favicon_id = GetFavicon(url_row1.url(), FAVICON);
636  EXPECT_TRUE(HasFavicon(favicon_id));
637  // TODO(sky): fix this, see comment in HasThumbnail.
638  // EXPECT_TRUE(HasThumbnail(url_row1.id()));
639
640  // Verify that the last URL was deleted.
641  FaviconID favicon_id2 = GetFavicon(url_row2.url(), FAVICON);
642  EnsureURLInfoGone(url_row2);
643  EXPECT_FALSE(HasFavicon(favicon_id2));
644}
645
646// Expires only a specific URLs more recent than a given time, with no starred
647// items.  Our time threshold is such that the URL should be updated (we delete
648// one of the two visits).
649TEST_F(ExpireHistoryTest, FlushRecentURLsUnstarredRestricted) {
650  URLID url_ids[3];
651  Time visit_times[4];
652  AddExampleData(url_ids, visit_times);
653
654  URLRow url_row1, url_row2;
655  ASSERT_TRUE(main_db_->GetURLRow(url_ids[1], &url_row1));
656  ASSERT_TRUE(main_db_->GetURLRow(url_ids[2], &url_row2));
657
658  // In this test we also make sure that any pending entries in the text
659  // database manager are removed.
660  VisitVector visits;
661  main_db_->GetVisitsForURL(url_ids[2], &visits);
662  ASSERT_EQ(1U, visits.size());
663  text_db_->AddPageURL(url_row2.url(), url_row2.id(), visits[0].visit_id,
664                       visits[0].visit_time);
665
666  // This should delete the last two visits.
667  std::set<GURL> restrict_urls;
668  restrict_urls.insert(url_row1.url());
669  expirer_.ExpireHistoryBetween(restrict_urls, visit_times[2], Time());
670
671  // Run the text database expirer. This will flush any pending entries so we
672  // can check that nothing was committed. We use a time far in the future so
673  // that anything added recently will get flushed.
674  TimeTicks expiration_time = TimeTicks::Now() + TimeDelta::FromDays(1);
675  text_db_->FlushOldChangesForTime(expiration_time);
676
677  // Verify that the middle URL had its last visit deleted only.
678  visits.clear();
679  main_db_->GetVisitsForURL(url_ids[1], &visits);
680  EXPECT_EQ(1U, visits.size());
681  EXPECT_EQ(0, CountTextMatchesForURL(url_row1.url()));
682
683  // Verify that the middle URL visit time and visit counts were updated.
684  URLRow temp_row;
685  ASSERT_TRUE(main_db_->GetURLRow(url_ids[1], &temp_row));
686  EXPECT_TRUE(visit_times[2] == url_row1.last_visit());  // Previous value.
687  EXPECT_TRUE(visit_times[1] == temp_row.last_visit());  // New value.
688  EXPECT_EQ(2, url_row1.visit_count());
689  EXPECT_EQ(1, temp_row.visit_count());
690  EXPECT_EQ(1, url_row1.typed_count());
691  EXPECT_EQ(0, temp_row.typed_count());
692
693  // Verify that the middle URL's favicon and thumbnail is still there.
694  FaviconID favicon_id = GetFavicon(url_row1.url(), FAVICON);
695  EXPECT_TRUE(HasFavicon(favicon_id));
696  // TODO(sky): fix this, see comment in HasThumbnail.
697  // EXPECT_TRUE(HasThumbnail(url_row1.id()));
698
699  // Verify that the last URL was not touched.
700  EXPECT_TRUE(main_db_->GetURLRow(url_ids[2], &temp_row));
701  EXPECT_TRUE(HasFavicon(favicon_id));
702  // TODO(sky): fix this, see comment in HasThumbnail.
703  // EXPECT_TRUE(HasThumbnail(url_row2.id()));
704}
705
706// Expire a starred URL, it shouldn't get deleted
707TEST_F(ExpireHistoryTest, FlushRecentURLsStarred) {
708  URLID url_ids[3];
709  Time visit_times[4];
710  AddExampleData(url_ids, visit_times);
711
712  URLRow url_row1, url_row2;
713  ASSERT_TRUE(main_db_->GetURLRow(url_ids[1], &url_row1));
714  ASSERT_TRUE(main_db_->GetURLRow(url_ids[2], &url_row2));
715
716  // Star the last two URLs.
717  StarURL(url_row1.url());
718  StarURL(url_row2.url());
719
720  // This should delete the last two visits.
721  std::set<GURL> restrict_urls;
722  expirer_.ExpireHistoryBetween(restrict_urls, visit_times[2], Time());
723
724  // The URL rows should still exist.
725  URLRow new_url_row1, new_url_row2;
726  ASSERT_TRUE(main_db_->GetURLRow(url_ids[1], &new_url_row1));
727  ASSERT_TRUE(main_db_->GetURLRow(url_ids[2], &new_url_row2));
728
729  // The visit times should be updated.
730  EXPECT_TRUE(new_url_row1.last_visit() == visit_times[1]);
731  EXPECT_TRUE(new_url_row2.last_visit().is_null());  // No last visit time.
732
733  // Visit/typed count should not be updated for bookmarks.
734  EXPECT_EQ(0, new_url_row1.typed_count());
735  EXPECT_EQ(1, new_url_row1.visit_count());
736  EXPECT_EQ(0, new_url_row2.typed_count());
737  EXPECT_EQ(0, new_url_row2.visit_count());
738
739  // Thumbnails and favicons should still exist. Note that we keep thumbnails
740  // that may have been updated since the time threshold. Since the URL still
741  // exists in history, this should not be a privacy problem, we only update
742  // the visit counts in this case for consistency anyway.
743  FaviconID favicon_id = GetFavicon(url_row1.url(), FAVICON);
744  EXPECT_TRUE(HasFavicon(favicon_id));
745  // TODO(sky): fix this, see comment in HasThumbnail.
746  // EXPECT_TRUE(HasThumbnail(new_url_row1.id()));
747  favicon_id = GetFavicon(url_row1.url(), FAVICON);
748  EXPECT_TRUE(HasFavicon(favicon_id));
749  // TODO(sky): fix this, see comment in HasThumbnail.
750  // EXPECT_TRUE(HasThumbnail(new_url_row2.id()));
751}
752
753TEST_F(ExpireHistoryTest, ArchiveHistoryBeforeUnstarred) {
754  URLID url_ids[3];
755  Time visit_times[4];
756  AddExampleData(url_ids, visit_times);
757
758  URLRow url_row1, url_row2;
759  ASSERT_TRUE(main_db_->GetURLRow(url_ids[1], &url_row1));
760  ASSERT_TRUE(main_db_->GetURLRow(url_ids[2], &url_row2));
761
762  // Archive the oldest two visits. This will actually result in deleting them
763  // since their transition types are empty (not important).
764  expirer_.ArchiveHistoryBefore(visit_times[1]);
765
766  // The first URL should be deleted, the second should not be affected.
767  URLRow temp_row;
768  EXPECT_FALSE(main_db_->GetURLRow(url_ids[0], &temp_row));
769  EXPECT_TRUE(main_db_->GetURLRow(url_ids[1], &temp_row));
770  EXPECT_TRUE(main_db_->GetURLRow(url_ids[2], &temp_row));
771
772  // Make sure the archived database has nothing in it.
773  EXPECT_FALSE(archived_db_->GetRowForURL(url_row1.url(), NULL));
774  EXPECT_FALSE(archived_db_->GetRowForURL(url_row2.url(), NULL));
775
776  // Now archive one more visit so that the middle URL should be removed. This
777  // one will actually be archived instead of deleted.
778  expirer_.ArchiveHistoryBefore(visit_times[2]);
779  EXPECT_FALSE(main_db_->GetURLRow(url_ids[1], &temp_row));
780  EXPECT_TRUE(main_db_->GetURLRow(url_ids[2], &temp_row));
781
782  // Make sure the archived database has an entry for the second URL.
783  URLRow archived_row;
784  // Note that the ID is different in the archived DB, so look up by URL.
785  EXPECT_TRUE(archived_db_->GetRowForURL(url_row1.url(), &archived_row));
786  VisitVector archived_visits;
787  archived_db_->GetVisitsForURL(archived_row.id(), &archived_visits);
788  EXPECT_EQ(1U, archived_visits.size());
789}
790
791TEST_F(ExpireHistoryTest, ArchiveHistoryBeforeStarred) {
792  URLID url_ids[3];
793  Time visit_times[4];
794  AddExampleData(url_ids, visit_times);
795
796  URLRow url_row0, url_row1;
797  ASSERT_TRUE(main_db_->GetURLRow(url_ids[0], &url_row0));
798  ASSERT_TRUE(main_db_->GetURLRow(url_ids[1], &url_row1));
799
800  // Star the URLs.
801  StarURL(url_row0.url());
802  StarURL(url_row1.url());
803
804  // Now archive the first three visits (first two URLs). The first two visits
805  // should be, the third deleted, but the URL records should not.
806  expirer_.ArchiveHistoryBefore(visit_times[2]);
807
808  // The first URL should have its visit deleted, but it should still be present
809  // in the main DB and not in the archived one since it is starred.
810  URLRow temp_row;
811  ASSERT_TRUE(main_db_->GetURLRow(url_ids[0], &temp_row));
812  // Note that the ID is different in the archived DB, so look up by URL.
813  EXPECT_FALSE(archived_db_->GetRowForURL(temp_row.url(), NULL));
814  VisitVector visits;
815  main_db_->GetVisitsForURL(temp_row.id(), &visits);
816  EXPECT_EQ(0U, visits.size());
817
818  // The second URL should have its first visit deleted and its second visit
819  // archived. It should be present in both the main DB (because it's starred)
820  // and the archived DB (for the archived visit).
821  ASSERT_TRUE(main_db_->GetURLRow(url_ids[1], &temp_row));
822  main_db_->GetVisitsForURL(temp_row.id(), &visits);
823  EXPECT_EQ(0U, visits.size());
824
825  // Note that the ID is different in the archived DB, so look up by URL.
826  ASSERT_TRUE(archived_db_->GetRowForURL(temp_row.url(), &temp_row));
827  archived_db_->GetVisitsForURL(temp_row.id(), &visits);
828  ASSERT_EQ(1U, visits.size());
829  EXPECT_TRUE(visit_times[2] == visits[0].visit_time);
830
831  // The third URL should be unchanged.
832  EXPECT_TRUE(main_db_->GetURLRow(url_ids[2], &temp_row));
833  EXPECT_FALSE(archived_db_->GetRowForURL(temp_row.url(), NULL));
834}
835
836// Tests the return values from ArchiveSomeOldHistory. The rest of the
837// functionality of this function is tested by the ArchiveHistoryBefore*
838// tests which use this function internally.
839TEST_F(ExpireHistoryTest, ArchiveSomeOldHistory) {
840  URLID url_ids[3];
841  Time visit_times[4];
842  AddExampleData(url_ids, visit_times);
843  const ExpiringVisitsReader* reader = expirer_.GetAllVisitsReader();
844
845  // Deleting a time range with no URLs should return false (nothing found).
846  EXPECT_FALSE(expirer_.ArchiveSomeOldHistory(
847      visit_times[0] - TimeDelta::FromDays(100), reader, 1));
848
849  // Deleting a time range with not up the the max results should also return
850  // false (there will only be one visit deleted in this range).
851  EXPECT_FALSE(expirer_.ArchiveSomeOldHistory(visit_times[0], reader, 2));
852
853  // Deleting a time range with the max number of results should return true
854  // (max deleted).
855  EXPECT_TRUE(expirer_.ArchiveSomeOldHistory(visit_times[2], reader, 1));
856}
857
858TEST_F(ExpireHistoryTest, ExpiringVisitsReader) {
859  URLID url_ids[3];
860  Time visit_times[4];
861  AddExampleData(url_ids, visit_times);
862
863  const ExpiringVisitsReader* all = expirer_.GetAllVisitsReader();
864  const ExpiringVisitsReader* auto_subframes =
865      expirer_.GetAutoSubframeVisitsReader();
866
867  VisitVector visits;
868  Time now = Time::Now();
869
870  // Verify that the early expiration threshold, stored in the meta table is
871  // initialized.
872  EXPECT_TRUE(main_db_->GetEarlyExpirationThreshold() ==
873      Time::FromInternalValue(1L));
874
875  // First, attempt reading AUTO_SUBFRAME visits. We should get none.
876  EXPECT_FALSE(auto_subframes->Read(now, main_db_.get(), &visits, 1));
877  EXPECT_EQ(0U, visits.size());
878
879  // Verify that the early expiration threshold was updated, since there are no
880  // AUTO_SUBFRAME visits in the given time range.
881  EXPECT_TRUE(now <= main_db_->GetEarlyExpirationThreshold());
882
883  // Now, read all visits and verify that there's at least one.
884  EXPECT_TRUE(all->Read(now, main_db_.get(), &visits, 1));
885  EXPECT_EQ(1U, visits.size());
886}
887
888// Tests how ArchiveSomeOldHistory treats source information.
889TEST_F(ExpireHistoryTest, ArchiveSomeOldHistoryWithSource) {
890  const GURL url("www.testsource.com");
891  URLID url_id;
892  AddExampleSourceData(url, &url_id);
893  const ExpiringVisitsReader* reader = expirer_.GetAllVisitsReader();
894
895  // Archiving all the visits we added.
896  ASSERT_FALSE(expirer_.ArchiveSomeOldHistory(Time::Now(), reader, 10));
897
898  URLRow archived_row;
899  ASSERT_TRUE(archived_db_->GetRowForURL(url, &archived_row));
900  VisitVector archived_visits;
901  archived_db_->GetVisitsForURL(archived_row.id(), &archived_visits);
902  ASSERT_EQ(4U, archived_visits.size());
903  VisitSourceMap sources;
904  archived_db_->GetVisitsSource(archived_visits, &sources);
905  ASSERT_EQ(3U, sources.size());
906  int result = 0;
907  VisitSourceMap::iterator iter;
908  for (int i = 0; i < 4; i++) {
909    iter = sources.find(archived_visits[i].visit_id);
910    if (iter == sources.end())
911      continue;
912    switch (iter->second) {
913      case history::SOURCE_EXTENSION:
914        result |= 0x1;
915        break;
916      case history::SOURCE_FIREFOX_IMPORTED:
917        result |= 0x2;
918        break;
919      case history::SOURCE_SYNCED:
920        result |= 0x4;
921      default:
922        break;
923    }
924  }
925  EXPECT_EQ(0x7, result);
926  main_db_->GetVisitsSource(archived_visits, &sources);
927  EXPECT_EQ(0U, sources.size());
928  main_db_->GetVisitsForURL(url_id, &archived_visits);
929  EXPECT_EQ(0U, archived_visits.size());
930}
931
932// TODO(brettw) add some visits with no URL to make sure everything is updated
933// properly. Have the visits also refer to nonexistent FTS rows.
934//
935// Maybe also refer to invalid favicons.
936
937}  // namespace history
938