blob: 652626b94dfd09a8e6a8a55e393b04f856752b9b [file]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/user_data_importer/content/stable_portability_data_importer.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/run_until.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_test_util.h"
#include "components/bookmarks/test/test_bookmark_client.h"
#include "components/bookmarks/test/test_matchers.h"
#include "components/history/core/browser/history_service.h"
#include "components/history/core/test/history_service_test_util.h"
#include "components/reading_list/core/fake_reading_list_model_storage.h"
#include "components/reading_list/core/reading_list_model.h"
#include "components/reading_list/core/reading_list_model_impl.h"
#include "components/strings/grit/components_strings.h"
#include "components/user_data_importer/content/content_bookmark_parser.h"
#include "components/user_data_importer/content/content_bookmark_parser_in_utility_process.h"
#include "components/user_data_importer/content/fake_bookmark_html_parser.h"
#include "components/user_data_importer/mojom/bookmark_html_parser.mojom.h"
#include "components/user_data_importer/utility/parsing_ffi/lib.rs.h"
#include "content/public/test/browser_task_environment.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/strings/str_format.h"
#include "ui/base/l10n/l10n_util.h"
using bookmarks::test::IsFolder;
using bookmarks::test::IsUrlBookmark;
using testing::ElementsAre;
using testing::IsEmpty;
using testing::UnorderedElementsAre;
namespace user_data_importer {
const std::string kHistogramPrefix = "UserDataImporter.StablePortabilityData.";
MATCHER_P(URLResultEq, expected, "") {
if (arg.url() != expected.url()) {
*result_listener << "which has url " << arg.url();
return false;
}
if (arg.title() != expected.title()) {
*result_listener << "which has title " << arg.title();
return false;
}
if (arg.visit_count() != expected.visit_count()) {
*result_listener << "which has visit_count " << arg.visit_count();
return false;
}
if (arg.typed_count() != expected.typed_count()) {
*result_listener << "which has typed_count " << arg.typed_count();
return false;
}
if (arg.last_visit() != expected.last_visit()) {
*result_listener << "which has last_visit " << arg.last_visit();
return false;
}
return true;
}
MATCHER_P4(IsUrlBookmarkWithinTimeRange, title, url, min_time, max_time, "") {
if (!testing::ExplainMatchResult(IsUrlBookmark(title, url), arg,
result_listener)) {
return false;
}
if (arg->date_added() < min_time || arg->date_added() > max_time) {
*result_listener << "which has creation time " << arg->date_added()
<< " not in range [" << min_time << ", " << max_time
<< "]";
return false;
}
return true;
}
MATCHER_P3(IsReadingListEntry, title, url, creation_time, "") {
if (arg->Title() != title) {
*result_listener << "which has title " << arg->Title();
return false;
}
if (arg->URL() != url) {
*result_listener << "which has url " << arg->URL();
return false;
}
base::Time actual_creation_time =
base::Time::UnixEpoch() + base::Microseconds(arg->CreationTime());
if (actual_creation_time != creation_time) {
*result_listener << "which has creation time " << actual_creation_time;
return false;
}
return true;
}
MATCHER_P4(IsReadingListEntryWithinTimeRange,
title,
url,
min_time,
max_time,
"") {
if (arg->Title() != title) {
*result_listener << "which has title " << arg->Title();
return false;
}
if (arg->URL() != url) {
*result_listener << "which has url " << arg->URL();
return false;
}
base::Time creation_time =
base::Time::UnixEpoch() + base::Microseconds(arg->CreationTime());
if (creation_time < min_time || creation_time > max_time) {
*result_listener << "which has creation time " << creation_time
<< " not in range [" << min_time << ", " << max_time
<< "]";
return false;
}
return true;
}
class StablePortabilityDataImporterTest : public testing::Test {
public:
StablePortabilityDataImporterTest() : receiver_(&fake_utility_parser_) {}
protected:
void SetUp() override {
bookmark_model_ = bookmarks::TestBookmarkClient::CreateModel();
auto parser = std::make_unique<ContentBookmarkParser>();
parser->SetServiceForTesting(receiver_.BindNewPipeAndPassRemote());
auto storage = std::make_unique<FakeReadingListModelStorage>();
auto* storage_raw = storage.get();
reading_list_model_ = std::make_unique<ReadingListModelImpl>(
std::move(storage), syncer::StorageType::kUnspecified,
syncer::WipeModelUponSyncDisabledBehavior::kNever,
base::DefaultClock::GetInstance());
storage_raw->TriggerLoadCompletion();
CHECK(history_dir_.CreateUniqueTempDir());
history_service_ =
history::CreateHistoryService(history_dir_.GetPath(), true);
importer_ = std::make_unique<StablePortabilityDataImporter>(
history_service_.get(), bookmark_model_.get(),
reading_list_model_.get(), std::move(parser));
}
void TearDown() override { task_environment_.RunUntilIdle(); }
history::HistoryService* history_service() const {
return history_service_.get();
}
int GetNumberOfBookmarksImported() const {
return number_bookmarks_imported_;
}
int GetNumberOfReadingListImported() const {
return number_reading_list_imported_;
}
int GetNumberOfHistoryImported() const { return number_history_imported_; }
const bookmarks::BookmarkNode* GetOtherBookmarkNode() {
return bookmark_model_->other_node();
}
const ReadingListModel& GetReadingListModel() { return *reading_list_model_; }
void ImportBookmarks(const std::string& html_data) {
bookmarks_callback_called_ = false;
base::ScopedTempDir dir;
ASSERT_TRUE(dir.CreateUniqueTempDir());
base::FilePath path = dir.GetPath().AppendASCII("bookmarks.html");
ASSERT_TRUE(base::WriteFile(path, html_data));
ImportBookmarksFile(path);
}
void ImportReadingList(const std::string& html_data) {
reading_list_callback_called_ = false;
base::ScopedTempDir dir;
ASSERT_TRUE(dir.CreateUniqueTempDir());
base::FilePath path = dir.GetPath().AppendASCII("reading_list.html");
ASSERT_TRUE(base::WriteFile(path, html_data));
ImportReadingListFile(path);
}
#if BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA)
void ImportHistory(const std::string& json_data) {
history_callback_called_ = false;
base::ScopedTempDir dir;
ASSERT_TRUE(dir.CreateUniqueTempDir());
base::FilePath path = dir.GetPath().AppendASCII("history.json");
ASSERT_TRUE(base::WriteFile(path, json_data));
ImportHistoryFile(path);
}
#endif // BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA)
void ImportBookmarksFile(const base::FilePath& bookmarks_file) {
PrepareCallbacks();
// Note: Some tests use an invalid (non-existent) file name, so `file` might
// be invalid here.
base::File file(bookmarks_file,
base::File::FLAG_OPEN | base::File::FLAG_READ);
importer_->ImportBookmarks(
std::move(file),
// Use of Unretained below is safe because the RunUntil loop below
// guarantees this outlives the tasks.
base::BindOnce(&StablePortabilityDataImporterTest::OnBookmarksConsumed,
base::Unretained(this)));
ASSERT_TRUE(
base::test::RunUntil([&]() { return bookmarks_callback_called_; }));
}
void ImportReadingListFile(const base::FilePath& reading_list_file) {
PrepareCallbacks();
// Note: Some tests use an invalid (non-existent) file name, so `file` might
// be invalid here.
base::File file(reading_list_file,
base::File::FLAG_OPEN | base::File::FLAG_READ);
importer_->ImportReadingList(
std::move(file),
// Use of Unretained below is safe because the RunUntil loop below
// guarantees this outlives the tasks.
base::BindOnce(
&StablePortabilityDataImporterTest::OnReadingListConsumed,
base::Unretained(this)));
ASSERT_TRUE(
base::test::RunUntil([&]() { return reading_list_callback_called_; }));
}
#if BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA)
void ImportHistoryFile(const base::FilePath& history_file) {
PrepareCallbacks();
// Note: Some tests use an invalid (non-existent) file name, so `file` might
// be invalid here.
base::File file(history_file,
base::File::FLAG_OPEN | base::File::FLAG_READ);
const size_t default_batch_size = 10;
importer_->ImportHistory(
std::move(file),
// Use of Unretained below is safe because the RunUntil loop below
// guarantees this outlives the tasks.
base::BindOnce(&StablePortabilityDataImporterTest::OnHistoryConsumed,
base::Unretained(this)),
default_batch_size);
ASSERT_TRUE(
base::test::RunUntil([&]() { return history_callback_called_; }));
}
#endif // BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA)
history::QueryResults QueryAllHistory() {
// Wait for any pending tasks to complete.
base::RunLoop run_loop;
history_service()->FlushForTest(run_loop.QuitClosure());
run_loop.Run();
base::test::TestFuture<history::QueryResults> future;
base::CancelableTaskTracker tracker;
history::QueryOptions options;
history_service()->QueryHistory(std::u16string(), options,
future.GetCallback(), &tracker);
return future.Take();
}
private:
void OnBookmarksConsumed(int number_imported) {
bookmarks_callback_called_ = true;
number_bookmarks_imported_ = number_imported;
}
void OnReadingListConsumed(int number_imported) {
reading_list_callback_called_ = true;
number_reading_list_imported_ = number_imported;
}
void OnHistoryConsumed(int number_imported) {
history_callback_called_ = true;
number_history_imported_ = number_imported;
}
void PrepareCallbacks() {
bookmarks_callback_called_ = false;
reading_list_callback_called_ = false;
history_callback_called_ = false;
}
content::BrowserTaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
FakeBookmarkHtmlParser fake_utility_parser_;
mojo::Receiver<user_data_importer::mojom::BookmarkHtmlParser> receiver_;
base::ScopedTempDir history_dir_;
std::unique_ptr<history::HistoryService> history_service_;
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model_;
std::unique_ptr<ReadingListModel> reading_list_model_;
bool bookmarks_callback_called_ = false;
bool reading_list_callback_called_ = false;
bool history_callback_called_ = false;
int number_bookmarks_imported_ = -1;
int number_reading_list_imported_ = -1;
int number_history_imported_ = -1;
std::unique_ptr<StablePortabilityDataImporter> importer_;
};
// Tests parsing a simple HTML file with two bookmarks.
TEST_F(StablePortabilityDataImporterTest, Bookmarks_Basic) {
base::HistogramTester histogram_tester;
const base::Time import_start_time = base::Time::Now();
ImportBookmarks(R"(
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<!--This is an automatically generated file.
It will be read and overwritten.
Do Not Edit! -->
<DL>
<DT><A HREF="https://www.google.com/" ADD_DATE="904914000">Google</A>
<DT><A HREF="https://www.chromium.org/">Chromium</A>
</DL>)");
EXPECT_EQ(GetNumberOfBookmarksImported(), 2);
const base::Time import_end_time = base::Time::Now();
const bookmarks::BookmarkNode* other_node = GetOtherBookmarkNode();
EXPECT_THAT(
other_node->children(),
ElementsAre(IsFolder(
l10n_util::GetStringUTF16(IDS_IMPORTED_FOLDER),
ElementsAre(
IsUrlBookmark(u"Google", GURL("https://www.google.com/"),
base::Time::FromSecondsSinceUnixEpoch(904914000)),
// No timestamp maps to current time, within the import time.
IsUrlBookmarkWithinTimeRange(
u"Chromium", GURL("https://www.chromium.org/"),
import_start_time, import_end_time)))));
EXPECT_EQ(GetReadingListModel().size(), 0u);
// Check that the histograms are recorded correctly.
histogram_tester.ExpectUniqueSample(kHistogramPrefix + "Bookmarks.Outcome",
DataTypeMetrics::ImportOutcome::kSuccess,
1);
histogram_tester.ExpectBucketCount(
kHistogramPrefix + "Bookmarks.PreparedCount", 2, 1);
histogram_tester.ExpectBucketCount(
kHistogramPrefix + "Bookmarks.ImportedCount", 2, 1);
}
// Identical to the above test, but without the top-level <DL> tag enclosing it.
TEST_F(StablePortabilityDataImporterTest, Bookmarks_NoTopLevelDL) {
const base::Time import_start_time = base::Time::Now();
ImportBookmarks(
R"(<!DOCTYPE NETSCAPE-Bookmark-file-1>
<!--This is an automatically generated file.
It will be read and overwritten.
Do Not Edit! -->
<DT><A HREF="https://www.google.com/" ADD_DATE="904914000">Google</A>
<DT><A HREF="https://www.chromium.org/">Chromium</A>)");
const base::Time import_end_time = base::Time::Now();
EXPECT_EQ(GetNumberOfBookmarksImported(), 2);
const bookmarks::BookmarkNode* other_node = GetOtherBookmarkNode();
EXPECT_THAT(
other_node->children(),
ElementsAre(IsFolder(
l10n_util::GetStringUTF16(IDS_IMPORTED_FOLDER),
ElementsAre(
IsUrlBookmark(u"Google", GURL("https://www.google.com/"),
base::Time::FromSecondsSinceUnixEpoch(904914000)),
IsUrlBookmarkWithinTimeRange(
u"Chromium", GURL("https://www.chromium.org/"),
import_start_time, import_end_time)))));
EXPECT_EQ(GetReadingListModel().size(), 0u);
}
// Tests parsing an HTML file with folders, both empty and with bookmarks.
TEST_F(StablePortabilityDataImporterTest, Bookmarks_Folders) {
ImportBookmarks(
R"(<!DOCTYPE NETSCAPE-Bookmark-file-1>
<!--This is an automatically generated file.
It will be read and overwritten.
Do Not Edit! -->
<DL>
<DT><A HREF="https://www.google.com/" ADD_DATE="904914000">Google</A>
<DT><H3>Folder 1</H3>
<DL><p>
<DT><A HREF="https://www.example.com/" ADD_DATE="915181200">Example</A>
<DT><H3 ADD_DATE="1145523600">Folder 1.1</H3>
<DL><p>
<DT><A HREF="https://en.wikipedia.org/wiki/Kitsune" ADD_DATE="1674205200">Kitsune</A>
</DL><p>
</DL><p>
<DT><H3>Empty Folder</H3>
<DL><p>
</DL>
</DL>)");
EXPECT_EQ(GetNumberOfBookmarksImported(), 3);
const bookmarks::BookmarkNode* other_node = GetOtherBookmarkNode();
EXPECT_THAT(
other_node->children(),
ElementsAre(IsFolder(
l10n_util::GetStringUTF16(IDS_IMPORTED_FOLDER),
ElementsAre(
IsUrlBookmark(u"Google", GURL("https://www.google.com/"),
base::Time::FromSecondsSinceUnixEpoch(904914000)),
IsFolder(
u"Folder 1",
ElementsAre(
IsUrlBookmark(
u"Example", GURL("https://www.example.com/"),
base::Time::FromSecondsSinceUnixEpoch(915181200)),
IsFolder(
u"Folder 1.1",
ElementsAre(IsUrlBookmark(
u"Kitsune",
GURL("https://en.wikipedia.org/wiki/Kitsune"),
base::Time::FromSecondsSinceUnixEpoch(
1674205200)))))),
IsFolder(u"Empty Folder", ElementsAre())))));
EXPECT_EQ(GetReadingListModel().size(), 0u);
}
// Tests parsing a simple HTML file with three reading list items.
TEST_F(StablePortabilityDataImporterTest, ReadingList) {
base::HistogramTester histogram_tester;
const base::Time import_start_time = base::Time::Now();
ImportReadingList(
R"(<!DOCTYPE NETSCAPE-Bookmark-file-1>
<!--This is an automatically generated file.
It will be read and overwritten.
Do Not Edit! -->
<DL>
<DT><A HREF="https://www.google.com/" ADD_DATE="904914000">Google</A>
<DT><A HREF="https://en.wikipedia.org/wiki/The_Beach_Boys">The Beach Boys</A>
<DT><A HREF="https://en.wikipedia.org/wiki/Brian_Wilson" ADD_DATE="-868878000">Brian Wilson</A>
</DL><p>
</DL>)");
const base::Time import_end_time = base::Time::Now();
EXPECT_EQ(GetNumberOfReadingListImported(), 3);
EXPECT_EQ(GetReadingListModel().size(), 3u);
// "Imported" bookmarks folder should *not* have been created.
const bookmarks::BookmarkNode* other_node = GetOtherBookmarkNode();
EXPECT_THAT(other_node->children(), ElementsAre());
std::vector<const ReadingListEntry*> entries;
for (const auto& gurl : GetReadingListModel().GetKeys()) {
entries.push_back(GetReadingListModel().GetEntryByURL(gurl).get());
}
EXPECT_THAT(
entries,
UnorderedElementsAre(
IsReadingListEntry("Google", GURL("https://www.google.com/"),
base::Time::FromSecondsSinceUnixEpoch(904914000)),
IsReadingListEntryWithinTimeRange(
"The Beach Boys",
GURL("https://en.wikipedia.org/wiki/The_Beach_Boys"),
import_start_time, import_end_time),
IsReadingListEntryWithinTimeRange(
"Brian Wilson",
GURL("https://en.wikipedia.org/wiki/Brian_Wilson"),
import_start_time, import_end_time)));
// Check that the histograms are recorded correctly.
histogram_tester.ExpectUniqueSample(kHistogramPrefix + "ReadingList.Outcome",
DataTypeMetrics::ImportOutcome::kSuccess,
1);
histogram_tester.ExpectBucketCount(
kHistogramPrefix + "ReadingList.PreparedCount", 3, 1);
histogram_tester.ExpectBucketCount(
kHistogramPrefix + "ReadingList.ImportedCount", 3, 1);
}
// Tests parsing an HTML with several not valid formats. The parser should still
// try to parse as many items as possible.
TEST_F(StablePortabilityDataImporterTest, Bookmarks_MiscJunk) {
const base::Time import_start_time = base::Time::Now();
ImportBookmarks(R"(
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<!--This is an automatically generated file.
It will be read and overwritten.
Do Not Edit! -->
<DL>
<DT><A>Google</A>
<DT><H3>Folder 1</H3>
<DL><p>
<DT><A HREF="https://www.chromium.org/">Chromium</A>
ICON_URI="https://www.chromium.org/favicon.ico"
<DT><A HREF="https://www.example.org/" ADD_DATE="Last Tuesday">Example</A>
<DT><A>Google Reader</A>
</DL><p>
<!-- Various unsupported types below -->
FEED="true"
FEEDURL="https://www.example.com"
WEBSLICE="true"
ISLIVEPREVIEW="true"
PREVIEWSIZE="100 x 100"
</DL>)");
const base::Time import_end_time = base::Time::Now();
EXPECT_EQ(GetNumberOfBookmarksImported(), 2);
const bookmarks::BookmarkNode* other_node = GetOtherBookmarkNode();
EXPECT_THAT(
other_node->children(),
ElementsAre(IsFolder(
l10n_util::GetStringUTF16(IDS_IMPORTED_FOLDER),
ElementsAre(
// <A>Google</A> was skipped for lack of URL.
IsFolder(u"Folder 1",
// The folder contains a mix of invalid and valid
// entries. Ensure the valid ones are preserved.
ElementsAre(
IsUrlBookmarkWithinTimeRange(
u"Chromium", GURL("https://www.chromium.org/"),
import_start_time, import_end_time),
IsUrlBookmarkWithinTimeRange(
u"Example", GURL("https://www.example.org/"),
import_start_time, import_end_time)
// <A>Google Reader</A> was skipped for lack of URL.
))))));
}
TEST_F(StablePortabilityDataImporterTest, Bookmarks_EmptyInput) {
base::HistogramTester histogram_tester;
ImportBookmarks("");
// Empty input data is considered an error. (In practice, this could also
// happen if the file read fails for some reason.)
EXPECT_EQ(GetNumberOfBookmarksImported(), -1);
// Check that the histograms are recorded correctly.
histogram_tester.ExpectUniqueSample(kHistogramPrefix + "Bookmarks.Outcome",
DataTypeMetrics::ImportOutcome::kFailure,
1);
histogram_tester.ExpectUniqueSample(
kHistogramPrefix + "Bookmarks.Error",
user_data_importer::BookmarksImportError::kFailedToRead, 1);
}
TEST_F(StablePortabilityDataImporterTest, ReadingList_EmptyInput) {
base::HistogramTester histogram_tester;
ImportReadingList("");
// Empty input data is considered an error. (In practice, this could also
// happen if the file read fails for some reason.)
EXPECT_EQ(GetNumberOfReadingListImported(), -1);
// Check that the histograms are recorded correctly.
histogram_tester.ExpectUniqueSample(kHistogramPrefix + "ReadingList.Outcome",
DataTypeMetrics::ImportOutcome::kFailure,
1);
histogram_tester.ExpectUniqueSample(
kHistogramPrefix + "ReadingList.Error",
user_data_importer::BookmarksImportError::kFailedToRead, 1);
}
// History parsing is only implemented on Posix systems for now, because the
// file is passed to the Rust parser in the form of a native "fd" (file
// descriptor).
#if BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA)
// Tests parsing a simple JSON file with two history entries.
TEST_F(StablePortabilityDataImporterTest, History_Basic) {
base::HistogramTester histogram_tester;
const char kHistoryJson[] = R"({
"metadata": {
"data_type": "history_visits"
},
"history_visits": [
{
"url": "https://www.google.com/",
"title": "Google",
"visit_time_unix_epoch_usec": 1674205200000000,
"visit_count": 5,
"typed_count": 2,
"synced": true
},
{
"url": "https://www.chromium.org/",
"title": "Chromium",
"visit_time_unix_epoch_usec": 1674205260000000
}
]
})";
ImportHistory(kHistoryJson);
EXPECT_EQ(GetNumberOfHistoryImported(), 2);
history::QueryResults query_results = QueryAllHistory();
std::vector<history::URLResult> results(query_results.begin(),
query_results.end());
history::URLResult expected_row1;
expected_row1.set_url(GURL("https://www.google.com/"));
expected_row1.set_title(u"Google");
expected_row1.set_visit_count(5);
expected_row1.set_typed_count(2);
expected_row1.set_last_visit(base::Time::UnixEpoch() +
base::Microseconds(1674205200000000));
history::URLResult expected_row2;
expected_row2.set_url(GURL("https://www.chromium.org/"));
expected_row2.set_title(u"Chromium");
expected_row2.set_visit_count(1);
expected_row2.set_typed_count(0);
expected_row2.set_last_visit(base::Time::UnixEpoch() +
base::Microseconds(1674205260000000));
EXPECT_THAT(results, UnorderedElementsAre(URLResultEq(expected_row1),
URLResultEq(expected_row2)));
// Check that the histograms are recorded correctly.
histogram_tester.ExpectUniqueSample(kHistogramPrefix + "History.Outcome",
DataTypeMetrics::ImportOutcome::kSuccess,
1);
histogram_tester.ExpectBucketCount(kHistogramPrefix + "History.ImportedCount",
2, 1);
}
TEST_F(StablePortabilityDataImporterTest, History_InvalidJson_Malformed) {
base::HistogramTester histogram_tester;
const char kHistoryJson[] = R"({
"metadata": {
"data_type": "history_visits"
},
"history_visits": [
{
"url": "https://www.google.com/",
"title": "Google",
})"; // Invalid JSON, missing closing brackets.
ImportHistory(kHistoryJson);
EXPECT_EQ(GetNumberOfHistoryImported(), -1);
histogram_tester.ExpectUniqueSample(kHistogramPrefix + "History.Outcome",
DataTypeMetrics::ImportOutcome::kFailure,
1);
}
TEST_F(StablePortabilityDataImporterTest, History_InvalidJson_NotJson) {
const char kHistoryData[] = "This is not a JSON file.";
ImportHistory(kHistoryData);
EXPECT_EQ(GetNumberOfHistoryImported(), -1);
}
TEST_F(StablePortabilityDataImporterTest,
History_InvalidJson_MissingHistoryVisits) {
const char kHistoryJson[] = R"({
"metadata": {
"data_type": "history_visits"
}
})";
ImportHistory(kHistoryJson);
EXPECT_EQ(GetNumberOfHistoryImported(), -1);
}
TEST_F(StablePortabilityDataImporterTest,
History_InvalidJson_HistoryVisitsNotArray) {
const char kHistoryJson[] = R"({
"metadata": {
"data_type": "history_visits"
},
"history_visits": {}
})";
ImportHistory(kHistoryJson);
EXPECT_EQ(GetNumberOfHistoryImported(), -1);
}
TEST_F(StablePortabilityDataImporterTest, History_InvalidJson_WrongDataType) {
const char kHistoryJson[] = R"({
"metadata": {
"data_type": "bookmarks"
},
"history_visits": []
})";
ImportHistory(kHistoryJson);
EXPECT_EQ(GetNumberOfHistoryImported(), -1);
}
TEST_F(StablePortabilityDataImporterTest, History_EmptyInput) {
ImportHistory("");
// Empty input data is considered an error.
EXPECT_EQ(GetNumberOfHistoryImported(), -1);
}
// Tests parsing a valid JSON file with no history entries.
TEST_F(StablePortabilityDataImporterTest, History_NoEntries) {
const char kHistoryJson[] = R"({
"metadata": {
"data_type": "history_visits"
},
"history_visits": []
})";
ImportHistory(kHistoryJson);
EXPECT_EQ(GetNumberOfHistoryImported(), 0);
}
// Tests parsing a large JSON file that is processed in chunks.
TEST_F(StablePortabilityDataImporterTest, History_LargeFileInChunks) {
const int num_visits = 15;
std::vector<std::string> visits;
for (int i = 0; i < num_visits; ++i) {
// The chunk size is 10, so 15 items will require two chunks.
visits.push_back(absl::StrFormat(
R"({"url":"https://www.example.com/%d","title":"Title %d",)"
R"("visit_time_unix_epoch_usec":%d})",
i, i, 1674205200000000ULL + i));
}
std::string history_json =
R"({"metadata":{"data_type":"history_visits"},"history_visits":[)" +
base::JoinString(visits, ",") + "]}";
ImportHistory(history_json);
EXPECT_EQ(GetNumberOfHistoryImported(), num_visits);
history::QueryResults query_results = QueryAllHistory();
std::vector<history::URLResult> results(query_results.begin(),
query_results.end());
std::vector<testing::Matcher<history::URLResult>> matchers;
for (int i = 0; i < num_visits; ++i) {
history::URLResult expected_row;
expected_row.set_url(
GURL(absl::StrFormat("https://www.example.com/%d", i)));
expected_row.set_title(base::UTF8ToUTF16(absl::StrFormat("Title %d", i)));
expected_row.set_visit_count(1);
expected_row.set_typed_count(0);
expected_row.set_last_visit(base::Time::UnixEpoch() +
base::Microseconds(1674205200000000ULL + i));
matchers.push_back(URLResultEq(expected_row));
}
EXPECT_THAT(results, UnorderedElementsAreArray(matchers));
}
// Tests parsing a JSON file with a mix of valid and invalid entries. Only
// valid entries should be imported.
TEST_F(StablePortabilityDataImporterTest, History_MixedValidAndInvalid) {
const char kHistoryJson[] = R"({
"metadata": {
"data_type": "history_visits"
},
"history_visits": [
{
"url": "https://www.google.com/",
"title": "Google",
"visit_time_unix_epoch_usec": 1674205200000000
},
{
"title": "Invalid Entry, no URL"
},
{
"url": "https://www.chromium.org/",
"title": "Chromium",
"visit_time_unix_epoch_usec": 1674205260000000
}
]
})";
ImportHistory(kHistoryJson);
EXPECT_EQ(GetNumberOfHistoryImported(), 2);
history::QueryResults query_results = QueryAllHistory();
std::vector<history::URLResult> results(query_results.begin(),
query_results.end());
history::URLResult expected_row1;
expected_row1.set_url(GURL("https://www.google.com/"));
expected_row1.set_title(u"Google");
expected_row1.set_visit_count(1);
expected_row1.set_typed_count(0);
expected_row1.set_last_visit(base::Time::UnixEpoch() +
base::Microseconds(1674205200000000));
history::URLResult expected_row2;
expected_row2.set_url(GURL("https://www.chromium.org/"));
expected_row2.set_title(u"Chromium");
expected_row2.set_visit_count(1);
expected_row2.set_typed_count(0);
expected_row2.set_last_visit(base::Time::UnixEpoch() +
base::Microseconds(1674205260000000));
EXPECT_THAT(results, UnorderedElementsAre(URLResultEq(expected_row1),
URLResultEq(expected_row2)));
}
#endif // BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA)
// Tests importing invalid files that do not exist.
TEST_F(StablePortabilityDataImporterTest, CallbacksAreCalled) {
base::HistogramTester histogram_tester;
ImportBookmarksFile(
base::FilePath(FILE_PATH_LITERAL("/invalid/path/to/bookmarks/file")));
EXPECT_EQ(GetNumberOfBookmarksImported(), -1);
histogram_tester.ExpectUniqueSample(
kHistogramPrefix + "Bookmarks.Outcome",
DataTypeMetrics::ImportOutcome::kNotPresent, 1);
ImportReadingListFile(
base::FilePath(FILE_PATH_LITERAL("/invalid/path/to/reading_list/file")));
EXPECT_EQ(GetNumberOfReadingListImported(), -1);
histogram_tester.ExpectUniqueSample(
kHistogramPrefix + "ReadingList.Outcome",
DataTypeMetrics::ImportOutcome::kNotPresent, 1);
#if BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA)
ImportHistoryFile(
base::FilePath(FILE_PATH_LITERAL("/invalid/path/to/history/file")));
EXPECT_EQ(GetNumberOfHistoryImported(), -1);
histogram_tester.ExpectUniqueSample(
kHistogramPrefix + "History.Outcome",
DataTypeMetrics::ImportOutcome::kNotPresent, 1);
#endif // BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA)
}
} // namespace user_data_importer