diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /browser/components/migration/tests/unit | |
parent | Initial commit. (diff) | |
download | firefox-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')
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 Binary files differnew file mode 100644 index 0000000000..fddee798b3 --- /dev/null +++ b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Favicons 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 Binary files differnew file mode 100644 index 0000000000..7e6e843a03 --- /dev/null +++ b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Login Data 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 Binary files differnew file mode 100644 index 0000000000..c557c9b851 --- /dev/null +++ b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Web Data 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 Binary files differnew file mode 100644 index 0000000000..fd135624c4 --- /dev/null +++ b/browser/components/migration/tests/unit/AppData/LocalWithNoData/Google/Chrome/User Data/Default/Login Data 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 Binary files differnew file mode 100644 index 0000000000..1835c33583 --- /dev/null +++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat 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 Binary files differnew file mode 100644 index 0000000000..83d855cb33 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Cookies 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 Binary files differnew file mode 100644 index 0000000000..8585f308c5 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryCorrupt 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 Binary files differnew file mode 100644 index 0000000000..7fb19903b0 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryMaster 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 Binary files differnew file mode 100644 index 0000000000..19b8542b98 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Login Data 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 Binary files differnew file mode 100644 index 0000000000..a9c33e1b1a --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Bookmarks.plist 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 Binary files differnew file mode 100644 index 0000000000..dd5d0c7512 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db 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 Binary files differnew file mode 100644 index 0000000000..edd607898b --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-shm 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 Binary files differnew file mode 100644 index 0000000000..e145119298 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-wal 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 Binary files differnew file mode 100644 index 0000000000..1c6741c165 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/04860FA3D07D8936B87D2B965317C6E9 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 Binary files differnew file mode 100644 index 0000000000..47b40f707f --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/19A777E4F7BDA0C0E350D6C681B6E271 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 Binary files differnew file mode 100644 index 0000000000..2a4c30b31e --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/2558A57A1AE576AA31F0DCD1364B3F42 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 Binary files differnew file mode 100644 index 0000000000..f4996ba082 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/57D1907A1EBDA1889AA85B8AB7A90804 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 Binary files differnew file mode 100644 index 0000000000..f519ce9ad2 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/6EEDD53B65A19CB364EB6FB07DEACF80 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 Binary files differnew file mode 100644 index 0000000000..e70021849b --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/7F65370AD319C7B294EDF2E2BEBA880F 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 Binary files differnew file mode 100644 index 0000000000..559502b02b --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/999E2BD5CD612AA550F222A1088DB3D8 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 Binary files differnew file mode 100644 index 0000000000..89ed9a1c39 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/9D8A6E2153D42043A7AE0430B41D374A 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 Binary files differnew file mode 100644 index 0000000000..7b86185e67 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/A21F634481CF5188329FD2052F07ADBC 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 Binary files differnew file mode 100644 index 0000000000..a1d03856b5 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/BC2288B5BA9B7BE352BA586257442E08 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 Binary files differnew file mode 100644 index 0000000000..ba1145ca83 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/CFFC3831D8E7201BF8B77728FC79B52B 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 Binary files differnew file mode 100644 index 0000000000..82339b3b1d --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/F3FA61DDA95B78A8B5F2C392C0382137 diff --git a/browser/components/migration/tests/unit/Library/Safari/HistoryStrangeEntries.db b/browser/components/migration/tests/unit/Library/Safari/HistoryStrangeEntries.db Binary files differnew file mode 100644 index 0000000000..533daba3df --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/HistoryStrangeEntries.db diff --git a/browser/components/migration/tests/unit/Library/Safari/HistoryTemplate.db b/browser/components/migration/tests/unit/Library/Safari/HistoryTemplate.db Binary files differnew file mode 100644 index 0000000000..5a317c70e8 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/HistoryTemplate.db 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 Binary files differnew file mode 100644 index 0000000000..b2d425eb4a --- /dev/null +++ b/browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Default/Login Data 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&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"] |