summaryrefslogtreecommitdiffstats
path: root/browser/components/migration/tests/unit
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /browser/components/migration/tests/unit
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/migration/tests/unit')
-rw-r--r--browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Faviconsbin0 -> 49152 bytes
-rw-r--r--browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Login Databin0 -> 24576 bytes
-rw-r--r--browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Web Databin0 -> 108544 bytes
-rw-r--r--browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Local State5
-rw-r--r--browser/components/migration/tests/unit/AppData/LocalWithNoData/Google/Chrome/User Data/Default/Login Databin0 -> 24576 bytes
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/0f3ab103a522f4463ecacc36d34eb996/360sefav.datbin0 -> 6144 bytes
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/360Bookmarks1
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2020_08_28.favdb3
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2021_12_02.favdb3
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2020_08_28.favdb1
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2021_12_02.favdb1
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat1
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/DailyBackup/360sefav_2020_08_28.favdb3
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/Bookmarks3
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Local State12
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Cookiesbin0 -> 10240 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/_locales/en_US/messages.json9
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/manifest.json10
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/_locales/en_US/messages.json9
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/manifest.json5
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-2/1.0_0/manifest.json4
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryCorruptbin0 -> 91558 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryMasterbin0 -> 118784 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Login Databin0 -> 24576 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Local State22
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Bookmarks.plistbin0 -> 2252 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.dbbin0 -> 40960 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-lock0
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-shmbin0 -> 32768 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-walbin0 -> 280192 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/04860FA3D07D8936B87D2B965317C6E9bin0 -> 24838 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/19A777E4F7BDA0C0E350D6C681B6E271bin0 -> 15086 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/2558A57A1AE576AA31F0DCD1364B3F42bin0 -> 5558 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/57D1907A1EBDA1889AA85B8AB7A90804bin0 -> 5558 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/6EEDD53B65A19CB364EB6FB07DEACF80bin0 -> 22382 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/7F65370AD319C7B294EDF2E2BEBA880Fbin0 -> 2734 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/999E2BD5CD612AA550F222A1088DB3D8bin0 -> 15406 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/9D8A6E2153D42043A7AE0430B41D374Abin0 -> 5558 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/A21F634481CF5188329FD2052F07ADBCbin0 -> 5558 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/BC2288B5BA9B7BE352BA586257442E08bin0 -> 22382 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/CFFC3831D8E7201BF8B77728FC79B52Bbin0 -> 15406 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/F3FA61DDA95B78A8B5F2C392C0382137bin0 -> 5430 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/HistoryStrangeEntries.dbbin0 -> 98304 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/HistoryTemplate.dbbin0 -> 98304 bytes
-rw-r--r--browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Default/Login Databin0 -> 24576 bytes
-rw-r--r--browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Local State22
-rw-r--r--browser/components/migration/tests/unit/bookmarks.exported.html32
-rw-r--r--browser/components/migration/tests/unit/bookmarks.exported.json194
-rw-r--r--browser/components/migration/tests/unit/bookmarks.invalid.html1
-rw-r--r--browser/components/migration/tests/unit/head_migration.js260
-rw-r--r--browser/components/migration/tests/unit/insertIEHistory/InsertIEHistory.cpp38
-rw-r--r--browser/components/migration/tests/unit/insertIEHistory/moz.build19
-rw-r--r--browser/components/migration/tests/unit/test_360seMigrationUtils.js164
-rw-r--r--browser/components/migration/tests/unit/test_360se_bookmarks.js62
-rw-r--r--browser/components/migration/tests/unit/test_BookmarksFileMigrator.js134
-rw-r--r--browser/components/migration/tests/unit/test_ChromeMigrationUtils.js87
-rw-r--r--browser/components/migration/tests/unit/test_ChromeMigrationUtils_path.js141
-rw-r--r--browser/components/migration/tests/unit/test_ChromeMigrationUtils_path_chromium_snap.js55
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_bookmarks.js205
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_corrupt_history.js83
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_credit_cards.js239
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_extensions.js165
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_formdata.js118
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_history.js206
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_passwords.js373
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_passwords_emptySource.js43
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_permissions.js205
-rw-r--r--browser/components/migration/tests/unit/test_Edge_db_migration.js849
-rw-r--r--browser/components/migration/tests/unit/test_Edge_registry_migration.js81
-rw-r--r--browser/components/migration/tests/unit/test_IE_bookmarks.js30
-rw-r--r--browser/components/migration/tests/unit/test_IE_history.js187
-rw-r--r--browser/components/migration/tests/unit/test_MigrationUtils_timedRetry.js29
-rw-r--r--browser/components/migration/tests/unit/test_PasswordFileMigrator.js116
-rw-r--r--browser/components/migration/tests/unit/test_Safari_bookmarks.js85
-rw-r--r--browser/components/migration/tests/unit/test_Safari_history.js101
-rw-r--r--browser/components/migration/tests/unit/test_Safari_history_strange_entries.js115
-rw-r--r--browser/components/migration/tests/unit/test_Safari_permissions.js145
-rw-r--r--browser/components/migration/tests/unit/test_fx_telemetry.js436
-rw-r--r--browser/components/migration/tests/unit/xpcshell.toml95
79 files changed, 5207 insertions, 0 deletions
diff --git a/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Favicons b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Favicons
new file mode 100644
index 0000000000..fddee798b3
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Favicons
Binary files differ
diff --git a/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Login Data b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Login Data
new file mode 100644
index 0000000000..7e6e843a03
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Login Data
Binary files differ
diff --git a/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Web Data b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Web Data
new file mode 100644
index 0000000000..c557c9b851
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Web Data
Binary files differ
diff --git a/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Local State b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Local State
new file mode 100644
index 0000000000..3f3fecb651
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Local State
@@ -0,0 +1,5 @@
+{
+ "os_crypt" : {
+ "encrypted_key" : "RFBBUEk/ThisNPAPIKeyCanOnlyBeDecryptedByTheOriginalDeviceSoThisWillThrowFromDecryptData"
+ }
+}
diff --git a/browser/components/migration/tests/unit/AppData/LocalWithNoData/Google/Chrome/User Data/Default/Login Data b/browser/components/migration/tests/unit/AppData/LocalWithNoData/Google/Chrome/User Data/Default/Login Data
new file mode 100644
index 0000000000..fd135624c4
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/LocalWithNoData/Google/Chrome/User Data/Default/Login Data
Binary files differ
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat
new file mode 100644
index 0000000000..1835c33583
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat
Binary files differ
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/360Bookmarks b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/360Bookmarks
new file mode 100644
index 0000000000..f51195f54c
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/360Bookmarks
@@ -0,0 +1 @@
+Encrypted canonical bookmarks storage, since 360 SE 10
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2020_08_28.favdb b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2020_08_28.favdb
new file mode 100644
index 0000000000..ea466a25bf
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2020_08_28.favdb
@@ -0,0 +1,3 @@
+{
+ "note": "Plain text bookmarks backup, since 360 SE 10."
+}
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2021_12_02.favdb b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2021_12_02.favdb
new file mode 100644
index 0000000000..ea466a25bf
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2021_12_02.favdb
@@ -0,0 +1,3 @@
+{
+ "note": "Plain text bookmarks backup, since 360 SE 10."
+}
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2020_08_28.favdb b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2020_08_28.favdb
new file mode 100644
index 0000000000..32b4002a32
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2020_08_28.favdb
@@ -0,0 +1 @@
+Encrypted bookmarks backup, since 360 SE 10.
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2021_12_02.favdb b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2021_12_02.favdb
new file mode 100644
index 0000000000..32b4002a32
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2021_12_02.favdb
@@ -0,0 +1 @@
+Encrypted bookmarks backup, since 360 SE 10.
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat
new file mode 100644
index 0000000000..440e7145bd
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat
@@ -0,0 +1 @@
+Bookmarks storage in legacy SQLite format.
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/DailyBackup/360sefav_2020_08_28.favdb b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/DailyBackup/360sefav_2020_08_28.favdb
new file mode 100644
index 0000000000..d5d939629c
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/DailyBackup/360sefav_2020_08_28.favdb
@@ -0,0 +1,3 @@
+{
+ "note": "Plain text bookmarks backup, 360 SE 9."
+}
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/Bookmarks b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/Bookmarks
new file mode 100644
index 0000000000..6f47e5a55c
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/Bookmarks
@@ -0,0 +1,3 @@
+{
+ "note": "Plain text canonical bookmarks storage, 360 SE 9."
+}
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Local State b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Local State
new file mode 100644
index 0000000000..dd3fecce45
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Local State
@@ -0,0 +1,12 @@
+{
+ "profile": {
+ "info_cache": {
+ "Default": {
+ "name": "用户1"
+ }
+ }
+ },
+ "sync_login_info": {
+ "filepath": "0f3ab103a522f4463ecacc36d34eb996"
+ }
+}
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Cookies b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Cookies
new file mode 100644
index 0000000000..83d855cb33
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Cookies
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/_locales/en_US/messages.json b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/_locales/en_US/messages.json
new file mode 100644
index 0000000000..44e855edbd
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/_locales/en_US/messages.json
@@ -0,0 +1,9 @@
+{
+ "description": {
+ "description": "Extension description in manifest. Should not exceed 132 characters.",
+ "message": "It is the description of fake app 1."
+ },
+ "name": {
+ "message": "Fake App 1"
+ }
+}
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/manifest.json b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/manifest.json
new file mode 100644
index 0000000000..1550bf1c0e
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/manifest.json
@@ -0,0 +1,10 @@
+{
+ "app": {
+ "launch": {
+ "local_path": "main.html"
+ }
+ },
+ "default_locale": "en_US",
+ "description": "__MSG_description__",
+ "name": "__MSG_name__"
+}
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/_locales/en_US/messages.json b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/_locales/en_US/messages.json
new file mode 100644
index 0000000000..11657460d8
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/_locales/en_US/messages.json
@@ -0,0 +1,9 @@
+{
+ "description": {
+ "description": "Extension description in manifest. Should not exceed 132 characters.",
+ "message": "It is the description of fake extension 1."
+ },
+ "name": {
+ "message": "Fake Extension 1"
+ }
+}
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/manifest.json b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/manifest.json
new file mode 100644
index 0000000000..5ceced8031
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/manifest.json
@@ -0,0 +1,5 @@
+{
+ "default_locale": "en_US",
+ "description": "__MSG_description__",
+ "name": "__MSG_name__"
+}
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-2/1.0_0/manifest.json b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-2/1.0_0/manifest.json
new file mode 100644
index 0000000000..983c37560c
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-2/1.0_0/manifest.json
@@ -0,0 +1,4 @@
+{
+ "default_locale": "en_US",
+ "name": "Fake Extension 2"
+}
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryCorrupt b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryCorrupt
new file mode 100644
index 0000000000..8585f308c5
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryCorrupt
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryMaster b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryMaster
new file mode 100644
index 0000000000..7fb19903b0
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryMaster
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Login Data b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Login Data
new file mode 100644
index 0000000000..19b8542b98
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Login Data
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Local State b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Local State
new file mode 100644
index 0000000000..01b99455e4
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Local State
@@ -0,0 +1,22 @@
+{
+ "profile" : {
+ "info_cache" : {
+ "Default" : {
+ "active_time" : 1430950755.65137,
+ "is_using_default_name" : true,
+ "is_ephemeral" : false,
+ "is_omitted_from_profile_list" : false,
+ "user_name" : "",
+ "background_apps" : false,
+ "is_using_default_avatar" : true,
+ "avatar_icon" : "chrome://theme/IDR_PROFILE_AVATAR_0",
+ "name" : "Person 1"
+ }
+ },
+ "profiles_created" : 1,
+ "last_used" : "Default",
+ "last_active_profiles" : [
+ "Default"
+ ]
+ }
+}
diff --git a/browser/components/migration/tests/unit/Library/Safari/Bookmarks.plist b/browser/components/migration/tests/unit/Library/Safari/Bookmarks.plist
new file mode 100644
index 0000000000..a9c33e1b1a
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Bookmarks.plist
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db
new file mode 100644
index 0000000000..dd5d0c7512
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-lock b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-lock
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-lock
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-shm b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-shm
new file mode 100644
index 0000000000..edd607898b
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-shm
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-wal b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-wal
new file mode 100644
index 0000000000..e145119298
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-wal
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/04860FA3D07D8936B87D2B965317C6E9 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/04860FA3D07D8936B87D2B965317C6E9
new file mode 100644
index 0000000000..1c6741c165
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/04860FA3D07D8936B87D2B965317C6E9
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/19A777E4F7BDA0C0E350D6C681B6E271 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/19A777E4F7BDA0C0E350D6C681B6E271
new file mode 100644
index 0000000000..47b40f707f
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/19A777E4F7BDA0C0E350D6C681B6E271
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/2558A57A1AE576AA31F0DCD1364B3F42 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/2558A57A1AE576AA31F0DCD1364B3F42
new file mode 100644
index 0000000000..2a4c30b31e
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/2558A57A1AE576AA31F0DCD1364B3F42
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/57D1907A1EBDA1889AA85B8AB7A90804 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/57D1907A1EBDA1889AA85B8AB7A90804
new file mode 100644
index 0000000000..f4996ba082
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/57D1907A1EBDA1889AA85B8AB7A90804
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/6EEDD53B65A19CB364EB6FB07DEACF80 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/6EEDD53B65A19CB364EB6FB07DEACF80
new file mode 100644
index 0000000000..f519ce9ad2
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/6EEDD53B65A19CB364EB6FB07DEACF80
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/7F65370AD319C7B294EDF2E2BEBA880F b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/7F65370AD319C7B294EDF2E2BEBA880F
new file mode 100644
index 0000000000..e70021849b
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/7F65370AD319C7B294EDF2E2BEBA880F
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/999E2BD5CD612AA550F222A1088DB3D8 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/999E2BD5CD612AA550F222A1088DB3D8
new file mode 100644
index 0000000000..559502b02b
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/999E2BD5CD612AA550F222A1088DB3D8
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/9D8A6E2153D42043A7AE0430B41D374A b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/9D8A6E2153D42043A7AE0430B41D374A
new file mode 100644
index 0000000000..89ed9a1c39
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/9D8A6E2153D42043A7AE0430B41D374A
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/A21F634481CF5188329FD2052F07ADBC b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/A21F634481CF5188329FD2052F07ADBC
new file mode 100644
index 0000000000..7b86185e67
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/A21F634481CF5188329FD2052F07ADBC
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/BC2288B5BA9B7BE352BA586257442E08 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/BC2288B5BA9B7BE352BA586257442E08
new file mode 100644
index 0000000000..a1d03856b5
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/BC2288B5BA9B7BE352BA586257442E08
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/CFFC3831D8E7201BF8B77728FC79B52B b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/CFFC3831D8E7201BF8B77728FC79B52B
new file mode 100644
index 0000000000..ba1145ca83
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/CFFC3831D8E7201BF8B77728FC79B52B
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/F3FA61DDA95B78A8B5F2C392C0382137 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/F3FA61DDA95B78A8B5F2C392C0382137
new file mode 100644
index 0000000000..82339b3b1d
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/F3FA61DDA95B78A8B5F2C392C0382137
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/HistoryStrangeEntries.db b/browser/components/migration/tests/unit/Library/Safari/HistoryStrangeEntries.db
new file mode 100644
index 0000000000..533daba3df
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/HistoryStrangeEntries.db
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/HistoryTemplate.db b/browser/components/migration/tests/unit/Library/Safari/HistoryTemplate.db
new file mode 100644
index 0000000000..5a317c70e8
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/HistoryTemplate.db
Binary files differ
diff --git a/browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Default/Login Data b/browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Default/Login Data
new file mode 100644
index 0000000000..b2d425eb4a
--- /dev/null
+++ b/browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Default/Login Data
Binary files differ
diff --git a/browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Local State b/browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Local State
new file mode 100644
index 0000000000..bb03d6b9a1
--- /dev/null
+++ b/browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Local State
@@ -0,0 +1,22 @@
+{
+ "profile" : {
+ "info_cache" : {
+ "Default" : {
+ "active_time" : 1430950755.65137,
+ "is_using_default_name" : true,
+ "is_ephemeral" : false,
+ "is_omitted_from_profile_list" : false,
+ "user_name" : "",
+ "background_apps" : false,
+ "is_using_default_avatar" : true,
+ "avatar_icon" : "chrome://theme/IDR_PROFILE_AVATAR_0",
+ "name" : "Person With No Data"
+ }
+ },
+ "profiles_created" : 1,
+ "last_used" : "Default",
+ "last_active_profiles" : [
+ "Default"
+ ]
+ }
+}
diff --git a/browser/components/migration/tests/unit/bookmarks.exported.html b/browser/components/migration/tests/unit/bookmarks.exported.html
new file mode 100644
index 0000000000..5a9ec43325
--- /dev/null
+++ b/browser/components/migration/tests/unit/bookmarks.exported.html
@@ -0,0 +1,32 @@
+<!DOCTYPE NETSCAPE-Bookmark-file-1>
+<!-- This is an automatically generated file.
+ It will be read and overwritten.
+ DO NOT EDIT! -->
+<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
+<meta http-equiv="Content-Security-Policy"
+ content="default-src 'self'; script-src 'none'; img-src data: *; object-src 'none'"></meta>
+<TITLE>Bookmarks</TITLE>
+<H1>Bookmarks Menu</H1>
+
+<DL><p>
+ <DT><H3 ADD_DATE="1685118888" LAST_MODIFIED="1685118888">Mozilla Firefox</H3>
+ <DL><p>
+ <DT><A HREF="http://en-us.www.mozilla.com/en-US/firefox/help/" ADD_DATE="1685118888" LAST_MODIFIED="1685118888" ICON_URI="fake-favicon-uri:http://en-us.www.mozilla.com/en-US/firefox/help/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==">Help and Tutorials</A>
+ <DT><A HREF="http://en-us.www.mozilla.com/en-US/firefox/customize/" ADD_DATE="1685118888" LAST_MODIFIED="1685118888" ICON_URI="fake-favicon-uri:http://en-us.www.mozilla.com/en-US/firefox/customize/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==">Customize Firefox</A>
+ <DT><A HREF="http://en-us.www.mozilla.com/en-US/firefox/community/" ADD_DATE="1685118888" LAST_MODIFIED="1685118888" ICON_URI="fake-favicon-uri:http://en-us.www.mozilla.com/en-US/firefox/community/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==">Get Involved</A>
+ <DT><A HREF="http://en-us.www.mozilla.com/en-US/about/" ADD_DATE="1685118888" LAST_MODIFIED="1685118888" ICON_URI="fake-favicon-uri:http://en-us.www.mozilla.com/en-US/about/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==">About Us</A>
+ </DL><p>
+ <HR> <DT><H3 ADD_DATE="1177541020" LAST_MODIFIED="1177541050">test</H3>
+ <DL><p>
+ <DT><A HREF="http://test/post" ADD_DATE="1177375336" LAST_MODIFIED="1177375423" SHORTCUTURL="test" POST_DATA="hidden1%3Dbar&amp;text1%3D%25s" LAST_CHARSET="ISO-8859-1">test post keyword</A>
+ </DL><p>
+ <DT><H3 ADD_DATE="1685118888" LAST_MODIFIED="1685118888" PERSONAL_TOOLBAR_FOLDER="true">Bookmarks Toolbar</H3>
+ <DL><p>
+ <DT><A HREF="http://en-us.www.mozilla.com/en-US/firefox/central/" ADD_DATE="1685118888" LAST_MODIFIED="1685118888" ICON_URI="fake-favicon-uri:http://en-us.www.mozilla.com/en-US/firefox/central/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==">Getting Started</A>
+ <DT><A HREF="http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/" ADD_DATE="1177541035" LAST_MODIFIED="1177541035">Latest Headlines</A>
+ </DL><p>
+ <DT><H3 ADD_DATE="1685118888" LAST_MODIFIED="1685118888" UNFILED_BOOKMARKS_FOLDER="true">Other Bookmarks</H3>
+ <DL><p>
+ <DT><A HREF="http://example.tld/" ADD_DATE="1685118888" LAST_MODIFIED="1685118888">Example.tld</A>
+ </DL><p>
+</DL>
diff --git a/browser/components/migration/tests/unit/bookmarks.exported.json b/browser/components/migration/tests/unit/bookmarks.exported.json
new file mode 100644
index 0000000000..2a73f00b31
--- /dev/null
+++ b/browser/components/migration/tests/unit/bookmarks.exported.json
@@ -0,0 +1,194 @@
+{
+ "guid": "root________",
+ "title": "",
+ "index": 0,
+ "dateAdded": 1685116351936000,
+ "lastModified": 1685372151518000,
+ "id": 1,
+ "typeCode": 2,
+ "type": "text/x-moz-place-container",
+ "root": "placesRoot",
+ "children": [
+ {
+ "guid": "menu________",
+ "title": "menu",
+ "index": 0,
+ "dateAdded": 1685116351936000,
+ "lastModified": 1685116352325000,
+ "id": 2,
+ "typeCode": 2,
+ "type": "text/x-moz-place-container",
+ "root": "bookmarksMenuFolder",
+ "children": [
+ {
+ "guid": "jCs_9YrgXKq7",
+ "title": "Firefox Nightly Resources",
+ "index": 0,
+ "dateAdded": 1685116352325000,
+ "lastModified": 1685116352325000,
+ "id": 7,
+ "typeCode": 2,
+ "type": "text/x-moz-place-container",
+ "children": [
+ {
+ "guid": "xwdRLsUWYFwm",
+ "title": "Firefox Nightly blog",
+ "index": 0,
+ "dateAdded": 1685116352325000,
+ "lastModified": 1685116352325000,
+ "id": 8,
+ "typeCode": 1,
+ "iconUri": "fake-favicon-uri:https://blog.nightly.mozilla.org/",
+ "type": "text/x-moz-place",
+ "uri": "https://blog.nightly.mozilla.org/"
+ },
+ {
+ "guid": "uhdiDrWjH0-n",
+ "title": "Mozilla Bug Tracker",
+ "index": 1,
+ "dateAdded": 1685116352325000,
+ "lastModified": 1685116352325000,
+ "id": 9,
+ "typeCode": 1,
+ "iconUri": "fake-favicon-uri:https://bugzilla.mozilla.org/",
+ "type": "text/x-moz-place",
+ "uri": "https://bugzilla.mozilla.org/",
+ "keyword": "bz",
+ "postData": null
+ },
+ {
+ "guid": "zOK7d-gjJ5Vy",
+ "title": "Mozilla Developer Network",
+ "index": 2,
+ "dateAdded": 1685116352325000,
+ "lastModified": 1685116352325000,
+ "id": 10,
+ "typeCode": 1,
+ "iconUri": "fake-favicon-uri:https://developer.mozilla.org/",
+ "type": "text/x-moz-place",
+ "uri": "https://developer.mozilla.org/",
+ "keyword": "mdn",
+ "postData": null
+ },
+ {
+ "guid": "7gcb4320A_y6",
+ "title": "Nightly Tester Tools",
+ "index": 3,
+ "dateAdded": 1685116352325000,
+ "lastModified": 1685116352325000,
+ "id": 11,
+ "typeCode": 1,
+ "iconUri": "fake-favicon-uri:https://addons.mozilla.org/firefox/addon/nightly-tester-tools/",
+ "type": "text/x-moz-place",
+ "uri": "https://addons.mozilla.org/firefox/addon/nightly-tester-tools/"
+ },
+ {
+ "guid": "c4753lDvJwNE",
+ "title": "All your crashes",
+ "index": 4,
+ "dateAdded": 1685116352325000,
+ "lastModified": 1685116352325000,
+ "id": 12,
+ "typeCode": 1,
+ "iconUri": "fake-favicon-uri:about:crashes",
+ "type": "text/x-moz-place",
+ "uri": "about:crashes"
+ },
+ {
+ "guid": "IyYGIH9VCs2t",
+ "title": "Planet Mozilla",
+ "index": 5,
+ "dateAdded": 1685116352325000,
+ "lastModified": 1685116352325000,
+ "id": 13,
+ "typeCode": 1,
+ "iconUri": "fake-favicon-uri:https://planet.mozilla.org/",
+ "type": "text/x-moz-place",
+ "uri": "https://planet.mozilla.org/"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "guid": "toolbar_____",
+ "title": "toolbar",
+ "index": 1,
+ "dateAdded": 1685116351936000,
+ "lastModified": 1685372151518000,
+ "id": 3,
+ "typeCode": 2,
+ "type": "text/x-moz-place-container",
+ "root": "toolbarFolder",
+ "children": [
+ {
+ "guid": "5jN1vdzOEnHx",
+ "title": "Get Involved",
+ "index": 0,
+ "dateAdded": 1685116352413000,
+ "lastModified": 1685116352413000,
+ "id": 14,
+ "typeCode": 1,
+ "iconUri": "fake-favicon-uri:https://www.mozilla.org/contribute/?utm_medium=firefox-desktop&utm_source=bookmarks-toolbar&utm_campaign=new-users-nightly&utm_content=-global",
+ "type": "text/x-moz-place",
+ "uri": "https://www.mozilla.org/contribute/?utm_medium=firefox-desktop&utm_source=bookmarks-toolbar&utm_campaign=new-users-nightly&utm_content=-global"
+ },
+ {
+ "guid": "5RsMT9sWsmIe",
+ "title": "Why More Psychiatrists Think Mindfulness Can Help Treat ADHD",
+ "index": 1,
+ "dateAdded": 1685372143048000,
+ "lastModified": 1685372143048000,
+ "id": 15,
+ "typeCode": 1,
+ "type": "text/x-moz-place",
+ "uri": "https://getpocket.com/explore/item/why-more-psychiatrists-think-mindfulness-can-help-treat-adhd?utm_source=pocket-newtab"
+ },
+ {
+ "guid": "ejoNUqAfEMQL",
+ "title": "Your New Favorite Weeknight Recipe Is Meat-Free (and Easy, Too)",
+ "index": 2,
+ "dateAdded": 1685372148200000,
+ "lastModified": 1685372148200000,
+ "id": 16,
+ "typeCode": 1,
+ "type": "text/x-moz-place",
+ "uri": "https://getpocket.com/collections/your-new-favorite-weeknight-recipe-is-meat-free-and-easy-too?utm_source=pocket-newtab"
+ },
+ {
+ "guid": "O5QCiQ1zrqHY",
+ "title": "8 Natural Ways to Repel Insects Without Bug Spray",
+ "index": 3,
+ "dateAdded": 1685372151518000,
+ "lastModified": 1685372151518000,
+ "id": 17,
+ "typeCode": 1,
+ "type": "text/x-moz-place",
+ "uri": "https://getpocket.com/explore/item/8-natural-ways-to-repel-insects-without-bug-spray?utm_source=pocket-newtab"
+ }
+ ]
+ },
+ {
+ "guid": "unfiled_____",
+ "title": "unfiled",
+ "index": 3,
+ "dateAdded": 1685116351936000,
+ "lastModified": 1685116352272000,
+ "id": 5,
+ "typeCode": 2,
+ "type": "text/x-moz-place-container",
+ "root": "unfiledBookmarksFolder"
+ },
+ {
+ "guid": "mobile______",
+ "title": "mobile",
+ "index": 4,
+ "dateAdded": 1685116351968000,
+ "lastModified": 1685116352272000,
+ "id": 6,
+ "typeCode": 2,
+ "type": "text/x-moz-place-container",
+ "root": "mobileFolder"
+ }
+ ]
+}
diff --git a/browser/components/migration/tests/unit/bookmarks.invalid.html b/browser/components/migration/tests/unit/bookmarks.invalid.html
new file mode 100644
index 0000000000..900ec52e1d
--- /dev/null
+++ b/browser/components/migration/tests/unit/bookmarks.invalid.html
@@ -0,0 +1 @@
+This shouldn't cause anything to be imported.
diff --git a/browser/components/migration/tests/unit/head_migration.js b/browser/components/migration/tests/unit/head_migration.js
new file mode 100644
index 0000000000..9900f34232
--- /dev/null
+++ b/browser/components/migration/tests/unit/head_migration.js
@@ -0,0 +1,260 @@
+"use strict";
+
+var { MigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/MigrationUtils.sys.mjs"
+);
+var { LoginHelper } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginHelper.sys.mjs"
+);
+var { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+var { PlacesUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PlacesUtils.sys.mjs"
+);
+var { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+var { PlacesTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PlacesTestUtils.sys.mjs"
+);
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
+});
+
+// Initialize profile.
+var gProfD = do_get_profile();
+
+var { getAppInfo, newAppInfo, updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+updateAppInfo();
+
+/**
+ * Migrates the requested resource and waits for the migration to be complete.
+ *
+ * @param {MigratorBase} migrator
+ * The migrator being used to migrate the data.
+ * @param {number} resourceType
+ * This is a bitfield with bits from nsIBrowserProfileMigrator flipped to indicate what
+ * resources should be migrated.
+ * @param {object|string|null} [aProfile=null]
+ * The profile to be migrated. If set to null, the default profile will be
+ * migrated.
+ * @param {boolean} succeeds
+ * True if this migration is expected to succeed.
+ * @returns {Promise<Array<string[]>>}
+ * An array of the results from each nsIObserver topics being observed to
+ * verify if the migration succeeded or failed. Those results are 2-element
+ * arrays of [subject, data].
+ */
+async function promiseMigration(
+ migrator,
+ resourceType,
+ aProfile = null,
+ succeeds = null
+) {
+ // Ensure resource migration is available.
+ let availableSources = await migrator.getMigrateData(aProfile);
+ Assert.ok(
+ (availableSources & resourceType) > 0,
+ "Resource supported by migrator"
+ );
+ let promises = [TestUtils.topicObserved("Migration:Ended")];
+
+ if (succeeds !== null) {
+ // Check that the specific resource type succeeded
+ promises.push(
+ TestUtils.topicObserved(
+ succeeds ? "Migration:ItemAfterMigrate" : "Migration:ItemError",
+ (_, data) => data == resourceType
+ )
+ );
+ }
+
+ // Start the migration.
+ migrator.migrate(resourceType, null, aProfile);
+
+ return Promise.all(promises);
+}
+/**
+ * Function that returns a favicon url for a given page url
+ *
+ * @param {string} uri
+ * The Bookmark URI
+ * @returns {string} faviconURI
+ * The Favicon URI
+ */
+async function getFaviconForPageURI(uri) {
+ let faviconURI = await new Promise(resolve => {
+ PlacesUtils.favicons.getFaviconDataForPage(uri, favURI => {
+ resolve(favURI);
+ });
+ });
+ return faviconURI;
+}
+
+/**
+ * Takes an array of page URIs and checks that the favicon was imported for each page URI
+ *
+ * @param {Array} pageURIs An array of page URIs
+ */
+async function assertFavicons(pageURIs) {
+ for (let uri of pageURIs) {
+ let faviconURI = await getFaviconForPageURI(uri);
+ Assert.ok(faviconURI, `Got favicon for ${uri.spec}`);
+ }
+}
+
+/**
+ * Replaces a directory service entry with a given nsIFile.
+ *
+ * @param {string} key
+ * The nsIDirectoryService directory key to register a fake path for.
+ * For example: "AppData", "ULibDir".
+ * @param {nsIFile} file
+ * The nsIFile to map the key to. Note that this nsIFile should represent
+ * a directory and not an individual file.
+ * @see nsDirectoryServiceDefs.h for the list of directories that can be
+ * overridden.
+ */
+function registerFakePath(key, file) {
+ let dirsvc = Services.dirsvc.QueryInterface(Ci.nsIProperties);
+ let originalFile;
+ try {
+ // If a file is already provided save it and undefine, otherwise set will
+ // throw for persistent entries (ones that are cached).
+ originalFile = dirsvc.get(key, Ci.nsIFile);
+ dirsvc.undefine(key);
+ } catch (e) {
+ // dirsvc.get will throw if nothing provides for the key and dirsvc.undefine
+ // will throw if it's not a persistent entry, in either case we don't want
+ // to set the original file in cleanup.
+ originalFile = undefined;
+ }
+
+ dirsvc.set(key, file);
+ registerCleanupFunction(() => {
+ dirsvc.undefine(key);
+ if (originalFile) {
+ dirsvc.set(key, originalFile);
+ }
+ });
+}
+
+function getRootPath() {
+ let dirKey;
+ if (AppConstants.platform == "win") {
+ dirKey = "LocalAppData";
+ } else if (AppConstants.platform == "macosx") {
+ dirKey = "ULibDir";
+ } else {
+ dirKey = "Home";
+ }
+ return Services.dirsvc.get(dirKey, Ci.nsIFile).path;
+}
+
+/**
+ * Returns a PRTime value for the current date minus daysAgo number
+ * of days.
+ *
+ * @param {number} daysAgo
+ * How many days in the past from now the returned date should be.
+ * @returns {number}
+ */
+function PRTimeDaysAgo(daysAgo) {
+ return PlacesUtils.toPRTime(Date.now() - daysAgo * 24 * 60 * 60 * 1000);
+}
+
+/**
+ * Returns a Date value for the current date minus daysAgo number
+ * of days.
+ *
+ * @param {number} daysAgo
+ * How many days in the past from now the returned date should be.
+ * @returns {Date}
+ */
+function dateDaysAgo(daysAgo) {
+ return new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000);
+}
+
+/**
+ * Constructs and returns a data structure consistent with the Chrome
+ * browsers bookmarks storage. This data structure can then be serialized
+ * to JSON and written to disk to simulate a Chrome browser's bookmarks
+ * database.
+ *
+ * @param {number} [totalBookmarks=100]
+ * How many bookmarks to create.
+ * @returns {object}
+ */
+function createChromeBookmarkStructure(totalBookmarks = 100) {
+ let bookmarksData = {
+ roots: {
+ bookmark_bar: { children: [] },
+ other: { children: [] },
+ synced: { children: [] },
+ },
+ };
+ const MAX_BMS = totalBookmarks;
+ let barKids = bookmarksData.roots.bookmark_bar.children;
+ let menuKids = bookmarksData.roots.other.children;
+ let syncedKids = bookmarksData.roots.synced.children;
+ let currentMenuKids = menuKids;
+ let currentBarKids = barKids;
+ let currentSyncedKids = syncedKids;
+ for (let i = 0; i < MAX_BMS; i++) {
+ currentBarKids.push({
+ url: "https://www.chrome-bookmark-bar-bookmark" + i + ".com",
+ name: "bookmark " + i,
+ type: "url",
+ });
+ currentMenuKids.push({
+ url: "https://www.chrome-menu-bookmark" + i + ".com",
+ name: "bookmark for menu " + i,
+ type: "url",
+ });
+ currentSyncedKids.push({
+ url: "https://www.chrome-synced-bookmark" + i + ".com",
+ name: "bookmark for synced " + i,
+ type: "url",
+ });
+ if (i % 20 == 19) {
+ let nextFolder = {
+ name: "toolbar folder " + Math.ceil(i / 20),
+ type: "folder",
+ children: [],
+ };
+ currentBarKids.push(nextFolder);
+ currentBarKids = nextFolder.children;
+
+ nextFolder = {
+ name: "menu folder " + Math.ceil(i / 20),
+ type: "folder",
+ children: [],
+ };
+ currentMenuKids.push(nextFolder);
+ currentMenuKids = nextFolder.children;
+
+ nextFolder = {
+ name: "synced folder " + Math.ceil(i / 20),
+ type: "folder",
+ children: [],
+ };
+ currentSyncedKids.push(nextFolder);
+ currentSyncedKids = nextFolder.children;
+ }
+ }
+ return bookmarksData;
+}
diff --git a/browser/components/migration/tests/unit/insertIEHistory/InsertIEHistory.cpp b/browser/components/migration/tests/unit/insertIEHistory/InsertIEHistory.cpp
new file mode 100644
index 0000000000..dea79a9289
--- /dev/null
+++ b/browser/components/migration/tests/unit/insertIEHistory/InsertIEHistory.cpp
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Insert URLs into Internet Explorer (IE) history so we can test importing
+ * them.
+ *
+ * See API docs at
+ * https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/ms774949(v%3dvs.85)
+ */
+
+#include <urlhist.h> // IUrlHistoryStg
+#include <shlguid.h> // SID_SUrlHistory
+
+int main(int argc, char** argv) {
+ HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
+ if (FAILED(hr)) {
+ CoUninitialize();
+ return -1;
+ }
+ IUrlHistoryStg* ieHist;
+
+ hr =
+ ::CoCreateInstance(CLSID_CUrlHistory, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IUrlHistoryStg, reinterpret_cast<void**>(&ieHist));
+ if (FAILED(hr)) return -2;
+
+ hr = ieHist->AddUrl(L"http://www.mozilla.org/1", L"Mozilla HTTP Test", 0);
+ if (FAILED(hr)) return -3;
+
+ hr =
+ ieHist->AddUrl(L"https://www.mozilla.org/2", L"Mozilla HTTPS Test 🦊", 0);
+ if (FAILED(hr)) return -4;
+
+ CoUninitialize();
+
+ return 0;
+}
diff --git a/browser/components/migration/tests/unit/insertIEHistory/moz.build b/browser/components/migration/tests/unit/insertIEHistory/moz.build
new file mode 100644
index 0000000000..61ca96d48a
--- /dev/null
+++ b/browser/components/migration/tests/unit/insertIEHistory/moz.build
@@ -0,0 +1,19 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+FINAL_TARGET = "_tests/xpcshell/browser/components/migration/tests/unit"
+
+Program("InsertIEHistory")
+OS_LIBS += [
+ "ole32",
+ "uuid",
+]
+SOURCES += [
+ "InsertIEHistory.cpp",
+]
+
+NO_PGO = True
+DisableStlWrapping()
diff --git a/browser/components/migration/tests/unit/test_360seMigrationUtils.js b/browser/components/migration/tests/unit/test_360seMigrationUtils.js
new file mode 100644
index 0000000000..3a882b516d
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_360seMigrationUtils.js
@@ -0,0 +1,164 @@
+"use strict";
+
+const { Qihoo360seMigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/360seMigrationUtils.sys.mjs"
+);
+
+const parentPath = do_get_file("AppData/Roaming/360se6/User Data").path;
+const loggedInPath = "0f3ab103a522f4463ecacc36d34eb996";
+const loggedInBackup = PathUtils.join(
+ parentPath,
+ "Default",
+ loggedInPath,
+ "DailyBackup",
+ "360default_ori_2021_12_02.favdb"
+);
+const loggedOutBackup = PathUtils.join(
+ parentPath,
+ "Default",
+ "DailyBackup",
+ "360default_ori_2021_12_02.favdb"
+);
+
+function getSqlitePath(profileId) {
+ return PathUtils.join(parentPath, profileId, loggedInPath, "360sefav.dat");
+}
+
+add_task(async function test_360se10_logged_in() {
+ const sqlitePath = getSqlitePath("Default");
+ await IOUtils.setModificationTime(sqlitePath);
+ await IOUtils.copy(
+ PathUtils.join(parentPath, "Default", "360Bookmarks"),
+ PathUtils.join(parentPath, "Default", loggedInPath)
+ );
+ await IOUtils.copy(loggedOutBackup, loggedInBackup);
+
+ const alternativeBookmarks =
+ await Qihoo360seMigrationUtils.getAlternativeBookmarks({
+ bookmarksPath: PathUtils.join(parentPath, "Default", "Bookmarks"),
+ localState: {
+ sync_login_info: {
+ filepath: loggedInPath,
+ },
+ },
+ });
+ Assert.ok(
+ alternativeBookmarks.resource && alternativeBookmarks.resource.exists,
+ "Should return the legacy bookmark resource."
+ );
+ Assert.strictEqual(
+ alternativeBookmarks.path,
+ undefined,
+ "Should not return any path to plain text bookmarks."
+ );
+});
+
+add_task(async function test_360se10_logged_in_outdated_sqlite() {
+ const sqlitePath = getSqlitePath("Default");
+ await IOUtils.setModificationTime(
+ sqlitePath,
+ new Date("2020-08-18").valueOf()
+ );
+
+ const alternativeBookmarks =
+ await Qihoo360seMigrationUtils.getAlternativeBookmarks({
+ bookmarksPath: PathUtils.join(parentPath, "Default", "Bookmarks"),
+ localState: {
+ sync_login_info: {
+ filepath: loggedInPath,
+ },
+ },
+ });
+ Assert.strictEqual(
+ alternativeBookmarks.resource,
+ undefined,
+ "Should not return the outdated legacy bookmark resource."
+ );
+ Assert.strictEqual(
+ alternativeBookmarks.path,
+ loggedInBackup,
+ "Should return path to the most recent plain text bookmarks backup."
+ );
+
+ await IOUtils.setModificationTime(sqlitePath);
+});
+
+add_task(async function test_360se10_logged_out() {
+ const alternativeBookmarks =
+ await Qihoo360seMigrationUtils.getAlternativeBookmarks({
+ bookmarksPath: PathUtils.join(parentPath, "Default", "Bookmarks"),
+ localState: {
+ sync_login_info: {
+ filepath: "",
+ },
+ },
+ });
+ Assert.strictEqual(
+ alternativeBookmarks.resource,
+ undefined,
+ "Should not return the legacy bookmark resource."
+ );
+ Assert.strictEqual(
+ alternativeBookmarks.path,
+ loggedOutBackup,
+ "Should return path to the most recent plain text bookmarks backup."
+ );
+});
+
+add_task(async function test_360se9_logged_in_outdated_sqlite() {
+ const sqlitePath = getSqlitePath("Default4SE9Test");
+ await IOUtils.setModificationTime(
+ sqlitePath,
+ new Date("2020-08-18").valueOf()
+ );
+
+ const alternativeBookmarks =
+ await Qihoo360seMigrationUtils.getAlternativeBookmarks({
+ bookmarksPath: PathUtils.join(parentPath, "Default4SE9Test", "Bookmarks"),
+ localState: {
+ sync_login_info: {
+ filepath: loggedInPath,
+ },
+ },
+ });
+ Assert.strictEqual(
+ alternativeBookmarks.resource,
+ undefined,
+ "Should not return the legacy bookmark resource."
+ );
+ Assert.strictEqual(
+ alternativeBookmarks.path,
+ PathUtils.join(
+ parentPath,
+ "Default4SE9Test",
+ loggedInPath,
+ "DailyBackup",
+ "360sefav_2020_08_28.favdb"
+ ),
+ "Should return path to the most recent plain text bookmarks backup."
+ );
+
+ await IOUtils.setModificationTime(sqlitePath);
+});
+
+add_task(async function test_360se9_logged_out() {
+ const alternativeBookmarks =
+ await Qihoo360seMigrationUtils.getAlternativeBookmarks({
+ bookmarksPath: PathUtils.join(parentPath, "Default4SE9Test", "Bookmarks"),
+ localState: {
+ sync_login_info: {
+ filepath: "",
+ },
+ },
+ });
+ Assert.strictEqual(
+ alternativeBookmarks.resource,
+ undefined,
+ "Should not return the legacy bookmark resource."
+ );
+ Assert.strictEqual(
+ alternativeBookmarks.path,
+ PathUtils.join(parentPath, "Default4SE9Test", "Bookmarks"),
+ "Should return path to the plain text canonical bookmarks."
+ );
+});
diff --git a/browser/components/migration/tests/unit/test_360se_bookmarks.js b/browser/components/migration/tests/unit/test_360se_bookmarks.js
new file mode 100644
index 0000000000..e4f42d1880
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_360se_bookmarks.js
@@ -0,0 +1,62 @@
+"use strict";
+
+const { CustomizableUI } = ChromeUtils.importESModule(
+ "resource:///modules/CustomizableUI.sys.mjs"
+);
+
+add_task(async function () {
+ registerFakePath("AppData", do_get_file("AppData/Roaming/"));
+
+ let migrator = await MigrationUtils.getMigrator("chromium-360se");
+ // Sanity check for the source.
+ Assert.ok(await migrator.isSourceAvailable());
+
+ let importedToBookmarksToolbar = false;
+ let itemsSeen = { bookmarks: 0, folders: 0 };
+
+ let listener = events => {
+ for (let event of events) {
+ itemsSeen[
+ event.itemType == PlacesUtils.bookmarks.TYPE_FOLDER
+ ? "folders"
+ : "bookmarks"
+ ]++;
+ if (event.parentGuid == PlacesUtils.bookmarks.toolbarGuid) {
+ importedToBookmarksToolbar = true;
+ }
+ }
+ };
+ PlacesUtils.observers.addListener(["bookmark-added"], listener);
+ let observerNotified = false;
+ Services.obs.addObserver((aSubject, aTopic, aData) => {
+ let [toolbar, visibility] = JSON.parse(aData);
+ Assert.equal(
+ toolbar,
+ CustomizableUI.AREA_BOOKMARKS,
+ "Notification should be received for bookmarks toolbar"
+ );
+ Assert.equal(
+ visibility,
+ "true",
+ "Notification should say to reveal the bookmarks toolbar"
+ );
+ observerNotified = true;
+ }, "browser-set-toolbar-visibility");
+
+ await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS, {
+ id: "Default",
+ });
+ PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+
+ // Check the bookmarks have been imported to all the expected parents.
+ Assert.ok(importedToBookmarksToolbar, "Bookmarks imported in the toolbar");
+ Assert.equal(itemsSeen.bookmarks, 8, "Should import all bookmarks.");
+ Assert.equal(itemsSeen.folders, 2, "Should import all folders.");
+ // Check that the telemetry matches:
+ Assert.equal(
+ MigrationUtils._importQuantities.bookmarks,
+ itemsSeen.bookmarks + itemsSeen.folders,
+ "Telemetry reporting correct."
+ );
+ Assert.ok(observerNotified, "The observer should be notified upon migration");
+});
diff --git a/browser/components/migration/tests/unit/test_BookmarksFileMigrator.js b/browser/components/migration/tests/unit/test_BookmarksFileMigrator.js
new file mode 100644
index 0000000000..ec10097e76
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_BookmarksFileMigrator.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { BookmarksFileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/FileMigrators.sys.mjs"
+);
+
+const { MigrationWizardConstants } = ChromeUtils.importESModule(
+ "chrome://browser/content/migration/migration-wizard-constants.mjs"
+);
+
+/**
+ * Tests that the BookmarksFileMigrator properly subclasses FileMigratorBase
+ * and delegates to BookmarkHTMLUtils or BookmarkJSONUtils.
+ *
+ * Normally, we'd override the BookmarkHTMLUtils and BookmarkJSONUtils methods
+ * in our test here so that we just ensure that they're called with the
+ * right arguments, rather than testing their behaviour. Unfortunately, both
+ * BookmarkHTMLUtils and BookmarkJSONUtils are frozen with Object.freeze, which
+ * prevents sinon from stubbing out any of their methods. Rather than unfreezing
+ * those objects just for testing, we test the whole flow end-to-end, including
+ * the import to Places.
+ */
+
+add_setup(() => {
+ Services.prefs.setBoolPref("browser.migrate.bookmarks-file.enabled", true);
+ registerCleanupFunction(async () => {
+ await PlacesUtils.bookmarks.eraseEverything();
+ Services.prefs.clearUserPref("browser.migrate.bookmarks-file.enabled");
+ });
+});
+
+/**
+ * First check that the BookmarksFileMigrator implements the required parts
+ * of the parent class.
+ */
+add_task(async function test_BookmarksFileMigrator_members() {
+ let migrator = new BookmarksFileMigrator();
+ Assert.ok(
+ migrator.constructor.key,
+ `BookmarksFileMigrator implements static getter 'key'`
+ );
+
+ Assert.ok(
+ migrator.constructor.displayNameL10nID,
+ `BookmarksFileMigrator implements static getter 'displayNameL10nID'`
+ );
+
+ Assert.ok(
+ migrator.constructor.brandImage,
+ `BookmarksFileMigrator implements static getter 'brandImage'`
+ );
+
+ Assert.ok(
+ migrator.progressHeaderL10nID,
+ `BookmarksFileMigrator implements getter 'progressHeaderL10nID'`
+ );
+
+ Assert.ok(
+ migrator.successHeaderL10nID,
+ `BookmarksFileMigrator implements getter 'successHeaderL10nID'`
+ );
+
+ Assert.ok(
+ await migrator.getFilePickerConfig(),
+ `BookmarksFileMigrator implements method 'getFilePickerConfig'`
+ );
+
+ Assert.ok(
+ migrator.displayedResourceTypes,
+ `BookmarksFileMigrator implements getter 'displayedResourceTypes'`
+ );
+
+ Assert.ok(migrator.enabled, `BookmarksFileMigrator is enabled`);
+});
+
+add_task(async function test_BookmarksFileMigrator_HTML() {
+ let migrator = new BookmarksFileMigrator();
+ const EXPECTED_SUCCESS_STATE = {
+ [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES
+ .BOOKMARKS_FROM_FILE]: "8 bookmarks",
+ };
+
+ const BOOKMARKS_PATH = PathUtils.join(
+ do_get_cwd().path,
+ "bookmarks.exported.html"
+ );
+
+ let result = await migrator.migrate(BOOKMARKS_PATH);
+
+ Assert.deepEqual(
+ result,
+ EXPECTED_SUCCESS_STATE,
+ "Returned the expected success state."
+ );
+});
+
+add_task(async function test_BookmarksFileMigrator_JSON() {
+ let migrator = new BookmarksFileMigrator();
+
+ const EXPECTED_SUCCESS_STATE = {
+ [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES
+ .BOOKMARKS_FROM_FILE]: "10 bookmarks",
+ };
+
+ const BOOKMARKS_PATH = PathUtils.join(
+ do_get_cwd().path,
+ "bookmarks.exported.json"
+ );
+
+ let result = await migrator.migrate(BOOKMARKS_PATH);
+
+ Assert.deepEqual(
+ result,
+ EXPECTED_SUCCESS_STATE,
+ "Returned the expected success state."
+ );
+});
+
+add_task(async function test_BookmarksFileMigrator_invalid() {
+ let migrator = new BookmarksFileMigrator();
+
+ const INVALID_FILE_PATH = PathUtils.join(
+ do_get_cwd().path,
+ "bookmarks.invalid.html"
+ );
+
+ await Assert.rejects(
+ migrator.migrate(INVALID_FILE_PATH),
+ /Pick another file/
+ );
+});
diff --git a/browser/components/migration/tests/unit/test_ChromeMigrationUtils.js b/browser/components/migration/tests/unit/test_ChromeMigrationUtils.js
new file mode 100644
index 0000000000..32a8d1b4bc
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_ChromeMigrationUtils.js
@@ -0,0 +1,87 @@
+"use strict";
+
+const { ChromeMigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMigrationUtils.sys.mjs"
+);
+
+// Setup chrome user data path for all platforms.
+ChromeMigrationUtils.getDataPath = () => {
+ return Promise.resolve(
+ do_get_file("Library/Application Support/Google/Chrome/").path
+ );
+};
+
+add_task(async function test_getExtensionList_function() {
+ let extensionList = await ChromeMigrationUtils.getExtensionList("Default");
+ Assert.equal(
+ extensionList.length,
+ 2,
+ "There should be 2 extensions installed."
+ );
+ Assert.deepEqual(
+ extensionList.find(extension => extension.id == "fake-extension-1"),
+ {
+ id: "fake-extension-1",
+ name: "Fake Extension 1",
+ description: "It is the description of fake extension 1.",
+ },
+ "First extension should match expectations."
+ );
+ Assert.deepEqual(
+ extensionList.find(extension => extension.id == "fake-extension-2"),
+ {
+ id: "fake-extension-2",
+ name: "Fake Extension 2",
+ // There is no description in the `manifest.json` file of this extension.
+ description: null,
+ },
+ "Second extension should match expectations."
+ );
+});
+
+add_task(async function test_getExtensionInformation_function() {
+ let extension = await ChromeMigrationUtils.getExtensionInformation(
+ "fake-extension-1",
+ "Default"
+ );
+ Assert.deepEqual(
+ extension,
+ {
+ id: "fake-extension-1",
+ name: "Fake Extension 1",
+ description: "It is the description of fake extension 1.",
+ },
+ "Should get the extension information correctly."
+ );
+});
+
+add_task(async function test_getLocaleString_function() {
+ let name = await ChromeMigrationUtils._getLocaleString(
+ "__MSG_name__",
+ "en_US",
+ "fake-extension-1",
+ "Default"
+ );
+ Assert.deepEqual(
+ name,
+ "Fake Extension 1",
+ "The value of __MSG_name__ locale key is Fake Extension 1."
+ );
+});
+
+add_task(async function test_isExtensionInstalled_function() {
+ let isInstalled = await ChromeMigrationUtils.isExtensionInstalled(
+ "fake-extension-1",
+ "Default"
+ );
+ Assert.ok(isInstalled, "The fake-extension-1 extension should be installed.");
+});
+
+add_task(async function test_getLastUsedProfileId_function() {
+ let profileId = await ChromeMigrationUtils.getLastUsedProfileId();
+ Assert.equal(
+ profileId,
+ "Default",
+ "The last used profile ID should be Default."
+ );
+});
diff --git a/browser/components/migration/tests/unit/test_ChromeMigrationUtils_path.js b/browser/components/migration/tests/unit/test_ChromeMigrationUtils_path.js
new file mode 100644
index 0000000000..ca75595ea9
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_ChromeMigrationUtils_path.js
@@ -0,0 +1,141 @@
+"use strict";
+
+const { ChromeMigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMigrationUtils.sys.mjs"
+);
+
+const SUB_DIRECTORIES = {
+ win: {
+ Chrome: ["Google", "Chrome", "User Data"],
+ Chromium: ["Chromium", "User Data"],
+ Canary: ["Google", "Chrome SxS", "User Data"],
+ },
+ macosx: {
+ Chrome: ["Application Support", "Google", "Chrome"],
+ Chromium: ["Application Support", "Chromium"],
+ Canary: ["Application Support", "Google", "Chrome Canary"],
+ },
+ linux: {
+ Chrome: [".config", "google-chrome"],
+ Chromium: [".config", "chromium"],
+ Canary: [],
+ },
+};
+
+add_task(async function setup_fakePaths() {
+ let pathId;
+ if (AppConstants.platform == "macosx") {
+ pathId = "ULibDir";
+ } else if (AppConstants.platform == "win") {
+ pathId = "LocalAppData";
+ } else {
+ pathId = "Home";
+ }
+
+ registerFakePath(pathId, do_get_file("chromefiles/", true));
+});
+
+add_task(async function test_getDataPath_function() {
+ let projects = ["Chrome", "Chromium", "Canary"];
+ let rootPath = getRootPath();
+
+ for (let project of projects) {
+ let subfolders = SUB_DIRECTORIES[AppConstants.platform][project];
+
+ await IOUtils.makeDirectory(PathUtils.join(rootPath, ...subfolders), {
+ createAncestor: true,
+ ignoreExisting: true,
+ });
+ }
+
+ let chromeUserDataPath = await ChromeMigrationUtils.getDataPath("Chrome");
+ let chromiumUserDataPath = await ChromeMigrationUtils.getDataPath("Chromium");
+ let canaryUserDataPath = await ChromeMigrationUtils.getDataPath("Canary");
+ if (AppConstants.platform == "win") {
+ Assert.equal(
+ chromeUserDataPath,
+ PathUtils.join(getRootPath(), "Google", "Chrome", "User Data"),
+ "Should get the path of Chrome data directory."
+ );
+ Assert.equal(
+ chromiumUserDataPath,
+ PathUtils.join(getRootPath(), "Chromium", "User Data"),
+ "Should get the path of Chromium data directory."
+ );
+ Assert.equal(
+ canaryUserDataPath,
+ PathUtils.join(getRootPath(), "Google", "Chrome SxS", "User Data"),
+ "Should get the path of Canary data directory."
+ );
+ } else if (AppConstants.platform == "macosx") {
+ Assert.equal(
+ chromeUserDataPath,
+ PathUtils.join(getRootPath(), "Application Support", "Google", "Chrome"),
+ "Should get the path of Chrome data directory."
+ );
+ Assert.equal(
+ chromiumUserDataPath,
+ PathUtils.join(getRootPath(), "Application Support", "Chromium"),
+ "Should get the path of Chromium data directory."
+ );
+ Assert.equal(
+ canaryUserDataPath,
+ PathUtils.join(
+ getRootPath(),
+ "Application Support",
+ "Google",
+ "Chrome Canary"
+ ),
+ "Should get the path of Canary data directory."
+ );
+ } else {
+ Assert.equal(
+ chromeUserDataPath,
+ PathUtils.join(getRootPath(), ".config", "google-chrome"),
+ "Should get the path of Chrome data directory."
+ );
+ Assert.equal(
+ chromiumUserDataPath,
+ PathUtils.join(getRootPath(), ".config", "chromium"),
+ "Should get the path of Chromium data directory."
+ );
+ Assert.equal(canaryUserDataPath, null, "Should get null for Canary.");
+ }
+});
+
+add_task(async function test_getExtensionPath_function() {
+ let extensionPath = await ChromeMigrationUtils.getExtensionPath("Default");
+ let expectedPath;
+ if (AppConstants.platform == "win") {
+ expectedPath = PathUtils.join(
+ getRootPath(),
+ "Google",
+ "Chrome",
+ "User Data",
+ "Default",
+ "Extensions"
+ );
+ } else if (AppConstants.platform == "macosx") {
+ expectedPath = PathUtils.join(
+ getRootPath(),
+ "Application Support",
+ "Google",
+ "Chrome",
+ "Default",
+ "Extensions"
+ );
+ } else {
+ expectedPath = PathUtils.join(
+ getRootPath(),
+ ".config",
+ "google-chrome",
+ "Default",
+ "Extensions"
+ );
+ }
+ Assert.equal(
+ extensionPath,
+ expectedPath,
+ "Should get the path of extensions directory."
+ );
+});
diff --git a/browser/components/migration/tests/unit/test_ChromeMigrationUtils_path_chromium_snap.js b/browser/components/migration/tests/unit/test_ChromeMigrationUtils_path_chromium_snap.js
new file mode 100644
index 0000000000..5011991536
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_ChromeMigrationUtils_path_chromium_snap.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ChromeMigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMigrationUtils.sys.mjs"
+);
+
+const SUB_DIRECTORIES = {
+ linux: {
+ Chromium: [".config", "chromium"],
+ SnapChromium: ["snap", "chromium", "common", "chromium"],
+ },
+};
+
+add_task(async function setup_fakePaths() {
+ let pathId;
+ if (AppConstants.platform == "macosx") {
+ pathId = "ULibDir";
+ } else if (AppConstants.platform == "win") {
+ pathId = "LocalAppData";
+ } else {
+ pathId = "Home";
+ }
+
+ registerFakePath(pathId, do_get_file("chromefiles/", true));
+});
+
+add_task(async function test_getDataPath_function() {
+ let rootPath = getRootPath();
+ let chromiumSubFolders = SUB_DIRECTORIES[AppConstants.platform].Chromium;
+ // must remove normal chromium path
+ await IOUtils.remove(PathUtils.join(rootPath, ...chromiumSubFolders), {
+ ignoreAbsent: true,
+ });
+
+ let snapChromiumSubFolders =
+ SUB_DIRECTORIES[AppConstants.platform].SnapChromium;
+ // must create snap chromium path
+ await IOUtils.makeDirectory(
+ PathUtils.join(rootPath, ...snapChromiumSubFolders),
+ {
+ createAncestor: true,
+ ignoreExisting: true,
+ }
+ );
+
+ let chromiumUserDataPath = await ChromeMigrationUtils.getDataPath("Chromium");
+ Assert.equal(
+ chromiumUserDataPath,
+ PathUtils.join(getRootPath(), ...snapChromiumSubFolders),
+ "Should get the path of Snap Chromium data directory."
+ );
+});
diff --git a/browser/components/migration/tests/unit/test_Chrome_bookmarks.js b/browser/components/migration/tests/unit/test_Chrome_bookmarks.js
new file mode 100644
index 0000000000..d115cda412
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_bookmarks.js
@@ -0,0 +1,205 @@
+"use strict";
+
+const { CustomizableUI } = ChromeUtils.importESModule(
+ "resource:///modules/CustomizableUI.sys.mjs"
+);
+
+const { PlacesUIUtils } = ChromeUtils.importESModule(
+ "resource:///modules/PlacesUIUtils.sys.mjs"
+);
+
+let rootDir = do_get_file("chromefiles/", true);
+
+add_task(async function setup_fakePaths() {
+ let pathId;
+ if (AppConstants.platform == "macosx") {
+ pathId = "ULibDir";
+ } else if (AppConstants.platform == "win") {
+ pathId = "LocalAppData";
+ } else {
+ pathId = "Home";
+ }
+ registerFakePath(pathId, rootDir);
+});
+
+add_task(async function setup_initialBookmarks() {
+ let bookmarks = [];
+ for (let i = 0; i < PlacesUIUtils.NUM_TOOLBAR_BOOKMARKS_TO_UNHIDE + 1; i++) {
+ bookmarks.push({ url: "https://example.com/" + i, title: "" + i });
+ }
+
+ // Ensure we have enough items in both the menu and toolbar to trip creating a "from" folder.
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: bookmarks,
+ });
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: bookmarks,
+ });
+});
+
+async function testBookmarks(migratorKey, subDirs) {
+ if (AppConstants.platform == "macosx") {
+ subDirs.unshift("Application Support");
+ } else if (AppConstants.platform == "win") {
+ subDirs.push("User Data");
+ } else {
+ subDirs.unshift(".config");
+ }
+
+ let target = rootDir.clone();
+
+ // Pretend this is the default profile
+ while (subDirs.length) {
+ target.append(subDirs.shift());
+ }
+
+ let localStatePath = PathUtils.join(target.path, "Local State");
+ await IOUtils.writeJSON(localStatePath, []);
+
+ target.append("Default");
+
+ await IOUtils.makeDirectory(target.path, {
+ createAncestor: true,
+ ignoreExisting: true,
+ });
+
+ // Copy Favicons database into Default profile
+ const sourcePath = do_get_file(
+ "AppData/Local/Google/Chrome/User Data/Default/Favicons"
+ ).path;
+ await IOUtils.copy(sourcePath, target.path);
+
+ // Get page url for each favicon
+ let faviconURIs = await MigrationUtils.getRowsFromDBWithoutLocks(
+ sourcePath,
+ "Chrome Bookmark Favicons",
+ `select page_url from icon_mapping`
+ );
+
+ target.append("Bookmarks");
+ await IOUtils.remove(target.path, { ignoreAbsent: true });
+
+ let bookmarksData = createChromeBookmarkStructure();
+ await IOUtils.writeJSON(target.path, bookmarksData);
+
+ let migrator = await MigrationUtils.getMigrator(migratorKey);
+ Assert.ok(await migrator.hasPermissions(), "Has permissions");
+ // Sanity check for the source.
+ Assert.ok(await migrator.isSourceAvailable());
+
+ let itemsSeen = { bookmarks: 0, folders: 0 };
+ let listener = events => {
+ for (let event of events) {
+ itemsSeen[
+ event.itemType == PlacesUtils.bookmarks.TYPE_FOLDER
+ ? "folders"
+ : "bookmarks"
+ ]++;
+ }
+ };
+
+ PlacesUtils.observers.addListener(["bookmark-added"], listener);
+ const PROFILE = {
+ id: "Default",
+ name: "Default",
+ };
+ let observerNotified = false;
+ Services.obs.addObserver((aSubject, aTopic, aData) => {
+ let [toolbar, visibility] = JSON.parse(aData);
+ Assert.equal(
+ toolbar,
+ CustomizableUI.AREA_BOOKMARKS,
+ "Notification should be received for bookmarks toolbar"
+ );
+ Assert.equal(
+ visibility,
+ "true",
+ "Notification should say to reveal the bookmarks toolbar"
+ );
+ observerNotified = true;
+ }, "browser-set-toolbar-visibility");
+ const initialToolbarCount = await getFolderItemCount(
+ PlacesUtils.bookmarks.toolbarGuid
+ );
+ const initialUnfiledCount = await getFolderItemCount(
+ PlacesUtils.bookmarks.unfiledGuid
+ );
+ const initialmenuCount = await getFolderItemCount(
+ PlacesUtils.bookmarks.menuGuid
+ );
+
+ await promiseMigration(
+ migrator,
+ MigrationUtils.resourceTypes.BOOKMARKS,
+ PROFILE
+ );
+ const postToolbarCount = await getFolderItemCount(
+ PlacesUtils.bookmarks.toolbarGuid
+ );
+ const postUnfiledCount = await getFolderItemCount(
+ PlacesUtils.bookmarks.unfiledGuid
+ );
+ const postmenuCount = await getFolderItemCount(
+ PlacesUtils.bookmarks.menuGuid
+ );
+
+ Assert.equal(
+ postUnfiledCount - initialUnfiledCount,
+ 210,
+ "Should have seen 210 items in unsorted bookmarks"
+ );
+ Assert.equal(
+ postToolbarCount - initialToolbarCount,
+ 105,
+ "Should have seen 105 items in toolbar"
+ );
+ Assert.equal(
+ postmenuCount - initialmenuCount,
+ 0,
+ "Should have seen 0 items in menu toolbar"
+ );
+
+ PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+
+ Assert.equal(itemsSeen.bookmarks, 300, "Should have seen 300 bookmarks.");
+ Assert.equal(itemsSeen.folders, 15, "Should have seen 15 folders.");
+ Assert.equal(
+ MigrationUtils._importQuantities.bookmarks,
+ itemsSeen.bookmarks + itemsSeen.folders,
+ "Telemetry reporting correct."
+ );
+ Assert.ok(observerNotified, "The observer should be notified upon migration");
+ let pageUrls = Array.from(faviconURIs, f =>
+ Services.io.newURI(f.getResultByName("page_url"))
+ );
+ await assertFavicons(pageUrls);
+}
+
+add_task(async function test_Chrome() {
+ // Expire all favicons before the test to make sure favicons are imported
+ PlacesUtils.favicons.expireAllFavicons();
+ let subDirs =
+ AppConstants.platform == "linux" ? ["google-chrome"] : ["Google", "Chrome"];
+ await testBookmarks("chrome", subDirs);
+});
+
+add_task(async function test_ChromiumEdge() {
+ PlacesUtils.favicons.expireAllFavicons();
+ if (AppConstants.platform == "linux") {
+ // Edge isn't available on Linux.
+ return;
+ }
+ let subDirs =
+ AppConstants.platform == "macosx"
+ ? ["Microsoft Edge"]
+ : ["Microsoft", "Edge"];
+ await testBookmarks("chromium-edge", subDirs);
+});
+
+async function getFolderItemCount(guid) {
+ let results = await PlacesUtils.promiseBookmarksTree(guid);
+
+ return results.itemsCount;
+}
diff --git a/browser/components/migration/tests/unit/test_Chrome_corrupt_history.js b/browser/components/migration/tests/unit/test_Chrome_corrupt_history.js
new file mode 100644
index 0000000000..31541f9fdb
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_corrupt_history.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let rootDir = do_get_file("chromefiles/", true);
+
+const SOURCE_PROFILE_DIR = "Library/Application Support/Google/Chrome/Default/";
+const PROFILE = {
+ id: "Default",
+ name: "Person 1",
+};
+
+add_setup(async function setup_fake_paths() {
+ let pathId;
+ if (AppConstants.platform == "macosx") {
+ pathId = "ULibDir";
+ } else if (AppConstants.platform == "win") {
+ pathId = "LocalAppData";
+ } else {
+ pathId = "Home";
+ }
+ registerFakePath(pathId, rootDir);
+
+ let file = do_get_file(`${SOURCE_PROFILE_DIR}HistoryCorrupt`);
+ file.copyTo(file.parent, "History");
+
+ registerCleanupFunction(() => {
+ let historyFile = do_get_file(`${SOURCE_PROFILE_DIR}History`, true);
+ try {
+ historyFile.remove(false);
+ } catch (ex) {
+ // It is ok if this doesn't exist.
+ if (ex.result != Cr.NS_ERROR_FILE_NOT_FOUND) {
+ throw ex;
+ }
+ }
+ });
+
+ let subDirs =
+ AppConstants.platform == "linux" ? ["google-chrome"] : ["Google", "Chrome"];
+
+ if (AppConstants.platform == "macosx") {
+ subDirs.unshift("Application Support");
+ } else if (AppConstants.platform == "win") {
+ subDirs.push("User Data");
+ } else {
+ subDirs.unshift(".config");
+ }
+
+ let target = rootDir.clone();
+ // Pretend this is the default profile
+ subDirs.push("Default");
+ while (subDirs.length) {
+ target.append(subDirs.shift());
+ }
+
+ await IOUtils.makeDirectory(target.path, {
+ createAncestor: true,
+ ignoreExisting: true,
+ });
+
+ target.append("Bookmarks");
+ await IOUtils.remove(target.path, { ignoreAbsent: true });
+
+ let bookmarksData = createChromeBookmarkStructure();
+ await IOUtils.writeJSON(target.path, bookmarksData);
+});
+
+add_task(async function test_corrupt_history() {
+ let migrator = await MigrationUtils.getMigrator("chrome");
+ Assert.ok(await migrator.isSourceAvailable());
+
+ let data = await migrator.getMigrateData(PROFILE);
+ Assert.ok(
+ data & MigrationUtils.resourceTypes.BOOKMARKS,
+ "Bookmarks resource available."
+ );
+ Assert.ok(
+ !(data & MigrationUtils.resourceTypes.HISTORY),
+ "Corrupt history resource unavailable."
+ );
+});
diff --git a/browser/components/migration/tests/unit/test_Chrome_credit_cards.js b/browser/components/migration/tests/unit/test_Chrome_credit_cards.js
new file mode 100644
index 0000000000..5c4d3517d2
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_credit_cards.js
@@ -0,0 +1,239 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* global structuredClone */
+
+const PROFILE = {
+ id: "Default",
+ name: "Default",
+};
+
+const PAYMENT_METHODS = [
+ {
+ name_on_card: "Name Name",
+ card_number: "4532962432748929", // Visa
+ expiration_month: 3,
+ expiration_year: 2027,
+ },
+ {
+ name_on_card: "Name Name Name",
+ card_number: "5359908373796416", // Mastercard
+ expiration_month: 5,
+ expiration_year: 2028,
+ },
+ {
+ name_on_card: "Name",
+ card_number: "346624461807588", // AMEX
+ expiration_month: 4,
+ expiration_year: 2026,
+ },
+];
+
+let OSKeyStoreTestUtils;
+add_setup(async function os_key_store_setup() {
+ ({ OSKeyStoreTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/OSKeyStoreTestUtils.sys.mjs"
+ ));
+ OSKeyStoreTestUtils.setup();
+ registerCleanupFunction(async function cleanup() {
+ await OSKeyStoreTestUtils.cleanup();
+ });
+});
+
+let rootDir = do_get_file("chromefiles/", true);
+
+function checkCardsAreEqual(importedCard, testCard, id) {
+ const CC_NUMBER_RE = /^(\*+)(.{4})$/;
+
+ Assert.equal(
+ importedCard["cc-name"],
+ testCard.name_on_card,
+ "The two logins ID " + id + " have the same name on card"
+ );
+
+ let matches = CC_NUMBER_RE.exec(importedCard["cc-number"]);
+ Assert.notEqual(matches, null);
+ Assert.equal(importedCard["cc-number"].length, testCard.card_number.length);
+ Assert.equal(testCard.card_number.endsWith(matches[2]), true);
+ Assert.notEqual(importedCard["cc-number-encrypted"], "");
+
+ Assert.equal(
+ importedCard["cc-exp-month"],
+ testCard.expiration_month,
+ "The two logins ID " + id + " have the same expiration month"
+ );
+ Assert.equal(
+ importedCard["cc-exp-year"],
+ testCard.expiration_year,
+ "The two logins ID " + id + " have the same expiration year"
+ );
+}
+
+add_task(async function setup_fakePaths() {
+ let pathId;
+ if (AppConstants.platform == "macosx") {
+ pathId = "ULibDir";
+ } else if (AppConstants.platform == "win") {
+ pathId = "LocalAppData";
+ } else {
+ pathId = "Home";
+ }
+ registerFakePath(pathId, rootDir);
+});
+
+add_task(async function test_credit_cards() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo_check_true(
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+ "Cannot test OS key store login on official builds."
+ );
+ return;
+ }
+
+ let loginCrypto;
+ let profilePathSegments;
+
+ let mockMacOSKeychain = {
+ passphrase: "bW96aWxsYWZpcmVmb3g=",
+ serviceName: "TESTING Chrome Safe Storage",
+ accountName: "TESTING Chrome",
+ };
+ if (AppConstants.platform == "macosx") {
+ let { ChromeMacOSLoginCrypto } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMacOSLoginCrypto.sys.mjs"
+ );
+ loginCrypto = new ChromeMacOSLoginCrypto(
+ mockMacOSKeychain.serviceName,
+ mockMacOSKeychain.accountName,
+ mockMacOSKeychain.passphrase
+ );
+ profilePathSegments = [
+ "Application Support",
+ "Google",
+ "Chrome",
+ "Default",
+ ];
+ } else if (AppConstants.platform == "win") {
+ let { ChromeWindowsLoginCrypto } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeWindowsLoginCrypto.sys.mjs"
+ );
+ loginCrypto = new ChromeWindowsLoginCrypto("Chrome");
+ profilePathSegments = ["Google", "Chrome", "User Data", "Default"];
+ } else {
+ throw new Error("Not implemented");
+ }
+
+ let target = rootDir.clone();
+ let defaultFolderPath = PathUtils.join(target.path, ...profilePathSegments);
+ let webDataPath = PathUtils.join(defaultFolderPath, "Web Data");
+ let localStatePath = defaultFolderPath.replace("Default", "");
+
+ await IOUtils.makeDirectory(defaultFolderPath, {
+ createAncestor: true,
+ ignoreExisting: true,
+ });
+
+ // Copy Web Data database into Default profile
+ const sourcePathWebData = do_get_file(
+ "AppData/Local/Google/Chrome/User Data/Default/Web Data"
+ ).path;
+ await IOUtils.copy(sourcePathWebData, webDataPath);
+
+ const sourcePathLocalState = do_get_file(
+ "AppData/Local/Google/Chrome/User Data/Local State"
+ ).path;
+ await IOUtils.copy(sourcePathLocalState, localStatePath);
+
+ let dbConn = await Sqlite.openConnection({ path: webDataPath });
+
+ for (let card of PAYMENT_METHODS) {
+ let encryptedCardNumber = await loginCrypto.encryptData(card.card_number);
+ let cardNumberEncryptedValue = new Uint8Array(
+ loginCrypto.stringToArray(encryptedCardNumber)
+ );
+
+ let cardCopy = structuredClone(card);
+
+ cardCopy.card_number_encrypted = cardNumberEncryptedValue;
+ delete cardCopy.card_number;
+
+ await dbConn.execute(
+ `INSERT INTO credit_cards
+ (name_on_card, card_number_encrypted, expiration_month, expiration_year)
+ VALUES (:name_on_card, :card_number_encrypted, :expiration_month, :expiration_year)
+ `,
+ cardCopy
+ );
+ }
+
+ await dbConn.close();
+
+ let migrator = await MigrationUtils.getMigrator("chrome");
+ if (AppConstants.platform == "macosx") {
+ Object.assign(migrator, {
+ _keychainServiceName: mockMacOSKeychain.serviceName,
+ _keychainAccountName: mockMacOSKeychain.accountName,
+ _keychainMockPassphrase: mockMacOSKeychain.passphrase,
+ });
+ }
+ Assert.ok(
+ await migrator.isSourceAvailable(),
+ "Sanity check the source exists"
+ );
+
+ Services.prefs.setBoolPref(
+ "browser.migrate.chrome.payment_methods.enabled",
+ false
+ );
+ Assert.ok(
+ !(
+ (await migrator.getMigrateData(PROFILE)) &
+ MigrationUtils.resourceTypes.PAYMENT_METHODS
+ ),
+ "Should be able to disable migrating payment methods"
+ );
+ // Clear the cached resources now so that a re-check for payment methods
+ // will look again.
+ delete migrator._resourcesByProfile[PROFILE.id];
+
+ Services.prefs.setBoolPref(
+ "browser.migrate.chrome.payment_methods.enabled",
+ true
+ );
+
+ Assert.ok(
+ (await migrator.getMigrateData(PROFILE)) &
+ MigrationUtils.resourceTypes.PAYMENT_METHODS,
+ "Should be able to enable migrating payment methods"
+ );
+
+ let { formAutofillStorage } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofillStorage.sys.mjs"
+ );
+ await formAutofillStorage.initialize();
+
+ await promiseMigration(
+ migrator,
+ MigrationUtils.resourceTypes.PAYMENT_METHODS,
+ PROFILE
+ );
+
+ let cards = await formAutofillStorage.creditCards.getAll();
+
+ Assert.equal(
+ cards.length,
+ PAYMENT_METHODS.length,
+ "Check there are still the same number of credit cards after re-importing the data"
+ );
+ Assert.equal(
+ cards.length,
+ MigrationUtils._importQuantities.cards,
+ "Check telemetry matches the actual import."
+ );
+
+ for (let i = 0; i < PAYMENT_METHODS.length; i++) {
+ checkCardsAreEqual(cards[i], PAYMENT_METHODS[i], i + 1);
+ }
+});
diff --git a/browser/components/migration/tests/unit/test_Chrome_extensions.js b/browser/components/migration/tests/unit/test_Chrome_extensions.js
new file mode 100644
index 0000000000..7aa7a94194
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_extensions.js
@@ -0,0 +1,165 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AMBrowserExtensionsImport, AddonManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { ChromeMigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMigrationUtils.sys.mjs"
+);
+const { ChromeProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeProfileMigrator.sys.mjs"
+);
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+const TEST_SERVER = AddonTestUtils.createHttpServer({ hosts: ["example.com"] });
+
+const PROFILE = {
+ id: "Default",
+ name: "Person 1",
+};
+
+const IMPORTED_ADDON_1 = {
+ name: "A Firefox extension",
+ version: "1.0",
+ browser_specific_settings: { gecko: { id: "some-ff@extension" } },
+};
+
+// Setup chrome user data path for all platforms.
+ChromeMigrationUtils.getDataPath = () => {
+ return Promise.resolve(
+ do_get_file("Library/Application Support/Google/Chrome/").path
+ );
+};
+
+const mockAddonRepository = ({ addons = [] } = {}) => {
+ return {
+ async getMappedAddons(browserID, extensionIDs) {
+ Assert.equal(browserID, "chrome", "expected browser ID");
+ // Sort extension IDs to have a predictable order.
+ extensionIDs.sort();
+ Assert.deepEqual(
+ extensionIDs,
+ ["fake-extension-1", "fake-extension-2"],
+ "expected an array of 2 extension IDs"
+ );
+ return Promise.resolve({
+ addons,
+ matchedIDs: [],
+ unmatchedIDs: [],
+ });
+ },
+ };
+};
+
+add_setup(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+
+ // Create a Firefox XPI that we can use during the import.
+ const xpi = AddonTestUtils.createTempWebExtensionFile({
+ manifest: IMPORTED_ADDON_1,
+ });
+ TEST_SERVER.registerFile(`/addons/addon-1.xpi`, xpi);
+
+ // Override the `AddonRepository` in `AMBrowserExtensionsImport` with our own
+ // mock so that we control the add-ons that are mapped to the list of Chrome
+ // extension IDs.
+ const addons = [
+ {
+ id: IMPORTED_ADDON_1.browser_specific_settings.gecko.id,
+ name: IMPORTED_ADDON_1.name,
+ version: IMPORTED_ADDON_1.version,
+ sourceURI: Services.io.newURI(`http://example.com/addons/addon-1.xpi`),
+ icons: {},
+ },
+ ];
+ AMBrowserExtensionsImport._addonRepository = mockAddonRepository({ addons });
+
+ registerCleanupFunction(() => {
+ // Clear the add-on repository override.
+ AMBrowserExtensionsImport._addonRepository = null;
+ });
+});
+
+add_task(
+ { pref_set: [["browser.migrate.chrome.extensions.enabled", true]] },
+ async function test_import_extensions() {
+ const migrator = await MigrationUtils.getMigrator("chrome");
+ Assert.ok(
+ await migrator.isSourceAvailable(),
+ "Sanity check the source exists"
+ );
+
+ let promiseTopic = TestUtils.topicObserved(
+ "webextension-imported-addons-pending"
+ );
+ await promiseMigration(
+ migrator,
+ MigrationUtils.resourceTypes.EXTENSIONS,
+ PROFILE,
+ true
+ );
+ await promiseTopic;
+ // When this property is `true`, the UI should show a badge on the appmenu
+ // button, and the user can finalize the import later.
+ Assert.ok(
+ AMBrowserExtensionsImport.canCompleteOrCancelInstalls,
+ "expected some add-ons to have been imported"
+ );
+
+ // Let's actually complete the import programatically below.
+ promiseTopic = TestUtils.topicObserved(
+ "webextension-imported-addons-complete"
+ );
+ await AMBrowserExtensionsImport.completeInstalls();
+ await Promise.all([
+ AddonTestUtils.promiseInstallEvent("onInstallEnded"),
+ promiseTopic,
+ ]);
+
+ // The add-on should be installed and therefore it can be uninstalled.
+ const addon = await AddonManager.getAddonByID(
+ IMPORTED_ADDON_1.browser_specific_settings.gecko.id
+ );
+ await addon.uninstall();
+ }
+);
+
+/**
+ * Test that, for now at least, the extension resource type is only made
+ * available for Chrome and none of the derivitive browsers.
+ */
+add_task(
+ { pref_set: [["browser.migrate.chrome.extensions.enabled", true]] },
+ async function test_only_chrome_migrates_extensions() {
+ for (const key of MigrationUtils.availableMigratorKeys) {
+ let migrator = await MigrationUtils.getMigrator(key);
+
+ if (migrator instanceof ChromeProfileMigrator && key != "chrome") {
+ info("Testing migrator with key " + key);
+ Assert.ok(
+ await migrator.isSourceAvailable(),
+ "First check the source exists"
+ );
+ let resourceTypes = await migrator.getMigrateData(PROFILE);
+ Assert.ok(
+ !(resourceTypes & MigrationUtils.resourceTypes.EXTENSIONS),
+ "Should not offer the extension resource type"
+ );
+ }
+ }
+ }
+);
diff --git a/browser/components/migration/tests/unit/test_Chrome_formdata.js b/browser/components/migration/tests/unit/test_Chrome_formdata.js
new file mode 100644
index 0000000000..1dc411cb14
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_formdata.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { FormHistory } = ChromeUtils.importESModule(
+ "resource://gre/modules/FormHistory.sys.mjs"
+);
+
+let rootDir = do_get_file("chromefiles/", true);
+
+add_setup(async function setup_fakePaths() {
+ let pathId;
+ if (AppConstants.platform == "macosx") {
+ pathId = "ULibDir";
+ } else if (AppConstants.platform == "win") {
+ pathId = "LocalAppData";
+ } else {
+ pathId = "Home";
+ }
+ registerFakePath(pathId, rootDir);
+});
+
+/**
+ * This function creates a testing database in the default profile,
+ * populates it with 10 example data entries,migrates the database,
+ * and then searches for each entry to ensure it exists in the FormHistory.
+ *
+ * @async
+ * @param {string} migratorKey
+ * A string that identifies the type of migrator object to be retrieved.
+ * @param {Array<string>} subDirs
+ * An array of strings that specifies the subdirectories for the target profile directory.
+ * @returns {Promise<undefined>}
+ * A Promise that resolves when the migration is completed.
+ */
+async function testFormdata(migratorKey, subDirs) {
+ if (AppConstants.platform == "macosx") {
+ subDirs.unshift("Application Support");
+ } else if (AppConstants.platform == "win") {
+ subDirs.push("User Data");
+ } else {
+ subDirs.unshift(".config");
+ }
+
+ let target = rootDir.clone();
+ // Pretend this is the default profile
+ subDirs.push("Default");
+ while (subDirs.length) {
+ target.append(subDirs.shift());
+ }
+
+ await IOUtils.makeDirectory(target.path, {
+ createAncestor: true,
+ ignoreExisting: true,
+ });
+
+ target.append("Web Data");
+ await IOUtils.remove(target.path, { ignoreAbsent: true });
+
+ // Clear any search history results
+ await FormHistory.update({ op: "remove" });
+
+ let dbConn = await Sqlite.openConnection({ path: target.path });
+
+ await dbConn.execute(
+ `CREATE TABLE "autofill" (name VARCHAR, value VARCHAR, value_lower VARCHAR, date_created INTEGER DEFAULT 0, date_last_used INTEGER DEFAULT 0, count INTEGER DEFAULT 1, PRIMARY KEY (name, value))`
+ );
+ for (let i = 0; i < 10; i++) {
+ await dbConn.execute(
+ `INSERT INTO autofill VALUES (:name, :value, :value_lower, :date_created, :date_last_used, :count)`,
+ {
+ name: `name${i}`,
+ value: `example${i}`,
+ value_lower: `example${i}`,
+ date_created: Math.round(Date.now() / 1000) - i * 10000,
+ date_last_used: Date.now(),
+ count: i,
+ }
+ );
+ }
+ await dbConn.close();
+
+ let migrator = await MigrationUtils.getMigrator(migratorKey);
+ // Sanity check for the source.
+ Assert.ok(await migrator.isSourceAvailable());
+
+ await promiseMigration(migrator, MigrationUtils.resourceTypes.FORMDATA, {
+ id: "Default",
+ name: "Person 1",
+ });
+
+ for (let i = 0; i < 10; i++) {
+ let results = await FormHistory.search(["fieldname", "value"], {
+ fieldname: `name${i}`,
+ value: `example${i}`,
+ });
+ Assert.ok(results.length, `Should have item${i} in FormHistory`);
+ }
+}
+
+add_task(async function test_Chrome() {
+ let subDirs =
+ AppConstants.platform == "linux" ? ["google-chrome"] : ["Google", "Chrome"];
+ await testFormdata("chrome", subDirs);
+});
+
+add_task(async function test_ChromiumEdge() {
+ if (AppConstants.platform == "linux") {
+ // Edge isn't available on Linux.
+ return;
+ }
+ let subDirs =
+ AppConstants.platform == "macosx"
+ ? ["Microsoft Edge"]
+ : ["Microsoft", "Edge"];
+ await testFormdata("chromium-edge", subDirs);
+});
diff --git a/browser/components/migration/tests/unit/test_Chrome_history.js b/browser/components/migration/tests/unit/test_Chrome_history.js
new file mode 100644
index 0000000000..c88a6380c2
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_history.js
@@ -0,0 +1,206 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ChromeMigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMigrationUtils.sys.mjs"
+);
+
+const SOURCE_PROFILE_DIR = "Library/Application Support/Google/Chrome/Default/";
+
+const PROFILE = {
+ id: "Default",
+ name: "Person 1",
+};
+
+/**
+ * TEST_URLS reflects the data stored in '${SOURCE_PROFILE_DIR}HistoryMaster'.
+ * The main object reflects the data in the 'urls' table. The visits property
+ * reflects the associated data in the 'visits' table.
+ */
+const TEST_URLS = [
+ {
+ id: 1,
+ url: "http://example.com/",
+ title: "test",
+ visit_count: 1,
+ typed_count: 0,
+ last_visit_time: 13193151310368000,
+ hidden: 0,
+ visits: [
+ {
+ id: 1,
+ url: 1,
+ visit_time: 13193151310368000,
+ from_visit: 0,
+ transition: 805306370,
+ segment_id: 0,
+ visit_duration: 10745006,
+ incremented_omnibox_typed_score: 0,
+ },
+ ],
+ },
+ {
+ id: 2,
+ url: "http://invalid.com/",
+ title: "test2",
+ visit_count: 1,
+ typed_count: 0,
+ last_visit_time: 13193154948901000,
+ hidden: 0,
+ visits: [
+ {
+ id: 2,
+ url: 2,
+ visit_time: 13193154948901000,
+ from_visit: 0,
+ transition: 805306376,
+ segment_id: 0,
+ visit_duration: 6568270,
+ incremented_omnibox_typed_score: 0,
+ },
+ ],
+ },
+];
+
+async function setVisitTimes(time) {
+ let loginDataFile = do_get_file(`${SOURCE_PROFILE_DIR}History`);
+ let dbConn = await Sqlite.openConnection({ path: loginDataFile.path });
+
+ await dbConn.execute(`UPDATE urls SET last_visit_time = :last_visit_time`, {
+ last_visit_time: time,
+ });
+ await dbConn.execute(`UPDATE visits SET visit_time = :visit_time`, {
+ visit_time: time,
+ });
+
+ await dbConn.close();
+}
+
+function setExpectedVisitTimes(time) {
+ for (let urlInfo of TEST_URLS) {
+ urlInfo.last_visit_time = time;
+ urlInfo.visits[0].visit_time = time;
+ }
+}
+
+function assertEntryMatches(entry, urlInfo, dateWasInFuture = false) {
+ info(`Checking url: ${urlInfo.url}`);
+ Assert.ok(entry, `Should have stored an entry`);
+
+ Assert.equal(entry.url, urlInfo.url, "Should have the correct URL");
+ Assert.equal(entry.title, urlInfo.title, "Should have the correct title");
+ Assert.equal(
+ entry.visits.length,
+ urlInfo.visits.length,
+ "Should have the correct number of visits"
+ );
+
+ for (let index in urlInfo.visits) {
+ Assert.equal(
+ entry.visits[index].transition,
+ PlacesUtils.history.TRANSITIONS.LINK,
+ "Should have Link type transition"
+ );
+
+ if (dateWasInFuture) {
+ Assert.lessOrEqual(
+ entry.visits[index].date.getTime(),
+ new Date().getTime(),
+ "Should have moved the date to no later than the current date."
+ );
+ } else {
+ Assert.equal(
+ entry.visits[index].date.getTime(),
+ ChromeMigrationUtils.chromeTimeToDate(
+ urlInfo.visits[index].visit_time,
+ new Date()
+ ).getTime(),
+ "Should have the correct date"
+ );
+ }
+ }
+}
+
+function setupHistoryFile() {
+ removeHistoryFile();
+ let file = do_get_file(`${SOURCE_PROFILE_DIR}HistoryMaster`);
+ file.copyTo(file.parent, "History");
+}
+
+function removeHistoryFile() {
+ let file = do_get_file(`${SOURCE_PROFILE_DIR}History`, true);
+ try {
+ file.remove(false);
+ } catch (ex) {
+ // It is ok if this doesn't exist.
+ if (ex.result != Cr.NS_ERROR_FILE_NOT_FOUND) {
+ throw ex;
+ }
+ }
+}
+
+add_task(async function setup() {
+ registerFakePath("ULibDir", do_get_file("Library/"));
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ removeHistoryFile();
+ });
+});
+
+add_task(async function test_import() {
+ setupHistoryFile();
+ await PlacesUtils.history.clear();
+ // Update to ~10 days ago since the date can't be too old or Places may expire it.
+ const pastDate = new Date(new Date().getTime() - 1000 * 60 * 60 * 24 * 10);
+ const pastChromeTime = ChromeMigrationUtils.dateToChromeTime(pastDate);
+ await setVisitTimes(pastChromeTime);
+ setExpectedVisitTimes(pastChromeTime);
+
+ let migrator = await MigrationUtils.getMigrator("chrome");
+ Assert.ok(
+ await migrator.isSourceAvailable(),
+ "Sanity check the source exists"
+ );
+
+ await promiseMigration(
+ migrator,
+ MigrationUtils.resourceTypes.HISTORY,
+ PROFILE
+ );
+
+ for (let urlInfo of TEST_URLS) {
+ let entry = await PlacesUtils.history.fetch(urlInfo.url, {
+ includeVisits: true,
+ });
+ assertEntryMatches(entry, urlInfo);
+ }
+});
+
+add_task(async function test_import_future_date() {
+ setupHistoryFile();
+ await PlacesUtils.history.clear();
+ const futureDate = new Date().getTime() + 6000 * 60 * 24;
+ await setVisitTimes(ChromeMigrationUtils.dateToChromeTime(futureDate));
+
+ let migrator = await MigrationUtils.getMigrator("chrome");
+ Assert.ok(
+ await migrator.isSourceAvailable(),
+ "Sanity check the source exists"
+ );
+
+ await promiseMigration(
+ migrator,
+ MigrationUtils.resourceTypes.HISTORY,
+ PROFILE
+ );
+
+ for (let urlInfo of TEST_URLS) {
+ let entry = await PlacesUtils.history.fetch(urlInfo.url, {
+ includeVisits: true,
+ });
+ assertEntryMatches(entry, urlInfo, true);
+ }
+});
diff --git a/browser/components/migration/tests/unit/test_Chrome_passwords.js b/browser/components/migration/tests/unit/test_Chrome_passwords.js
new file mode 100644
index 0000000000..374b697c75
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_passwords.js
@@ -0,0 +1,373 @@
+"use strict";
+
+const PROFILE = {
+ id: "Default",
+ name: "Person 1",
+};
+
+const TEST_LOGINS = [
+ {
+ id: 1, // id of the row in the chrome login db
+ username: "username",
+ password: "password",
+ origin: "https://c9.io",
+ formActionOrigin: "https://c9.io",
+ httpRealm: null,
+ usernameField: "inputEmail",
+ passwordField: "inputPassword",
+ timeCreated: 1437418416037,
+ timePasswordChanged: 1437418416037,
+ timesUsed: 1,
+ },
+ {
+ id: 2,
+ username: "username@gmail.com",
+ password: "password2",
+ origin: "https://accounts.google.com",
+ formActionOrigin: "https://accounts.google.com",
+ httpRealm: null,
+ usernameField: "Email",
+ passwordField: "Passwd",
+ timeCreated: 1437418446598,
+ timePasswordChanged: 1437418446598,
+ timesUsed: 6,
+ },
+ {
+ id: 3,
+ username: "username",
+ password: "password3",
+ origin: "https://www.facebook.com",
+ formActionOrigin: "https://www.facebook.com",
+ httpRealm: null,
+ usernameField: "email",
+ passwordField: "pass",
+ timeCreated: 1437418478851,
+ timePasswordChanged: 1437418478851,
+ timesUsed: 1,
+ },
+ {
+ id: 4,
+ username: "user",
+ password: "اقرأPÀßwörd",
+ origin: "http://httpbin.org",
+ formActionOrigin: null,
+ httpRealm: "me@kennethreitz.com", // Digest auth.
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1437787462368,
+ timePasswordChanged: 1437787462368,
+ timesUsed: 1,
+ },
+ {
+ id: 5,
+ username: "buser",
+ password: "bpassword",
+ origin: "http://httpbin.org",
+ formActionOrigin: null,
+ httpRealm: "Fake Realm", // Basic auth.
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1437787539233,
+ timePasswordChanged: 1437787539233,
+ timesUsed: 1,
+ },
+ {
+ id: 6,
+ username: "username",
+ password: "password6",
+ origin: "https://www.example.com",
+ formActionOrigin: "", // NULL `action_url`
+ httpRealm: null,
+ usernameField: "",
+ passwordField: "pass",
+ timeCreated: 1557291348878,
+ timePasswordChanged: 1557291348878,
+ timesUsed: 1,
+ },
+ {
+ id: 7,
+ version: "v10",
+ username: "username",
+ password: "password",
+ origin: "https://v10.io",
+ formActionOrigin: "https://v10.io",
+ httpRealm: null,
+ usernameField: "inputEmail",
+ passwordField: "inputPassword",
+ timeCreated: 1437418416037,
+ timePasswordChanged: 1437418416037,
+ timesUsed: 1,
+ },
+];
+
+var loginCrypto;
+var dbConn;
+
+async function promiseSetPassword(login) {
+ const encryptedString = await loginCrypto.encryptData(
+ login.password,
+ login.version
+ );
+ info(`promiseSetPassword: ${encryptedString}`);
+ const passwordValue = new Uint8Array(
+ loginCrypto.stringToArray(encryptedString)
+ );
+ return dbConn.execute(
+ `UPDATE logins
+ SET password_value = :password_value
+ WHERE rowid = :rowid
+ `,
+ { password_value: passwordValue, rowid: login.id }
+ );
+}
+
+function checkLoginsAreEqual(passwordManagerLogin, chromeLogin, id) {
+ passwordManagerLogin.QueryInterface(Ci.nsILoginMetaInfo);
+
+ Assert.equal(
+ passwordManagerLogin.username,
+ chromeLogin.username,
+ "The two logins ID " + id + " have the same username"
+ );
+ Assert.equal(
+ passwordManagerLogin.password,
+ chromeLogin.password,
+ "The two logins ID " + id + " have the same password"
+ );
+ Assert.equal(
+ passwordManagerLogin.origin,
+ chromeLogin.origin,
+ "The two logins ID " + id + " have the same origin"
+ );
+ Assert.equal(
+ passwordManagerLogin.formActionOrigin,
+ chromeLogin.formActionOrigin,
+ "The two logins ID " + id + " have the same formActionOrigin"
+ );
+ Assert.equal(
+ passwordManagerLogin.httpRealm,
+ chromeLogin.httpRealm,
+ "The two logins ID " + id + " have the same httpRealm"
+ );
+ Assert.equal(
+ passwordManagerLogin.usernameField,
+ chromeLogin.usernameField,
+ "The two logins ID " + id + " have the same usernameElement"
+ );
+ Assert.equal(
+ passwordManagerLogin.passwordField,
+ chromeLogin.passwordField,
+ "The two logins ID " + id + " have the same passwordElement"
+ );
+ Assert.equal(
+ passwordManagerLogin.timeCreated,
+ chromeLogin.timeCreated,
+ "The two logins ID " + id + " have the same timeCreated"
+ );
+ Assert.equal(
+ passwordManagerLogin.timePasswordChanged,
+ chromeLogin.timePasswordChanged,
+ "The two logins ID " + id + " have the same timePasswordChanged"
+ );
+ Assert.equal(
+ passwordManagerLogin.timesUsed,
+ chromeLogin.timesUsed,
+ "The two logins ID " + id + " have the same timesUsed"
+ );
+}
+
+function generateDifferentLogin(login) {
+ const newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+ Ci.nsILoginInfo
+ );
+
+ newLogin.init(
+ login.origin,
+ login.formActionOrigin,
+ null,
+ login.username,
+ login.password + 1,
+ login.usernameField + 1,
+ login.passwordField + 1
+ );
+ newLogin.QueryInterface(Ci.nsILoginMetaInfo);
+ newLogin.timeCreated = login.timeCreated + 1;
+ newLogin.timePasswordChanged = login.timePasswordChanged + 1;
+ newLogin.timesUsed = login.timesUsed + 1;
+ return newLogin;
+}
+
+add_task(async function setup() {
+ let dirSvcPath;
+ let pathId;
+ let profilePathSegments;
+
+ // Use a mock service and account name to avoid a Keychain auth. prompt that
+ // would block the test from finishing if Chrome has already created a matching
+ // Keychain entry. This allows us to still exercise the keychain lookup code.
+ // The mock encryption passphrase is used when the Keychain item isn't found.
+ const mockMacOSKeychain = {
+ passphrase: "bW96aWxsYWZpcmVmb3g=",
+ serviceName: "TESTING Chrome Safe Storage",
+ accountName: "TESTING Chrome",
+ };
+ if (AppConstants.platform == "macosx") {
+ const { ChromeMacOSLoginCrypto } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMacOSLoginCrypto.sys.mjs"
+ );
+ loginCrypto = new ChromeMacOSLoginCrypto(
+ mockMacOSKeychain.serviceName,
+ mockMacOSKeychain.accountName,
+ mockMacOSKeychain.passphrase
+ );
+ dirSvcPath = "Library/";
+ pathId = "ULibDir";
+ profilePathSegments = [
+ "Application Support",
+ "Google",
+ "Chrome",
+ "Default",
+ "Login Data",
+ ];
+ } else if (AppConstants.platform == "win") {
+ const { ChromeWindowsLoginCrypto } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeWindowsLoginCrypto.sys.mjs"
+ );
+ loginCrypto = new ChromeWindowsLoginCrypto("Chrome");
+ dirSvcPath = "AppData/Local/";
+ pathId = "LocalAppData";
+ profilePathSegments = [
+ "Google",
+ "Chrome",
+ "User Data",
+ "Default",
+ "Login Data",
+ ];
+ } else {
+ throw new Error("Not implemented");
+ }
+ const dirSvcFile = do_get_file(dirSvcPath);
+ registerFakePath(pathId, dirSvcFile);
+
+ info(PathUtils.join(dirSvcFile.path, ...profilePathSegments));
+ const loginDataFilePath = PathUtils.join(
+ dirSvcFile.path,
+ ...profilePathSegments
+ );
+ dbConn = await Sqlite.openConnection({ path: loginDataFilePath });
+
+ if (AppConstants.platform == "macosx") {
+ const migrator = await MigrationUtils.getMigrator("chrome");
+ Object.assign(migrator, {
+ _keychainServiceName: mockMacOSKeychain.serviceName,
+ _keychainAccountName: mockMacOSKeychain.accountName,
+ _keychainMockPassphrase: mockMacOSKeychain.passphrase,
+ });
+ }
+
+ registerCleanupFunction(() => {
+ Services.logins.removeAllUserFacingLogins();
+ if (loginCrypto.finalize) {
+ loginCrypto.finalize();
+ }
+ return dbConn.close();
+ });
+});
+
+add_task(async function test_importIntoEmptyDB() {
+ for (const login of TEST_LOGINS) {
+ await promiseSetPassword(login);
+ }
+
+ const migrator = await MigrationUtils.getMigrator("chrome");
+ Assert.ok(
+ await migrator.isSourceAvailable(),
+ "Sanity check the source exists"
+ );
+
+ let logins = await Services.logins.getAllLogins();
+ Assert.equal(logins.length, 0, "There are no logins initially");
+
+ // Migrate the logins.
+ await promiseMigration(
+ migrator,
+ MigrationUtils.resourceTypes.PASSWORDS,
+ PROFILE,
+ true
+ );
+
+ logins = await Services.logins.getAllLogins();
+ Assert.equal(
+ logins.length,
+ TEST_LOGINS.length,
+ "Check login count after importing the data"
+ );
+ Assert.equal(
+ logins.length,
+ MigrationUtils._importQuantities.logins,
+ "Check telemetry matches the actual import."
+ );
+
+ for (let i = 0; i < TEST_LOGINS.length; i++) {
+ checkLoginsAreEqual(logins[i], TEST_LOGINS[i], i + 1);
+ }
+});
+
+// Test that existing logins for the same primary key don't get overwritten
+add_task(async function test_importExistingLogins() {
+ const migrator = await MigrationUtils.getMigrator("chrome");
+ Assert.ok(
+ await migrator.isSourceAvailable(),
+ "Sanity check the source exists"
+ );
+
+ Services.logins.removeAllUserFacingLogins();
+ let logins = await Services.logins.getAllLogins();
+ Assert.equal(
+ logins.length,
+ 0,
+ "There are no logins after removing all of them"
+ );
+
+ const newLogins = [];
+
+ // Create 3 new logins that are different but where the key properties are still the same.
+ for (let i = 0; i < 3; i++) {
+ newLogins.push(generateDifferentLogin(TEST_LOGINS[i]));
+ await Services.logins.addLoginAsync(newLogins[i]);
+ }
+
+ logins = await Services.logins.getAllLogins();
+ Assert.equal(
+ logins.length,
+ newLogins.length,
+ "Check login count after the insertion"
+ );
+
+ for (let i = 0; i < newLogins.length; i++) {
+ checkLoginsAreEqual(logins[i], newLogins[i], i + 1);
+ }
+ // Migrate the logins.
+ await promiseMigration(
+ migrator,
+ MigrationUtils.resourceTypes.PASSWORDS,
+ PROFILE,
+ true
+ );
+
+ logins = await Services.logins.getAllLogins();
+ Assert.equal(
+ logins.length,
+ TEST_LOGINS.length,
+ "Check there are still the same number of logins after re-importing the data"
+ );
+ Assert.equal(
+ logins.length,
+ MigrationUtils._importQuantities.logins,
+ "Check telemetry matches the actual import."
+ );
+
+ for (let i = 0; i < newLogins.length; i++) {
+ checkLoginsAreEqual(logins[i], newLogins[i], i + 1);
+ }
+});
diff --git a/browser/components/migration/tests/unit/test_Chrome_passwords_emptySource.js b/browser/components/migration/tests/unit/test_Chrome_passwords_emptySource.js
new file mode 100644
index 0000000000..0e6993fded
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_passwords_emptySource.js
@@ -0,0 +1,43 @@
+"use strict";
+
+const { ChromeProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeProfileMigrator.sys.mjs"
+);
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+add_task(async function test_importEmptyDBWithoutAuthPrompts() {
+ let dirSvcPath;
+ let pathId;
+
+ if (AppConstants.platform == "macosx") {
+ dirSvcPath = "LibraryWithNoData/";
+ pathId = "ULibDir";
+ } else if (AppConstants.platform == "win") {
+ dirSvcPath = "AppData/LocalWithNoData/";
+ pathId = "LocalAppData";
+ } else {
+ throw new Error("Not implemented");
+ }
+ let dirSvcFile = do_get_file(dirSvcPath);
+ registerFakePath(pathId, dirSvcFile);
+
+ let sandbox = sinon.createSandbox();
+ sandbox
+ .stub(ChromeProfileMigrator.prototype, "canGetPermissions")
+ .resolves(true);
+ sandbox
+ .stub(ChromeProfileMigrator.prototype, "hasPermissions")
+ .resolves(true);
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let migrator = await MigrationUtils.getMigrator("chrome");
+ Assert.ok(
+ !migrator,
+ "Migrator should not be available since there are no passwords"
+ );
+});
diff --git a/browser/components/migration/tests/unit/test_Chrome_permissions.js b/browser/components/migration/tests/unit/test_Chrome_permissions.js
new file mode 100644
index 0000000000..6dfd8bcceb
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_permissions.js
@@ -0,0 +1,205 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that if the migrator does not have the permission to
+ * read from the Chrome data directory, that it can request
+ * permission to read from it, if the system allows.
+ */
+
+const { ChromeMigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMigrationUtils.sys.mjs"
+);
+
+const { ChromeProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeProfileMigrator.sys.mjs"
+);
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+const { MockFilePicker } = ChromeUtils.importESModule(
+ "resource://testing-common/MockFilePicker.sys.mjs"
+);
+
+let gTempDir;
+
+add_setup(async () => {
+ Services.prefs.setBoolPref(
+ "browser.migrate.chrome.get_permissions.enabled",
+ true
+ );
+ gTempDir = do_get_tempdir();
+ await IOUtils.writeJSON(PathUtils.join(gTempDir.path, "Local State"), []);
+
+ MockFilePicker.init(globalThis);
+ registerCleanupFunction(() => {
+ MockFilePicker.cleanup();
+ });
+});
+
+/**
+ * Tests that canGetPermissions will return false if the platform does
+ * not allow for folder selection in the native file picker, and returns
+ * the data path otherwise.
+ */
+add_task(async function test_canGetPermissions() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let migrator = new ChromeProfileMigrator();
+ let canGetPermissionsStub = sandbox
+ .stub(MigrationUtils, "canGetPermissionsOnPlatform")
+ .resolves(false);
+ sandbox.stub(ChromeMigrationUtils, "getDataPath").resolves(gTempDir.path);
+
+ Assert.ok(
+ !(await migrator.canGetPermissions()),
+ "Should not be able to get permissions."
+ );
+
+ canGetPermissionsStub.resolves(true);
+
+ Assert.equal(
+ await migrator.canGetPermissions(),
+ gTempDir.path,
+ "Should be able to get the permissions path."
+ );
+
+ sandbox.restore();
+});
+
+/**
+ * Tests that getPermissions will show the native file picker in a
+ * loop until either the user cancels or selects a folder that grants
+ * read permissions to the data directory.
+ */
+add_task(async function test_getPermissions() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let migrator = new ChromeProfileMigrator();
+ sandbox.stub(MigrationUtils, "canGetPermissionsOnPlatform").resolves(true);
+ sandbox.stub(ChromeMigrationUtils, "getDataPath").resolves(gTempDir.path);
+ let hasPermissionsStub = sandbox
+ .stub(migrator, "hasPermissions")
+ .resolves(false);
+
+ Assert.equal(
+ await migrator.canGetPermissions(),
+ gTempDir.path,
+ "Should be able to get the permissions path."
+ );
+
+ let filePickerSeenCount = 0;
+
+ let filePickerShownPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = () => {
+ Assert.ok(true, "Filepicker shown.");
+ MockFilePicker.useDirectory([gTempDir.path]);
+ filePickerSeenCount++;
+ if (filePickerSeenCount > 3) {
+ Assert.ok(true, "File picker looped 3 times.");
+ hasPermissionsStub.resolves(true);
+ resolve();
+ }
+ };
+ });
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+ Assert.ok(
+ await migrator.getPermissions(),
+ "Should report that we got permissions."
+ );
+
+ await filePickerShownPromise;
+
+ // Make sure that the user can also hit "cancel" and that we
+ // file picker loop.
+
+ hasPermissionsStub.resolves(false);
+
+ filePickerSeenCount = 0;
+ filePickerShownPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = () => {
+ Assert.ok(true, "Filepicker shown.");
+ filePickerSeenCount++;
+ Assert.equal(filePickerSeenCount, 1, "Saw the picker once.");
+ resolve();
+ };
+ });
+ MockFilePicker.returnValue = MockFilePicker.returnCancel;
+ Assert.ok(
+ !(await migrator.getPermissions()),
+ "Should report that we didn't get permissions."
+ );
+ await filePickerShownPromise;
+
+ sandbox.restore();
+});
+
+/**
+ * Tests that if the native file picker chooses a different directory
+ * than the one we originally asked for, that we remap attempts to
+ * read profiles from that new directory. This is because Ubuntu Snaps
+ * will return us paths from the native file picker that are symlinks
+ * to the original directories.
+ */
+add_task(async function test_remapDirectories() {
+ let remapDir = new FileUtils.File(
+ await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "test-chrome-migration"
+ )
+ );
+ let localStatePath = PathUtils.join(remapDir.path, "Local State");
+ await IOUtils.writeJSON(localStatePath, []);
+
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let migrator = new ChromeProfileMigrator();
+ sandbox.stub(MigrationUtils, "canGetPermissionsOnPlatform").resolves(true);
+ sandbox.stub(ChromeMigrationUtils, "getDataPath").resolves(gTempDir.path);
+ let hasPermissionsStub = sandbox
+ .stub(migrator, "hasPermissions")
+ .resolves(false);
+
+ Assert.equal(
+ await migrator.canGetPermissions(),
+ gTempDir.path,
+ "Should be able to get the permissions path."
+ );
+
+ let filePickerShownPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = () => {
+ Assert.ok(true, "Filepicker shown.");
+ MockFilePicker.useDirectory([remapDir.path]);
+ hasPermissionsStub.resolves(true);
+ resolve();
+ };
+ });
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+ Assert.ok(
+ await migrator.getPermissions(),
+ "Should report that we got permissions."
+ );
+
+ Assert.equal(
+ PathUtils.normalize(await migrator.canGetPermissions()),
+ PathUtils.normalize(remapDir.path),
+ "Should be able to get the remapped permissions path."
+ );
+
+ await filePickerShownPromise;
+
+ sandbox.restore();
+});
diff --git a/browser/components/migration/tests/unit/test_Edge_db_migration.js b/browser/components/migration/tests/unit/test_Edge_db_migration.js
new file mode 100644
index 0000000000..3b2672d9d8
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Edge_db_migration.js
@@ -0,0 +1,849 @@
+"use strict";
+
+const { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+);
+const { ESE, KERNEL, gLibs, COLUMN_TYPES, declareESEFunction, loadLibraries } =
+ ChromeUtils.importESModule("resource:///modules/ESEDBReader.sys.mjs");
+const { EdgeProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/EdgeProfileMigrator.sys.mjs"
+);
+
+let gESEInstanceCounter = 1;
+
+ESE.JET_COLUMNCREATE_W = new ctypes.StructType("JET_COLUMNCREATE_W", [
+ { cbStruct: ctypes.unsigned_long },
+ { szColumnName: ESE.JET_PCWSTR },
+ { coltyp: ESE.JET_COLTYP },
+ { cbMax: ctypes.unsigned_long },
+ { grbit: ESE.JET_GRBIT },
+ { pvDefault: ctypes.voidptr_t },
+ { cbDefault: ctypes.unsigned_long },
+ { cp: ctypes.unsigned_long },
+ { columnid: ESE.JET_COLUMNID },
+ { err: ESE.JET_ERR },
+]);
+
+function createColumnCreationWrapper({ name, type, cbMax }) {
+ // We use a wrapper object because we need to be sure the JS engine won't GC
+ // data that we're "only" pointing to.
+ let wrapper = {};
+ wrapper.column = new ESE.JET_COLUMNCREATE_W();
+ wrapper.column.cbStruct = ESE.JET_COLUMNCREATE_W.size;
+ let wchar_tArray = ctypes.ArrayType(ctypes.char16_t);
+ wrapper.name = new wchar_tArray(name.length + 1);
+ wrapper.name.value = String(name);
+ wrapper.column.szColumnName = wrapper.name;
+ wrapper.column.coltyp = type;
+ let fallback = 0;
+ switch (type) {
+ case COLUMN_TYPES.JET_coltypText:
+ fallback = 255;
+ // Intentional fall-through
+ case COLUMN_TYPES.JET_coltypLongText:
+ wrapper.column.cbMax = cbMax || fallback || 64 * 1024;
+ break;
+ case COLUMN_TYPES.JET_coltypGUID:
+ wrapper.column.cbMax = 16;
+ break;
+ case COLUMN_TYPES.JET_coltypBit:
+ wrapper.column.cbMax = 1;
+ break;
+ case COLUMN_TYPES.JET_coltypLongLong:
+ wrapper.column.cbMax = 8;
+ break;
+ default:
+ throw new Error("Unknown column type!");
+ }
+
+ wrapper.column.columnid = new ESE.JET_COLUMNID();
+ wrapper.column.grbit = 0;
+ wrapper.column.pvDefault = null;
+ wrapper.column.cbDefault = 0;
+ wrapper.column.cp = 0;
+
+ return wrapper;
+}
+
+// "forward declarations" of indexcreate and setinfo structs, which we don't use.
+ESE.JET_INDEXCREATE = new ctypes.StructType("JET_INDEXCREATE");
+ESE.JET_SETINFO = new ctypes.StructType("JET_SETINFO");
+
+ESE.JET_TABLECREATE_W = new ctypes.StructType("JET_TABLECREATE_W", [
+ { cbStruct: ctypes.unsigned_long },
+ { szTableName: ESE.JET_PCWSTR },
+ { szTemplateTableName: ESE.JET_PCWSTR },
+ { ulPages: ctypes.unsigned_long },
+ { ulDensity: ctypes.unsigned_long },
+ { rgcolumncreate: ESE.JET_COLUMNCREATE_W.ptr },
+ { cColumns: ctypes.unsigned_long },
+ { rgindexcreate: ESE.JET_INDEXCREATE.ptr },
+ { cIndexes: ctypes.unsigned_long },
+ { grbit: ESE.JET_GRBIT },
+ { tableid: ESE.JET_TABLEID },
+ { cCreated: ctypes.unsigned_long },
+]);
+
+function createTableCreationWrapper(tableName, columns) {
+ let wrapper = {};
+ let wchar_tArray = ctypes.ArrayType(ctypes.char16_t);
+ wrapper.name = new wchar_tArray(tableName.length + 1);
+ wrapper.name.value = String(tableName);
+ wrapper.table = new ESE.JET_TABLECREATE_W();
+ wrapper.table.cbStruct = ESE.JET_TABLECREATE_W.size;
+ wrapper.table.szTableName = wrapper.name;
+ wrapper.table.szTemplateTableName = null;
+ wrapper.table.ulPages = 1;
+ wrapper.table.ulDensity = 0;
+ let columnArrayType = ESE.JET_COLUMNCREATE_W.array(columns.length);
+ wrapper.columnAry = new columnArrayType();
+ wrapper.table.rgcolumncreate = wrapper.columnAry.addressOfElement(0);
+ wrapper.table.cColumns = columns.length;
+ wrapper.columns = [];
+ for (let i = 0; i < columns.length; i++) {
+ let column = columns[i];
+ let columnWrapper = createColumnCreationWrapper(column);
+ wrapper.columnAry.addressOfElement(i).contents = columnWrapper.column;
+ wrapper.columns.push(columnWrapper);
+ }
+ wrapper.table.rgindexcreate = null;
+ wrapper.table.cIndexes = 0;
+ return wrapper;
+}
+
+function convertValueForWriting(value, valueType) {
+ let buffer;
+ let valueOfValueType = ctypes.UInt64.lo(valueType);
+ switch (valueOfValueType) {
+ case COLUMN_TYPES.JET_coltypLongLong:
+ if (value instanceof Date) {
+ buffer = new KERNEL.FILETIME();
+ let sysTime = new KERNEL.SYSTEMTIME();
+ sysTime.wYear = value.getUTCFullYear();
+ sysTime.wMonth = value.getUTCMonth() + 1;
+ sysTime.wDay = value.getUTCDate();
+ sysTime.wHour = value.getUTCHours();
+ sysTime.wMinute = value.getUTCMinutes();
+ sysTime.wSecond = value.getUTCSeconds();
+ sysTime.wMilliseconds = value.getUTCMilliseconds();
+ let rv = KERNEL.SystemTimeToFileTime(
+ sysTime.address(),
+ buffer.address()
+ );
+ if (!rv) {
+ throw new Error("Failed to get FileTime.");
+ }
+ return [buffer, KERNEL.FILETIME.size];
+ }
+ throw new Error("Unrecognized value for longlong column");
+ case COLUMN_TYPES.JET_coltypLongText:
+ let wchar_tArray = ctypes.ArrayType(ctypes.char16_t);
+ buffer = new wchar_tArray(value.length + 1);
+ buffer.value = String(value);
+ return [buffer, buffer.length * 2];
+ case COLUMN_TYPES.JET_coltypBit:
+ buffer = new ctypes.uint8_t();
+ // Bizarre boolean values, but whatever:
+ buffer.value = value ? 255 : 0;
+ return [buffer, 1];
+ case COLUMN_TYPES.JET_coltypGUID:
+ let byteArray = ctypes.ArrayType(ctypes.uint8_t);
+ buffer = new byteArray(16);
+ let j = 0;
+ for (let i = 0; i < value.length; i++) {
+ if (!/[0-9a-f]/i.test(value[i])) {
+ continue;
+ }
+ let byteAsHex = value.substr(i, 2);
+ buffer[j++] = parseInt(byteAsHex, 16);
+ i++;
+ }
+ return [buffer, 16];
+ }
+
+ throw new Error("Unknown type " + valueType);
+}
+
+let initializedESE = false;
+
+let eseDBWritingHelpers = {
+ setupDB(dbFile, tables) {
+ if (!initializedESE) {
+ initializedESE = true;
+ loadLibraries();
+
+ KERNEL.SystemTimeToFileTime = gLibs.kernel.declare(
+ "SystemTimeToFileTime",
+ ctypes.winapi_abi,
+ ctypes.bool,
+ KERNEL.SYSTEMTIME.ptr,
+ KERNEL.FILETIME.ptr
+ );
+
+ declareESEFunction(
+ "CreateDatabaseW",
+ ESE.JET_SESID,
+ ESE.JET_PCWSTR,
+ ESE.JET_PCWSTR,
+ ESE.JET_DBID.ptr,
+ ESE.JET_GRBIT
+ );
+ declareESEFunction(
+ "CreateTableColumnIndexW",
+ ESE.JET_SESID,
+ ESE.JET_DBID,
+ ESE.JET_TABLECREATE_W.ptr
+ );
+ declareESEFunction("BeginTransaction", ESE.JET_SESID);
+ declareESEFunction("CommitTransaction", ESE.JET_SESID, ESE.JET_GRBIT);
+ declareESEFunction(
+ "PrepareUpdate",
+ ESE.JET_SESID,
+ ESE.JET_TABLEID,
+ ctypes.unsigned_long
+ );
+ declareESEFunction(
+ "Update",
+ ESE.JET_SESID,
+ ESE.JET_TABLEID,
+ ctypes.voidptr_t,
+ ctypes.unsigned_long,
+ ctypes.unsigned_long.ptr
+ );
+ declareESEFunction(
+ "SetColumn",
+ ESE.JET_SESID,
+ ESE.JET_TABLEID,
+ ESE.JET_COLUMNID,
+ ctypes.voidptr_t,
+ ctypes.unsigned_long,
+ ESE.JET_GRBIT,
+ ESE.JET_SETINFO.ptr
+ );
+ ESE.SetSystemParameterW(
+ null,
+ 0,
+ 64 /* JET_paramDatabasePageSize*/,
+ 8192,
+ null
+ );
+ }
+
+ let rootPath = dbFile.parent.path + "\\";
+ let logPath = rootPath + "LogFiles\\";
+
+ try {
+ this._instanceId = new ESE.JET_INSTANCE();
+ ESE.CreateInstanceW(
+ this._instanceId.address(),
+ "firefox-dbwriter-" + gESEInstanceCounter++
+ );
+ this._instanceCreated = true;
+
+ ESE.SetSystemParameterW(
+ this._instanceId.address(),
+ 0,
+ 0 /* JET_paramSystemPath*/,
+ 0,
+ rootPath
+ );
+ ESE.SetSystemParameterW(
+ this._instanceId.address(),
+ 0,
+ 1 /* JET_paramTempPath */,
+ 0,
+ rootPath
+ );
+ ESE.SetSystemParameterW(
+ this._instanceId.address(),
+ 0,
+ 2 /* JET_paramLogFilePath*/,
+ 0,
+ logPath
+ );
+ // Shouldn't try to call JetTerm if the following call fails.
+ this._instanceCreated = false;
+ ESE.Init(this._instanceId.address());
+ this._instanceCreated = true;
+ this._sessionId = new ESE.JET_SESID();
+ ESE.BeginSessionW(
+ this._instanceId,
+ this._sessionId.address(),
+ null,
+ null
+ );
+ this._sessionCreated = true;
+
+ this._dbId = new ESE.JET_DBID();
+ this._dbPath = rootPath + "spartan.edb";
+ ESE.CreateDatabaseW(
+ this._sessionId,
+ this._dbPath,
+ null,
+ this._dbId.address(),
+ 0
+ );
+ this._opened = this._attached = true;
+
+ for (let [tableName, data] of tables) {
+ let { rows, columns } = data;
+ let tableCreationWrapper = createTableCreationWrapper(
+ tableName,
+ columns
+ );
+ ESE.CreateTableColumnIndexW(
+ this._sessionId,
+ this._dbId,
+ tableCreationWrapper.table.address()
+ );
+ this._tableId = tableCreationWrapper.table.tableid;
+
+ let columnIdMap = new Map();
+ if (rows.length) {
+ // Iterate over the struct we passed into ESENT because they have the
+ // created column ids.
+ let columnCount = ctypes.UInt64.lo(
+ tableCreationWrapper.table.cColumns
+ );
+ let columnsPassed = tableCreationWrapper.table.rgcolumncreate;
+ for (let i = 0; i < columnCount; i++) {
+ let column = columnsPassed.contents;
+ columnIdMap.set(column.szColumnName.readString(), column);
+ columnsPassed = columnsPassed.increment();
+ }
+ ESE.ManualMove(
+ this._sessionId,
+ this._tableId,
+ -2147483648 /* JET_MoveFirst */,
+ 0
+ );
+ ESE.BeginTransaction(this._sessionId);
+ for (let row of rows) {
+ ESE.PrepareUpdate(
+ this._sessionId,
+ this._tableId,
+ 0 /* JET_prepInsert */
+ );
+ for (let columnName in row) {
+ let col = columnIdMap.get(columnName);
+ let colId = col.columnid;
+ let [val, valSize] = convertValueForWriting(
+ row[columnName],
+ col.coltyp
+ );
+ /* JET_bitSetOverwriteLV */
+ ESE.SetColumn(
+ this._sessionId,
+ this._tableId,
+ colId,
+ val.address(),
+ valSize,
+ 4,
+ null
+ );
+ }
+ let actualBookmarkSize = new ctypes.unsigned_long();
+ ESE.Update(
+ this._sessionId,
+ this._tableId,
+ null,
+ 0,
+ actualBookmarkSize.address()
+ );
+ }
+ ESE.CommitTransaction(
+ this._sessionId,
+ 0 /* JET_bitWaitLastLevel0Commit */
+ );
+ }
+ }
+ } finally {
+ try {
+ this._close();
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+ },
+
+ _close() {
+ if (this._tableId) {
+ ESE.FailSafeCloseTable(this._sessionId, this._tableId);
+ delete this._tableId;
+ }
+ if (this._opened) {
+ ESE.FailSafeCloseDatabase(this._sessionId, this._dbId, 0);
+ this._opened = false;
+ }
+ if (this._attached) {
+ ESE.FailSafeDetachDatabaseW(this._sessionId, this._dbPath);
+ this._attached = false;
+ }
+ if (this._sessionCreated) {
+ ESE.FailSafeEndSession(this._sessionId, 0);
+ this._sessionCreated = false;
+ }
+ if (this._instanceCreated) {
+ ESE.FailSafeTerm(this._instanceId);
+ this._instanceCreated = false;
+ }
+ },
+};
+
+add_task(async function () {
+ let tempFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ tempFile.append("fx-xpcshell-edge-db");
+ tempFile.createUnique(tempFile.DIRECTORY_TYPE, 0o600);
+
+ let db = tempFile.clone();
+ db.append("spartan.edb");
+
+ let logs = tempFile.clone();
+ logs.append("LogFiles");
+ logs.create(tempFile.DIRECTORY_TYPE, 0o600);
+
+ let creationDate = new Date(Date.now() - 5000);
+ const kEdgeMenuParent = "62d07e2b-5f0d-4e41-8426-5f5ec9717beb";
+ let bookmarkReferenceItems = [
+ {
+ URL: "http://www.mozilla.org/",
+ Title: "Mozilla",
+ DateUpdated: new Date(creationDate.valueOf() + 100),
+ ItemId: "1c00c10a-15f6-4618-92dd-22575102a4da",
+ ParentId: kEdgeMenuParent,
+ IsFolder: false,
+ IsDeleted: false,
+ },
+ {
+ Title: "Folder",
+ DateUpdated: new Date(creationDate.valueOf() + 200),
+ ItemId: "564b21f2-05d6-4f7d-8499-304d00ccc3aa",
+ ParentId: kEdgeMenuParent,
+ IsFolder: true,
+ IsDeleted: false,
+ },
+ {
+ Title: "Item in folder",
+ URL: "http://www.iteminfolder.org/",
+ DateUpdated: new Date(creationDate.valueOf() + 300),
+ ItemId: "c295ddaf-04a1-424a-866c-0ebde011e7c8",
+ ParentId: "564b21f2-05d6-4f7d-8499-304d00ccc3aa",
+ IsFolder: false,
+ IsDeleted: false,
+ },
+ {
+ Title: "Deleted folder",
+ DateUpdated: new Date(creationDate.valueOf() + 400),
+ ItemId: "a547573c-4d4d-4406-a736-5b5462d93bca",
+ ParentId: kEdgeMenuParent,
+ IsFolder: true,
+ IsDeleted: true,
+ },
+ {
+ Title: "Deleted item",
+ URL: "http://www.deleteditem.org/",
+ DateUpdated: new Date(creationDate.valueOf() + 500),
+ ItemId: "37a574bb-b44b-4bbc-a414-908615536435",
+ ParentId: kEdgeMenuParent,
+ IsFolder: false,
+ IsDeleted: true,
+ },
+ {
+ Title: "Item in deleted folder (should be in root)",
+ URL: "http://www.itemindeletedfolder.org/",
+ DateUpdated: new Date(creationDate.valueOf() + 600),
+ ItemId: "74dd1cc3-4c5d-471f-bccc-7bc7c72fa621",
+ ParentId: "a547573c-4d4d-4406-a736-5b5462d93bca",
+ IsFolder: false,
+ IsDeleted: false,
+ },
+ {
+ Title: "_Favorites_Bar_",
+ DateUpdated: new Date(creationDate.valueOf() + 700),
+ ItemId: "921dc8a0-6c83-40ef-8df1-9bd1c5c56aaf",
+ ParentId: kEdgeMenuParent,
+ IsFolder: true,
+ IsDeleted: false,
+ },
+ {
+ Title: "Item in favorites bar",
+ URL: "http://www.iteminfavoritesbar.org/",
+ DateUpdated: new Date(creationDate.valueOf() + 800),
+ ItemId: "9f2b1ff8-b651-46cf-8f41-16da8bcb6791",
+ ParentId: "921dc8a0-6c83-40ef-8df1-9bd1c5c56aaf",
+ IsFolder: false,
+ IsDeleted: false,
+ },
+ ];
+
+ let readingListReferenceItems = [
+ {
+ Title: "Some mozilla page",
+ URL: "http://www.mozilla.org/somepage/",
+ AddedDate: new Date(creationDate.valueOf() + 900),
+ ItemId: "c88426fd-52a7-419d-acbc-d2310e8afebe",
+ IsDeleted: false,
+ },
+ {
+ Title: "Some other page",
+ URL: "https://www.example.org/somepage/",
+ AddedDate: new Date(creationDate.valueOf() + 1000),
+ ItemId: "a35fc843-5d5a-4d1e-9be8-45214be24b5c",
+ IsDeleted: false,
+ },
+ ];
+
+ // The following entries are expected to be skipped as being too old to
+ // migrate.
+ let expiredTypedURLsReferenceItems = [
+ {
+ URL: "https://expired1.invalid/",
+ AccessDateTimeUTC: dateDaysAgo(500),
+ },
+ {
+ URL: "https://expired2.invalid/",
+ AccessDateTimeUTC: dateDaysAgo(300),
+ },
+ {
+ URL: "https://expired3.invalid/",
+ AccessDateTimeUTC: dateDaysAgo(190),
+ },
+ ];
+
+ // The following entries should be new enough to migrate.
+ let unexpiredTypedURLsReferenceItems = [
+ {
+ URL: "https://unexpired1.invalid/",
+ AccessDateTimeUTC: dateDaysAgo(179),
+ },
+ {
+ URL: "https://unexpired2.invalid/",
+ AccessDateTimeUTC: dateDaysAgo(50),
+ },
+ {
+ URL: "https://unexpired3.invalid/",
+ },
+ ];
+
+ let typedURLsReferenceItems = [
+ ...expiredTypedURLsReferenceItems,
+ ...unexpiredTypedURLsReferenceItems,
+ ];
+
+ Assert.ok(
+ MigrationUtils.HISTORY_MAX_AGE_IN_DAYS < 300,
+ "This test expects the current pref to be less than the youngest expired visit."
+ );
+ Assert.ok(
+ MigrationUtils.HISTORY_MAX_AGE_IN_DAYS > 160,
+ "This test expects the current pref to be greater than the oldest unexpired visit."
+ );
+
+ eseDBWritingHelpers.setupDB(
+ db,
+ new Map([
+ [
+ "Favorites",
+ {
+ columns: [
+ { type: COLUMN_TYPES.JET_coltypLongText, name: "URL", cbMax: 4096 },
+ {
+ type: COLUMN_TYPES.JET_coltypLongText,
+ name: "Title",
+ cbMax: 4096,
+ },
+ { type: COLUMN_TYPES.JET_coltypLongLong, name: "DateUpdated" },
+ { type: COLUMN_TYPES.JET_coltypGUID, name: "ItemId" },
+ { type: COLUMN_TYPES.JET_coltypBit, name: "IsDeleted" },
+ { type: COLUMN_TYPES.JET_coltypBit, name: "IsFolder" },
+ { type: COLUMN_TYPES.JET_coltypGUID, name: "ParentId" },
+ ],
+ rows: bookmarkReferenceItems,
+ },
+ ],
+ [
+ "ReadingList",
+ {
+ columns: [
+ { type: COLUMN_TYPES.JET_coltypLongText, name: "URL", cbMax: 4096 },
+ {
+ type: COLUMN_TYPES.JET_coltypLongText,
+ name: "Title",
+ cbMax: 4096,
+ },
+ { type: COLUMN_TYPES.JET_coltypLongLong, name: "AddedDate" },
+ { type: COLUMN_TYPES.JET_coltypGUID, name: "ItemId" },
+ { type: COLUMN_TYPES.JET_coltypBit, name: "IsDeleted" },
+ ],
+ rows: readingListReferenceItems,
+ },
+ ],
+ [
+ "TypedURLs",
+ {
+ columns: [
+ { type: COLUMN_TYPES.JET_coltypLongText, name: "URL", cbMax: 4096 },
+ {
+ type: COLUMN_TYPES.JET_coltypLongLong,
+ name: "AccessDateTimeUTC",
+ },
+ ],
+ rows: typedURLsReferenceItems,
+ },
+ ],
+ ])
+ );
+
+ // Manually create an EdgeProfileMigrator rather than going through
+ // MigrationUtils.getMigrator to avoid the user data availability check, since
+ // we're mocking out that stuff.
+ let migrator = new EdgeProfileMigrator();
+ let bookmarksMigrator = migrator.getBookmarksMigratorForTesting(db);
+ Assert.ok(bookmarksMigrator.exists, "Should recognize db we just created");
+
+ let seenBookmarks = [];
+ let listener = events => {
+ for (let event of events) {
+ let {
+ id,
+ itemType,
+ url,
+ title,
+ dateAdded,
+ guid,
+ index,
+ parentGuid,
+ parentId,
+ } = event;
+ if (title.startsWith("Deleted")) {
+ ok(false, "Should not see deleted items being bookmarked!");
+ }
+ seenBookmarks.push({
+ id,
+ parentId,
+ index,
+ itemType,
+ url,
+ title,
+ dateAdded,
+ guid,
+ parentGuid,
+ });
+ }
+ };
+ PlacesUtils.observers.addListener(["bookmark-added"], listener);
+
+ let migrateResult = await new Promise(resolve =>
+ bookmarksMigrator.migrate(resolve)
+ ).catch(ex => {
+ console.error(ex);
+ Assert.ok(false, "Got an exception trying to migrate data! " + ex);
+ return false;
+ });
+ PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+ Assert.ok(migrateResult, "Migration should succeed");
+ Assert.equal(
+ seenBookmarks.length,
+ 5,
+ "Should have seen 5 items being bookmarked."
+ );
+ Assert.equal(
+ seenBookmarks.length,
+ MigrationUtils._importQuantities.bookmarks,
+ "Telemetry should have items"
+ );
+
+ let menuParents = seenBookmarks.filter(
+ item => item.parentGuid == PlacesUtils.bookmarks.menuGuid
+ );
+ Assert.equal(
+ menuParents.length,
+ 3,
+ "Bookmarks are added to the menu without a folder"
+ );
+ let toolbarParents = seenBookmarks.filter(
+ item => item.parentGuid == PlacesUtils.bookmarks.toolbarGuid
+ );
+ Assert.equal(
+ toolbarParents.length,
+ 1,
+ "Should have a single item added to the toolbar"
+ );
+ let menuParentGuid = PlacesUtils.bookmarks.menuGuid;
+ let toolbarParentGuid = PlacesUtils.bookmarks.toolbarGuid;
+
+ let expectedTitlesInMenu = bookmarkReferenceItems
+ .filter(item => item.ParentId == kEdgeMenuParent)
+ .map(item => item.Title);
+ // Hacky, but seems like much the simplest way:
+ expectedTitlesInMenu.push("Item in deleted folder (should be in root)");
+ let expectedTitlesInToolbar = bookmarkReferenceItems
+ .filter(item => item.ParentId == "921dc8a0-6c83-40ef-8df1-9bd1c5c56aaf")
+ .map(item => item.Title);
+
+ for (let bookmark of seenBookmarks) {
+ let shouldBeInMenu = expectedTitlesInMenu.includes(bookmark.title);
+ let shouldBeInToolbar = expectedTitlesInToolbar.includes(bookmark.title);
+ if (bookmark.title == "Folder") {
+ Assert.equal(
+ bookmark.itemType,
+ PlacesUtils.bookmarks.TYPE_FOLDER,
+ "Bookmark " + bookmark.title + " should be a folder"
+ );
+ } else {
+ Assert.notEqual(
+ bookmark.itemType,
+ PlacesUtils.bookmarks.TYPE_FOLDER,
+ "Bookmark " + bookmark.title + " should not be a folder"
+ );
+ }
+
+ if (shouldBeInMenu) {
+ Assert.equal(
+ bookmark.parentGuid,
+ menuParentGuid,
+ "Item '" + bookmark.title + "' should be in menu"
+ );
+ } else if (shouldBeInToolbar) {
+ Assert.equal(
+ bookmark.parentGuid,
+ toolbarParentGuid,
+ "Item '" + bookmark.title + "' should be in toolbar"
+ );
+ } else if (
+ bookmark.guid == menuParentGuid ||
+ bookmark.guid == toolbarParentGuid
+ ) {
+ Assert.ok(
+ true,
+ "Expect toolbar and menu folders to not be in menu or toolbar"
+ );
+ } else {
+ // Bit hacky, but we do need to check this.
+ Assert.equal(
+ bookmark.title,
+ "Item in folder",
+ "Subfoldered item shouldn't be in menu or toolbar"
+ );
+ let parent = seenBookmarks.find(
+ maybeParent => maybeParent.guid == bookmark.parentGuid
+ );
+ Assert.equal(
+ parent && parent.title,
+ "Folder",
+ "Subfoldered item should be in subfolder labeled 'Folder'"
+ );
+ }
+
+ let dbItem = bookmarkReferenceItems.find(
+ someItem => bookmark.title == someItem.Title
+ );
+ if (!dbItem) {
+ Assert.ok(
+ [menuParentGuid, toolbarParentGuid].includes(bookmark.guid),
+ "This item should be one of the containers"
+ );
+ } else {
+ Assert.equal(dbItem.URL || "", bookmark.url, "URL is correct");
+ Assert.equal(
+ dbItem.DateUpdated.valueOf(),
+ new Date(bookmark.dateAdded).valueOf(),
+ "Date added is correct"
+ );
+ }
+ }
+
+ MigrationUtils._importQuantities.bookmarks = 0;
+ seenBookmarks = [];
+ listener = events => {
+ for (let event of events) {
+ let {
+ id,
+ itemType,
+ url,
+ title,
+ dateAdded,
+ guid,
+ index,
+ parentGuid,
+ parentId,
+ } = event;
+ seenBookmarks.push({
+ id,
+ parentId,
+ index,
+ itemType,
+ url,
+ title,
+ dateAdded,
+ guid,
+ parentGuid,
+ });
+ }
+ };
+ PlacesUtils.observers.addListener(["bookmark-added"], listener);
+
+ let readingListMigrator = migrator.getReadingListMigratorForTesting(db);
+ Assert.ok(readingListMigrator.exists, "Should recognize db we just created");
+ migrateResult = await new Promise(resolve =>
+ readingListMigrator.migrate(resolve)
+ ).catch(ex => {
+ console.error(ex);
+ Assert.ok(false, "Got an exception trying to migrate data! " + ex);
+ return false;
+ });
+ PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+ Assert.ok(migrateResult, "Migration should succeed");
+ Assert.equal(
+ seenBookmarks.length,
+ 3,
+ "Should have seen 3 items being bookmarked (2 items + 1 folder)."
+ );
+ Assert.equal(
+ seenBookmarks.length,
+ MigrationUtils._importQuantities.bookmarks,
+ "Telemetry should have items"
+ );
+ let readingListContainerLabel = await MigrationUtils.getLocalizedString(
+ "migration-imported-edge-reading-list"
+ );
+
+ for (let bookmark of seenBookmarks) {
+ if (readingListContainerLabel == bookmark.title) {
+ continue;
+ }
+ let referenceItem = readingListReferenceItems.find(
+ item => item.Title == bookmark.title
+ );
+ Assert.ok(referenceItem, "Should have imported what we expected");
+ Assert.equal(referenceItem.URL, bookmark.url, "Should have the right URL");
+ readingListReferenceItems.splice(
+ readingListReferenceItems.findIndex(item => item.Title == bookmark.title),
+ 1
+ );
+ }
+ Assert.ok(
+ !readingListReferenceItems.length,
+ "Should have seen all expected items."
+ );
+
+ let historyDBMigrator = migrator.getHistoryDBMigratorForTesting(db);
+ await new Promise(resolve => {
+ historyDBMigrator.migrate(resolve);
+ });
+ Assert.ok(true, "History DB migration done!");
+ for (let expiredEntry of expiredTypedURLsReferenceItems) {
+ let entry = await PlacesUtils.history.fetch(expiredEntry.URL, {
+ includeVisits: true,
+ });
+ Assert.equal(entry, null, "Should not have found an entry.");
+ }
+
+ for (let unexpiredEntry of unexpiredTypedURLsReferenceItems) {
+ let entry = await PlacesUtils.history.fetch(unexpiredEntry.URL, {
+ includeVisits: true,
+ });
+ Assert.equal(entry.url, unexpiredEntry.URL, "Should have the correct URL");
+ Assert.ok(!!entry.visits.length, "Should have some visits");
+ }
+});
diff --git a/browser/components/migration/tests/unit/test_Edge_registry_migration.js b/browser/components/migration/tests/unit/test_Edge_registry_migration.js
new file mode 100644
index 0000000000..2a400f7858
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Edge_registry_migration.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+const { EdgeProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/EdgeProfileMigrator.sys.mjs"
+);
+const { MSMigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/MSMigrationUtils.sys.mjs"
+);
+
+/**
+ * Tests that history visits loaded from the registry from Edge (EdgeHTML)
+ * that have a visit date older than maxAgeInDays days do not get imported.
+ */
+add_task(async function test_Edge_history_past_max_days() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ Assert.ok(
+ MigrationUtils.HISTORY_MAX_AGE_IN_DAYS < 300,
+ "This test expects the current pref to be less than the youngest expired visit."
+ );
+ Assert.ok(
+ MigrationUtils.HISTORY_MAX_AGE_IN_DAYS > 160,
+ "This test expects the current pref to be greater than the oldest unexpired visit."
+ );
+
+ const EXPIRED_VISITS = [
+ ["https://test1.invalid/", dateDaysAgo(500).getTime() * 1000],
+ ["https://test2.invalid/", dateDaysAgo(450).getTime() * 1000],
+ ["https://test3.invalid/", dateDaysAgo(300).getTime() * 1000],
+ ];
+
+ const UNEXPIRED_VISITS = [
+ ["https://test4.invalid/"],
+ ["https://test5.invalid/", dateDaysAgo(160).getTime() * 1000],
+ ["https://test6.invalid/", dateDaysAgo(50).getTime() * 1000],
+ ["https://test7.invalid/", dateDaysAgo(0).getTime() * 1000],
+ ];
+
+ const ALL_VISITS = [...EXPIRED_VISITS, ...UNEXPIRED_VISITS];
+
+ // Fake out the getResources method of the migrator so that we return
+ // a single fake MigratorResource per availableResourceType.
+ sandbox.stub(MSMigrationUtils, "getTypedURLs").callsFake(() => {
+ return new Map(ALL_VISITS);
+ });
+
+ // Manually create an EdgeProfileMigrator rather than going through
+ // MigrationUtils.getMigrator to avoid the user data availability check, since
+ // we're mocking out that stuff.
+ let migrator = new EdgeProfileMigrator();
+ let registryTypedHistoryMigrator =
+ migrator.getHistoryRegistryMigratorForTesting();
+ await new Promise(resolve => {
+ registryTypedHistoryMigrator.migrate(resolve);
+ });
+ Assert.ok(true, "History from registry migration done!");
+
+ for (let expiredEntry of EXPIRED_VISITS) {
+ let entry = await PlacesUtils.history.fetch(expiredEntry[0], {
+ includeVisits: true,
+ });
+ Assert.equal(entry, null, "Should not have found an entry.");
+ }
+
+ for (let unexpiredEntry of UNEXPIRED_VISITS) {
+ let entry = await PlacesUtils.history.fetch(unexpiredEntry[0], {
+ includeVisits: true,
+ });
+ Assert.equal(entry.url, unexpiredEntry[0], "Should have the correct URL");
+ Assert.ok(!!entry.visits.length, "Should have some visits");
+ }
+});
diff --git a/browser/components/migration/tests/unit/test_IE_bookmarks.js b/browser/components/migration/tests/unit/test_IE_bookmarks.js
new file mode 100644
index 0000000000..9816bb16e3
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_IE_bookmarks.js
@@ -0,0 +1,30 @@
+"use strict";
+
+add_task(async function () {
+ let migrator = await MigrationUtils.getMigrator("ie");
+ // Sanity check for the source.
+ Assert.ok(await migrator.isSourceAvailable(), "Check migrator source");
+
+ // Since this test doesn't mock out the favorites, execution is dependent
+ // on the actual favorites stored on the local machine's IE favorites database.
+ // As such, we can't assert that bookmarks were migrated to both the bookmarks
+ // menu and the bookmarks toolbar.
+ let itemCount = 0;
+ let listener = events => {
+ for (let event of events) {
+ if (event.itemType == PlacesUtils.bookmarks.TYPE_BOOKMARK) {
+ info("bookmark added: " + event.parentGuid);
+ itemCount++;
+ }
+ }
+ };
+ PlacesUtils.observers.addListener(["bookmark-added"], listener);
+
+ await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS);
+ PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+ Assert.equal(
+ MigrationUtils._importQuantities.bookmarks,
+ itemCount,
+ "Ensure telemetry matches actual number of imported items."
+ );
+});
diff --git a/browser/components/migration/tests/unit/test_IE_history.js b/browser/components/migration/tests/unit/test_IE_history.js
new file mode 100644
index 0000000000..f9a1e719a2
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_IE_history.js
@@ -0,0 +1,187 @@
+"use strict";
+
+const { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+// These match what we add to IE via InsertIEHistory.exe.
+const TEST_ENTRIES = [
+ {
+ url: "http://www.mozilla.org/1",
+ title: "Mozilla HTTP Test",
+ },
+ {
+ url: "https://www.mozilla.org/2",
+ // Test character encoding with a fox emoji:
+ title: "Mozilla HTTPS Test 🦊",
+ },
+];
+
+function insertIEHistory() {
+ let file = do_get_file("InsertIEHistory.exe", false);
+ let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+ process.init(file);
+
+ let args = [];
+ process.run(true, args, args.length);
+
+ Assert.ok(!process.isRunning, "Should be done running");
+ Assert.equal(process.exitValue, 0, "Check exit code");
+}
+
+add_task(async function setup() {
+ await PlacesUtils.history.clear();
+
+ insertIEHistory();
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function test_IE_history() {
+ let migrator = await MigrationUtils.getMigrator("ie");
+ Assert.ok(await migrator.isSourceAvailable(), "Source is available");
+
+ await promiseMigration(migrator, MigrationUtils.resourceTypes.HISTORY);
+
+ for (let { url, title } of TEST_ENTRIES) {
+ let entry = await PlacesUtils.history.fetch(url, { includeVisits: true });
+ Assert.equal(entry.url, url, "Should have the correct URL");
+ Assert.equal(entry.title, title, "Should have the correct title");
+ Assert.ok(!!entry.visits.length, "Should have some visits");
+ }
+
+ await PlacesUtils.history.clear();
+});
+
+/**
+ * Tests that history visits from IE that have a visit date older than
+ * maxAgeInDays days do not get imported.
+ */
+add_task(async function test_IE_history_past_max_days() {
+ // The InsertIEHistory program inserts two history visits using the MS COM
+ // IUrlHistoryStg interface. That interface does not allow us to dictate
+ // the visit times of those history visits. Thankfully, we can temporarily
+ // mock out the @mozilla.org/profile/migrator/iehistoryenumerator;1 to return
+ // some entries that we expect to expire.
+
+ /**
+ * An implmentation of nsISimpleEnumerator that wraps a JavaScript Array.
+ */
+ class nsSimpleEnumerator {
+ #items;
+ #nextIndex;
+
+ constructor(items) {
+ this.#items = items;
+ this.#nextIndex = 0;
+ }
+
+ hasMoreElements() {
+ return this.#nextIndex < this.#items.length;
+ }
+
+ getNext() {
+ if (!this.hasMoreElements()) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ return this.#items[this.#nextIndex++];
+ }
+
+ [Symbol.iterator]() {
+ return this.#items.values();
+ }
+
+ QueryInterface = ChromeUtils.generateQI(["nsISimpleEnumerator"]);
+ }
+
+ Assert.ok(
+ MigrationUtils.HISTORY_MAX_AGE_IN_DAYS < 300,
+ "This test expects the current pref to be less than the youngest expired visit."
+ );
+ Assert.ok(
+ MigrationUtils.HISTORY_MAX_AGE_IN_DAYS > 160,
+ "This test expects the current pref to be greater than the oldest unexpired visit."
+ );
+
+ const EXPIRED_VISITS = [
+ new Map([
+ ["uri", Services.io.newURI("https://test1.invalid")],
+ ["title", "Test history visit 1"],
+ ["time", PRTimeDaysAgo(500)],
+ ]),
+ new Map([
+ ["uri", Services.io.newURI("https://test2.invalid")],
+ ["title", "Test history visit 2"],
+ ["time", PRTimeDaysAgo(450)],
+ ]),
+ new Map([
+ ["uri", Services.io.newURI("https://test3.invalid")],
+ ["title", "Test history visit 3"],
+ ["time", PRTimeDaysAgo(300)],
+ ]),
+ ];
+
+ const UNEXPIRED_VISITS = [
+ new Map([
+ ["uri", Services.io.newURI("https://test4.invalid")],
+ ["title", "Test history visit 4"],
+ ]),
+ new Map([
+ ["uri", Services.io.newURI("https://test5.invalid")],
+ ["title", "Test history visit 5"],
+ ["time", PRTimeDaysAgo(160)],
+ ]),
+ new Map([
+ ["uri", Services.io.newURI("https://test6.invalid")],
+ ["title", "Test history visit 6"],
+ ["time", PRTimeDaysAgo(50)],
+ ]),
+ new Map([
+ ["uri", Services.io.newURI("https://test7.invalid")],
+ ["title", "Test history visit 7"],
+ ["time", PRTimeDaysAgo(0)],
+ ]),
+ ];
+
+ let fakeIEHistoryEnumerator = MockRegistrar.register(
+ "@mozilla.org/profile/migrator/iehistoryenumerator;1",
+ new nsSimpleEnumerator([...EXPIRED_VISITS, ...UNEXPIRED_VISITS])
+ );
+ registerCleanupFunction(() => {
+ MockRegistrar.unregister(fakeIEHistoryEnumerator);
+ });
+
+ let migrator = await MigrationUtils.getMigrator("ie");
+ Assert.ok(await migrator.isSourceAvailable(), "Source is available");
+
+ await promiseMigration(migrator, MigrationUtils.resourceTypes.HISTORY);
+
+ for (let visit of EXPIRED_VISITS) {
+ let entry = await PlacesUtils.history.fetch(visit.get("uri").spec, {
+ includeVisits: true,
+ });
+ Assert.equal(entry, null, "Should not have found an entry.");
+ }
+
+ for (let visit of UNEXPIRED_VISITS) {
+ let entry = await PlacesUtils.history.fetch(visit.get("uri"), {
+ includeVisits: true,
+ });
+ Assert.equal(
+ entry.url,
+ visit.get("uri").spec,
+ "Should have the correct URL"
+ );
+ Assert.equal(
+ entry.title,
+ visit.get("title"),
+ "Should have the correct title"
+ );
+ Assert.ok(!!entry.visits.length, "Should have some visits");
+ }
+
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/migration/tests/unit/test_MigrationUtils_timedRetry.js b/browser/components/migration/tests/unit/test_MigrationUtils_timedRetry.js
new file mode 100644
index 0000000000..83748d870d
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_MigrationUtils_timedRetry.js
@@ -0,0 +1,29 @@
+"use strict";
+
+let tmpFile = FileUtils.getDir("TmpD", []);
+let dbConn;
+
+add_task(async function setup() {
+ tmpFile.append("TestDB");
+ dbConn = await Sqlite.openConnection({ path: tmpFile.path });
+
+ registerCleanupFunction(async () => {
+ await dbConn.close();
+ await IOUtils.remove(tmpFile.path);
+ });
+});
+
+add_task(async function testgetRowsFromDBWithoutLocksRetries() {
+ let deferred = Promise.withResolvers();
+ let promise = MigrationUtils.getRowsFromDBWithoutLocks(
+ tmpFile.path,
+ "Temp DB",
+ "SELECT * FROM moz_temp_table",
+ deferred.promise
+ );
+ await new Promise(resolve => do_timeout(50, resolve));
+ dbConn
+ .execute("CREATE TABLE moz_temp_table (id INTEGER PRIMARY KEY)")
+ .then(deferred.resolve);
+ await promise;
+});
diff --git a/browser/components/migration/tests/unit/test_PasswordFileMigrator.js b/browser/components/migration/tests/unit/test_PasswordFileMigrator.js
new file mode 100644
index 0000000000..e22f207c5d
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_PasswordFileMigrator.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PasswordFileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/FileMigrators.sys.mjs"
+);
+const { LoginCSVImport } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginCSVImport.sys.mjs"
+);
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+const { MigrationWizardConstants } = ChromeUtils.importESModule(
+ "chrome://browser/content/migration/migration-wizard-constants.mjs"
+);
+
+add_setup(async function () {
+ Services.prefs.setBoolPref("signon.management.page.fileImport.enabled", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("signon.management.page.fileImport.enabled");
+ });
+});
+
+/**
+ * Tests that the PasswordFileMigrator properly subclasses FileMigratorBase
+ * and delegates to the LoginCSVImport module.
+ */
+add_task(async function test_PasswordFileMigrator() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let migrator = new PasswordFileMigrator();
+ Assert.ok(
+ migrator.constructor.key,
+ "PasswordFileMigrator implements static getter 'key'"
+ );
+ Assert.ok(
+ migrator.constructor.displayNameL10nID,
+ "PasswordFileMigrator implements static getter 'displayNameL10nID'"
+ );
+ Assert.ok(
+ await migrator.getFilePickerConfig(),
+ "PasswordFileMigrator returns something for getFilePickerConfig()"
+ );
+ Assert.ok(
+ migrator.displayedResourceTypes,
+ "PasswordFileMigrator returns something for displayedResourceTypes"
+ );
+ Assert.ok(migrator.enabled, "PasswordFileMigrator is enabled.");
+
+ const IMPORT_SUMMARY = [
+ {
+ result: "added",
+ },
+ {
+ result: "added",
+ },
+ {
+ result: "modified",
+ },
+ ];
+ const EXPECTED_SUCCESS_STATE = {
+ [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_NEW]:
+ "2 added",
+ [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_UPDATED]:
+ "1 updated",
+ };
+ const FAKE_PATH = "some/fake/path.csv";
+
+ let importFromCSVStub = sandbox
+ .stub(LoginCSVImport, "importFromCSV")
+ .callsFake(somePath => {
+ Assert.equal(somePath, FAKE_PATH, "Got expected path");
+ return Promise.resolve(IMPORT_SUMMARY);
+ });
+ let result = await migrator.migrate(FAKE_PATH);
+
+ Assert.ok(importFromCSVStub.called, "The stub should have been called.");
+ Assert.deepEqual(
+ result,
+ EXPECTED_SUCCESS_STATE,
+ "Got back the expected success state."
+ );
+
+ sandbox.restore();
+});
+
+/**
+ * Tests that the PasswordFileMigrator will throw an exception with a
+ * consistent error message if the LoginCSVImport function rejects.
+ */
+add_task(async function test_PasswordFileMigrator_exception() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let migrator = new PasswordFileMigrator();
+
+ const FAKE_PATH = "some/fake/path.csv";
+
+ sandbox.stub(LoginCSVImport, "importFromCSV").callsFake(() => {
+ return Promise.reject("Some error");
+ });
+
+ await Assert.rejects(
+ migrator.migrate(FAKE_PATH),
+ /The file doesn’t include any valid password data/
+ );
+
+ sandbox.restore();
+});
diff --git a/browser/components/migration/tests/unit/test_Safari_bookmarks.js b/browser/components/migration/tests/unit/test_Safari_bookmarks.js
new file mode 100644
index 0000000000..85be9f0049
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Safari_bookmarks.js
@@ -0,0 +1,85 @@
+"use strict";
+
+const { CustomizableUI } = ChromeUtils.importESModule(
+ "resource:///modules/CustomizableUI.sys.mjs"
+);
+
+add_task(async function () {
+ registerFakePath("ULibDir", do_get_file("Library/"));
+ const faviconPath = do_get_file(
+ "Library/Safari/Favicon Cache/favicons.db"
+ ).path;
+
+ let migrator = await MigrationUtils.getMigrator("safari");
+ // Sanity check for the source.
+ Assert.ok(await migrator.isSourceAvailable());
+
+ // Wait for the imported bookmarks. We don't check that "From Safari"
+ // folders are created on the toolbar since the profile
+ // we're importing to has less than 3 bookmarks in the destination
+ // so a "From Safari" folder isn't created.
+ let expectedParentGuids = [PlacesUtils.bookmarks.toolbarGuid];
+ let itemCount = 0;
+
+ let gotFolder = false;
+ let listener = events => {
+ for (let event of events) {
+ itemCount++;
+ if (
+ event.itemType == PlacesUtils.bookmarks.TYPE_FOLDER &&
+ event.title == "Food and Travel"
+ ) {
+ gotFolder = true;
+ }
+ if (expectedParentGuids.length) {
+ let index = expectedParentGuids.indexOf(event.parentGuid);
+ Assert.ok(index != -1, "Found expected parent");
+ expectedParentGuids.splice(index, 1);
+ }
+ }
+ };
+ PlacesUtils.observers.addListener(["bookmark-added"], listener);
+ let observerNotified = false;
+ Services.obs.addObserver((aSubject, aTopic, aData) => {
+ let [toolbar, visibility] = JSON.parse(aData);
+ Assert.equal(
+ toolbar,
+ CustomizableUI.AREA_BOOKMARKS,
+ "Notification should be received for bookmarks toolbar"
+ );
+ Assert.equal(
+ visibility,
+ "true",
+ "Notification should say to reveal the bookmarks toolbar"
+ );
+ observerNotified = true;
+ }, "browser-set-toolbar-visibility");
+
+ await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS);
+ PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+
+ // Check the bookmarks have been imported to all the expected parents.
+ Assert.ok(!expectedParentGuids.length, "No more expected parents");
+ Assert.ok(gotFolder, "Should have seen the folder get imported");
+ Assert.equal(itemCount, 14, "Should import all 14 items.");
+ // Check that the telemetry matches:
+ Assert.equal(
+ MigrationUtils._importQuantities.bookmarks,
+ itemCount,
+ "Telemetry reporting correct."
+ );
+
+ // Check that favicons migrated
+ let faviconURIs = await MigrationUtils.getRowsFromDBWithoutLocks(
+ faviconPath,
+ "Safari Bookmark Favicons",
+ `SELECT I.uuid, I.url AS favicon_url, P.url
+ FROM icon_info I
+ INNER JOIN page_url P ON I.uuid = P.uuid;`
+ );
+ let pageUrls = Array.from(faviconURIs, row =>
+ Services.io.newURI(row.getResultByName("url"))
+ );
+ await assertFavicons(pageUrls);
+ Assert.ok(observerNotified, "The observer should be notified upon migration");
+});
diff --git a/browser/components/migration/tests/unit/test_Safari_history.js b/browser/components/migration/tests/unit/test_Safari_history.js
new file mode 100644
index 0000000000..c5b1210073
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Safari_history.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const HISTORY_TEMPLATE_FILE_PATH = "Library/Safari/HistoryTemplate.db";
+const HISTORY_FILE_PATH = "Library/Safari/History.db";
+
+// We want this to be some recent time, so we'll always add some time to our
+// dates to keep them ~ five days ago.
+const MS_FROM_REFERENCE_TIME =
+ new Date() - new Date("May 31, 2023 00:00:00 UTC");
+
+const TEST_URLS = [
+ {
+ url: "http://example.com/",
+ title: "Example Domain",
+ time: 706743588.04751,
+ jsTime: 1685050788047 + MS_FROM_REFERENCE_TIME,
+ visits: 1,
+ },
+ {
+ url: "http://mozilla.org/",
+ title: "",
+ time: 706743581.133386,
+ jsTime: 1685050781133 + MS_FROM_REFERENCE_TIME,
+ visits: 1,
+ },
+ {
+ url: "https://www.mozilla.org/en-CA/",
+ title: "Internet for people, not profit - Mozilla",
+ time: 706743581.133679,
+ jsTime: 1685050781133 + MS_FROM_REFERENCE_TIME,
+ visits: 1,
+ },
+];
+
+async function setupHistoryFile() {
+ removeHistoryFile();
+ let file = do_get_file(HISTORY_TEMPLATE_FILE_PATH);
+ file.copyTo(file.parent, "History.db");
+ await updateVisitTimes();
+}
+
+function removeHistoryFile() {
+ let file = do_get_file(HISTORY_FILE_PATH, true);
+ try {
+ file.remove(false);
+ } catch (ex) {
+ // It is ok if this doesn't exist.
+ if (ex.result != Cr.NS_ERROR_FILE_NOT_FOUND) {
+ throw ex;
+ }
+ }
+}
+
+async function updateVisitTimes() {
+ let cocoaDifference = MS_FROM_REFERENCE_TIME / 1000;
+ let historyFile = do_get_file(HISTORY_FILE_PATH);
+ let dbConn = await Sqlite.openConnection({ path: historyFile.path });
+ await dbConn.execute(
+ "UPDATE history_visits SET visit_time = visit_time + :difference;",
+ { difference: cocoaDifference }
+ );
+ await dbConn.close();
+}
+
+add_setup(async function setup() {
+ registerFakePath("ULibDir", do_get_file("Library/"));
+ await setupHistoryFile();
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ removeHistoryFile();
+ });
+});
+
+add_task(async function testHistoryImport() {
+ await PlacesUtils.history.clear();
+
+ let migrator = await MigrationUtils.getMigrator("safari");
+ await promiseMigration(migrator, MigrationUtils.resourceTypes.HISTORY);
+
+ for (let urlInfo of TEST_URLS) {
+ let entry = await PlacesUtils.history.fetch(urlInfo.url, {
+ includeVisits: true,
+ });
+
+ Assert.equal(entry.url, urlInfo.url, "Should have the correct URL");
+ Assert.equal(entry.title, urlInfo.title, "Should have the correct title");
+ Assert.equal(
+ entry.visits.length,
+ urlInfo.visits,
+ "Should have the correct number of visits"
+ );
+ Assert.equal(
+ entry.visits[0].date.getTime(),
+ urlInfo.jsTime,
+ "Should have the correct date"
+ );
+ }
+});
diff --git a/browser/components/migration/tests/unit/test_Safari_history_strange_entries.js b/browser/components/migration/tests/unit/test_Safari_history_strange_entries.js
new file mode 100644
index 0000000000..2578353e35
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Safari_history_strange_entries.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PlacesQuery } = ChromeUtils.importESModule(
+ "resource://gre/modules/PlacesQuery.sys.mjs"
+);
+
+const HISTORY_FILE_PATH = "Library/Safari/History.db";
+const HISTORY_STRANGE_ENTRIES_FILE_PATH =
+ "Library/Safari/HistoryStrangeEntries.db";
+
+// By default, our migrators will cut off migrating any history older than
+// 180 days. In order to make sure this test continues to run correctly
+// in the future, we copy the reference database to History.db, and then
+// use Sqlite.sys.mjs to connect to it and manually update all of the visit
+// times to be "now", so that they all fall within the 180 day window. The
+// Nov 10th date below is right around when the reference database visit
+// entries were created.
+//
+// This update occurs in `updateVisitTimes`.
+const MS_SINCE_SNAPSHOT_TIME =
+ new Date() - new Date("Nov 10, 2022 00:00:00 UTC");
+
+async function setupHistoryFile() {
+ removeHistoryFile();
+ let file = do_get_file(HISTORY_STRANGE_ENTRIES_FILE_PATH);
+ file.copyTo(file.parent, "History.db");
+ await updateVisitTimes();
+}
+
+function removeHistoryFile() {
+ let file = do_get_file(HISTORY_FILE_PATH, true);
+ try {
+ file.remove(false);
+ } catch (ex) {
+ // It is ok if this doesn't exist.
+ if (ex.result != Cr.NS_ERROR_FILE_NOT_FOUND) {
+ throw ex;
+ }
+ }
+}
+
+add_setup(async function setup() {
+ registerFakePath("ULibDir", do_get_file("Library/"));
+ await setupHistoryFile();
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ removeHistoryFile();
+ });
+});
+
+async function updateVisitTimes() {
+ let cocoaSnapshotDelta = MS_SINCE_SNAPSHOT_TIME / 1000;
+ let historyFile = do_get_file(HISTORY_FILE_PATH);
+ let dbConn = await Sqlite.openConnection({ path: historyFile.path });
+
+ await dbConn.execute(
+ "UPDATE history_visits SET visit_time = visit_time + :cocoaSnapshotDelta;",
+ {
+ cocoaSnapshotDelta,
+ }
+ );
+
+ await dbConn.close();
+}
+
+/**
+ * Tests that we can import successfully from Safari when Safari's history
+ * database contains malformed URLs.
+ */
+add_task(async function testHistoryImportStrangeEntries() {
+ await PlacesUtils.history.clear();
+
+ let placesQuery = new PlacesQuery();
+ let emptyHistory = await placesQuery.getHistory();
+ Assert.equal(emptyHistory.size, 0, "Empty history should indeed be empty.");
+
+ const EXPECTED_MIGRATED_SITES = 10;
+ const EXPECTED_MIGRATED_VISTS = 23;
+
+ let historyFile = do_get_file(HISTORY_FILE_PATH);
+ let dbConn = await Sqlite.openConnection({ path: historyFile.path });
+ let [rowCountResult] = await dbConn.execute(
+ "SELECT COUNT(*) FROM history_visits"
+ );
+ Assert.greater(
+ rowCountResult.getResultByName("COUNT(*)"),
+ EXPECTED_MIGRATED_VISTS,
+ "There are more total rows than valid rows"
+ );
+ await dbConn.close();
+
+ let migrator = await MigrationUtils.getMigrator("safari");
+ await promiseMigration(migrator, MigrationUtils.resourceTypes.HISTORY);
+ let migratedHistory = await placesQuery.getHistory({ sortBy: "site" });
+ let siteCount = migratedHistory.size;
+ let visitCount = 0;
+ for (let [, visits] of migratedHistory) {
+ visitCount += visits.length;
+ }
+ Assert.equal(
+ siteCount,
+ EXPECTED_MIGRATED_SITES,
+ "Should have migrated all valid history sites"
+ );
+ Assert.equal(
+ visitCount,
+ EXPECTED_MIGRATED_VISTS,
+ "Should have migrated all valid history visits"
+ );
+
+ placesQuery.close();
+});
diff --git a/browser/components/migration/tests/unit/test_Safari_permissions.js b/browser/components/migration/tests/unit/test_Safari_permissions.js
new file mode 100644
index 0000000000..eaa6c7788e
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Safari_permissions.js
@@ -0,0 +1,145 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that if the migrator does not have the permission to
+ * read from the Safari data directory, that it can request
+ * permission to read from it, if the system allows.
+ */
+
+const { SafariProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/SafariProfileMigrator.sys.mjs"
+);
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+const { MockFilePicker } = ChromeUtils.importESModule(
+ "resource://testing-common/MockFilePicker.sys.mjs"
+);
+
+let gDataDir;
+
+add_setup(async () => {
+ let tempDir = do_get_tempdir();
+ gDataDir = PathUtils.join(tempDir.path, "Safari");
+ await IOUtils.makeDirectory(gDataDir);
+
+ registerFakePath("ULibDir", tempDir);
+
+ MockFilePicker.init(globalThis);
+ registerCleanupFunction(() => {
+ MockFilePicker.cleanup();
+ });
+});
+
+/**
+ * Tests that canGetPermissions will return false if the platform does
+ * not allow for folder selection in the native file picker, and returns
+ * the data path otherwise.
+ */
+add_task(async function test_canGetPermissions() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let migrator = new SafariProfileMigrator();
+ // Not being able to get a folder picker is not a problem on macOS, but
+ // we'll test that case anyways.
+ let canGetPermissionsStub = sandbox
+ .stub(MigrationUtils, "canGetPermissionsOnPlatform")
+ .resolves(false);
+
+ Assert.ok(
+ !(await migrator.canGetPermissions()),
+ "Should not be able to get permissions."
+ );
+
+ canGetPermissionsStub.resolves(true);
+
+ Assert.equal(
+ await migrator.canGetPermissions(),
+ gDataDir,
+ "Should be able to get the permissions path."
+ );
+
+ sandbox.restore();
+});
+
+/**
+ * Tests that getPermissions will show the native file picker in a
+ * loop until either the user cancels or selects a folder that grants
+ * read permissions to the data directory.
+ */
+add_task(async function test_getPermissions() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let migrator = new SafariProfileMigrator();
+ sandbox.stub(MigrationUtils, "canGetPermissionsOnPlatform").resolves(true);
+ let hasPermissionsStub = sandbox
+ .stub(migrator, "hasPermissions")
+ .resolves(false);
+
+ Assert.equal(
+ await migrator.canGetPermissions(),
+ gDataDir,
+ "Should be able to get the permissions path."
+ );
+
+ let filePickerSeenCount = 0;
+
+ let filePickerShownPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = () => {
+ Assert.ok(true, "Filepicker shown.");
+ MockFilePicker.useDirectory([gDataDir]);
+ filePickerSeenCount++;
+ if (filePickerSeenCount > 3) {
+ Assert.ok(true, "File picker looped 3 times.");
+ hasPermissionsStub.resolves(true);
+ resolve();
+ }
+ };
+ });
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+
+ // This is a little awkward, but we need to ensure that the
+ // filePickerShownPromise resolves first before we await
+ // the getPermissionsPromise in order to get the correct
+ // filePickerSeenCount.
+ let getPermissionsPromise = migrator.getPermissions();
+ await filePickerShownPromise;
+ Assert.ok(
+ await getPermissionsPromise,
+ "Should report that we got permissions."
+ );
+
+ // Make sure that the user can also hit "cancel" and that we
+ // file picker loop.
+
+ hasPermissionsStub.resolves(false);
+
+ filePickerSeenCount = 0;
+ filePickerShownPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = () => {
+ Assert.ok(true, "Filepicker shown.");
+ filePickerSeenCount++;
+ Assert.equal(filePickerSeenCount, 1, "Saw the picker once.");
+ resolve();
+ };
+ });
+ MockFilePicker.returnValue = MockFilePicker.returnCancel;
+ Assert.ok(
+ !(await migrator.getPermissions()),
+ "Should report that we didn't get permissions."
+ );
+ await filePickerShownPromise;
+
+ sandbox.restore();
+});
diff --git a/browser/components/migration/tests/unit/test_fx_telemetry.js b/browser/components/migration/tests/unit/test_fx_telemetry.js
new file mode 100644
index 0000000000..68e34beab3
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_fx_telemetry.js
@@ -0,0 +1,436 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+const { FirefoxProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/FirefoxProfileMigrator.sys.mjs"
+);
+const { InternalTestingProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/InternalTestingProfileMigrator.sys.mjs"
+);
+const { LoginCSVImport } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginCSVImport.sys.mjs"
+);
+const { PasswordFileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/FileMigrators.sys.mjs"
+);
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+// These preferences are set to true anytime MigratorBase.migrate
+// successfully completes a migration of their type.
+const BOOKMARKS_PREF = "browser.migrate.interactions.bookmarks";
+const CSV_PASSWORDS_PREF = "browser.migrate.interactions.csvpasswords";
+const HISTORY_PREF = "browser.migrate.interactions.history";
+const PASSWORDS_PREF = "browser.migrate.interactions.passwords";
+
+function readFile(file) {
+ let stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ stream.init(file, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF);
+
+ let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ sis.init(stream);
+ let contents = sis.read(file.fileSize);
+ sis.close();
+ return contents;
+}
+
+function checkDirectoryContains(dir, files) {
+ print("checking " + dir.path + " - should contain " + Object.keys(files));
+ let seen = new Set();
+ for (let file of dir.directoryEntries) {
+ print("found file: " + file.path);
+ Assert.ok(file.leafName in files, file.leafName + " exists, but shouldn't");
+
+ let expectedContents = files[file.leafName];
+ if (typeof expectedContents != "string") {
+ // it's a subdir - recurse!
+ Assert.ok(file.isDirectory(), "should be a subdir");
+ let newDir = dir.clone();
+ newDir.append(file.leafName);
+ checkDirectoryContains(newDir, expectedContents);
+ } else {
+ Assert.ok(!file.isDirectory(), "should be a regular file");
+ let contents = readFile(file);
+ Assert.equal(contents, expectedContents);
+ }
+ seen.add(file.leafName);
+ }
+ let missing = [];
+ for (let x in files) {
+ if (!seen.has(x)) {
+ missing.push(x);
+ }
+ }
+ Assert.deepEqual(missing, [], "no missing files in " + dir.path);
+}
+
+function getTestDirs() {
+ // we make a directory structure in a temp dir which mirrors what we are
+ // testing.
+ let tempDir = do_get_tempdir();
+ let srcDir = tempDir.clone();
+ srcDir.append("test_source_dir");
+ srcDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ let targetDir = tempDir.clone();
+ targetDir.append("test_target_dir");
+ targetDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ // no need to cleanup these dirs - the xpcshell harness will do it for us.
+ return [srcDir, targetDir];
+}
+
+function writeToFile(dir, leafName, contents) {
+ let file = dir.clone();
+ file.append(leafName);
+
+ let outputStream = FileUtils.openFileOutputStream(file);
+ outputStream.write(contents, contents.length);
+ outputStream.close();
+}
+
+function createSubDir(dir, subDirName) {
+ let subDir = dir.clone();
+ subDir.append(subDirName);
+ subDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ return subDir;
+}
+
+async function promiseMigrator(name, srcDir, targetDir) {
+ // As the FirefoxProfileMigrator is a startup-only migrator, we import its
+ // module and instantiate it directly rather than going through MigrationUtils,
+ // to bypass that availability check.
+ let migrator = new FirefoxProfileMigrator();
+ let migrators = migrator._getResourcesInternal(srcDir, targetDir);
+ for (let m of migrators) {
+ if (m.name == name) {
+ return new Promise(resolve => m.migrate(resolve));
+ }
+ }
+ throw new Error("failed to find the " + name + " migrator");
+}
+
+function promiseTelemetryMigrator(srcDir, targetDir) {
+ return promiseMigrator("telemetry", srcDir, targetDir);
+}
+
+add_task(async function test_empty() {
+ let [srcDir, targetDir] = getTestDirs();
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true with empty directories");
+ // check both are empty
+ checkDirectoryContains(srcDir, {});
+ checkDirectoryContains(targetDir, {});
+});
+
+add_task(async function test_migrate_files() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // Set up datareporting files, some to copy, some not.
+ let stateContent = JSON.stringify({
+ clientId: "68d5474e-19dc-45c1-8e9a-81fca592707c",
+ });
+ let sessionStateContent = "foobar 5432";
+ let subDir = createSubDir(srcDir, "datareporting");
+ writeToFile(subDir, "state.json", stateContent);
+ writeToFile(subDir, "session-state.json", sessionStateContent);
+ writeToFile(subDir, "other.file", "do not copy");
+
+ let archived = createSubDir(subDir, "archived");
+ writeToFile(archived, "other.file", "do not copy");
+
+ // Set up FHR files, they should not be copied.
+ writeToFile(srcDir, "healthreport.sqlite", "do not copy");
+ writeToFile(srcDir, "healthreport.sqlite-wal", "do not copy");
+ subDir = createSubDir(srcDir, "healthreport");
+ writeToFile(subDir, "state.json", "do not copy");
+ writeToFile(subDir, "other.file", "do not copy");
+
+ // Perform migration.
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(
+ ok,
+ "callback should have been true with important telemetry files copied"
+ );
+
+ checkDirectoryContains(targetDir, {
+ datareporting: {
+ "state.json": stateContent,
+ "session-state.json": sessionStateContent,
+ },
+ });
+});
+
+add_task(async function test_datareporting_not_dir() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ writeToFile(srcDir, "datareporting", "I'm a file but should be a directory");
+
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(
+ ok,
+ "callback should have been true even though the directory was a file"
+ );
+
+ checkDirectoryContains(targetDir, {});
+});
+
+add_task(async function test_datareporting_empty() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // Migrate with an empty 'datareporting' subdir.
+ createSubDir(srcDir, "datareporting");
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ // We should end up with no migrated files.
+ checkDirectoryContains(targetDir, {
+ datareporting: {},
+ });
+});
+
+add_task(async function test_healthreport_empty() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // Migrate with no 'datareporting' and an empty 'healthreport' subdir.
+ createSubDir(srcDir, "healthreport");
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ // We should end up with no migrated files.
+ checkDirectoryContains(targetDir, {});
+});
+
+add_task(async function test_datareporting_many() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // Create some datareporting files.
+ let subDir = createSubDir(srcDir, "datareporting");
+ let shouldBeCopied = "should be copied";
+ writeToFile(subDir, "state.json", shouldBeCopied);
+ writeToFile(subDir, "session-state.json", shouldBeCopied);
+ writeToFile(subDir, "something.else", "should not");
+ createSubDir(subDir, "emptyDir");
+
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ checkDirectoryContains(targetDir, {
+ datareporting: {
+ "state.json": shouldBeCopied,
+ "session-state.json": shouldBeCopied,
+ },
+ });
+});
+
+add_task(async function test_no_session_state() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // Check that migration still works properly if we only have state.json.
+ let subDir = createSubDir(srcDir, "datareporting");
+ let stateContent = "abcd984";
+ writeToFile(subDir, "state.json", stateContent);
+
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ checkDirectoryContains(targetDir, {
+ datareporting: {
+ "state.json": stateContent,
+ },
+ });
+});
+
+add_task(async function test_no_state() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // Check that migration still works properly if we only have session-state.json.
+ let subDir = createSubDir(srcDir, "datareporting");
+ let sessionStateContent = "abcd512";
+ writeToFile(subDir, "session-state.json", sessionStateContent);
+
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ checkDirectoryContains(targetDir, {
+ datareporting: {
+ "session-state.json": sessionStateContent,
+ },
+ });
+});
+
+add_task(async function test_times_migration() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // create a times.json in the source directory.
+ let contents = JSON.stringify({ created: 1234 });
+ writeToFile(srcDir, "times.json", contents);
+
+ let earliest = Date.now();
+ let ok = await promiseMigrator("times", srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+ let latest = Date.now();
+
+ let timesFile = targetDir.clone();
+ timesFile.append("times.json");
+
+ let raw = readFile(timesFile);
+ let times = JSON.parse(raw);
+ Assert.ok(times.reset >= earliest && times.reset <= latest);
+ // and it should have left the creation time alone.
+ Assert.equal(times.created, 1234);
+});
+
+/**
+ * Tests that when importing bookmarks, history, or passwords, we
+ * set interaction prefs. These preferences are sent using
+ * TelemetryEnvironment.sys.mjs.
+ */
+add_task(async function test_interaction_telemetry() {
+ let testingMigrator = await MigrationUtils.getMigrator(
+ InternalTestingProfileMigrator.key
+ );
+
+ Services.prefs.clearUserPref(BOOKMARKS_PREF);
+ Services.prefs.clearUserPref(HISTORY_PREF);
+ Services.prefs.clearUserPref(PASSWORDS_PREF);
+
+ // Ensure that these prefs start false.
+ Assert.ok(!Services.prefs.getBoolPref(BOOKMARKS_PREF));
+ Assert.ok(!Services.prefs.getBoolPref(HISTORY_PREF));
+ Assert.ok(!Services.prefs.getBoolPref(PASSWORDS_PREF));
+
+ await testingMigrator.migrate(
+ MigrationUtils.resourceTypes.BOOKMARKS,
+ false,
+ InternalTestingProfileMigrator.testProfile
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref(BOOKMARKS_PREF),
+ "Bookmarks pref should have been set."
+ );
+ Assert.ok(!Services.prefs.getBoolPref(HISTORY_PREF));
+ Assert.ok(!Services.prefs.getBoolPref(PASSWORDS_PREF));
+
+ await testingMigrator.migrate(
+ MigrationUtils.resourceTypes.HISTORY,
+ false,
+ InternalTestingProfileMigrator.testProfile
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref(BOOKMARKS_PREF),
+ "Bookmarks pref should have been set."
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref(HISTORY_PREF),
+ "History pref should have been set."
+ );
+ Assert.ok(!Services.prefs.getBoolPref(PASSWORDS_PREF));
+
+ await testingMigrator.migrate(
+ MigrationUtils.resourceTypes.PASSWORDS,
+ false,
+ InternalTestingProfileMigrator.testProfile
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref(BOOKMARKS_PREF),
+ "Bookmarks pref should have been set."
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref(HISTORY_PREF),
+ "History pref should have been set."
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref(PASSWORDS_PREF),
+ "Passwords pref should have been set."
+ );
+
+ // Now make sure that we still record these if we migrate a
+ // series of resources at the same time.
+ Services.prefs.clearUserPref(BOOKMARKS_PREF);
+ Services.prefs.clearUserPref(HISTORY_PREF);
+ Services.prefs.clearUserPref(PASSWORDS_PREF);
+
+ await testingMigrator.migrate(
+ MigrationUtils.resourceTypes.ALL,
+ false,
+ InternalTestingProfileMigrator.testProfile
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref(BOOKMARKS_PREF),
+ "Bookmarks pref should have been set."
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref(HISTORY_PREF),
+ "History pref should have been set."
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref(PASSWORDS_PREF),
+ "Passwords pref should have been set."
+ );
+});
+
+/**
+ * Tests that when importing passwords from a CSV file using the
+ * migration wizard, we set an interaction pref. This preference
+ * is sent using TelemetryEnvironment.sys.mjs.
+ */
+add_task(async function test_csv_password_interaction_telemetry() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let testingMigrator = new PasswordFileMigrator();
+
+ Services.prefs.clearUserPref(CSV_PASSWORDS_PREF);
+ Assert.ok(!Services.prefs.getBoolPref(CSV_PASSWORDS_PREF));
+
+ sandbox.stub(LoginCSVImport, "importFromCSV").resolves([]);
+ await testingMigrator.migrate("some/fake/path.csv");
+
+ Assert.ok(
+ Services.prefs.getBoolPref(CSV_PASSWORDS_PREF),
+ "CSV import pref should have been set."
+ );
+
+ sandbox.restore();
+});
+
+/**
+ * Tests that interaction preferences used for TelemetryEnvironment are
+ * persisted across profile resets.
+ */
+add_task(async function test_interaction_telemetry_persist_across_reset() {
+ const PREFS = `
+user_pref("${BOOKMARKS_PREF}", true);
+user_pref("${CSV_PASSWORDS_PREF}", true);
+user_pref("${HISTORY_PREF}", true);
+user_pref("${PASSWORDS_PREF}", true);
+ `;
+
+ let [srcDir, targetDir] = getTestDirs();
+ writeToFile(srcDir, "prefs.js", PREFS);
+
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ let prefsPath = PathUtils.join(targetDir.path, "prefs.js");
+ Assert.ok(await IOUtils.exists(prefsPath), "Prefs should have been written.");
+ let writtenPrefsString = await IOUtils.readUTF8(prefsPath);
+ for (let prefKey of [
+ BOOKMARKS_PREF,
+ CSV_PASSWORDS_PREF,
+ HISTORY_PREF,
+ PASSWORDS_PREF,
+ ]) {
+ const EXPECTED = `user_pref("${prefKey}", true);`;
+ Assert.ok(writtenPrefsString.includes(EXPECTED), "Found persisted pref.");
+ }
+});
diff --git a/browser/components/migration/tests/unit/xpcshell.toml b/browser/components/migration/tests/unit/xpcshell.toml
new file mode 100644
index 0000000000..b599a64362
--- /dev/null
+++ b/browser/components/migration/tests/unit/xpcshell.toml
@@ -0,0 +1,95 @@
+[DEFAULT]
+head = "head_migration.js"
+tags = "condprof"
+firefox-appdir = "browser"
+skip-if = ["os == 'android'"] # bug 1730213
+prefs = ["browser.migrate.showBookmarksToolbarAfterMigration=true"]
+support-files = [
+ "Library/**",
+ "AppData/**",
+ "bookmarks.exported.html",
+ "bookmarks.exported.json",
+ "bookmarks.invalid.html",
+]
+
+["test_360seMigrationUtils.js"]
+run-if = ["os == 'win'"]
+
+["test_360se_bookmarks.js"]
+run-if = ["os == 'win'"]
+
+["test_BookmarksFileMigrator.js"]
+
+["test_ChromeMigrationUtils.js"]
+
+["test_ChromeMigrationUtils_path.js"]
+
+["test_ChromeMigrationUtils_path_chromium_snap.js"]
+run-if = ["os == 'linux'"]
+
+["test_Chrome_bookmarks.js"]
+
+["test_Chrome_corrupt_history.js"]
+
+["test_Chrome_credit_cards.js"]
+skip-if = [
+ "os == 'linux'",
+ "os == 'android'",
+ "condprof", # bug 1769154 - not realistic for condprof
+]
+
+["test_Chrome_extensions.js"]
+
+["test_Chrome_formdata.js"]
+
+["test_Chrome_history.js"]
+skip-if = ["os != 'mac'"] # Relies on ULibDir
+
+["test_Chrome_passwords.js"]
+skip-if = [
+ "os == 'linux'",
+ "os == 'android'",
+ "condprof", # bug 1769154 - not realistic for condprof
+]
+
+["test_Chrome_passwords_emptySource.js"]
+skip-if = [
+ "os == 'linux'",
+ "os == 'android'",
+ "condprof", # bug 1769154 - not realistic for condprof
+]
+support-files = ["LibraryWithNoData/**"]
+
+["test_Chrome_permissions.js"]
+
+["test_Edge_db_migration.js"]
+run-if = ["os == 'win'"]
+
+["test_Edge_registry_migration.js"]
+run-if = ["os == 'win'"]
+
+["test_IE_bookmarks.js"]
+run-if = ["os == 'win' && bits == 64"] # bug 1392396
+
+["test_IE_history.js"]
+run-if = ["os == 'win'"]
+skip-if = ["os == 'win' && msix"] # https://bugzilla.mozilla.org/show_bug.cgi?id=1807928
+
+["test_MigrationUtils_timedRetry.js"]
+skip-if = ["os == 'mac' && !debug"] #Bug 1558330
+
+["test_PasswordFileMigrator.js"]
+
+["test_Safari_bookmarks.js"]
+run-if = ["os == 'mac'"]
+
+["test_Safari_history.js"]
+run-if = ["os == 'mac'"]
+
+["test_Safari_history_strange_entries.js"]
+run-if = ["os == 'mac'"]
+
+["test_Safari_permissions.js"]
+run-if = ["os == 'mac'"]
+
+["test_fx_telemetry.js"]