// Copyright 2018 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "components/sync_bookmarks/bookmark_specifics_conversions.h" #include #include #include #include #include "base/guid.h" #include "base/strings/strcat.h" #include "base/strings/utf_string_conversions.h" #include "base/test/metrics/histogram_tester.h" #include "base/time/time.h" #include "components/bookmarks/browser/bookmark_model.h" #include "components/bookmarks/browser/bookmark_node.h" #include "components/bookmarks/test/test_bookmark_client.h" #include "components/favicon/core/test/mock_favicon_service.h" #include "components/sync/base/client_tag_hash.h" #include "components/sync/base/unique_position.h" #include "components/sync/protocol/bookmark_specifics.pb.h" #include "components/sync/protocol/entity_data.h" #include "components/sync/protocol/entity_specifics.pb.h" #include "components/sync/protocol/model_type_state.pb.h" #include "components/sync_bookmarks/synced_bookmark_tracker.h" #include "components/sync_bookmarks/synced_bookmark_tracker_entity.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/skia/include/core/SkBitmap.h" #include "ui/gfx/image/image.h" #include "url/gurl.h" namespace sync_bookmarks { namespace { using bookmarks::TestBookmarkClient; using testing::_; using testing::Eq; using testing::Ge; using testing::IsEmpty; using testing::Not; using testing::NotNull; // Fork of enum InvalidBookmarkSpecificsError. enum class InvalidBookmarkSpecificsError { kEmptySpecifics = 0, kInvalidURL = 1, kIconURLWithoutFavicon = 2, kInvalidIconURL = 3, kNonUniqueMetaInfoKeys = 4, kInvalidGUID = 5, kInvalidParentGUID = 6, kInvalidUniquePosition = 7, kBannedGUID = 8, kMaxValue = kBannedGUID, }; sync_pb::UniquePosition RandomUniquePosition() { return syncer::UniquePosition::InitialPosition( syncer::UniquePosition::RandomSuffix()) .ToProto(); } // Returns a single-color 16x16 image using |color|. gfx::Image CreateTestImage(SkColor color) { SkBitmap bitmap; bitmap.allocN32Pixels(16, 16); bitmap.eraseColor(color); return gfx::Image::CreateFrom1xBitmap(bitmap); } TEST(BookmarkSpecificsConversionsTest, ShouldCreateSpecificsFromBookmarkNode) { const GURL kUrl("http://www.url.com"); const std::string kTitle = "Title"; const base::Time kTime = base::Time::Now(); const std::string kKey1 = "key1"; const std::string kValue1 = "value1"; const std::string kKey2 = "key2"; const std::string kValue2 = "value2"; const syncer::UniquePosition kUniquePosition = syncer::UniquePosition::InitialPosition( syncer::UniquePosition::RandomSuffix()); std::unique_ptr model = bookmarks::TestBookmarkClient::CreateModel(); const bookmarks::BookmarkNode* bookmark_bar_node = model->bookmark_bar_node(); const bookmarks::BookmarkNode* node = model->AddURL( /*parent=*/bookmark_bar_node, /*index=*/0, base::UTF8ToUTF16(kTitle), kUrl); ASSERT_THAT(node, NotNull()); model->SetDateAdded(node, kTime); model->UpdateLastUsedTime(node, kTime); model->SetNodeMetaInfo(node, kKey1, kValue1); model->SetNodeMetaInfo(node, kKey2, kValue2); sync_pb::EntitySpecifics specifics = CreateSpecificsFromBookmarkNode( node, model.get(), kUniquePosition.ToProto(), /*force_favicon_load=*/false); const sync_pb::BookmarkSpecifics& bm_specifics = specifics.bookmark(); EXPECT_THAT(bm_specifics.guid(), Eq(node->guid().AsLowercaseString())); EXPECT_THAT(bm_specifics.parent_guid(), Eq(bookmarks::BookmarkNode::kBookmarkBarNodeGuid)); EXPECT_THAT(bm_specifics.type(), Eq(sync_pb::BookmarkSpecifics::URL)); EXPECT_THAT(bm_specifics.legacy_canonicalized_title(), Eq(kTitle)); EXPECT_THAT(GURL(bm_specifics.url()), Eq(kUrl)); EXPECT_THAT(base::Time::FromDeltaSinceWindowsEpoch( base::Microseconds(bm_specifics.creation_time_us())), Eq(kTime)); EXPECT_THAT(base::Time::FromDeltaSinceWindowsEpoch( base::Microseconds(bm_specifics.last_used_time_us())), Eq(kTime)); EXPECT_TRUE(syncer::UniquePosition::FromProto(bm_specifics.unique_position()) .Equals(kUniquePosition)); for (const sync_pb::MetaInfo& meta_info : bm_specifics.meta_info()) { std::string value; node->GetMetaInfo(meta_info.key(), &value); EXPECT_THAT(meta_info.value(), Eq(value)); } } TEST(BookmarkSpecificsConversionsTest, ShouldCreateSpecificsFromBookmarkNodeNoDateLastUsed) { const GURL kUrl("http://www.url.com"); const std::string kTitle = "Title"; const syncer::UniquePosition kUniquePosition = syncer::UniquePosition::InitialPosition( syncer::UniquePosition::RandomSuffix()); std::unique_ptr model = bookmarks::TestBookmarkClient::CreateModel(); const bookmarks::BookmarkNode* bookmark_bar_node = model->bookmark_bar_node(); const bookmarks::BookmarkNode* node = model->AddURL( /*parent=*/bookmark_bar_node, /*index=*/0, base::UTF8ToUTF16(kTitle), kUrl); sync_pb::EntitySpecifics specifics = CreateSpecificsFromBookmarkNode( node, model.get(), kUniquePosition.ToProto(), /*force_favicon_load=*/false); const sync_pb::BookmarkSpecifics& bm_specifics = specifics.bookmark(); EXPECT_THAT(bm_specifics.guid(), Eq(node->guid().AsLowercaseString())); EXPECT_THAT(bm_specifics.parent_guid(), Eq(bookmarks::BookmarkNode::kBookmarkBarNodeGuid)); EXPECT_FALSE(bm_specifics.has_last_used_time_us()); } TEST(BookmarkSpecificsConversionsTest, ShouldCreateSpecificsFromBookmarkNodeWithIllegalTitle) { std::unique_ptr model = bookmarks::TestBookmarkClient::CreateModel(); const bookmarks::BookmarkNode* bookmark_bar_node = model->bookmark_bar_node(); const std::vector illegal_titles = {"", ".", ".."}; size_t index = 0; for (const std::string& illegal_title : illegal_titles) { const bookmarks::BookmarkNode* node = model->AddURL( /*parent=*/bookmark_bar_node, index++, base::UTF8ToUTF16(illegal_title), GURL("http://www.url.com")); ASSERT_THAT(node, NotNull()); sync_pb::EntitySpecifics specifics = CreateSpecificsFromBookmarkNode( node, model.get(), RandomUniquePosition(), /*force_favicon_load=*/false); // Legacy clients append a space to illegal titles. EXPECT_THAT(specifics.bookmark().legacy_canonicalized_title(), Eq(illegal_title + " ")); } } TEST(BookmarkSpecificsConversionsTest, ShouldCreateSpecificsWithoutUrlFromFolderNode) { std::unique_ptr model = bookmarks::TestBookmarkClient::CreateModel(); const bookmarks::BookmarkNode* bookmark_bar_node = model->bookmark_bar_node(); const bookmarks::BookmarkNode* node = model->AddFolder( /*parent=*/bookmark_bar_node, /*index=*/0, u"Title"); ASSERT_THAT(node, NotNull()); sync_pb::EntitySpecifics specifics = CreateSpecificsFromBookmarkNode(node, model.get(), RandomUniquePosition(), /*force_favicon_load=*/false); const sync_pb::BookmarkSpecifics& bm_specifics = specifics.bookmark(); EXPECT_FALSE(bm_specifics.has_url()); EXPECT_THAT(bm_specifics.type(), Eq(sync_pb::BookmarkSpecifics::FOLDER)); EXPECT_FALSE(bm_specifics.has_last_used_time_us()); } TEST(BookmarkSpecificsConversionsTest, ShouldLoadFaviconWhenCreatingSpecificsFromBookmarkNode) { auto client = std::make_unique(); TestBookmarkClient* client_ptr = client.get(); std::unique_ptr model = TestBookmarkClient::CreateModelWithClient(std::move(client)); const bookmarks::BookmarkNode* bookmark_bar_node = model->bookmark_bar_node(); const bookmarks::BookmarkNode* node = model->AddURL( /*parent=*/bookmark_bar_node, /*index=*/0, u"Title", GURL("http://www.url.com")); ASSERT_THAT(node, NotNull()); ASSERT_FALSE(node->is_favicon_loaded()); ASSERT_FALSE(client_ptr->HasFaviconLoadTasks()); sync_pb::EntitySpecifics specifics = CreateSpecificsFromBookmarkNode(node, model.get(), RandomUniquePosition(), /*force_favicon_load=*/true); EXPECT_TRUE(client_ptr->HasFaviconLoadTasks()); EXPECT_FALSE(specifics.bookmark().has_favicon()); EXPECT_FALSE(specifics.bookmark().has_icon_url()); } TEST(BookmarkSpecificsConversionsTest, ShouldNotLoadFaviconWhenCreatingSpecificsFromBookmarkNode) { auto client = std::make_unique(); TestBookmarkClient* client_ptr = client.get(); std::unique_ptr model = TestBookmarkClient::CreateModelWithClient(std::move(client)); const bookmarks::BookmarkNode* bookmark_bar_node = model->bookmark_bar_node(); const bookmarks::BookmarkNode* node = model->AddURL( /*parent=*/bookmark_bar_node, /*index=*/0, u"Title", GURL("http://www.url.com")); ASSERT_THAT(node, NotNull()); ASSERT_FALSE(node->is_favicon_loaded()); ASSERT_FALSE(client_ptr->HasFaviconLoadTasks()); sync_pb::EntitySpecifics specifics = CreateSpecificsFromBookmarkNode( node, model.get(), RandomUniquePosition(), /*force_favicon_load=*/false); EXPECT_FALSE(client_ptr->HasFaviconLoadTasks()); EXPECT_FALSE(specifics.bookmark().has_favicon()); EXPECT_FALSE(specifics.bookmark().has_icon_url()); } TEST(BookmarkSpecificsConversionsTest, ShouldIncludeFaviconWhenCreatingSpecificsFromBookmarkNodeIfLoaded) { const GURL kBookmarkUrl("http://www.url.com"); const GURL kIconUrl("http://www.icon-url.com"); const SkColor kColor = SK_ColorRED; auto client = std::make_unique(); TestBookmarkClient* client_ptr = client.get(); std::unique_ptr model = TestBookmarkClient::CreateModelWithClient(std::move(client)); const bookmarks::BookmarkNode* bookmark_bar_node = model->bookmark_bar_node(); const bookmarks::BookmarkNode* node = model->AddURL( /*parent=*/bookmark_bar_node, /*index=*/0, u"Title", kBookmarkUrl); ASSERT_THAT(node, NotNull()); ASSERT_FALSE(node->is_favicon_loaded()); // Complete the loading of the favicon as part of the test setup. model->GetFavicon(node); ASSERT_TRUE(client_ptr->HasFaviconLoadTasks()); client_ptr->SimulateFaviconLoaded(kBookmarkUrl, kIconUrl, CreateTestImage(kColor)); ASSERT_TRUE(node->is_favicon_loaded()); sync_pb::EntitySpecifics specifics = CreateSpecificsFromBookmarkNode(node, model.get(), RandomUniquePosition(), /*force_favicon_load=*/false); EXPECT_THAT(specifics.bookmark().favicon(), Not(IsEmpty())); EXPECT_THAT(specifics.bookmark().icon_url(), Eq(kIconUrl)); // Verify that the |favicon| field is properly encoded. const gfx::Image favicon = gfx::Image::CreateFrom1xPNGBytes( reinterpret_cast( specifics.bookmark().favicon().data()), specifics.bookmark().favicon().size()); EXPECT_THAT(favicon.Width(), Eq(16)); EXPECT_THAT(favicon.Height(), Eq(16)); EXPECT_THAT(favicon.AsBitmap().getColor(1, 1), Eq(kColor)); } TEST(BookmarkSpecificsConversionsTest, ShouldOmitLargeFaviconUrlWhenCreatingSpecificsFromBookmarkNode) { const GURL kBookmarkUrl("http://www.url.com"); const GURL kIconUrl( base::StrCat({"http://www.icon-url.com/", std::string(5000, 'a')})); const SkColor kColor = SK_ColorRED; // This test uses a valid but very long icon URL, larger than // |kMaxFaviconUrlSize|. ASSERT_TRUE(kIconUrl.is_valid()); ASSERT_THAT(kIconUrl.spec().size(), Ge(5000u)); auto client = std::make_unique(); TestBookmarkClient* client_ptr = client.get(); std::unique_ptr model = TestBookmarkClient::CreateModelWithClient(std::move(client)); const bookmarks::BookmarkNode* bookmark_bar_node = model->bookmark_bar_node(); const bookmarks::BookmarkNode* node = model->AddURL( /*parent=*/bookmark_bar_node, /*index=*/0, u"Title", kBookmarkUrl); ASSERT_THAT(node, NotNull()); ASSERT_FALSE(node->is_favicon_loaded()); // Complete the loading of the favicon as part of the test setup. model->GetFavicon(node); ASSERT_TRUE(client_ptr->HasFaviconLoadTasks()); client_ptr->SimulateFaviconLoaded(kBookmarkUrl, kIconUrl, CreateTestImage(kColor)); ASSERT_TRUE(node->is_favicon_loaded()); sync_pb::EntitySpecifics specifics = CreateSpecificsFromBookmarkNode(node, model.get(), RandomUniquePosition(), /*force_favicon_load=*/false); // The icon URL should be omitted (populated with the empty string). EXPECT_TRUE(specifics.bookmark().has_icon_url()); EXPECT_THAT(specifics.bookmark().icon_url(), IsEmpty()); // The favicon image itself should be synced. EXPECT_THAT(specifics.bookmark().favicon(), Not(IsEmpty())); } TEST(BookmarkSpecificsConversionsTest, ShouldCreateNonFolderBookmarkNodeFromSpecifics) { const GURL kUrl("http://www.url.com"); const base::GUID kGuid = base::GUID::GenerateRandomV4(); const std::string kTitle = "Title"; const base::Time kTime = base::Time::Now(); const GURL kIconUrl("http://www.icon-url.com"); const std::string kKey1 = "key1"; const std::string kValue1 = "value1"; const std::string kKey2 = "key2"; const std::string kValue2 = "value2"; sync_pb::BookmarkSpecifics bm_specifics; bm_specifics.set_url(kUrl.spec()); bm_specifics.set_guid(kGuid.AsLowercaseString()); bm_specifics.set_icon_url(kIconUrl.spec()); bm_specifics.set_favicon("PNG"); bm_specifics.set_legacy_canonicalized_title(kTitle); bm_specifics.set_creation_time_us( kTime.ToDeltaSinceWindowsEpoch().InMicroseconds()); bm_specifics.set_last_used_time_us( kTime.ToDeltaSinceWindowsEpoch().InMicroseconds()); bm_specifics.set_type(sync_pb::BookmarkSpecifics::URL); // Parent GUID and unique position are ignored by // CreateBookmarkNodeFromSpecifics(), but are required here to pass DCHECKs. bm_specifics.set_parent_guid(bookmarks::BookmarkNode::kBookmarkBarNodeGuid); *bm_specifics.mutable_unique_position() = RandomUniquePosition(); sync_pb::MetaInfo* meta_info1 = bm_specifics.add_meta_info(); meta_info1->set_key(kKey1); meta_info1->set_value(kValue1); sync_pb::MetaInfo* meta_info2 = bm_specifics.add_meta_info(); meta_info2->set_key(kKey2); meta_info2->set_value(kValue2); std::unique_ptr model = bookmarks::TestBookmarkClient::CreateModel(); testing::NiceMock favicon_service; EXPECT_CALL(favicon_service, AddPageNoVisitForBookmark(kUrl, base::UTF8ToUTF16(kTitle))); EXPECT_CALL(favicon_service, MergeFavicon(kUrl, kIconUrl, _, _, _)); base::HistogramTester histogram_tester; const bookmarks::BookmarkNode* node = CreateBookmarkNodeFromSpecifics( bm_specifics, /*parent=*/model->bookmark_bar_node(), /*index=*/0, model.get(), &favicon_service); ASSERT_THAT(node, NotNull()); EXPECT_THAT(node->guid(), Eq(kGuid)); EXPECT_THAT(node->GetTitle(), Eq(base::UTF8ToUTF16(kTitle))); EXPECT_FALSE(node->is_folder()); EXPECT_THAT(node->url(), Eq(kUrl)); EXPECT_THAT(node->date_added(), Eq(kTime)); EXPECT_THAT(node->date_last_used(), Eq(kTime)); std::string value1; node->GetMetaInfo(kKey1, &value1); EXPECT_THAT(value1, Eq(kValue1)); std::string value2; node->GetMetaInfo(kKey2, &value2); EXPECT_THAT(value2, Eq(kValue2)); histogram_tester.ExpectUniqueSample( "Sync.BookmarkSpecificsExcludingFoldersContainFavicon", /*sample=*/true, /*expected_bucket_count=*/1); } TEST(BookmarkSpecificsConversionsTest, ShouldCreateFolderFromSpecifics) { const base::GUID kGuid = base::GUID::GenerateRandomV4(); const std::string kTitle = "Title"; const base::Time kTime = base::Time::Now(); const std::string kKey1 = "key1"; const std::string kValue1 = "value1"; const std::string kKey2 = "key2"; const std::string kValue2 = "value2"; sync_pb::BookmarkSpecifics bm_specifics; bm_specifics.set_guid(kGuid.AsLowercaseString()); bm_specifics.set_legacy_canonicalized_title(kTitle); bm_specifics.set_creation_time_us( kTime.ToDeltaSinceWindowsEpoch().InMicroseconds()); bm_specifics.set_type(sync_pb::BookmarkSpecifics::FOLDER); // Parent GUID and unique position are ignored by // CreateBookmarkNodeFromSpecifics(), but are required here to pass DCHECKs. bm_specifics.set_parent_guid(bookmarks::BookmarkNode::kBookmarkBarNodeGuid); *bm_specifics.mutable_unique_position() = RandomUniquePosition(); sync_pb::MetaInfo* meta_info1 = bm_specifics.add_meta_info(); meta_info1->set_key(kKey1); meta_info1->set_value(kValue1); sync_pb::MetaInfo* meta_info2 = bm_specifics.add_meta_info(); meta_info2->set_key(kKey2); meta_info2->set_value(kValue2); std::unique_ptr model = bookmarks::TestBookmarkClient::CreateModel(); testing::NiceMock favicon_service; EXPECT_CALL(favicon_service, AddPageNoVisitForBookmark).Times(0); EXPECT_CALL(favicon_service, MergeFavicon).Times(0); EXPECT_CALL(favicon_service, DeleteFaviconMappings).Times(0); base::HistogramTester histogram_tester; const bookmarks::BookmarkNode* node = CreateBookmarkNodeFromSpecifics( bm_specifics, /*parent=*/model->bookmark_bar_node(), /*index=*/0, model.get(), &favicon_service); ASSERT_THAT(node, NotNull()); EXPECT_THAT(node->guid(), Eq(kGuid)); EXPECT_THAT(node->GetTitle(), Eq(base::UTF8ToUTF16(kTitle))); EXPECT_TRUE(node->is_folder()); // TODO(crbug.com/1214840): Folders should propagate the creation time into // BookmarkModel, just like non-folders. EXPECT_THAT(node->date_added(), Ge(kTime)); std::string value1; node->GetMetaInfo(kKey1, &value1); EXPECT_THAT(value1, Eq(kValue1)); std::string value2; node->GetMetaInfo(kKey2, &value2); EXPECT_THAT(value2, Eq(kValue2)); // The histogram should not be recorded for folders. histogram_tester.ExpectTotalCount( "Sync.BookmarkSpecificsExcludingFoldersContainFavicon", 0); } TEST(BookmarkSpecificsConversionsTest, ShouldPreferFullTitleOnCreatingBookmarkNodeFromSpecifics) { const GURL kUrl("http://www.url.com"); const base::GUID kGuid = base::GUID::GenerateRandomV4(); const std::string kTitle = "Title"; const std::string kFullTitle = "Title Long Version"; const base::Time kTime = base::Time::Now(); const GURL kIconUrl("http://www.icon-url.com"); const std::string kKey1 = "key1"; const std::string kValue1 = "value1"; const std::string kKey2 = "key2"; const std::string kValue2 = "value2"; sync_pb::BookmarkSpecifics bm_specifics; bm_specifics.set_url(kUrl.spec()); bm_specifics.set_guid(kGuid.AsLowercaseString()); bm_specifics.set_icon_url(kIconUrl.spec()); bm_specifics.set_favicon("PNG"); bm_specifics.set_legacy_canonicalized_title(kTitle); bm_specifics.set_full_title(kFullTitle); bm_specifics.set_creation_time_us( kTime.ToDeltaSinceWindowsEpoch().InMicroseconds()); bm_specifics.set_type(sync_pb::BookmarkSpecifics::URL); // Parent GUID and unique position are ignored by // CreateBookmarkNodeFromSpecifics(), but are required here to pass DCHECKs. bm_specifics.set_parent_guid(bookmarks::BookmarkNode::kBookmarkBarNodeGuid); *bm_specifics.mutable_unique_position() = RandomUniquePosition(); sync_pb::MetaInfo* meta_info1 = bm_specifics.add_meta_info(); meta_info1->set_key(kKey1); meta_info1->set_value(kValue1); sync_pb::MetaInfo* meta_info2 = bm_specifics.add_meta_info(); meta_info2->set_key(kKey2); meta_info2->set_value(kValue2); std::unique_ptr model = bookmarks::TestBookmarkClient::CreateModel(); testing::NiceMock favicon_service; EXPECT_CALL(favicon_service, AddPageNoVisitForBookmark(kUrl, base::UTF8ToUTF16(kFullTitle))); EXPECT_CALL(favicon_service, MergeFavicon(kUrl, kIconUrl, _, _, _)); const bookmarks::BookmarkNode* node = CreateBookmarkNodeFromSpecifics( bm_specifics, /*parent=*/model->bookmark_bar_node(), /*index=*/0, model.get(), &favicon_service); ASSERT_THAT(node, NotNull()); EXPECT_THAT(node->guid(), Eq(kGuid)); EXPECT_THAT(node->GetTitle(), Eq(base::UTF8ToUTF16(kFullTitle))); EXPECT_THAT(node->url(), Eq(kUrl)); EXPECT_THAT(node->date_added(), Eq(kTime)); std::string value1; node->GetMetaInfo(kKey1, &value1); EXPECT_THAT(value1, Eq(kValue1)); std::string value2; node->GetMetaInfo(kKey2, &value2); EXPECT_THAT(value2, Eq(kValue2)); } TEST(BookmarkSpecificsConversionsTest, ShouldCreateBookmarkNodeFromSpecificsWithIllegalTitle) { std::unique_ptr model = bookmarks::TestBookmarkClient::CreateModel(); testing::NiceMock favicon_service; const std::vector illegal_titles = {"", ".", ".."}; size_t index = 0; for (const std::string& illegal_title : illegal_titles) { sync_pb::BookmarkSpecifics bm_specifics; bm_specifics.set_url("http://www.url.com"); bm_specifics.set_guid(base::GUID::GenerateRandomV4().AsLowercaseString()); // Legacy clients append an extra space to illegal clients. bm_specifics.set_legacy_canonicalized_title(illegal_title + " "); bm_specifics.set_type(sync_pb::BookmarkSpecifics::URL); // Parent GUID and unique position are ignored by // CreateBookmarkNodeFromSpecifics(), but are required here to pass DCHECKs. bm_specifics.set_parent_guid(bookmarks::BookmarkNode::kBookmarkBarNodeGuid); *bm_specifics.mutable_unique_position() = RandomUniquePosition(); const bookmarks::BookmarkNode* node = CreateBookmarkNodeFromSpecifics(bm_specifics, /*parent=*/model->bookmark_bar_node(), index++, model.get(), &favicon_service); ASSERT_THAT(node, NotNull()); // The node should be created without the extra space. EXPECT_THAT(node->GetTitle(), Eq(base::UTF8ToUTF16(illegal_title))); } } TEST(BookmarkSpecificsConversionsTest, ShouldCreateBookmarkNodeFromSpecificsWithFaviconAndWithoutIconUrl) { const GURL kUrl("http://www.url.com"); const base::GUID kGuid = base::GUID::GenerateRandomV4(); const std::string kTitle = "Title"; const GURL kIconUrl("http://www.icon-url.com"); sync_pb::BookmarkSpecifics bm_specifics; bm_specifics.set_url(kUrl.spec()); bm_specifics.set_guid(kGuid.AsLowercaseString()); bm_specifics.set_favicon("PNG"); bm_specifics.set_legacy_canonicalized_title(kTitle); bm_specifics.set_type(sync_pb::BookmarkSpecifics::URL); // Parent GUID and unique position are ignored by // CreateBookmarkNodeFromSpecifics(), but are required here to pass DCHECKs. bm_specifics.set_parent_guid(bookmarks::BookmarkNode::kBookmarkBarNodeGuid); *bm_specifics.mutable_unique_position() = RandomUniquePosition(); std::unique_ptr model = bookmarks::TestBookmarkClient::CreateModel(); testing::NiceMock favicon_service; // The favicon service should be called with page url since the icon url is // missing. EXPECT_CALL(favicon_service, MergeFavicon(kUrl, kUrl, _, _, _)); const bookmarks::BookmarkNode* node = CreateBookmarkNodeFromSpecifics( bm_specifics, /*parent=*/model->bookmark_bar_node(), /*index=*/0, model.get(), &favicon_service); EXPECT_THAT(node, NotNull()); } TEST(BookmarkSpecificsConversionsTest, ShouldUpdateBookmarkNodeFromSpecifics) { const GURL kUrl("http://www.url.com"); const std::string kTitle = "Title"; const base::Time kTime = base::Time::Now(); const std::string kKey1 = "key1"; const std::string kValue1 = "value1"; const std::string kKey2 = "key2"; const std::string kValue2 = "value2"; std::unique_ptr model = bookmarks::TestBookmarkClient::CreateModel(); const bookmarks::BookmarkNode* bookmark_bar_node = model->bookmark_bar_node(); const bookmarks::BookmarkNode* node = model->AddURL( /*parent=*/bookmark_bar_node, /*index=*/0, base::UTF8ToUTF16(kTitle), GURL(kUrl)); ASSERT_THAT(node, NotNull()); model->SetNodeMetaInfo(node, kKey1, kValue1); model->SetNodeMetaInfo(node, kKey2, kValue2); const GURL kNewUrl("http://www.new-url.com"); const std::string kNewTitle = "NewTitle"; const GURL kNewIconUrl("http://www.new-icon-url.com"); const std::string kNewValue1 = "new-value1"; const std::string kNewValue2 = "new-value2"; sync_pb::BookmarkSpecifics bm_specifics; bm_specifics.set_url(kNewUrl.spec()); bm_specifics.set_guid(node->guid().AsLowercaseString()); bm_specifics.set_icon_url(kNewIconUrl.spec()); bm_specifics.set_favicon("PNG"); bm_specifics.set_legacy_canonicalized_title(kNewTitle); bm_specifics.set_creation_time_us( kTime.ToDeltaSinceWindowsEpoch().InMicroseconds()); bm_specifics.set_last_used_time_us( kTime.ToDeltaSinceWindowsEpoch().InMicroseconds()); sync_pb::MetaInfo* meta_info1 = bm_specifics.add_meta_info(); meta_info1->set_key(kKey1); meta_info1->set_value(kNewValue1); sync_pb::MetaInfo* meta_info2 = bm_specifics.add_meta_info(); meta_info2->set_key(kKey2); meta_info2->set_value(kNewValue2); testing::NiceMock favicon_service; EXPECT_CALL(favicon_service, AddPageNoVisitForBookmark(kNewUrl, base::UTF8ToUTF16(kNewTitle))); EXPECT_CALL(favicon_service, MergeFavicon(kNewUrl, kNewIconUrl, _, _, _)); UpdateBookmarkNodeFromSpecifics(bm_specifics, node, model.get(), &favicon_service); EXPECT_THAT(node->GetTitle(), Eq(base::UTF8ToUTF16(kNewTitle))); EXPECT_THAT(node->url(), Eq(kNewUrl)); std::string value1; node->GetMetaInfo(kKey1, &value1); EXPECT_THAT(value1, Eq(kNewValue1)); std::string value2; node->GetMetaInfo(kKey2, &value2); EXPECT_THAT(value2, Eq(kNewValue2)); } TEST(BookmarkSpecificsConversionsTest, ShouldPreferFullTitleOnUpdateBookmarkNodeFromSpecifics) { const GURL kUrl("http://www.url.com"); const std::string kTitle = "Title"; const base::Time kTime = base::Time::Now(); const std::string kKey1 = "key1"; const std::string kValue1 = "value1"; const std::string kKey2 = "key2"; const std::string kValue2 = "value2"; std::unique_ptr model = bookmarks::TestBookmarkClient::CreateModel(); const bookmarks::BookmarkNode* bookmark_bar_node = model->bookmark_bar_node(); const bookmarks::BookmarkNode* node = model->AddURL( /*parent=*/bookmark_bar_node, /*index=*/0, base::UTF8ToUTF16(kTitle), GURL(kUrl)); ASSERT_THAT(node, NotNull()); model->SetNodeMetaInfo(node, kKey1, kValue1); model->SetNodeMetaInfo(node, kKey2, kValue2); const GURL kNewUrl("http://www.new-url.com"); const std::string kNewTitle = "NewTitle"; const std::string kNewFullTitle = "NewTitle Long Version"; const GURL kNewIconUrl("http://www.new-icon-url.com"); const std::string kNewValue1 = "new-value1"; const std::string kNewValue2 = "new-value2"; sync_pb::BookmarkSpecifics bm_specifics; bm_specifics.set_url(kNewUrl.spec()); bm_specifics.set_guid(node->guid().AsLowercaseString()); bm_specifics.set_icon_url(kNewIconUrl.spec()); bm_specifics.set_favicon("PNG"); bm_specifics.set_legacy_canonicalized_title(kNewTitle); bm_specifics.set_full_title(kNewFullTitle); bm_specifics.set_creation_time_us( kTime.ToDeltaSinceWindowsEpoch().InMicroseconds()); sync_pb::MetaInfo* meta_info1 = bm_specifics.add_meta_info(); meta_info1->set_key(kKey1); meta_info1->set_value(kNewValue1); sync_pb::MetaInfo* meta_info2 = bm_specifics.add_meta_info(); meta_info2->set_key(kKey2); meta_info2->set_value(kNewValue2); testing::NiceMock favicon_service; EXPECT_CALL(favicon_service, AddPageNoVisitForBookmark( kNewUrl, base::UTF8ToUTF16(kNewFullTitle))); EXPECT_CALL(favicon_service, MergeFavicon(kNewUrl, kNewIconUrl, _, _, _)); UpdateBookmarkNodeFromSpecifics(bm_specifics, node, model.get(), &favicon_service); EXPECT_THAT(node->GetTitle(), Eq(base::UTF8ToUTF16(kNewFullTitle))); EXPECT_THAT(node->url(), Eq(kNewUrl)); std::string value1; node->GetMetaInfo(kKey1, &value1); EXPECT_THAT(value1, Eq(kNewValue1)); std::string value2; node->GetMetaInfo(kKey2, &value2); EXPECT_THAT(value2, Eq(kNewValue2)); } TEST(BookmarkSpecificsConversionsTest, ShouldUpdateBookmarkNodeFromSpecificsWithFaviconAndWithoutIconUrl) { const GURL kUrl("http://www.url.com"); const std::string kTitle = "Title"; std::unique_ptr model = bookmarks::TestBookmarkClient::CreateModel(); const bookmarks::BookmarkNode* bookmark_bar_node = model->bookmark_bar_node(); const bookmarks::BookmarkNode* node = model->AddURL( /*parent=*/bookmark_bar_node, /*index=*/0, base::UTF8ToUTF16(kTitle), GURL(kUrl)); ASSERT_THAT(node, NotNull()); const GURL kNewUrl("http://www.new-url.com"); sync_pb::BookmarkSpecifics bm_specifics; bm_specifics.set_url(kNewUrl.spec()); bm_specifics.set_guid(node->guid().AsLowercaseString()); bm_specifics.set_favicon("PNG"); testing::NiceMock favicon_service; // The favicon service should be called with page url since the icon url is // missing. EXPECT_CALL(favicon_service, MergeFavicon(kNewUrl, kNewUrl, _, _, _)); UpdateBookmarkNodeFromSpecifics(bm_specifics, node, model.get(), &favicon_service); } TEST(BookmarkSpecificsConversionsTest, ShouldBeValidBookmarkSpecifics) { sync_pb::BookmarkSpecifics bm_specifics; // URL is irrelevant for a folder. bm_specifics.set_url("INVALID_URL"); bm_specifics.set_guid(base::GUID::GenerateRandomV4().AsLowercaseString()); bm_specifics.set_parent_guid( base::GUID::GenerateRandomV4().AsLowercaseString()); bm_specifics.set_type(sync_pb::BookmarkSpecifics::FOLDER); *bm_specifics.mutable_unique_position() = RandomUniquePosition(); EXPECT_TRUE(IsValidBookmarkSpecifics(bm_specifics)); bm_specifics.set_type(sync_pb::BookmarkSpecifics::URL); ASSERT_FALSE(IsValidBookmarkSpecifics(bm_specifics)); bm_specifics.set_url("http://www.valid-url.com"); EXPECT_TRUE(IsValidBookmarkSpecifics(bm_specifics)); } TEST(BookmarkSpecificsConversionsTest, ShouldBeValidBookmarkSpecificsWithFaviconAndWithoutIconUrl) { sync_pb::BookmarkSpecifics bm_specifics; bm_specifics.set_url("http://www.valid-url.com"); bm_specifics.set_favicon("PNG"); bm_specifics.set_guid(base::GUID::GenerateRandomV4().AsLowercaseString()); bm_specifics.set_parent_guid( base::GUID::GenerateRandomV4().AsLowercaseString()); bm_specifics.set_type(sync_pb::BookmarkSpecifics::URL); *bm_specifics.mutable_unique_position() = RandomUniquePosition(); EXPECT_TRUE(IsValidBookmarkSpecifics(bm_specifics)); } TEST(BookmarkSpecificsConversionsTest, ShouldBeInvalidBookmarkSpecificsWithoutFaviconAndWithIconUrl) { sync_pb::BookmarkSpecifics bm_specifics; bm_specifics.set_url("http://www.valid-url.com"); bm_specifics.set_icon_url("http://www.valid-icon-url.com"); bm_specifics.set_guid(base::GUID::GenerateRandomV4().AsLowercaseString()); bm_specifics.set_type(sync_pb::BookmarkSpecifics::URL); base::HistogramTester histogram_tester; EXPECT_FALSE(IsValidBookmarkSpecifics(bm_specifics)); histogram_tester.ExpectBucketCount( "Sync.InvalidBookmarkSpecifics", /*sample=*/InvalidBookmarkSpecificsError::kIconURLWithoutFavicon, /*expected_count=*/1); } TEST(BookmarkSpecificsConversionsTest, ShouldBeInvalidBookmarkSpecificsWithInvalidGUID) { base::HistogramTester histogram_tester; sync_pb::BookmarkSpecifics bm_specifics; bm_specifics.set_type(sync_pb::BookmarkSpecifics::FOLDER); bm_specifics.set_parent_guid( base::GUID::GenerateRandomV4().AsLowercaseString()); *bm_specifics.mutable_unique_position() = RandomUniquePosition(); // No GUID. bm_specifics.clear_guid(); EXPECT_FALSE(IsValidBookmarkSpecifics(bm_specifics)); histogram_tester.ExpectBucketCount( "Sync.InvalidBookmarkSpecifics", /*sample=*/InvalidBookmarkSpecificsError::kInvalidGUID, /*expected_count=*/1); // Add empty GUID. bm_specifics.set_guid(""); EXPECT_FALSE(IsValidBookmarkSpecifics(bm_specifics)); histogram_tester.ExpectBucketCount( "Sync.InvalidBookmarkSpecifics", /*sample=*/InvalidBookmarkSpecificsError::kInvalidGUID, /*expected_count=*/2); // Add invalid GUID. bm_specifics.set_guid("INVALID GUID"); EXPECT_FALSE(IsValidBookmarkSpecifics(bm_specifics)); histogram_tester.ExpectBucketCount( "Sync.InvalidBookmarkSpecifics", /*sample=*/InvalidBookmarkSpecificsError::kInvalidGUID, /*expected_count=*/3); // Add valid GUID. bm_specifics.set_guid(base::GUID::GenerateRandomV4().AsLowercaseString()); ASSERT_TRUE(IsValidBookmarkSpecifics(bm_specifics)); } TEST(BookmarkSpecificsConversionsTest, ShouldBeInvalidBookmarkSpecificsWithInvalidParentGUID) { base::HistogramTester histogram_tester; sync_pb::BookmarkSpecifics bm_specifics; bm_specifics.set_guid(base::GUID::GenerateRandomV4().AsLowercaseString()); bm_specifics.set_type(sync_pb::BookmarkSpecifics::FOLDER); *bm_specifics.mutable_unique_position() = RandomUniquePosition(); // No parent GUID. bm_specifics.clear_parent_guid(); EXPECT_FALSE(IsValidBookmarkSpecifics(bm_specifics)); histogram_tester.ExpectBucketCount( "Sync.InvalidBookmarkSpecifics", /*sample=*/InvalidBookmarkSpecificsError::kInvalidParentGUID, /*expected_count=*/1); // Add invalid parent GUID. bm_specifics.set_parent_guid("INVALID GUID"); EXPECT_FALSE(IsValidBookmarkSpecifics(bm_specifics)); histogram_tester.ExpectBucketCount( "Sync.InvalidBookmarkSpecifics", /*sample=*/InvalidBookmarkSpecificsError::kInvalidParentGUID, /*expected_count=*/2); // Add valid GUID. bm_specifics.set_parent_guid( base::GUID::GenerateRandomV4().AsLowercaseString()); ASSERT_TRUE(IsValidBookmarkSpecifics(bm_specifics)); } TEST(BookmarkSpecificsConversionsTest, ShouldBeInvalidBookmarkSpecificsDueToInvalidUniquePosition) { sync_pb::BookmarkSpecifics bm_specifics; bm_specifics.set_type(sync_pb::BookmarkSpecifics::FOLDER); // Leave |unique_position| field populated but empty. bm_specifics.mutable_unique_position(); base::HistogramTester histogram_tester; EXPECT_FALSE(IsValidBookmarkSpecifics(bm_specifics)); histogram_tester.ExpectBucketCount( "Sync.InvalidBookmarkSpecifics", /*sample=*/InvalidBookmarkSpecificsError::kInvalidUniquePosition, /*expected_count=*/1); } TEST(BookmarkSpecificsConversionsTest, ShouldBeInvalidBookmarkSpecificsWithBannedGUID) { ASSERT_THAT(bookmarks::BookmarkNode::kBannedGuidDueToPastSyncBug, Eq(InferGuidFromLegacyOriginatorId( /*originator_cache_guid=*/"", /*originator_client_item_id=*/"") .AsLowercaseString())); base::HistogramTester histogram_tester; sync_pb::EntitySpecifics specifics; sync_pb::BookmarkSpecifics* bm_specifics = specifics.mutable_bookmark(); bm_specifics->set_type(sync_pb::BookmarkSpecifics::FOLDER); *bm_specifics->mutable_unique_position() = RandomUniquePosition(); bm_specifics->set_guid(bookmarks::BookmarkNode::kBannedGuidDueToPastSyncBug); EXPECT_FALSE(IsValidBookmarkSpecifics(*bm_specifics)); histogram_tester.ExpectBucketCount( "Sync.InvalidBookmarkSpecifics", /*sample=*/InvalidBookmarkSpecificsError::kBannedGUID, /*expected_count=*/1); } TEST(BookmarkSpecificsConversionsTest, ShouldBeInvalidBookmarkSpecifics) { sync_pb::BookmarkSpecifics bm_specifics; // Empty specifics. { base::HistogramTester histogram_tester; EXPECT_FALSE(IsValidBookmarkSpecifics(bm_specifics)); histogram_tester.ExpectBucketCount( "Sync.InvalidBookmarkSpecifics", /*sample=*/InvalidBookmarkSpecificsError::kEmptySpecifics, /*expected_count=*/1); } { base::HistogramTester histogram_tester; bm_specifics.set_type(sync_pb::BookmarkSpecifics::FOLDER); EXPECT_FALSE(IsValidBookmarkSpecifics(bm_specifics)); histogram_tester.ExpectBucketCount( "Sync.InvalidBookmarkSpecifics", /*sample=*/InvalidBookmarkSpecificsError::kInvalidGUID, /*expected_count=*/1); } // Populate the required fields. bm_specifics.set_guid(base::GUID::GenerateRandomV4().AsLowercaseString()); bm_specifics.set_parent_guid( base::GUID::GenerateRandomV4().AsLowercaseString()); *bm_specifics.mutable_unique_position() = RandomUniquePosition(); ASSERT_TRUE(IsValidBookmarkSpecifics(bm_specifics)); { base::HistogramTester histogram_tester; bm_specifics.set_type(sync_pb::BookmarkSpecifics::URL); EXPECT_FALSE(IsValidBookmarkSpecifics(bm_specifics)); histogram_tester.ExpectBucketCount( "Sync.InvalidBookmarkSpecifics", /*sample=*/InvalidBookmarkSpecificsError::kInvalidURL, /*expected_count=*/1); } // Add invalid url. { base::HistogramTester histogram_tester; bm_specifics.set_url("INVALID_URL"); EXPECT_FALSE(IsValidBookmarkSpecifics(bm_specifics)); histogram_tester.ExpectBucketCount( "Sync.InvalidBookmarkSpecifics", /*sample=*/InvalidBookmarkSpecificsError::kInvalidURL, /*expected_count=*/1); } // Add a valid url. bm_specifics.set_url("http://www.valid-url.com"); ASSERT_TRUE(IsValidBookmarkSpecifics(bm_specifics)); sync_pb::MetaInfo* meta_info1 = bm_specifics.add_meta_info(); meta_info1->set_key("key"); meta_info1->set_value("value1"); ASSERT_TRUE(IsValidBookmarkSpecifics(bm_specifics)); // Add redudant keys in meta_info. base::HistogramTester histogram_tester; sync_pb::MetaInfo* meta_info2 = bm_specifics.add_meta_info(); meta_info2->set_key("key"); meta_info2->set_value("value2"); EXPECT_FALSE(IsValidBookmarkSpecifics(bm_specifics)); histogram_tester.ExpectBucketCount( "Sync.InvalidBookmarkSpecifics", /*sample=*/InvalidBookmarkSpecificsError::kNonUniqueMetaInfoKeys, /*expected_count=*/1); } TEST(BookmarkSpecificsConversionsTest, ReplaceUrlNodeWithUpdatedGUID) { std::unique_ptr model = bookmarks::TestBookmarkClient::CreateModel(); const bookmarks::BookmarkNode* bookmark_bar_node = model->bookmark_bar_node(); const base::GUID kGuid = base::GUID::GenerateRandomV4(); const std::u16string kTitle = u"bar"; const GURL kUrl = GURL("http://foo.com"); const base::Time kCreationTime = base::Time::Now(); auto meta_info_map = std::make_unique(); const std::string kKey = "key"; const std::string kValue = "value"; (*meta_info_map)[kKey] = kValue; // Add a bookmark URL. const bookmarks::BookmarkNode* original_url = model->AddURL( bookmark_bar_node, 0, kTitle, kUrl, meta_info_map.get(), kCreationTime); // Replace url1. const bookmarks::BookmarkNode* new_url = ReplaceBookmarkNodeGUID(original_url, kGuid, model.get()); // All data except for the GUID should be the same. EXPECT_EQ(kGuid, new_url->guid()); EXPECT_EQ(kTitle, new_url->GetTitle()); EXPECT_EQ(bookmark_bar_node, new_url->parent()); EXPECT_EQ(0u, bookmark_bar_node->GetIndexOf(new_url)); EXPECT_EQ(kUrl, new_url->url()); EXPECT_EQ(kCreationTime, new_url->date_added()); std::string out_value_url; EXPECT_TRUE(new_url->GetMetaInfo(kKey, &out_value_url)); EXPECT_EQ(kValue, out_value_url); } TEST(BookmarkSpecificsConversionsTest, ReplaceFolderNodeWithUpdatedGUID) { std::unique_ptr model = bookmarks::TestBookmarkClient::CreateModel(); const bookmarks::BookmarkNode* bookmark_bar_node = model->bookmark_bar_node(); const base::GUID kGuid = base::GUID::GenerateRandomV4(); const std::u16string kTitle = u"foobar"; auto meta_info_map = std::make_unique(); const std::string kKey = "key"; const std::string kValue = "value"; (*meta_info_map)[kKey] = kValue; // Add a folder with child URLs. const bookmarks::BookmarkNode* original_folder = model->AddFolder(bookmark_bar_node, 0, kTitle, meta_info_map.get()); const bookmarks::BookmarkNode* url1 = model->AddURL(original_folder, 0, u"bar", GURL("http://bar.com")); const bookmarks::BookmarkNode* url2 = model->AddURL(original_folder, 1, u"foo", GURL("http://foo.com")); // Replace folder1. const bookmarks::BookmarkNode* new_folder = ReplaceBookmarkNodeGUID(original_folder, kGuid, model.get()); // All data except for the GUID should be the same. EXPECT_EQ(kGuid, new_folder->guid()); EXPECT_EQ(kTitle, new_folder->GetTitle()); EXPECT_EQ(bookmark_bar_node, new_folder->parent()); EXPECT_EQ(0u, bookmark_bar_node->GetIndexOf(new_folder)); std::string out_value_folder; EXPECT_TRUE(new_folder->GetMetaInfo(kKey, &out_value_folder)); EXPECT_EQ(kValue, out_value_folder); EXPECT_EQ(2u, new_folder->children().size()); EXPECT_EQ(0u, new_folder->GetIndexOf(url1)); EXPECT_EQ(1u, new_folder->GetIndexOf(url2)); } TEST(BookmarkSpecificsConversionsTest, ShouldConsiderValidBookmarkGuidIfMatchesClientTag) { const std::string kGuid = base::GUID::GenerateRandomV4().AsLowercaseString(); sync_pb::BookmarkSpecifics specifics; specifics.set_guid(kGuid); EXPECT_TRUE(HasExpectedBookmarkGuid( specifics, syncer::ClientTagHash::FromUnhashed(syncer::BOOKMARKS, kGuid), /*originator_cache_guid=*/"", /*originator_client_item_id=*/"")); } TEST(BookmarkSpecificsConversionsTest, ShouldConsiderValidBookmarkGuidIfMatchesOriginator) { const std::string kGuid = base::GUID::GenerateRandomV4().AsLowercaseString(); sync_pb::BookmarkSpecifics specifics; specifics.set_guid(kGuid); EXPECT_TRUE(HasExpectedBookmarkGuid(specifics, syncer::ClientTagHash(), /*originator_cache_guid=*/"", /*originator_client_item_id=*/kGuid)); } TEST(BookmarkSpecificsConversionsTest, ShouldConsiderInvalidBookmarkGuidIfEmptyOriginator) { const std::string kGuid = InferGuidFromLegacyOriginatorId( /*originator_cache_guid=*/"", /*=originator_client_item_id=*/"") .AsLowercaseString(); sync_pb::BookmarkSpecifics specifics; specifics.set_guid(kGuid); EXPECT_FALSE(HasExpectedBookmarkGuid(specifics, syncer::ClientTagHash::FromHashed("foo"), /*originator_cache_guid=*/"", /*originator_client_item_id=*/"")); } } // namespace } // namespace sync_bookmarks