bookmark_model_unittest.cc revision 72a454cd3513ac24fbdd0e0cb9ad70b86a99b801
1// Copyright (c) 2010 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 <set>
7
8#include "base/base_paths.h"
9#include "base/file_util.h"
10#include "base/hash_tables.h"
11#include "base/path_service.h"
12#include "base/string16.h"
13#include "base/string_number_conversions.h"
14#include "base/string_split.h"
15#include "base/string_util.h"
16#include "base/utf_string_conversions.h"
17#include "chrome/browser/bookmarks/bookmark_codec.h"
18#include "chrome/browser/bookmarks/bookmark_model.h"
19#include "chrome/browser/bookmarks/bookmark_utils.h"
20#include "chrome/browser/browser_thread.h"
21#include "chrome/browser/history/history_notifications.h"
22#include "chrome/common/chrome_constants.h"
23#include "chrome/common/chrome_paths.h"
24#include "chrome/common/notification_details.h"
25#include "chrome/common/notification_registrar.h"
26#include "chrome/common/notification_source.h"
27#include "chrome/test/model_test_utils.h"
28#include "chrome/test/testing_profile.h"
29#include "testing/gtest/include/gtest/gtest.h"
30#include "ui/base/models/tree_node_model.h"
31#include "ui/base/models/tree_node_iterator.h"
32
33using base::Time;
34using base::TimeDelta;
35
36namespace {
37
38// Helper to get a mutable bookmark node.
39static BookmarkNode* AsMutable(const BookmarkNode* node) {
40  return const_cast<BookmarkNode*>(node);
41}
42
43void SwapDateAdded(BookmarkNode* n1, BookmarkNode* n2) {
44  Time tmp = n1->date_added();
45  n1->set_date_added(n2->date_added());
46  n2->set_date_added(tmp);
47}
48
49}  // anonymous namespace
50
51class BookmarkModelTest : public testing::Test, public BookmarkModelObserver {
52 public:
53  struct ObserverDetails {
54    ObserverDetails() {
55      Set(NULL, NULL, -1, -1);
56    }
57
58    void Set(const BookmarkNode* node1,
59             const BookmarkNode* node2,
60             int index1,
61             int index2) {
62      this->node1 = node1;
63      this->node2 = node2;
64      this->index1 = index1;
65      this->index2 = index2;
66    }
67
68    void AssertEquals(const BookmarkNode* node1,
69                      const BookmarkNode* node2,
70                      int index1,
71                      int index2) {
72      ASSERT_TRUE(this->node1 == node1);
73      ASSERT_TRUE(this->node2 == node2);
74      ASSERT_EQ(index1, this->index1);
75      ASSERT_EQ(index2, this->index2);
76    }
77
78    const BookmarkNode* node1;
79    const BookmarkNode* node2;
80    int index1;
81    int index2;
82  };
83
84  BookmarkModelTest() : model(NULL) {
85    model.AddObserver(this);
86    ClearCounts();
87  }
88
89
90  void Loaded(BookmarkModel* model) {
91    // We never load from the db, so that this should never get invoked.
92    NOTREACHED();
93  }
94
95  virtual void BookmarkNodeMoved(BookmarkModel* model,
96                                 const BookmarkNode* old_parent,
97                                 int old_index,
98                                 const BookmarkNode* new_parent,
99                                 int new_index) {
100    moved_count++;
101    observer_details.Set(old_parent, new_parent, old_index, new_index);
102  }
103
104  virtual void BookmarkNodeAdded(BookmarkModel* model,
105                                 const BookmarkNode* parent,
106                                 int index) {
107    added_count++;
108    observer_details.Set(parent, NULL, index, -1);
109  }
110
111  virtual void BookmarkNodeRemoved(BookmarkModel* model,
112                                   const BookmarkNode* parent,
113                                   int old_index,
114                                   const BookmarkNode* node) {
115    removed_count++;
116    observer_details.Set(parent, NULL, old_index, -1);
117  }
118
119  virtual void BookmarkNodeChanged(BookmarkModel* model,
120                                   const BookmarkNode* node) {
121    changed_count++;
122    observer_details.Set(node, NULL, -1, -1);
123  }
124
125  virtual void BookmarkNodeChildrenReordered(BookmarkModel* model,
126                                             const BookmarkNode* node) {
127    reordered_count_++;
128  }
129
130  virtual void BookmarkNodeFavIconLoaded(BookmarkModel* model,
131                                         const BookmarkNode* node) {
132    // We never attempt to load favicons, so that this method never
133    // gets invoked.
134  }
135
136  void ClearCounts() {
137    reordered_count_ = moved_count = added_count = removed_count =
138        changed_count = 0;
139  }
140
141  void AssertObserverCount(int added_count,
142                           int moved_count,
143                           int removed_count,
144                           int changed_count,
145                           int reordered_count) {
146    ASSERT_EQ(added_count, this->added_count);
147    ASSERT_EQ(moved_count, this->moved_count);
148    ASSERT_EQ(removed_count, this->removed_count);
149    ASSERT_EQ(changed_count, this->changed_count);
150    ASSERT_EQ(reordered_count, reordered_count_);
151  }
152
153  BookmarkModel model;
154
155  int moved_count;
156
157  int added_count;
158
159  int removed_count;
160
161  int changed_count;
162
163  int reordered_count_;
164
165  ObserverDetails observer_details;
166};
167
168TEST_F(BookmarkModelTest, InitialState) {
169  const BookmarkNode* bb_node = model.GetBookmarkBarNode();
170  ASSERT_TRUE(bb_node != NULL);
171  EXPECT_EQ(0, bb_node->GetChildCount());
172  EXPECT_EQ(BookmarkNode::BOOKMARK_BAR, bb_node->type());
173
174  const BookmarkNode* other_node = model.other_node();
175  ASSERT_TRUE(other_node != NULL);
176  EXPECT_EQ(0, other_node->GetChildCount());
177  EXPECT_EQ(BookmarkNode::OTHER_NODE, other_node->type());
178
179  EXPECT_TRUE(bb_node->id() != other_node->id());
180}
181
182TEST_F(BookmarkModelTest, AddURL) {
183  const BookmarkNode* root = model.GetBookmarkBarNode();
184  const string16 title(ASCIIToUTF16("foo"));
185  const GURL url("http://foo.com");
186
187  const BookmarkNode* new_node = model.AddURL(root, 0, title, url);
188  AssertObserverCount(1, 0, 0, 0, 0);
189  observer_details.AssertEquals(root, NULL, 0, -1);
190
191  ASSERT_EQ(1, root->GetChildCount());
192  ASSERT_EQ(title, new_node->GetTitle());
193  ASSERT_TRUE(url == new_node->GetURL());
194  ASSERT_EQ(BookmarkNode::URL, new_node->type());
195  ASSERT_TRUE(new_node == model.GetMostRecentlyAddedNodeForURL(url));
196
197  EXPECT_TRUE(new_node->id() != root->id() &&
198              new_node->id() != model.other_node()->id());
199}
200
201TEST_F(BookmarkModelTest, AddGroup) {
202  const BookmarkNode* root = model.GetBookmarkBarNode();
203  const string16 title(ASCIIToUTF16("foo"));
204
205  const BookmarkNode* new_node = model.AddGroup(root, 0, title);
206  AssertObserverCount(1, 0, 0, 0, 0);
207  observer_details.AssertEquals(root, NULL, 0, -1);
208
209  ASSERT_EQ(1, root->GetChildCount());
210  ASSERT_EQ(title, new_node->GetTitle());
211  ASSERT_EQ(BookmarkNode::FOLDER, new_node->type());
212
213  EXPECT_TRUE(new_node->id() != root->id() &&
214              new_node->id() != model.other_node()->id());
215
216  // Add another group, just to make sure group_ids are incremented correctly.
217  ClearCounts();
218  model.AddGroup(root, 0, title);
219  AssertObserverCount(1, 0, 0, 0, 0);
220  observer_details.AssertEquals(root, NULL, 0, -1);
221}
222
223TEST_F(BookmarkModelTest, RemoveURL) {
224  const BookmarkNode* root = model.GetBookmarkBarNode();
225  const string16 title(ASCIIToUTF16("foo"));
226  const GURL url("http://foo.com");
227  model.AddURL(root, 0, title, url);
228  ClearCounts();
229
230  model.Remove(root, 0);
231  ASSERT_EQ(0, root->GetChildCount());
232  AssertObserverCount(0, 0, 1, 0, 0);
233  observer_details.AssertEquals(root, NULL, 0, -1);
234
235  // Make sure there is no mapping for the URL.
236  ASSERT_TRUE(model.GetMostRecentlyAddedNodeForURL(url) == NULL);
237}
238
239TEST_F(BookmarkModelTest, RemoveGroup) {
240  const BookmarkNode* root = model.GetBookmarkBarNode();
241  const BookmarkNode* group = model.AddGroup(root, 0, ASCIIToUTF16("foo"));
242
243  ClearCounts();
244
245  // Add a URL as a child.
246  const string16 title(ASCIIToUTF16("foo"));
247  const GURL url("http://foo.com");
248  model.AddURL(group, 0, title, url);
249
250  ClearCounts();
251
252  // Now remove the group.
253  model.Remove(root, 0);
254  ASSERT_EQ(0, root->GetChildCount());
255  AssertObserverCount(0, 0, 1, 0, 0);
256  observer_details.AssertEquals(root, NULL, 0, -1);
257
258  // Make sure there is no mapping for the URL.
259  ASSERT_TRUE(model.GetMostRecentlyAddedNodeForURL(url) == NULL);
260}
261
262TEST_F(BookmarkModelTest, SetTitle) {
263  const BookmarkNode* root = model.GetBookmarkBarNode();
264  string16 title(ASCIIToUTF16("foo"));
265  const GURL url("http://foo.com");
266  const BookmarkNode* node = model.AddURL(root, 0, title, url);
267
268  ClearCounts();
269
270  title = ASCIIToUTF16("foo2");
271  model.SetTitle(node, title);
272  AssertObserverCount(0, 0, 0, 1, 0);
273  observer_details.AssertEquals(node, NULL, -1, -1);
274  EXPECT_EQ(title, node->GetTitle());
275}
276
277TEST_F(BookmarkModelTest, SetURL) {
278  const BookmarkNode* root = model.GetBookmarkBarNode();
279  const string16 title(ASCIIToUTF16("foo"));
280  GURL url("http://foo.com");
281  const BookmarkNode* node = model.AddURL(root, 0, title, url);
282
283  ClearCounts();
284
285  url = GURL("http://foo2.com");
286  model.SetURL(node, url);
287  AssertObserverCount(0, 0, 0, 1, 0);
288  observer_details.AssertEquals(node, NULL, -1, -1);
289  EXPECT_EQ(url, node->GetURL());
290}
291
292TEST_F(BookmarkModelTest, Move) {
293  const BookmarkNode* root = model.GetBookmarkBarNode();
294  const string16 title(ASCIIToUTF16("foo"));
295  const GURL url("http://foo.com");
296  const BookmarkNode* node = model.AddURL(root, 0, title, url);
297  const BookmarkNode* group1 = model.AddGroup(root, 0, ASCIIToUTF16("foo"));
298  ClearCounts();
299
300  model.Move(node, group1, 0);
301
302  AssertObserverCount(0, 1, 0, 0, 0);
303  observer_details.AssertEquals(root, group1, 1, 0);
304  EXPECT_TRUE(group1 == node->GetParent());
305  EXPECT_EQ(1, root->GetChildCount());
306  EXPECT_EQ(group1, root->GetChild(0));
307  EXPECT_EQ(1, group1->GetChildCount());
308  EXPECT_EQ(node, group1->GetChild(0));
309
310  // And remove the group.
311  ClearCounts();
312  model.Remove(root, 0);
313  AssertObserverCount(0, 0, 1, 0, 0);
314  observer_details.AssertEquals(root, NULL, 0, -1);
315  EXPECT_TRUE(model.GetMostRecentlyAddedNodeForURL(url) == NULL);
316  EXPECT_EQ(0, root->GetChildCount());
317}
318
319TEST_F(BookmarkModelTest, Copy) {
320  const BookmarkNode* root = model.GetBookmarkBarNode();
321  static const std::string model_string("a 1:[ b c ] d 2:[ e f g ] h ");
322  model_test_utils::AddNodesFromModelString(model, root, model_string);
323
324  // Validate initial model.
325  std::string actualModelString = model_test_utils::ModelStringFromNode(root);
326  EXPECT_EQ(model_string, actualModelString);
327
328  // Copy 'd' to be after '1:b': URL item from bar to folder.
329  const BookmarkNode* nodeToCopy = root->GetChild(2);
330  const BookmarkNode* destination = root->GetChild(1);
331  model.Copy(nodeToCopy, destination, 1);
332  actualModelString = model_test_utils::ModelStringFromNode(root);
333  EXPECT_EQ("a 1:[ b d c ] d 2:[ e f g ] h ", actualModelString);
334
335  // Copy '1:d' to be after 'a': URL item from folder to bar.
336  const BookmarkNode* group = root->GetChild(1);
337  nodeToCopy = group->GetChild(1);
338  model.Copy(nodeToCopy, root, 1);
339  actualModelString = model_test_utils::ModelStringFromNode(root);
340  EXPECT_EQ("a d 1:[ b d c ] d 2:[ e f g ] h ", actualModelString);
341
342  // Copy '1' to be after '2:e': Folder from bar to folder.
343  nodeToCopy = root->GetChild(2);
344  destination = root->GetChild(4);
345  model.Copy(nodeToCopy, destination, 1);
346  actualModelString = model_test_utils::ModelStringFromNode(root);
347  EXPECT_EQ("a d 1:[ b d c ] d 2:[ e 1:[ b d c ] f g ] h ", actualModelString);
348
349  // Copy '2:1' to be after '2:f': Folder within same folder.
350  group = root->GetChild(4);
351  nodeToCopy = group->GetChild(1);
352  model.Copy(nodeToCopy, group, 3);
353  actualModelString = model_test_utils::ModelStringFromNode(root);
354  EXPECT_EQ("a d 1:[ b d c ] d 2:[ e 1:[ b d c ] f 1:[ b d c ] g ] h ",
355            actualModelString);
356
357  // Copy first 'd' to be after 'h': URL item within the bar.
358  nodeToCopy = root->GetChild(1);
359  model.Copy(nodeToCopy, root, 6);
360  actualModelString = model_test_utils::ModelStringFromNode(root);
361  EXPECT_EQ("a d 1:[ b d c ] d 2:[ e 1:[ b d c ] f 1:[ b d c ] g ] h d ",
362            actualModelString);
363
364  // Copy '2' to be after 'a': Folder within the bar.
365  nodeToCopy = root->GetChild(4);
366  model.Copy(nodeToCopy, root, 1);
367  actualModelString = model_test_utils::ModelStringFromNode(root);
368  EXPECT_EQ("a 2:[ e 1:[ b d c ] f 1:[ b d c ] g ] d 1:[ b d c ] "
369            "d 2:[ e 1:[ b d c ] f 1:[ b d c ] g ] h d ",
370            actualModelString);
371}
372
373// Tests that adding a URL to a folder updates the last modified time.
374TEST_F(BookmarkModelTest, ParentForNewNodes) {
375  ASSERT_EQ(model.GetBookmarkBarNode(), model.GetParentForNewNodes());
376
377  const string16 title(ASCIIToUTF16("foo"));
378  const GURL url("http://foo.com");
379
380  model.AddURL(model.other_node(), 0, title, url);
381  ASSERT_EQ(model.other_node(), model.GetParentForNewNodes());
382}
383
384// Make sure recently modified stays in sync when adding a URL.
385TEST_F(BookmarkModelTest, MostRecentlyModifiedGroups) {
386  // Add a group.
387  const BookmarkNode* group = model.AddGroup(model.other_node(), 0,
388                                             ASCIIToUTF16("foo"));
389  // Add a URL to it.
390  model.AddURL(group, 0, ASCIIToUTF16("blah"), GURL("http://foo.com"));
391
392  // Make sure group is in the most recently modified.
393  std::vector<const BookmarkNode*> most_recent_groups =
394      bookmark_utils::GetMostRecentlyModifiedGroups(&model, 1);
395  ASSERT_EQ(1U, most_recent_groups.size());
396  ASSERT_EQ(group, most_recent_groups[0]);
397
398  // Nuke the group and do another fetch, making sure group isn't in the
399  // returned list.
400  model.Remove(group->GetParent(), 0);
401  most_recent_groups =
402      bookmark_utils::GetMostRecentlyModifiedGroups(&model, 1);
403  ASSERT_EQ(1U, most_recent_groups.size());
404  ASSERT_TRUE(most_recent_groups[0] != group);
405}
406
407// Make sure MostRecentlyAddedEntries stays in sync.
408TEST_F(BookmarkModelTest, MostRecentlyAddedEntries) {
409  // Add a couple of nodes such that the following holds for the time of the
410  // nodes: n1 > n2 > n3 > n4.
411  Time base_time = Time::Now();
412  BookmarkNode* n1 = AsMutable(model.AddURL(model.GetBookmarkBarNode(),
413                                  0,
414                                  ASCIIToUTF16("blah"),
415                                  GURL("http://foo.com/0")));
416  BookmarkNode* n2 = AsMutable(model.AddURL(model.GetBookmarkBarNode(),
417                                  1,
418                                  ASCIIToUTF16("blah"),
419                                  GURL("http://foo.com/1")));
420  BookmarkNode* n3 = AsMutable(model.AddURL(model.GetBookmarkBarNode(),
421                                  2,
422                                  ASCIIToUTF16("blah"),
423                                  GURL("http://foo.com/2")));
424  BookmarkNode* n4 = AsMutable(model.AddURL(model.GetBookmarkBarNode(),
425                                  3,
426                                  ASCIIToUTF16("blah"),
427                                  GURL("http://foo.com/3")));
428  n1->set_date_added(base_time + TimeDelta::FromDays(4));
429  n2->set_date_added(base_time + TimeDelta::FromDays(3));
430  n3->set_date_added(base_time + TimeDelta::FromDays(2));
431  n4->set_date_added(base_time + TimeDelta::FromDays(1));
432
433  // Make sure order is honored.
434  std::vector<const BookmarkNode*> recently_added;
435  bookmark_utils::GetMostRecentlyAddedEntries(&model, 2, &recently_added);
436  ASSERT_EQ(2U, recently_added.size());
437  ASSERT_TRUE(n1 == recently_added[0]);
438  ASSERT_TRUE(n2 == recently_added[1]);
439
440  // swap 1 and 2, then check again.
441  recently_added.clear();
442  SwapDateAdded(n1, n2);
443  bookmark_utils::GetMostRecentlyAddedEntries(&model, 4, &recently_added);
444  ASSERT_EQ(4U, recently_added.size());
445  ASSERT_TRUE(n2 == recently_added[0]);
446  ASSERT_TRUE(n1 == recently_added[1]);
447  ASSERT_TRUE(n3 == recently_added[2]);
448  ASSERT_TRUE(n4 == recently_added[3]);
449}
450
451// Makes sure GetMostRecentlyAddedNodeForURL stays in sync.
452TEST_F(BookmarkModelTest, GetMostRecentlyAddedNodeForURL) {
453  // Add a couple of nodes such that the following holds for the time of the
454  // nodes: n1 > n2
455  Time base_time = Time::Now();
456  const GURL url("http://foo.com/0");
457  BookmarkNode* n1 = AsMutable(model.AddURL(
458      model.GetBookmarkBarNode(), 0, ASCIIToUTF16("blah"), url));
459  BookmarkNode* n2 = AsMutable(model.AddURL(
460      model.GetBookmarkBarNode(), 1, ASCIIToUTF16("blah"), url));
461  n1->set_date_added(base_time + TimeDelta::FromDays(4));
462  n2->set_date_added(base_time + TimeDelta::FromDays(3));
463
464  // Make sure order is honored.
465  ASSERT_EQ(n1, model.GetMostRecentlyAddedNodeForURL(url));
466
467  // swap 1 and 2, then check again.
468  SwapDateAdded(n1, n2);
469  ASSERT_EQ(n2, model.GetMostRecentlyAddedNodeForURL(url));
470}
471
472// Makes sure GetBookmarks removes duplicates.
473TEST_F(BookmarkModelTest, GetBookmarksWithDups) {
474  const GURL url("http://foo.com/0");
475  model.AddURL(model.GetBookmarkBarNode(), 0, ASCIIToUTF16("blah"), url);
476  model.AddURL(model.GetBookmarkBarNode(), 1, ASCIIToUTF16("blah"), url);
477
478  std::vector<GURL> urls;
479  model.GetBookmarks(&urls);
480  EXPECT_EQ(1U, urls.size());
481  ASSERT_TRUE(urls[0] == url);
482}
483
484TEST_F(BookmarkModelTest, HasBookmarks) {
485  const GURL url("http://foo.com/");
486  model.AddURL(model.GetBookmarkBarNode(), 0, ASCIIToUTF16("bar"), url);
487
488  EXPECT_TRUE(model.HasBookmarks());
489}
490
491namespace {
492
493// NotificationObserver implementation used in verifying we've received the
494// NOTIFY_URLS_STARRED method correctly.
495class StarredListener : public NotificationObserver {
496 public:
497  StarredListener() : notification_count_(0), details_(false) {
498    registrar_.Add(this, NotificationType::URLS_STARRED, Source<Profile>(NULL));
499  }
500
501  virtual void Observe(NotificationType type,
502                       const NotificationSource& source,
503                       const NotificationDetails& details) {
504    if (type == NotificationType::URLS_STARRED) {
505      notification_count_++;
506      details_ = *(Details<history::URLsStarredDetails>(details).ptr());
507    }
508  }
509
510  // Number of times NOTIFY_URLS_STARRED has been observed.
511  int notification_count_;
512
513  // Details from the last NOTIFY_URLS_STARRED.
514  history::URLsStarredDetails details_;
515
516 private:
517  NotificationRegistrar registrar_;
518
519  DISALLOW_COPY_AND_ASSIGN(StarredListener);
520};
521
522}  // namespace
523
524// Makes sure NOTIFY_URLS_STARRED is sent correctly.
525TEST_F(BookmarkModelTest, NotifyURLsStarred) {
526  StarredListener listener;
527  const GURL url("http://foo.com/0");
528  const BookmarkNode* n1 = model.AddURL(
529      model.GetBookmarkBarNode(), 0, ASCIIToUTF16("blah"), url);
530
531  // Starred notification should be sent.
532  EXPECT_EQ(1, listener.notification_count_);
533  ASSERT_TRUE(listener.details_.starred);
534  ASSERT_EQ(1U, listener.details_.changed_urls.size());
535  EXPECT_TRUE(url == *(listener.details_.changed_urls.begin()));
536  listener.notification_count_ = 0;
537  listener.details_.changed_urls.clear();
538
539  // Add another bookmark for the same URL. This should not send any
540  // notification.
541  const BookmarkNode* n2 = model.AddURL(
542      model.GetBookmarkBarNode(), 1, ASCIIToUTF16("blah"), url);
543
544  EXPECT_EQ(0, listener.notification_count_);
545
546  // Remove n2.
547  model.Remove(n2->GetParent(), 1);
548  n2 = NULL;
549
550  // Shouldn't have received any notification as n1 still exists with the same
551  // URL.
552  EXPECT_EQ(0, listener.notification_count_);
553
554  EXPECT_TRUE(model.GetMostRecentlyAddedNodeForURL(url) == n1);
555
556  // Remove n1.
557  model.Remove(n1->GetParent(), 0);
558
559  // Now we should get the notification.
560  EXPECT_EQ(1, listener.notification_count_);
561  ASSERT_FALSE(listener.details_.starred);
562  ASSERT_EQ(1U, listener.details_.changed_urls.size());
563  EXPECT_TRUE(url == *(listener.details_.changed_urls.begin()));
564}
565
566namespace {
567
568// See comment in PopulateNodeFromString.
569typedef ui::TreeNodeWithValue<BookmarkNode::Type> TestNode;
570
571// Does the work of PopulateNodeFromString. index gives the index of the current
572// element in description to process.
573static void PopulateNodeImpl(const std::vector<std::string>& description,
574                             size_t* index,
575                             TestNode* parent) {
576  while (*index < description.size()) {
577    const std::string& element = description[*index];
578    (*index)++;
579    if (element == "[") {
580      // Create a new group and recurse to add all the children.
581      // Groups are given a unique named by way of an ever increasing integer
582      // value. The groups need not have a name, but one is assigned to help
583      // in debugging.
584      static int next_group_id = 1;
585      TestNode* new_node =
586          new TestNode(base::IntToString16(next_group_id++),
587                       BookmarkNode::FOLDER);
588      parent->Add(parent->GetChildCount(), new_node);
589      PopulateNodeImpl(description, index, new_node);
590    } else if (element == "]") {
591      // End the current group.
592      return;
593    } else {
594      // Add a new URL.
595
596      // All tokens must be space separated. If there is a [ or ] in the name it
597      // likely means a space was forgotten.
598      DCHECK(element.find('[') == std::string::npos);
599      DCHECK(element.find(']') == std::string::npos);
600      parent->Add(parent->GetChildCount(),
601                  new TestNode(UTF8ToUTF16(element), BookmarkNode::URL));
602    }
603  }
604}
605
606// Creates and adds nodes to parent based on description. description consists
607// of the following tokens (all space separated):
608//   [ : creates a new USER_GROUP node. All elements following the [ until the
609//       next balanced ] is encountered are added as children to the node.
610//   ] : closes the last group created by [ so that any further nodes are added
611//       to the current groups parent.
612//   text: creates a new URL node.
613// For example, "a [b] c" creates the following nodes:
614//   a 1 c
615//     |
616//     b
617// In words: a node of type URL with the title a, followed by a group node with
618// the title 1 having the single child of type url with name b, followed by
619// the url node with the title c.
620//
621// NOTE: each name must be unique, and groups are assigned a unique title by way
622// of an increasing integer.
623static void PopulateNodeFromString(const std::string& description,
624                                   TestNode* parent) {
625  std::vector<std::string> elements;
626  size_t index = 0;
627  base::SplitStringAlongWhitespace(description, &elements);
628  PopulateNodeImpl(elements, &index, parent);
629}
630
631// Populates the BookmarkNode with the children of parent.
632static void PopulateBookmarkNode(TestNode* parent,
633                                 BookmarkModel* model,
634                                 const BookmarkNode* bb_node) {
635  for (int i = 0; i < parent->GetChildCount(); ++i) {
636    TestNode* child = parent->GetChild(i);
637    if (child->value == BookmarkNode::FOLDER) {
638      const BookmarkNode* new_bb_node =
639          model->AddGroup(bb_node, i, child->GetTitle());
640      PopulateBookmarkNode(child, model, new_bb_node);
641    } else {
642      model->AddURL(bb_node, i, child->GetTitle(),
643          GURL("http://" + UTF16ToASCII(child->GetTitle())));
644    }
645  }
646}
647
648}  // namespace
649
650// Test class that creates a BookmarkModel with a real history backend.
651class BookmarkModelTestWithProfile : public testing::Test,
652                                     public BookmarkModelObserver {
653 public:
654  BookmarkModelTestWithProfile()
655      : ui_thread_(BrowserThread::UI, &message_loop_),
656        file_thread_(BrowserThread::FILE, &message_loop_) {}
657
658  virtual void SetUp() {
659  }
660
661  virtual void TearDown() {
662    profile_.reset(NULL);
663  }
664
665  // The profile.
666  scoped_ptr<TestingProfile> profile_;
667
668 protected:
669  // Verifies the contents of the bookmark bar node match the contents of the
670  // TestNode.
671  void VerifyModelMatchesNode(TestNode* expected, const BookmarkNode* actual) {
672    ASSERT_EQ(expected->GetChildCount(), actual->GetChildCount());
673    for (int i = 0; i < expected->GetChildCount(); ++i) {
674      TestNode* expected_child = expected->GetChild(i);
675      const BookmarkNode* actual_child = actual->GetChild(i);
676      ASSERT_EQ(expected_child->GetTitle(), actual_child->GetTitle());
677      if (expected_child->value == BookmarkNode::FOLDER) {
678        ASSERT_TRUE(actual_child->type() == BookmarkNode::FOLDER);
679        // Recurse throught children.
680        VerifyModelMatchesNode(expected_child, actual_child);
681        if (HasFatalFailure())
682          return;
683      } else {
684        // No need to check the URL, just the title is enough.
685        ASSERT_TRUE(actual_child->type() == BookmarkNode::URL);
686      }
687    }
688  }
689
690  void VerifyNoDuplicateIDs(BookmarkModel* model) {
691    ui::TreeNodeIterator<const BookmarkNode> it(model->root_node());
692    base::hash_set<int64> ids;
693    while (it.has_next())
694      ASSERT_TRUE(ids.insert(it.Next()->id()).second);
695  }
696
697  void BlockTillBookmarkModelLoaded() {
698    bb_model_ = profile_->GetBookmarkModel();
699    if (!bb_model_->IsLoaded())
700      BlockTillLoaded(bb_model_);
701    else
702      bb_model_->AddObserver(this);
703  }
704
705  // Destroys the current profile, creates a new one and creates the history
706  // service.
707  void RecreateProfile() {
708    // Need to shutdown the old one before creating a new one.
709    profile_.reset(NULL);
710    profile_.reset(new TestingProfile());
711    profile_->CreateHistoryService(true, false);
712  }
713
714  BookmarkModel* bb_model_;
715
716 private:
717  // Blocks until the BookmarkModel has finished loading.
718  void BlockTillLoaded(BookmarkModel* model) {
719    model->AddObserver(this);
720    MessageLoop::current()->Run();
721  }
722
723  // BookmarkModelObserver methods.
724  virtual void Loaded(BookmarkModel* model) {
725    // Balances the call in BlockTillLoaded.
726    MessageLoop::current()->Quit();
727  }
728  virtual void BookmarkNodeMoved(BookmarkModel* model,
729                                 const BookmarkNode* old_parent,
730                                 int old_index,
731                                 const BookmarkNode* new_parent,
732                                 int new_index) {}
733  virtual void BookmarkNodeAdded(BookmarkModel* model,
734                                 const BookmarkNode* parent,
735                                 int index) {}
736  virtual void BookmarkNodeRemoved(BookmarkModel* model,
737                                   const BookmarkNode* parent,
738                                   int old_index,
739                                   const BookmarkNode* node) {}
740  virtual void BookmarkNodeChanged(BookmarkModel* model,
741                                   const BookmarkNode* node) {}
742  virtual void BookmarkNodeChildrenReordered(BookmarkModel* model,
743                                             const BookmarkNode* node) {}
744  virtual void BookmarkNodeFavIconLoaded(BookmarkModel* model,
745                                         const BookmarkNode* node) {}
746
747  MessageLoopForUI message_loop_;
748  BrowserThread ui_thread_;
749  BrowserThread file_thread_;
750};
751
752// Creates a set of nodes in the bookmark bar model, then recreates the
753// bookmark bar model which triggers loading from the db and checks the loaded
754// structure to make sure it is what we first created.
755TEST_F(BookmarkModelTestWithProfile, CreateAndRestore) {
756  struct TestData {
757    // Structure of the children of the bookmark bar model node.
758    const std::string bbn_contents;
759    // Structure of the children of the other node.
760    const std::string other_contents;
761  } data[] = {
762    // See PopulateNodeFromString for a description of these strings.
763    { "", "" },
764    { "a", "b" },
765    { "a [ b ]", "" },
766    { "", "[ b ] a [ c [ d e [ f ] ] ]" },
767    { "a [ b ]", "" },
768    { "a b c [ d e [ f ] ]", "g h i [ j k [ l ] ]"},
769  };
770  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(data); ++i) {
771    // Recreate the profile. We need to reset with NULL first so that the last
772    // HistoryService releases the locks on the files it creates and we can
773    // delete them.
774    profile_.reset(NULL);
775    profile_.reset(new TestingProfile());
776    profile_->CreateBookmarkModel(true);
777    profile_->CreateHistoryService(true, false);
778    BlockTillBookmarkModelLoaded();
779
780    TestNode bbn;
781    PopulateNodeFromString(data[i].bbn_contents, &bbn);
782    PopulateBookmarkNode(&bbn, bb_model_, bb_model_->GetBookmarkBarNode());
783
784    TestNode other;
785    PopulateNodeFromString(data[i].other_contents, &other);
786    PopulateBookmarkNode(&other, bb_model_, bb_model_->other_node());
787
788    profile_->CreateBookmarkModel(false);
789    BlockTillBookmarkModelLoaded();
790
791    VerifyModelMatchesNode(&bbn, bb_model_->GetBookmarkBarNode());
792    VerifyModelMatchesNode(&other, bb_model_->other_node());
793    VerifyNoDuplicateIDs(bb_model_);
794  }
795}
796
797// Test class that creates a BookmarkModel with a real history backend.
798class BookmarkModelTestWithProfile2 : public BookmarkModelTestWithProfile {
799 public:
800  virtual void SetUp() {
801    profile_.reset(new TestingProfile());
802  }
803
804 protected:
805  // Verifies the state of the model matches that of the state in the saved
806  // history file.
807  void VerifyExpectedState() {
808    // Here's the structure we expect:
809    // bbn
810    //   www.google.com - Google
811    //   F1
812    //     http://www.google.com/intl/en/ads/ - Google Advertising
813    //     F11
814    //       http://www.google.com/services/ - Google Business Solutions
815    // other
816    //   OF1
817    //   http://www.google.com/intl/en/about.html - About Google
818    const BookmarkNode* bbn = bb_model_->GetBookmarkBarNode();
819    ASSERT_EQ(2, bbn->GetChildCount());
820
821    const BookmarkNode* child = bbn->GetChild(0);
822    ASSERT_EQ(BookmarkNode::URL, child->type());
823    ASSERT_EQ(ASCIIToUTF16("Google"), child->GetTitle());
824    ASSERT_TRUE(child->GetURL() == GURL("http://www.google.com"));
825
826    child = bbn->GetChild(1);
827    ASSERT_TRUE(child->is_folder());
828    ASSERT_EQ(ASCIIToUTF16("F1"), child->GetTitle());
829    ASSERT_EQ(2, child->GetChildCount());
830
831    const BookmarkNode* parent = child;
832    child = parent->GetChild(0);
833    ASSERT_EQ(BookmarkNode::URL, child->type());
834    ASSERT_EQ(ASCIIToUTF16("Google Advertising"), child->GetTitle());
835    ASSERT_TRUE(child->GetURL() == GURL("http://www.google.com/intl/en/ads/"));
836
837    child = parent->GetChild(1);
838    ASSERT_TRUE(child->is_folder());
839    ASSERT_EQ(ASCIIToUTF16("F11"), child->GetTitle());
840    ASSERT_EQ(1, child->GetChildCount());
841
842    parent = child;
843    child = parent->GetChild(0);
844    ASSERT_EQ(BookmarkNode::URL, child->type());
845    ASSERT_EQ(ASCIIToUTF16("Google Business Solutions"), child->GetTitle());
846    ASSERT_TRUE(child->GetURL() == GURL("http://www.google.com/services/"));
847
848    parent = bb_model_->other_node();
849    ASSERT_EQ(2, parent->GetChildCount());
850
851    child = parent->GetChild(0);
852    ASSERT_TRUE(child->is_folder());
853    ASSERT_EQ(ASCIIToUTF16("OF1"), child->GetTitle());
854    ASSERT_EQ(0, child->GetChildCount());
855
856    child = parent->GetChild(1);
857    ASSERT_EQ(BookmarkNode::URL, child->type());
858    ASSERT_EQ(ASCIIToUTF16("About Google"), child->GetTitle());
859    ASSERT_TRUE(child->GetURL() ==
860                GURL("http://www.google.com/intl/en/about.html"));
861
862    ASSERT_TRUE(bb_model_->IsBookmarked(GURL("http://www.google.com")));
863  }
864
865  void VerifyUniqueIDs() {
866    std::set<int64> ids;
867    bool has_unique = true;
868    VerifyUniqueIDImpl(bb_model_->GetBookmarkBarNode(), &ids, &has_unique);
869    VerifyUniqueIDImpl(bb_model_->other_node(), &ids, &has_unique);
870    ASSERT_TRUE(has_unique);
871  }
872
873 private:
874  void VerifyUniqueIDImpl(const BookmarkNode* node,
875                          std::set<int64>* ids,
876                          bool* has_unique) {
877    if (!*has_unique)
878      return;
879    if (ids->count(node->id()) != 0) {
880      *has_unique = false;
881      return;
882    }
883    ids->insert(node->id());
884    for (int i = 0; i < node->GetChildCount(); ++i) {
885      VerifyUniqueIDImpl(node->GetChild(i), ids, has_unique);
886      if (!*has_unique)
887        return;
888    }
889  }
890};
891
892// Tests migrating bookmarks from db into file. This copies an old history db
893// file containing bookmarks and make sure they are loaded correctly and
894// persisted correctly.
895TEST_F(BookmarkModelTestWithProfile2, MigrateFromDBToFileTest) {
896  // Copy db file over that contains starred table.
897  FilePath old_history_path;
898  PathService::Get(chrome::DIR_TEST_DATA, &old_history_path);
899  old_history_path = old_history_path.AppendASCII("bookmarks");
900  old_history_path = old_history_path.AppendASCII("History_with_starred");
901  FilePath new_history_path = profile_->GetPath();
902  file_util::Delete(new_history_path, true);
903  file_util::CreateDirectory(new_history_path);
904  FilePath new_history_file = new_history_path.Append(
905      chrome::kHistoryFilename);
906  ASSERT_TRUE(file_util::CopyFile(old_history_path, new_history_file));
907
908  // Create the history service making sure it doesn't blow away the file we
909  // just copied.
910  profile_->CreateHistoryService(false, false);
911  profile_->CreateBookmarkModel(true);
912  BlockTillBookmarkModelLoaded();
913
914  // Make sure we loaded OK.
915  VerifyExpectedState();
916  if (HasFatalFailure())
917    return;
918
919  // Make sure the ids are unique.
920  VerifyUniqueIDs();
921  if (HasFatalFailure())
922    return;
923
924  // Create again. This time we shouldn't load from history at all.
925  profile_->CreateBookmarkModel(false);
926  BlockTillBookmarkModelLoaded();
927
928  // Make sure we loaded OK.
929  VerifyExpectedState();
930  if (HasFatalFailure())
931    return;
932
933  VerifyUniqueIDs();
934  if (HasFatalFailure())
935    return;
936
937  // Recreate the history service (with a clean db). Do this just to make sure
938  // we're loading correctly from the bookmarks file.
939  profile_->CreateHistoryService(true, false);
940  profile_->CreateBookmarkModel(false);
941  BlockTillBookmarkModelLoaded();
942  VerifyExpectedState();
943  VerifyUniqueIDs();
944}
945
946// Simple test that removes a bookmark. This test exercises the code paths in
947// History that block till bookmark bar model is loaded.
948TEST_F(BookmarkModelTestWithProfile2, RemoveNotification) {
949  profile_->CreateHistoryService(false, false);
950  profile_->CreateBookmarkModel(true);
951  BlockTillBookmarkModelLoaded();
952
953  // Add a URL.
954  GURL url("http://www.google.com");
955  bb_model_->SetURLStarred(url, string16(), true);
956
957  profile_->GetHistoryService(Profile::EXPLICIT_ACCESS)->AddPage(
958      url, NULL, 1, GURL(), PageTransition::TYPED,
959      history::RedirectList(), history::SOURCE_BROWSED, false);
960
961  // This won't actually delete the URL, rather it'll empty out the visits.
962  // This triggers blocking on the BookmarkModel.
963  profile_->GetHistoryService(Profile::EXPLICIT_ACCESS)->DeleteURL(url);
964}
965
966TEST_F(BookmarkModelTest, Sort) {
967  // Populate the bookmark bar node with nodes for 'B', 'a', 'd' and 'C'.
968  // 'C' and 'a' are folders.
969  TestNode bbn;
970  PopulateNodeFromString("B [ a ] d [ a ]", &bbn);
971  const BookmarkNode* parent = model.GetBookmarkBarNode();
972  PopulateBookmarkNode(&bbn, &model, parent);
973
974  BookmarkNode* child1 = AsMutable(parent->GetChild(1));
975  child1->SetTitle(ASCIIToUTF16("a"));
976  delete child1->Remove(0);
977  BookmarkNode* child3 = AsMutable(parent->GetChild(3));
978  child3->SetTitle(ASCIIToUTF16("C"));
979  delete child3->Remove(0);
980
981  ClearCounts();
982
983  // Sort the children of the bookmark bar node.
984  model.SortChildren(parent);
985
986  // Make sure we were notified.
987  AssertObserverCount(0, 0, 0, 0, 1);
988
989  // Make sure the order matches (remember, 'a' and 'C' are folders and
990  // come first).
991  EXPECT_EQ(parent->GetChild(0)->GetTitle(), ASCIIToUTF16("a"));
992  EXPECT_EQ(parent->GetChild(1)->GetTitle(), ASCIIToUTF16("C"));
993  EXPECT_EQ(parent->GetChild(2)->GetTitle(), ASCIIToUTF16("B"));
994  EXPECT_EQ(parent->GetChild(3)->GetTitle(), ASCIIToUTF16("d"));
995}
996