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