From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- toolkit/components/passwordmgr/test/.eslintrc.js | 13 + .../components/passwordmgr/test/LoginTestUtils.jsm | 295 +++++++ .../components/passwordmgr/test/authenticate.sjs | 228 ++++++ toolkit/components/passwordmgr/test/blank.html | 8 + .../passwordmgr/test/browser/.eslintrc.js | 7 + .../passwordmgr/test/browser/authenticate.sjs | 110 +++ .../passwordmgr/test/browser/browser.ini | 72 ++ .../test/browser/browser_DOMFormHasPassword.js | 94 +++ .../test/browser/browser_DOMInputPasswordAdded.js | 99 +++ .../browser_autocomplete_insecure_warning.js | 41 + .../test/browser/browser_capture_doorhanger.js | 600 ++++++++++++++ .../browser_capture_doorhanger_httpsUpgrade.js | 123 +++ .../browser_capture_doorhanger_window_open.js | 144 ++++ .../test/browser/browser_context_menu.js | 432 +++++++++++ ...rowser_context_menu_autocomplete_interaction.js | 99 +++ .../test/browser/browser_context_menu_iframe.js | 144 ++++ .../test/browser/browser_exceptions_dialog.js | 56 ++ .../test/browser/browser_formless_submit_chrome.js | 126 +++ .../test/browser/browser_hasInsecureLoginForms.js | 93 +++ ...rowser_hasInsecureLoginForms_streamConverter.js | 102 +++ .../test/browser/browser_http_autofill.js | 78 ++ .../browser_insecurePasswordConsoleWarning.js | 94 +++ .../browser_master_password_autocomplete.js | 59 ++ .../test/browser/browser_notifications.js | 81 ++ .../test/browser/browser_notifications_2.js | 125 +++ .../test/browser/browser_notifications_password.js | 145 ++++ .../test/browser/browser_notifications_username.js | 119 +++ .../browser/browser_passwordmgr_contextmenu.js | 100 +++ .../test/browser/browser_passwordmgr_editing.js | 126 +++ .../test/browser/browser_passwordmgr_fields.js | 65 ++ .../test/browser/browser_passwordmgr_observers.js | 129 +++ .../test/browser/browser_passwordmgr_sort.js | 208 +++++ .../test/browser/browser_passwordmgr_switchtab.js | 42 + .../test/browser/browser_passwordmgrdlg.js | 192 +++++ .../test/browser/browser_username_select_dialog.js | 144 ++++ .../test/browser/form_autofocus_js.html | 10 + .../passwordmgr/test/browser/form_basic.html | 12 + .../test/browser/form_basic_iframe.html | 13 + .../browser/form_cross_origin_insecure_action.html | 12 + .../browser/form_cross_origin_secure_action.html | 12 + .../test/browser/form_same_origin_action.html | 12 + .../passwordmgr/test/browser/formless_basic.html | 18 + .../components/passwordmgr/test/browser/head.js | 137 ++++ .../passwordmgr/test/browser/insecure_test.html | 9 + .../test/browser/insecure_test_subframe.html | 13 + .../passwordmgr/test/browser/multiple_forms.html | 129 +++ .../test/browser/streamConverter_content.sjs | 6 + .../test/browser/subtst_notifications_1.html | 29 + .../test/browser/subtst_notifications_10.html | 27 + .../test/browser/subtst_notifications_11.html | 25 + .../browser/subtst_notifications_11_popup.html | 32 + .../test/browser/subtst_notifications_2.html | 30 + .../test/browser/subtst_notifications_2pw_0un.html | 27 + .../subtst_notifications_2pw_1un_1text.html | 31 + .../test/browser/subtst_notifications_3.html | 30 + .../test/browser/subtst_notifications_4.html | 30 + .../test/browser/subtst_notifications_5.html | 26 + .../test/browser/subtst_notifications_6.html | 27 + .../test/browser/subtst_notifications_8.html | 29 + .../test/browser/subtst_notifications_9.html | 29 + .../browser/subtst_notifications_change_p.html | 32 + .../components/passwordmgr/test/chrome/chrome.ini | 13 + .../passwordmgr/test/chrome/notification_common.js | 111 +++ .../chrome/privbrowsing_perwindowpb_iframe.html | 9 + .../test/chrome/subtst_privbrowsing_1.html | 33 + .../test/chrome/subtst_privbrowsing_2.html | 33 + .../test/chrome/subtst_privbrowsing_3.html | 29 + .../test/chrome/subtst_privbrowsing_4.html | 40 + .../test/chrome/test_privbrowsing_perwindowpb.html | 322 ++++++++ .../components/passwordmgr/test/chrome_timeout.js | 11 + toolkit/components/passwordmgr/test/formsubmit.sjs | 37 + toolkit/components/passwordmgr/test/mochitest.ini | 20 + .../test/mochitest/auth2/authenticate.sjs | 220 ++++++ .../passwordmgr/test/mochitest/mochitest.ini | 69 ++ .../mochitest/test_autocomplete_https_upgrade.html | 218 ++++++ .../mochitest/test_autofill_https_upgrade.html | 117 +++ .../mochitest/test_autofill_password-only.html | 143 ++++ .../test/mochitest/test_autofocus_js.html | 115 +++ .../test/mochitest/test_basic_form.html | 44 ++ .../test/mochitest/test_basic_form_0pw.html | 72 ++ .../test/mochitest/test_basic_form_1pw.html | 167 ++++ .../test/mochitest/test_basic_form_1pw_2.html | 109 +++ .../test/mochitest/test_basic_form_2pw_1.html | 187 +++++ .../test/mochitest/test_basic_form_2pw_2.html | 105 +++ .../test/mochitest/test_basic_form_3pw_1.html | 177 +++++ .../mochitest/test_basic_form_autocomplete.html | 859 ++++++++++++++++++++ .../test/mochitest/test_basic_form_html5.html | 164 ++++ .../test/mochitest/test_basic_form_pwevent.html | 55 ++ .../test/mochitest/test_basic_form_pwonly.html | 213 +++++ .../test/mochitest/test_bug_627616.html | 145 ++++ .../test/mochitest/test_bug_776171.html | 56 ++ .../test/mochitest/test_case_differences.html | 147 ++++ .../test/mochitest/test_form_action_1.html | 137 ++++ .../test/mochitest/test_form_action_2.html | 170 ++++ .../mochitest/test_form_action_javascript.html | 52 ++ .../test/mochitest/test_formless_autofill.html | 147 ++++ .../test/mochitest/test_formless_submit.html | 183 +++++ .../mochitest/test_formless_submit_navigation.html | 191 +++++ .../test_formless_submit_navigation_negative.html | 121 +++ .../test/mochitest/test_input_events.html | 96 +++ .../test_input_events_for_identical_values.html | 51 ++ .../test_insecure_form_field_autocomplete.html | 861 +++++++++++++++++++++ .../test_insecure_form_field_no_saved_login.html | 103 +++ .../passwordmgr/test/mochitest/test_maxlength.html | 137 ++++ .../test_password_field_autocomplete.html | 291 +++++++ .../mochitest/test_passwords_in_type_password.html | 122 +++ .../passwordmgr/test/mochitest/test_prompt.html | 705 +++++++++++++++++ .../test/mochitest/test_prompt_http.html | 362 +++++++++ .../test/mochitest/test_prompt_noWindow.html | 81 ++ .../test/mochitest/test_prompt_promptAuth.html | 406 ++++++++++ .../mochitest/test_prompt_promptAuth_proxy.html | 264 +++++++ .../test/mochitest/test_recipe_login_fields.html | 145 ++++ .../test/mochitest/test_username_focus.html | 263 +++++++ .../passwordmgr/test/mochitest/test_xhr_2.html | 55 ++ .../components/passwordmgr/test/prompt_common.js | 79 ++ .../components/passwordmgr/test/pwmgr_common.js | 509 ++++++++++++ .../passwordmgr/test/subtst_master_pass.html | 12 + .../passwordmgr/test/subtst_prompt_async.html | 12 + .../passwordmgr/test/test_master_password.html | 308 ++++++++ .../passwordmgr/test/test_prompt_async.html | 540 +++++++++++++ toolkit/components/passwordmgr/test/test_xhr.html | 201 +++++ .../components/passwordmgr/test/test_xml_load.html | 191 +++++ .../components/passwordmgr/test/unit/.eslintrc.js | 7 + .../passwordmgr/test/unit/data/corruptDB.sqlite | Bin 0 -> 32772 bytes .../components/passwordmgr/test/unit/data/key3.db | Bin 0 -> 16384 bytes .../passwordmgr/test/unit/data/signons-v1.sqlite | Bin 0 -> 8192 bytes .../passwordmgr/test/unit/data/signons-v1v2.sqlite | Bin 0 -> 10240 bytes .../passwordmgr/test/unit/data/signons-v2.sqlite | Bin 0 -> 11264 bytes .../passwordmgr/test/unit/data/signons-v2v3.sqlite | Bin 0 -> 12288 bytes .../passwordmgr/test/unit/data/signons-v3.sqlite | Bin 0 -> 11264 bytes .../passwordmgr/test/unit/data/signons-v3v4.sqlite | Bin 0 -> 11264 bytes .../passwordmgr/test/unit/data/signons-v4.sqlite | Bin 0 -> 294912 bytes .../passwordmgr/test/unit/data/signons-v4v5.sqlite | Bin 0 -> 327680 bytes .../passwordmgr/test/unit/data/signons-v5v6.sqlite | Bin 0 -> 327680 bytes .../test/unit/data/signons-v999-2.sqlite | Bin 0 -> 8192 bytes .../passwordmgr/test/unit/data/signons-v999.sqlite | Bin 0 -> 11264 bytes toolkit/components/passwordmgr/test/unit/head.js | 135 ++++ .../passwordmgr/test/unit/test_OSCrypto_win.js | 75 ++ .../passwordmgr/test/unit/test_context_menu.js | 165 ++++ .../passwordmgr/test/unit/test_dedupeLogins.js | 284 +++++++ .../passwordmgr/test/unit/test_disabled_hosts.js | 196 +++++ .../passwordmgr/test/unit/test_getFormFields.js | 147 ++++ .../test/unit/test_getPasswordFields.js | 156 ++++ .../test/unit/test_getPasswordOrigin.js | 28 + .../passwordmgr/test/unit/test_isOriginMatching.js | 40 + .../test/unit/test_legacy_empty_formSubmitURL.js | 107 +++ .../test/unit/test_legacy_validation.js | 76 ++ .../passwordmgr/test/unit/test_logins_change.js | 384 +++++++++ .../test/unit/test_logins_decrypt_failure.js | 77 ++ .../passwordmgr/test/unit/test_logins_metainfo.js | 284 +++++++ .../passwordmgr/test/unit/test_logins_search.js | 221 ++++++ .../passwordmgr/test/unit/test_maybeImportLogin.js | 169 ++++ .../test/unit/test_module_LoginImport.js | 243 ++++++ .../test/unit/test_module_LoginStore.js | 206 +++++ .../passwordmgr/test/unit/test_notifications.js | 172 ++++ .../passwordmgr/test/unit/test_recipes_add.js | 177 +++++ .../passwordmgr/test/unit/test_recipes_content.js | 39 + .../test/unit/test_removeLegacySignonFiles.js | 69 ++ .../test/unit/test_search_schemeUpgrades.js | 184 +++++ .../passwordmgr/test/unit/test_storage.js | 102 +++ .../test/unit/test_storage_mozStorage.js | 507 ++++++++++++ .../passwordmgr/test/unit/test_telemetry.js | 187 +++++ .../test/unit/test_user_autocomplete_result.js | 488 ++++++++++++ .../components/passwordmgr/test/unit/xpcshell.ini | 46 ++ 164 files changed, 21186 insertions(+) create mode 100644 toolkit/components/passwordmgr/test/.eslintrc.js create mode 100644 toolkit/components/passwordmgr/test/LoginTestUtils.jsm create mode 100644 toolkit/components/passwordmgr/test/authenticate.sjs create mode 100644 toolkit/components/passwordmgr/test/blank.html create mode 100644 toolkit/components/passwordmgr/test/browser/.eslintrc.js create mode 100644 toolkit/components/passwordmgr/test/browser/authenticate.sjs create mode 100644 toolkit/components/passwordmgr/test/browser/browser.ini create mode 100644 toolkit/components/passwordmgr/test/browser/browser_DOMFormHasPassword.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_DOMInputPasswordAdded.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_autocomplete_insecure_warning.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_httpsUpgrade.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_window_open.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_context_menu.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_context_menu_autocomplete_interaction.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_context_menu_iframe.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_exceptions_dialog.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_formless_submit_chrome.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms_streamConverter.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_http_autofill.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_insecurePasswordConsoleWarning.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_master_password_autocomplete.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_notifications.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_notifications_2.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_notifications_password.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_notifications_username.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_passwordmgr_contextmenu.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_passwordmgr_editing.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_passwordmgr_fields.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_passwordmgr_observers.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_passwordmgr_sort.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_passwordmgr_switchtab.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_passwordmgrdlg.js create mode 100644 toolkit/components/passwordmgr/test/browser/browser_username_select_dialog.js create mode 100644 toolkit/components/passwordmgr/test/browser/form_autofocus_js.html create mode 100644 toolkit/components/passwordmgr/test/browser/form_basic.html create mode 100644 toolkit/components/passwordmgr/test/browser/form_basic_iframe.html create mode 100644 toolkit/components/passwordmgr/test/browser/form_cross_origin_insecure_action.html create mode 100644 toolkit/components/passwordmgr/test/browser/form_cross_origin_secure_action.html create mode 100644 toolkit/components/passwordmgr/test/browser/form_same_origin_action.html create mode 100644 toolkit/components/passwordmgr/test/browser/formless_basic.html create mode 100644 toolkit/components/passwordmgr/test/browser/head.js create mode 100644 toolkit/components/passwordmgr/test/browser/insecure_test.html create mode 100644 toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html create mode 100644 toolkit/components/passwordmgr/test/browser/multiple_forms.html create mode 100644 toolkit/components/passwordmgr/test/browser/streamConverter_content.sjs create mode 100644 toolkit/components/passwordmgr/test/browser/subtst_notifications_1.html create mode 100644 toolkit/components/passwordmgr/test/browser/subtst_notifications_10.html create mode 100644 toolkit/components/passwordmgr/test/browser/subtst_notifications_11.html create mode 100644 toolkit/components/passwordmgr/test/browser/subtst_notifications_11_popup.html create mode 100644 toolkit/components/passwordmgr/test/browser/subtst_notifications_2.html create mode 100644 toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_0un.html create mode 100644 toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_1un_1text.html create mode 100644 toolkit/components/passwordmgr/test/browser/subtst_notifications_3.html create mode 100644 toolkit/components/passwordmgr/test/browser/subtst_notifications_4.html create mode 100644 toolkit/components/passwordmgr/test/browser/subtst_notifications_5.html create mode 100644 toolkit/components/passwordmgr/test/browser/subtst_notifications_6.html create mode 100644 toolkit/components/passwordmgr/test/browser/subtst_notifications_8.html create mode 100644 toolkit/components/passwordmgr/test/browser/subtst_notifications_9.html create mode 100644 toolkit/components/passwordmgr/test/browser/subtst_notifications_change_p.html create mode 100644 toolkit/components/passwordmgr/test/chrome/chrome.ini create mode 100644 toolkit/components/passwordmgr/test/chrome/notification_common.js create mode 100644 toolkit/components/passwordmgr/test/chrome/privbrowsing_perwindowpb_iframe.html create mode 100644 toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_1.html create mode 100644 toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_2.html create mode 100644 toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_3.html create mode 100644 toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_4.html create mode 100644 toolkit/components/passwordmgr/test/chrome/test_privbrowsing_perwindowpb.html create mode 100644 toolkit/components/passwordmgr/test/chrome_timeout.js create mode 100644 toolkit/components/passwordmgr/test/formsubmit.sjs create mode 100644 toolkit/components/passwordmgr/test/mochitest.ini create mode 100644 toolkit/components/passwordmgr/test/mochitest/auth2/authenticate.sjs create mode 100644 toolkit/components/passwordmgr/test/mochitest/mochitest.ini create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_upgrade.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_autofill_password-only.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_basic_form.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_basic_form_0pw.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw_2.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_1.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_basic_form_3pw_1.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_basic_form_html5.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwevent.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwonly.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_bug_776171.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_case_differences.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_form_action_1.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_form_action_2.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_form_action_javascript.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_formless_autofill.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_input_events.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_input_events_for_identical_values.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_autocomplete.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_no_saved_login.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_maxlength.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_passwords_in_type_password.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_prompt.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_recipe_login_fields.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_username_focus.html create mode 100644 toolkit/components/passwordmgr/test/mochitest/test_xhr_2.html create mode 100644 toolkit/components/passwordmgr/test/prompt_common.js create mode 100644 toolkit/components/passwordmgr/test/pwmgr_common.js create mode 100644 toolkit/components/passwordmgr/test/subtst_master_pass.html create mode 100644 toolkit/components/passwordmgr/test/subtst_prompt_async.html create mode 100644 toolkit/components/passwordmgr/test/test_master_password.html create mode 100644 toolkit/components/passwordmgr/test/test_prompt_async.html create mode 100644 toolkit/components/passwordmgr/test/test_xhr.html create mode 100644 toolkit/components/passwordmgr/test/test_xml_load.html create mode 100644 toolkit/components/passwordmgr/test/unit/.eslintrc.js create mode 100644 toolkit/components/passwordmgr/test/unit/data/corruptDB.sqlite create mode 100644 toolkit/components/passwordmgr/test/unit/data/key3.db create mode 100644 toolkit/components/passwordmgr/test/unit/data/signons-v1.sqlite create mode 100644 toolkit/components/passwordmgr/test/unit/data/signons-v1v2.sqlite create mode 100644 toolkit/components/passwordmgr/test/unit/data/signons-v2.sqlite create mode 100644 toolkit/components/passwordmgr/test/unit/data/signons-v2v3.sqlite create mode 100644 toolkit/components/passwordmgr/test/unit/data/signons-v3.sqlite create mode 100644 toolkit/components/passwordmgr/test/unit/data/signons-v3v4.sqlite create mode 100644 toolkit/components/passwordmgr/test/unit/data/signons-v4.sqlite create mode 100644 toolkit/components/passwordmgr/test/unit/data/signons-v4v5.sqlite create mode 100644 toolkit/components/passwordmgr/test/unit/data/signons-v5v6.sqlite create mode 100644 toolkit/components/passwordmgr/test/unit/data/signons-v999-2.sqlite create mode 100644 toolkit/components/passwordmgr/test/unit/data/signons-v999.sqlite create mode 100644 toolkit/components/passwordmgr/test/unit/head.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_OSCrypto_win.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_context_menu.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_dedupeLogins.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_disabled_hosts.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_getFormFields.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_getPasswordFields.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_getPasswordOrigin.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_isOriginMatching.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_legacy_empty_formSubmitURL.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_legacy_validation.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_logins_change.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_logins_decrypt_failure.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_logins_metainfo.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_logins_search.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_maybeImportLogin.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_module_LoginImport.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_module_LoginStore.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_notifications.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_recipes_add.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_recipes_content.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_removeLegacySignonFiles.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_search_schemeUpgrades.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_storage.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_storage_mozStorage.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_telemetry.js create mode 100644 toolkit/components/passwordmgr/test/unit/test_user_autocomplete_result.js create mode 100644 toolkit/components/passwordmgr/test/unit/xpcshell.ini (limited to 'toolkit/components/passwordmgr/test') diff --git a/toolkit/components/passwordmgr/test/.eslintrc.js b/toolkit/components/passwordmgr/test/.eslintrc.js new file mode 100644 index 000000000..ca626f31c --- /dev/null +++ b/toolkit/components/passwordmgr/test/.eslintrc.js @@ -0,0 +1,13 @@ +"use strict"; + +module.exports = { // eslint-disable-line no-undef + "extends": [ + "../../../../testing/mochitest/mochitest.eslintrc.js", + "../../../../testing/mochitest/chrome.eslintrc.js" + ], + "rules": { + "brace-style": "off", + "no-undef": "off", + "no-unused-vars": "off", + }, +}; diff --git a/toolkit/components/passwordmgr/test/LoginTestUtils.jsm b/toolkit/components/passwordmgr/test/LoginTestUtils.jsm new file mode 100644 index 000000000..2fd8a31a3 --- /dev/null +++ b/toolkit/components/passwordmgr/test/LoginTestUtils.jsm @@ -0,0 +1,295 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Shared functions generally available for testing login components. + */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "LoginTestUtils", +]; + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +Cu.import("resource://testing-common/TestUtils.jsm"); + +const LoginInfo = + Components.Constructor("@mozilla.org/login-manager/loginInfo;1", + "nsILoginInfo", "init"); + +// For now, we need consumers to provide a reference to Assert.jsm. +var Assert = null; + +this.LoginTestUtils = { + set Assert(assert) { + Assert = assert; // eslint-disable-line no-native-reassign + }, + + /** + * Forces the storage module to save all data, and the Login Manager service + * to replace the storage module with a newly initialized instance. + */ + * reloadData() { + Services.obs.notifyObservers(null, "passwordmgr-storage-replace", null); + yield TestUtils.topicObserved("passwordmgr-storage-replace-complete"); + }, + + /** + * Erases all the data stored by the Login Manager service. + */ + clearData() { + Services.logins.removeAllLogins(); + for (let hostname of Services.logins.getAllDisabledHosts()) { + Services.logins.setLoginSavingEnabled(hostname, true); + } + }, + + /** + * Checks that the currently stored list of nsILoginInfo matches the provided + * array. The comparison uses the "equals" method of nsILoginInfo, that does + * not include nsILoginMetaInfo properties in the test. + */ + checkLogins(expectedLogins) { + this.assertLoginListsEqual(Services.logins.getAllLogins(), expectedLogins); + }, + + /** + * Checks that the two provided arrays of nsILoginInfo have the same length, + * and every login in "expected" is also found in "actual". The comparison + * uses the "equals" method of nsILoginInfo, that does not include + * nsILoginMetaInfo properties in the test. + */ + assertLoginListsEqual(actual, expected) { + Assert.equal(expected.length, actual.length); + Assert.ok(expected.every(e => actual.some(a => a.equals(e)))); + }, + + /** + * Checks that the two provided arrays of strings contain the same values, + * maybe in a different order, case-sensitively. + */ + assertDisabledHostsEqual(actual, expected) { + Assert.deepEqual(actual.sort(), expected.sort()); + }, + + /** + * Checks whether the given time, expressed as the number of milliseconds + * since January 1, 1970, 00:00:00 UTC, falls within 30 seconds of now. + */ + assertTimeIsAboutNow(timeMs) { + Assert.ok(Math.abs(timeMs - Date.now()) < 30000); + }, +}; + +/** + * This object contains functions that return new instances of nsILoginInfo for + * every call. The returned instances can be compared using their "equals" or + * "matches" methods, or modified for the needs of the specific test being run. + * + * Any modification to the test data requires updating the tests accordingly, in + * particular the search tests. + */ +this.LoginTestUtils.testData = { + /** + * Returns a new nsILoginInfo for use with form submits. + * + * @param modifications + * Each property of this object replaces the property of the same name + * in the returned nsILoginInfo or nsILoginMetaInfo. + */ + formLogin(modifications) { + let loginInfo = new LoginInfo("http://www3.example.com", + "http://www.example.com", null, + "the username", "the password", + "form_field_username", "form_field_password"); + loginInfo.QueryInterface(Ci.nsILoginMetaInfo); + if (modifications) { + for (let [name, value] of Object.entries(modifications)) { + loginInfo[name] = value; + } + } + return loginInfo; + }, + + /** + * Returns a new nsILoginInfo for use with HTTP authentication. + * + * @param modifications + * Each property of this object replaces the property of the same name + * in the returned nsILoginInfo or nsILoginMetaInfo. + */ + authLogin(modifications) { + let loginInfo = new LoginInfo("http://www.example.org", null, + "The HTTP Realm", "the username", + "the password", "", ""); + loginInfo.QueryInterface(Ci.nsILoginMetaInfo); + if (modifications) { + for (let [name, value] of Object.entries(modifications)) { + loginInfo[name] = value; + } + } + return loginInfo; + }, + + /** + * Returns an array of typical nsILoginInfo that could be stored in the + * database. + */ + loginList() { + return [ + // --- Examples of form logins (subdomains of example.com) --- + + // Simple form login with named fields for username and password. + new LoginInfo("http://www.example.com", "http://www.example.com", null, + "the username", "the password for www.example.com", + "form_field_username", "form_field_password"), + + // Different schemes are treated as completely different sites. + new LoginInfo("https://www.example.com", "https://www.example.com", null, + "the username", "the password for https", + "form_field_username", "form_field_password"), + + // Subdomains are treated as completely different sites. + new LoginInfo("https://example.com", "https://example.com", null, + "the username", "the password for example.com", + "form_field_username", "form_field_password"), + + // Forms found on the same host, but with different hostnames in the + // "action" attribute, are handled independently. + new LoginInfo("http://www3.example.com", "http://www.example.com", null, + "the username", "the password", + "form_field_username", "form_field_password"), + new LoginInfo("http://www3.example.com", "https://www.example.com", null, + "the username", "the password", + "form_field_username", "form_field_password"), + new LoginInfo("http://www3.example.com", "http://example.com", null, + "the username", "the password", + "form_field_username", "form_field_password"), + + // It is not possible to store multiple passwords for the same username, + // however multiple passwords can be stored when the usernames differ. + // An empty username is a valid case and different from the others. + new LoginInfo("http://www4.example.com", "http://www4.example.com", null, + "username one", "password one", + "form_field_username", "form_field_password"), + new LoginInfo("http://www4.example.com", "http://www4.example.com", null, + "username two", "password two", + "form_field_username", "form_field_password"), + new LoginInfo("http://www4.example.com", "http://www4.example.com", null, + "", "password three", + "form_field_username", "form_field_password"), + + // Username and passwords fields in forms may have no "name" attribute. + new LoginInfo("http://www5.example.com", "http://www5.example.com", null, + "multi username", "multi password", "", ""), + + // Forms with PIN-type authentication will typically have no username. + new LoginInfo("http://www6.example.com", "http://www6.example.com", null, + "", "12345", "", "form_field_password"), + + // --- Examples of authentication logins (subdomains of example.org) --- + + // Simple HTTP authentication login. + new LoginInfo("http://www.example.org", null, "The HTTP Realm", + "the username", "the password", "", ""), + + // Simple FTP authentication login. + new LoginInfo("ftp://ftp.example.org", null, "ftp://ftp.example.org", + "the username", "the password", "", ""), + + // Multiple HTTP authentication logins can be stored for different realms. + new LoginInfo("http://www2.example.org", null, "The HTTP Realm", + "the username", "the password", "", ""), + new LoginInfo("http://www2.example.org", null, "The HTTP Realm Other", + "the username other", "the password other", "", ""), + + // --- Both form and authentication logins (example.net) --- + + new LoginInfo("http://example.net", "http://example.net", null, + "the username", "the password", + "form_field_username", "form_field_password"), + new LoginInfo("http://example.net", "http://www.example.net", null, + "the username", "the password", + "form_field_username", "form_field_password"), + new LoginInfo("http://example.net", "http://www.example.net", null, + "username two", "the password", + "form_field_username", "form_field_password"), + new LoginInfo("http://example.net", null, "The HTTP Realm", + "the username", "the password", "", ""), + new LoginInfo("http://example.net", null, "The HTTP Realm Other", + "username two", "the password", "", ""), + new LoginInfo("ftp://example.net", null, "ftp://example.net", + "the username", "the password", "", ""), + + // --- Examples of logins added by extensions (chrome scheme) --- + + new LoginInfo("chrome://example_extension", null, "Example Login One", + "the username", "the password one", "", ""), + new LoginInfo("chrome://example_extension", null, "Example Login Two", + "the username", "the password two", "", ""), + ]; + }, +}; + +this.LoginTestUtils.recipes = { + getRecipeParent() { + let { LoginManagerParent } = Cu.import("resource://gre/modules/LoginManagerParent.jsm", {}); + if (!LoginManagerParent.recipeParentPromise) { + return null; + } + return LoginManagerParent.recipeParentPromise.then((recipeParent) => { + return recipeParent; + }); + }, +}; + +this.LoginTestUtils.masterPassword = { + masterPassword: "omgsecret!", + + _set(enable) { + let oldPW, newPW; + if (enable) { + oldPW = ""; + newPW = this.masterPassword; + } else { + oldPW = this.masterPassword; + newPW = ""; + } + + let secmodDB = Cc["@mozilla.org/security/pkcs11moduledb;1"] + .getService(Ci.nsIPKCS11ModuleDB); + let slot = secmodDB.findSlotByName(""); + if (!slot) { + throw new Error("Can't find slot"); + } + + // Set master password. Note that this does not log you in, so the next + // invocation of pwmgr can trigger a MP prompt. + let pk11db = Cc["@mozilla.org/security/pk11tokendb;1"] + .getService(Ci.nsIPK11TokenDB); + let token = pk11db.findTokenByName(""); + if (slot.status == Ci.nsIPKCS11Slot.SLOT_UNINITIALIZED) { + dump("MP initialized to " + newPW + "\n"); + token.initPassword(newPW); + } else { + token.checkPassword(oldPW); + dump("MP change from " + oldPW + " to " + newPW + "\n"); + token.changePassword(oldPW, newPW); + } + }, + + enable() { + this._set(true); + }, + + disable() { + this._set(false); + }, +}; diff --git a/toolkit/components/passwordmgr/test/authenticate.sjs b/toolkit/components/passwordmgr/test/authenticate.sjs new file mode 100644 index 000000000..42edc3220 --- /dev/null +++ b/toolkit/components/passwordmgr/test/authenticate.sjs @@ -0,0 +1,228 @@ +function handleRequest(request, response) +{ + try { + reallyHandleRequest(request, response); + } catch (e) { + response.setStatusLine("1.0", 200, "AlmostOK"); + response.write("Error handling request: " + e); + } +} + + +function reallyHandleRequest(request, response) { + var match; + var requestAuth = true, requestProxyAuth = true; + + // Allow the caller to drive how authentication is processed via the query. + // Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar + // The extra ? allows the user/pass/realm checks to succeed if the name is + // at the beginning of the query string. + var query = "?" + request.queryString; + + var expected_user = "", expected_pass = "", realm = "mochitest"; + var proxy_expected_user = "", proxy_expected_pass = "", proxy_realm = "mochi-proxy"; + var huge = false, plugin = false, anonymous = false, formauth = false; + var authHeaderCount = 1; + // user=xxx + match = /[^_]user=([^&]*)/.exec(query); + if (match) + expected_user = match[1]; + + // pass=xxx + match = /[^_]pass=([^&]*)/.exec(query); + if (match) + expected_pass = match[1]; + + // realm=xxx + match = /[^_]realm=([^&]*)/.exec(query); + if (match) + realm = match[1]; + + // proxy_user=xxx + match = /proxy_user=([^&]*)/.exec(query); + if (match) + proxy_expected_user = match[1]; + + // proxy_pass=xxx + match = /proxy_pass=([^&]*)/.exec(query); + if (match) + proxy_expected_pass = match[1]; + + // proxy_realm=xxx + match = /proxy_realm=([^&]*)/.exec(query); + if (match) + proxy_realm = match[1]; + + // huge=1 + match = /huge=1/.exec(query); + if (match) + huge = true; + + // plugin=1 + match = /plugin=1/.exec(query); + if (match) + plugin = true; + + // multiple=1 + match = /multiple=([^&]*)/.exec(query); + if (match) + authHeaderCount = match[1]+0; + + // anonymous=1 + match = /anonymous=1/.exec(query); + if (match) + anonymous = true; + + // formauth=1 + match = /formauth=1/.exec(query); + if (match) + formauth = true; + + // Look for an authentication header, if any, in the request. + // + // EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== + // + // This test only supports Basic auth. The value sent by the client is + // "username:password", obscured with base64 encoding. + + var actual_user = "", actual_pass = "", authHeader, authPresent = false; + if (request.hasHeader("Authorization")) { + authPresent = true; + authHeader = request.getHeader("Authorization"); + match = /Basic (.+)/.exec(authHeader); + if (match.length != 2) + throw new Error("Couldn't parse auth header: " + authHeader); + + var userpass = base64ToString(match[1]); // no atob() :-( + match = /(.*):(.*)/.exec(userpass); + if (match.length != 3) + throw new Error("Couldn't decode auth header: " + userpass); + actual_user = match[1]; + actual_pass = match[2]; + } + + var proxy_actual_user = "", proxy_actual_pass = ""; + if (request.hasHeader("Proxy-Authorization")) { + authHeader = request.getHeader("Proxy-Authorization"); + match = /Basic (.+)/.exec(authHeader); + if (match.length != 2) + throw new Error("Couldn't parse auth header: " + authHeader); + + var userpass = base64ToString(match[1]); // no atob() :-( + match = /(.*):(.*)/.exec(userpass); + if (match.length != 3) + throw new Error("Couldn't decode auth header: " + userpass); + proxy_actual_user = match[1]; + proxy_actual_pass = match[2]; + } + + // Don't request authentication if the credentials we got were what we + // expected. + if (expected_user == actual_user && + expected_pass == actual_pass) { + requestAuth = false; + } + if (proxy_expected_user == proxy_actual_user && + proxy_expected_pass == proxy_actual_pass) { + requestProxyAuth = false; + } + + if (anonymous) { + if (authPresent) { + response.setStatusLine("1.0", 400, "Unexpected authorization header found"); + } else { + response.setStatusLine("1.0", 200, "Authorization header not found"); + } + } else { + if (requestProxyAuth) { + response.setStatusLine("1.0", 407, "Proxy authentication required"); + for (i = 0; i < authHeaderCount; ++i) + response.setHeader("Proxy-Authenticate", "basic realm=\"" + proxy_realm + "\"", true); + } else if (requestAuth) { + if (formauth && authPresent) + response.setStatusLine("1.0", 403, "Form authentication required"); + else + response.setStatusLine("1.0", 401, "Authentication required"); + for (i = 0; i < authHeaderCount; ++i) + response.setHeader("WWW-Authenticate", "basic realm=\"" + realm + "\"", true); + } else { + response.setStatusLine("1.0", 200, "OK"); + } + } + + response.setHeader("Content-Type", "application/xhtml+xml", false); + response.write(""); + response.write("

Login: " + (requestAuth ? "FAIL" : "PASS") + "

\n"); + response.write("

Proxy: " + (requestProxyAuth ? "FAIL" : "PASS") + "

\n"); + response.write("

Auth: " + authHeader + "

\n"); + response.write("

User: " + actual_user + "

\n"); + response.write("

Pass: " + actual_pass + "

\n"); + + if (huge) { + response.write("
"); + for (i = 0; i < 100000; i++) { + response.write("123456789\n"); + } + response.write("
"); + response.write("This is a footnote after the huge content fill"); + } + + if (plugin) { + response.write("\n"); + } + + response.write(""); +} + + +// base64 decoder +// +// Yoinked from extensions/xml-rpc/src/nsXmlRpcClient.js because btoa() +// doesn't seem to exist. :-( +/* Convert Base64 data to a string */ +const toBinaryTable = [ + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63, + 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, + 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1, + -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, + 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1 +]; +const base64Pad = '='; + +function base64ToString(data) { + + var result = ''; + var leftbits = 0; // number of bits decoded, but yet to be appended + var leftdata = 0; // bits decoded, but yet to be appended + + // Convert one by one. + for (var i = 0; i < data.length; i++) { + var c = toBinaryTable[data.charCodeAt(i) & 0x7f]; + var padding = (data[i] == base64Pad); + // Skip illegal characters and whitespace + if (c == -1) continue; + + // Collect data into leftdata, update bitcount + leftdata = (leftdata << 6) | c; + leftbits += 6; + + // If we have 8 or more bits, append 8 bits to the result + if (leftbits >= 8) { + leftbits -= 8; + // Append if not padding. + if (!padding) + result += String.fromCharCode((leftdata >> leftbits) & 0xff); + leftdata &= (1 << leftbits) - 1; + } + } + + // If there are any bits left, the base64 string was corrupted + if (leftbits) + throw Components.Exception('Corrupted base64 string'); + + return result; +} diff --git a/toolkit/components/passwordmgr/test/blank.html b/toolkit/components/passwordmgr/test/blank.html new file mode 100644 index 000000000..81ddc2235 --- /dev/null +++ b/toolkit/components/passwordmgr/test/blank.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/toolkit/components/passwordmgr/test/browser/.eslintrc.js b/toolkit/components/passwordmgr/test/browser/.eslintrc.js new file mode 100644 index 000000000..7c8021192 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/mochitest/browser.eslintrc.js" + ] +}; diff --git a/toolkit/components/passwordmgr/test/browser/authenticate.sjs b/toolkit/components/passwordmgr/test/browser/authenticate.sjs new file mode 100644 index 000000000..fe2d2423c --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/authenticate.sjs @@ -0,0 +1,110 @@ +function handleRequest(request, response) +{ + var match; + var requestAuth = true; + + // Allow the caller to drive how authentication is processed via the query. + // Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar + // The extra ? allows the user/pass/realm checks to succeed if the name is + // at the beginning of the query string. + var query = "?" + request.queryString; + + var expected_user = "test", expected_pass = "testpass", realm = "mochitest"; + + // Look for an authentication header, if any, in the request. + // + // EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== + // + // This test only supports Basic auth. The value sent by the client is + // "username:password", obscured with base64 encoding. + + var actual_user = "", actual_pass = "", authHeader, authPresent = false; + if (request.hasHeader("Authorization")) { + authPresent = true; + authHeader = request.getHeader("Authorization"); + match = /Basic (.+)/.exec(authHeader); + if (match.length != 2) + throw new Error("Couldn't parse auth header: " + authHeader); + + var userpass = base64ToString(match[1]); // no atob() :-( + match = /(.*):(.*)/.exec(userpass); + if (match.length != 3) + throw new Error("Couldn't decode auth header: " + userpass); + actual_user = match[1]; + actual_pass = match[2]; + } + + // Don't request authentication if the credentials we got were what we + // expected. + if (expected_user == actual_user && + expected_pass == actual_pass) { + requestAuth = false; + } + + if (requestAuth) { + response.setStatusLine("1.0", 401, "Authentication required"); + response.setHeader("WWW-Authenticate", "basic realm=\"" + realm + "\"", true); + } else { + response.setStatusLine("1.0", 200, "OK"); + } + + response.setHeader("Content-Type", "application/xhtml+xml", false); + response.write(""); + response.write("

Login: " + (requestAuth ? "FAIL" : "PASS") + "

\n"); + response.write("

Auth: " + authHeader + "

\n"); + response.write("

User: " + actual_user + "

\n"); + response.write("

Pass: " + actual_pass + "

\n"); + response.write(""); +} + + +// base64 decoder +// +// Yoinked from extensions/xml-rpc/src/nsXmlRpcClient.js because btoa() +// doesn't seem to exist. :-( +/* Convert Base64 data to a string */ +const toBinaryTable = [ + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63, + 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, + 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1, + -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, + 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1 +]; +const base64Pad = '='; + +function base64ToString(data) { + + var result = ''; + var leftbits = 0; // number of bits decoded, but yet to be appended + var leftdata = 0; // bits decoded, but yet to be appended + + // Convert one by one. + for (var i = 0; i < data.length; i++) { + var c = toBinaryTable[data.charCodeAt(i) & 0x7f]; + var padding = (data[i] == base64Pad); + // Skip illegal characters and whitespace + if (c == -1) continue; + + // Collect data into leftdata, update bitcount + leftdata = (leftdata << 6) | c; + leftbits += 6; + + // If we have 8 or more bits, append 8 bits to the result + if (leftbits >= 8) { + leftbits -= 8; + // Append if not padding. + if (!padding) + result += String.fromCharCode((leftdata >> leftbits) & 0xff); + leftdata &= (1 << leftbits) - 1; + } + } + + // If there are any bits left, the base64 string was corrupted + if (leftbits) + throw Components.Exception('Corrupted base64 string'); + + return result; +} diff --git a/toolkit/components/passwordmgr/test/browser/browser.ini b/toolkit/components/passwordmgr/test/browser/browser.ini new file mode 100644 index 000000000..b17591436 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser.ini @@ -0,0 +1,72 @@ +[DEFAULT] +support-files = + ../formsubmit.sjs + authenticate.sjs + form_basic.html + form_basic_iframe.html + formless_basic.html + form_same_origin_action.html + form_cross_origin_secure_action.html + head.js + insecure_test.html + insecure_test_subframe.html + multiple_forms.html + streamConverter_content.sjs + +[browser_autocomplete_insecure_warning.js] +support-files = + form_cross_origin_insecure_action.html +[browser_capture_doorhanger.js] +support-files = + subtst_notifications_1.html + subtst_notifications_2.html + subtst_notifications_2pw_0un.html + subtst_notifications_2pw_1un_1text.html + subtst_notifications_3.html + subtst_notifications_4.html + subtst_notifications_5.html + subtst_notifications_6.html + subtst_notifications_8.html + subtst_notifications_9.html + subtst_notifications_10.html + subtst_notifications_change_p.html +[browser_capture_doorhanger_httpsUpgrade.js] +support-files = + subtst_notifications_1.html + subtst_notifications_8.html +[browser_capture_doorhanger_window_open.js] +support-files = + subtst_notifications_11.html + subtst_notifications_11_popup.html +skip-if = os == "linux" # Bug 1312981, bug 1313136 +[browser_context_menu_autocomplete_interaction.js] +[browser_username_select_dialog.js] +support-files = + subtst_notifications_change_p.html +[browser_DOMFormHasPassword.js] +[browser_DOMInputPasswordAdded.js] +[browser_exceptions_dialog.js] +[browser_formless_submit_chrome.js] +[browser_hasInsecureLoginForms.js] +[browser_hasInsecureLoginForms_streamConverter.js] +[browser_http_autofill.js] +[browser_insecurePasswordConsoleWarning.js] +support-files = + form_cross_origin_insecure_action.html +[browser_master_password_autocomplete.js] +[browser_notifications.js] +[browser_notifications_username.js] +[browser_notifications_password.js] +[browser_notifications_2.js] +skip-if = os == "linux" # Bug 1272849 Main action button disabled state intermittent +[browser_passwordmgr_editing.js] +skip-if = os == "linux" +[browser_context_menu.js] +[browser_context_menu_iframe.js] +[browser_passwordmgr_contextmenu.js] +subsuite = clipboard +[browser_passwordmgr_fields.js] +[browser_passwordmgr_observers.js] +[browser_passwordmgr_sort.js] +[browser_passwordmgr_switchtab.js] +[browser_passwordmgrdlg.js] diff --git a/toolkit/components/passwordmgr/test/browser/browser_DOMFormHasPassword.js b/toolkit/components/passwordmgr/test/browser/browser_DOMFormHasPassword.js new file mode 100644 index 000000000..80a0dd903 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_DOMFormHasPassword.js @@ -0,0 +1,94 @@ +const ids = { + INPUT_ID: "input1", + FORM1_ID: "form1", + FORM2_ID: "form2", + CHANGE_INPUT_ID: "input2", +}; + +function task(contentIds) { + let resolve; + let promise = new Promise(r => { resolve = r; }); + + function unexpectedContentEvent(evt) { + ok(false, "Received a " + evt.type + " event on content"); + } + + var gDoc = null; + + addEventListener("load", tabLoad, true); + + function tabLoad() { + if (content.location.href == "about:blank") + return; + removeEventListener("load", tabLoad, true); + + gDoc = content.document; + gDoc.addEventListener("DOMFormHasPassword", unexpectedContentEvent, false); + gDoc.defaultView.setTimeout(test_inputAdd, 0); + } + + function test_inputAdd() { + addEventListener("DOMFormHasPassword", test_inputAddHandler, false); + let input = gDoc.createElementNS("http://www.w3.org/1999/xhtml", "input"); + input.setAttribute("type", "password"); + input.setAttribute("id", contentIds.INPUT_ID); + input.setAttribute("data-test", "unique-attribute"); + gDoc.getElementById(contentIds.FORM1_ID).appendChild(input); + } + + function test_inputAddHandler(evt) { + removeEventListener(evt.type, test_inputAddHandler, false); + is(evt.target.id, contentIds.FORM1_ID, + evt.type + " event targets correct form element (added password element)"); + gDoc.defaultView.setTimeout(test_inputChangeForm, 0); + } + + function test_inputChangeForm() { + addEventListener("DOMFormHasPassword", test_inputChangeFormHandler, false); + let input = gDoc.getElementById(contentIds.INPUT_ID); + input.setAttribute("form", contentIds.FORM2_ID); + } + + function test_inputChangeFormHandler(evt) { + removeEventListener(evt.type, test_inputChangeFormHandler, false); + is(evt.target.id, contentIds.FORM2_ID, + evt.type + " event targets correct form element (changed form)"); + gDoc.defaultView.setTimeout(test_inputChangesType, 0); + } + + function test_inputChangesType() { + addEventListener("DOMFormHasPassword", test_inputChangesTypeHandler, false); + let input = gDoc.getElementById(contentIds.CHANGE_INPUT_ID); + input.setAttribute("type", "password"); + } + + function test_inputChangesTypeHandler(evt) { + removeEventListener(evt.type, test_inputChangesTypeHandler, false); + is(evt.target.id, contentIds.FORM1_ID, + evt.type + " event targets correct form element (changed type)"); + gDoc.defaultView.setTimeout(finish, 0); + } + + function finish() { + gDoc.removeEventListener("DOMFormHasPassword", unexpectedContentEvent, false); + resolve(); + } + + return promise; +} + +add_task(function* () { + let tab = gBrowser.selectedTab = gBrowser.addTab(); + + let promise = ContentTask.spawn(tab.linkedBrowser, ids, task); + tab.linkedBrowser.loadURI("data:text/html;charset=utf-8," + + "" + + "
" + + "
" + + "
" + + ""); + yield promise; + + ok(true, "Test completed"); + gBrowser.removeCurrentTab(); +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_DOMInputPasswordAdded.js b/toolkit/components/passwordmgr/test/browser/browser_DOMInputPasswordAdded.js new file mode 100644 index 000000000..f54892e19 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_DOMInputPasswordAdded.js @@ -0,0 +1,99 @@ +const consts = { + HTML_NS: "http://www.w3.org/1999/xhtml", + + INPUT_ID: "input1", + FORM1_ID: "form1", + FORM2_ID: "form2", + CHANGE_INPUT_ID: "input2", + BODY_INPUT_ID: "input3", +}; + +function task(contentConsts) { + let resolve; + let promise = new Promise(r => { resolve = r; }); + + function unexpectedContentEvent(evt) { + Assert.ok(false, "Received a " + evt.type + " event on content"); + } + + var gDoc = null; + + addEventListener("load", tabLoad, true); + + function tabLoad() { + removeEventListener("load", tabLoad, true); + gDoc = content.document; + // These events shouldn't escape to content. + gDoc.addEventListener("DOMInputPasswordAdded", unexpectedContentEvent, false); + gDoc.defaultView.setTimeout(test_inputAdd, 0); + } + + function test_inputAdd() { + addEventListener("DOMInputPasswordAdded", test_inputAddHandler, false); + let input = gDoc.createElementNS(contentConsts.HTML_NS, "input"); + input.setAttribute("type", "password"); + input.setAttribute("id", contentConsts.INPUT_ID); + input.setAttribute("data-test", "unique-attribute"); + gDoc.getElementById(contentConsts.FORM1_ID).appendChild(input); + info("Done appending the input element"); + } + + function test_inputAddHandler(evt) { + removeEventListener(evt.type, test_inputAddHandler, false); + Assert.equal(evt.target.id, contentConsts.INPUT_ID, + evt.type + " event targets correct input element (added password element)"); + gDoc.defaultView.setTimeout(test_inputAddOutsideForm, 0); + } + + function test_inputAddOutsideForm() { + addEventListener("DOMInputPasswordAdded", test_inputAddOutsideFormHandler, false); + let input = gDoc.createElementNS(contentConsts.HTML_NS, "input"); + input.setAttribute("type", "password"); + input.setAttribute("id", contentConsts.BODY_INPUT_ID); + input.setAttribute("data-test", "unique-attribute"); + gDoc.body.appendChild(input); + info("Done appending the input element to the body"); + } + + function test_inputAddOutsideFormHandler(evt) { + removeEventListener(evt.type, test_inputAddOutsideFormHandler, false); + Assert.equal(evt.target.id, contentConsts.BODY_INPUT_ID, + evt.type + " event targets correct input element (added password element outside form)"); + gDoc.defaultView.setTimeout(test_inputChangesType, 0); + } + + function test_inputChangesType() { + addEventListener("DOMInputPasswordAdded", test_inputChangesTypeHandler, false); + let input = gDoc.getElementById(contentConsts.CHANGE_INPUT_ID); + input.setAttribute("type", "password"); + } + + function test_inputChangesTypeHandler(evt) { + removeEventListener(evt.type, test_inputChangesTypeHandler, false); + Assert.equal(evt.target.id, contentConsts.CHANGE_INPUT_ID, + evt.type + " event targets correct input element (changed type)"); + gDoc.defaultView.setTimeout(completeTest, 0); + } + + function completeTest() { + Assert.ok(true, "Test completed"); + gDoc.removeEventListener("DOMInputPasswordAdded", unexpectedContentEvent, false); + resolve(); + } + + return promise; +} + +add_task(function* () { + let tab = gBrowser.selectedTab = gBrowser.addTab(); + let promise = ContentTask.spawn(tab.linkedBrowser, consts, task); + tab.linkedBrowser.loadURI("data:text/html;charset=utf-8," + + "" + + "
" + + "
" + + "
" + + ""); + yield promise; + gBrowser.removeCurrentTab(); +}); + diff --git a/toolkit/components/passwordmgr/test/browser/browser_autocomplete_insecure_warning.js b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_insecure_warning.js new file mode 100644 index 000000000..6aa8e5cf7 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_insecure_warning.js @@ -0,0 +1,41 @@ +"use strict"; + +const EXPECTED_SUPPORT_URL = Services.urlFormatter.formatURLPref("app.support.baseURL") + + "insecure-password"; + +add_task(function* test_clickInsecureFieldWarning() { + let url = "https://example.com" + DIRECTORY_PATH + "form_cross_origin_insecure_action.html"; + + yield BrowserTestUtils.withNewTab({ + gBrowser, + url, + }, function*(browser) { + let popup = document.getElementById("PopupAutoComplete"); + ok(popup, "Got popup"); + + let promiseShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + + yield SimpleTest.promiseFocus(browser); + info("content window focused"); + + // Focus the username field to open the popup. + yield ContentTask.spawn(browser, null, function openAutocomplete() { + content.document.getElementById("form-basic-username").focus(); + }); + + yield promiseShown; + ok(promiseShown, "autocomplete shown"); + + let warningItem = document.getAnonymousElementByAttribute(popup, "type", "insecureWarning"); + ok(warningItem, "Got warning richlistitem"); + + yield BrowserTestUtils.waitForCondition(() => !warningItem.collapsed, "Wait for warning to show"); + + info("Clicking on warning"); + let supportTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, EXPECTED_SUPPORT_URL); + EventUtils.synthesizeMouseAtCenter(warningItem, {}); + let supportTab = yield supportTabPromise; + ok(supportTab, "Support tab opened"); + yield BrowserTestUtils.removeTab(supportTab); + }); +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js new file mode 100644 index 000000000..b6bfdbf50 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js @@ -0,0 +1,600 @@ +/* + * Test capture popup notifications + */ + +const BRAND_BUNDLE = Services.strings.createBundle("chrome://branding/locale/brand.properties"); +const BRAND_SHORT_NAME = BRAND_BUNDLE.GetStringFromName("brandShortName"); + +let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, "init"); +let login1 = new nsLoginInfo("http://example.com", "http://example.com", null, + "notifyu1", "notifyp1", "user", "pass"); +let login2 = new nsLoginInfo("http://example.com", "http://example.com", null, + "", "notifyp1", "", "pass"); +let login1B = new nsLoginInfo("http://example.com", "http://example.com", null, + "notifyu1B", "notifyp1B", "user", "pass"); +let login2B = new nsLoginInfo("http://example.com", "http://example.com", null, + "", "notifyp1B", "", "pass"); + +requestLongerTimeout(2); + +add_task(function* setup() { + // Load recipes for this test. + let recipeParent = yield LoginManagerParent.recipeParentPromise; + yield recipeParent.load({ + siteRecipes: [{ + hosts: ["example.org"], + usernameSelector: "#user", + passwordSelector: "#pass", + }], + }); +}); + +add_task(function* test_remember_opens() { + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "got notification popup"); + notif.remove(); + }); +}); + +add_task(function* test_clickNever() { + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "got notification popup"); + is(true, Services.logins.getLoginSavingEnabled("http://example.com"), + "Checking for login saving enabled"); + + yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1"); + clickDoorhangerButton(notif, NEVER_BUTTON); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); + + info("Make sure Never took effect"); + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(!notif, "checking for no notification popup"); + is(false, Services.logins.getLoginSavingEnabled("http://example.com"), + "Checking for login saving disabled"); + Services.logins.setLoginSavingEnabled("http://example.com", true); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); +}); + +add_task(function* test_clickRemember() { + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "got notification popup"); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); + + yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1"); + clickDoorhangerButton(notif, REMEMBER_BUTTON); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username used on the new entry"); + is(login.password, "notifyp1", "Check the password used on the new entry"); + is(login.timesUsed, 1, "Check times used on new entry"); + + info("Make sure Remember took effect and we don't prompt for an existing login"); + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(!notif, "checking for no notification popup"); + }); + + logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username used"); + is(login.password, "notifyp1", "Check the password used"); + is(login.timesUsed, 2, "Check times used incremented"); + + checkOnlyLoginWasUsedTwice({ justChanged: false }); + + // remove that login + Services.logins.removeLogin(login1); +}); + +/* signons.rememberSignons pref tests... */ + +add_task(function* test_rememberSignonsFalse() { + info("Make sure we don't prompt with rememberSignons=false"); + Services.prefs.setBoolPref("signon.rememberSignons", false); + + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(!notif, "checking for no notification popup"); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); +}); + +add_task(function* test_rememberSignonsTrue() { + info("Make sure we prompt with rememberSignons=true"); + Services.prefs.setBoolPref("signon.rememberSignons", true); + + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "got notification popup"); + notif.remove(); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); +}); + +/* autocomplete=off tests... */ + +add_task(function* test_autocompleteOffUsername() { + info("Check for notification popup when autocomplete=off present on username"); + + yield testSubmittingLoginForm("subtst_notifications_2.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "checking for notification popup"); + notif.remove(); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); +}); + +add_task(function* test_autocompleteOffPassword() { + info("Check for notification popup when autocomplete=off present on password"); + + yield testSubmittingLoginForm("subtst_notifications_3.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "checking for notification popup"); + notif.remove(); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); +}); + +add_task(function* test_autocompleteOffForm() { + info("Check for notification popup when autocomplete=off present on form"); + + yield testSubmittingLoginForm("subtst_notifications_4.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "checking for notification popup"); + notif.remove(); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); +}); + + +add_task(function* test_noPasswordField() { + info("Check for no notification popup when no password field present"); + + yield testSubmittingLoginForm("subtst_notifications_5.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "null", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(!notif, "checking for no notification popup"); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); +}); + +add_task(function* test_pwOnlyLoginMatchesForm() { + info("Check for update popup when existing pw-only login matches form."); + Services.logins.addLogin(login2); + + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-change"); + ok(notif, "checking for notification popup"); + notif.remove(); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "", "Check the username"); + is(login.password, "notifyp1", "Check the password"); + is(login.timesUsed, 1, "Check times used"); + + Services.logins.removeLogin(login2); +}); + +add_task(function* test_pwOnlyFormMatchesLogin() { + info("Check for no notification popup when pw-only form matches existing login."); + Services.logins.addLogin(login1); + + yield testSubmittingLoginForm("subtst_notifications_6.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(!notif, "checking for no notification popup"); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username"); + is(login.password, "notifyp1", "Check the password"); + is(login.timesUsed, 2, "Check times used"); + + Services.logins.removeLogin(login1); +}); + +add_task(function* test_pwOnlyFormDoesntMatchExisting() { + info("Check for notification popup when pw-only form doesn't match existing login."); + Services.logins.addLogin(login1B); + + yield testSubmittingLoginForm("subtst_notifications_6.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "got notification popup"); + notif.remove(); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1B", "Check the username unchanged"); + is(login.password, "notifyp1B", "Check the password unchanged"); + is(login.timesUsed, 1, "Check times used"); + + Services.logins.removeLogin(login1B); +}); + +add_task(function* test_changeUPLoginOnUPForm_dont() { + info("Check for change-password popup, u+p login on u+p form. (not changed)"); + Services.logins.addLogin(login1); + + yield testSubmittingLoginForm("subtst_notifications_8.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "pass2", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-change"); + ok(notif, "got notification popup"); + + yield* checkDoorhangerUsernamePassword("notifyu1", "pass2"); + clickDoorhangerButton(notif, DONT_CHANGE_BUTTON); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username unchanged"); + is(login.password, "notifyp1", "Check the password unchanged"); + is(login.timesUsed, 1, "Check times used"); + + Services.logins.removeLogin(login1); +}); + +add_task(function* test_changeUPLoginOnUPForm_change() { + info("Check for change-password popup, u+p login on u+p form."); + Services.logins.addLogin(login1); + + yield testSubmittingLoginForm("subtst_notifications_8.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "pass2", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-change"); + ok(notif, "got notification popup"); + + yield* checkDoorhangerUsernamePassword("notifyu1", "pass2"); + clickDoorhangerButton(notif, CHANGE_BUTTON); + + ok(!getCaptureDoorhanger("password-change"), "popup should be gone"); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username unchanged"); + is(login.password, "pass2", "Check the password changed"); + is(login.timesUsed, 2, "Check times used"); + + checkOnlyLoginWasUsedTwice({ justChanged: true }); + + // cleanup + login1.password = "pass2"; + Services.logins.removeLogin(login1); + login1.password = "notifyp1"; +}); + +add_task(function* test_changePLoginOnUPForm() { + info("Check for change-password popup, p-only login on u+p form."); + Services.logins.addLogin(login2); + + yield testSubmittingLoginForm("subtst_notifications_9.html", function*(fieldValues) { + is(fieldValues.username, "", "Checking submitted username"); + is(fieldValues.password, "pass2", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-change"); + ok(notif, "got notification popup"); + + yield* checkDoorhangerUsernamePassword("", "pass2"); + clickDoorhangerButton(notif, CHANGE_BUTTON); + + ok(!getCaptureDoorhanger("password-change"), "popup should be gone"); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "", "Check the username unchanged"); + is(login.password, "pass2", "Check the password changed"); + is(login.timesUsed, 2, "Check times used"); + + // no cleanup -- saved password to be used in the next test. +}); + +add_task(function* test_changePLoginOnPForm() { + info("Check for change-password popup, p-only login on p-only form."); + + yield testSubmittingLoginForm("subtst_notifications_10.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-change"); + ok(notif, "got notification popup"); + + yield* checkDoorhangerUsernamePassword("", "notifyp1"); + clickDoorhangerButton(notif, CHANGE_BUTTON); + + ok(!getCaptureDoorhanger("password-change"), "popup should be gone"); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "", "Check the username unchanged"); + is(login.password, "notifyp1", "Check the password changed"); + is(login.timesUsed, 3, "Check times used"); + + Services.logins.removeLogin(login2); +}); + +add_task(function* test_checkUPSaveText() { + info("Check text on a user+pass notification popup"); + + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "got notification popup"); + // Check the text, which comes from the localized saveLoginText string. + let notificationText = notif.message; + let expectedText = "Would you like " + BRAND_SHORT_NAME + " to remember this login?"; + is(expectedText, notificationText, "Checking text: " + notificationText); + notif.remove(); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); +}); + +add_task(function* test_checkPSaveText() { + info("Check text on a pass-only notification popup"); + + yield testSubmittingLoginForm("subtst_notifications_6.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "got notification popup"); + // Check the text, which comes from the localized saveLoginTextNoUser string. + let notificationText = notif.message; + let expectedText = "Would you like " + BRAND_SHORT_NAME + " to remember this password?"; + is(expectedText, notificationText, "Checking text: " + notificationText); + notif.remove(); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); +}); + +add_task(function* test_capture2pw0un() { + info("Check for notification popup when a form with 2 password fields (no username) " + + "is submitted and there are no saved logins."); + + yield testSubmittingLoginForm("subtst_notifications_2pw_0un.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "got notification popup"); + notif.remove(); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); +}); + +add_task(function* test_change2pw0unExistingDifferentUP() { + info("Check for notification popup when a form with 2 password fields (no username) " + + "is submitted and there is a saved login with a username and different password."); + + Services.logins.addLogin(login1B); + + yield testSubmittingLoginForm("subtst_notifications_2pw_0un.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-change"); + ok(notif, "got notification popup"); + notif.remove(); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1B", "Check the username unchanged"); + is(login.password, "notifyp1B", "Check the password unchanged"); + is(login.timesUsed, 1, "Check times used"); + + Services.logins.removeLogin(login1B); +}); + +add_task(function* test_change2pw0unExistingDifferentP() { + info("Check for notification popup when a form with 2 password fields (no username) " + + "is submitted and there is a saved login with no username and different password."); + + Services.logins.addLogin(login2B); + + yield testSubmittingLoginForm("subtst_notifications_2pw_0un.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-change"); + ok(notif, "got notification popup"); + notif.remove(); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "", "Check the username unchanged"); + is(login.password, "notifyp1B", "Check the password unchanged"); + is(login.timesUsed, 1, "Check times used"); + + Services.logins.removeLogin(login2B); +}); + +add_task(function* test_change2pw0unExistingWithSameP() { + info("Check for no notification popup when a form with 2 password fields (no username) " + + "is submitted and there is a saved login with a username and the same password."); + + Services.logins.addLogin(login2); + + yield testSubmittingLoginForm("subtst_notifications_2pw_0un.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-change"); + ok(!notif, "checking for no notification popup"); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "", "Check the username unchanged"); + is(login.password, "notifyp1", "Check the password unchanged"); + is(login.timesUsed, 2, "Check times used incremented"); + + checkOnlyLoginWasUsedTwice({ justChanged: false }); + + Services.logins.removeLogin(login2); +}); + +add_task(function* test_changeUPLoginOnPUpdateForm() { + info("Check for change-password popup, u+p login on password update form."); + Services.logins.addLogin(login1); + + yield testSubmittingLoginForm("subtst_notifications_change_p.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "pass2", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-change"); + ok(notif, "got notification popup"); + + yield* checkDoorhangerUsernamePassword("notifyu1", "pass2"); + clickDoorhangerButton(notif, CHANGE_BUTTON); + + ok(!getCaptureDoorhanger("password-change"), "popup should be gone"); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username unchanged"); + is(login.password, "pass2", "Check the password changed"); + is(login.timesUsed, 2, "Check times used"); + + checkOnlyLoginWasUsedTwice({ justChanged: true }); + + // cleanup + login1.password = "pass2"; + Services.logins.removeLogin(login1); + login1.password = "notifyp1"; +}); + +add_task(function* test_recipeCaptureFields_NewLogin() { + info("Check that we capture the proper fields when a field recipe is in use."); + + yield testSubmittingLoginForm("subtst_notifications_2pw_1un_1text.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "got notification popup"); + + // Sanity check, no logins should exist yet. + let logins = Services.logins.getAllLogins(); + is(logins.length, 0, "Should not have any logins yet"); + + yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1"); + clickDoorhangerButton(notif, REMEMBER_BUTTON); + + }, "http://example.org"); // The recipe is for example.org + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username unchanged"); + is(login.password, "notifyp1", "Check the password unchanged"); + is(login.timesUsed, 1, "Check times used"); +}); + +add_task(function* test_recipeCaptureFields_ExistingLogin() { + info("Check that we capture the proper fields when a field recipe is in use " + + "and there is a matching login"); + + yield testSubmittingLoginForm("subtst_notifications_2pw_1un_1text.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(!notif, "checking for no notification popup"); + }, "http://example.org"); + + checkOnlyLoginWasUsedTwice({ justChanged: false }); + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username unchanged"); + is(login.password, "notifyp1", "Check the password unchanged"); + is(login.timesUsed, 2, "Check times used incremented"); + + Services.logins.removeAllLogins(); +}); + +add_task(function* test_noShowPasswordOnDismissal() { + info("Check for no Show Password field when the doorhanger is dismissed"); + + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + info("Opening popup"); + let notif = getCaptureDoorhanger("password-save"); + let { panel } = PopupNotifications; + + info("Hiding popup."); + let promiseHidden = BrowserTestUtils.waitForEvent(panel, "popuphidden"); + panel.hidePopup(); + yield promiseHidden; + + info("Clicking on anchor to reshow popup."); + let promiseShown = BrowserTestUtils.waitForEvent(panel, "popupshown"); + notif.anchorElement.click(); + yield promiseShown; + + let passwordVisiblityToggle = panel.querySelector("#password-notification-visibilityToggle"); + is(passwordVisiblityToggle.hidden, true, "Check that the Show Password field is Hidden"); + }); +}); + +// TODO: +// * existing login test, form has different password --> change password, no save prompt diff --git a/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_httpsUpgrade.js b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_httpsUpgrade.js new file mode 100644 index 000000000..9be0aa631 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_httpsUpgrade.js @@ -0,0 +1,123 @@ +/* + * Test capture popup notifications with HTTPS upgrades + */ + +let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, "init"); +let login1 = new nsLoginInfo("http://example.com", "http://example.com", null, + "notifyu1", "notifyp1", "user", "pass"); +let login1HTTPS = new nsLoginInfo("https://example.com", "https://example.com", null, + "notifyu1", "notifyp1", "user", "pass"); + +add_task(function* test_httpsUpgradeCaptureFields_noChange() { + info("Check that we don't prompt to remember when capturing an upgraded login with no change"); + Services.logins.addLogin(login1); + // Sanity check the HTTP login exists. + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should have the HTTP login"); + + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(!notif, "checking for no notification popup"); + }, "https://example.com"); // This is HTTPS whereas the saved login is HTTP + + logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login still"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.hostname, "http://example.com", "Check the hostname is unchanged"); + is(login.username, "notifyu1", "Check the username is unchanged"); + is(login.password, "notifyp1", "Check the password is unchanged"); + is(login.timesUsed, 2, "Check times used increased"); + + Services.logins.removeLogin(login1); +}); + +add_task(function* test_httpsUpgradeCaptureFields_changePW() { + info("Check that we prompt to change when capturing an upgraded login with a new PW"); + Services.logins.addLogin(login1); + // Sanity check the HTTP login exists. + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should have the HTTP login"); + + yield testSubmittingLoginForm("subtst_notifications_8.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "pass2", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-change"); + ok(notif, "checking for a change popup"); + + yield* checkDoorhangerUsernamePassword("notifyu1", "pass2"); + clickDoorhangerButton(notif, CHANGE_BUTTON); + + ok(!getCaptureDoorhanger("password-change"), "popup should be gone"); + }, "https://example.com"); // This is HTTPS whereas the saved login is HTTP + + checkOnlyLoginWasUsedTwice({ justChanged: true }); + logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login still"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.hostname, "https://example.com", "Check the hostname is upgraded"); + is(login.formSubmitURL, "https://example.com", "Check the formSubmitURL is upgraded"); + is(login.username, "notifyu1", "Check the username is unchanged"); + is(login.password, "pass2", "Check the password changed"); + is(login.timesUsed, 2, "Check times used increased"); + + Services.logins.removeAllLogins(); +}); + +add_task(function* test_httpsUpgradeCaptureFields_captureMatchingHTTP() { + info("Capture a new HTTP login which matches a stored HTTPS one."); + Services.logins.addLogin(login1HTTPS); + + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "got notification popup"); + + is(Services.logins.getAllLogins().length, 1, "Should only have the HTTPS login"); + + yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1"); + clickDoorhangerButton(notif, REMEMBER_BUTTON); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 2, "Should have both HTTP and HTTPS logins"); + for (let login of logins) { + login = login.QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username used on the new entry"); + is(login.password, "notifyp1", "Check the password used on the new entry"); + is(login.timesUsed, 1, "Check times used on entry"); + } + + info("Make sure Remember took effect and we don't prompt for an existing HTTP login"); + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(!notif, "checking for no notification popup"); + }); + + logins = Services.logins.getAllLogins(); + is(logins.length, 2, "Should have both HTTP and HTTPS still"); + + let httpsLogins = LoginHelper.searchLoginsWithObject({ + hostname: "https://example.com", + }); + is(httpsLogins.length, 1, "Check https logins count"); + let httpsLogin = httpsLogins[0].QueryInterface(Ci.nsILoginMetaInfo); + ok(httpsLogin.equals(login1HTTPS), "Check HTTPS login didn't change"); + is(httpsLogin.timesUsed, 1, "Check times used"); + + let httpLogins = LoginHelper.searchLoginsWithObject({ + hostname: "http://example.com", + }); + is(httpLogins.length, 1, "Check http logins count"); + let httpLogin = httpLogins[0].QueryInterface(Ci.nsILoginMetaInfo); + ok(httpLogin.equals(login1), "Check HTTP login is as expected"); + is(httpLogin.timesUsed, 2, "Check times used increased"); + + Services.logins.removeLogin(login1); + Services.logins.removeLogin(login1HTTPS); +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_window_open.js b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_window_open.js new file mode 100644 index 000000000..1bcfec5eb --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_window_open.js @@ -0,0 +1,144 @@ +/* + * Test capture popup notifications in content opened by window.open + */ + +let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, "init"); +let login1 = new nsLoginInfo("http://mochi.test:8888", "http://mochi.test:8888", null, + "notifyu1", "notifyp1", "user", "pass"); +let login2 = new nsLoginInfo("http://mochi.test:8888", "http://mochi.test:8888", null, + "notifyu2", "notifyp2", "user", "pass"); + + +function withTestTabUntilStorageChange(aPageFile, aTaskFn) { + function storageChangedObserved(subject, data) { + // Watch for actions triggered from a doorhanger (not cleanup tasks with removeLogin) + if (data == "removeLogin") { + return false; + } + return true; + } + + let storageChangedPromised = TestUtils.topicObserved("passwordmgr-storage-changed", + storageChangedObserved); + return BrowserTestUtils.withNewTab({ + gBrowser, + url: "http://mochi.test:8888" + DIRECTORY_PATH + aPageFile, + }, function*(browser) { + ok(true, "loaded " + aPageFile); + info("running test case task"); + yield* aTaskFn(); + info("waiting for storage change"); + yield storageChangedPromised; + }); +} + +add_task(function* setup() { + yield SimpleTest.promiseFocus(window); +}); + +add_task(function* test_saveChromeHiddenAutoClose() { + let notifShownPromise = BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown"); + // query arguments are: username, password, features, auto-close (delimited by '|') + let url = "subtst_notifications_11.html?notifyu1|notifyp1|" + + "menubar=no,toolbar=no,location=no|autoclose"; + yield withTestTabUntilStorageChange(url, function*() { + info("waiting for popupshown"); + yield notifShownPromise; + // the popup closes and the doorhanger should appear in the opener + let popup = getCaptureDoorhanger("password-save"); + ok(popup, "got notification popup"); + yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1"); + // Sanity check, no logins should exist yet. + let logins = Services.logins.getAllLogins(); + is(logins.length, 0, "Should not have any logins yet"); + + clickDoorhangerButton(popup, REMEMBER_BUTTON); + }); + // Check result of clicking Remember + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.timesUsed, 1, "Check times used on new entry"); + is(login.username, "notifyu1", "Check the username used on the new entry"); + is(login.password, "notifyp1", "Check the password used on the new entry"); +}); + +add_task(function* test_changeChromeHiddenAutoClose() { + let notifShownPromise = BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown"); + let url = "subtst_notifications_11.html?notifyu1|pass2|menubar=no,toolbar=no,location=no|autoclose"; + yield withTestTabUntilStorageChange(url, function*() { + info("waiting for popupshown"); + yield notifShownPromise; + let popup = getCaptureDoorhanger("password-change"); + ok(popup, "got notification popup"); + yield* checkDoorhangerUsernamePassword("notifyu1", "pass2"); + clickDoorhangerButton(popup, CHANGE_BUTTON); + }); + + // Check to make sure we updated the password, timestamps and use count for + // the login being changed with this form. + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username"); + is(login.password, "pass2", "Check password changed"); + is(login.timesUsed, 2, "check .timesUsed incremented on change"); + ok(login.timeCreated < login.timeLastUsed, "timeLastUsed bumped"); + ok(login.timeLastUsed == login.timePasswordChanged, "timeUsed == timeChanged"); + + login1.password = "pass2"; + Services.logins.removeLogin(login1); + login1.password = "notifyp1"; +}); + +add_task(function* test_saveChromeVisibleSameWindow() { + // This test actually opens a new tab in the same window with default browser settings. + let url = "subtst_notifications_11.html?notifyu2|notifyp2||"; + let notifShownPromise = BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown"); + yield withTestTabUntilStorageChange(url, function*() { + yield notifShownPromise; + let popup = getCaptureDoorhanger("password-save"); + ok(popup, "got notification popup"); + yield* checkDoorhangerUsernamePassword("notifyu2", "notifyp2"); + clickDoorhangerButton(popup, REMEMBER_BUTTON); + yield BrowserTestUtils.removeTab(gBrowser.selectedTab); + }); + + // Check result of clicking Remember + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login now"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu2", "Check the username used on the new entry"); + is(login.password, "notifyp2", "Check the password used on the new entry"); + is(login.timesUsed, 1, "Check times used on new entry"); +}); + +add_task(function* test_changeChromeVisibleSameWindow() { + let url = "subtst_notifications_11.html?notifyu2|pass2||"; + let notifShownPromise = BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown"); + yield withTestTabUntilStorageChange(url, function*() { + yield notifShownPromise; + let popup = getCaptureDoorhanger("password-change"); + ok(popup, "got notification popup"); + yield* checkDoorhangerUsernamePassword("notifyu2", "pass2"); + clickDoorhangerButton(popup, CHANGE_BUTTON); + yield BrowserTestUtils.removeTab(gBrowser.selectedTab); + }); + + // Check to make sure we updated the password, timestamps and use count for + // the login being changed with this form. + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu2", "Check the username"); + is(login.password, "pass2", "Check password changed"); + is(login.timesUsed, 2, "check .timesUsed incremented on change"); + ok(login.timeCreated < login.timeLastUsed, "timeLastUsed bumped"); + ok(login.timeLastUsed == login.timePasswordChanged, "timeUsed == timeChanged"); + + // cleanup + login2.password = "pass2"; + Services.logins.removeLogin(login2); + login2.password = "notifyp2"; +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_context_menu.js b/toolkit/components/passwordmgr/test/browser/browser_context_menu.js new file mode 100644 index 000000000..6cfcaa7c2 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_context_menu.js @@ -0,0 +1,432 @@ +/* + * Test the password manager context menu. + */ + +/* eslint no-shadow:"off" */ + +"use strict"; + +// The hostname for the test URIs. +const TEST_HOSTNAME = "https://example.com"; +const MULTIPLE_FORMS_PAGE_PATH = "/browser/toolkit/components/passwordmgr/test/browser/multiple_forms.html"; + +const CONTEXT_MENU = document.getElementById("contentAreaContextMenu"); +const POPUP_HEADER = document.getElementById("fill-login"); + +/** + * Initialize logins needed for the tests and disable autofill + * for login forms for easier testing of manual fill. + */ +add_task(function* test_initialize() { + Services.prefs.setBoolPref("signon.autofillForms", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("signon.autofillForms"); + Services.prefs.clearUserPref("signon.schemeUpgrades"); + }); + for (let login of loginList()) { + Services.logins.addLogin(login); + } +}); + +/** + * Check if the context menu is populated with the right + * menuitems for the target password input field. + */ +add_task(function* test_context_menu_populate_password_noSchemeUpgrades() { + Services.prefs.setBoolPref("signon.schemeUpgrades", false); + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH, + }, function* (browser) { + yield openPasswordContextMenu(browser, "#test-password-1"); + + // Check the content of the password manager popup + let popupMenu = document.getElementById("fill-login-popup"); + checkMenu(popupMenu, 2); + + CONTEXT_MENU.hidePopup(); + }); +}); + +/** + * Check if the context menu is populated with the right + * menuitems for the target password input field. + */ +add_task(function* test_context_menu_populate_password_schemeUpgrades() { + Services.prefs.setBoolPref("signon.schemeUpgrades", true); + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH, + }, function* (browser) { + yield openPasswordContextMenu(browser, "#test-password-1"); + + // Check the content of the password manager popup + let popupMenu = document.getElementById("fill-login-popup"); + checkMenu(popupMenu, 3); + + CONTEXT_MENU.hidePopup(); + }); +}); + +/** + * Check if the context menu is populated with the right menuitems + * for the target username field with a password field present. + */ +add_task(function* test_context_menu_populate_username_with_password_noSchemeUpgrades() { + Services.prefs.setBoolPref("signon.schemeUpgrades", false); + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: TEST_HOSTNAME + "/browser/toolkit/components/" + + "passwordmgr/test/browser/multiple_forms.html", + }, function* (browser) { + yield openPasswordContextMenu(browser, "#test-username-2"); + + // Check the content of the password manager popup + let popupMenu = document.getElementById("fill-login-popup"); + checkMenu(popupMenu, 2); + + CONTEXT_MENU.hidePopup(); + }); +}); +/** + * Check if the context menu is populated with the right menuitems + * for the target username field with a password field present. + */ +add_task(function* test_context_menu_populate_username_with_password_schemeUpgrades() { + Services.prefs.setBoolPref("signon.schemeUpgrades", true); + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: TEST_HOSTNAME + "/browser/toolkit/components/" + + "passwordmgr/test/browser/multiple_forms.html", + }, function* (browser) { + yield openPasswordContextMenu(browser, "#test-username-2"); + + // Check the content of the password manager popup + let popupMenu = document.getElementById("fill-login-popup"); + checkMenu(popupMenu, 3); + + CONTEXT_MENU.hidePopup(); + }); +}); + +/** + * Check if the password field is correctly filled when one + * login menuitem is clicked. + */ +add_task(function* test_context_menu_password_fill() { + Services.prefs.setBoolPref("signon.schemeUpgrades", true); + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH, + }, function* (browser) { + let formDescriptions = yield ContentTask.spawn(browser, {}, function*() { + let forms = Array.from(content.document.getElementsByClassName("test-form")); + return forms.map((f) => f.getAttribute("description")); + }); + + for (let description of formDescriptions) { + info("Testing form: " + description); + + let passwordInputIds = yield ContentTask.spawn(browser, {description}, function*({description}) { + let formElement = content.document.querySelector(`[description="${description}"]`); + let passwords = Array.from(formElement.querySelectorAll("input[type='password']")); + return passwords.map((p) => p.id); + }); + + for (let inputId of passwordInputIds) { + info("Testing password field: " + inputId); + + // Synthesize a right mouse click over the username input element. + yield openPasswordContextMenu(browser, "#" + inputId, function*() { + let inputDisabled = yield ContentTask + .spawn(browser, {inputId}, function*({inputId}) { + let input = content.document.getElementById(inputId); + return input.disabled || input.readOnly; + }); + + // If the password field is disabled or read-only, we want to see + // the disabled Fill Password popup header. + if (inputDisabled) { + Assert.ok(!POPUP_HEADER.hidden, "Popup menu is not hidden."); + Assert.ok(POPUP_HEADER.disabled, "Popup menu is disabled."); + CONTEXT_MENU.hidePopup(); + } + + return !inputDisabled; + }); + + if (CONTEXT_MENU.state != "open") { + continue; + } + + // The only field affected by the password fill + // should be the target password field itself. + yield assertContextMenuFill(browser, description, null, inputId, 1); + yield ContentTask.spawn(browser, {inputId}, function*({inputId}) { + let passwordField = content.document.getElementById(inputId); + Assert.equal(passwordField.value, "password1", "Check upgraded login was actually used"); + }); + + CONTEXT_MENU.hidePopup(); + } + } + }); +}); + +/** + * Check if the form is correctly filled when one + * username context menu login menuitem is clicked. + */ +add_task(function* test_context_menu_username_login_fill() { + Services.prefs.setBoolPref("signon.schemeUpgrades", true); + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH, + }, function* (browser) { + + let formDescriptions = yield ContentTask.spawn(browser, {}, function*() { + let forms = Array.from(content.document.getElementsByClassName("test-form")); + return forms.map((f) => f.getAttribute("description")); + }); + + for (let description of formDescriptions) { + info("Testing form: " + description); + let usernameInputIds = yield ContentTask + .spawn(browser, {description}, function*({description}) { + let formElement = content.document.querySelector(`[description="${description}"]`); + let inputs = Array.from(formElement.querySelectorAll("input[type='text']")); + return inputs.map((p) => p.id); + }); + + for (let inputId of usernameInputIds) { + info("Testing username field: " + inputId); + + // Synthesize a right mouse click over the username input element. + yield openPasswordContextMenu(browser, "#" + inputId, function*() { + let headerHidden = POPUP_HEADER.hidden; + let headerDisabled = POPUP_HEADER.disabled; + + let data = {description, inputId, headerHidden, headerDisabled}; + let shouldContinue = yield ContentTask.spawn(browser, data, function*(data) { + let {description, inputId, headerHidden, headerDisabled} = data; + let formElement = content.document.querySelector(`[description="${description}"]`); + let usernameField = content.document.getElementById(inputId); + // We always want to check if the first password field is filled, + // since this is the current behavior from the _fillForm function. + let passwordField = formElement.querySelector("input[type='password']"); + + // If we don't want to see the actual popup menu, + // check if the popup is hidden or disabled. + if (!passwordField || usernameField.disabled || usernameField.readOnly || + passwordField.disabled || passwordField.readOnly) { + if (!passwordField) { + Assert.ok(headerHidden, "Popup menu is hidden."); + } else { + Assert.ok(!headerHidden, "Popup menu is not hidden."); + Assert.ok(headerDisabled, "Popup menu is disabled."); + } + return false; + } + return true; + }); + + if (!shouldContinue) { + CONTEXT_MENU.hidePopup(); + } + + return shouldContinue; + }); + + if (CONTEXT_MENU.state != "open") { + continue; + } + + let passwordFieldId = yield ContentTask + .spawn(browser, {description}, function*({description}) { + let formElement = content.document.querySelector(`[description="${description}"]`); + return formElement.querySelector("input[type='password']").id; + }); + + // We shouldn't change any field that's not the target username field or the first password field + yield assertContextMenuFill(browser, description, inputId, passwordFieldId, 1); + + yield ContentTask.spawn(browser, {passwordFieldId}, function*({passwordFieldId}) { + let passwordField = content.document.getElementById(passwordFieldId); + if (!passwordField.hasAttribute("expectedFail")) { + Assert.equal(passwordField.value, "password1", "Check upgraded login was actually used"); + } + }); + + CONTEXT_MENU.hidePopup(); + } + } + }); +}); + +/** + * Synthesize mouse clicks to open the password manager context menu popup + * for a target password input element. + * + * assertCallback should return true if we should continue or else false. + */ +function* openPasswordContextMenu(browser, passwordInput, assertCallback = null) { + // Synthesize a right mouse click over the password input element. + let contextMenuShownPromise = BrowserTestUtils.waitForEvent(CONTEXT_MENU, "popupshown"); + let eventDetails = {type: "contextmenu", button: 2}; + BrowserTestUtils.synthesizeMouseAtCenter(passwordInput, eventDetails, browser); + yield contextMenuShownPromise; + + if (assertCallback) { + let shouldContinue = yield assertCallback(); + if (!shouldContinue) { + return; + } + } + + // Synthesize a mouse click over the fill login menu header. + let popupShownPromise = BrowserTestUtils.waitForEvent(POPUP_HEADER, "popupshown"); + EventUtils.synthesizeMouseAtCenter(POPUP_HEADER, {}); + yield popupShownPromise; +} + +/** + * Verify that only the expected form fields are filled. + */ +function* assertContextMenuFill(browser, formId, usernameFieldId, passwordFieldId, loginIndex) { + let popupMenu = document.getElementById("fill-login-popup"); + let unchangedSelector = `[description="${formId}"] input:not(#${passwordFieldId})`; + + if (usernameFieldId) { + unchangedSelector += `:not(#${usernameFieldId})`; + } + + yield ContentTask.spawn(browser, {unchangedSelector}, function*({unchangedSelector}) { + let unchangedFields = content.document.querySelectorAll(unchangedSelector); + + // Store the value of fields that should remain unchanged. + if (unchangedFields.length) { + for (let field of unchangedFields) { + field.setAttribute("original-value", field.value); + } + } + }); + + // Execute the default command of the specified login menuitem found in the context menu. + let loginItem = popupMenu.getElementsByClassName("context-login-item")[loginIndex]; + + // Find the used login by it's username (Use only unique usernames in this test). + let {username, password} = getLoginFromUsername(loginItem.label); + + let data = {username, password, usernameFieldId, passwordFieldId, formId, unchangedSelector}; + let continuePromise = ContentTask.spawn(browser, data, function*(data) { + let {username, password, usernameFieldId, passwordFieldId, formId, unchangedSelector} = data; + let form = content.document.querySelector(`[description="${formId}"]`); + yield ContentTaskUtils.waitForEvent(form, "input", "Username input value changed"); + + if (usernameFieldId) { + let usernameField = content.document.getElementById(usernameFieldId); + + // If we have an username field, check if it's correctly filled + if (usernameField.getAttribute("expectedFail") == null) { + Assert.equal(username, usernameField.value, "Username filled and correct."); + } + } + + if (passwordFieldId) { + let passwordField = content.document.getElementById(passwordFieldId); + + // If we have a password field, check if it's correctly filled + if (passwordField && passwordField.getAttribute("expectedFail") == null) { + Assert.equal(password, passwordField.value, "Password filled and correct."); + } + } + + let unchangedFields = content.document.querySelectorAll(unchangedSelector); + + // Check that all fields that should not change have the same value as before. + if (unchangedFields.length) { + Assert.ok(() => { + for (let field of unchangedFields) { + if (field.value != field.getAttribute("original-value")) { + return false; + } + } + return true; + }, "Other fields were not changed."); + } + }); + + loginItem.doCommand(); + + return continuePromise; +} + +/** + * Check if every login that matches the page hostname are available at the context menu. + * @param {Element} contextMenu + * @param {Number} expectedCount - Number of logins expected in the context menu. Used to ensure +* we continue testing something useful. + */ +function checkMenu(contextMenu, expectedCount) { + let logins = loginList().filter(login => { + return LoginHelper.isOriginMatching(login.hostname, TEST_HOSTNAME, { + schemeUpgrades: Services.prefs.getBoolPref("signon.schemeUpgrades"), + }); + }); + // Make an array of menuitems for easier comparison. + let menuitems = [...CONTEXT_MENU.getElementsByClassName("context-login-item")]; + Assert.equal(menuitems.length, expectedCount, "Expected number of menu items"); + Assert.ok(logins.every(l => menuitems.some(m => l.username == m.label)), "Every login have an item at the menu."); +} + +/** + * Search for a login by it's username. + * + * Only unique login/hostname combinations should be used at this test. + */ +function getLoginFromUsername(username) { + return loginList().find(login => login.username == username); +} + +/** + * List of logins used for the test. + * + * We should only use unique usernames in this test, + * because we need to search logins by username. There is one duplicate u+p combo + * in order to test de-duping in the menu. + */ +function loginList() { + return [ + LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: "username", + password: "password", + }), + // Same as above but HTTP in order to test de-duping. + LoginTestUtils.testData.formLogin({ + hostname: "http://example.com", + formSubmitURL: "http://example.com", + username: "username", + password: "password", + }), + LoginTestUtils.testData.formLogin({ + hostname: "http://example.com", + formSubmitURL: "http://example.com", + username: "username1", + password: "password1", + }), + LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: "username2", + password: "password2", + }), + LoginTestUtils.testData.formLogin({ + hostname: "http://example.org", + formSubmitURL: "http://example.org", + username: "username-cross-origin", + password: "password-cross-origin", + }), + ]; +} diff --git a/toolkit/components/passwordmgr/test/browser/browser_context_menu_autocomplete_interaction.js b/toolkit/components/passwordmgr/test/browser/browser_context_menu_autocomplete_interaction.js new file mode 100644 index 000000000..1b37e3f79 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_context_menu_autocomplete_interaction.js @@ -0,0 +1,99 @@ +/* + * Test the password manager context menu interaction with autocomplete. + */ + +"use strict"; + +const TEST_HOSTNAME = "https://example.com"; +const BASIC_FORM_PAGE_PATH = DIRECTORY_PATH + "form_basic.html"; + +var gUnexpectedIsTODO = false; + +/** + * Initialize logins needed for the tests and disable autofill + * for login forms for easier testing of manual fill. + */ +add_task(function* test_initialize() { + let autocompletePopup = document.getElementById("PopupAutoComplete"); + Services.prefs.setBoolPref("signon.autofillForms", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("signon.autofillForms"); + autocompletePopup.removeEventListener("popupshowing", autocompleteUnexpectedPopupShowing); + }); + for (let login of loginList()) { + Services.logins.addLogin(login); + } + autocompletePopup.addEventListener("popupshowing", autocompleteUnexpectedPopupShowing); +}); + +add_task(function* test_context_menu_username() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: TEST_HOSTNAME + BASIC_FORM_PAGE_PATH, + }, function* (browser) { + yield openContextMenu(browser, "#form-basic-username"); + + let contextMenu = document.getElementById("contentAreaContextMenu"); + Assert.equal(contextMenu.state, "open", "Context menu opened"); + contextMenu.hidePopup(); + }); +}); + +add_task(function* test_context_menu_password() { + gUnexpectedIsTODO = true; + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: TEST_HOSTNAME + BASIC_FORM_PAGE_PATH, + }, function* (browser) { + yield openContextMenu(browser, "#form-basic-password"); + + let contextMenu = document.getElementById("contentAreaContextMenu"); + Assert.equal(contextMenu.state, "open", "Context menu opened"); + contextMenu.hidePopup(); + }); +}); + +function autocompleteUnexpectedPopupShowing(event) { + if (gUnexpectedIsTODO) { + todo(false, "Autocomplete shouldn't appear"); + } else { + Assert.ok(false, "Autocomplete shouldn't appear"); + } + event.target.hidePopup(); +} + +/** + * Synthesize mouse clicks to open the context menu popup + * for a target login input element. + */ +function* openContextMenu(browser, loginInput) { + // First synthesize a mousedown. We need this to get the focus event with the "contextmenu" event. + let eventDetails1 = {type: "mousedown", button: 2}; + BrowserTestUtils.synthesizeMouseAtCenter(loginInput, eventDetails1, browser); + + // Then synthesize the contextmenu click over the input element. + let contextMenuShownPromise = BrowserTestUtils.waitForEvent(window, "popupshown"); + let eventDetails = {type: "contextmenu", button: 2}; + BrowserTestUtils.synthesizeMouseAtCenter(loginInput, eventDetails, browser); + yield contextMenuShownPromise; + + // Wait to see which popups are shown. + yield new Promise(resolve => setTimeout(resolve, 1000)); +} + +function loginList() { + return [ + LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: "username", + password: "password", + }), + LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: "username2", + password: "password2", + }), + ]; +} diff --git a/toolkit/components/passwordmgr/test/browser/browser_context_menu_iframe.js b/toolkit/components/passwordmgr/test/browser/browser_context_menu_iframe.js new file mode 100644 index 000000000..c5219789d --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_context_menu_iframe.js @@ -0,0 +1,144 @@ +/* + * Test the password manager context menu. + */ + +"use strict"; + +const TEST_HOSTNAME = "https://example.com"; + +// Test with a page that only has a form within an iframe, not in the top-level document +const IFRAME_PAGE_PATH = "/browser/toolkit/components/passwordmgr/test/browser/form_basic_iframe.html"; + +/** + * Initialize logins needed for the tests and disable autofill + * for login forms for easier testing of manual fill. + */ +add_task(function* test_initialize() { + Services.prefs.setBoolPref("signon.autofillForms", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("signon.autofillForms"); + Services.prefs.clearUserPref("signon.schemeUpgrades"); + }); + for (let login of loginList()) { + Services.logins.addLogin(login); + } +}); + +/** + * Check if the password field is correctly filled when it's in an iframe. + */ +add_task(function* test_context_menu_iframe_fill() { + Services.prefs.setBoolPref("signon.schemeUpgrades", true); + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: TEST_HOSTNAME + IFRAME_PAGE_PATH + }, function* (browser) { + function getPasswordInput() { + let frame = content.document.getElementById("test-iframe"); + return frame.contentDocument.getElementById("form-basic-password"); + } + + let contextMenuShownPromise = BrowserTestUtils.waitForEvent(window, "popupshown"); + let eventDetails = {type: "contextmenu", button: 2}; + + // To click at the right point we have to take into account the iframe offset. + // Synthesize a right mouse click over the password input element. + BrowserTestUtils.synthesizeMouseAtCenter(getPasswordInput, eventDetails, browser); + yield contextMenuShownPromise; + + // Synthesize a mouse click over the fill login menu header. + let popupHeader = document.getElementById("fill-login"); + let popupShownPromise = BrowserTestUtils.waitForEvent(popupHeader, "popupshown"); + EventUtils.synthesizeMouseAtCenter(popupHeader, {}); + yield popupShownPromise; + + let popupMenu = document.getElementById("fill-login-popup"); + + // Stores the original value of username + function promiseFrameInputValue(name) { + return ContentTask.spawn(browser, name, function(inputname) { + let iframe = content.document.getElementById("test-iframe"); + let input = iframe.contentDocument.getElementById(inputname); + return input.value; + }); + } + let usernameOriginalValue = yield promiseFrameInputValue("form-basic-username"); + + // Execute the command of the first login menuitem found at the context menu. + let passwordChangedPromise = ContentTask.spawn(browser, null, function* () { + let frame = content.document.getElementById("test-iframe"); + let passwordInput = frame.contentDocument.getElementById("form-basic-password"); + yield ContentTaskUtils.waitForEvent(passwordInput, "input"); + }); + + let firstLoginItem = popupMenu.getElementsByClassName("context-login-item")[0]; + firstLoginItem.doCommand(); + + yield passwordChangedPromise; + + // Find the used login by it's username. + let login = getLoginFromUsername(firstLoginItem.label); + let passwordValue = yield promiseFrameInputValue("form-basic-password"); + is(login.password, passwordValue, "Password filled and correct."); + + let usernameNewValue = yield promiseFrameInputValue("form-basic-username"); + is(usernameOriginalValue, + usernameNewValue, + "Username value was not changed."); + + let contextMenu = document.getElementById("contentAreaContextMenu"); + contextMenu.hidePopup(); + }); +}); + +/** + * Search for a login by it's username. + * + * Only unique login/hostname combinations should be used at this test. + */ +function getLoginFromUsername(username) { + return loginList().find(login => login.username == username); +} + +/** + * List of logins used for the test. + * + * We should only use unique usernames in this test, + * because we need to search logins by username. There is one duplicate u+p combo + * in order to test de-duping in the menu. + */ +function loginList() { + return [ + LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: "username", + password: "password", + }), + // Same as above but HTTP in order to test de-duping. + LoginTestUtils.testData.formLogin({ + hostname: "http://example.com", + formSubmitURL: "http://example.com", + username: "username", + password: "password", + }), + LoginTestUtils.testData.formLogin({ + hostname: "http://example.com", + formSubmitURL: "http://example.com", + username: "username1", + password: "password1", + }), + LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: "username2", + password: "password2", + }), + LoginTestUtils.testData.formLogin({ + hostname: "http://example.org", + formSubmitURL: "http://example.org", + username: "username-cross-origin", + password: "password-cross-origin", + }), + ]; +} diff --git a/toolkit/components/passwordmgr/test/browser/browser_exceptions_dialog.js b/toolkit/components/passwordmgr/test/browser/browser_exceptions_dialog.js new file mode 100644 index 000000000..09fbe0eea --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_exceptions_dialog.js @@ -0,0 +1,56 @@ + +"use strict"; + +const LOGIN_HOST = "http://example.com"; + +function openExceptionsDialog() { + return window.openDialog( + "chrome://browser/content/preferences/permissions.xul", + "Toolkit:PasswordManagerExceptions", "", + { + blockVisible: true, + sessionVisible: false, + allowVisible: false, + hideStatusColumn: true, + prefilledHost: "", + permissionType: "login-saving" + } + ); +} + +function countDisabledHosts(dialog) { + let doc = dialog.document; + let rejectsTree = doc.getElementById("permissionsTree"); + + return rejectsTree.view.rowCount; +} + +function promiseStorageChanged(expectedData) { + function observer(subject, data) { + return data == expectedData && subject.QueryInterface(Ci.nsISupportsString).data == LOGIN_HOST; + } + + return TestUtils.topicObserved("passwordmgr-storage-changed", observer); +} + +add_task(function* test_disable() { + let dialog = openExceptionsDialog(); + let promiseChanged = promiseStorageChanged("hostSavingDisabled"); + + yield BrowserTestUtils.waitForEvent(dialog, "load"); + Services.logins.setLoginSavingEnabled(LOGIN_HOST, false); + yield promiseChanged; + is(countDisabledHosts(dialog), 1, "Verify disabled host added"); + yield BrowserTestUtils.closeWindow(dialog); +}); + +add_task(function* test_enable() { + let dialog = openExceptionsDialog(); + let promiseChanged = promiseStorageChanged("hostSavingEnabled"); + + yield BrowserTestUtils.waitForEvent(dialog, "load"); + Services.logins.setLoginSavingEnabled(LOGIN_HOST, true); + yield promiseChanged; + is(countDisabledHosts(dialog), 0, "Verify disabled host removed"); + yield BrowserTestUtils.closeWindow(dialog); +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_formless_submit_chrome.js b/toolkit/components/passwordmgr/test/browser/browser_formless_submit_chrome.js new file mode 100644 index 000000000..c6d9ce50a --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_formless_submit_chrome.js @@ -0,0 +1,126 @@ +/* + * Test that browser chrome UI interactions don't trigger a capture doorhanger. + */ + +"use strict"; + +function* fillTestPage(aBrowser) { + yield ContentTask.spawn(aBrowser, null, function*() { + content.document.getElementById("form-basic-username").value = "my_username"; + content.document.getElementById("form-basic-password").value = "my_password"; + }); + info("fields filled"); +} + +function* withTestPage(aTaskFn) { + return BrowserTestUtils.withNewTab({ + gBrowser, + url: "https://example.com" + DIRECTORY_PATH + "formless_basic.html", + }, function*(aBrowser) { + info("tab opened"); + yield fillTestPage(aBrowser); + yield* aTaskFn(aBrowser); + + // Give a chance for the doorhanger to appear + yield new Promise(resolve => SimpleTest.executeSoon(resolve)); + ok(!getCaptureDoorhanger("any"), "No doorhanger should be present"); + }); +} + +add_task(function* setup() { + yield SimpleTest.promiseFocus(window); +}); + +add_task(function* test_urlbar_new_URL() { + yield withTestPage(function*(aBrowser) { + gURLBar.value = ""; + let focusPromise = BrowserTestUtils.waitForEvent(gURLBar, "focus"); + gURLBar.focus(); + yield focusPromise; + info("focused"); + EventUtils.sendString("http://mochi.test:8888/"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield BrowserTestUtils.browserLoaded(aBrowser, false, "http://mochi.test:8888/"); + }); +}); + +add_task(function* test_urlbar_fragment_enter() { + yield withTestPage(function*(aBrowser) { + gURLBar.focus(); + EventUtils.synthesizeKey("VK_RIGHT", {}); + EventUtils.sendString("#fragment"); + EventUtils.synthesizeKey("VK_RETURN", {}); + }); +}); + +add_task(function* test_backButton_forwardButton() { + yield withTestPage(function*(aBrowser) { + // Load a new page in the tab so we can test going back + aBrowser.loadURI("https://example.com" + DIRECTORY_PATH + "formless_basic.html?second"); + yield BrowserTestUtils.browserLoaded(aBrowser, false, + "https://example.com" + DIRECTORY_PATH + + "formless_basic.html?second"); + yield fillTestPage(aBrowser); + + let forwardButton = document.getElementById("forward-button"); + // We need to wait for the forward button transition to complete before we + // can click it, so we hook up a listener to wait for it to be ready. + let forwardTransitionPromise = BrowserTestUtils.waitForEvent(forwardButton, "transitionend"); + + let backPromise = BrowserTestUtils.browserStopped(aBrowser); + EventUtils.synthesizeMouseAtCenter(document.getElementById("back-button"), {}); + yield backPromise; + + // Give a chance for the doorhanger to appear + yield new Promise(resolve => SimpleTest.executeSoon(resolve)); + ok(!getCaptureDoorhanger("any"), "No doorhanger should be present"); + + // Now go forward again after filling + yield fillTestPage(aBrowser); + + yield forwardTransitionPromise; + info("transition done"); + yield BrowserTestUtils.waitForCondition(() => { + return forwardButton.disabled == false; + }); + let forwardPromise = BrowserTestUtils.browserStopped(aBrowser); + info("click the forward button"); + EventUtils.synthesizeMouseAtCenter(forwardButton, {}); + yield forwardPromise; + }); +}); + + +add_task(function* test_reloadButton() { + yield withTestPage(function*(aBrowser) { + let reloadButton = document.getElementById("urlbar-reload-button"); + let loadPromise = BrowserTestUtils.browserLoaded(aBrowser, false, + "https://example.com" + DIRECTORY_PATH + + "formless_basic.html"); + + yield BrowserTestUtils.waitForCondition(() => { + return reloadButton.disabled == false; + }); + EventUtils.synthesizeMouseAtCenter(reloadButton, {}); + yield loadPromise; + }); +}); + +add_task(function* test_back_keyboard_shortcut() { + if (Services.prefs.getIntPref("browser.backspace_action") != 0) { + ok(true, "Skipped testing backspace to go back since it's disabled"); + return; + } + yield withTestPage(function*(aBrowser) { + // Load a new page in the tab so we can test going back + aBrowser.loadURI("https://example.com" + DIRECTORY_PATH + "formless_basic.html?second"); + yield BrowserTestUtils.browserLoaded(aBrowser, false, + "https://example.com" + DIRECTORY_PATH + + "formless_basic.html?second"); + yield fillTestPage(aBrowser); + + let backPromise = BrowserTestUtils.browserStopped(aBrowser); + EventUtils.synthesizeKey("VK_BACK_SPACE", {}); + yield backPromise; + }); +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms.js b/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms.js new file mode 100644 index 000000000..039312b7d --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Cu.import("resource://gre/modules/LoginManagerParent.jsm", this); + +const testUrlPath = + "://example.com/browser/toolkit/components/passwordmgr/test/browser/"; + +/** + * Waits for the given number of occurrences of InsecureLoginFormsStateChange + * on the given browser element. + */ +function waitForInsecureLoginFormsStateChange(browser, count) { + return BrowserTestUtils.waitForEvent(browser, "InsecureLoginFormsStateChange", + false, () => --count == 0); +} + +/** + * Checks that hasInsecureLoginForms is true for a simple HTTP page and false + * for a simple HTTPS page. + */ +add_task(function* test_simple() { + for (let scheme of ["http", "https"]) { + let tab = gBrowser.addTab(scheme + testUrlPath + "form_basic.html"); + let browser = tab.linkedBrowser; + yield Promise.all([ + BrowserTestUtils.switchTab(gBrowser, tab), + BrowserTestUtils.browserLoaded(browser), + // One event is triggered by pageshow and one by DOMFormHasPassword. + waitForInsecureLoginFormsStateChange(browser, 2), + ]); + + Assert.equal(LoginManagerParent.hasInsecureLoginForms(browser), + scheme == "http"); + + gBrowser.removeTab(tab); + } +}); + +/** + * Checks that hasInsecureLoginForms is true if a password field is present in + * an HTTP page loaded as a subframe of a top-level HTTPS page, when mixed + * active content blocking is disabled. + * + * When the subframe is navigated to an HTTPS page, hasInsecureLoginForms should + * be set to false. + * + * Moving back in history should set hasInsecureLoginForms to true again. + */ +add_task(function* test_subframe_navigation() { + yield new Promise(resolve => SpecialPowers.pushPrefEnv({ + "set": [["security.mixed_content.block_active_content", false]], + }, resolve)); + + // Load the page with the subframe in a new tab. + let tab = gBrowser.addTab("https" + testUrlPath + "insecure_test.html"); + let browser = tab.linkedBrowser; + yield Promise.all([ + BrowserTestUtils.switchTab(gBrowser, tab), + BrowserTestUtils.browserLoaded(browser), + // Two events are triggered by pageshow and one by DOMFormHasPassword. + waitForInsecureLoginFormsStateChange(browser, 3), + ]); + + Assert.ok(LoginManagerParent.hasInsecureLoginForms(browser)); + + // Navigate the subframe to a secure page. + let promiseSubframeReady = Promise.all([ + BrowserTestUtils.browserLoaded(browser, true), + // One event is triggered by pageshow and one by DOMFormHasPassword. + waitForInsecureLoginFormsStateChange(browser, 2), + ]); + yield ContentTask.spawn(browser, null, function* () { + content.document.getElementById("test-iframe") + .contentDocument.getElementById("test-link").click(); + }); + yield promiseSubframeReady; + + Assert.ok(!LoginManagerParent.hasInsecureLoginForms(browser)); + + // Navigate back to the insecure page. We only have to wait for the + // InsecureLoginFormsStateChange event that is triggered by pageshow. + let promise = waitForInsecureLoginFormsStateChange(browser, 1); + yield ContentTask.spawn(browser, null, function* () { + content.document.getElementById("test-iframe") + .contentWindow.history.back(); + }); + yield promise; + + Assert.ok(LoginManagerParent.hasInsecureLoginForms(browser)); + + gBrowser.removeTab(tab); +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms_streamConverter.js b/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms_streamConverter.js new file mode 100644 index 000000000..2dbffb9cc --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms_streamConverter.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Cu.import("resource://gre/modules/LoginManagerParent.jsm", this); + +function* registerConverter() { + Cu.import("resource://gre/modules/Services.jsm", this); + Cu.import("resource://gre/modules/NetUtil.jsm", this); + + /** + * Converts the "test/content" MIME type, served by the test over HTTP, to an + * HTML viewer page containing the "form_basic.html" code. The viewer is + * served from a "resource:" URI while keeping the "resource:" principal. + */ + function TestStreamConverter() {} + + TestStreamConverter.prototype = { + classID: Components.ID("{5f01d6ef-c090-45a4-b3e5-940d64713eb7}"), + contractID: "@mozilla.org/streamconv;1?from=test/content&to=*/*", + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIRequestObserver, + Ci.nsIStreamListener, + Ci.nsIStreamConverter, + ]), + + // nsIStreamConverter + convert() {}, + + // nsIStreamConverter + asyncConvertData(aFromType, aToType, aListener, aCtxt) { + this.listener = aListener; + }, + + // nsIRequestObserver + onStartRequest(aRequest, aContext) { + let channel = NetUtil.newChannel({ + uri: "resource://testing-common/form_basic.html", + loadUsingSystemPrincipal: true, + }); + channel.originalURI = aRequest.QueryInterface(Ci.nsIChannel).URI; + channel.loadGroup = aRequest.loadGroup; + channel.owner = Services.scriptSecurityManager + .createCodebasePrincipal(channel.URI, {}); + // In this test, we pass the new channel to the listener but don't fire a + // redirect notification, even if it would be required. This keeps the + // test code simpler and doesn't impact the principal check we're testing. + channel.asyncOpen2(this.listener); + }, + + // nsIRequestObserver + onStopRequest() {}, + + // nsIStreamListener + onDataAvailable() {}, + }; + + let factory = XPCOMUtils._getFactory(TestStreamConverter); + let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + registrar.registerFactory(TestStreamConverter.prototype.classID, "", + TestStreamConverter.prototype.contractID, factory); + this.cleanupFunction = function () { + registrar.unregisterFactory(TestStreamConverter.prototype.classID, factory); + }; +} + +/** + * Waits for the given number of occurrences of InsecureLoginFormsStateChange + * on the given browser element. + */ +function waitForInsecureLoginFormsStateChange(browser, count) { + return BrowserTestUtils.waitForEvent(browser, "InsecureLoginFormsStateChange", + false, () => --count == 0); +} + +/** + * Checks that hasInsecureLoginForms is false for a viewer served internally + * using a "resource:" URI. + */ +add_task(function* test_streamConverter() { + let originalBrowser = gBrowser.selectedTab.linkedBrowser; + + yield ContentTask.spawn(originalBrowser, null, registerConverter); + + let tab = gBrowser.addTab("http://example.com/browser/toolkit/components/" + + "passwordmgr/test/browser/streamConverter_content.sjs", + { relatedBrowser: originalBrowser.linkedBrowser }); + let browser = tab.linkedBrowser; + yield Promise.all([ + BrowserTestUtils.switchTab(gBrowser, tab), + BrowserTestUtils.browserLoaded(browser), + // One event is triggered by pageshow and one by DOMFormHasPassword. + waitForInsecureLoginFormsStateChange(browser, 2), + ]); + + Assert.ok(!LoginManagerParent.hasInsecureLoginForms(browser)); + + yield BrowserTestUtils.removeTab(tab); + + yield ContentTask.spawn(originalBrowser, null, function* () { + this.cleanupFunction(); + }); +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_http_autofill.js b/toolkit/components/passwordmgr/test/browser/browser_http_autofill.js new file mode 100644 index 000000000..beb928a34 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_http_autofill.js @@ -0,0 +1,78 @@ +const TEST_URL_PATH = "://example.org/browser/toolkit/components/passwordmgr/test/browser/"; + +add_task(function* setup() { + let login = LoginTestUtils.testData.formLogin({ + hostname: "http://example.org", + formSubmitURL: "http://example.org", + username: "username", + password: "password", + }); + Services.logins.addLogin(login); + login = LoginTestUtils.testData.formLogin({ + hostname: "http://example.org", + formSubmitURL: "http://another.domain", + username: "username", + password: "password", + }); + Services.logins.addLogin(login); + yield SpecialPowers.pushPrefEnv({ "set": [["signon.autofillForms.http", false]] }); +}); + +add_task(function* test_http_autofill() { + for (let scheme of ["http", "https"]) { + let tab = yield BrowserTestUtils + .openNewForegroundTab(gBrowser, `${scheme}${TEST_URL_PATH}form_basic.html`); + + let [username, password] = yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + let doc = content.document; + let contentUsername = doc.getElementById("form-basic-username").value; + let contentPassword = doc.getElementById("form-basic-password").value; + return [contentUsername, contentPassword]; + }); + + is(username, scheme == "http" ? "" : "username", "Username filled correctly"); + is(password, scheme == "http" ? "" : "password", "Password filled correctly"); + + gBrowser.removeTab(tab); + } +}); + +add_task(function* test_iframe_in_http_autofill() { + for (let scheme of ["http", "https"]) { + let tab = yield BrowserTestUtils + .openNewForegroundTab(gBrowser, `${scheme}${TEST_URL_PATH}form_basic_iframe.html`); + + let [username, password] = yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + let doc = content.document; + let iframe = doc.getElementById("test-iframe"); + let contentUsername = iframe.contentWindow.document.getElementById("form-basic-username").value; + let contentPassword = iframe.contentWindow.document.getElementById("form-basic-password").value; + return [contentUsername, contentPassword]; + }); + + is(username, scheme == "http" ? "" : "username", "Username filled correctly"); + is(password, scheme == "http" ? "" : "password", "Password filled correctly"); + + gBrowser.removeTab(tab); + } +}); + +add_task(function* test_http_action_autofill() { + for (let type of ["insecure", "secure"]) { + let tab = yield BrowserTestUtils + .openNewForegroundTab(gBrowser, `https${TEST_URL_PATH}form_cross_origin_${type}_action.html`); + + let [username, password] = yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + let doc = content.document; + let contentUsername = doc.getElementById("form-basic-username").value; + let contentPassword = doc.getElementById("form-basic-password").value; + return [contentUsername, contentPassword]; + }); + + is(username, type == "insecure" ? "" : "username", "Username filled correctly"); + is(password, type == "insecure" ? "" : "password", "Password filled correctly"); + + gBrowser.removeTab(tab); + } +}); + diff --git a/toolkit/components/passwordmgr/test/browser/browser_insecurePasswordConsoleWarning.js b/toolkit/components/passwordmgr/test/browser/browser_insecurePasswordConsoleWarning.js new file mode 100644 index 000000000..f16ae1b98 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_insecurePasswordConsoleWarning.js @@ -0,0 +1,94 @@ +"use strict"; + +const WARNING_PATTERN = [{ + key: "INSECURE_FORM_ACTION", + msg: 'JavaScript Warning: "Password fields present in a form with an insecure (http://) form action. This is a security risk that allows user login credentials to be stolen."' +}, { + key: "INSECURE_PAGE", + msg: 'JavaScript Warning: "Password fields present on an insecure (http://) page. This is a security risk that allows user login credentials to be stolen."' +}]; + +add_task(function* testInsecurePasswordWarning() { + let warningPatternHandler; + + function messageHandler(msgObj) { + function findWarningPattern(msg) { + return WARNING_PATTERN.find(patternPair => { + return msg.indexOf(patternPair.msg) !== -1; + }); + } + + let warning = findWarningPattern(msgObj.message); + + // Only handle the insecure password related warning messages. + if (warning) { + // Prevent any unexpected or redundant matched warning message coming after + // the test case is ended. + ok(warningPatternHandler, "Invoke a valid warning message handler"); + warningPatternHandler(warning, msgObj.message); + } + } + Services.console.registerListener(messageHandler); + registerCleanupFunction(function() { + Services.console.unregisterListener(messageHandler); + }); + + for (let [origin, testFile, expectWarnings] of [ + ["http://127.0.0.1", "form_basic.html", []], + ["http://127.0.0.1", "formless_basic.html", []], + ["http://example.com", "form_basic.html", ["INSECURE_PAGE"]], + ["http://example.com", "formless_basic.html", ["INSECURE_PAGE"]], + ["https://example.com", "form_basic.html", []], + ["https://example.com", "formless_basic.html", []], + + // For a form with customized action link in the same origin. + ["http://127.0.0.1", "form_same_origin_action.html", []], + ["http://example.com", "form_same_origin_action.html", ["INSECURE_PAGE"]], + ["https://example.com", "form_same_origin_action.html", []], + + // For a form with an insecure (http) customized action link. + ["http://127.0.0.1", "form_cross_origin_insecure_action.html", ["INSECURE_FORM_ACTION"]], + ["http://example.com", "form_cross_origin_insecure_action.html", ["INSECURE_PAGE"]], + ["https://example.com", "form_cross_origin_insecure_action.html", ["INSECURE_FORM_ACTION"]], + + // For a form with a secure (https) customized action link. + ["http://127.0.0.1", "form_cross_origin_secure_action.html", []], + ["http://example.com", "form_cross_origin_secure_action.html", ["INSECURE_PAGE"]], + ["https://example.com", "form_cross_origin_secure_action.html", []], + ]) { + let testURL = origin + DIRECTORY_PATH + testFile; + let promiseConsoleMessages = new Promise(resolve => { + warningPatternHandler = function (warning, originMessage) { + ok(warning, "Handling a warning pattern"); + let fullMessage = `[${warning.msg} {file: "${testURL}" line: 0 column: 0 source: "0"}]`; + is(originMessage, fullMessage, "Message full matched:" + originMessage); + + let index = expectWarnings.indexOf(warning.key); + isnot(index, -1, "Found warning: " + warning.key + " for URL:" + testURL); + if (index !== -1) { + // Remove the shown message. + expectWarnings.splice(index, 1); + } + if (expectWarnings.length === 0) { + info("All warnings are shown for URL:" + testURL); + resolve(); + } + }; + }); + + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: testURL + }, function*() { + if (expectWarnings.length === 0) { + info("All warnings are shown for URL:" + testURL); + return Promise.resolve(); + } + return promiseConsoleMessages; + }); + + // Remove warningPatternHandler to stop handling the matched warning pattern + // and the task should not get any warning anymore. + warningPatternHandler = null; + } +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_master_password_autocomplete.js b/toolkit/components/passwordmgr/test/browser/browser_master_password_autocomplete.js new file mode 100644 index 000000000..f3bc62b0a --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_master_password_autocomplete.js @@ -0,0 +1,59 @@ +const HOST = "https://example.com"; +const URL = HOST + "/browser/toolkit/components/passwordmgr/test/browser/form_basic.html"; +const TIMEOUT_PREF = "signon.masterPasswordReprompt.timeout_ms"; + +// Waits for the master password prompt and cancels it. +function waitForDialog() { + let dialogShown = TestUtils.topicObserved("common-dialog-loaded"); + return dialogShown.then(function([subject]) { + let dialog = subject.Dialog; + is(dialog.args.title, "Password Required"); + dialog.ui.button1.click(); + }); +} + +// Test that autocomplete does not trigger a master password prompt +// for a certain time after it was cancelled. +add_task(function* test_mpAutocompleteTimeout() { + let login = LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: "username", + password: "password", + }); + Services.logins.addLogin(login); + LoginTestUtils.masterPassword.enable(); + + registerCleanupFunction(function() { + LoginTestUtils.masterPassword.disable(); + Services.logins.removeAllLogins(); + }); + + // Set master password prompt timeout to 3s. + // If this test goes intermittent, you likely have to increase this value. + yield SpecialPowers.pushPrefEnv({set: [[TIMEOUT_PREF, 3000]]}); + + // Wait for initial master password dialog after opening the tab. + let dialogShown = waitForDialog(); + + yield BrowserTestUtils.withNewTab(URL, function*(browser) { + yield dialogShown; + + yield ContentTask.spawn(browser, null, function*() { + // Focus the password field to trigger autocompletion. + content.document.getElementById("form-basic-password").focus(); + }); + + // Wait 4s, dialog should not have been shown + // (otherwise the code below will not work). + yield new Promise((c) => setTimeout(c, 4000)); + + dialogShown = waitForDialog(); + yield ContentTask.spawn(browser, null, function*() { + // Re-focus the password field to trigger autocompletion. + content.document.getElementById("form-basic-username").focus(); + content.document.getElementById("form-basic-password").focus(); + }); + yield dialogShown; + }); +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_notifications.js b/toolkit/components/passwordmgr/test/browser/browser_notifications.js new file mode 100644 index 000000000..4fb012f14 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_notifications.js @@ -0,0 +1,81 @@ +/** + * Test that the doorhanger notification for password saving is populated with + * the correct values in various password capture cases. + */ +add_task(function* test_save_change() { + let testCases = [{ + username: "username", + password: "password", + }, { + username: "", + password: "password", + }, { + username: "username", + oldPassword: "password", + password: "newPassword", + }, { + username: "", + oldPassword: "password", + password: "newPassword", + }]; + + for (let { username, oldPassword, password } of testCases) { + // Add a login for the origin of the form if testing a change notification. + if (oldPassword) { + Services.logins.addLogin(LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username, + password: oldPassword, + })); + } + + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "https://example.com/browser/toolkit/components/" + + "passwordmgr/test/browser/form_basic.html", + }, function* (browser) { + // Submit the form in the content page with the credentials from the test + // case. This will cause the doorhanger notification to be displayed. + let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel, + "popupshown", + (event) => event.target == PopupNotifications.panel); + yield ContentTask.spawn(browser, [username, password], + function* ([contentUsername, contentPassword]) { + let doc = content.document; + doc.getElementById("form-basic-username").value = contentUsername; + doc.getElementById("form-basic-password").value = contentPassword; + doc.getElementById("form-basic").submit(); + }); + yield promiseShown; + let notificationElement = PopupNotifications.panel.childNodes[0]; + // Style flush to make sure binding is attached + notificationElement.querySelector("#password-notification-password").clientTop; + + // Check the actual content of the popup notification. + Assert.equal(notificationElement.querySelector("#password-notification-username") + .value, username); + Assert.equal(notificationElement.querySelector("#password-notification-password") + .value, password); + + // Simulate the action on the notification to request the login to be + // saved, and wait for the data to be updated or saved based on the type + // of operation we expect. + let expectedNotification = oldPassword ? "modifyLogin" : "addLogin"; + let promiseLogin = TestUtils.topicObserved("passwordmgr-storage-changed", + (_, data) => data == expectedNotification); + notificationElement.button.doCommand(); + let [result] = yield promiseLogin; + + // Check that the values in the database match the expected values. + let login = oldPassword ? result.QueryInterface(Ci.nsIArray) + .queryElementAt(1, Ci.nsILoginInfo) + : result.QueryInterface(Ci.nsILoginInfo); + Assert.equal(login.username, username); + Assert.equal(login.password, password); + }); + + // Clean up the database before the next test case is executed. + Services.logins.removeAllLogins(); + } +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_notifications_2.js b/toolkit/components/passwordmgr/test/browser/browser_notifications_2.js new file mode 100644 index 000000000..48c73b0e6 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_notifications_2.js @@ -0,0 +1,125 @@ +add_task(function* setup() { + yield SpecialPowers.pushPrefEnv({"set": [ + ["signon.rememberSignons.visibilityToggle", true] + ]}); +}); + +/** + * Test that the doorhanger main action button is disabled + * when the password field is empty. + * + * Also checks that submiting an empty password throws an error. + */ +add_task(function* test_empty_password() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "https://example.com/browser/toolkit/components/" + + "passwordmgr/test/browser/form_basic.html", + }, function* (browser) { + // Submit the form in the content page with the credentials from the test + // case. This will cause the doorhanger notification to be displayed. + let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel, + "popupshown", + (event) => event.target == PopupNotifications.panel); + yield ContentTask.spawn(browser, null, + function* () { + let doc = content.document; + doc.getElementById("form-basic-username").value = "username"; + doc.getElementById("form-basic-password").value = "p"; + doc.getElementById("form-basic").submit(); + }); + yield promiseShown; + + let notificationElement = PopupNotifications.panel.childNodes[0]; + let passwordTextbox = notificationElement.querySelector("#password-notification-password"); + let toggleCheckbox = notificationElement.querySelector("#password-notification-visibilityToggle"); + + // Synthesize input to empty the field + passwordTextbox.focus(); + yield EventUtils.synthesizeKey("VK_RIGHT", {}); + yield EventUtils.synthesizeKey("VK_BACK_SPACE", {}); + + let mainActionButton = document.getAnonymousElementByAttribute(notificationElement.button, "anonid", "button"); + Assert.ok(mainActionButton.disabled, "Main action button is disabled"); + + // Makes sure submiting an empty password throws an error + Assert.throws(notificationElement.button.doCommand(), + "Can't add a login with a null or empty password.", + "Should fail for an empty password"); + }); +}); + +/** + * Test that the doorhanger password field shows plain or * text + * when the checkbox is checked. + */ +add_task(function* test_toggle_password() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "https://example.com/browser/toolkit/components/" + + "passwordmgr/test/browser/form_basic.html", + }, function* (browser) { + // Submit the form in the content page with the credentials from the test + // case. This will cause the doorhanger notification to be displayed. + let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel, + "popupshown", + (event) => event.target == PopupNotifications.panel); + yield ContentTask.spawn(browser, null, + function* () { + let doc = content.document; + doc.getElementById("form-basic-username").value = "username"; + doc.getElementById("form-basic-password").value = "p"; + doc.getElementById("form-basic").submit(); + }); + yield promiseShown; + + let notificationElement = PopupNotifications.panel.childNodes[0]; + let passwordTextbox = notificationElement.querySelector("#password-notification-password"); + let toggleCheckbox = notificationElement.querySelector("#password-notification-visibilityToggle"); + + yield EventUtils.synthesizeMouseAtCenter(toggleCheckbox, {}); + Assert.ok(toggleCheckbox.checked); + Assert.equal(passwordTextbox.type, "", "Password textbox changed to plain text"); + + yield EventUtils.synthesizeMouseAtCenter(toggleCheckbox, {}); + Assert.ok(!toggleCheckbox.checked); + Assert.equal(passwordTextbox.type, "password", "Password textbox changed to * text"); + }); +}); + +/** + * Test that the doorhanger password toggle checkbox is disabled + * when the master password is set. + */ +add_task(function* test_checkbox_disabled_if_has_master_password() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "https://example.com/browser/toolkit/components/" + + "passwordmgr/test/browser/form_basic.html", + }, function* (browser) { + // Submit the form in the content page with the credentials from the test + // case. This will cause the doorhanger notification to be displayed. + let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel, + "popupshown", + (event) => event.target == PopupNotifications.panel); + + LoginTestUtils.masterPassword.enable(); + + yield ContentTask.spawn(browser, null, function* () { + let doc = content.document; + doc.getElementById("form-basic-username").value = "username"; + doc.getElementById("form-basic-password").value = "p"; + doc.getElementById("form-basic").submit(); + }); + yield promiseShown; + + let notificationElement = PopupNotifications.panel.childNodes[0]; + let passwordTextbox = notificationElement.querySelector("#password-notification-password"); + let toggleCheckbox = notificationElement.querySelector("#password-notification-visibilityToggle"); + + Assert.equal(passwordTextbox.type, "password", "Password textbox should show * text"); + Assert.ok(toggleCheckbox.getAttribute("hidden"), "checkbox is hidden when master password is set"); + }); + + LoginTestUtils.masterPassword.disable(); +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_notifications_password.js b/toolkit/components/passwordmgr/test/browser/browser_notifications_password.js new file mode 100644 index 000000000..8ac49dac5 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_notifications_password.js @@ -0,0 +1,145 @@ +/** + * Test changing the password inside the doorhanger notification for passwords. + * + * We check the following cases: + * - Editing the password of a new login. + * - Editing the password of an existing login. + * - Changing both username and password to an existing login. + * - Changing the username to an existing login. + * - Editing username to an empty one and a new password. + * + * If both the username and password matches an already existing login, we should not + * update it's password, but only it's usage timestamp and count. + */ +add_task(function* test_edit_password() { + let testCases = [{ + usernameInPage: "username", + passwordInPage: "password", + passwordChangedTo: "newPassword", + timesUsed: 1, + }, { + usernameInPage: "username", + usernameInPageExists: true, + passwordInPage: "password", + passwordInStorage: "oldPassword", + passwordChangedTo: "newPassword", + timesUsed: 2, + }, { + usernameInPage: "username", + usernameChangedTo: "newUsername", + usernameChangedToExists: true, + passwordInPage: "password", + passwordChangedTo: "newPassword", + timesUsed: 2, + }, { + usernameInPage: "username", + usernameChangedTo: "newUsername", + usernameChangedToExists: true, + passwordInPage: "password", + passwordChangedTo: "password", + timesUsed: 2, + checkPasswordNotUpdated: true, + }, { + usernameInPage: "newUsername", + usernameChangedTo: "", + usernameChangedToExists: true, + passwordInPage: "password", + passwordChangedTo: "newPassword", + timesUsed: 2, + }]; + + for (let testCase of testCases) { + info("Test case: " + JSON.stringify(testCase)); + + // Create the pre-existing logins when needed. + if (testCase.usernameInPageExists) { + Services.logins.addLogin(LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: testCase.usernameInPage, + password: testCase.passwordInStorage, + })); + } + + if (testCase.usernameChangedToExists) { + Services.logins.addLogin(LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: testCase.usernameChangedTo, + password: testCase.passwordChangedTo, + })); + } + + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "https://example.com/browser/toolkit/components/" + + "passwordmgr/test/browser/form_basic.html", + }, function* (browser) { + // Submit the form in the content page with the credentials from the test + // case. This will cause the doorhanger notification to be displayed. + let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel, + "popupshown", + (event) => event.target == PopupNotifications.panel); + yield ContentTask.spawn(browser, testCase, + function* (contentTestCase) { + let doc = content.document; + doc.getElementById("form-basic-username").value = contentTestCase.usernameInPage; + doc.getElementById("form-basic-password").value = contentTestCase.passwordInPage; + doc.getElementById("form-basic").submit(); + }); + yield promiseShown; + let notificationElement = PopupNotifications.panel.childNodes[0]; + // Style flush to make sure binding is attached + notificationElement.querySelector("#password-notification-password").clientTop; + + // Modify the username in the dialog if requested. + if (testCase.usernameChangedTo) { + notificationElement.querySelector("#password-notification-username") + .value = testCase.usernameChangedTo; + } + + // Modify the password in the dialog if requested. + if (testCase.passwordChangedTo) { + notificationElement.querySelector("#password-notification-password") + .value = testCase.passwordChangedTo; + } + + // We expect a modifyLogin notification if the final username used by the + // dialog exists in the logins database, otherwise an addLogin one. + let expectModifyLogin = typeof testCase.usernameChangedTo !== "undefined" + ? testCase.usernameChangedToExists + : testCase.usernameInPageExists; + + // Simulate the action on the notification to request the login to be + // saved, and wait for the data to be updated or saved based on the type + // of operation we expect. + let expectedNotification = expectModifyLogin ? "modifyLogin" : "addLogin"; + let promiseLogin = TestUtils.topicObserved("passwordmgr-storage-changed", + (_, data) => data == expectedNotification); + notificationElement.button.doCommand(); + let [result] = yield promiseLogin; + + // Check that the values in the database match the expected values. + let login = expectModifyLogin ? result.QueryInterface(Ci.nsIArray) + .queryElementAt(1, Ci.nsILoginInfo) + : result.QueryInterface(Ci.nsILoginInfo); + + Assert.equal(login.username, testCase.usernameChangedTo || + testCase.usernameInPage); + Assert.equal(login.password, testCase.passwordChangedTo || + testCase.passwordInPage); + + let meta = login.QueryInterface(Ci.nsILoginMetaInfo); + Assert.equal(meta.timesUsed, testCase.timesUsed); + + // Check that the password was not updated if the user is empty + if (testCase.checkPasswordNotUpdated) { + Assert.ok(meta.timeLastUsed > meta.timeCreated); + Assert.ok(meta.timeCreated == meta.timePasswordChanged); + } + }); + + // Clean up the database before the next test case is executed. + Services.logins.removeAllLogins(); + } +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_notifications_username.js b/toolkit/components/passwordmgr/test/browser/browser_notifications_username.js new file mode 100644 index 000000000..2c9ea2607 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_notifications_username.js @@ -0,0 +1,119 @@ +/** + * Test changing the username inside the doorhanger notification for passwords. + * + * We have to test combination of existing and non-existing logins both for + * the original one from the webpage and the final one used by the dialog. + * + * We also check switching to and from empty usernames. + */ +add_task(function* test_edit_username() { + let testCases = [{ + usernameInPage: "username", + usernameChangedTo: "newUsername", + }, { + usernameInPage: "username", + usernameInPageExists: true, + usernameChangedTo: "newUsername", + }, { + usernameInPage: "username", + usernameChangedTo: "newUsername", + usernameChangedToExists: true, + }, { + usernameInPage: "username", + usernameInPageExists: true, + usernameChangedTo: "newUsername", + usernameChangedToExists: true, + }, { + usernameInPage: "", + usernameChangedTo: "newUsername", + }, { + usernameInPage: "newUsername", + usernameChangedTo: "", + }, { + usernameInPage: "", + usernameChangedTo: "newUsername", + usernameChangedToExists: true, + }, { + usernameInPage: "newUsername", + usernameChangedTo: "", + usernameChangedToExists: true, + }]; + + for (let testCase of testCases) { + info("Test case: " + JSON.stringify(testCase)); + + // Create the pre-existing logins when needed. + if (testCase.usernameInPageExists) { + Services.logins.addLogin(LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: testCase.usernameInPage, + password: "old password", + })); + } + + if (testCase.usernameChangedToExists) { + Services.logins.addLogin(LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: testCase.usernameChangedTo, + password: "old password", + })); + } + + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "https://example.com/browser/toolkit/components/" + + "passwordmgr/test/browser/form_basic.html", + }, function* (browser) { + // Submit the form in the content page with the credentials from the test + // case. This will cause the doorhanger notification to be displayed. + let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel, + "popupshown", + (event) => event.target == PopupNotifications.panel); + yield ContentTask.spawn(browser, testCase.usernameInPage, + function* (usernameInPage) { + let doc = content.document; + doc.getElementById("form-basic-username").value = usernameInPage; + doc.getElementById("form-basic-password").value = "password"; + doc.getElementById("form-basic").submit(); + }); + yield promiseShown; + let notificationElement = PopupNotifications.panel.childNodes[0]; + // Style flush to make sure binding is attached + notificationElement.querySelector("#password-notification-password").clientTop; + + // Modify the username in the dialog if requested. + if (testCase.usernameChangedTo) { + notificationElement.querySelector("#password-notification-username") + .value = testCase.usernameChangedTo; + } + + // We expect a modifyLogin notification if the final username used by the + // dialog exists in the logins database, otherwise an addLogin one. + let expectModifyLogin = testCase.usernameChangedTo + ? testCase.usernameChangedToExists + : testCase.usernameInPageExists; + + // Simulate the action on the notification to request the login to be + // saved, and wait for the data to be updated or saved based on the type + // of operation we expect. + let expectedNotification = expectModifyLogin ? "modifyLogin" : "addLogin"; + let promiseLogin = TestUtils.topicObserved("passwordmgr-storage-changed", + (_, data) => data == expectedNotification); + notificationElement.button.doCommand(); + let [result] = yield promiseLogin; + + // Check that the values in the database match the expected values. + let login = expectModifyLogin ? result.QueryInterface(Ci.nsIArray) + .queryElementAt(1, Ci.nsILoginInfo) + : result.QueryInterface(Ci.nsILoginInfo); + Assert.equal(login.username, testCase.usernameChangedTo || + testCase.usernameInPage); + Assert.equal(login.password, "password"); + }); + + // Clean up the database before the next test case is executed. + Services.logins.removeAllLogins(); + } +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_contextmenu.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_contextmenu.js new file mode 100644 index 000000000..ece2b731f --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_contextmenu.js @@ -0,0 +1,100 @@ +/* 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/. */ + +function test() { + waitForExplicitFinish(); + + Services.logins.removeAllLogins(); + + // Add some initial logins + let urls = [ + "http://example.com/", + "http://mozilla.org/", + "http://spreadfirefox.com/", + "https://support.mozilla.org/", + "http://hg.mozilla.org/" + ]; + let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, "init"); + let logins = [ + new nsLoginInfo(urls[0], urls[0], null, "", "o hai", "u1", "p1"), + new nsLoginInfo(urls[1], urls[1], null, "ehsan", "coded", "u2", "p2"), + new nsLoginInfo(urls[2], urls[2], null, "this", "awesome", "u3", "p3"), + new nsLoginInfo(urls[3], urls[3], null, "array of", "logins", "u4", "p4"), + new nsLoginInfo(urls[4], urls[4], null, "then", "i wrote the test", "u5", "p5") + ]; + logins.forEach(login => Services.logins.addLogin(login)); + + // Open the password manager dialog + const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul"; + let pwmgrdlg = window.openDialog(PWMGR_DLG, "Toolkit:PasswordManager", ""); + SimpleTest.waitForFocus(doTest, pwmgrdlg); + + // Test if "Copy Username" and "Copy Password" works + function doTest() { + let doc = pwmgrdlg.document; + let selection = doc.getElementById("signonsTree").view.selection; + let menuitem = doc.getElementById("context-copyusername"); + + function copyField() { + info("Select all"); + selection.selectAll(); + assertMenuitemEnabled("copyusername", false); + assertMenuitemEnabled("editusername", false); + assertMenuitemEnabled("copypassword", false); + assertMenuitemEnabled("editpassword", false); + + info("Select the first row (with an empty username)"); + selection.select(0); + assertMenuitemEnabled("copyusername", false, "empty username"); + assertMenuitemEnabled("editusername", true); + assertMenuitemEnabled("copypassword", true); + assertMenuitemEnabled("editpassword", false, "password column hidden"); + + info("Clear the selection"); + selection.clearSelection(); + assertMenuitemEnabled("copyusername", false); + assertMenuitemEnabled("editusername", false); + assertMenuitemEnabled("copypassword", false); + assertMenuitemEnabled("editpassword", false); + + info("Select the third row and making the password column visible"); + selection.select(2); + doc.getElementById("passwordCol").hidden = false; + assertMenuitemEnabled("copyusername", true); + assertMenuitemEnabled("editusername", true); + assertMenuitemEnabled("copypassword", true); + assertMenuitemEnabled("editpassword", true, "password column visible"); + menuitem.doCommand(); + } + + function assertMenuitemEnabled(idSuffix, expected, reason = "") { + doc.defaultView.UpdateContextMenu(); + let actual = !doc.getElementById("context-" + idSuffix).getAttribute("disabled"); + is(actual, expected, idSuffix + " should be " + (expected ? "enabled" : "disabled") + + (reason ? ": " + reason : "")); + } + + function cleanUp() { + Services.ww.registerNotification(function (aSubject, aTopic, aData) { + Services.ww.unregisterNotification(arguments.callee); + Services.logins.removeAllLogins(); + doc.getElementById("passwordCol").hidden = true; + finish(); + }); + pwmgrdlg.close(); + } + + function testPassword() { + info("Testing Copy Password"); + waitForClipboard("coded", function copyPassword() { + menuitem = doc.getElementById("context-copypassword"); + menuitem.doCommand(); + }, cleanUp, cleanUp); + } + + info("Testing Copy Username"); + waitForClipboard("ehsan", copyField, testPassword, testPassword); + } +} diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_editing.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_editing.js new file mode 100644 index 000000000..2b2e42273 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_editing.js @@ -0,0 +1,126 @@ +const { ContentTaskUtils } = Cu.import("resource://testing-common/ContentTaskUtils.jsm", {}); +const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul"; + +var doc; +var pwmgr; +var pwmgrdlg; +var signonsTree; + +function addLogin(site, username, password) { + let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, "init"); + let login = new nsLoginInfo(site, site, null, username, password, "u", "p"); + Services.logins.addLogin(login); +} + +function getUsername(row) { + return signonsTree.view.getCellText(row, signonsTree.columns.getNamedColumn("userCol")); +} + +function getPassword(row) { + return signonsTree.view.getCellText(row, signonsTree.columns.getNamedColumn("passwordCol")); +} + +function synthesizeDblClickOnCell(aTree, column, row) { + let tbo = aTree.treeBoxObject; + let rect = tbo.getCoordsForCellItem(row, aTree.columns[column], "text"); + let x = rect.x + rect.width / 2; + let y = rect.y + rect.height / 2; + // Simulate the double click. + EventUtils.synthesizeMouse(aTree.body, x, y, { clickCount: 2 }, + aTree.ownerDocument.defaultView); +} + +function* togglePasswords() { + pwmgrdlg.document.querySelector("#togglePasswords").doCommand(); + yield new Promise(resolve => waitForFocus(resolve, pwmgrdlg)); +} + +function* editUsernamePromises(site, oldUsername, newUsername) { + is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login found"); + let login = Services.logins.findLogins({}, site, "", "")[0]; + is(login.username, oldUsername, "Correct username saved"); + is(getUsername(0), oldUsername, "Correct username shown"); + synthesizeDblClickOnCell(signonsTree, 1, 0); + yield ContentTaskUtils.waitForCondition(() => signonsTree.getAttribute("editing"), + "Waiting for editing"); + + EventUtils.sendString(newUsername, pwmgrdlg); + let signonsIntro = doc.querySelector("#signonsIntro"); + EventUtils.sendMouseEvent({type: "click"}, signonsIntro, pwmgrdlg); + yield ContentTaskUtils.waitForCondition(() => !signonsTree.getAttribute("editing"), + "Waiting for editing to stop"); + + is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login replaced"); + login = Services.logins.findLogins({}, site, "", "")[0]; + is(login.username, newUsername, "Correct username updated"); + is(getUsername(0), newUsername, "Correct username shown after the update"); +} + +function* editPasswordPromises(site, oldPassword, newPassword) { + is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login found"); + let login = Services.logins.findLogins({}, site, "", "")[0]; + is(login.password, oldPassword, "Correct password saved"); + is(getPassword(0), oldPassword, "Correct password shown"); + + synthesizeDblClickOnCell(signonsTree, 2, 0); + yield ContentTaskUtils.waitForCondition(() => signonsTree.getAttribute("editing"), + "Waiting for editing"); + + EventUtils.sendString(newPassword, pwmgrdlg); + let signonsIntro = doc.querySelector("#signonsIntro"); + EventUtils.sendMouseEvent({type: "click"}, signonsIntro, pwmgrdlg); + yield ContentTaskUtils.waitForCondition(() => !signonsTree.getAttribute("editing"), + "Waiting for editing to stop"); + + is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login replaced"); + login = Services.logins.findLogins({}, site, "", "")[0]; + is(login.password, newPassword, "Correct password updated"); + is(getPassword(0), newPassword, "Correct password shown after the update"); +} + +add_task(function* test_setup() { + registerCleanupFunction(function() { + Services.logins.removeAllLogins(); + }); + + Services.logins.removeAllLogins(); + // Open the password manager dialog. + pwmgrdlg = window.openDialog(PWMGR_DLG, "Toolkit:PasswordManager", ""); + + Services.ww.registerNotification(function (aSubject, aTopic, aData) { + if (aTopic == "domwindowopened") { + let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget); + SimpleTest.waitForFocus(function() { + EventUtils.sendKey("RETURN", win); + }, win); + } else if (aSubject.location == pwmgrdlg.location && aTopic == "domwindowclosed") { + // Unregister ourself. + Services.ww.unregisterNotification(arguments.callee); + } + }); + + yield new Promise((resolve) => { + SimpleTest.waitForFocus(() => { + doc = pwmgrdlg.document; + signonsTree = doc.querySelector("#signonsTree"); + resolve(); + }, pwmgrdlg); + }); +}); + +add_task(function* test_edit_multiple_logins() { + function* testLoginChange(site, oldUsername, oldPassword, newUsername, newPassword) { + addLogin(site, oldUsername, oldPassword); + yield* editUsernamePromises(site, oldUsername, newUsername); + yield* togglePasswords(); + yield* editPasswordPromises(site, oldPassword, newPassword); + yield* togglePasswords(); + } + + yield* testLoginChange("http://c.tn/", "userC", "passC", "usernameC", "passwordC"); + yield* testLoginChange("http://b.tn/", "userB", "passB", "usernameB", "passwordB"); + yield* testLoginChange("http://a.tn/", "userA", "passA", "usernameA", "passwordA"); + + pwmgrdlg.close(); +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_fields.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_fields.js new file mode 100644 index 000000000..95bcee9ed --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_fields.js @@ -0,0 +1,65 @@ +/* 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/. */ + +function test() { + waitForExplicitFinish(); + + let pwmgr = Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); + pwmgr.removeAllLogins(); + + // add login data + let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, "init"); + let login = new nsLoginInfo("http://example.com/", "http://example.com/", null, + "user", "password", "u1", "p1"); + pwmgr.addLogin(login); + + // Open the password manager dialog + const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul"; + let pwmgrdlg = window.openDialog(PWMGR_DLG, "Toolkit:PasswordManager", ""); + SimpleTest.waitForFocus(doTest, pwmgrdlg); + + function doTest() { + let doc = pwmgrdlg.document; + + let signonsTree = doc.querySelector("#signonsTree"); + is(signonsTree.view.rowCount, 1, "One entry in the passwords list"); + + is(signonsTree.view.getCellText(0, signonsTree.columns.getNamedColumn("siteCol")), + "http://example.com/", + "Correct website saved"); + + is(signonsTree.view.getCellText(0, signonsTree.columns.getNamedColumn("userCol")), + "user", + "Correct user saved"); + + let timeCreatedCol = doc.getElementById("timeCreatedCol"); + is(timeCreatedCol.getAttribute("hidden"), "true", + "Time created column is not displayed"); + + + let timeLastUsedCol = doc.getElementById("timeLastUsedCol"); + is(timeLastUsedCol.getAttribute("hidden"), "true", + "Last Used column is not displayed"); + + let timePasswordChangedCol = doc.getElementById("timePasswordChangedCol"); + is(timePasswordChangedCol.getAttribute("hidden"), "", + "Last Changed column is displayed"); + + // cleanup + Services.ww.registerNotification(function (aSubject, aTopic, aData) { + if (aSubject.location == pwmgrdlg.location && aTopic == "domwindowclosed") { + // unregister ourself + Services.ww.unregisterNotification(arguments.callee); + + pwmgr.removeAllLogins(); + + finish(); + } + }); + + pwmgrdlg.close(); + } +} diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_observers.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_observers.js new file mode 100644 index 000000000..1dc7076aa --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_observers.js @@ -0,0 +1,129 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + waitForExplicitFinish(); + + const LOGIN_HOST = "http://example.com"; + const LOGIN_COUNT = 5; + + let nsLoginInfo = new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); + let pmDialog = window.openDialog( + "chrome://passwordmgr/content/passwordManager.xul", + "Toolkit:PasswordManager", ""); + + let logins = []; + let loginCounter = 0; + let loginOrder = null; + let modifiedLogin; + let testNumber = 0; + let testObserver = { + observe: function (subject, topic, data) { + if (topic == "passwordmgr-dialog-updated") { + switch (testNumber) { + case 1: + case 2: + case 3: + case 4: + case 5: + is(countLogins(), loginCounter, "Verify login added"); + ok(getLoginOrder().startsWith(loginOrder), "Verify login order"); + runNextTest(); + break; + case 6: + is(countLogins(), loginCounter, "Verify login count"); + is(getLoginOrder(), loginOrder, "Verify login order"); + is(getLoginPassword(), "newpassword0", "Verify login modified"); + runNextTest(); + break; + case 7: + is(countLogins(), loginCounter, "Verify login removed"); + ok(loginOrder.endsWith(getLoginOrder()), "Verify login order"); + runNextTest(); + break; + case 8: + is(countLogins(), 0, "Verify all logins removed"); + runNextTest(); + break; + } + } + } + }; + + SimpleTest.waitForFocus(startTest, pmDialog); + + function createLogins() { + let login; + for (let i = 0; i < LOGIN_COUNT; i++) { + login = new nsLoginInfo(LOGIN_HOST + "?n=" + i, LOGIN_HOST + "?n=" + i, + null, "user" + i, "password" + i, "u" + i, "p" + i); + logins.push(login); + } + modifiedLogin = new nsLoginInfo(LOGIN_HOST + "?n=0", LOGIN_HOST + "?n=0", + null, "user0", "newpassword0", "u0", "p0"); + is(logins.length, LOGIN_COUNT, "Verify logins created"); + } + + function countLogins() { + let doc = pmDialog.document; + let signonsTree = doc.getElementById("signonsTree"); + return signonsTree.view.rowCount; + } + + function getLoginOrder() { + let doc = pmDialog.document; + let signonsTree = doc.getElementById("signonsTree"); + let column = signonsTree.columns[0]; // host column + let order = []; + for (let i = 0; i < signonsTree.view.rowCount; i++) { + order.push(signonsTree.view.getCellText(i, column)); + } + return order.join(','); + } + + function getLoginPassword() { + let doc = pmDialog.document; + let loginsTree = doc.getElementById("signonsTree"); + let column = loginsTree.columns[2]; // password column + return loginsTree.view.getCellText(0, column); + } + + function startTest() { + Services.obs.addObserver( + testObserver, "passwordmgr-dialog-updated", false); + is(countLogins(), 0, "Verify starts with 0 logins"); + createLogins(); + runNextTest(); + } + + function runNextTest() { + switch (++testNumber) { + case 1: // add the logins + for (let i = 0; i < logins.length; i++) { + loginCounter++; + loginOrder = getLoginOrder(); + Services.logins.addLogin(logins[i]); + } + break; + case 6: // modify a login + loginOrder = getLoginOrder(); + Services.logins.modifyLogin(logins[0], modifiedLogin); + break; + case 7: // remove a login + loginCounter--; + loginOrder = getLoginOrder(); + Services.logins.removeLogin(modifiedLogin); + break; + case 8: // remove all logins + Services.logins.removeAllLogins(); + break; + case 9: // finish + Services.obs.removeObserver( + testObserver, "passwordmgr-dialog-updated", false); + pmDialog.close(); + finish(); + break; + } + } +} diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_sort.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_sort.js new file mode 100644 index 000000000..83272a9c4 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_sort.js @@ -0,0 +1,208 @@ +/* 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/. */ + +function test() { + waitForExplicitFinish(); + + let pwmgr = Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); + pwmgr.removeAllLogins(); + + // Add some initial logins + let urls = [ + "http://example.com/", + "http://example.org/", + "http://mozilla.com/", + "http://mozilla.org/", + "http://spreadfirefox.com/", + "http://planet.mozilla.org/", + "https://developer.mozilla.org/", + "http://hg.mozilla.org/", + "http://dxr.mozilla.org/", + "http://feeds.mozilla.org/", + ]; + let users = [ + "user", + "username", + "ehsan", + "ehsan", + "john", + "what?", + "really?", + "you sure?", + "my user name", + "my username", + ]; + let pwds = [ + "password", + "password", + "mypass", + "mypass", + "smith", + "very secret", + "super secret", + "absolutely", + "mozilla", + "mozilla.com", + ]; + let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, "init"); + for (let i = 0; i < 10; i++) + pwmgr.addLogin(new nsLoginInfo(urls[i], urls[i], null, users[i], pwds[i], + "u" + (i + 1), "p" + (i + 1))); + + // Open the password manager dialog + const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul"; + let pwmgrdlg = window.openDialog(PWMGR_DLG, "Toolkit:PasswordManager", ""); + SimpleTest.waitForFocus(doTest, pwmgrdlg); + + // the meat of the test + function doTest() { + let doc = pwmgrdlg.document; + let win = doc.defaultView; + let sTree = doc.getElementById("signonsTree"); + let filter = doc.getElementById("filter"); + let siteCol = doc.getElementById("siteCol"); + let userCol = doc.getElementById("userCol"); + let passwordCol = doc.getElementById("passwordCol"); + + let toggleCalls = 0; + function toggleShowPasswords(func) { + let toggleButton = doc.getElementById("togglePasswords"); + let showMode = (toggleCalls++ % 2) == 0; + + // only watch for a confirmation dialog every other time being called + if (showMode) { + Services.ww.registerNotification(function (aSubject, aTopic, aData) { + if (aTopic == "domwindowclosed") + Services.ww.unregisterNotification(arguments.callee); + else if (aTopic == "domwindowopened") { + let targetWin = aSubject.QueryInterface(Ci.nsIDOMEventTarget); + SimpleTest.waitForFocus(function() { + EventUtils.sendKey("RETURN", targetWin); + }, targetWin); + } + }); + } + + Services.obs.addObserver(function (aSubject, aTopic, aData) { + if (aTopic == "passwordmgr-password-toggle-complete") { + Services.obs.removeObserver(arguments.callee, aTopic); + func(); + } + }, "passwordmgr-password-toggle-complete", false); + + EventUtils.synthesizeMouse(toggleButton, 1, 1, {}, win); + } + + function clickCol(col) { + EventUtils.synthesizeMouse(col, 20, 1, {}, win); + setTimeout(runNextTest, 0); + } + + function setFilter(string) { + filter.value = string; + filter.doCommand(); + setTimeout(runNextTest, 0); + } + + function checkSortMarkers(activeCol) { + let isOk = true; + let col = null; + let hasAttr = false; + let treecols = activeCol.parentNode; + for (let i = 0; i < treecols.childNodes.length; i++) { + col = treecols.childNodes[i]; + if (col.nodeName != "treecol") + continue; + hasAttr = col.hasAttribute("sortDirection"); + isOk &= col == activeCol ? hasAttr : !hasAttr; + } + ok(isOk, "Only " + activeCol.id + " has a sort marker"); + } + + function checkSortDirection(col, ascending) { + checkSortMarkers(col); + let direction = ascending ? "ascending" : "descending"; + is(col.getAttribute("sortDirection"), direction, + col.id + ": sort direction is " + direction); + } + + function checkColumnEntries(aCol, expectedValues) { + let actualValues = getColumnEntries(aCol); + is(actualValues.length, expectedValues.length, "Checking length of expected column"); + for (let i = 0; i < expectedValues.length; i++) + is(actualValues[i], expectedValues[i], "Checking column entry #" + i); + } + + function getColumnEntries(aCol) { + let entries = []; + let column = sTree.columns[aCol]; + let numRows = sTree.view.rowCount; + for (let i = 0; i < numRows; i++) + entries.push(sTree.view.getCellText(i, column)); + return entries; + } + + let testCounter = 0; + let expectedValues; + function runNextTest() { + switch (testCounter++) { + case 0: + expectedValues = urls.slice().sort(); + checkColumnEntries(0, expectedValues); + checkSortDirection(siteCol, true); + // Toggle sort direction on Host column + clickCol(siteCol); + break; + case 1: + expectedValues.reverse(); + checkColumnEntries(0, expectedValues); + checkSortDirection(siteCol, false); + // Sort by Username + clickCol(userCol); + break; + case 2: + expectedValues = users.slice().sort(); + checkColumnEntries(1, expectedValues); + checkSortDirection(userCol, true); + // Sort by Password + clickCol(passwordCol); + break; + case 3: + expectedValues = pwds.slice().sort(); + checkColumnEntries(2, expectedValues); + checkSortDirection(passwordCol, true); + // Set filter + setFilter("moz"); + break; + case 4: + expectedValues = [ "absolutely", "mozilla", "mozilla.com", + "mypass", "mypass", "super secret", + "very secret" ]; + checkColumnEntries(2, expectedValues); + checkSortDirection(passwordCol, true); + // Reset filter + setFilter(""); + break; + case 5: + expectedValues = pwds.slice().sort(); + checkColumnEntries(2, expectedValues); + checkSortDirection(passwordCol, true); + // cleanup + Services.ww.registerNotification(function (aSubject, aTopic, aData) { + // unregister ourself + Services.ww.unregisterNotification(arguments.callee); + + pwmgr.removeAllLogins(); + finish(); + }); + pwmgrdlg.close(); + } + } + + // Toggle Show Passwords to display Password column, then start tests + toggleShowPasswords(runNextTest); + } +} diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_switchtab.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_switchtab.js new file mode 100644 index 000000000..bd4f265b5 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_switchtab.js @@ -0,0 +1,42 @@ +/* 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/. */ + +const PROMPT_URL = "chrome://global/content/commonDialog.xul"; +var { interfaces: Ci } = Components; + +function test() { + waitForExplicitFinish(); + + let tab = gBrowser.addTab(); + isnot(tab, gBrowser.selectedTab, "New tab shouldn't be selected"); + + let listener = { + onOpenWindow: function(window) { + var domwindow = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + waitForFocus(() => { + is(domwindow.document.location.href, PROMPT_URL, "Should have seen a prompt window"); + is(domwindow.args.promptType, "promptUserAndPass", "Should be an authenticate prompt"); + + is(gBrowser.selectedTab, tab, "Should have selected the new tab"); + + domwindow.document.documentElement.cancelDialog(); + }, domwindow); + }, + + onCloseWindow: function() { + } + }; + + Services.wm.addListener(listener); + registerCleanupFunction(() => { + Services.wm.removeListener(listener); + gBrowser.removeTab(tab); + }); + + tab.linkedBrowser.addEventListener("load", () => { + finish(); + }, true); + tab.linkedBrowser.loadURI("http://example.com/browser/toolkit/components/passwordmgr/test/browser/authenticate.sjs"); +} diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgrdlg.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgrdlg.js new file mode 100644 index 000000000..57cfa9f83 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgrdlg.js @@ -0,0 +1,192 @@ +/* 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/. */ + +function test() { + waitForExplicitFinish(); + + let pwmgr = Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); + pwmgr.removeAllLogins(); + + // Add some initial logins + let urls = [ + "http://example.com/", + "http://example.org/", + "http://mozilla.com/", + "http://mozilla.org/", + "http://spreadfirefox.com/", + "http://planet.mozilla.org/", + "https://developer.mozilla.org/", + "http://hg.mozilla.org/", + "http://dxr.mozilla.org/", + "http://feeds.mozilla.org/", + ]; + let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, "init"); + let logins = [ + new nsLoginInfo(urls[0], urls[0], null, "user", "password", "u1", "p1"), + new nsLoginInfo(urls[1], urls[1], null, "username", "password", "u2", "p2"), + new nsLoginInfo(urls[2], urls[2], null, "ehsan", "mypass", "u3", "p3"), + new nsLoginInfo(urls[3], urls[3], null, "ehsan", "mypass", "u4", "p4"), + new nsLoginInfo(urls[4], urls[4], null, "john", "smith", "u5", "p5"), + new nsLoginInfo(urls[5], urls[5], null, "what?", "very secret", "u6", "p6"), + new nsLoginInfo(urls[6], urls[6], null, "really?", "super secret", "u7", "p7"), + new nsLoginInfo(urls[7], urls[7], null, "you sure?", "absolutely", "u8", "p8"), + new nsLoginInfo(urls[8], urls[8], null, "my user name", "mozilla", "u9", "p9"), + new nsLoginInfo(urls[9], urls[9], null, "my username", "mozilla.com", "u10", "p10"), + ]; + logins.forEach(login => pwmgr.addLogin(login)); + + // Open the password manager dialog + const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul"; + let pwmgrdlg = window.openDialog(PWMGR_DLG, "Toolkit:PasswordManager", ""); + SimpleTest.waitForFocus(doTest, pwmgrdlg); + + // the meat of the test + function doTest() { + let doc = pwmgrdlg.document; + let win = doc.defaultView; + let filter = doc.getElementById("filter"); + let tree = doc.getElementById("signonsTree"); + let view = tree.view; + + is(filter.value, "", "Filter box should initially be empty"); + is(view.rowCount, 10, "There should be 10 passwords initially"); + + // Prepare a set of tests + // filter: the text entered in the filter search box + // count: the number of logins which should match the respective filter + // count2: the number of logins which should match the respective filter + // if the passwords are being shown as well + // Note: if a test doesn't have count2 set, count is used instead. + let tests = [ + {filter: "pass", count: 0, count2: 4}, + {filter: "", count: 10}, // test clearing the filter + {filter: "moz", count: 7}, + {filter: "mozi", count: 7}, + {filter: "mozil", count: 7}, + {filter: "mozill", count: 7}, + {filter: "mozilla", count: 7}, + {filter: "mozilla.com", count: 1, count2: 2}, + {filter: "user", count: 4}, + {filter: "user ", count: 1}, + {filter: " user", count: 2}, + {filter: "http", count: 10}, + {filter: "https", count: 1}, + {filter: "secret", count: 0, count2: 2}, + {filter: "secret!", count: 0}, + ]; + + let toggleCalls = 0; + function toggleShowPasswords(func) { + let toggleButton = doc.getElementById("togglePasswords"); + let showMode = (toggleCalls++ % 2) == 0; + + // only watch for a confirmation dialog every other time being called + if (showMode) { + Services.ww.registerNotification(function (aSubject, aTopic, aData) { + if (aTopic == "domwindowclosed") + Services.ww.unregisterNotification(arguments.callee); + else if (aTopic == "domwindowopened") { + let targetWin = aSubject.QueryInterface(Ci.nsIDOMEventTarget); + SimpleTest.waitForFocus(function() { + EventUtils.sendKey("RETURN", targetWin); + }, targetWin); + } + }); + } + + Services.obs.addObserver(function (aSubject, aTopic, aData) { + if (aTopic == "passwordmgr-password-toggle-complete") { + Services.obs.removeObserver(arguments.callee, aTopic); + func(); + } + }, "passwordmgr-password-toggle-complete", false); + + EventUtils.synthesizeMouse(toggleButton, 1, 1, {}, win); + } + + function runTests(mode, endFunction) { + let testCounter = 0; + + function setFilter(string) { + filter.value = string; + filter.doCommand(); + } + + function runOneTest(testCase) { + function tester() { + is(view.rowCount, expected, expected + " logins should match '" + testCase.filter + "'"); + } + + let expected; + switch (mode) { + case 1: // without showing passwords + expected = testCase.count; + break; + case 2: // showing passwords + expected = ("count2" in testCase) ? testCase.count2 : testCase.count; + break; + case 3: // toggle + expected = testCase.count; + tester(); + toggleShowPasswords(function () { + expected = ("count2" in testCase) ? testCase.count2 : testCase.count; + tester(); + toggleShowPasswords(proceed); + }); + return; + } + tester(); + proceed(); + } + + function proceed() { + // run the next test if necessary or proceed with the tests + if (testCounter != tests.length) + runNextTest(); + else + endFunction(); + } + + function runNextTest() { + let testCase = tests[testCounter++]; + setFilter(testCase.filter); + setTimeout(runOneTest, 0, testCase); + } + + runNextTest(); + } + + function step1() { + runTests(1, step2); + } + + function step2() { + toggleShowPasswords(function() { + runTests(2, step3); + }); + } + + function step3() { + toggleShowPasswords(function() { + runTests(3, lastStep); + }); + } + + function lastStep() { + // cleanup + Services.ww.registerNotification(function (aSubject, aTopic, aData) { + // unregister ourself + Services.ww.unregisterNotification(arguments.callee); + + pwmgr.removeAllLogins(); + finish(); + }); + pwmgrdlg.close(); + } + + step1(); + } +} diff --git a/toolkit/components/passwordmgr/test/browser/browser_username_select_dialog.js b/toolkit/components/passwordmgr/test/browser/browser_username_select_dialog.js new file mode 100644 index 000000000..8df89b510 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_username_select_dialog.js @@ -0,0 +1,144 @@ +/* + * Test username selection dialog, on password update from a p-only form, + * when there are multiple saved logins on the domain. + */ + +// Copied from prompt_common.js. TODO: share the code. +function getSelectDialogDoc() { + // Trudge through all the open windows, until we find the one + // that has selectDialog.xul loaded. + var wm = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + // var enumerator = wm.getEnumerator("navigator:browser"); + var enumerator = wm.getXULWindowEnumerator(null); + + while (enumerator.hasMoreElements()) { + var win = enumerator.getNext(); + var windowDocShell = win.QueryInterface(Ci.nsIXULWindow).docShell; + + var containedDocShells = windowDocShell.getDocShellEnumerator( + Ci.nsIDocShellTreeItem.typeChrome, + Ci.nsIDocShell.ENUMERATE_FORWARDS); + while (containedDocShells.hasMoreElements()) { + // Get the corresponding document for this docshell + var childDocShell = containedDocShells.getNext(); + // We don't want it if it's not done loading. + if (childDocShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE) + continue; + var childDoc = childDocShell.QueryInterface(Ci.nsIDocShell) + .contentViewer + .DOMDocument; + + if (childDoc.location.href == "chrome://global/content/selectDialog.xul") + return childDoc; + } + } + + return null; +} + +let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, "init"); +let login1 = new nsLoginInfo("http://example.com", "http://example.com", null, + "notifyu1", "notifyp1", "user", "pass"); +let login1B = new nsLoginInfo("http://example.com", "http://example.com", null, + "notifyu1B", "notifyp1B", "user", "pass"); + +add_task(function* test_changeUPLoginOnPUpdateForm_accept() { + info("Select an u+p login from multiple logins, on password update form, and accept."); + Services.logins.addLogin(login1); + Services.logins.addLogin(login1B); + + yield testSubmittingLoginForm("subtst_notifications_change_p.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "pass2", "Checking submitted password"); + + yield ContentTaskUtils.waitForCondition(() => { + return getSelectDialogDoc(); + }, "Wait for selection dialog to be accessible."); + + let doc = getSelectDialogDoc(); + let dialog = doc.getElementsByTagName("dialog")[0]; + let listbox = doc.getElementById("list"); + + is(listbox.selectedIndex, 0, "Checking selected index"); + is(listbox.itemCount, 2, "Checking selected length"); + ['notifyu1', 'notifyu1B'].forEach((username, i) => { + is(listbox.getItemAtIndex(i).label, username, "Check username selection on dialog"); + }); + + dialog.acceptDialog(); + + yield ContentTaskUtils.waitForCondition(() => { + return !getSelectDialogDoc(); + }, "Wait for selection dialog to disappear."); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 2, "Should have 2 logins"); + + let login = SpecialPowers.wrap(logins[0]).QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username unchanged"); + is(login.password, "pass2", "Check the password changed"); + is(login.timesUsed, 2, "Check times used"); + + login = SpecialPowers.wrap(logins[1]).QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1B", "Check the username unchanged"); + is(login.password, "notifyp1B", "Check the password unchanged"); + is(login.timesUsed, 1, "Check times used"); + + // cleanup + login1.password = "pass2"; + Services.logins.removeLogin(login1); + login1.password = "notifyp1"; + + Services.logins.removeLogin(login1B); +}); + +add_task(function* test_changeUPLoginOnPUpdateForm_cancel() { + info("Select an u+p login from multiple logins, on password update form, and cancel."); + Services.logins.addLogin(login1); + Services.logins.addLogin(login1B); + + yield testSubmittingLoginForm("subtst_notifications_change_p.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "pass2", "Checking submitted password"); + + yield ContentTaskUtils.waitForCondition(() => { + return getSelectDialogDoc(); + }, "Wait for selection dialog to be accessible."); + + let doc = getSelectDialogDoc(); + let dialog = doc.getElementsByTagName("dialog")[0]; + let listbox = doc.getElementById("list"); + + is(listbox.selectedIndex, 0, "Checking selected index"); + is(listbox.itemCount, 2, "Checking selected length"); + ['notifyu1', 'notifyu1B'].forEach((username, i) => { + is(listbox.getItemAtIndex(i).label, username, "Check username selection on dialog"); + }); + + dialog.cancelDialog(); + + yield ContentTaskUtils.waitForCondition(() => { + return !getSelectDialogDoc(); + }, "Wait for selection dialog to disappear."); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 2, "Should have 2 logins"); + + let login = SpecialPowers.wrap(logins[0]).QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username unchanged"); + is(login.password, "notifyp1", "Check the password unchanged"); + is(login.timesUsed, 1, "Check times used"); + + login = SpecialPowers.wrap(logins[1]).QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1B", "Check the username unchanged"); + is(login.password, "notifyp1B", "Check the password unchanged"); + is(login.timesUsed, 1, "Check times used"); + + // cleanup + Services.logins.removeLogin(login1); + Services.logins.removeLogin(login1B); +}); diff --git a/toolkit/components/passwordmgr/test/browser/form_autofocus_js.html b/toolkit/components/passwordmgr/test/browser/form_autofocus_js.html new file mode 100644 index 000000000..76056e375 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/form_autofocus_js.html @@ -0,0 +1,10 @@ + + + +
+ + + +
+ + diff --git a/toolkit/components/passwordmgr/test/browser/form_basic.html b/toolkit/components/passwordmgr/test/browser/form_basic.html new file mode 100644 index 000000000..df2083a93 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/form_basic.html @@ -0,0 +1,12 @@ + + + + +
+ + + +
+ + diff --git a/toolkit/components/passwordmgr/test/browser/form_basic_iframe.html b/toolkit/components/passwordmgr/test/browser/form_basic_iframe.html new file mode 100644 index 000000000..616f56947 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/form_basic_iframe.html @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/toolkit/components/passwordmgr/test/browser/form_cross_origin_insecure_action.html b/toolkit/components/passwordmgr/test/browser/form_cross_origin_insecure_action.html new file mode 100644 index 000000000..e8aa8b215 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/form_cross_origin_insecure_action.html @@ -0,0 +1,12 @@ + + + + +
+ + + +
+ + diff --git a/toolkit/components/passwordmgr/test/browser/form_cross_origin_secure_action.html b/toolkit/components/passwordmgr/test/browser/form_cross_origin_secure_action.html new file mode 100644 index 000000000..892a9f6f6 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/form_cross_origin_secure_action.html @@ -0,0 +1,12 @@ + + + + +
+ + + +
+ + diff --git a/toolkit/components/passwordmgr/test/browser/form_same_origin_action.html b/toolkit/components/passwordmgr/test/browser/form_same_origin_action.html new file mode 100644 index 000000000..8f0c9a14e --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/form_same_origin_action.html @@ -0,0 +1,12 @@ + + + + +
+ + + +
+ + diff --git a/toolkit/components/passwordmgr/test/browser/formless_basic.html b/toolkit/components/passwordmgr/test/browser/formless_basic.html new file mode 100644 index 000000000..2f4c5de52 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/formless_basic.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/toolkit/components/passwordmgr/test/browser/head.js b/toolkit/components/passwordmgr/test/browser/head.js new file mode 100644 index 000000000..926cb6616 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/head.js @@ -0,0 +1,137 @@ +const DIRECTORY_PATH = "/browser/toolkit/components/passwordmgr/test/browser/"; + +Cu.import("resource://testing-common/LoginTestUtils.jsm", this); +Cu.import("resource://testing-common/ContentTaskUtils.jsm", this); + +registerCleanupFunction(function* cleanup_removeAllLoginsAndResetRecipes() { + Services.logins.removeAllLogins(); + + let recipeParent = LoginTestUtils.recipes.getRecipeParent(); + if (!recipeParent) { + // No need to reset the recipes if the recipe module wasn't even loaded. + return; + } + yield recipeParent.then(recipeParentResult => recipeParentResult.reset()); +}); + +/** + * Loads a test page in `DIRECTORY_URL` which automatically submits to formsubmit.sjs and returns a + * promise resolving with the field values when the optional `aTaskFn` is done. + * + * @param {String} aPageFile - test page file name which auto-submits to formsubmit.sjs + * @param {Function} aTaskFn - task which can be run before the tab closes. + * @param {String} [aOrigin="http://example.com"] - origin of the server to use + * to load `aPageFile`. + */ +function testSubmittingLoginForm(aPageFile, aTaskFn, aOrigin = "http://example.com") { + return BrowserTestUtils.withNewTab({ + gBrowser, + url: aOrigin + DIRECTORY_PATH + aPageFile, + }, function*(browser) { + ok(true, "loaded " + aPageFile); + let fieldValues = yield ContentTask.spawn(browser, undefined, function*() { + yield ContentTaskUtils.waitForCondition(() => { + return content.location.pathname.endsWith("/formsubmit.sjs") && + content.document.readyState == "complete"; + }, "Wait for form submission load (formsubmit.sjs)"); + let username = content.document.getElementById("user").textContent; + let password = content.document.getElementById("pass").textContent; + return { + username, + password, + }; + }); + ok(true, "form submission loaded"); + if (aTaskFn) { + yield* aTaskFn(fieldValues); + } + return fieldValues; + }); +} + +function checkOnlyLoginWasUsedTwice({ justChanged }) { + // Check to make sure we updated the timestamps and use count on the + // existing login that was submitted for the test. + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + ok(logins[0] instanceof Ci.nsILoginMetaInfo, "metainfo QI"); + is(logins[0].timesUsed, 2, "check .timesUsed for existing login submission"); + ok(logins[0].timeCreated < logins[0].timeLastUsed, "timeLastUsed bumped"); + if (justChanged) { + is(logins[0].timeLastUsed, logins[0].timePasswordChanged, "timeLastUsed == timePasswordChanged"); + } else { + is(logins[0].timeCreated, logins[0].timePasswordChanged, "timeChanged not updated"); + } +} + +// Begin popup notification (doorhanger) functions // + +const REMEMBER_BUTTON = 0; +const NEVER_BUTTON = 1; + +const CHANGE_BUTTON = 0; +const DONT_CHANGE_BUTTON = 1; + +/** + * Checks if we have a password capture popup notification + * of the right type and with the right label. + * + * @param {String} aKind The desired `passwordNotificationType` + * @param {Object} [popupNotifications = PopupNotifications] + * @return the found password popup notification. + */ +function getCaptureDoorhanger(aKind, popupNotifications = PopupNotifications) { + ok(true, "Looking for " + aKind + " popup notification"); + let notification = popupNotifications.getNotification("password"); + if (notification) { + is(notification.options.passwordNotificationType, aKind, "Notification type matches."); + if (aKind == "password-change") { + is(notification.mainAction.label, "Update", "Main action label matches update doorhanger."); + } else if (aKind == "password-save") { + is(notification.mainAction.label, "Remember", "Main action label matches save doorhanger."); + } + } + return notification; +} + +/** + * Clicks the specified popup notification button. + * + * @param {Element} aPopup Popup Notification element + * @param {Number} aButtonIndex Number indicating which button to click. + * See the constants in this file. + */ +function clickDoorhangerButton(aPopup, aButtonIndex) { + ok(true, "Looking for action at index " + aButtonIndex); + + let notifications = aPopup.owner.panel.childNodes; + ok(notifications.length > 0, "at least one notification displayed"); + ok(true, notifications.length + " notification(s)"); + let notification = notifications[0]; + + if (aButtonIndex == 0) { + ok(true, "Triggering main action"); + notification.button.doCommand(); + } else if (aButtonIndex <= aPopup.secondaryActions.length) { + ok(true, "Triggering secondary action " + aButtonIndex); + notification.childNodes[aButtonIndex].doCommand(); + } +} + +/** + * Checks the doorhanger's username and password. + * + * @param {String} username The username. + * @param {String} password The password. + */ +function* checkDoorhangerUsernamePassword(username, password) { + yield BrowserTestUtils.waitForCondition(() => { + return document.getElementById("password-notification-username").value == username; + }, "Wait for nsLoginManagerPrompter writeDataToUI()"); + is(document.getElementById("password-notification-username").value, username, + "Check doorhanger username"); + is(document.getElementById("password-notification-password").value, password, + "Check doorhanger password"); +} + +// End popup notification (doorhanger) functions // diff --git a/toolkit/components/passwordmgr/test/browser/insecure_test.html b/toolkit/components/passwordmgr/test/browser/insecure_test.html new file mode 100644 index 000000000..fedea1428 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/insecure_test.html @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/toolkit/components/passwordmgr/test/browser/streamConverter_content.sjs b/toolkit/components/passwordmgr/test/browser/streamConverter_content.sjs new file mode 100644 index 000000000..84c75437e --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/streamConverter_content.sjs @@ -0,0 +1,6 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(request, response) { + response.setHeader("Content-Type", "test/content", false); +} diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_1.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_1.html new file mode 100644 index 000000000..b96faf2ee --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_1.html @@ -0,0 +1,29 @@ + + + + + Subtest for Login Manager notifications - Basic 1un 1pw + + +

Subtest 1

+
+ + + +
+ + + + diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_10.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_10.html new file mode 100644 index 000000000..2dc96b4fd --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_10.html @@ -0,0 +1,27 @@ + + + + + Subtest for Login Manager notifications + + +

Subtest 10

+
+ + +
+ + + + diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_11.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_11.html new file mode 100644 index 000000000..cf3df5275 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_11.html @@ -0,0 +1,25 @@ + + + + + Subtest for Login Manager notifications - Popup Windows + + +

Subtest 11 (popup windows)

+ + + diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_11_popup.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_11_popup.html new file mode 100644 index 000000000..2e8e4135c --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_11_popup.html @@ -0,0 +1,32 @@ + + + + + Subtest for Login Manager notifications + + +

Subtest 11

+
+ + + +
+ + + + diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_2.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2.html new file mode 100644 index 000000000..72651d6c1 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2.html @@ -0,0 +1,30 @@ + + + + + Subtest for Login Manager notifications - autocomplete=off on the username field + + +

Subtest 2

+(username autocomplete=off) +
+ + + +
+ + + + diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_0un.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_0un.html new file mode 100644 index 000000000..7ddbf0851 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_0un.html @@ -0,0 +1,27 @@ + + + + + Subtest for Login Manager notifications with 2 password fields and no username + + +

Subtest 24

+
+ + + +
+ + + + diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_1un_1text.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_1un_1text.html new file mode 100644 index 000000000..893f18724 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_1un_1text.html @@ -0,0 +1,31 @@ + + + + + Subtest for Login Manager notifications with 2 password fields and 1 username field and one other text field before the first password field + + +

1 username field followed by a text field followed by 2 username fields

+
+ + + + + +
+ + + + diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_3.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_3.html new file mode 100644 index 000000000..291e735d0 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_3.html @@ -0,0 +1,30 @@ + + + + + Subtest for Login Manager notifications - autocomplete=off on the password field + + +

Subtest 3

+(password autocomplete=off) +
+ + + +
+ + + + diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_4.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_4.html new file mode 100644 index 000000000..63df3a42d --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_4.html @@ -0,0 +1,30 @@ + + + + + Subtest for Login Manager notifications + + +

Subtest 4

+(form autocomplete=off) +
+ + + +
+ + + + diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_5.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_5.html new file mode 100644 index 000000000..72a3df95f --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_5.html @@ -0,0 +1,26 @@ + + + + + Subtest for Login Manager notifications - Form with only a username field + + +

Subtest 5

+
+ + +
+ + + + diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_6.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_6.html new file mode 100644 index 000000000..47e23e972 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_6.html @@ -0,0 +1,27 @@ + + + + + Subtest for Login Manager notifications + + +

Subtest 6

+(password-only form) +
+ + +
+ + + + diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_8.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_8.html new file mode 100644 index 000000000..abeea4262 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_8.html @@ -0,0 +1,29 @@ + + + + + Subtest for Login Manager notifications + + +

Subtest 8

+
+ + + +
+ + + + diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_9.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_9.html new file mode 100644 index 000000000..c6f741068 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_9.html @@ -0,0 +1,29 @@ + + + + + Subtest for Login Manager notifications + + +

Subtest 9

+
+ + + +
+ + + + diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_change_p.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_change_p.html new file mode 100644 index 000000000..d74f3bcdf --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_change_p.html @@ -0,0 +1,32 @@ + + + + + Subtest for Login Manager notifications + + +

Change password

+
+ + + + +
+ + + + diff --git a/toolkit/components/passwordmgr/test/chrome/chrome.ini b/toolkit/components/passwordmgr/test/chrome/chrome.ini new file mode 100644 index 000000000..093b87b7d --- /dev/null +++ b/toolkit/components/passwordmgr/test/chrome/chrome.ini @@ -0,0 +1,13 @@ +[DEFAULT] +skip-if = os == 'android' + +[test_privbrowsing_perwindowpb.html] +skip-if = true # Bug 1173337 +support-files = + ../formsubmit.sjs + notification_common.js + privbrowsing_perwindowpb_iframe.html + subtst_privbrowsing_1.html + subtst_privbrowsing_2.html + subtst_privbrowsing_3.html + subtst_privbrowsing_4.html diff --git a/toolkit/components/passwordmgr/test/chrome/notification_common.js b/toolkit/components/passwordmgr/test/chrome/notification_common.js new file mode 100644 index 000000000..e8a52929d --- /dev/null +++ b/toolkit/components/passwordmgr/test/chrome/notification_common.js @@ -0,0 +1,111 @@ +/* + * Initialization: for each test, remove any prior notifications. + */ +function cleanUpPopupNotifications() { + var container = getPopupNotifications(window.top); + var notes = container._currentNotifications; + info(true, "Removing " + notes.length + " popup notifications."); + for (var i = notes.length - 1; i >= 0; i--) { + notes[i].remove(); + } +} +cleanUpPopupNotifications(); + +/* + * getPopupNotifications + * + * Fetches the popup notification for the specified window. + */ +function getPopupNotifications(aWindow) { + var Ci = SpecialPowers.Ci; + var Cc = SpecialPowers.Cc; + ok(Ci != null, "Access Ci"); + ok(Cc != null, "Access Cc"); + + var chromeWin = SpecialPowers.wrap(aWindow) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler.ownerDocument.defaultView; + + var popupNotifications = chromeWin.PopupNotifications; + return popupNotifications; +} + + +/** + * Checks if we have a password popup notification + * of the right type and with the right label. + * + * @deprecated Write a browser-chrome test instead and use the fork of this method there. + * @returns the found password popup notification. + */ +function getPopup(aPopupNote, aKind) { + ok(true, "Looking for " + aKind + " popup notification"); + var notification = aPopupNote.getNotification("password"); + if (notification) { + is(notification.options.passwordNotificationType, aKind, "Notification type matches."); + if (aKind == "password-change") { + is(notification.mainAction.label, "Update", "Main action label matches update doorhanger."); + } else if (aKind == "password-save") { + is(notification.mainAction.label, "Remember", "Main action label matches save doorhanger."); + } + } + return notification; +} + + +/** + * @deprecated - Use a browser chrome test instead. + * + * Clicks the specified popup notification button. + */ +function clickPopupButton(aPopup, aButtonIndex) { + ok(true, "Looking for action at index " + aButtonIndex); + + var notifications = SpecialPowers.wrap(aPopup.owner).panel.childNodes; + ok(notifications.length > 0, "at least one notification displayed"); + ok(true, notifications.length + " notifications"); + var notification = notifications[0]; + + if (aButtonIndex == 0) { + ok(true, "Triggering main action"); + notification.button.doCommand(); + } else if (aButtonIndex <= aPopup.secondaryActions.length) { + var index = aButtonIndex; + ok(true, "Triggering secondary action " + index); + notification.childNodes[index].doCommand(); + } +} + +const kRememberButton = 0; +const kNeverButton = 1; + +const kChangeButton = 0; +const kDontChangeButton = 1; + +function dumpNotifications() { + try { + // PopupNotifications + var container = getPopupNotifications(window.top); + ok(true, "is popup panel open? " + container.isPanelOpen); + var notes = container._currentNotifications; + ok(true, "Found " + notes.length + " popup notifications."); + for (let i = 0; i < notes.length; i++) { + ok(true, "#" + i + ": " + notes[i].id); + } + + // Notification bars + var chromeWin = SpecialPowers.wrap(window.top) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler.ownerDocument.defaultView; + var nb = chromeWin.getNotificationBox(window.top); + notes = nb.allNotifications; + ok(true, "Found " + notes.length + " notification bars."); + for (let i = 0; i < notes.length; i++) { + ok(true, "#" + i + ": " + notes[i].getAttribute("value")); + } + } catch (e) { todo(false, "WOAH! " + e); } +} diff --git a/toolkit/components/passwordmgr/test/chrome/privbrowsing_perwindowpb_iframe.html b/toolkit/components/passwordmgr/test/chrome/privbrowsing_perwindowpb_iframe.html new file mode 100644 index 000000000..2efdab265 --- /dev/null +++ b/toolkit/components/passwordmgr/test/chrome/privbrowsing_perwindowpb_iframe.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_1.html b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_1.html new file mode 100644 index 000000000..8c7202dd0 --- /dev/null +++ b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_1.html @@ -0,0 +1,33 @@ + + + + + Subtest for Login Manager notifications (private browsing) + + +

Subtest 1

+ +
+ + + +
+ + + + diff --git a/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_2.html b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_2.html new file mode 100644 index 000000000..bf3b85159 --- /dev/null +++ b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_2.html @@ -0,0 +1,33 @@ + + + + + Subtest for Login Manager notifications (private browsing) + + +

Subtest 2

+ +
+ + + +
+ + + + diff --git a/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_3.html b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_3.html new file mode 100644 index 000000000..e88a302e0 --- /dev/null +++ b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_3.html @@ -0,0 +1,29 @@ + + + + + Subtest for Login Manager notifications (private browsing) + + +

Subtest 3

+ +
+ + + +
+ + + + diff --git a/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_4.html b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_4.html new file mode 100644 index 000000000..184142743 --- /dev/null +++ b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_4.html @@ -0,0 +1,40 @@ + + + + + Subtest for Login Manager notifications (private browsing) + + + +

Subtest 4

+ +
+ + + +
+ + + + diff --git a/toolkit/components/passwordmgr/test/chrome/test_privbrowsing_perwindowpb.html b/toolkit/components/passwordmgr/test/chrome/test_privbrowsing_perwindowpb.html new file mode 100644 index 000000000..6b7d4abb3 --- /dev/null +++ b/toolkit/components/passwordmgr/test/chrome/test_privbrowsing_perwindowpb.html @@ -0,0 +1,322 @@ + + + + + + Test for Private Browsing + + + + + +Mozilla Bug 248970 +

+
+
+
+ + + diff --git a/toolkit/components/passwordmgr/test/chrome_timeout.js b/toolkit/components/passwordmgr/test/chrome_timeout.js new file mode 100644 index 000000000..9049d0bea --- /dev/null +++ b/toolkit/components/passwordmgr/test/chrome_timeout.js @@ -0,0 +1,11 @@ +const Cc = Components.classes; +const Ci = Components.interfaces; + +addMessageListener('setTimeout', msg => { + let timer = Cc['@mozilla.org/timer;1'].createInstance(Ci.nsITimer); + timer.init(_ => { + sendAsyncMessage('timeout'); + }, msg.delay, Ci.nsITimer.TYPE_ONE_SHOT); +}); + +sendAsyncMessage('ready'); diff --git a/toolkit/components/passwordmgr/test/formsubmit.sjs b/toolkit/components/passwordmgr/test/formsubmit.sjs new file mode 100644 index 000000000..4b4a387f7 --- /dev/null +++ b/toolkit/components/passwordmgr/test/formsubmit.sjs @@ -0,0 +1,37 @@ +function handleRequest(request, response) +{ + try { + reallyHandleRequest(request, response); + } catch (e) { + response.setStatusLine("1.0", 200, "AlmostOK"); + response.write("Error handling request: " + e); + } +} + + +function reallyHandleRequest(request, response) { + var match; + var requestAuth = true; + + // XXX I bet this doesn't work for POST requests. + var query = request.queryString; + + var user = null, pass = null; + // user=xxx + match = /user=([^&]*)/.exec(query); + if (match) + user = match[1]; + + // pass=xxx + match = /pass=([^&]*)/.exec(query); + if (match) + pass = match[1]; + + response.setStatusLine("1.0", 200, "OK"); + + response.setHeader("Content-Type", "application/xhtml+xml", false); + response.write(""); + response.write("

User: " + user + "

\n"); + response.write("

Pass: " + pass + "

\n"); + response.write(""); +} diff --git a/toolkit/components/passwordmgr/test/mochitest.ini b/toolkit/components/passwordmgr/test/mochitest.ini new file mode 100644 index 000000000..640f5c256 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest.ini @@ -0,0 +1,20 @@ +[DEFAULT] +skip-if = e10s +support-files = + authenticate.sjs + blank.html + formsubmit.sjs + prompt_common.js + pwmgr_common.js + subtst_master_pass.html + subtst_prompt_async.html + chrome_timeout.js + +[test_master_password.html] +skip-if = toolkit == 'android' # Tests desktop prompts +[test_prompt_async.html] +skip-if = toolkit == 'android' # Tests desktop prompts +[test_xhr.html] +skip-if = toolkit == 'android' # Tests desktop prompts +[test_xml_load.html] +skip-if = toolkit == 'android' # Tests desktop prompts diff --git a/toolkit/components/passwordmgr/test/mochitest/auth2/authenticate.sjs b/toolkit/components/passwordmgr/test/mochitest/auth2/authenticate.sjs new file mode 100644 index 000000000..d2f650013 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/auth2/authenticate.sjs @@ -0,0 +1,220 @@ +function handleRequest(request, response) +{ + try { + reallyHandleRequest(request, response); + } catch (e) { + response.setStatusLine("1.0", 200, "AlmostOK"); + response.write("Error handling request: " + e); + } +} + + +function reallyHandleRequest(request, response) { + var match; + var requestAuth = true, requestProxyAuth = true; + + // Allow the caller to drive how authentication is processed via the query. + // Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar + // The extra ? allows the user/pass/realm checks to succeed if the name is + // at the beginning of the query string. + var query = "?" + request.queryString; + + var expected_user = "", expected_pass = "", realm = "mochitest"; + var proxy_expected_user = "", proxy_expected_pass = "", proxy_realm = "mochi-proxy"; + var huge = false, plugin = false, anonymous = false; + var authHeaderCount = 1; + // user=xxx + match = /[^_]user=([^&]*)/.exec(query); + if (match) + expected_user = match[1]; + + // pass=xxx + match = /[^_]pass=([^&]*)/.exec(query); + if (match) + expected_pass = match[1]; + + // realm=xxx + match = /[^_]realm=([^&]*)/.exec(query); + if (match) + realm = match[1]; + + // proxy_user=xxx + match = /proxy_user=([^&]*)/.exec(query); + if (match) + proxy_expected_user = match[1]; + + // proxy_pass=xxx + match = /proxy_pass=([^&]*)/.exec(query); + if (match) + proxy_expected_pass = match[1]; + + // proxy_realm=xxx + match = /proxy_realm=([^&]*)/.exec(query); + if (match) + proxy_realm = match[1]; + + // huge=1 + match = /huge=1/.exec(query); + if (match) + huge = true; + + // plugin=1 + match = /plugin=1/.exec(query); + if (match) + plugin = true; + + // multiple=1 + match = /multiple=([^&]*)/.exec(query); + if (match) + authHeaderCount = match[1]+0; + + // anonymous=1 + match = /anonymous=1/.exec(query); + if (match) + anonymous = true; + + // Look for an authentication header, if any, in the request. + // + // EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== + // + // This test only supports Basic auth. The value sent by the client is + // "username:password", obscured with base64 encoding. + + var actual_user = "", actual_pass = "", authHeader, authPresent = false; + if (request.hasHeader("Authorization")) { + authPresent = true; + authHeader = request.getHeader("Authorization"); + match = /Basic (.+)/.exec(authHeader); + if (match.length != 2) + throw new Error("Couldn't parse auth header: " + authHeader); + + var userpass = base64ToString(match[1]); // no atob() :-( + match = /(.*):(.*)/.exec(userpass); + if (match.length != 3) + throw new Error("Couldn't decode auth header: " + userpass); + actual_user = match[1]; + actual_pass = match[2]; + } + + var proxy_actual_user = "", proxy_actual_pass = ""; + if (request.hasHeader("Proxy-Authorization")) { + authHeader = request.getHeader("Proxy-Authorization"); + match = /Basic (.+)/.exec(authHeader); + if (match.length != 2) + throw new Error("Couldn't parse auth header: " + authHeader); + + var userpass = base64ToString(match[1]); // no atob() :-( + match = /(.*):(.*)/.exec(userpass); + if (match.length != 3) + throw new Error("Couldn't decode auth header: " + userpass); + proxy_actual_user = match[1]; + proxy_actual_pass = match[2]; + } + + // Don't request authentication if the credentials we got were what we + // expected. + if (expected_user == actual_user && + expected_pass == actual_pass) { + requestAuth = false; + } + if (proxy_expected_user == proxy_actual_user && + proxy_expected_pass == proxy_actual_pass) { + requestProxyAuth = false; + } + + if (anonymous) { + if (authPresent) { + response.setStatusLine("1.0", 400, "Unexpected authorization header found"); + } else { + response.setStatusLine("1.0", 200, "Authorization header not found"); + } + } else { + if (requestProxyAuth) { + response.setStatusLine("1.0", 407, "Proxy authentication required"); + for (i = 0; i < authHeaderCount; ++i) + response.setHeader("Proxy-Authenticate", "basic realm=\"" + proxy_realm + "\"", true); + } else if (requestAuth) { + response.setStatusLine("1.0", 401, "Authentication required"); + for (i = 0; i < authHeaderCount; ++i) + response.setHeader("WWW-Authenticate", "basic realm=\"" + realm + "\"", true); + } else { + response.setStatusLine("1.0", 200, "OK"); + } + } + + response.setHeader("Content-Type", "application/xhtml+xml", false); + response.write(""); + response.write("

Login: " + (requestAuth ? "FAIL" : "PASS") + "

\n"); + response.write("

Proxy: " + (requestProxyAuth ? "FAIL" : "PASS") + "

\n"); + response.write("

Auth: " + authHeader + "

\n"); + response.write("

User: " + actual_user + "

\n"); + response.write("

Pass: " + actual_pass + "

\n"); + + if (huge) { + response.write("
"); + for (i = 0; i < 100000; i++) { + response.write("123456789\n"); + } + response.write("
"); + response.write("This is a footnote after the huge content fill"); + } + + if (plugin) { + response.write("\n"); + } + + response.write(""); +} + + +// base64 decoder +// +// Yoinked from extensions/xml-rpc/src/nsXmlRpcClient.js because btoa() +// doesn't seem to exist. :-( +/* Convert Base64 data to a string */ +const toBinaryTable = [ + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63, + 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, + 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1, + -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, + 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1 +]; +const base64Pad = '='; + +function base64ToString(data) { + + var result = ''; + var leftbits = 0; // number of bits decoded, but yet to be appended + var leftdata = 0; // bits decoded, but yet to be appended + + // Convert one by one. + for (var i = 0; i < data.length; i++) { + var c = toBinaryTable[data.charCodeAt(i) & 0x7f]; + var padding = (data[i] == base64Pad); + // Skip illegal characters and whitespace + if (c == -1) continue; + + // Collect data into leftdata, update bitcount + leftdata = (leftdata << 6) | c; + leftbits += 6; + + // If we have 8 or more bits, append 8 bits to the result + if (leftbits >= 8) { + leftbits -= 8; + // Append if not padding. + if (!padding) + result += String.fromCharCode((leftdata >> leftbits) & 0xff); + leftdata &= (1 << leftbits) - 1; + } + } + + // If there are any bits left, the base64 string was corrupted + if (leftbits) + throw Components.Exception('Corrupted base64 string'); + + return result; +} diff --git a/toolkit/components/passwordmgr/test/mochitest/mochitest.ini b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini new file mode 100644 index 000000000..a4170d7e0 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini @@ -0,0 +1,69 @@ +[DEFAULT] +support-files = + ../../../prompts/test/chromeScript.js + ../../../prompts/test/prompt_common.js + ../../../satchel/test/parent_utils.js + ../../../satchel/test/satchel_common.js + ../authenticate.sjs + ../blank.html + ../browser/form_autofocus_js.html + ../browser/form_basic.html + ../browser/form_cross_origin_secure_action.html + ../pwmgr_common.js + auth2/authenticate.sjs + +[test_autocomplete_https_upgrade.html] +skip-if = toolkit == 'android' # autocomplete +[test_autofill_https_upgrade.html] +skip-if = toolkit == 'android' # Bug 1259768 +[test_autofill_password-only.html] +[test_autofocus_js.html] +skip-if = toolkit == 'android' # autocomplete +[test_basic_form.html] +[test_basic_form_0pw.html] +[test_basic_form_1pw.html] +[test_basic_form_1pw_2.html] +[test_basic_form_2pw_1.html] +[test_basic_form_2pw_2.html] +[test_basic_form_3pw_1.html] +[test_basic_form_autocomplete.html] +skip-if = toolkit == 'android' # android:autocomplete. +[test_insecure_form_field_autocomplete.html] +skip-if = toolkit == 'android' # android:autocomplete. +[test_password_field_autocomplete.html] +skip-if = toolkit == 'android' # android:autocomplete. +[test_insecure_form_field_no_saved_login.html] +skip-if = toolkit == 'android' || os == 'linux' # android:autocomplete., linux: bug 1325778 +[test_basic_form_html5.html] +[test_basic_form_pwevent.html] +[test_basic_form_pwonly.html] +[test_bug_627616.html] +skip-if = toolkit == 'android' # Tests desktop prompts +[test_bug_776171.html] +[test_case_differences.html] +skip-if = toolkit == 'android' # autocomplete +[test_form_action_1.html] +[test_form_action_2.html] +[test_form_action_javascript.html] +[test_formless_autofill.html] +[test_formless_submit.html] +[test_formless_submit_navigation.html] +[test_formless_submit_navigation_negative.html] +[test_input_events.html] +[test_input_events_for_identical_values.html] +[test_maxlength.html] +[test_passwords_in_type_password.html] +[test_prompt.html] +skip-if = os == "linux" || toolkit == 'android' # Tests desktop prompts +[test_prompt_http.html] +skip-if = os == "linux" || toolkit == 'android' # Tests desktop prompts +[test_prompt_noWindow.html] +skip-if = e10s || toolkit == 'android' # Tests desktop prompts. e10s: bug 1217876 +[test_prompt_promptAuth.html] +skip-if = os == "linux" || toolkit == 'android' # Tests desktop prompts +[test_prompt_promptAuth_proxy.html] +skip-if = e10s || os == "linux" || toolkit == 'android' # Tests desktop prompts +[test_recipe_login_fields.html] +[test_username_focus.html] +skip-if = toolkit == 'android' # android:autocomplete. +[test_xhr_2.html] diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_upgrade.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_upgrade.html new file mode 100644 index 000000000..7d5725322 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_upgrade.html @@ -0,0 +1,218 @@ + + + + + Test autocomplete on an HTTPS page using upgraded HTTP logins + + + + + + + + + +

+ + +
+ +
+ +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html new file mode 100644 index 000000000..ee1424002 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html @@ -0,0 +1,117 @@ + + + + + Test autocomplete on an HTTPS page using upgraded HTTP logins + + + + + + + + + +

+ + +
+ +
+ +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_password-only.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_password-only.html new file mode 100644 index 000000000..983356371 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_password-only.html @@ -0,0 +1,143 @@ + + + + + Test password-only forms should prefer a password-only login when present + + + + + +Login Manager test: Bug 444968 + + +

+ +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html b/toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html new file mode 100644 index 000000000..2ce3293dd --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html @@ -0,0 +1,115 @@ + + + + + Test login autocomplete is activated when focused by js on load + + + + + + + + + +

+ +
+ +
+ +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form.html new file mode 100644 index 000000000..3c38343a5 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form.html @@ -0,0 +1,44 @@ + + + + + Test basic autofill + + + + + +Login Manager test: simple form fill + + + +

+ + + +

+
+
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_0pw.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_0pw.html
new file mode 100644
index 000000000..0b416673b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_0pw.html
@@ -0,0 +1,72 @@
+
+
+
+  
+  Test forms with no password fields
+  
+  
+  
+
+
+Login Manager test: forms with no password fields
+

+ + + +
+
+
+ + + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw.html new file mode 100644 index 000000000..3937fad4b --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw.html @@ -0,0 +1,167 @@ + + + + + Test autofill for forms with 1 password field + + + + + +Login Manager test: forms with 1 password field + +

+ + + +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw_2.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw_2.html new file mode 100644 index 000000000..0f6566b9c --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw_2.html @@ -0,0 +1,109 @@ + + + + + Test forms with 1 password field, part 2 + + + + + +Login Manager test: forms with 1 password field, part 2 + +

+ + + +
+
+
+ + + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_1.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_1.html new file mode 100644 index 000000000..128ffca7c --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_1.html @@ -0,0 +1,187 @@ + + + + + Test autofill for forms with 2 password fields + + + + + +Login Manager test: forms with 2 password fields + +

+ + + +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html new file mode 100644 index 000000000..eba811cf9 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html @@ -0,0 +1,105 @@ + + + + + Test for form fill with 2 password fields + + + + + +Login Manager test: form fill, 2 password fields +

+ +
+
+
+ + + + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_3pw_1.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_3pw_1.html new file mode 100644 index 000000000..30b5a319f --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_3pw_1.html @@ -0,0 +1,177 @@ + + + + + Test autofill for forms with 3 password fields + + + + + +Login Manager test: forms with 3 password fields (form filling) + +

+ + + +
+
+
+ + + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html new file mode 100644 index 000000000..0eee8e696 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html @@ -0,0 +1,859 @@ + + + + + Test basic login autocomplete + + + + + + + + +Login Manager test: multiple login autocomplete + + +

+ + +
+ + +
+ + + +
+ + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+
+ +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_html5.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_html5.html new file mode 100644 index 000000000..40e322afd --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_html5.html @@ -0,0 +1,164 @@ + + + + + Test for html5 input types (email, tel, url, etc.) + + + + + +Login Manager test: html5 input types (email, tel, url, etc.) + + +

+ +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwevent.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwevent.html new file mode 100644 index 000000000..e0a2883c8 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwevent.html @@ -0,0 +1,55 @@ + + + + + + Test for Bug 355063 + + + + + + +Mozilla Bug 355063 +

+
+forms go here! +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwonly.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwonly.html new file mode 100644 index 000000000..40fec8c46 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwonly.html @@ -0,0 +1,213 @@ + + + + + Test forms and logins without a username + + + + + +Login Manager test: forms and logins without a username. + +

+ + + +
+
+
+ + + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html b/toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html new file mode 100644 index 000000000..ad4a41cdb --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html @@ -0,0 +1,145 @@ + + + + + Test bug 627616 related to proxy authentication + + + + + + + + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_bug_776171.html b/toolkit/components/passwordmgr/test/mochitest/test_bug_776171.html new file mode 100644 index 000000000..4ad08bee2 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_bug_776171.html @@ -0,0 +1,56 @@ + + + + + + Test for Bug 776171 related to HTTP auth + + + + + + + + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_case_differences.html b/toolkit/components/passwordmgr/test/mochitest/test_case_differences.html new file mode 100644 index 000000000..316f59da7 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_case_differences.html @@ -0,0 +1,147 @@ + + + + + Test autocomplete due to multiple matching logins + + + + + + + + +Login Manager test: autocomplete due to multiple matching logins + + +

+ + +
+ + +
+ + + +
+ +
+ +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_form_action_1.html b/toolkit/components/passwordmgr/test/mochitest/test_form_action_1.html new file mode 100644 index 000000000..430081b3a --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_form_action_1.html @@ -0,0 +1,137 @@ + + + + + Test for considering form action + + + + + +Login Manager test: Bug 360493 + +

+ +
+
+
+ + + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_form_action_2.html b/toolkit/components/passwordmgr/test/mochitest/test_form_action_2.html new file mode 100644 index 000000000..0f0056de0 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_form_action_2.html @@ -0,0 +1,170 @@ + + + + + Test for considering form action + + + + + +Login Manager test: Bug 360493 + +

+ +
+
+
+ + + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_form_action_javascript.html b/toolkit/components/passwordmgr/test/mochitest/test_form_action_javascript.html new file mode 100644 index 000000000..d37e92c40 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_form_action_javascript.html @@ -0,0 +1,52 @@ + + + + + Test forms with a JS submit action + + + + + +Login Manager test: form with JS submit action + + +

+ + + +

+
+
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_autofill.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_autofill.html
new file mode 100644
index 000000000..6263c818d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_autofill.html
@@ -0,0 +1,147 @@
+
+
+
+  
+  Test autofilling of fields outside of a form
+  
+  
+  
+  
+
+
+
+
+

+ +
+ +
+

+
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html
new file mode 100644
index 000000000..468da1e7f
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html
@@ -0,0 +1,183 @@
+
+
+
+  
+  Test capturing of fields outside of a form
+  
+  
+  
+  
+
+
+
+
+

+ +
+ +
+

+
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html
new file mode 100644
index 000000000..b07d0886c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html
@@ -0,0 +1,191 @@
+
+
+
+  
+  Test capturing of fields outside of a form due to navigation
+  
+  
+  
+  
+
+
+
+
+

+ +
+ +
+

+
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html
new file mode 100644
index 000000000..4283f128c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html
@@ -0,0 +1,121 @@
+
+
+
+  
+  Test no capturing of fields outside of a form due to navigation
+  
+  
+  
+  
+
+
+
+
+

+ +
+ +
+

+
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_input_events.html b/toolkit/components/passwordmgr/test/mochitest/test_input_events.html
new file mode 100644
index 000000000..0e77956d8
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_input_events.html
@@ -0,0 +1,96 @@
+
+
+
+  
+  Test for input events in Login Manager
+  
+  
+  
+
+
+Login Manager test: input events should fire.
+
+
+
+

+ + +

+
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_input_events_for_identical_values.html b/toolkit/components/passwordmgr/test/mochitest/test_input_events_for_identical_values.html
new file mode 100644
index 000000000..d058a87f9
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_input_events_for_identical_values.html
@@ -0,0 +1,51 @@
+
+
+
+  
+  Test for input events in Login Manager when username/password are filled in already
+  
+  
+  
+  
+
+
+Login Manager test: input events should fire.
+
+
+
+

+ +
+ +
+

This is form 1.

+ + + + + +
+ +
+

+
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_autocomplete.html b/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_autocomplete.html
new file mode 100644
index 000000000..c5d0a44fa
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_autocomplete.html
@@ -0,0 +1,861 @@
+
+
+
+  
+  Test insecure form field autocomplete
+  
+  
+  
+  
+  
+  
+
+
+
+
+

+ + +
+ + +
+ + + +
+ + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+
+ +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_no_saved_login.html b/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_no_saved_login.html new file mode 100644 index 000000000..c3a894958 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_no_saved_login.html @@ -0,0 +1,103 @@ + + + + + Test basic login, contextual inscure password warning without saved logins + + + + + + + + +Login Manager test: contextual inscure password warning without saved logins + + +

+ + +
+ +
+ + + +
+ +
+ +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_maxlength.html b/toolkit/components/passwordmgr/test/mochitest/test_maxlength.html new file mode 100644 index 000000000..2b6da33ec --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_maxlength.html @@ -0,0 +1,137 @@ + + + + + Test for maxlength attributes + + + + + +Login Manager test: Bug 391514 + +

+ +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html b/toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html new file mode 100644 index 000000000..443c8a5e9 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html @@ -0,0 +1,291 @@ + + + + + Test basic login autocomplete + + + + + + + + +Login Manager test: multiple login autocomplete + + +

+ + +
+ + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_passwords_in_type_password.html b/toolkit/components/passwordmgr/test/mochitest/test_passwords_in_type_password.html new file mode 100644 index 000000000..e107cebe6 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_passwords_in_type_password.html @@ -0,0 +1,122 @@ + + + + + Test that passwords only get filled in type=password + + + + + +Login Manager test: Bug 242956 + +

+ +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt.html new file mode 100644 index 000000000..1050ab66b --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt.html @@ -0,0 +1,705 @@ + + + + + Test prompter.{prompt,promptPassword,promptUsernameAndPassword} + + + + + + + +

+ + + +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html new file mode 100644 index 000000000..0dc8fdf9c --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html @@ -0,0 +1,362 @@ + + + + + Test HTTP auth prompts by loading authenticate.sjs + + + + + + + +

+ + + +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html new file mode 100644 index 000000000..92af172ca --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html @@ -0,0 +1,81 @@ + + + + + Test HTTP auth prompts by loading authenticate.sjs with no window + + + + + + + +

+ + + +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html new file mode 100644 index 000000000..36f53a54a --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html @@ -0,0 +1,406 @@ + + + + + Test promptAuth prompts + + + + + + + +

+ + + +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html new file mode 100644 index 000000000..95dd4c7bc --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html @@ -0,0 +1,264 @@ + + + + + Test promptAuth proxy prompts + + + + + + + +

+ + + +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_recipe_login_fields.html b/toolkit/components/passwordmgr/test/mochitest/test_recipe_login_fields.html new file mode 100644 index 000000000..943bffc52 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_recipe_login_fields.html @@ -0,0 +1,145 @@ + + + + + Test for recipes overriding login fields + + + + + + + + +

+ +
+ // Forms are inserted dynamically +
+

+
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_username_focus.html b/toolkit/components/passwordmgr/test/mochitest/test_username_focus.html
new file mode 100644
index 000000000..c93c1e9c9
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_username_focus.html
@@ -0,0 +1,263 @@
+
+
+
+
+  
+  Test interaction between autocomplete and focus on username fields
+  
+  
+  
+  
+  
+  
+
+
+
+
+

+
+ + + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + + + + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ +
+
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_xhr_2.html b/toolkit/components/passwordmgr/test/mochitest/test_xhr_2.html new file mode 100644 index 000000000..fa8357792 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_xhr_2.html @@ -0,0 +1,55 @@ + + + + + + Test XHR auth with user and pass arguments + + + + + + + + diff --git a/toolkit/components/passwordmgr/test/prompt_common.js b/toolkit/components/passwordmgr/test/prompt_common.js new file mode 100644 index 000000000..267e697ae --- /dev/null +++ b/toolkit/components/passwordmgr/test/prompt_common.js @@ -0,0 +1,79 @@ +/** + * NOTE: + * This file is currently only being used for tests which haven't been + * fixed to work with e10s. Favor using the `prompt_common.js` file that + * is in `toolkit/components/prompts/test/` instead. + */ + +var Ci = SpecialPowers.Ci; +ok(Ci != null, "Access Ci"); +var Cc = SpecialPowers.Cc; +ok(Cc != null, "Access Cc"); + +var didDialog; + +var timer; // keep in outer scope so it's not GC'd before firing +function startCallbackTimer() { + didDialog = false; + + // Delay before the callback twiddles the prompt. + const dialogDelay = 10; + + // Use a timer to invoke a callback to twiddle the authentication dialog + timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.init(observer, dialogDelay, Ci.nsITimer.TYPE_ONE_SHOT); +} + + +var observer = SpecialPowers.wrapCallbackObject({ + QueryInterface : function (iid) { + const interfaces = [Ci.nsIObserver, + Ci.nsISupports, Ci.nsISupportsWeakReference]; + + if (!interfaces.some( function(v) { return iid.equals(v); } )) + throw SpecialPowers.Components.results.NS_ERROR_NO_INTERFACE; + return this; + }, + + observe : function (subject, topic, data) { + var doc = getDialogDoc(); + if (doc) + handleDialog(doc, testNum); + else + startCallbackTimer(); // try again in a bit + } +}); + +function getDialogDoc() { + // Find the which contains notifyWindow, by looking + // through all the open windows and all the in each. + var wm = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + // var enumerator = wm.getEnumerator("navigator:browser"); + var enumerator = wm.getXULWindowEnumerator(null); + + while (enumerator.hasMoreElements()) { + var win = enumerator.getNext(); + var windowDocShell = win.QueryInterface(Ci.nsIXULWindow).docShell; + + var containedDocShells = windowDocShell.getDocShellEnumerator( + Ci.nsIDocShellTreeItem.typeChrome, + Ci.nsIDocShell.ENUMERATE_FORWARDS); + while (containedDocShells.hasMoreElements()) { + // Get the corresponding document for this docshell + var childDocShell = containedDocShells.getNext(); + // We don't want it if it's not done loading. + if (childDocShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE) + continue; + var childDoc = childDocShell.QueryInterface(Ci.nsIDocShell) + .contentViewer + .DOMDocument; + + // ok(true, "Got window: " + childDoc.location.href); + if (childDoc.location.href == "chrome://global/content/commonDialog.xul") + return childDoc; + } + } + + return null; +} diff --git a/toolkit/components/passwordmgr/test/pwmgr_common.js b/toolkit/components/passwordmgr/test/pwmgr_common.js new file mode 100644 index 000000000..fa7c4fd85 --- /dev/null +++ b/toolkit/components/passwordmgr/test/pwmgr_common.js @@ -0,0 +1,509 @@ +const TESTS_DIR = "/tests/toolkit/components/passwordmgr/test/"; + +/** + * Returns the element with the specified |name| attribute. + */ +function $_(formNum, name) { + var form = document.getElementById("form" + formNum); + if (!form) { + logWarning("$_ couldn't find requested form " + formNum); + return null; + } + + var element = form.children.namedItem(name); + if (!element) { + logWarning("$_ couldn't find requested element " + name); + return null; + } + + // Note that namedItem is a bit stupid, and will prefer an + // |id| attribute over a |name| attribute when looking for + // the element. Login Mananger happens to use .namedItem + // anyway, but let's rigorously check it here anyway so + // that we don't end up with tests that mistakenly pass. + + if (element.getAttribute("name") != name) { + logWarning("$_ got confused."); + return null; + } + + return element; +} + +/** + * Check a form for expected values. If an argument is null, a field's + * expected value will be the default value. + * + *
+ * checkForm(#, "foo"); + */ +function checkForm(formNum, val1, val2, val3) { + var e, form = document.getElementById("form" + formNum); + ok(form, "Locating form " + formNum); + + var numToCheck = arguments.length - 1; + + if (!numToCheck--) + return; + e = form.elements[0]; + if (val1 == null) + is(e.value, e.defaultValue, "Test default value of field " + e.name + + " in form " + formNum); + else + is(e.value, val1, "Test value of field " + e.name + + " in form " + formNum); + + + if (!numToCheck--) + return; + e = form.elements[1]; + if (val2 == null) + is(e.value, e.defaultValue, "Test default value of field " + e.name + + " in form " + formNum); + else + is(e.value, val2, "Test value of field " + e.name + + " in form " + formNum); + + + if (!numToCheck--) + return; + e = form.elements[2]; + if (val3 == null) + is(e.value, e.defaultValue, "Test default value of field " + e.name + + " in form " + formNum); + else + is(e.value, val3, "Test value of field " + e.name + + " in form " + formNum); +} + +/** + * Check a form for unmodified values from when page was loaded. + * + * + * checkUnmodifiedForm(#); + */ +function checkUnmodifiedForm(formNum) { + var form = document.getElementById("form" + formNum); + ok(form, "Locating form " + formNum); + + for (var i = 0; i < form.elements.length; i++) { + var ele = form.elements[i]; + + // No point in checking form submit/reset buttons. + if (ele.type == "submit" || ele.type == "reset") + continue; + + is(ele.value, ele.defaultValue, "Test to default value of field " + + ele.name + " in form " + formNum); + } +} + +/** + * Mochitest gives us a sendKey(), but it's targeted to a specific element. + * This basically sends an untargeted key event, to whatever's focused. + */ +function doKey(aKey, modifier) { + var keyName = "DOM_VK_" + aKey.toUpperCase(); + var key = KeyEvent[keyName]; + + // undefined --> null + if (!modifier) + modifier = null; + + // Window utils for sending fake sey events. + var wutils = SpecialPowers.wrap(window). + QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor). + getInterface(SpecialPowers.Ci.nsIDOMWindowUtils); + + if (wutils.sendKeyEvent("keydown", key, 0, modifier)) { + wutils.sendKeyEvent("keypress", key, 0, modifier); + } + wutils.sendKeyEvent("keyup", key, 0, modifier); +} + +/** + * Init with a common login + * If selfFilling is true or non-undefined, fires an event at the page so that + * the test can start checking filled-in values. Tests that check observer + * notifications might be confused by this. + */ +function commonInit(selfFilling) { + var pwmgr = SpecialPowers.Cc["@mozilla.org/login-manager;1"]. + getService(SpecialPowers.Ci.nsILoginManager); + ok(pwmgr != null, "Access LoginManager"); + + // Check that initial state has no logins + var logins = pwmgr.getAllLogins(); + is(logins.length, 0, "Not expecting logins to be present"); + var disabledHosts = pwmgr.getAllDisabledHosts(); + if (disabledHosts.length) { + ok(false, "Warning: wasn't expecting disabled hosts to be present."); + for (var host of disabledHosts) + pwmgr.setLoginSavingEnabled(host, true); + } + + // Add a login that's used in multiple tests + var login = SpecialPowers.Cc["@mozilla.org/login-manager/loginInfo;1"]. + createInstance(SpecialPowers.Ci.nsILoginInfo); + login.init("http://mochi.test:8888", "http://mochi.test:8888", null, + "testuser", "testpass", "uname", "pword"); + pwmgr.addLogin(login); + + // Last sanity check + logins = pwmgr.getAllLogins(); + is(logins.length, 1, "Checking for successful init login"); + disabledHosts = pwmgr.getAllDisabledHosts(); + is(disabledHosts.length, 0, "Checking for no disabled hosts"); + + if (selfFilling) + return; + + if (this.sendAsyncMessage) { + sendAsyncMessage("registerRunTests"); + } else { + registerRunTests(); + } +} + +function registerRunTests() { + return new Promise(resolve => { + // We provide a general mechanism for our tests to know when they can + // safely run: we add a final form that we know will be filled in, wait + // for the login manager to tell us that it's filled in and then continue + // with the rest of the tests. + window.addEventListener("DOMContentLoaded", (event) => { + var form = document.createElement('form'); + form.id = 'observerforcer'; + var username = document.createElement('input'); + username.name = 'testuser'; + form.appendChild(username); + var password = document.createElement('input'); + password.name = 'testpass'; + password.type = 'password'; + form.appendChild(password); + + var observer = SpecialPowers.wrapCallback(function(subject, topic, data) { + var formLikeRoot = subject.QueryInterface(SpecialPowers.Ci.nsIDOMNode); + if (formLikeRoot.id !== 'observerforcer') + return; + SpecialPowers.removeObserver(observer, "passwordmgr-processed-form"); + formLikeRoot.remove(); + SimpleTest.executeSoon(() => { + var runTestEvent = new Event("runTests"); + window.dispatchEvent(runTestEvent); + resolve(); + }); + }); + SpecialPowers.addObserver(observer, "passwordmgr-processed-form", false); + + document.body.appendChild(form); + }); + }); +} + +const masterPassword = "omgsecret!"; + +function enableMasterPassword() { + setMasterPassword(true); +} + +function disableMasterPassword() { + setMasterPassword(false); +} + +function setMasterPassword(enable) { + var oldPW, newPW; + if (enable) { + oldPW = ""; + newPW = masterPassword; + } else { + oldPW = masterPassword; + newPW = ""; + } + // Set master password. Note that this does not log you in, so the next + // invocation of pwmgr can trigger a MP prompt. + + var pk11db = Cc["@mozilla.org/security/pk11tokendb;1"].getService(Ci.nsIPK11TokenDB); + var token = pk11db.findTokenByName(""); + info("MP change from " + oldPW + " to " + newPW); + token.changePassword(oldPW, newPW); +} + +function logoutMasterPassword() { + var sdr = Cc["@mozilla.org/security/sdr;1"].getService(Ci.nsISecretDecoderRing); + sdr.logoutAndTeardown(); +} + +function dumpLogins(pwmgr) { + var logins = pwmgr.getAllLogins(); + ok(true, "----- dumpLogins: have " + logins.length + " logins. -----"); + for (var i = 0; i < logins.length; i++) + dumpLogin("login #" + i + " --- ", logins[i]); +} + +function dumpLogin(label, login) { + var loginText = ""; + loginText += "host: "; + loginText += login.hostname; + loginText += " / formURL: "; + loginText += login.formSubmitURL; + loginText += " / realm: "; + loginText += login.httpRealm; + loginText += " / user: "; + loginText += login.username; + loginText += " / pass: "; + loginText += login.password; + loginText += " / ufield: "; + loginText += login.usernameField; + loginText += " / pfield: "; + loginText += login.passwordField; + ok(true, label + loginText); +} + +function getRecipeParent() { + var { LoginManagerParent } = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerParent.jsm", {}); + if (!LoginManagerParent.recipeParentPromise) { + return null; + } + return LoginManagerParent.recipeParentPromise.then((recipeParent) => { + return SpecialPowers.wrap(recipeParent); + }); +} + +/** + * Resolves when a specified number of forms have been processed. + */ +function promiseFormsProcessed(expectedCount = 1) { + var processedCount = 0; + return new Promise((resolve, reject) => { + function onProcessedForm(subject, topic, data) { + processedCount++; + if (processedCount == expectedCount) { + SpecialPowers.removeObserver(onProcessedForm, "passwordmgr-processed-form"); + resolve(SpecialPowers.Cu.waiveXrays(subject), data); + } + } + SpecialPowers.addObserver(onProcessedForm, "passwordmgr-processed-form", false); + }); +} + +function loadRecipes(recipes) { + info("Loading recipes"); + return new Promise(resolve => { + chromeScript.addMessageListener("loadedRecipes", function loaded() { + chromeScript.removeMessageListener("loadedRecipes", loaded); + resolve(recipes); + }); + chromeScript.sendAsyncMessage("loadRecipes", recipes); + }); +} + +function resetRecipes() { + info("Resetting recipes"); + return new Promise(resolve => { + chromeScript.addMessageListener("recipesReset", function reset() { + chromeScript.removeMessageListener("recipesReset", reset); + resolve(); + }); + chromeScript.sendAsyncMessage("resetRecipes"); + }); +} + +function promiseStorageChanged(expectedChangeTypes) { + return new Promise((resolve, reject) => { + function onStorageChanged({ topic, data }) { + let changeType = expectedChangeTypes.shift(); + is(data, changeType, "Check expected passwordmgr-storage-changed type"); + if (expectedChangeTypes.length === 0) { + chromeScript.removeMessageListener("storageChanged", onStorageChanged); + resolve(); + } + } + chromeScript.addMessageListener("storageChanged", onStorageChanged); + }); +} + +function promisePromptShown(expectedTopic) { + return new Promise((resolve, reject) => { + function onPromptShown({ topic, data }) { + is(topic, expectedTopic, "Check expected prompt topic"); + chromeScript.removeMessageListener("promptShown", onPromptShown); + resolve(); + } + chromeScript.addMessageListener("promptShown", onPromptShown); + }); +} + +/** + * Run a function synchronously in the parent process and destroy it in the test cleanup function. + * @param {Function|String} aFunctionOrURL - either a function that will be stringified and run + * or the URL to a JS file. + * @return {Object} - the return value of loadChromeScript providing message-related methods. + * @see loadChromeScript in specialpowersAPI.js + */ +function runInParent(aFunctionOrURL) { + let chromeScript = SpecialPowers.loadChromeScript(aFunctionOrURL); + SimpleTest.registerCleanupFunction(() => { + chromeScript.destroy(); + }); + return chromeScript; +} + +/** + * Run commonInit synchronously in the parent then run the test function after the runTests event. + * + * @param {Function} aFunction The test function to run + */ +function runChecksAfterCommonInit(aFunction = null) { + SimpleTest.waitForExplicitFinish(); + let pwmgrCommonScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js")); + if (aFunction) { + window.addEventListener("runTests", aFunction); + pwmgrCommonScript.addMessageListener("registerRunTests", () => registerRunTests()); + } + pwmgrCommonScript.sendSyncMessage("setupParent"); + return pwmgrCommonScript; +} + +// Code to run when loaded as a chrome script in tests via loadChromeScript +if (this.addMessageListener) { + const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; + var SpecialPowers = { Cc, Ci, Cr, Cu, }; + var ok, is; + // Ignore ok/is in commonInit since they aren't defined in a chrome script. + ok = is = () => {}; // eslint-disable-line no-native-reassign + + Cu.import("resource://gre/modules/LoginHelper.jsm"); + Cu.import("resource://gre/modules/LoginManagerParent.jsm"); + Cu.import("resource://gre/modules/Services.jsm"); + Cu.import("resource://gre/modules/Task.jsm"); + + function onStorageChanged(subject, topic, data) { + sendAsyncMessage("storageChanged", { + topic, + data, + }); + } + Services.obs.addObserver(onStorageChanged, "passwordmgr-storage-changed", false); + + function onPrompt(subject, topic, data) { + sendAsyncMessage("promptShown", { + topic, + data, + }); + } + Services.obs.addObserver(onPrompt, "passwordmgr-prompt-change", false); + Services.obs.addObserver(onPrompt, "passwordmgr-prompt-save", false); + + addMessageListener("setupParent", ({selfFilling = false} = {selfFilling: false}) => { + // Force LoginManagerParent to init for the tests since it's normally delayed + // by apps such as on Android. + LoginManagerParent.init(); + + commonInit(selfFilling); + sendAsyncMessage("doneSetup"); + }); + + addMessageListener("loadRecipes", Task.async(function*(recipes) { + var recipeParent = yield LoginManagerParent.recipeParentPromise; + yield recipeParent.load(recipes); + sendAsyncMessage("loadedRecipes", recipes); + })); + + addMessageListener("resetRecipes", Task.async(function*() { + let recipeParent = yield LoginManagerParent.recipeParentPromise; + yield recipeParent.reset(); + sendAsyncMessage("recipesReset"); + })); + + addMessageListener("proxyLoginManager", msg => { + // Recreate nsILoginInfo objects from vanilla JS objects. + let recreatedArgs = msg.args.map((arg, index) => { + if (msg.loginInfoIndices.includes(index)) { + return LoginHelper.vanillaObjectToLogin(arg); + } + + return arg; + }); + + let rv = Services.logins[msg.methodName](...recreatedArgs); + if (rv instanceof Ci.nsILoginInfo) { + rv = LoginHelper.loginToVanillaObject(rv); + } + return rv; + }); + + var globalMM = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager); + globalMM.addMessageListener("RemoteLogins:onFormSubmit", function onFormSubmit(message) { + sendAsyncMessage("formSubmissionProcessed", message.data, message.objects); + }); +} else { + // Code to only run in the mochitest pages (not in the chrome script). + SpecialPowers.pushPrefEnv({"set": [["signon.autofillForms.http", true], + ["security.insecure_field_warning.contextual.enabled", false]] + }); + + SimpleTest.registerCleanupFunction(() => { + SpecialPowers.popPrefEnv(); + runInParent(function cleanupParent() { + const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; + Cu.import("resource://gre/modules/Services.jsm"); + Cu.import("resource://gre/modules/LoginManagerParent.jsm"); + + // Remove all logins and disabled hosts + Services.logins.removeAllLogins(); + + let disabledHosts = Services.logins.getAllDisabledHosts(); + disabledHosts.forEach(host => Services.logins.setLoginSavingEnabled(host, true)); + + let authMgr = Cc["@mozilla.org/network/http-auth-manager;1"]. + getService(Ci.nsIHttpAuthManager); + authMgr.clearAll(); + + if (LoginManagerParent._recipeManager) { + LoginManagerParent._recipeManager.reset(); + } + + // Cleanup PopupNotifications (if on a relevant platform) + let chromeWin = Services.wm.getMostRecentWindow("navigator:browser"); + if (chromeWin && chromeWin.PopupNotifications) { + let notes = chromeWin.PopupNotifications._currentNotifications; + if (notes.length > 0) { + dump("Removing " + notes.length + " popup notifications.\n"); + } + for (let note of notes) { + note.remove(); + } + } + }); + }); + + + let { LoginHelper } = SpecialPowers.Cu.import("resource://gre/modules/LoginHelper.jsm", {}); + /** + * Proxy for Services.logins (nsILoginManager). + * Only supports arguments which support structured clone plus {nsILoginInfo} + * Assumes properties are methods. + */ + this.LoginManager = new Proxy({}, { + get(target, prop, receiver) { + return (...args) => { + let loginInfoIndices = []; + let cloneableArgs = args.map((val, index) => { + if (SpecialPowers.call_Instanceof(val, SpecialPowers.Ci.nsILoginInfo)) { + loginInfoIndices.push(index); + return LoginHelper.loginToVanillaObject(val); + } + + return val; + }); + + return chromeScript.sendSyncMessage("proxyLoginManager", { + args: cloneableArgs, + loginInfoIndices, + methodName: prop, + })[0][0]; + }; + }, + }); +} diff --git a/toolkit/components/passwordmgr/test/subtst_master_pass.html b/toolkit/components/passwordmgr/test/subtst_master_pass.html new file mode 100644 index 000000000..20211866a --- /dev/null +++ b/toolkit/components/passwordmgr/test/subtst_master_pass.html @@ -0,0 +1,12 @@ +

MP subtest

+This form triggers a MP and gets filled in.
+ +Username:
+Password:
+ +
diff --git a/toolkit/components/passwordmgr/test/subtst_prompt_async.html b/toolkit/components/passwordmgr/test/subtst_prompt_async.html new file mode 100644 index 000000000..f60f63814 --- /dev/null +++ b/toolkit/components/passwordmgr/test/subtst_prompt_async.html @@ -0,0 +1,12 @@ + + + + + Multiple auth request + + + + + + + diff --git a/toolkit/components/passwordmgr/test/test_master_password.html b/toolkit/components/passwordmgr/test/test_master_password.html new file mode 100644 index 000000000..c8884811f --- /dev/null +++ b/toolkit/components/passwordmgr/test/test_master_password.html @@ -0,0 +1,308 @@ + + + + + Test for master password + + + + + + +Login Manager test: master password. + + +

+ + + +
+
+
+ + + diff --git a/toolkit/components/passwordmgr/test/test_prompt_async.html b/toolkit/components/passwordmgr/test/test_prompt_async.html new file mode 100644 index 000000000..38b34679a --- /dev/null +++ b/toolkit/components/passwordmgr/test/test_prompt_async.html @@ -0,0 +1,540 @@ + + + + + Test for Async Auth Prompt + + + + + + + + + + + + + diff --git a/toolkit/components/passwordmgr/test/test_xhr.html b/toolkit/components/passwordmgr/test/test_xhr.html new file mode 100644 index 000000000..296371685 --- /dev/null +++ b/toolkit/components/passwordmgr/test/test_xhr.html @@ -0,0 +1,201 @@ + + + + + Test for XHR prompts + + + + + + +Login Manager test: XHR prompt +

+ + + +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/test_xml_load.html b/toolkit/components/passwordmgr/test/test_xml_load.html new file mode 100644 index 000000000..5672c7117 --- /dev/null +++ b/toolkit/components/passwordmgr/test/test_xml_load.html @@ -0,0 +1,191 @@ + + + + + Test XML document prompts + + + + + + +Login Manager test: XML prompt +

+ + + +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/unit/.eslintrc.js b/toolkit/components/passwordmgr/test/unit/.eslintrc.js new file mode 100644 index 000000000..d35787cd2 --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/components/passwordmgr/test/unit/data/corruptDB.sqlite b/toolkit/components/passwordmgr/test/unit/data/corruptDB.sqlite new file mode 100644 index 000000000..b234246ca Binary files /dev/null and b/toolkit/components/passwordmgr/test/unit/data/corruptDB.sqlite differ diff --git a/toolkit/components/passwordmgr/test/unit/data/key3.db b/toolkit/components/passwordmgr/test/unit/data/key3.db new file mode 100644 index 000000000..a83a0a577 Binary files /dev/null and b/toolkit/components/passwordmgr/test/unit/data/key3.db differ diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v1.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v1.sqlite new file mode 100644 index 000000000..fe030b61f Binary files /dev/null and b/toolkit/components/passwordmgr/test/unit/data/signons-v1.sqlite differ diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v1v2.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v1v2.sqlite new file mode 100644 index 000000000..729512a12 Binary files /dev/null and b/toolkit/components/passwordmgr/test/unit/data/signons-v1v2.sqlite differ diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v2.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v2.sqlite new file mode 100644 index 000000000..a6c72b31e Binary files /dev/null and b/toolkit/components/passwordmgr/test/unit/data/signons-v2.sqlite differ diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v2v3.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v2v3.sqlite new file mode 100644 index 000000000..359df5d31 Binary files /dev/null and b/toolkit/components/passwordmgr/test/unit/data/signons-v2v3.sqlite differ diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v3.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v3.sqlite new file mode 100644 index 000000000..918f4142f Binary files /dev/null and b/toolkit/components/passwordmgr/test/unit/data/signons-v3.sqlite differ diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v3v4.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v3v4.sqlite new file mode 100644 index 000000000..e06c33aae Binary files /dev/null and b/toolkit/components/passwordmgr/test/unit/data/signons-v3v4.sqlite differ diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v4.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v4.sqlite new file mode 100644 index 000000000..227c09c81 Binary files /dev/null and b/toolkit/components/passwordmgr/test/unit/data/signons-v4.sqlite differ diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v4v5.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v4v5.sqlite new file mode 100644 index 000000000..4534cf255 Binary files /dev/null and b/toolkit/components/passwordmgr/test/unit/data/signons-v4v5.sqlite differ diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v5v6.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v5v6.sqlite new file mode 100644 index 000000000..eb4ee6d01 Binary files /dev/null and b/toolkit/components/passwordmgr/test/unit/data/signons-v5v6.sqlite differ diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v999-2.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v999-2.sqlite new file mode 100644 index 000000000..e09c4f710 Binary files /dev/null and b/toolkit/components/passwordmgr/test/unit/data/signons-v999-2.sqlite differ diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v999.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v999.sqlite new file mode 100644 index 000000000..0328a1a02 Binary files /dev/null and b/toolkit/components/passwordmgr/test/unit/data/signons-v999.sqlite differ diff --git a/toolkit/components/passwordmgr/test/unit/head.js b/toolkit/components/passwordmgr/test/unit/head.js new file mode 100644 index 000000000..baf958ab4 --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/head.js @@ -0,0 +1,135 @@ +/** + * Provides infrastructure for automated login components tests. + */ + +"use strict"; + +// Globals + +let { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/LoginRecipes.jsm"); +Cu.import("resource://gre/modules/LoginHelper.jsm"); +Cu.import("resource://testing-common/MockDocument.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths", + "resource://gre/modules/DownloadPaths.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); + +const LoginInfo = + Components.Constructor("@mozilla.org/login-manager/loginInfo;1", + "nsILoginInfo", "init"); + +// Import LoginTestUtils.jsm as LoginTestUtils. +XPCOMUtils.defineLazyModuleGetter(this, "LoginTestUtils", + "resource://testing-common/LoginTestUtils.jsm"); +LoginTestUtils.Assert = Assert; +const TestData = LoginTestUtils.testData; +const newPropertyBag = LoginHelper.newPropertyBag; + +/** + * All the tests are implemented with add_task, this starts them automatically. + */ +function run_test() +{ + do_get_profile(); + run_next_test(); +} + +// Global helpers + +// Some of these functions are already implemented in other parts of the source +// tree, see bug 946708 about sharing more code. + +// While the previous test file should have deleted all the temporary files it +// used, on Windows these might still be pending deletion on the physical file +// system. Thus, start from a new base number every time, to make a collision +// with a file that is still pending deletion highly unlikely. +let gFileCounter = Math.floor(Math.random() * 1000000); + +/** + * Returns a reference to a temporary file, that is guaranteed not to exist, and + * to have never been created before. + * + * @param aLeafName + * Suggested leaf name for the file to be created. + * + * @return nsIFile pointing to a non-existent file in a temporary directory. + * + * @note It is not enough to delete the file if it exists, or to delete the file + * after calling nsIFile.createUnique, because on Windows the delete + * operation in the file system may still be pending, preventing a new + * file with the same name to be created. + */ +function getTempFile(aLeafName) +{ + // Prepend a serial number to the extension in the suggested leaf name. + let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName); + let leafName = base + "-" + gFileCounter + ext; + gFileCounter++; + + // Get a file reference under the temporary directory for this test file. + let file = FileUtils.getFile("TmpD", [leafName]); + do_check_false(file.exists()); + + do_register_cleanup(function () { + if (file.exists()) { + file.remove(false); + } + }); + + return file; +} + +const RecipeHelpers = { + initNewParent() { + return (new LoginRecipesParent({ defaults: null })).initializationPromise; + }, +}; + +// Initialization functions common to all tests + +add_task(function* test_common_initialize() +{ + // Before initializing the service for the first time, we should copy the key + // file required to decrypt the logins contained in the SQLite databases used + // by migration tests. This file is not required for the other tests. + yield OS.File.copy(do_get_file("data/key3.db").path, + OS.Path.join(OS.Constants.Path.profileDir, "key3.db")); + + // Ensure that the service and the storage module are initialized. + yield Services.logins.initializationPromise; + + // Ensure that every test file starts with an empty database. + LoginTestUtils.clearData(); + + // Clean up after every test. + do_register_cleanup(() => LoginTestUtils.clearData()); +}); + +/** + * Compare two FormLike to see if they represent the same information. Elements + * are compared using their @id attribute. + */ +function formLikeEqual(a, b) { + Assert.strictEqual(Object.keys(a).length, Object.keys(b).length, + "Check the formLikes have the same number of properties"); + + for (let propName of Object.keys(a)) { + if (propName == "elements") { + Assert.strictEqual(a.elements.length, b.elements.length, "Check element count"); + for (let i = 0; i < a.elements.length; i++) { + Assert.strictEqual(a.elements[i].id, b.elements[i].id, "Check element " + i + " id"); + } + continue; + } + Assert.strictEqual(a[propName], b[propName], "Compare formLike " + propName + " property"); + } +} diff --git a/toolkit/components/passwordmgr/test/unit/test_OSCrypto_win.js b/toolkit/components/passwordmgr/test/unit/test_OSCrypto_win.js new file mode 100644 index 000000000..94d2e50c0 --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_OSCrypto_win.js @@ -0,0 +1,75 @@ +/** + * Tests the OSCrypto object. + */ + +"use strict"; + +// Globals + +XPCOMUtils.defineLazyModuleGetter(this, "OSCrypto", + "resource://gre/modules/OSCrypto.jsm"); + +var crypto = new OSCrypto(); + +// Tests + +add_task(function test_getIELoginHash() +{ + do_check_eq(crypto.getIELoginHash("https://bugzilla.mozilla.org/page.cgi"), + "4A66FE96607885790F8E67B56EEE52AB539BAFB47D"); + + do_check_eq(crypto.getIELoginHash("https://github.com/login"), + "0112F7DCE67B8579EA01367678AA44AB9868B5A143"); + + do_check_eq(crypto.getIELoginHash("https://login.live.com/login.srf"), + "FBF92E5D804C82717A57856533B779676D92903688"); + + do_check_eq(crypto.getIELoginHash("https://preview.c9.io/riadh/w1/pass.1.html"), + "6935CF27628830605927F86AB53831016FC8973D1A"); + + + do_check_eq(crypto.getIELoginHash("https://reviewboard.mozilla.org/account/login/"), + "09141FD287E2E59A8B1D3BB5671537FD3D6B61337A"); + + do_check_eq(crypto.getIELoginHash("https://www.facebook.com/"), + "EF44D3E034009CB0FD1B1D81A1FF3F3335213BD796"); + +}); + +add_task(function test_decryptData_encryptData() +{ + function decryptEncryptTest(key) { + do_check_eq(crypto.decryptData(crypto.encryptData("", key), key), + ""); + + do_check_eq(crypto.decryptData(crypto.encryptData("secret", key), key), + "secret"); + + do_check_eq(crypto.decryptData(crypto.encryptData("https://www.mozilla.org", key), + key), + "https://www.mozilla.org"); + + do_check_eq(crypto.decryptData(crypto.encryptData("https://reviewboard.mozilla.org", key), + key), + "https://reviewboard.mozilla.org"); + + do_check_eq(crypto.decryptData(crypto.encryptData("https://bugzilla.mozilla.org/page.cgi", + key), + key), + "https://bugzilla.mozilla.org/page.cgi"); + } + + let keys = [null, "a", "keys", "abcdedf", "pass", "https://bugzilla.mozilla.org/page.cgi", + "https://login.live.com/login.srf"]; + for (let key of keys) { + decryptEncryptTest(key); + } + let url = "https://twitter.com/"; + let value = [1, 0, 0, 0, 208, 140, 157, 223, 1, 21, 209, 17, 140, 122, 0, 192, 79, 194, 151, 235, 1, 0, 0, 0, 254, 58, 230, 75, 132, 228, 181, 79, 184, 160, 37, 106, 201, 29, 42, 152, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 16, 102, 0, 0, 0, 1, 0, 0, 32, 0, 0, 0, 90, 136, 17, 124, 122, 57, 178, 24, 34, 86, 209, 198, 184, 107, 58, 58, 32, 98, 61, 239, 129, 101, 56, 239, 114, 159, 139, 165, 183, 40, 183, 85, 0, 0, 0, 0, 14, 128, 0, 0, 0, 2, 0, 0, 32, 0, 0, 0, 147, 170, 34, 21, 53, 227, 191, 6, 201, 84, 106, 31, 57, 227, 46, 127, 219, 199, 80, 142, 37, 104, 112, 223, 26, 165, 223, 55, 176, 89, 55, 37, 112, 0, 0, 0, 98, 70, 221, 109, 5, 152, 46, 11, 190, 213, 226, 58, 244, 20, 180, 217, 63, 155, 227, 132, 7, 151, 235, 6, 37, 232, 176, 182, 141, 191, 251, 50, 20, 123, 53, 11, 247, 233, 112, 121, 130, 27, 168, 68, 92, 144, 192, 7, 12, 239, 53, 217, 253, 155, 54, 109, 236, 216, 225, 245, 79, 234, 165, 225, 104, 36, 77, 13, 195, 237, 143, 165, 100, 107, 230, 70, 54, 19, 179, 35, 8, 101, 93, 202, 121, 210, 222, 28, 93, 122, 36, 84, 185, 249, 238, 3, 102, 149, 248, 94, 137, 16, 192, 22, 251, 220, 22, 223, 16, 58, 104, 187, 64, 0, 0, 0, 70, 72, 15, 119, 144, 66, 117, 203, 190, 82, 131, 46, 111, 130, 238, 191, 170, 63, 186, 117, 46, 88, 171, 3, 94, 146, 75, 86, 243, 159, 63, 195, 149, 25, 105, 141, 42, 217, 108, 18, 63, 62, 98, 182, 241, 195, 12, 216, 152, 230, 176, 253, 202, 129, 41, 185, 135, 111, 226, 92, 27, 78, 27, 198]; + + let arr1 = crypto.arrayToString(value); + let arr2 = crypto.stringToArray(crypto.decryptData(crypto.encryptData(arr1, url), url)); + for (let i = 0; i < arr1.length; i++) { + do_check_eq(arr2[i], value[i]); + } +}); diff --git a/toolkit/components/passwordmgr/test/unit/test_context_menu.js b/toolkit/components/passwordmgr/test/unit/test_context_menu.js new file mode 100644 index 000000000..722c13e15 --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_context_menu.js @@ -0,0 +1,165 @@ +/* + * Test the password manager context menu. + */ + +"use strict"; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/LoginManagerContextMenu.jsm"); + +XPCOMUtils.defineLazyGetter(this, "_stringBundle", function() { + return Services.strings. + createBundle("chrome://passwordmgr/locale/passwordmgr.properties"); +}); + +/** + * Prepare data for the following tests. + */ +add_task(function* test_initialize() { + for (let login of loginList()) { + Services.logins.addLogin(login); + } +}); + +/** + * Tests if the LoginManagerContextMenu returns the correct login items. + */ +add_task(function* test_contextMenuAddAndRemoveLogins() { + const DOCUMENT_CONTENT = "
"; + const INPUT_QUERY = "input[type='password']"; + + let testHostnames = [ + "http://www.example.com", + "http://www2.example.com", + "http://www3.example.com", + "http://empty.example.com", + ]; + + for (let hostname of testHostnames) { + do_print("test for hostname: " + hostname); + // Get expected logins for this test. + let logins = getExpectedLogins(hostname); + + // Create the logins menuitems fragment. + let {fragment, document} = createLoginsFragment(hostname, DOCUMENT_CONTENT, INPUT_QUERY); + + if (!logins.length) { + Assert.ok(fragment === null, "Null returned. No logins where found."); + continue; + } + let items = [...fragment.querySelectorAll("menuitem")]; + + // Check if the items are those expected to be listed. + Assert.ok(checkLoginItems(logins, items), "All expected logins found."); + document.body.appendChild(fragment); + + // Try to clear the fragment. + LoginManagerContextMenu.clearLoginsFromMenu(document); + Assert.equal(fragment.querySelectorAll("menuitem").length, 0, "All items correctly cleared."); + } + + Services.logins.removeAllLogins(); +}); + +/** + * Create a fragment with a menuitem for each login. + */ +function createLoginsFragment(url, content, elementQuery) { + const CHROME_URL = "chrome://mock-chrome"; + + // Create a mock document. + let document = MockDocument.createTestDocument(CHROME_URL, content); + let inputElement = document.querySelector(elementQuery); + MockDocument.mockOwnerDocumentProperty(inputElement, document, url); + + // We also need a simple mock Browser object for this test. + let browser = { + ownerDocument: document + }; + + let URI = Services.io.newURI(url, null, null); + return { + document, + fragment: LoginManagerContextMenu.addLoginsToMenu(inputElement, browser, URI), + }; +} + +/** + * Check if every login have it's corresponding menuitem. + * Duplicates and empty usernames have a date appended. + */ +function checkLoginItems(logins, items) { + function findDuplicates(unfilteredLoginList) { + var seen = new Set(); + var duplicates = new Set(); + for (let login of unfilteredLoginList) { + if (seen.has(login.username)) { + duplicates.add(login.username); + } + seen.add(login.username); + } + return duplicates; + } + let duplicates = findDuplicates(logins); + + let dateAndTimeFormatter = new Intl.DateTimeFormat(undefined, + { day: "numeric", month: "short", year: "numeric" }); + for (let login of logins) { + if (login.username && !duplicates.has(login.username)) { + // If login is not duplicate and we can't find an item for it, fail. + if (!items.find(item => item.label == login.username)) { + return false; + } + continue; + } + + let meta = login.QueryInterface(Ci.nsILoginMetaInfo); + let time = dateAndTimeFormatter.format(new Date(meta.timePasswordChanged)); + // If login is duplicate, check if we have a login item with appended date. + if (login.username && !items.find(item => item.label == login.username + " (" + time + ")")) { + return false; + } + // If login is empty, check if we have a login item with appended date. + if (!login.username && + !items.find(item => item.label == _stringBundle.GetStringFromName("noUsername") + " (" + time + ")")) { + return false; + } + } + return true; +} + +/** + * Gets the list of expected logins for a hostname. + */ +function getExpectedLogins(hostname) { + return Services.logins.getAllLogins().filter(entry => entry["hostname"] === hostname); +} + +function loginList() { + return [ + new LoginInfo("http://www.example.com", "http://www.example.com", null, + "username1", "password", + "form_field_username", "form_field_password"), + + new LoginInfo("http://www.example.com", "http://www.example.com", null, + "username2", "password", + "form_field_username", "form_field_password"), + + new LoginInfo("http://www2.example.com", "http://www.example.com", null, + "username", "password", + "form_field_username", "form_field_password"), + new LoginInfo("http://www2.example.com", "http://www2.example.com", null, + "username", "password2", + "form_field_username", "form_field_password"), + new LoginInfo("http://www2.example.com", "http://www2.example.com", null, + "username2", "password2", + "form_field_username", "form_field_password"), + + new LoginInfo("http://www3.example.com", "http://www.example.com", null, + "", "password", + "form_field_username", "form_field_password"), + new LoginInfo("http://www3.example.com", "http://www3.example.com", null, + "", "password2", + "form_field_username", "form_field_password"), + ]; +} diff --git a/toolkit/components/passwordmgr/test/unit/test_dedupeLogins.js b/toolkit/components/passwordmgr/test/unit/test_dedupeLogins.js new file mode 100644 index 000000000..d688a6dbf --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_dedupeLogins.js @@ -0,0 +1,284 @@ +/* + * Test LoginHelper.dedupeLogins + */ + +"use strict"; + +Cu.import("resource://gre/modules/LoginHelper.jsm"); + +const DOMAIN1_HTTP_TO_HTTP_U1_P1 = TestData.formLogin({ + timePasswordChanged: 3000, + timeLastUsed: 2000, +}); +const DOMAIN1_HTTP_TO_HTTP_U1_P2 = TestData.formLogin({ + password: "password two", +}); +const DOMAIN1_HTTP_TO_HTTP_U2_P2 = TestData.formLogin({ + password: "password two", + username: "username two", +}); +const DOMAIN1_HTTPS_TO_HTTPS_U1_P1 = TestData.formLogin({ + formSubmitURL: "http://www.example.com", + hostname: "https://www3.example.com", + timePasswordChanged: 4000, + timeLastUsed: 1000, +}); +const DOMAIN1_HTTPS_TO_EMPTY_U1_P1 = TestData.formLogin({ + formSubmitURL: "", + hostname: "https://www3.example.com", +}); +const DOMAIN1_HTTPS_TO_EMPTYU_P1 = TestData.formLogin({ + hostname: "https://www3.example.com", + username: "", +}); +const DOMAIN1_HTTP_AUTH = TestData.authLogin({ + hostname: "http://www3.example.com", +}); +const DOMAIN1_HTTPS_AUTH = TestData.authLogin({ + hostname: "https://www3.example.com", +}); + + +add_task(function test_dedupeLogins() { + // [description, expectedOutput, dedupe arg. 0, dedupe arg 1, ...] + let testcases = [ + [ + "exact dupes", + [DOMAIN1_HTTP_TO_HTTP_U1_P1], + [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1], + undefined, + [], // force no resolveBy logic to test behavior of preferring the first.. + ], + [ + "default uniqueKeys is un + pw", + [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P2], + [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P2], + undefined, + [], + ], + [ + "same usernames, different passwords, dedupe username only", + [DOMAIN1_HTTP_TO_HTTP_U1_P1], + [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P2], + ["username"], + [], + ], + [ + "same un+pw, different scheme", + [DOMAIN1_HTTP_TO_HTTP_U1_P1], + [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1], + undefined, + [], + ], + [ + "same un+pw, different scheme, reverse order", + [DOMAIN1_HTTPS_TO_HTTPS_U1_P1], + [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1], + undefined, + [], + ], + [ + "same un+pw, different scheme, include hostname", + [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1], + [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1], + ["hostname", "username", "password"], + [], + ], + [ + "empty username is not deduped with non-empty", + [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_EMPTYU_P1], + [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_EMPTYU_P1], + undefined, + [], + ], + [ + "empty username is deduped with same passwords", + [DOMAIN1_HTTPS_TO_EMPTYU_P1], + [DOMAIN1_HTTPS_TO_EMPTYU_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1], + ["password"], + [], + ], + [ + "mix of form and HTTP auth", + [DOMAIN1_HTTP_TO_HTTP_U1_P1], + [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTP_AUTH], + undefined, + [], + ], + ]; + + for (let tc of testcases) { + let description = tc.shift(); + let expected = tc.shift(); + let actual = LoginHelper.dedupeLogins(...tc); + Assert.strictEqual(actual.length, expected.length, `Check: ${description}`); + for (let [i, login] of expected.entries()) { + Assert.strictEqual(actual[i], login, `Check index ${i}`); + } + } +}); + + +add_task(function* test_dedupeLogins_resolveBy() { + Assert.ok(DOMAIN1_HTTP_TO_HTTP_U1_P1.timeLastUsed > DOMAIN1_HTTPS_TO_HTTPS_U1_P1.timeLastUsed, + "Sanity check timeLastUsed difference"); + Assert.ok(DOMAIN1_HTTP_TO_HTTP_U1_P1.timePasswordChanged < DOMAIN1_HTTPS_TO_HTTPS_U1_P1.timePasswordChanged, + "Sanity check timePasswordChanged difference"); + + let testcases = [ + [ + "default resolveBy is timeLastUsed", + [DOMAIN1_HTTP_TO_HTTP_U1_P1], + [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1], + ], + [ + "default resolveBy is timeLastUsed, reversed input", + [DOMAIN1_HTTP_TO_HTTP_U1_P1], + [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1], + ], + [ + "resolveBy timeLastUsed + timePasswordChanged", + [DOMAIN1_HTTP_TO_HTTP_U1_P1], + [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1], + undefined, + ["timeLastUsed", "timePasswordChanged"], + ], + [ + "resolveBy timeLastUsed + timePasswordChanged, reversed input", + [DOMAIN1_HTTP_TO_HTTP_U1_P1], + [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1], + undefined, + ["timeLastUsed", "timePasswordChanged"], + ], + [ + "resolveBy timePasswordChanged", + [DOMAIN1_HTTPS_TO_HTTPS_U1_P1], + [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1], + undefined, + ["timePasswordChanged"], + ], + [ + "resolveBy timePasswordChanged, reversed", + [DOMAIN1_HTTPS_TO_HTTPS_U1_P1], + [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1], + undefined, + ["timePasswordChanged"], + ], + [ + "resolveBy timePasswordChanged + timeLastUsed", + [DOMAIN1_HTTPS_TO_HTTPS_U1_P1], + [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1], + undefined, + ["timePasswordChanged", "timeLastUsed"], + ], + [ + "resolveBy timePasswordChanged + timeLastUsed, reversed", + [DOMAIN1_HTTPS_TO_HTTPS_U1_P1], + [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1], + undefined, + ["timePasswordChanged", "timeLastUsed"], + ], + [ + "resolveBy scheme + timePasswordChanged, prefer HTTP", + [DOMAIN1_HTTP_TO_HTTP_U1_P1], + [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1], + undefined, + ["scheme", "timePasswordChanged"], + DOMAIN1_HTTP_TO_HTTP_U1_P1.hostname, + ], + [ + "resolveBy scheme + timePasswordChanged, prefer HTTP, reversed input", + [DOMAIN1_HTTP_TO_HTTP_U1_P1], + [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1], + undefined, + ["scheme", "timePasswordChanged"], + DOMAIN1_HTTP_TO_HTTP_U1_P1.hostname, + ], + [ + "resolveBy scheme + timePasswordChanged, prefer HTTPS", + [DOMAIN1_HTTPS_TO_HTTPS_U1_P1], + [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1], + undefined, + ["scheme", "timePasswordChanged"], + DOMAIN1_HTTPS_TO_HTTPS_U1_P1.hostname, + ], + [ + "resolveBy scheme + timePasswordChanged, prefer HTTPS, reversed input", + [DOMAIN1_HTTPS_TO_HTTPS_U1_P1], + [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1], + undefined, + ["scheme", "timePasswordChanged"], + DOMAIN1_HTTPS_TO_HTTPS_U1_P1.hostname, + ], + [ + "resolveBy scheme HTTP auth", + [DOMAIN1_HTTPS_AUTH], + [DOMAIN1_HTTP_AUTH, DOMAIN1_HTTPS_AUTH], + undefined, + ["scheme"], + DOMAIN1_HTTPS_AUTH.hostname, + ], + [ + "resolveBy scheme HTTP auth, reversed input", + [DOMAIN1_HTTPS_AUTH], + [DOMAIN1_HTTPS_AUTH, DOMAIN1_HTTP_AUTH], + undefined, + ["scheme"], + DOMAIN1_HTTPS_AUTH.hostname, + ], + [ + "resolveBy scheme, empty form submit URL", + [DOMAIN1_HTTPS_TO_HTTPS_U1_P1], + [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTPS_TO_EMPTY_U1_P1], + undefined, + ["scheme"], + DOMAIN1_HTTPS_TO_HTTPS_U1_P1.hostname, + ], + ]; + + for (let tc of testcases) { + let description = tc.shift(); + let expected = tc.shift(); + let actual = LoginHelper.dedupeLogins(...tc); + Assert.strictEqual(actual.length, expected.length, `Check: ${description}`); + for (let [i, login] of expected.entries()) { + Assert.strictEqual(actual[i], login, `Check index ${i}`); + } + } + +}); + +add_task(function* test_dedupeLogins_preferredOriginMissing() { + let testcases = [ + [ + "resolveBy scheme + timePasswordChanged, missing preferredOrigin", + /preferredOrigin/, + [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1], + undefined, + ["scheme", "timePasswordChanged"], + ], + [ + "resolveBy timePasswordChanged + scheme, missing preferredOrigin", + /preferredOrigin/, + [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1], + undefined, + ["timePasswordChanged", "scheme"], + ], + [ + "resolveBy scheme + timePasswordChanged, empty preferredOrigin", + /preferredOrigin/, + [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1], + undefined, + ["scheme", "timePasswordChanged"], + "", + ], + ]; + + for (let tc of testcases) { + let description = tc.shift(); + let expectedException = tc.shift(); + Assert.throws(() => { + LoginHelper.dedupeLogins(...tc); + }, expectedException, `Check: ${description}`); + } +}); diff --git a/toolkit/components/passwordmgr/test/unit/test_disabled_hosts.js b/toolkit/components/passwordmgr/test/unit/test_disabled_hosts.js new file mode 100644 index 000000000..ff3b7e868 --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_disabled_hosts.js @@ -0,0 +1,196 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests getLoginSavingEnabled, setLoginSavingEnabled, and getAllDisabledHosts. + */ + +"use strict"; + +// Tests + +/** + * Tests setLoginSavingEnabled and getAllDisabledHosts. + */ +add_task(function test_setLoginSavingEnabled_getAllDisabledHosts() +{ + // Add some disabled hosts, and verify that different schemes for the same + // domain are considered different hosts. + let hostname1 = "http://disabled1.example.com"; + let hostname2 = "http://disabled2.example.com"; + let hostname3 = "https://disabled2.example.com"; + Services.logins.setLoginSavingEnabled(hostname1, false); + Services.logins.setLoginSavingEnabled(hostname2, false); + Services.logins.setLoginSavingEnabled(hostname3, false); + + LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), + [hostname1, hostname2, hostname3]); + + // Adding the same host twice should not result in an error. + Services.logins.setLoginSavingEnabled(hostname2, false); + LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), + [hostname1, hostname2, hostname3]); + + // Removing a disabled host should work. + Services.logins.setLoginSavingEnabled(hostname2, true); + LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), + [hostname1, hostname3]); + + // Removing the last disabled host should work. + Services.logins.setLoginSavingEnabled(hostname1, true); + Services.logins.setLoginSavingEnabled(hostname3, true); + LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), + []); +}); + +/** + * Tests setLoginSavingEnabled and getLoginSavingEnabled. + */ +add_task(function test_setLoginSavingEnabled_getLoginSavingEnabled() +{ + let hostname1 = "http://disabled.example.com"; + let hostname2 = "https://disabled.example.com"; + + // Hosts should not be disabled by default. + do_check_true(Services.logins.getLoginSavingEnabled(hostname1)); + do_check_true(Services.logins.getLoginSavingEnabled(hostname2)); + + // Test setting initial values. + Services.logins.setLoginSavingEnabled(hostname1, false); + Services.logins.setLoginSavingEnabled(hostname2, true); + do_check_false(Services.logins.getLoginSavingEnabled(hostname1)); + do_check_true(Services.logins.getLoginSavingEnabled(hostname2)); + + // Test changing values. + Services.logins.setLoginSavingEnabled(hostname1, true); + Services.logins.setLoginSavingEnabled(hostname2, false); + do_check_true(Services.logins.getLoginSavingEnabled(hostname1)); + do_check_false(Services.logins.getLoginSavingEnabled(hostname2)); + + // Clean up. + Services.logins.setLoginSavingEnabled(hostname2, true); +}); + +/** + * Tests setLoginSavingEnabled with invalid NUL characters in the hostname. + */ +add_task(function test_setLoginSavingEnabled_invalid_characters() +{ + let hostname = "http://null\0X.example.com"; + Assert.throws(() => Services.logins.setLoginSavingEnabled(hostname, false), + /Invalid hostname/); + + // Verify that no data was stored by the previous call. + LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), + []); +}); + +/** + * Tests different values of the "signon.rememberSignons" property. + */ +add_task(function test_rememberSignons() +{ + let hostname1 = "http://example.com"; + let hostname2 = "http://localhost"; + + // The default value for the preference should be true. + do_check_true(Services.prefs.getBoolPref("signon.rememberSignons")); + + // Hosts should not be disabled by default. + Services.logins.setLoginSavingEnabled(hostname1, false); + do_check_false(Services.logins.getLoginSavingEnabled(hostname1)); + do_check_true(Services.logins.getLoginSavingEnabled(hostname2)); + + // Disable storage of saved passwords globally. + Services.prefs.setBoolPref("signon.rememberSignons", false); + do_register_cleanup( + () => Services.prefs.clearUserPref("signon.rememberSignons")); + + // All hosts should now appear disabled. + do_check_false(Services.logins.getLoginSavingEnabled(hostname1)); + do_check_false(Services.logins.getLoginSavingEnabled(hostname2)); + + // The list of disabled hosts should be unaltered. + LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), + [hostname1]); + + // Changing values with the preference set should work. + Services.logins.setLoginSavingEnabled(hostname1, true); + Services.logins.setLoginSavingEnabled(hostname2, false); + + // All hosts should still appear disabled. + do_check_false(Services.logins.getLoginSavingEnabled(hostname1)); + do_check_false(Services.logins.getLoginSavingEnabled(hostname2)); + + // The list of disabled hosts should have been changed. + LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), + [hostname2]); + + // Enable storage of saved passwords again. + Services.prefs.setBoolPref("signon.rememberSignons", true); + + // Hosts should now appear enabled as requested. + do_check_true(Services.logins.getLoginSavingEnabled(hostname1)); + do_check_false(Services.logins.getLoginSavingEnabled(hostname2)); + + // Clean up. + Services.logins.setLoginSavingEnabled(hostname2, true); + LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), + []); +}); + +/** + * Tests storing disabled hosts with non-ASCII characters where IDN is supported. + */ +add_task(function* test_storage_setLoginSavingEnabled_nonascii_IDN_is_supported() +{ + let hostname = "http://大.net"; + let encoding = "http://xn--pss.net"; + + // Test adding disabled host with nonascii URL (http://大.net). + Services.logins.setLoginSavingEnabled(hostname, false); + yield* LoginTestUtils.reloadData(); + Assert.equal(Services.logins.getLoginSavingEnabled(hostname), false); + Assert.equal(Services.logins.getLoginSavingEnabled(encoding), false); + LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), [hostname]); + + LoginTestUtils.clearData(); + + // Test adding disabled host with IDN ("http://xn--pss.net"). + Services.logins.setLoginSavingEnabled(encoding, false); + yield* LoginTestUtils.reloadData(); + Assert.equal(Services.logins.getLoginSavingEnabled(hostname), false); + Assert.equal(Services.logins.getLoginSavingEnabled(encoding), false); + LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), [hostname]); + + LoginTestUtils.clearData(); +}); + +/** + * Tests storing disabled hosts with non-ASCII characters where IDN is not supported. + */ +add_task(function* test_storage_setLoginSavingEnabled_nonascii_IDN_not_supported() +{ + let hostname = "http://√.com"; + let encoding = "http://xn--19g.com"; + + // Test adding disabled host with nonascii URL (http://√.com). + Services.logins.setLoginSavingEnabled(hostname, false); + yield* LoginTestUtils.reloadData(); + Assert.equal(Services.logins.getLoginSavingEnabled(hostname), false); + Assert.equal(Services.logins.getLoginSavingEnabled(encoding), false); + LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), [encoding]); + + LoginTestUtils.clearData(); + + // Test adding disabled host with IDN ("http://xn--19g.com"). + Services.logins.setLoginSavingEnabled(encoding, false); + yield* LoginTestUtils.reloadData(); + Assert.equal(Services.logins.getLoginSavingEnabled(hostname), false); + Assert.equal(Services.logins.getLoginSavingEnabled(encoding), false); + LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), [encoding]); + + LoginTestUtils.clearData(); +}); diff --git a/toolkit/components/passwordmgr/test/unit/test_getFormFields.js b/toolkit/components/passwordmgr/test/unit/test_getFormFields.js new file mode 100644 index 000000000..46912ab8f --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_getFormFields.js @@ -0,0 +1,147 @@ +/* + * Test for LoginManagerContent._getFormFields. + */ + +"use strict"; + +// Services.prefs.setBoolPref("signon.debug", true); + +Cu.importGlobalProperties(["URL"]); + +const LMCBackstagePass = Cu.import("resource://gre/modules/LoginManagerContent.jsm"); +const { LoginManagerContent, LoginFormFactory } = LMCBackstagePass; +const TESTCASES = [ + { + description: "1 password field outside of a
", + document: ``, + returnedFieldIDs: [null, "pw1", null], + skipEmptyFields: undefined, + }, + { + description: "1 text field outside of a without a password field", + document: ``, + returnedFieldIDs: [null, null, null], + skipEmptyFields: undefined, + }, + { + description: "1 username & password field outside of a ", + document: ` + `, + returnedFieldIDs: ["un1", "pw1", null], + skipEmptyFields: undefined, + }, + { + description: "1 username & password field in a ", + document: ` + + +
`, + returnedFieldIDs: ["un1", "pw1", null], + skipEmptyFields: undefined, + }, + { + description: "4 empty password fields outside of a
", + document: ` + + + `, + returnedFieldIDs: [null, null, null], + skipEmptyFields: undefined, + }, + { + description: "4 password fields outside of a (1 empty, 3 full) with skipEmpty", + document: ` + + + `, + returnedFieldIDs: [null, null, null], + skipEmptyFields: true, + }, + { + description: "Form with 1 password field", + document: `
`, + returnedFieldIDs: [null, "pw1", null], + skipEmptyFields: undefined, + }, + { + description: "Form with 2 password fields", + document: `
`, + returnedFieldIDs: [null, "pw1", null], + skipEmptyFields: undefined, + }, + { + description: "1 password field in a form, 1 outside (not processed)", + document: `
`, + returnedFieldIDs: [null, "pw1", null], + skipEmptyFields: undefined, + }, + { + description: "1 password field in a form, 1 text field outside (not processed)", + document: `
`, + returnedFieldIDs: [null, "pw1", null], + skipEmptyFields: undefined, + }, + { + description: "1 text field in a form, 1 password field outside (not processed)", + document: `
`, + returnedFieldIDs: [null, null, null], + skipEmptyFields: undefined, + }, + { + description: "2 password fields outside of a
with 1 linked via @form", + document: ` +
`, + returnedFieldIDs: [null, "pw1", null], + skipEmptyFields: undefined, + }, + { + description: "2 password fields outside of a
with 1 linked via @form + skipEmpty", + document: ` +
`, + returnedFieldIDs: [null, null, null], + skipEmptyFields: true, + }, + { + description: "2 password fields outside of a
with 1 linked via @form + skipEmpty with 1 empty", + document: ` +
`, + returnedFieldIDs: [null, "pw1", null], + skipEmptyFields: true, + }, +]; + +for (let tc of TESTCASES) { + do_print("Sanity checking the testcase: " + tc.description); + + (function() { + let testcase = tc; + add_task(function*() { + do_print("Starting testcase: " + testcase.description); + let document = MockDocument.createTestDocument("http://localhost:8080/test/", + testcase.document); + + let input = document.querySelector("input"); + MockDocument.mockOwnerDocumentProperty(input, document, "http://localhost:8080/test/"); + + let formLike = LoginFormFactory.createFromField(input); + + let actual = LoginManagerContent._getFormFields(formLike, + testcase.skipEmptyFields, + new Set()); + + Assert.strictEqual(testcase.returnedFieldIDs.length, 3, + "_getFormFields returns 3 elements"); + + for (let i = 0; i < testcase.returnedFieldIDs.length; i++) { + let expectedID = testcase.returnedFieldIDs[i]; + if (expectedID === null) { + Assert.strictEqual(actual[i], expectedID, + "Check returned field " + i + " is null"); + } else { + Assert.strictEqual(actual[i].id, expectedID, + "Check returned field " + i + " ID"); + } + } + }); + })(); +} diff --git a/toolkit/components/passwordmgr/test/unit/test_getPasswordFields.js b/toolkit/components/passwordmgr/test/unit/test_getPasswordFields.js new file mode 100644 index 000000000..08fa422ab --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_getPasswordFields.js @@ -0,0 +1,156 @@ +/* + * Test for LoginManagerContent._getPasswordFields using LoginFormFactory. + */ + +"use strict"; + +const LMCBackstagePass = Cu.import("resource://gre/modules/LoginManagerContent.jsm"); +const { LoginManagerContent, LoginFormFactory } = LMCBackstagePass; +const TESTCASES = [ + { + description: "Empty document", + document: ``, + returnedFieldIDsByFormLike: [], + skipEmptyFields: undefined, + }, + { + description: "Non-password input with no
present", + document: ``, + // Only the IDs of password fields should be in this array + returnedFieldIDsByFormLike: [[]], + skipEmptyFields: undefined, + }, + { + description: "1 password field outside of a ", + document: ``, + returnedFieldIDsByFormLike: [["pw1"]], + skipEmptyFields: undefined, + }, + { + description: "4 empty password fields outside of a ", + document: ` + + + `, + returnedFieldIDsByFormLike: [[]], + skipEmptyFields: undefined, + }, + { + description: "4 password fields outside of a (1 empty, 3 full) with skipEmpty", + document: ` + + + `, + returnedFieldIDsByFormLike: [["pw2", "pw3", "pw4"]], + skipEmptyFields: true, + }, + { + description: "Form with 1 password field", + document: `
`, + returnedFieldIDsByFormLike: [["pw1"]], + skipEmptyFields: undefined, + }, + { + description: "Form with 2 password fields", + document: `
`, + returnedFieldIDsByFormLike: [["pw1", "pw2"]], + skipEmptyFields: undefined, + }, + { + description: "1 password field in a form, 1 outside", + document: `
`, + returnedFieldIDsByFormLike: [["pw1"], ["pw2"]], + skipEmptyFields: undefined, + }, + { + description: "2 password fields outside of a
with 1 linked via @form", + document: ` +
`, + returnedFieldIDsByFormLike: [["pw1"], ["pw2"]], + skipEmptyFields: undefined, + }, + { + description: "2 password fields outside of a
with 1 linked via @form + skipEmpty", + document: ` +
`, + returnedFieldIDsByFormLike: [[], []], + skipEmptyFields: true, + }, + { + description: "skipEmptyFields should also skip white-space only fields", + document: ` + + +
`, + returnedFieldIDsByFormLike: [[], []], + skipEmptyFields: true, + }, + { + description: "2 password fields outside of a
with 1 linked via @form + skipEmpty with 1 empty", + document: ` +
`, + returnedFieldIDsByFormLike: [["pw1"], []], + skipEmptyFields: true, + }, +]; + +for (let tc of TESTCASES) { + do_print("Sanity checking the testcase: " + tc.description); + + (function() { + let testcase = tc; + add_task(function*() { + do_print("Starting testcase: " + testcase.description); + let document = MockDocument.createTestDocument("http://localhost:8080/test/", + testcase.document); + + let mapRootElementToFormLike = new Map(); + for (let input of document.querySelectorAll("input")) { + let formLike = LoginFormFactory.createFromField(input); + let existingFormLike = mapRootElementToFormLike.get(formLike.rootElement); + if (!existingFormLike) { + mapRootElementToFormLike.set(formLike.rootElement, formLike); + continue; + } + + // If the formLike is already present, ensure that the properties are the same. + do_print("Checking if the new FormLike for the same root has the same properties"); + formLikeEqual(formLike, existingFormLike); + } + + Assert.strictEqual(mapRootElementToFormLike.size, testcase.returnedFieldIDsByFormLike.length, + "Check the correct number of different formLikes were returned"); + + let formLikeIndex = -1; + for (let formLikeFromInput of mapRootElementToFormLike.values()) { + formLikeIndex++; + let pwFields = LoginManagerContent._getPasswordFields(formLikeFromInput, + testcase.skipEmptyFields); + + if (formLikeFromInput.rootElement instanceof Ci.nsIDOMHTMLFormElement) { + let formLikeFromForm = LoginFormFactory.createFromForm(formLikeFromInput.rootElement); + do_print("Checking that the FormLike created for the
matches" + + " the one from a password field"); + formLikeEqual(formLikeFromInput, formLikeFromForm); + } + + + if (testcase.returnedFieldIDsByFormLike[formLikeIndex].length === 0) { + Assert.strictEqual(pwFields, null, + "If no password fields were found null should be returned"); + } else { + Assert.strictEqual(pwFields.length, + testcase.returnedFieldIDsByFormLike[formLikeIndex].length, + "Check the # of password fields for formLike #" + formLikeIndex); + } + + for (let i = 0; i < testcase.returnedFieldIDsByFormLike[formLikeIndex].length; i++) { + let expectedID = testcase.returnedFieldIDsByFormLike[formLikeIndex][i]; + Assert.strictEqual(pwFields[i].element.id, expectedID, + "Check password field " + i + " ID"); + } + } + }); + })(); +} diff --git a/toolkit/components/passwordmgr/test/unit/test_getPasswordOrigin.js b/toolkit/components/passwordmgr/test/unit/test_getPasswordOrigin.js new file mode 100644 index 000000000..f2773ec62 --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_getPasswordOrigin.js @@ -0,0 +1,28 @@ +/* + * Test for LoginUtils._getPasswordOrigin + */ + +"use strict"; + +const LMCBackstagePass = Cu.import("resource://gre/modules/LoginManagerContent.jsm"); +const TESTCASES = [ + ["javascript:void(0);", null], + ["javascript:void(0);", "javascript:", true], + ["chrome://MyAccount", null], + ["data:text/html,example", null], + ["http://username:password@example.com:80/foo?bar=baz#fragment", "http://example.com", true], + ["http://127.0.0.1:80/foo", "http://127.0.0.1"], + ["http://[::1]:80/foo", "http://[::1]"], + ["http://example.com:8080/foo", "http://example.com:8080"], + ["http://127.0.0.1:8080/foo", "http://127.0.0.1:8080", true], + ["http://[::1]:8080/foo", "http://[::1]:8080"], + ["https://example.com:443/foo", "https://example.com"], + ["https://[::1]:443/foo", "https://[::1]"], + ["https://[::1]:8443/foo", "https://[::1]:8443"], + ["ftp://username:password@[::1]:2121/foo", "ftp://[::1]:2121"], +]; + +for (let [input, expected, allowJS] of TESTCASES) { + let actual = LMCBackstagePass.LoginUtils._getPasswordOrigin(input, allowJS); + Assert.strictEqual(actual, expected, "Checking: " + input); +} diff --git a/toolkit/components/passwordmgr/test/unit/test_isOriginMatching.js b/toolkit/components/passwordmgr/test/unit/test_isOriginMatching.js new file mode 100644 index 000000000..660910dff --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_isOriginMatching.js @@ -0,0 +1,40 @@ +/* + * Test LoginHelper.isOriginMatching + */ + +"use strict"; + +Cu.import("resource://gre/modules/LoginHelper.jsm"); + +add_task(function test_isOriginMatching() { + let testcases = [ + // Index 0 holds the expected return value followed by arguments to isOriginMatching. + [true, "http://example.com", "http://example.com"], + [true, "http://example.com:8080", "http://example.com:8080"], + [true, "https://example.com", "https://example.com"], + [true, "https://example.com:8443", "https://example.com:8443"], + [false, "http://example.com", "http://mozilla.org"], + [false, "http://example.com", "http://example.com:8080"], + [false, "https://example.com", "http://example.com"], + [false, "https://example.com", "https://mozilla.org"], + [false, "http://example.com", "http://sub.example.com"], + [false, "https://example.com", "https://sub.example.com"], + [false, "http://example.com", "https://example.com:8443"], + [false, "http://example.com:8080", "http://example.com:8081"], + [false, "http://example.com", ""], + [false, "", "http://example.com"], + [true, "http://example.com", "https://example.com", { schemeUpgrades: true }], + [true, "https://example.com", "https://example.com", { schemeUpgrades: true }], + [true, "http://example.com:8080", "http://example.com:8080", { schemeUpgrades: true }], + [true, "https://example.com:8443", "https://example.com:8443", { schemeUpgrades: true }], + [false, "https://example.com", "http://example.com", { schemeUpgrades: true }], // downgrade + [false, "http://example.com:8080", "https://example.com", { schemeUpgrades: true }], // port mismatch + [false, "http://example.com", "https://example.com:8443", { schemeUpgrades: true }], // port mismatch + [false, "http://sub.example.com", "http://example.com", { schemeUpgrades: true }], + ]; + for (let tc of testcases) { + let expected = tc.shift(); + Assert.strictEqual(LoginHelper.isOriginMatching(...tc), expected, + "Check " + JSON.stringify(tc)); + } +}); diff --git a/toolkit/components/passwordmgr/test/unit/test_legacy_empty_formSubmitURL.js b/toolkit/components/passwordmgr/test/unit/test_legacy_empty_formSubmitURL.js new file mode 100644 index 000000000..4e16aa267 --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_legacy_empty_formSubmitURL.js @@ -0,0 +1,107 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the legacy case of a login store containing entries that have an empty + * string in the formSubmitURL field. + * + * In normal conditions, for the purpose of login autocomplete, HTML forms are + * identified using both the prePath of the URI on which they are located, and + * the prePath of the URI where the data will be submitted. This is represented + * by the hostname and formSubmitURL properties of the stored nsILoginInfo. + * + * When a new login for use in forms is saved (after the user replies to the + * password prompt), it is always stored with both the hostname and the + * formSubmitURL (that will be equal to the hostname when the form has no + * "action" attribute). + * + * When the same form is displayed again, the password is autocompleted. If + * there is another form on the same site that submits to a different site, it + * is considered a different form, so the password is not autocompleted, but a + * new password can be stored for the other form. + * + * However, the login database might contain data for an nsILoginInfo that has a + * valid hostname, but an empty formSubmitURL. This means that the login + * applies to all forms on the site, regardless of where they submit data to. + * + * A site can have at most one such login, and in case it is present, then it is + * not possible to store separate logins for forms on the same site that submit + * data to different sites. + * + * The only way to have such condition is to be using logins that were initially + * saved by a very old version of the browser, or because of data manually added + * by an extension in an old version. + */ + +"use strict"; + +// Tests + +/** + * Adds a login with an empty formSubmitURL, then it verifies that no other + * form logins can be added for the same host. + */ +add_task(function test_addLogin_wildcard() +{ + let loginInfo = TestData.formLogin({ hostname: "http://any.example.com", + formSubmitURL: "" }); + Services.logins.addLogin(loginInfo); + + // Normal form logins cannot be added anymore. + loginInfo = TestData.formLogin({ hostname: "http://any.example.com" }); + Assert.throws(() => Services.logins.addLogin(loginInfo), /already exists/); + + // Authentication logins can still be added. + loginInfo = TestData.authLogin({ hostname: "http://any.example.com" }); + Services.logins.addLogin(loginInfo); + + // Form logins can be added for other hosts. + loginInfo = TestData.formLogin({ hostname: "http://other.example.com" }); + Services.logins.addLogin(loginInfo); +}); + +/** + * Verifies that findLogins, searchLogins, and countLogins include all logins + * that have an empty formSubmitURL in the store, even when a formSubmitURL is + * specified. + */ +add_task(function test_search_all_wildcard() +{ + // Search a given formSubmitURL on any host. + let matchData = newPropertyBag({ formSubmitURL: "http://www.example.com" }); + do_check_eq(Services.logins.searchLogins({}, matchData).length, 2); + + do_check_eq(Services.logins.findLogins({}, "", "http://www.example.com", + null).length, 2); + + do_check_eq(Services.logins.countLogins("", "http://www.example.com", + null), 2); + + // Restrict the search to one host. + matchData.setProperty("hostname", "http://any.example.com"); + do_check_eq(Services.logins.searchLogins({}, matchData).length, 1); + + do_check_eq(Services.logins.findLogins({}, "http://any.example.com", + "http://www.example.com", + null).length, 1); + + do_check_eq(Services.logins.countLogins("http://any.example.com", + "http://www.example.com", + null), 1); +}); + +/** + * Verifies that specifying an empty string for formSubmitURL in searchLogins + * includes only logins that have an empty formSubmitURL in the store. + */ +add_task(function test_searchLogins_wildcard() +{ + let logins = Services.logins.searchLogins({}, + newPropertyBag({ formSubmitURL: "" })); + + let loginInfo = TestData.formLogin({ hostname: "http://any.example.com", + formSubmitURL: "" }); + LoginTestUtils.assertLoginListsEqual(logins, [loginInfo]); +}); diff --git a/toolkit/components/passwordmgr/test/unit/test_legacy_validation.js b/toolkit/components/passwordmgr/test/unit/test_legacy_validation.js new file mode 100644 index 000000000..709bc9818 --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_legacy_validation.js @@ -0,0 +1,76 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the legacy validation made when storing nsILoginInfo or disabled hosts. + * + * These rules exist because of limitations of the "signons.txt" storage file, + * that is not used anymore. They are still enforced by the Login Manager + * service, despite these values can now be safely stored in the back-end. + */ + +"use strict"; + +// Tests + +/** + * Tests legacy validation with addLogin. + */ +add_task(function test_addLogin_invalid_characters_legacy() +{ + // Test newlines and carriage returns in properties that contain URLs. + for (let testValue of ["http://newline\n.example.com", + "http://carriagereturn.example.com\r"]) { + let loginInfo = TestData.formLogin({ hostname: testValue }); + Assert.throws(() => Services.logins.addLogin(loginInfo), + /login values can't contain newlines/); + + loginInfo = TestData.formLogin({ formSubmitURL: testValue }); + Assert.throws(() => Services.logins.addLogin(loginInfo), + /login values can't contain newlines/); + + loginInfo = TestData.authLogin({ httpRealm: testValue }); + Assert.throws(() => Services.logins.addLogin(loginInfo), + /login values can't contain newlines/); + } + + // Test newlines and carriage returns in form field names. + for (let testValue of ["newline_field\n", "carriagereturn\r_field"]) { + let loginInfo = TestData.formLogin({ usernameField: testValue }); + Assert.throws(() => Services.logins.addLogin(loginInfo), + /login values can't contain newlines/); + + loginInfo = TestData.formLogin({ passwordField: testValue }); + Assert.throws(() => Services.logins.addLogin(loginInfo), + /login values can't contain newlines/); + } + + // Test a single dot as the value of usernameField and formSubmitURL. + let loginInfo = TestData.formLogin({ usernameField: "." }); + Assert.throws(() => Services.logins.addLogin(loginInfo), + /login values can't be periods/); + + loginInfo = TestData.formLogin({ formSubmitURL: "." }); + Assert.throws(() => Services.logins.addLogin(loginInfo), + /login values can't be periods/); + + // Test the sequence " (" inside the value of the "hostname" property. + loginInfo = TestData.formLogin({ hostname: "http://parens (.example.com" }); + Assert.throws(() => Services.logins.addLogin(loginInfo), + /bad parens in hostname/); +}); + +/** + * Tests legacy validation with setLoginSavingEnabled. + */ +add_task(function test_setLoginSavingEnabled_invalid_characters_legacy() +{ + for (let hostname of ["http://newline\n.example.com", + "http://carriagereturn.example.com\r", + "."]) { + Assert.throws(() => Services.logins.setLoginSavingEnabled(hostname, false), + /Invalid hostname/); + } +}); diff --git a/toolkit/components/passwordmgr/test/unit/test_logins_change.js b/toolkit/components/passwordmgr/test/unit/test_logins_change.js new file mode 100644 index 000000000..79c6d2f54 --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_logins_change.js @@ -0,0 +1,384 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests methods that add, remove, and modify logins. + */ + +"use strict"; + +// Globals + +/** + * Verifies that the specified login is considered invalid by addLogin and by + * modifyLogin with both nsILoginInfo and nsIPropertyBag arguments. + * + * This test requires that the login store is empty. + * + * @param aLoginInfo + * nsILoginInfo corresponding to an invalid login. + * @param aExpectedError + * This argument is passed to the "Assert.throws" test to determine which + * error is expected from the modification functions. + */ +function checkLoginInvalid(aLoginInfo, aExpectedError) +{ + // Try to add the new login, and verify that no data is stored. + Assert.throws(() => Services.logins.addLogin(aLoginInfo), aExpectedError); + LoginTestUtils.checkLogins([]); + + // Add a login for the modification tests. + let testLogin = TestData.formLogin({ hostname: "http://modify.example.com" }); + Services.logins.addLogin(testLogin); + + // Try to modify the existing login using nsILoginInfo and nsIPropertyBag. + Assert.throws(() => Services.logins.modifyLogin(testLogin, aLoginInfo), + aExpectedError); + Assert.throws(() => Services.logins.modifyLogin(testLogin, newPropertyBag({ + hostname: aLoginInfo.hostname, + formSubmitURL: aLoginInfo.formSubmitURL, + httpRealm: aLoginInfo.httpRealm, + username: aLoginInfo.username, + password: aLoginInfo.password, + usernameField: aLoginInfo.usernameField, + passwordField: aLoginInfo.passwordField, + })), aExpectedError); + + // Verify that no data was stored by the previous calls. + LoginTestUtils.checkLogins([testLogin]); + Services.logins.removeLogin(testLogin); +} + +/** + * Verifies that two objects are not the same instance + * but have equal attributes. + * + * @param {Object} objectA + * An object to compare. + * + * @param {Object} objectB + * Another object to compare. + * + * @param {string[]} attributes + * Attributes to compare. + * + * @return true if all passed attributes are equal for both objects, false otherwise. + */ +function compareAttributes(objectA, objectB, attributes) { + // If it's the same object, we want to return false. + if (objectA == objectB) { + return false; + } + return attributes.every(attr => objectA[attr] == objectB[attr]); +} + +// Tests + +/** + * Tests that adding logins to the database works. + */ +add_task(function test_addLogin_removeLogin() +{ + // Each login from the test data should be valid and added to the list. + for (let loginInfo of TestData.loginList()) { + Services.logins.addLogin(loginInfo); + } + LoginTestUtils.checkLogins(TestData.loginList()); + + // Trying to add each login again should result in an error. + for (let loginInfo of TestData.loginList()) { + Assert.throws(() => Services.logins.addLogin(loginInfo), /already exists/); + } + + // Removing each login should succeed. + for (let loginInfo of TestData.loginList()) { + Services.logins.removeLogin(loginInfo); + } + + LoginTestUtils.checkLogins([]); +}); + +/** + * Tests invalid combinations of httpRealm and formSubmitURL. + * + * For an nsILoginInfo to be valid for storage, one of the two properties should + * be strictly equal to null, and the other must not be null or an empty string. + * + * The legacy case of an empty string in formSubmitURL and a null value in + * httpRealm is also supported for storage at the moment. + */ +add_task(function test_invalid_httpRealm_formSubmitURL() +{ + // httpRealm === null, formSubmitURL === null + checkLoginInvalid(TestData.formLogin({ formSubmitURL: null }), + /without a httpRealm or formSubmitURL/); + + // httpRealm === "", formSubmitURL === null + checkLoginInvalid(TestData.authLogin({ httpRealm: "" }), + /without a httpRealm or formSubmitURL/); + + // httpRealm === null, formSubmitURL === "" + // This is not enforced for now. + // checkLoginInvalid(TestData.formLogin({ formSubmitURL: "" }), + // /without a httpRealm or formSubmitURL/); + + // httpRealm === "", formSubmitURL === "" + checkLoginInvalid(TestData.formLogin({ formSubmitURL: "", httpRealm: "" }), + /both a httpRealm and formSubmitURL/); + + // !!httpRealm, !!formSubmitURL + checkLoginInvalid(TestData.formLogin({ httpRealm: "The HTTP Realm" }), + /both a httpRealm and formSubmitURL/); + + // httpRealm === "", !!formSubmitURL + checkLoginInvalid(TestData.formLogin({ httpRealm: "" }), + /both a httpRealm and formSubmitURL/); + + // !!httpRealm, formSubmitURL === "" + checkLoginInvalid(TestData.authLogin({ formSubmitURL: "" }), + /both a httpRealm and formSubmitURL/); +}); + +/** + * Tests null or empty values in required login properties. + */ +add_task(function test_missing_properties() +{ + checkLoginInvalid(TestData.formLogin({ hostname: null }), + /null or empty hostname/); + + checkLoginInvalid(TestData.formLogin({ hostname: "" }), + /null or empty hostname/); + + checkLoginInvalid(TestData.formLogin({ username: null }), + /null username/); + + checkLoginInvalid(TestData.formLogin({ password: null }), + /null or empty password/); + + checkLoginInvalid(TestData.formLogin({ password: "" }), + /null or empty password/); +}); + +/** + * Tests invalid NUL characters in nsILoginInfo properties. + */ +add_task(function test_invalid_characters() +{ + let loginList = [ + TestData.authLogin({ hostname: "http://null\0X.example.com" }), + TestData.authLogin({ httpRealm: "realm\0" }), + TestData.formLogin({ formSubmitURL: "http://null\0X.example.com" }), + TestData.formLogin({ usernameField: "field\0_null" }), + TestData.formLogin({ usernameField: ".\0" }), // Special single dot case + TestData.formLogin({ passwordField: "field\0_null" }), + TestData.formLogin({ username: "user\0name" }), + TestData.formLogin({ password: "pass\0word" }), + ]; + for (let loginInfo of loginList) { + checkLoginInvalid(loginInfo, /login values can't contain nulls/); + } +}); + +/** + * Tests removing a login that does not exists. + */ +add_task(function test_removeLogin_nonexisting() +{ + Assert.throws(() => Services.logins.removeLogin(TestData.formLogin()), + /No matching logins/); +}); + +/** + * Tests removing all logins at once. + */ +add_task(function test_removeAllLogins() +{ + for (let loginInfo of TestData.loginList()) { + Services.logins.addLogin(loginInfo); + } + Services.logins.removeAllLogins(); + LoginTestUtils.checkLogins([]); + + // The function should also work when there are no logins to delete. + Services.logins.removeAllLogins(); +}); + +/** + * Tests the modifyLogin function with an nsILoginInfo argument. + */ +add_task(function test_modifyLogin_nsILoginInfo() +{ + let loginInfo = TestData.formLogin(); + let updatedLoginInfo = TestData.formLogin({ + username: "new username", + password: "new password", + usernameField: "new_form_field_username", + passwordField: "new_form_field_password", + }); + let differentLoginInfo = TestData.authLogin(); + + // Trying to modify a login that does not exist should throw. + Assert.throws(() => Services.logins.modifyLogin(loginInfo, updatedLoginInfo), + /No matching logins/); + + // Add the first form login, then modify it to match the second. + Services.logins.addLogin(loginInfo); + Services.logins.modifyLogin(loginInfo, updatedLoginInfo); + + // The data should now match the second login. + LoginTestUtils.checkLogins([updatedLoginInfo]); + Assert.throws(() => Services.logins.modifyLogin(loginInfo, updatedLoginInfo), + /No matching logins/); + + // The login can be changed to have a different type and hostname. + Services.logins.modifyLogin(updatedLoginInfo, differentLoginInfo); + LoginTestUtils.checkLogins([differentLoginInfo]); + + // It is now possible to add a login with the old type and hostname. + Services.logins.addLogin(loginInfo); + LoginTestUtils.checkLogins([loginInfo, differentLoginInfo]); + + // Modifying a login to match an existing one should not be possible. + Assert.throws( + () => Services.logins.modifyLogin(loginInfo, differentLoginInfo), + /already exists/); + LoginTestUtils.checkLogins([loginInfo, differentLoginInfo]); + + LoginTestUtils.clearData(); +}); + +/** + * Tests the modifyLogin function with an nsIPropertyBag argument. + */ +add_task(function test_modifyLogin_nsIProperyBag() +{ + let loginInfo = TestData.formLogin(); + let updatedLoginInfo = TestData.formLogin({ + username: "new username", + password: "new password", + usernameField: "", + passwordField: "new_form_field_password", + }); + let differentLoginInfo = TestData.authLogin(); + let differentLoginProperties = newPropertyBag({ + hostname: differentLoginInfo.hostname, + formSubmitURL: differentLoginInfo.formSubmitURL, + httpRealm: differentLoginInfo.httpRealm, + username: differentLoginInfo.username, + password: differentLoginInfo.password, + usernameField: differentLoginInfo.usernameField, + passwordField: differentLoginInfo.passwordField, + }); + + // Trying to modify a login that does not exist should throw. + Assert.throws(() => Services.logins.modifyLogin(loginInfo, newPropertyBag()), + /No matching logins/); + + // Add the first form login, then modify it to match the second, changing + // only some of its properties and checking the behavior with an empty string. + Services.logins.addLogin(loginInfo); + Services.logins.modifyLogin(loginInfo, newPropertyBag({ + username: "new username", + password: "new password", + usernameField: "", + passwordField: "new_form_field_password", + })); + + // The data should now match the second login. + LoginTestUtils.checkLogins([updatedLoginInfo]); + Assert.throws(() => Services.logins.modifyLogin(loginInfo, newPropertyBag()), + /No matching logins/); + + // It is also possible to provide no properties to be modified. + Services.logins.modifyLogin(updatedLoginInfo, newPropertyBag()); + + // Specifying a null property for a required value should throw. + Assert.throws(() => Services.logins.modifyLogin(loginInfo, newPropertyBag({ + usernameField: null, + }))); + + // The login can be changed to have a different type and hostname. + Services.logins.modifyLogin(updatedLoginInfo, differentLoginProperties); + LoginTestUtils.checkLogins([differentLoginInfo]); + + // It is now possible to add a login with the old type and hostname. + Services.logins.addLogin(loginInfo); + LoginTestUtils.checkLogins([loginInfo, differentLoginInfo]); + + // Modifying a login to match an existing one should not be possible. + Assert.throws( + () => Services.logins.modifyLogin(loginInfo, differentLoginProperties), + /already exists/); + LoginTestUtils.checkLogins([loginInfo, differentLoginInfo]); + + LoginTestUtils.clearData(); +}); + +/** + * Tests the login deduplication function. + */ +add_task(function test_deduplicate_logins() { + // Different key attributes combinations and the amount of unique + // results expected for the TestData login list. + let keyCombinations = [ + { + keyset: ["username", "password"], + results: 13, + }, + { + keyset: ["hostname", "username"], + results: 17, + }, + { + keyset: ["hostname", "username", "password"], + results: 18, + }, + { + keyset: ["hostname", "username", "password", "formSubmitURL"], + results: 23, + }, + ]; + + let logins = TestData.loginList(); + + for (let testCase of keyCombinations) { + // Deduplicate the logins using the current testcase keyset. + let deduped = LoginHelper.dedupeLogins(logins, testCase.keyset); + Assert.equal(deduped.length, testCase.results, "Correct amount of results."); + + // Checks that every login after deduping is unique. + Assert.ok(deduped.every(loginA => + deduped.every(loginB => !compareAttributes(loginA, loginB, testCase.keyset)) + ), "Every login is unique."); + } +}); + +/** + * Ensure that the login deduplication function keeps the most recent login. + */ +add_task(function test_deduplicate_keeps_most_recent() { + // Logins to deduplicate. + let logins = [ + TestData.formLogin({timeLastUsed: Date.UTC(2004, 11, 4, 0, 0, 0)}), + TestData.formLogin({formSubmitURL: "http://example.com", timeLastUsed: Date.UTC(2015, 11, 4, 0, 0, 0)}), + ]; + + // Deduplicate the logins. + let deduped = LoginHelper.dedupeLogins(logins); + Assert.equal(deduped.length, 1, "Deduplicated the logins array."); + + // Verify that the remaining login have the most recent date. + let loginTimeLastUsed = deduped[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed; + Assert.equal(loginTimeLastUsed, Date.UTC(2015, 11, 4, 0, 0, 0), "Most recent login was kept."); + + // Deduplicate the reverse logins array. + deduped = LoginHelper.dedupeLogins(logins.reverse()); + Assert.equal(deduped.length, 1, "Deduplicated the reversed logins array."); + + // Verify that the remaining login have the most recent date. + loginTimeLastUsed = deduped[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed; + Assert.equal(loginTimeLastUsed, Date.UTC(2015, 11, 4, 0, 0, 0), "Most recent login was kept."); +}); diff --git a/toolkit/components/passwordmgr/test/unit/test_logins_decrypt_failure.js b/toolkit/components/passwordmgr/test/unit/test_logins_decrypt_failure.js new file mode 100644 index 000000000..ffbedb4de --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_logins_decrypt_failure.js @@ -0,0 +1,77 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the case where there are logins that cannot be decrypted. + */ + +"use strict"; + +// Globals + +/** + * Resets the token used to decrypt logins. This is equivalent to resetting the + * master password when it is not known. + */ +function resetMasterPassword() +{ + let token = Cc["@mozilla.org/security/pk11tokendb;1"] + .getService(Ci.nsIPK11TokenDB).getInternalKeyToken(); + token.reset(); + token.changePassword("", ""); +} + +// Tests + +/** + * Resets the master password after some logins were added to the database. + */ +add_task(function test_logins_decrypt_failure() +{ + let logins = TestData.loginList(); + for (let loginInfo of logins) { + Services.logins.addLogin(loginInfo); + } + + // This makes the existing logins non-decryptable. + resetMasterPassword(); + + // These functions don't see the non-decryptable entries anymore. + do_check_eq(Services.logins.getAllLogins().length, 0); + do_check_eq(Services.logins.findLogins({}, "", "", "").length, 0); + do_check_eq(Services.logins.searchLogins({}, newPropertyBag()).length, 0); + Assert.throws(() => Services.logins.modifyLogin(logins[0], newPropertyBag()), + /No matching logins/); + Assert.throws(() => Services.logins.removeLogin(logins[0]), + /No matching logins/); + + // The function that counts logins sees the non-decryptable entries also. + do_check_eq(Services.logins.countLogins("", "", ""), logins.length); + + // Equivalent logins can be added. + for (let loginInfo of logins) { + Services.logins.addLogin(loginInfo); + } + LoginTestUtils.checkLogins(logins); + do_check_eq(Services.logins.countLogins("", "", ""), logins.length * 2); + + // Finding logins doesn't return the non-decryptable duplicates. + do_check_eq(Services.logins.findLogins({}, "http://www.example.com", + "", "").length, 1); + let matchData = newPropertyBag({ hostname: "http://www.example.com" }); + do_check_eq(Services.logins.searchLogins({}, matchData).length, 1); + + // Removing single logins does not remove non-decryptable logins. + for (let loginInfo of TestData.loginList()) { + Services.logins.removeLogin(loginInfo); + } + do_check_eq(Services.logins.getAllLogins().length, 0); + do_check_eq(Services.logins.countLogins("", "", ""), logins.length); + + // Removing all logins removes the non-decryptable entries also. + Services.logins.removeAllLogins(); + do_check_eq(Services.logins.getAllLogins().length, 0); + do_check_eq(Services.logins.countLogins("", "", ""), 0); +}); diff --git a/toolkit/components/passwordmgr/test/unit/test_logins_metainfo.js b/toolkit/components/passwordmgr/test/unit/test_logins_metainfo.js new file mode 100644 index 000000000..38344aa7d --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_logins_metainfo.js @@ -0,0 +1,284 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the handling of nsILoginMetaInfo by methods that add, remove, modify, + * and find logins. + */ + +"use strict"; + +// Globals + +XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator", + "@mozilla.org/uuid-generator;1", + "nsIUUIDGenerator"); + +var gLooksLikeUUIDRegex = /^\{\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\}$/; + +/** + * Retrieves the only login among the current data that matches the hostname of + * the given nsILoginInfo. In case there is more than one login for the + * hostname, the test fails. + */ +function retrieveLoginMatching(aLoginInfo) +{ + let logins = Services.logins.findLogins({}, aLoginInfo.hostname, "", ""); + do_check_eq(logins.length, 1); + return logins[0].QueryInterface(Ci.nsILoginMetaInfo); +} + +/** + * Checks that the nsILoginInfo and nsILoginMetaInfo properties of two different + * login instances are equal. + */ +function assertMetaInfoEqual(aActual, aExpected) +{ + do_check_neq(aActual, aExpected); + + // Check the nsILoginInfo properties. + do_check_true(aActual.equals(aExpected)); + + // Check the nsILoginMetaInfo properties. + do_check_eq(aActual.guid, aExpected.guid); + do_check_eq(aActual.timeCreated, aExpected.timeCreated); + do_check_eq(aActual.timeLastUsed, aExpected.timeLastUsed); + do_check_eq(aActual.timePasswordChanged, aExpected.timePasswordChanged); + do_check_eq(aActual.timesUsed, aExpected.timesUsed); +} + +/** + * nsILoginInfo instances with or without nsILoginMetaInfo properties. + */ +var gLoginInfo1; +var gLoginInfo2; +var gLoginInfo3; + +/** + * nsILoginInfo instances reloaded with all the nsILoginMetaInfo properties. + * These are often used to provide the reference values to test against. + */ +var gLoginMetaInfo1; +var gLoginMetaInfo2; +var gLoginMetaInfo3; + +// Tests + +/** + * Prepare the test objects that will be used by the following tests. + */ +add_task(function test_initialize() +{ + // Use a reference time from ten minutes ago to initialize one instance of + // nsILoginMetaInfo, to test that reference times are updated when needed. + let baseTimeMs = Date.now() - 600000; + + gLoginInfo1 = TestData.formLogin(); + gLoginInfo2 = TestData.formLogin({ + hostname: "http://other.example.com", + guid: gUUIDGenerator.generateUUID().toString(), + timeCreated: baseTimeMs, + timeLastUsed: baseTimeMs + 2, + timePasswordChanged: baseTimeMs + 1, + timesUsed: 2, + }); + gLoginInfo3 = TestData.authLogin(); +}); + +/** + * Tests the behavior of addLogin with regard to metadata. The logins added + * here are also used by the following tests. + */ +add_task(function test_addLogin_metainfo() +{ + // Add a login without metadata to the database. + Services.logins.addLogin(gLoginInfo1); + + // The object provided to addLogin should not have been modified. + do_check_eq(gLoginInfo1.guid, null); + do_check_eq(gLoginInfo1.timeCreated, 0); + do_check_eq(gLoginInfo1.timeLastUsed, 0); + do_check_eq(gLoginInfo1.timePasswordChanged, 0); + do_check_eq(gLoginInfo1.timesUsed, 0); + + // A login with valid metadata should have been stored. + gLoginMetaInfo1 = retrieveLoginMatching(gLoginInfo1); + do_check_true(gLooksLikeUUIDRegex.test(gLoginMetaInfo1.guid)); + let creationTime = gLoginMetaInfo1.timeCreated; + LoginTestUtils.assertTimeIsAboutNow(creationTime); + do_check_eq(gLoginMetaInfo1.timeLastUsed, creationTime); + do_check_eq(gLoginMetaInfo1.timePasswordChanged, creationTime); + do_check_eq(gLoginMetaInfo1.timesUsed, 1); + + // Add a login without metadata to the database. + let originalLogin = gLoginInfo2.clone().QueryInterface(Ci.nsILoginMetaInfo); + Services.logins.addLogin(gLoginInfo2); + + // The object provided to addLogin should not have been modified. + assertMetaInfoEqual(gLoginInfo2, originalLogin); + + // A login with the provided metadata should have been stored. + gLoginMetaInfo2 = retrieveLoginMatching(gLoginInfo2); + assertMetaInfoEqual(gLoginMetaInfo2, gLoginInfo2); + + // Add an authentication login to the database before continuing. + Services.logins.addLogin(gLoginInfo3); + gLoginMetaInfo3 = retrieveLoginMatching(gLoginInfo3); + LoginTestUtils.checkLogins([gLoginInfo1, gLoginInfo2, gLoginInfo3]); +}); + +/** + * Tests that adding a login with a duplicate GUID throws an exception. + */ +add_task(function test_addLogin_metainfo_duplicate() +{ + let loginInfo = TestData.formLogin({ + hostname: "http://duplicate.example.com", + guid: gLoginMetaInfo2.guid, + }); + Assert.throws(() => Services.logins.addLogin(loginInfo), + /specified GUID already exists/); + + // Verify that no data was stored by the previous call. + LoginTestUtils.checkLogins([gLoginInfo1, gLoginInfo2, gLoginInfo3]); +}); + +/** + * Tests that the existing metadata is not changed when modifyLogin is called + * with an nsILoginInfo argument. + */ +add_task(function test_modifyLogin_nsILoginInfo_metainfo_ignored() +{ + let newLoginInfo = gLoginInfo1.clone().QueryInterface(Ci.nsILoginMetaInfo); + newLoginInfo.guid = gUUIDGenerator.generateUUID().toString(); + newLoginInfo.timeCreated = Date.now(); + newLoginInfo.timeLastUsed = Date.now(); + newLoginInfo.timePasswordChanged = Date.now(); + newLoginInfo.timesUsed = 12; + Services.logins.modifyLogin(gLoginInfo1, newLoginInfo); + + newLoginInfo = retrieveLoginMatching(gLoginInfo1); + assertMetaInfoEqual(newLoginInfo, gLoginMetaInfo1); +}); + +/** + * Tests the modifyLogin function with an nsIProperyBag argument. + */ +add_task(function test_modifyLogin_nsIProperyBag_metainfo() +{ + // Use a new reference time that is two minutes from now. + let newTimeMs = Date.now() + 120000; + let newUUIDValue = gUUIDGenerator.generateUUID().toString(); + + // Check that properties are changed as requested. + Services.logins.modifyLogin(gLoginInfo1, newPropertyBag({ + guid: newUUIDValue, + timeCreated: newTimeMs, + timeLastUsed: newTimeMs + 2, + timePasswordChanged: newTimeMs + 1, + timesUsed: 2, + })); + + gLoginMetaInfo1 = retrieveLoginMatching(gLoginInfo1); + do_check_eq(gLoginMetaInfo1.guid, newUUIDValue); + do_check_eq(gLoginMetaInfo1.timeCreated, newTimeMs); + do_check_eq(gLoginMetaInfo1.timeLastUsed, newTimeMs + 2); + do_check_eq(gLoginMetaInfo1.timePasswordChanged, newTimeMs + 1); + do_check_eq(gLoginMetaInfo1.timesUsed, 2); + + // Check that timePasswordChanged is updated when changing the password. + let originalLogin = gLoginInfo2.clone().QueryInterface(Ci.nsILoginMetaInfo); + Services.logins.modifyLogin(gLoginInfo2, newPropertyBag({ + password: "new password", + })); + gLoginInfo2.password = "new password"; + + gLoginMetaInfo2 = retrieveLoginMatching(gLoginInfo2); + do_check_eq(gLoginMetaInfo2.password, gLoginInfo2.password); + do_check_eq(gLoginMetaInfo2.timeCreated, originalLogin.timeCreated); + do_check_eq(gLoginMetaInfo2.timeLastUsed, originalLogin.timeLastUsed); + LoginTestUtils.assertTimeIsAboutNow(gLoginMetaInfo2.timePasswordChanged); + + // Check that timePasswordChanged is not set to the current time when changing + // the password and specifying a new value for the property at the same time. + Services.logins.modifyLogin(gLoginInfo2, newPropertyBag({ + password: "other password", + timePasswordChanged: newTimeMs, + })); + gLoginInfo2.password = "other password"; + + gLoginMetaInfo2 = retrieveLoginMatching(gLoginInfo2); + do_check_eq(gLoginMetaInfo2.password, gLoginInfo2.password); + do_check_eq(gLoginMetaInfo2.timeCreated, originalLogin.timeCreated); + do_check_eq(gLoginMetaInfo2.timeLastUsed, originalLogin.timeLastUsed); + do_check_eq(gLoginMetaInfo2.timePasswordChanged, newTimeMs); + + // Check the special timesUsedIncrement property. + Services.logins.modifyLogin(gLoginInfo2, newPropertyBag({ + timesUsedIncrement: 2, + })); + + gLoginMetaInfo2 = retrieveLoginMatching(gLoginInfo2); + do_check_eq(gLoginMetaInfo2.timeCreated, originalLogin.timeCreated); + do_check_eq(gLoginMetaInfo2.timeLastUsed, originalLogin.timeLastUsed); + do_check_eq(gLoginMetaInfo2.timePasswordChanged, newTimeMs); + do_check_eq(gLoginMetaInfo2.timesUsed, 4); +}); + +/** + * Tests that modifying a login to a duplicate GUID throws an exception. + */ +add_task(function test_modifyLogin_nsIProperyBag_metainfo_duplicate() +{ + Assert.throws(() => Services.logins.modifyLogin(gLoginInfo1, newPropertyBag({ + guid: gLoginInfo2.guid, + })), /specified GUID already exists/); + LoginTestUtils.checkLogins([gLoginInfo1, gLoginInfo2, gLoginInfo3]); +}); + +/** + * Tests searching logins using nsILoginMetaInfo properties. + */ +add_task(function test_searchLogins_metainfo() +{ + // Find by GUID. + let logins = Services.logins.searchLogins({}, newPropertyBag({ + guid: gLoginMetaInfo1.guid, + })); + do_check_eq(logins.length, 1); + let foundLogin = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + assertMetaInfoEqual(foundLogin, gLoginMetaInfo1); + + // Find by timestamp. + logins = Services.logins.searchLogins({}, newPropertyBag({ + timePasswordChanged: gLoginMetaInfo2.timePasswordChanged, + })); + do_check_eq(logins.length, 1); + foundLogin = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + assertMetaInfoEqual(foundLogin, gLoginMetaInfo2); + + // Find using two properties at the same time. + logins = Services.logins.searchLogins({}, newPropertyBag({ + guid: gLoginMetaInfo3.guid, + timePasswordChanged: gLoginMetaInfo3.timePasswordChanged, + })); + do_check_eq(logins.length, 1); + foundLogin = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + assertMetaInfoEqual(foundLogin, gLoginMetaInfo3); +}); + +/** + * Tests that the default nsILoginManagerStorage module attached to the Login + * Manager service is able to save and reload nsILoginMetaInfo properties. + */ +add_task(function* test_storage_metainfo() +{ + yield* LoginTestUtils.reloadData(); + LoginTestUtils.checkLogins([gLoginInfo1, gLoginInfo2, gLoginInfo3]); + + assertMetaInfoEqual(retrieveLoginMatching(gLoginInfo1), gLoginMetaInfo1); + assertMetaInfoEqual(retrieveLoginMatching(gLoginInfo2), gLoginMetaInfo2); + assertMetaInfoEqual(retrieveLoginMatching(gLoginInfo3), gLoginMetaInfo3); +}); diff --git a/toolkit/components/passwordmgr/test/unit/test_logins_search.js b/toolkit/components/passwordmgr/test/unit/test_logins_search.js new file mode 100644 index 000000000..188c75039 --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_logins_search.js @@ -0,0 +1,221 @@ +/* + * Tests methods that find specific logins in the store (findLogins, + * searchLogins, and countLogins). + * + * The getAllLogins method is not tested explicitly here, because it is used by + * all tests to verify additions, removals and modifications to the login store. + */ + +"use strict"; + +// Globals + +/** + * Returns a list of new nsILoginInfo objects that are a subset of the test + * data, built to match the specified query. + * + * @param aQuery + * Each property and value of this object restricts the search to those + * entries from the test data that match the property exactly. + */ +function buildExpectedLogins(aQuery) +{ + return TestData.loginList().filter( + entry => Object.keys(aQuery).every(name => entry[name] === aQuery[name])); +} + +/** + * Tests the searchLogins function. + * + * @param aQuery + * Each property and value of this object is translated to an entry in + * the nsIPropertyBag parameter of searchLogins. + * @param aExpectedCount + * Number of logins from the test data that should be found. The actual + * list of logins is obtained using the buildExpectedLogins helper, and + * this value is just used to verify that modifications to the test data + * don't make the current test meaningless. + */ +function checkSearchLogins(aQuery, aExpectedCount) +{ + do_print("Testing searchLogins for " + JSON.stringify(aQuery)); + + let expectedLogins = buildExpectedLogins(aQuery); + do_check_eq(expectedLogins.length, aExpectedCount); + + let outCount = {}; + let logins = Services.logins.searchLogins(outCount, newPropertyBag(aQuery)); + do_check_eq(outCount.value, expectedLogins.length); + LoginTestUtils.assertLoginListsEqual(logins, expectedLogins); +} + +/** + * Tests findLogins, searchLogins, and countLogins with the same query. + * + * @param aQuery + * The "hostname", "formSubmitURL", and "httpRealm" properties of this + * object are passed as parameters to findLogins and countLogins. The + * same object is then passed to the checkSearchLogins function. + * @param aExpectedCount + * Number of logins from the test data that should be found. The actual + * list of logins is obtained using the buildExpectedLogins helper, and + * this value is just used to verify that modifications to the test data + * don't make the current test meaningless. + */ +function checkAllSearches(aQuery, aExpectedCount) +{ + do_print("Testing all search functions for " + JSON.stringify(aQuery)); + + let expectedLogins = buildExpectedLogins(aQuery); + do_check_eq(expectedLogins.length, aExpectedCount); + + // The findLogins and countLogins functions support wildcard matches by + // specifying empty strings as parameters, while searchLogins requires + // omitting the property entirely. + let hostname = ("hostname" in aQuery) ? aQuery.hostname : ""; + let formSubmitURL = ("formSubmitURL" in aQuery) ? aQuery.formSubmitURL : ""; + let httpRealm = ("httpRealm" in aQuery) ? aQuery.httpRealm : ""; + + // Test findLogins. + let outCount = {}; + let logins = Services.logins.findLogins(outCount, hostname, formSubmitURL, + httpRealm); + do_check_eq(outCount.value, expectedLogins.length); + LoginTestUtils.assertLoginListsEqual(logins, expectedLogins); + + // Test countLogins. + let count = Services.logins.countLogins(hostname, formSubmitURL, httpRealm); + do_check_eq(count, expectedLogins.length); + + // Test searchLogins. + checkSearchLogins(aQuery, aExpectedCount); +} + +// Tests + +/** + * Prepare data for the following tests. + */ +add_task(function test_initialize() +{ + for (let login of TestData.loginList()) { + Services.logins.addLogin(login); + } +}); + +/** + * Tests findLogins, searchLogins, and countLogins with basic queries. + */ +add_task(function test_search_all_basic() +{ + // Find all logins, using no filters in the search functions. + checkAllSearches({}, 23); + + // Find all form logins, then all authentication logins. + checkAllSearches({ httpRealm: null }, 14); + checkAllSearches({ formSubmitURL: null }, 9); + + // Find all form logins on one host, then all authentication logins. + checkAllSearches({ hostname: "http://www4.example.com", + httpRealm: null }, 3); + checkAllSearches({ hostname: "http://www2.example.org", + formSubmitURL: null }, 2); + + // Verify that scheme and subdomain are distinct in the hostname. + checkAllSearches({ hostname: "http://www.example.com" }, 1); + checkAllSearches({ hostname: "https://www.example.com" }, 1); + checkAllSearches({ hostname: "https://example.com" }, 1); + checkAllSearches({ hostname: "http://www3.example.com" }, 3); + + // Verify that scheme and subdomain are distinct in formSubmitURL. + checkAllSearches({ formSubmitURL: "http://www.example.com" }, 2); + checkAllSearches({ formSubmitURL: "https://www.example.com" }, 2); + checkAllSearches({ formSubmitURL: "http://example.com" }, 1); + + // Find by formSubmitURL on a single host. + checkAllSearches({ hostname: "http://www3.example.com", + formSubmitURL: "http://www.example.com" }, 1); + checkAllSearches({ hostname: "http://www3.example.com", + formSubmitURL: "https://www.example.com" }, 1); + checkAllSearches({ hostname: "http://www3.example.com", + formSubmitURL: "http://example.com" }, 1); + + // Find by httpRealm on all hosts. + checkAllSearches({ httpRealm: "The HTTP Realm" }, 3); + checkAllSearches({ httpRealm: "ftp://ftp.example.org" }, 1); + checkAllSearches({ httpRealm: "The HTTP Realm Other" }, 2); + + // Find by httpRealm on a single host. + checkAllSearches({ hostname: "http://example.net", + httpRealm: "The HTTP Realm" }, 1); + checkAllSearches({ hostname: "http://example.net", + httpRealm: "The HTTP Realm Other" }, 1); + checkAllSearches({ hostname: "ftp://example.net", + httpRealm: "ftp://example.net" }, 1); +}); + +/** + * Tests searchLogins with advanced queries. + */ +add_task(function test_searchLogins() +{ + checkSearchLogins({ usernameField: "form_field_username" }, 12); + checkSearchLogins({ passwordField: "form_field_password" }, 13); + + // Find all logins with an empty usernameField, including for authentication. + checkSearchLogins({ usernameField: "" }, 11); + + // Find form logins with an empty usernameField. + checkSearchLogins({ httpRealm: null, + usernameField: "" }, 2); + + // Find logins with an empty usernameField on one host. + checkSearchLogins({ hostname: "http://www6.example.com", + usernameField: "" }, 1); +}); + +/** + * Tests searchLogins with invalid arguments. + */ +add_task(function test_searchLogins_invalid() +{ + Assert.throws(() => Services.logins.searchLogins({}, + newPropertyBag({ username: "value" })), + /Unexpected field/); +}); + +/** + * Tests that matches are case-sensitive, compare the full field value, and are + * strict when interpreting the prePath of URIs. + */ +add_task(function test_search_all_full_case_sensitive() +{ + checkAllSearches({ hostname: "http://www.example.com" }, 1); + checkAllSearches({ hostname: "http://www.example.com/" }, 0); + checkAllSearches({ hostname: "http://" }, 0); + checkAllSearches({ hostname: "example.com" }, 0); + + checkAllSearches({ formSubmitURL: "http://www.example.com" }, 2); + checkAllSearches({ formSubmitURL: "http://www.example.com/" }, 0); + checkAllSearches({ formSubmitURL: "http://" }, 0); + checkAllSearches({ formSubmitURL: "example.com" }, 0); + + checkAllSearches({ httpRealm: "The HTTP Realm" }, 3); + checkAllSearches({ httpRealm: "The http Realm" }, 0); + checkAllSearches({ httpRealm: "The HTTP" }, 0); + checkAllSearches({ httpRealm: "Realm" }, 0); +}); + +/** + * Tests findLogins, searchLogins, and countLogins with queries that should + * return no values. + */ +add_task(function test_search_all_empty() +{ + checkAllSearches({ hostname: "http://nonexistent.example.com" }, 0); + checkAllSearches({ formSubmitURL: "http://www.example.com", + httpRealm: "The HTTP Realm" }, 0); + + checkSearchLogins({ hostname: "" }, 0); + checkSearchLogins({ id: "1000" }, 0); +}); diff --git a/toolkit/components/passwordmgr/test/unit/test_maybeImportLogin.js b/toolkit/components/passwordmgr/test/unit/test_maybeImportLogin.js new file mode 100644 index 000000000..19175df59 --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_maybeImportLogin.js @@ -0,0 +1,169 @@ +"use strict"; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/LoginHelper.jsm"); + +const HOST1 = "https://www.example.com/"; +const HOST2 = "https://www.mozilla.org/"; + +const USER1 = "myuser"; +const USER2 = "anotheruser"; + +const PASS1 = "mypass"; +const PASS2 = "anotherpass"; +const PASS3 = "yetanotherpass"; + +add_task(function test_new_logins() { + let importedLogin = LoginHelper.maybeImportLogin({ + username: USER1, + password: PASS1, + hostname: HOST1, + formSubmitURL: HOST1, + }); + Assert.ok(importedLogin, "Return value should indicate imported login."); + let matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1}); + Assert.equal(matchingLogins.length, 1, `There should be 1 login for ${HOST1}`); + + importedLogin = LoginHelper.maybeImportLogin({ + username: USER1, + password: PASS1, + hostname: HOST2, + formSubmitURL: HOST2, + }); + + Assert.ok(importedLogin, "Return value should indicate another imported login."); + matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1}); + Assert.equal(matchingLogins.length, 1, `There should still be 1 login for ${HOST1}`); + + matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST2}); + Assert.equal(matchingLogins.length, 1, `There should also be 1 login for ${HOST2}`); + Assert.equal(Services.logins.getAllLogins().length, 2, "There should be 2 logins in total"); + Services.logins.removeAllLogins(); +}); + +add_task(function test_duplicate_logins() { + let importedLogin = LoginHelper.maybeImportLogin({ + username: USER1, + password: PASS1, + hostname: HOST1, + formSubmitURL: HOST1, + }); + Assert.ok(importedLogin, "Return value should indicate imported login."); + let matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1}); + Assert.equal(matchingLogins.length, 1, `There should be 1 login for ${HOST1}`); + + importedLogin = LoginHelper.maybeImportLogin({ + username: USER1, + password: PASS1, + hostname: HOST1, + formSubmitURL: HOST1, + }); + Assert.ok(!importedLogin, "Return value should indicate no new login was imported."); + matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1}); + Assert.equal(matchingLogins.length, 1, `There should still be 1 login for ${HOST1}`); + Services.logins.removeAllLogins(); +}); + +add_task(function test_different_passwords() { + let importedLogin = LoginHelper.maybeImportLogin({ + username: USER1, + password: PASS1, + hostname: HOST1, + formSubmitURL: HOST1, + timeCreated: new Date(Date.now() - 1000), + }); + Assert.ok(importedLogin, "Return value should indicate imported login."); + let matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1}); + Assert.equal(matchingLogins.length, 1, `There should be 1 login for ${HOST1}`); + + // This item will be newer, so its password should take precedence. + importedLogin = LoginHelper.maybeImportLogin({ + username: USER1, + password: PASS2, + hostname: HOST1, + formSubmitURL: HOST1, + timeCreated: new Date(), + }); + Assert.ok(!importedLogin, "Return value should not indicate imported login (as we updated an existing one)."); + matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1}); + Assert.equal(matchingLogins.length, 1, `There should still be 1 login for ${HOST1}`); + Assert.equal(matchingLogins[0].password, PASS2, "We should have updated the password for this login."); + + // Now try to update with an older password: + importedLogin = LoginHelper.maybeImportLogin({ + username: USER1, + password: PASS3, + hostname: HOST1, + formSubmitURL: HOST1, + timeCreated: new Date(Date.now() - 1000000), + }); + Assert.ok(!importedLogin, "Return value should not indicate imported login (as we didn't update anything)."); + matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1}); + Assert.equal(matchingLogins.length, 1, `There should still be 1 login for ${HOST1}`); + Assert.equal(matchingLogins[0].password, PASS2, "We should NOT have updated the password for this login."); + + Services.logins.removeAllLogins(); +}); + +add_task(function test_different_usernames() { + let importedLogin = LoginHelper.maybeImportLogin({ + username: USER1, + password: PASS1, + hostname: HOST1, + formSubmitURL: HOST1, + }); + Assert.ok(importedLogin, "Return value should indicate imported login."); + let matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1}); + Assert.equal(matchingLogins.length, 1, `There should be 1 login for ${HOST1}`); + + importedLogin = LoginHelper.maybeImportLogin({ + username: USER2, + password: PASS1, + hostname: HOST1, + formSubmitURL: HOST1, + }); + Assert.ok(importedLogin, "Return value should indicate another imported login."); + matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1}); + Assert.equal(matchingLogins.length, 2, `There should now be 2 logins for ${HOST1}`); + + Services.logins.removeAllLogins(); +}); + +add_task(function test_different_targets() { + let importedLogin = LoginHelper.maybeImportLogin({ + username: USER1, + password: PASS1, + hostname: HOST1, + formSubmitURL: HOST1, + }); + Assert.ok(importedLogin, "Return value should indicate imported login."); + let matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1}); + Assert.equal(matchingLogins.length, 1, `There should be 1 login for ${HOST1}`); + + // Not passing either a formSubmitURL or a httpRealm should be treated as + // the same as the previous login + importedLogin = LoginHelper.maybeImportLogin({ + username: USER1, + password: PASS1, + hostname: HOST1, + }); + Assert.ok(!importedLogin, "Return value should NOT indicate imported login " + + "(because a missing formSubmitURL and httpRealm should be duped to the existing login)."); + matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1}); + Assert.equal(matchingLogins.length, 1, `There should still be 1 login for ${HOST1}`); + Assert.equal(matchingLogins[0].formSubmitURL, HOST1, "The form submission URL should have been kept."); + + importedLogin = LoginHelper.maybeImportLogin({ + username: USER1, + password: PASS1, + hostname: HOST1, + httpRealm: HOST1, + }); + Assert.ok(importedLogin, "Return value should indicate another imported login " + + "as an httpRealm login shouldn't be duped."); + matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1}); + Assert.equal(matchingLogins.length, 2, `There should now be 2 logins for ${HOST1}`); + + Services.logins.removeAllLogins(); +}); + diff --git a/toolkit/components/passwordmgr/test/unit/test_module_LoginImport.js b/toolkit/components/passwordmgr/test/unit/test_module_LoginImport.js new file mode 100644 index 000000000..b8793e1bd --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_module_LoginImport.js @@ -0,0 +1,243 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the LoginImport object. + */ + +"use strict"; + +// Globals + +Cu.import("resource://gre/modules/Task.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper", + "resource://gre/modules/LoginHelper.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "LoginImport", + "resource://gre/modules/LoginImport.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "LoginStore", + "resource://gre/modules/LoginStore.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Sqlite", + "resource://gre/modules/Sqlite.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "gLoginManagerCrypto", + "@mozilla.org/login-manager/crypto/SDR;1", + "nsILoginManagerCrypto"); +XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator", + "@mozilla.org/uuid-generator;1", + "nsIUUIDGenerator"); + +/** + * Creates empty login data tables in the given SQLite connection, resembling + * the most recent schema version (excluding indices). + */ +function promiseCreateDatabaseSchema(aConnection) +{ + return Task.spawn(function* () { + yield aConnection.setSchemaVersion(5); + yield aConnection.execute("CREATE TABLE moz_logins (" + + "id INTEGER PRIMARY KEY," + + "hostname TEXT NOT NULL," + + "httpRealm TEXT," + + "formSubmitURL TEXT," + + "usernameField TEXT NOT NULL," + + "passwordField TEXT NOT NULL," + + "encryptedUsername TEXT NOT NULL," + + "encryptedPassword TEXT NOT NULL," + + "guid TEXT," + + "encType INTEGER," + + "timeCreated INTEGER," + + "timeLastUsed INTEGER," + + "timePasswordChanged INTEGER," + + "timesUsed INTEGER)"); + yield aConnection.execute("CREATE TABLE moz_disabledHosts (" + + "id INTEGER PRIMARY KEY," + + "hostname TEXT UNIQUE)"); + yield aConnection.execute("CREATE TABLE moz_deleted_logins (" + + "id INTEGER PRIMARY KEY," + + "guid TEXT," + + "timeDeleted INTEGER)"); + }); +} + +/** + * Inserts a new entry in the database resembling the given nsILoginInfo object. + */ +function promiseInsertLoginInfo(aConnection, aLoginInfo) +{ + aLoginInfo.QueryInterface(Ci.nsILoginMetaInfo); + + // We can't use the aLoginInfo object directly in the execute statement + // because the bind code in Sqlite.jsm doesn't allow objects with extra + // properties beyond those being binded. So we might as well use an array as + // it is simpler. + let values = [ + aLoginInfo.hostname, + aLoginInfo.httpRealm, + aLoginInfo.formSubmitURL, + aLoginInfo.usernameField, + aLoginInfo.passwordField, + gLoginManagerCrypto.encrypt(aLoginInfo.username), + gLoginManagerCrypto.encrypt(aLoginInfo.password), + aLoginInfo.guid, + aLoginInfo.encType, + aLoginInfo.timeCreated, + aLoginInfo.timeLastUsed, + aLoginInfo.timePasswordChanged, + aLoginInfo.timesUsed, + ]; + + return aConnection.execute("INSERT INTO moz_logins (hostname, " + + "httpRealm, formSubmitURL, usernameField, " + + "passwordField, encryptedUsername, " + + "encryptedPassword, guid, encType, timeCreated, " + + "timeLastUsed, timePasswordChanged, timesUsed) " + + "VALUES (?" + ",?".repeat(12) + ")", values); +} + +/** + * Inserts a new disabled host entry in the database. + */ +function promiseInsertDisabledHost(aConnection, aHostname) +{ + return aConnection.execute("INSERT INTO moz_disabledHosts (hostname) " + + "VALUES (?)", [aHostname]); +} + +// Tests + +/** + * Imports login data from a SQLite file constructed using the test data. + */ +add_task(function* test_import() +{ + let store = new LoginStore(getTempFile("test-import.json").path); + let loginsSqlite = getTempFile("test-logins.sqlite").path; + + // Prepare the logins to be imported, including the nsILoginMetaInfo data. + let loginList = TestData.loginList(); + for (let loginInfo of loginList) { + loginInfo.QueryInterface(Ci.nsILoginMetaInfo); + loginInfo.guid = gUUIDGenerator.generateUUID().toString(); + loginInfo.timeCreated = Date.now(); + loginInfo.timeLastUsed = Date.now(); + loginInfo.timePasswordChanged = Date.now(); + loginInfo.timesUsed = 1; + } + + // Create and populate the SQLite database first. + let connection = yield Sqlite.openConnection({ path: loginsSqlite }); + try { + yield promiseCreateDatabaseSchema(connection); + for (let loginInfo of loginList) { + yield promiseInsertLoginInfo(connection, loginInfo); + } + yield promiseInsertDisabledHost(connection, "http://www.example.com"); + yield promiseInsertDisabledHost(connection, "https://www.example.org"); + } finally { + yield connection.close(); + } + + // The "load" method must be called before importing data. + yield store.load(); + yield new LoginImport(store, loginsSqlite).import(); + + // Verify that every login in the test data has a matching imported row. + do_check_eq(loginList.length, store.data.logins.length); + do_check_true(loginList.every(function (loginInfo) { + return store.data.logins.some(function (loginDataItem) { + let username = gLoginManagerCrypto.decrypt(loginDataItem.encryptedUsername); + let password = gLoginManagerCrypto.decrypt(loginDataItem.encryptedPassword); + return loginDataItem.hostname == loginInfo.hostname && + loginDataItem.httpRealm == loginInfo.httpRealm && + loginDataItem.formSubmitURL == loginInfo.formSubmitURL && + loginDataItem.usernameField == loginInfo.usernameField && + loginDataItem.passwordField == loginInfo.passwordField && + username == loginInfo.username && + password == loginInfo.password && + loginDataItem.guid == loginInfo.guid && + loginDataItem.encType == loginInfo.encType && + loginDataItem.timeCreated == loginInfo.timeCreated && + loginDataItem.timeLastUsed == loginInfo.timeLastUsed && + loginDataItem.timePasswordChanged == loginInfo.timePasswordChanged && + loginDataItem.timesUsed == loginInfo.timesUsed; + }); + })); + + // Verify that disabled hosts have been imported. + do_check_eq(store.data.disabledHosts.length, 2); + do_check_true(store.data.disabledHosts.indexOf("http://www.example.com") != -1); + do_check_true(store.data.disabledHosts.indexOf("https://www.example.org") != -1); +}); + +/** + * Tests imports of NULL values due to a downgraded database. + */ +add_task(function* test_import_downgraded() +{ + let store = new LoginStore(getTempFile("test-import-downgraded.json").path); + let loginsSqlite = getTempFile("test-logins-downgraded.sqlite").path; + + // Create and populate the SQLite database first. + let connection = yield Sqlite.openConnection({ path: loginsSqlite }); + try { + yield promiseCreateDatabaseSchema(connection); + yield connection.setSchemaVersion(3); + yield promiseInsertLoginInfo(connection, TestData.formLogin({ + guid: gUUIDGenerator.generateUUID().toString(), + timeCreated: null, + timeLastUsed: null, + timePasswordChanged: null, + timesUsed: 0, + })); + } finally { + yield connection.close(); + } + + // The "load" method must be called before importing data. + yield store.load(); + yield new LoginImport(store, loginsSqlite).import(); + + // Verify that the missing metadata was generated correctly. + let loginItem = store.data.logins[0]; + let creationTime = loginItem.timeCreated; + LoginTestUtils.assertTimeIsAboutNow(creationTime); + do_check_eq(loginItem.timeLastUsed, creationTime); + do_check_eq(loginItem.timePasswordChanged, creationTime); + do_check_eq(loginItem.timesUsed, 1); +}); + +/** + * Verifies that importing from a SQLite file with database version 2 fails. + */ +add_task(function* test_import_v2() +{ + let store = new LoginStore(getTempFile("test-import-v2.json").path); + let loginsSqlite = do_get_file("data/signons-v2.sqlite").path; + + // The "load" method must be called before importing data. + yield store.load(); + try { + yield new LoginImport(store, loginsSqlite).import(); + do_throw("The operation should have failed."); + } catch (ex) { } +}); + +/** + * Imports login data from a SQLite file, with database version 3. + */ +add_task(function* test_import_v3() +{ + let store = new LoginStore(getTempFile("test-import-v3.json").path); + let loginsSqlite = do_get_file("data/signons-v3.sqlite").path; + + // The "load" method must be called before importing data. + yield store.load(); + yield new LoginImport(store, loginsSqlite).import(); + + // We only execute basic integrity checks. + do_check_eq(store.data.logins[0].usernameField, "u1"); + do_check_eq(store.data.disabledHosts.length, 0); +}); diff --git a/toolkit/components/passwordmgr/test/unit/test_module_LoginStore.js b/toolkit/components/passwordmgr/test/unit/test_module_LoginStore.js new file mode 100644 index 000000000..335eb601b --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_module_LoginStore.js @@ -0,0 +1,206 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the LoginStore object. + */ + +"use strict"; + +// Globals + +XPCOMUtils.defineLazyModuleGetter(this, "LoginStore", + "resource://gre/modules/LoginStore.jsm"); + +const TEST_STORE_FILE_NAME = "test-logins.json"; + +// Tests + +/** + * Saves login data to a file, then reloads it. + */ +add_task(function* test_save_reload() +{ + let storeForSave = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path); + + // The "load" method must be called before preparing the data to be saved. + yield storeForSave.load(); + + let rawLoginData = { + id: storeForSave.data.nextId++, + hostname: "http://www.example.com", + httpRealm: null, + formSubmitURL: "http://www.example.com/submit-url", + usernameField: "field_" + String.fromCharCode(533, 537, 7570, 345), + passwordField: "field_" + String.fromCharCode(421, 259, 349, 537), + encryptedUsername: "(test)", + encryptedPassword: "(test)", + guid: "(test)", + encType: Ci.nsILoginManagerCrypto.ENCTYPE_SDR, + timeCreated: Date.now(), + timeLastUsed: Date.now(), + timePasswordChanged: Date.now(), + timesUsed: 1, + }; + storeForSave.data.logins.push(rawLoginData); + + storeForSave.data.disabledHosts.push("http://www.example.org"); + + yield storeForSave._save(); + + // Test the asynchronous initialization path. + let storeForLoad = new LoginStore(storeForSave.path); + yield storeForLoad.load(); + + do_check_eq(storeForLoad.data.logins.length, 1); + do_check_matches(storeForLoad.data.logins[0], rawLoginData); + do_check_eq(storeForLoad.data.disabledHosts.length, 1); + do_check_eq(storeForLoad.data.disabledHosts[0], "http://www.example.org"); + + // Test the synchronous initialization path. + storeForLoad = new LoginStore(storeForSave.path); + storeForLoad.ensureDataReady(); + + do_check_eq(storeForLoad.data.logins.length, 1); + do_check_matches(storeForLoad.data.logins[0], rawLoginData); + do_check_eq(storeForLoad.data.disabledHosts.length, 1); + do_check_eq(storeForLoad.data.disabledHosts[0], "http://www.example.org"); +}); + +/** + * Checks that loading from a missing file results in empty arrays. + */ +add_task(function* test_load_empty() +{ + let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path); + + do_check_false(yield OS.File.exists(store.path)); + + yield store.load(); + + do_check_false(yield OS.File.exists(store.path)); + + do_check_eq(store.data.logins.length, 0); + do_check_eq(store.data.disabledHosts.length, 0); +}); + +/** + * Checks that saving empty data still overwrites any existing file. + */ +add_task(function* test_save_empty() +{ + let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path); + + yield store.load(); + + let createdFile = yield OS.File.open(store.path, { create: true }); + yield createdFile.close(); + + yield store._save(); + + do_check_true(yield OS.File.exists(store.path)); +}); + +/** + * Loads data from a string in a predefined format. The purpose of this test is + * to verify that the JSON format used in previous versions can be loaded. + */ +add_task(function* test_load_string_predefined() +{ + let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path); + + let string = "{\"logins\":[{" + + "\"id\":1," + + "\"hostname\":\"http://www.example.com\"," + + "\"httpRealm\":null," + + "\"formSubmitURL\":\"http://www.example.com/submit-url\"," + + "\"usernameField\":\"usernameField\"," + + "\"passwordField\":\"passwordField\"," + + "\"encryptedUsername\":\"(test)\"," + + "\"encryptedPassword\":\"(test)\"," + + "\"guid\":\"(test)\"," + + "\"encType\":1," + + "\"timeCreated\":1262304000000," + + "\"timeLastUsed\":1262390400000," + + "\"timePasswordChanged\":1262476800000," + + "\"timesUsed\":1}],\"disabledHosts\":[" + + "\"http://www.example.org\"]}"; + + yield OS.File.writeAtomic(store.path, + new TextEncoder().encode(string), + { tmpPath: store.path + ".tmp" }); + + yield store.load(); + + do_check_eq(store.data.logins.length, 1); + do_check_matches(store.data.logins[0], { + id: 1, + hostname: "http://www.example.com", + httpRealm: null, + formSubmitURL: "http://www.example.com/submit-url", + usernameField: "usernameField", + passwordField: "passwordField", + encryptedUsername: "(test)", + encryptedPassword: "(test)", + guid: "(test)", + encType: Ci.nsILoginManagerCrypto.ENCTYPE_SDR, + timeCreated: 1262304000000, + timeLastUsed: 1262390400000, + timePasswordChanged: 1262476800000, + timesUsed: 1, + }); + + do_check_eq(store.data.disabledHosts.length, 1); + do_check_eq(store.data.disabledHosts[0], "http://www.example.org"); +}); + +/** + * Loads login data from a malformed JSON string. + */ +add_task(function* test_load_string_malformed() +{ + let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path); + + let string = "{\"logins\":[{\"hostname\":\"http://www.example.com\"," + + "\"id\":1,"; + + yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string), + { tmpPath: store.path + ".tmp" }); + + yield store.load(); + + // A backup file should have been created. + do_check_true(yield OS.File.exists(store.path + ".corrupt")); + yield OS.File.remove(store.path + ".corrupt"); + + // The store should be ready to accept new data. + do_check_eq(store.data.logins.length, 0); + do_check_eq(store.data.disabledHosts.length, 0); +}); + +/** + * Loads login data from a malformed JSON string, using the synchronous + * initialization path. + */ +add_task(function* test_load_string_malformed_sync() +{ + let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path); + + let string = "{\"logins\":[{\"hostname\":\"http://www.example.com\"," + + "\"id\":1,"; + + yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string), + { tmpPath: store.path + ".tmp" }); + + store.ensureDataReady(); + + // A backup file should have been created. + do_check_true(yield OS.File.exists(store.path + ".corrupt")); + yield OS.File.remove(store.path + ".corrupt"); + + // The store should be ready to accept new data. + do_check_eq(store.data.logins.length, 0); + do_check_eq(store.data.disabledHosts.length, 0); +}); diff --git a/toolkit/components/passwordmgr/test/unit/test_notifications.js b/toolkit/components/passwordmgr/test/unit/test_notifications.js new file mode 100644 index 000000000..41caa2c1b --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_notifications.js @@ -0,0 +1,172 @@ +/* + * Tests notifications dispatched when modifying stored logins. + */ + +var expectedNotification; +var expectedData; + +var TestObserver = { + QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), + + observe : function (subject, topic, data) { + do_check_eq(topic, "passwordmgr-storage-changed"); + do_check_eq(data, expectedNotification); + + switch (data) { + case "addLogin": + do_check_true(subject instanceof Ci.nsILoginInfo); + do_check_true(subject instanceof Ci.nsILoginMetaInfo); + do_check_true(expectedData.equals(subject)); // nsILoginInfo.equals() + break; + case "modifyLogin": + do_check_true(subject instanceof Ci.nsIArray); + do_check_eq(subject.length, 2); + var oldLogin = subject.queryElementAt(0, Ci.nsILoginInfo); + var newLogin = subject.queryElementAt(1, Ci.nsILoginInfo); + do_check_true(expectedData[0].equals(oldLogin)); // nsILoginInfo.equals() + do_check_true(expectedData[1].equals(newLogin)); + break; + case "removeLogin": + do_check_true(subject instanceof Ci.nsILoginInfo); + do_check_true(subject instanceof Ci.nsILoginMetaInfo); + do_check_true(expectedData.equals(subject)); // nsILoginInfo.equals() + break; + case "removeAllLogins": + do_check_eq(subject, null); + break; + case "hostSavingEnabled": + case "hostSavingDisabled": + do_check_true(subject instanceof Ci.nsISupportsString); + do_check_eq(subject.data, expectedData); + break; + default: + do_throw("Unhandled notification: " + data + " / " + topic); + } + + expectedNotification = null; // ensure a duplicate is flagged as unexpected. + expectedData = null; + } +}; + +add_task(function test_notifications() +{ + +try { + +var testnum = 0; +var testdesc = "Setup of nsLoginInfo test-users"; + +var testuser1 = new LoginInfo("http://testhost1", "", null, + "dummydude", "itsasecret", "put_user_here", "put_pw_here"); + +var testuser2 = new LoginInfo("http://testhost2", "", null, + "dummydude2", "itsasecret2", "put_user2_here", "put_pw2_here"); + +Services.obs.addObserver(TestObserver, "passwordmgr-storage-changed", false); + + +/* ========== 1 ========== */ +testnum = 1; +testdesc = "Initial connection to storage module"; + +/* ========== 2 ========== */ +testnum++; +testdesc = "addLogin"; + +expectedNotification = "addLogin"; +expectedData = testuser1; +Services.logins.addLogin(testuser1); +LoginTestUtils.checkLogins([testuser1]); +do_check_eq(expectedNotification, null); // check that observer got a notification + +/* ========== 3 ========== */ +testnum++; +testdesc = "modifyLogin"; + +expectedNotification = "modifyLogin"; +expectedData = [testuser1, testuser2]; +Services.logins.modifyLogin(testuser1, testuser2); +do_check_eq(expectedNotification, null); +LoginTestUtils.checkLogins([testuser2]); + +/* ========== 4 ========== */ +testnum++; +testdesc = "removeLogin"; + +expectedNotification = "removeLogin"; +expectedData = testuser2; +Services.logins.removeLogin(testuser2); +do_check_eq(expectedNotification, null); +LoginTestUtils.checkLogins([]); + +/* ========== 5 ========== */ +testnum++; +testdesc = "removeAllLogins"; + +expectedNotification = "removeAllLogins"; +expectedData = null; +Services.logins.removeAllLogins(); +do_check_eq(expectedNotification, null); +LoginTestUtils.checkLogins([]); + +/* ========== 6 ========== */ +testnum++; +testdesc = "removeAllLogins (again)"; + +expectedNotification = "removeAllLogins"; +expectedData = null; +Services.logins.removeAllLogins(); +do_check_eq(expectedNotification, null); +LoginTestUtils.checkLogins([]); + +/* ========== 7 ========== */ +testnum++; +testdesc = "setLoginSavingEnabled / false"; + +expectedNotification = "hostSavingDisabled"; +expectedData = "http://site.com"; +Services.logins.setLoginSavingEnabled("http://site.com", false); +do_check_eq(expectedNotification, null); +LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), + ["http://site.com"]); + +/* ========== 8 ========== */ +testnum++; +testdesc = "setLoginSavingEnabled / false (again)"; + +expectedNotification = "hostSavingDisabled"; +expectedData = "http://site.com"; +Services.logins.setLoginSavingEnabled("http://site.com", false); +do_check_eq(expectedNotification, null); +LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), + ["http://site.com"]); + +/* ========== 9 ========== */ +testnum++; +testdesc = "setLoginSavingEnabled / true"; + +expectedNotification = "hostSavingEnabled"; +expectedData = "http://site.com"; +Services.logins.setLoginSavingEnabled("http://site.com", true); +do_check_eq(expectedNotification, null); +LoginTestUtils.checkLogins([]); + +/* ========== 10 ========== */ +testnum++; +testdesc = "setLoginSavingEnabled / true (again)"; + +expectedNotification = "hostSavingEnabled"; +expectedData = "http://site.com"; +Services.logins.setLoginSavingEnabled("http://site.com", true); +do_check_eq(expectedNotification, null); +LoginTestUtils.checkLogins([]); + +Services.obs.removeObserver(TestObserver, "passwordmgr-storage-changed"); + +LoginTestUtils.clearData(); + +} catch (e) { + throw new Error("FAILED in test #" + testnum + " -- " + testdesc + ": " + e); +} + +}); diff --git a/toolkit/components/passwordmgr/test/unit/test_recipes_add.js b/toolkit/components/passwordmgr/test/unit/test_recipes_add.js new file mode 100644 index 000000000..ef5086c3b --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_recipes_add.js @@ -0,0 +1,177 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests adding and retrieving LoginRecipes in the parent process. + */ + +"use strict"; + +add_task(function* test_init() { + let parent = new LoginRecipesParent({ defaults: null }); + let initPromise1 = parent.initializationPromise; + let initPromise2 = parent.initializationPromise; + Assert.strictEqual(initPromise1, initPromise2, "Check that the same promise is returned"); + + let recipesParent = yield initPromise1; + Assert.ok(recipesParent instanceof LoginRecipesParent, "Check init return value"); + Assert.strictEqual(recipesParent._recipesByHost.size, 0, "Initially 0 recipes"); +}); + +add_task(function* test_get_missing_host() { + let recipesParent = yield RecipeHelpers.initNewParent(); + let exampleRecipes = recipesParent.getRecipesForHost("example.invalid"); + Assert.strictEqual(exampleRecipes.size, 0, "Check recipe count for example.invalid"); + +}); + +add_task(function* test_add_get_simple_host() { + let recipesParent = yield RecipeHelpers.initNewParent(); + Assert.strictEqual(recipesParent._recipesByHost.size, 0, "Initially 0 recipes"); + recipesParent.add({ + hosts: ["example.com"], + }); + Assert.strictEqual(recipesParent._recipesByHost.size, 1, + "Check number of hosts after the addition"); + + let exampleRecipes = recipesParent.getRecipesForHost("example.com"); + Assert.strictEqual(exampleRecipes.size, 1, "Check recipe count for example.com"); + let recipe = [...exampleRecipes][0]; + Assert.strictEqual(typeof(recipe), "object", "Check recipe type"); + Assert.strictEqual(recipe.hosts.length, 1, "Check that one host is present"); + Assert.strictEqual(recipe.hosts[0], "example.com", "Check the one host"); +}); + +add_task(function* test_add_get_non_standard_port_host() { + let recipesParent = yield RecipeHelpers.initNewParent(); + recipesParent.add({ + hosts: ["example.com:8080"], + }); + Assert.strictEqual(recipesParent._recipesByHost.size, 1, + "Check number of hosts after the addition"); + + let exampleRecipes = recipesParent.getRecipesForHost("example.com:8080"); + Assert.strictEqual(exampleRecipes.size, 1, "Check recipe count for example.com:8080"); + let recipe = [...exampleRecipes][0]; + Assert.strictEqual(typeof(recipe), "object", "Check recipe type"); + Assert.strictEqual(recipe.hosts.length, 1, "Check that one host is present"); + Assert.strictEqual(recipe.hosts[0], "example.com:8080", "Check the one host"); +}); + +add_task(function* test_add_multiple_hosts() { + let recipesParent = yield RecipeHelpers.initNewParent(); + recipesParent.add({ + hosts: ["example.com", "foo.invalid"], + }); + Assert.strictEqual(recipesParent._recipesByHost.size, 2, + "Check number of hosts after the addition"); + + let exampleRecipes = recipesParent.getRecipesForHost("example.com"); + Assert.strictEqual(exampleRecipes.size, 1, "Check recipe count for example.com"); + let recipe = [...exampleRecipes][0]; + Assert.strictEqual(typeof(recipe), "object", "Check recipe type"); + Assert.strictEqual(recipe.hosts.length, 2, "Check that two hosts are present"); + Assert.strictEqual(recipe.hosts[0], "example.com", "Check the first host"); + Assert.strictEqual(recipe.hosts[1], "foo.invalid", "Check the second host"); + + let fooRecipes = recipesParent.getRecipesForHost("foo.invalid"); + Assert.strictEqual(fooRecipes.size, 1, "Check recipe count for foo.invalid"); + let fooRecipe = [...fooRecipes][0]; + Assert.strictEqual(fooRecipe, recipe, "Check that the recipe is shared"); + Assert.strictEqual(typeof(fooRecipe), "object", "Check recipe type"); + Assert.strictEqual(fooRecipe.hosts.length, 2, "Check that two hosts are present"); + Assert.strictEqual(fooRecipe.hosts[0], "example.com", "Check the first host"); + Assert.strictEqual(fooRecipe.hosts[1], "foo.invalid", "Check the second host"); +}); + +add_task(function* test_add_pathRegex() { + let recipesParent = yield RecipeHelpers.initNewParent(); + recipesParent.add({ + hosts: ["example.com"], + pathRegex: /^\/mypath\//, + }); + Assert.strictEqual(recipesParent._recipesByHost.size, 1, + "Check number of hosts after the addition"); + + let exampleRecipes = recipesParent.getRecipesForHost("example.com"); + Assert.strictEqual(exampleRecipes.size, 1, "Check recipe count for example.com"); + let recipe = [...exampleRecipes][0]; + Assert.strictEqual(typeof(recipe), "object", "Check recipe type"); + Assert.strictEqual(recipe.hosts.length, 1, "Check that one host is present"); + Assert.strictEqual(recipe.hosts[0], "example.com", "Check the one host"); + Assert.strictEqual(recipe.pathRegex.toString(), "/^\\/mypath\\//", "Check the pathRegex"); +}); + +add_task(function* test_add_selectors() { + let recipesParent = yield RecipeHelpers.initNewParent(); + recipesParent.add({ + hosts: ["example.com"], + usernameSelector: "#my-username", + passwordSelector: "#my-form > input.password", + }); + Assert.strictEqual(recipesParent._recipesByHost.size, 1, + "Check number of hosts after the addition"); + + let exampleRecipes = recipesParent.getRecipesForHost("example.com"); + Assert.strictEqual(exampleRecipes.size, 1, "Check recipe count for example.com"); + let recipe = [...exampleRecipes][0]; + Assert.strictEqual(typeof(recipe), "object", "Check recipe type"); + Assert.strictEqual(recipe.hosts.length, 1, "Check that one host is present"); + Assert.strictEqual(recipe.hosts[0], "example.com", "Check the one host"); + Assert.strictEqual(recipe.usernameSelector, "#my-username", "Check the usernameSelector"); + Assert.strictEqual(recipe.passwordSelector, "#my-form > input.password", "Check the passwordSelector"); +}); + +/* Begin checking errors with add */ + +add_task(function* test_add_missing_prop() { + let recipesParent = yield RecipeHelpers.initNewParent(); + Assert.throws(() => recipesParent.add({}), /required/, "Some properties are required"); +}); + +add_task(function* test_add_unknown_prop() { + let recipesParent = yield RecipeHelpers.initNewParent(); + Assert.throws(() => recipesParent.add({ + unknownProp: true, + }), /supported/, "Unknown properties should cause an error to help with typos"); +}); + +add_task(function* test_add_invalid_hosts() { + let recipesParent = yield RecipeHelpers.initNewParent(); + Assert.throws(() => recipesParent.add({ + hosts: 404, + }), /array/, "hosts should be an array"); +}); + +add_task(function* test_add_empty_host_array() { + let recipesParent = yield RecipeHelpers.initNewParent(); + Assert.throws(() => recipesParent.add({ + hosts: [], + }), /array/, "hosts should be a non-empty array"); +}); + +add_task(function* test_add_pathRegex_non_regexp() { + let recipesParent = yield RecipeHelpers.initNewParent(); + Assert.throws(() => recipesParent.add({ + hosts: ["example.com"], + pathRegex: "foo", + }), /regular expression/, "pathRegex should be a RegExp"); +}); + +add_task(function* test_add_usernameSelector_non_string() { + let recipesParent = yield RecipeHelpers.initNewParent(); + Assert.throws(() => recipesParent.add({ + hosts: ["example.com"], + usernameSelector: 404, + }), /string/, "usernameSelector should be a string"); +}); + +add_task(function* test_add_passwordSelector_non_string() { + let recipesParent = yield RecipeHelpers.initNewParent(); + Assert.throws(() => recipesParent.add({ + hosts: ["example.com"], + passwordSelector: 404, + }), /string/, "passwordSelector should be a string"); +}); + +/* End checking errors with add */ diff --git a/toolkit/components/passwordmgr/test/unit/test_recipes_content.js b/toolkit/components/passwordmgr/test/unit/test_recipes_content.js new file mode 100644 index 000000000..3d3751452 --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_recipes_content.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test filtering recipes in LoginRecipesContent. + */ + +"use strict"; + +Cu.importGlobalProperties(["URL"]); + +add_task(function* test_getFieldOverrides() { + let recipes = new Set([ + { // path doesn't match but otherwise good + hosts: ["example.com:8080"], + passwordSelector: "#password", + pathRegex: /^\/$/, + usernameSelector: ".username", + }, + { // match with no field overrides + hosts: ["example.com:8080"], + }, + { // best match (field selectors + path match) + description: "best match", + hosts: ["a.invalid", "example.com:8080", "other.invalid"], + passwordSelector: "#password", + pathRegex: /^\/first\/second\/$/, + usernameSelector: ".username", + }, + ]); + + let form = MockDocument.createTestDocument("http://localhost:8080/first/second/", ""). + forms[0]; + let override = LoginRecipesContent.getFieldOverrides(recipes, form); + Assert.strictEqual(override.description, "best match", + "Check the best field override recipe was returned"); + Assert.strictEqual(override.usernameSelector, ".username", "Check usernameSelector"); + Assert.strictEqual(override.passwordSelector, "#password", "Check passwordSelector"); +}); diff --git a/toolkit/components/passwordmgr/test/unit/test_removeLegacySignonFiles.js b/toolkit/components/passwordmgr/test/unit/test_removeLegacySignonFiles.js new file mode 100644 index 000000000..51a107170 --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_removeLegacySignonFiles.js @@ -0,0 +1,69 @@ +/** + * Tests the LoginHelper object. + */ + +"use strict"; + + +Cu.import("resource://gre/modules/Task.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper", + "resource://gre/modules/LoginHelper.jsm"); + + +function* createSignonFile(singon) { + let {file, pref} = singon; + + if (pref) { + Services.prefs.setCharPref(pref, file); + } + + yield OS.File.writeAtomic( + OS.Path.join(OS.Constants.Path.profileDir, file), new Uint8Array(1)); +} + +function* isSignonClear(singon) { + const {file, pref} = singon; + const fileExists = yield OS.File.exists( + OS.Path.join(OS.Constants.Path.profileDir, file)); + + if (pref) { + try { + Services.prefs.getCharPref(pref); + return false; + } catch (e) {} + } + + return !fileExists; +} + +add_task(function* test_remove_lagecy_signonfile() { + // In the last test case, signons3.txt being deleted even when + // it doesn't exist. + const signonsSettings = [[ + { file: "signons.txt" }, + { file: "signons2.txt" }, + { file: "signons3.txt" } + ], [ + { file: "signons.txt", pref: "signon.SignonFileName" }, + { file: "signons2.txt", pref: "signon.SignonFileName2" }, + { file: "signons3.txt", pref: "signon.SignonFileName3" } + ], [ + { file: "signons2.txt" }, + { file: "singons.txt", pref: "signon.SignonFileName" }, + { file: "customized2.txt", pref: "signon.SignonFileName2" }, + { file: "customized3.txt", pref: "signon.SignonFileName3" } + ]]; + + for (let setting of signonsSettings) { + for (let singon of setting) { + yield createSignonFile(singon); + } + + LoginHelper.removeLegacySignonFiles(); + + for (let singon of setting) { + equal(yield isSignonClear(singon), true); + } + } +}); diff --git a/toolkit/components/passwordmgr/test/unit/test_search_schemeUpgrades.js b/toolkit/components/passwordmgr/test/unit/test_search_schemeUpgrades.js new file mode 100644 index 000000000..3406becff --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_search_schemeUpgrades.js @@ -0,0 +1,184 @@ +/* + * Test Services.logins.searchLogins with the `schemeUpgrades` property. + */ + +const HTTP3_ORIGIN = "http://www3.example.com"; +const HTTPS_ORIGIN = "https://www.example.com"; +const HTTP_ORIGIN = "http://www.example.com"; + +/** + * Returns a list of new nsILoginInfo objects that are a subset of the test + * data, built to match the specified query. + * + * @param {Object} aQuery + * Each property and value of this object restricts the search to those + * entries from the test data that match the property exactly. + */ +function buildExpectedLogins(aQuery) { + return TestData.loginList().filter( + entry => Object.keys(aQuery).every(name => { + if (name == "schemeUpgrades") { + return true; + } + if (["hostname", "formSubmitURL"].includes(name)) { + return LoginHelper.isOriginMatching(entry[name], aQuery[name], { + schemeUpgrades: aQuery.schemeUpgrades, + }); + } + return entry[name] === aQuery[name]; + })); +} + +/** + * Tests the searchLogins function. + * + * @param {Object} aQuery + * Each property and value of this object is translated to an entry in + * the nsIPropertyBag parameter of searchLogins. + * @param {Number} aExpectedCount + * Number of logins from the test data that should be found. The actual + * list of logins is obtained using the buildExpectedLogins helper, and + * this value is just used to verify that modifications to the test data + * don't make the current test meaningless. + */ +function checkSearch(aQuery, aExpectedCount) { + do_print("Testing searchLogins for " + JSON.stringify(aQuery)); + + let expectedLogins = buildExpectedLogins(aQuery); + do_check_eq(expectedLogins.length, aExpectedCount); + + let outCount = {}; + let logins = Services.logins.searchLogins(outCount, newPropertyBag(aQuery)); + do_check_eq(outCount.value, expectedLogins.length); + LoginTestUtils.assertLoginListsEqual(logins, expectedLogins); +} + +/** + * Prepare data for the following tests. + */ +add_task(function test_initialize() { + for (let login of TestData.loginList()) { + Services.logins.addLogin(login); + } +}); + +/** + * Tests searchLogins with the `schemeUpgrades` property + */ +add_task(function test_search_schemeUpgrades_hostname() { + // Hostname-only + checkSearch({ + hostname: HTTPS_ORIGIN, + }, 1); + checkSearch({ + hostname: HTTPS_ORIGIN, + schemeUpgrades: false, + }, 1); + checkSearch({ + hostname: HTTPS_ORIGIN, + schemeUpgrades: undefined, + }, 1); + checkSearch({ + hostname: HTTPS_ORIGIN, + schemeUpgrades: true, + }, 2); +}); + +/** + * Same as above but replacing hostname with formSubmitURL. + */ +add_task(function test_search_schemeUpgrades_formSubmitURL() { + checkSearch({ + formSubmitURL: HTTPS_ORIGIN, + }, 2); + checkSearch({ + formSubmitURL: HTTPS_ORIGIN, + schemeUpgrades: false, + }, 2); + checkSearch({ + formSubmitURL: HTTPS_ORIGIN, + schemeUpgrades: undefined, + }, 2); + checkSearch({ + formSubmitURL: HTTPS_ORIGIN, + schemeUpgrades: true, + }, 4); +}); + + +add_task(function test_search_schemeUpgrades_hostname_formSubmitURL() { + checkSearch({ + formSubmitURL: HTTPS_ORIGIN, + hostname: HTTPS_ORIGIN, + }, 1); + checkSearch({ + formSubmitURL: HTTPS_ORIGIN, + hostname: HTTPS_ORIGIN, + schemeUpgrades: false, + }, 1); + checkSearch({ + formSubmitURL: HTTPS_ORIGIN, + hostname: HTTPS_ORIGIN, + schemeUpgrades: undefined, + }, 1); + checkSearch({ + formSubmitURL: HTTPS_ORIGIN, + hostname: HTTPS_ORIGIN, + schemeUpgrades: true, + }, 2); + checkSearch({ + formSubmitURL: HTTPS_ORIGIN, + hostname: HTTPS_ORIGIN, + schemeUpgrades: true, + usernameField: "form_field_username", + }, 2); + checkSearch({ + formSubmitURL: HTTPS_ORIGIN, + hostname: HTTPS_ORIGIN, + passwordField: "form_field_password", + schemeUpgrades: true, + usernameField: "form_field_username", + }, 2); + checkSearch({ + formSubmitURL: HTTPS_ORIGIN, + hostname: HTTPS_ORIGIN, + httpRealm: null, + passwordField: "form_field_password", + schemeUpgrades: true, + usernameField: "form_field_username", + }, 2); +}); + +/** + * HTTP submitting to HTTPS + */ +add_task(function test_http_to_https() { + checkSearch({ + formSubmitURL: HTTPS_ORIGIN, + hostname: HTTP3_ORIGIN, + httpRealm: null, + schemeUpgrades: false, + }, 1); + checkSearch({ + formSubmitURL: HTTPS_ORIGIN, + hostname: HTTP3_ORIGIN, + httpRealm: null, + schemeUpgrades: true, + }, 2); +}); + +/** + * schemeUpgrades shouldn't cause downgrades + */ +add_task(function test_search_schemeUpgrades_downgrade() { + checkSearch({ + formSubmitURL: HTTP_ORIGIN, + hostname: HTTP_ORIGIN, + }, 1); + do_print("The same number should be found with schemeUpgrades since we're searching for HTTP"); + checkSearch({ + formSubmitURL: HTTP_ORIGIN, + hostname: HTTP_ORIGIN, + schemeUpgrades: true, + }, 1); +}); diff --git a/toolkit/components/passwordmgr/test/unit/test_storage.js b/toolkit/components/passwordmgr/test/unit/test_storage.js new file mode 100644 index 000000000..d65516d9b --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_storage.js @@ -0,0 +1,102 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the default nsILoginManagerStorage module attached to the Login + * Manager service is able to save and reload nsILoginInfo properties correctly, + * even when they include special characters. + */ + +"use strict"; + +// Globals + +function* reloadAndCheckLoginsGen(aExpectedLogins) +{ + yield LoginTestUtils.reloadData(); + LoginTestUtils.checkLogins(aExpectedLogins); + LoginTestUtils.clearData(); +} + +// Tests + +/** + * Tests addLogin with valid non-ASCII characters. + */ +add_task(function* test_storage_addLogin_nonascii() +{ + let hostname = "http://" + String.fromCharCode(355) + ".example.com"; + + // Store the strings "user" and "pass" using similarly looking glyphs. + let loginInfo = TestData.formLogin({ + hostname: hostname, + formSubmitURL: hostname, + username: String.fromCharCode(533, 537, 7570, 345), + password: String.fromCharCode(421, 259, 349, 537), + usernameField: "field_" + String.fromCharCode(533, 537, 7570, 345), + passwordField: "field_" + String.fromCharCode(421, 259, 349, 537), + }); + Services.logins.addLogin(loginInfo); + yield* reloadAndCheckLoginsGen([loginInfo]); + + // Store the string "test" using similarly looking glyphs. + loginInfo = TestData.authLogin({ + httpRealm: String.fromCharCode(355, 277, 349, 357), + }); + Services.logins.addLogin(loginInfo); + yield* reloadAndCheckLoginsGen([loginInfo]); +}); + +/** + * Tests addLogin with newline characters in the username and password. + */ +add_task(function* test_storage_addLogin_newlines() +{ + let loginInfo = TestData.formLogin({ + username: "user\r\nname", + password: "password\r\n", + }); + Services.logins.addLogin(loginInfo); + yield* reloadAndCheckLoginsGen([loginInfo]); +}); + +/** + * Tests addLogin with a single dot in fields where it is allowed. + * + * These tests exist to verify the legacy "signons.txt" storage format. + */ +add_task(function* test_storage_addLogin_dot() +{ + let loginInfo = TestData.formLogin({ hostname: ".", passwordField: "." }); + Services.logins.addLogin(loginInfo); + yield* reloadAndCheckLoginsGen([loginInfo]); + + loginInfo = TestData.authLogin({ httpRealm: "." }); + Services.logins.addLogin(loginInfo); + yield* reloadAndCheckLoginsGen([loginInfo]); +}); + +/** + * Tests addLogin with parentheses in hostnames. + * + * These tests exist to verify the legacy "signons.txt" storage format. + */ +add_task(function* test_storage_addLogin_parentheses() +{ + let loginList = [ + TestData.authLogin({ httpRealm: "(realm" }), + TestData.authLogin({ httpRealm: "realm)" }), + TestData.authLogin({ httpRealm: "(realm)" }), + TestData.authLogin({ httpRealm: ")realm(" }), + TestData.authLogin({ hostname: "http://parens(.example.com" }), + TestData.authLogin({ hostname: "http://parens).example.com" }), + TestData.authLogin({ hostname: "http://parens(example).example.com" }), + TestData.authLogin({ hostname: "http://parens)example(.example.com" }), + ]; + for (let loginInfo of loginList) { + Services.logins.addLogin(loginInfo); + } + yield* reloadAndCheckLoginsGen(loginList); +}); diff --git a/toolkit/components/passwordmgr/test/unit/test_storage_mozStorage.js b/toolkit/components/passwordmgr/test/unit/test_storage_mozStorage.js new file mode 100644 index 000000000..8eab6efe5 --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_storage_mozStorage.js @@ -0,0 +1,507 @@ +/* + * This test interfaces directly with the mozStorage password storage module, + * bypassing the normal password manager usage. + */ + + +const ENCTYPE_BASE64 = 0; +const ENCTYPE_SDR = 1; +const PERMISSION_SAVE_LOGINS = "login-saving"; + +// Current schema version used by storage-mozStorage.js. This will need to be +// kept in sync with the version there (or else the tests fail). +const CURRENT_SCHEMA = 6; + +function* copyFile(aLeafName) +{ + yield OS.File.copy(OS.Path.join(do_get_file("data").path, aLeafName), + OS.Path.join(OS.Constants.Path.profileDir, aLeafName)); +} + +function openDB(aLeafName) +{ + var dbFile = new FileUtils.File(OS.Constants.Path.profileDir); + dbFile.append(aLeafName); + + return Services.storage.openDatabase(dbFile); +} + +function deleteFile(pathname, filename) +{ + var file = new FileUtils.File(pathname); + file.append(filename); + + // Suppress failures, this happens in the mozstorage tests on Windows + // because the module may still be holding onto the DB. (We don't + // have a way to explicitly shutdown/GC the module). + try { + if (file.exists()) + file.remove(false); + } catch (e) {} +} + +function reloadStorage(aInputPathName, aInputFileName) +{ + var inputFile = null; + if (aInputFileName) { + inputFile = Cc["@mozilla.org/file/local;1"]. + createInstance(Ci.nsILocalFile); + inputFile.initWithPath(aInputPathName); + inputFile.append(aInputFileName); + } + + let storage = Cc["@mozilla.org/login-manager/storage/mozStorage;1"] + .createInstance(Ci.nsILoginManagerStorage); + storage.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIVariant) + .initWithFile(inputFile); + + return storage; +} + +function checkStorageData(storage, ref_disabledHosts, ref_logins) +{ + LoginTestUtils.assertLoginListsEqual(storage.getAllLogins(), ref_logins); + LoginTestUtils.assertDisabledHostsEqual(getAllDisabledHostsFromPermissionManager(), + ref_disabledHosts); +} + +function getAllDisabledHostsFromPermissionManager() { + let disabledHosts = []; + let enumerator = Services.perms.enumerator; + + while (enumerator.hasMoreElements()) { + let perm = enumerator.getNext(); + if (perm.type == PERMISSION_SAVE_LOGINS && perm.capability == Services.perms.DENY_ACTION) { + disabledHosts.push(perm.principal.URI.prePath); + } + } + + return disabledHosts; +} + +function setLoginSavingEnabled(origin, enabled) { + let uri = Services.io.newURI(origin, null, null); + + if (enabled) { + Services.perms.remove(uri, PERMISSION_SAVE_LOGINS); + } else { + Services.perms.add(uri, PERMISSION_SAVE_LOGINS, Services.perms.DENY_ACTION); + } +} + +add_task(function* test_execute() +{ + +const OUTDIR = OS.Constants.Path.profileDir; + +try { + +var isGUID = /^\{[0-9a-f\d]{8}-[0-9a-f\d]{4}-[0-9a-f\d]{4}-[0-9a-f\d]{4}-[0-9a-f\d]{12}\}$/; +function getGUIDforID(conn, id) { + var stmt = conn.createStatement("SELECT guid from moz_logins WHERE id = " + id); + stmt.executeStep(); + var guid = stmt.getString(0); + stmt.finalize(); + return guid; +} + +function getEncTypeForID(conn, id) { + var stmt = conn.createStatement("SELECT encType from moz_logins WHERE id = " + id); + stmt.executeStep(); + var encType = stmt.row.encType; + stmt.finalize(); + return encType; +} + +function getAllDisabledHostsFromMozStorage(conn) { + let disabledHosts = []; + let stmt = conn.createStatement("SELECT hostname from moz_disabledHosts"); + + while (stmt.executeStep()) { + disabledHosts.push(stmt.row.hostname); + } + + return disabledHosts; +} + +var storage; +var dbConnection; +var testnum = 0; +var testdesc = "Setup of nsLoginInfo test-users"; +var nsLoginInfo = new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", + Components.interfaces.nsILoginInfo); +do_check_true(nsLoginInfo != null); + +var testuser1 = new nsLoginInfo; +testuser1.init("http://test.com", "http://test.com", null, + "testuser1", "testpass1", "u1", "p1"); +var testuser1B = new nsLoginInfo; +testuser1B.init("http://test.com", "http://test.com", null, + "testuser1B", "testpass1B", "u1", "p1"); +var testuser2 = new nsLoginInfo; +testuser2.init("http://test.org", "http://test.org", null, + "testuser2", "testpass2", "u2", "p2"); +var testuser3 = new nsLoginInfo; +testuser3.init("http://test.gov", "http://test.gov", null, + "testuser3", "testpass3", "u3", "p3"); +var testuser4 = new nsLoginInfo; +testuser4.init("http://test.gov", "http://test.gov", null, + "testuser1", "testpass2", "u4", "p4"); +var testuser5 = new nsLoginInfo; +testuser5.init("http://test.gov", "http://test.gov", null, + "testuser2", "testpass1", "u5", "p5"); + + +/* ========== 1 ========== */ +testnum++; +testdesc = "Test downgrade from v999 storage"; + +yield* copyFile("signons-v999.sqlite"); +// Verify the schema version in the test file. +dbConnection = openDB("signons-v999.sqlite"); +do_check_eq(999, dbConnection.schemaVersion); +dbConnection.close(); + +storage = reloadStorage(OUTDIR, "signons-v999.sqlite"); +setLoginSavingEnabled("https://disabled.net", false); +checkStorageData(storage, ["https://disabled.net"], [testuser1]); + +// Check to make sure we downgraded the schema version. +dbConnection = openDB("signons-v999.sqlite"); +do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion); +dbConnection.close(); + +deleteFile(OUTDIR, "signons-v999.sqlite"); + +/* ========== 2 ========== */ +testnum++; +testdesc = "Test downgrade from incompat v999 storage"; +// This file has a testuser999/testpass999, but is missing an expected column + +var origFile = OS.Path.join(OUTDIR, "signons-v999-2.sqlite"); +var failFile = OS.Path.join(OUTDIR, "signons-v999-2.sqlite.corrupt"); + +// Make sure we always start clean in a clean state. +yield* copyFile("signons-v999-2.sqlite"); +yield OS.File.remove(failFile); + +Assert.throws(() => reloadStorage(OUTDIR, "signons-v999-2.sqlite"), + /Initialization failed/); + +// Check to ensure the DB file was renamed to .corrupt. +do_check_false(yield OS.File.exists(origFile)); +do_check_true(yield OS.File.exists(failFile)); + +yield OS.File.remove(failFile); + +/* ========== 3 ========== */ +testnum++; +testdesc = "Test upgrade from v1->v2 storage"; + +yield* copyFile("signons-v1.sqlite"); +// Sanity check the test file. +dbConnection = openDB("signons-v1.sqlite"); +do_check_eq(1, dbConnection.schemaVersion); +dbConnection.close(); + +storage = reloadStorage(OUTDIR, "signons-v1.sqlite"); +checkStorageData(storage, ["https://disabled.net"], [testuser1, testuser2]); + +// Check to see that we added a GUIDs to the logins. +dbConnection = openDB("signons-v1.sqlite"); +do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion); +var guid = getGUIDforID(dbConnection, 1); +do_check_true(isGUID.test(guid)); +guid = getGUIDforID(dbConnection, 2); +do_check_true(isGUID.test(guid)); +dbConnection.close(); + +deleteFile(OUTDIR, "signons-v1.sqlite"); + +/* ========== 4 ========== */ +testnum++; +testdesc = "Test upgrade v2->v1 storage"; +// This is the case where a v2 DB has been accessed with v1 code, and now we +// are upgrading it again. Any logins added by the v1 code must be properly +// upgraded. + +yield* copyFile("signons-v1v2.sqlite"); +// Sanity check the test file. +dbConnection = openDB("signons-v1v2.sqlite"); +do_check_eq(1, dbConnection.schemaVersion); +dbConnection.close(); + +storage = reloadStorage(OUTDIR, "signons-v1v2.sqlite"); +checkStorageData(storage, ["https://disabled.net"], [testuser1, testuser2, testuser3]); + +// While we're here, try modifying a login, to ensure that doing so doesn't +// change the existing GUID. +storage.modifyLogin(testuser1, testuser1B); +checkStorageData(storage, ["https://disabled.net"], [testuser1B, testuser2, testuser3]); + +// Check the GUIDs. Logins 1 and 2 should retain their original GUID, login 3 +// should have one created (because it didn't have one previously). +dbConnection = openDB("signons-v1v2.sqlite"); +do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion); +guid = getGUIDforID(dbConnection, 1); +do_check_eq("{655c7358-f1d6-6446-adab-53f98ac5d80f}", guid); +guid = getGUIDforID(dbConnection, 2); +do_check_eq("{13d9bfdc-572a-4d4e-9436-68e9803e84c1}", guid); +guid = getGUIDforID(dbConnection, 3); +do_check_true(isGUID.test(guid)); +dbConnection.close(); + +deleteFile(OUTDIR, "signons-v1v2.sqlite"); + +/* ========== 5 ========== */ +testnum++; +testdesc = "Test upgrade from v2->v3 storage"; + +yield* copyFile("signons-v2.sqlite"); +// Sanity check the test file. +dbConnection = openDB("signons-v2.sqlite"); +do_check_eq(2, dbConnection.schemaVersion); + +storage = reloadStorage(OUTDIR, "signons-v2.sqlite"); + +// Check to see that we added the correct encType to the logins. +do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion); +var encTypes = [ENCTYPE_BASE64, ENCTYPE_SDR, ENCTYPE_BASE64, ENCTYPE_BASE64]; +for (let i = 0; i < encTypes.length; i++) + do_check_eq(encTypes[i], getEncTypeForID(dbConnection, i + 1)); +dbConnection.close(); + +// There are 4 logins, but 3 will be invalid because we can no longer decrypt +// base64-encoded items. (testuser1/4/5) +checkStorageData(storage, ["https://disabled.net"], + [testuser2]); + +deleteFile(OUTDIR, "signons-v2.sqlite"); + +/* ========== 6 ========== */ +testnum++; +testdesc = "Test upgrade v3->v2 storage"; +// This is the case where a v3 DB has been accessed with v2 code, and now we +// are upgrading it again. Any logins added by the v2 code must be properly +// upgraded. + +yield* copyFile("signons-v2v3.sqlite"); +// Sanity check the test file. +dbConnection = openDB("signons-v2v3.sqlite"); +do_check_eq(2, dbConnection.schemaVersion); +encTypes = [ENCTYPE_BASE64, ENCTYPE_SDR, ENCTYPE_BASE64, ENCTYPE_BASE64, null]; +for (let i = 0; i < encTypes.length; i++) + do_check_eq(encTypes[i], getEncTypeForID(dbConnection, i + 1)); + +// Reload storage, check that the new login now has encType=1, others untouched +storage = reloadStorage(OUTDIR, "signons-v2v3.sqlite"); +do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion); + +encTypes = [ENCTYPE_BASE64, ENCTYPE_SDR, ENCTYPE_BASE64, ENCTYPE_BASE64, ENCTYPE_SDR]; +for (let i = 0; i < encTypes.length; i++) + do_check_eq(encTypes[i], getEncTypeForID(dbConnection, i + 1)); + +// Sanity check that the data gets migrated +// There are 5 logins, but 3 will be invalid because we can no longer decrypt +// base64-encoded items. (testuser1/4/5). We no longer reencrypt with SDR. +checkStorageData(storage, ["https://disabled.net"], [testuser2, testuser3]); +encTypes = [ENCTYPE_BASE64, ENCTYPE_SDR, ENCTYPE_BASE64, ENCTYPE_BASE64, ENCTYPE_SDR]; +for (let i = 0; i < encTypes.length; i++) + do_check_eq(encTypes[i], getEncTypeForID(dbConnection, i + 1)); +dbConnection.close(); + +deleteFile(OUTDIR, "signons-v2v3.sqlite"); + + +/* ========== 7 ========== */ +testnum++; +testdesc = "Test upgrade from v3->v4 storage"; + +yield* copyFile("signons-v3.sqlite"); +// Sanity check the test file. +dbConnection = openDB("signons-v3.sqlite"); +do_check_eq(3, dbConnection.schemaVersion); + +storage = reloadStorage(OUTDIR, "signons-v3.sqlite"); +do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion); + +// Remove old entry from permission manager. +setLoginSavingEnabled("https://disabled.net", true); + +// Check that timestamps and counts were initialized correctly +checkStorageData(storage, [], [testuser1, testuser2]); + +var logins = storage.getAllLogins(); +for (var i = 0; i < 2; i++) { + do_check_true(logins[i] instanceof Ci.nsILoginMetaInfo); + do_check_eq(1, logins[i].timesUsed); + LoginTestUtils.assertTimeIsAboutNow(logins[i].timeCreated); + LoginTestUtils.assertTimeIsAboutNow(logins[i].timeLastUsed); + LoginTestUtils.assertTimeIsAboutNow(logins[i].timePasswordChanged); +} + +/* ========== 8 ========== */ +testnum++; +testdesc = "Test upgrade from v3->v4->v3 storage"; + +yield* copyFile("signons-v3v4.sqlite"); +// Sanity check the test file. +dbConnection = openDB("signons-v3v4.sqlite"); +do_check_eq(3, dbConnection.schemaVersion); + +storage = reloadStorage(OUTDIR, "signons-v3v4.sqlite"); +do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion); + +// testuser1 already has timestamps, testuser2 does not. +checkStorageData(storage, [], [testuser1, testuser2]); + +logins = storage.getAllLogins(); + +var t1, t2; +if (logins[0].username == "testuser1") { + t1 = logins[0]; + t2 = logins[1]; +} else { + t1 = logins[1]; + t2 = logins[0]; +} + +do_check_true(t1 instanceof Ci.nsILoginMetaInfo); +do_check_true(t2 instanceof Ci.nsILoginMetaInfo); + +do_check_eq(9, t1.timesUsed); +do_check_eq(1262049951275, t1.timeCreated); +do_check_eq(1262049951275, t1.timeLastUsed); +do_check_eq(1262049951275, t1.timePasswordChanged); + +do_check_eq(1, t2.timesUsed); +LoginTestUtils.assertTimeIsAboutNow(t2.timeCreated); +LoginTestUtils.assertTimeIsAboutNow(t2.timeLastUsed); +LoginTestUtils.assertTimeIsAboutNow(t2.timePasswordChanged); + + +/* ========== 9 ========== */ +testnum++; +testdesc = "Test upgrade from v4 storage"; + +yield* copyFile("signons-v4.sqlite"); +// Sanity check the test file. +dbConnection = openDB("signons-v4.sqlite"); +do_check_eq(4, dbConnection.schemaVersion); +do_check_false(dbConnection.tableExists("moz_deleted_logins")); + +storage = reloadStorage(OUTDIR, "signons-v4.sqlite"); +do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion); +do_check_true(dbConnection.tableExists("moz_deleted_logins")); + + +/* ========== 10 ========== */ +testnum++; +testdesc = "Test upgrade from v4->v5->v4 storage"; + +yield copyFile("signons-v4v5.sqlite"); +// Sanity check the test file. +dbConnection = openDB("signons-v4v5.sqlite"); +do_check_eq(4, dbConnection.schemaVersion); +do_check_true(dbConnection.tableExists("moz_deleted_logins")); + +storage = reloadStorage(OUTDIR, "signons-v4v5.sqlite"); +do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion); +do_check_true(dbConnection.tableExists("moz_deleted_logins")); + +/* ========== 11 ========== */ +testnum++; +testdesc = "Test upgrade from v5->v6 storage"; + +yield* copyFile("signons-v5v6.sqlite"); + +// Sanity check the test file. +dbConnection = openDB("signons-v5v6.sqlite"); +do_check_eq(5, dbConnection.schemaVersion); +do_check_true(dbConnection.tableExists("moz_disabledHosts")); + +// Initial disabled hosts inside signons-v5v6.sqlite +var disabledHosts = [ + "http://disabled1.example.com", + "http://大.net", + "http://xn--19g.com" +]; + +LoginTestUtils.assertDisabledHostsEqual(disabledHosts, getAllDisabledHostsFromMozStorage(dbConnection)); + +// Reload storage +storage = reloadStorage(OUTDIR, "signons-v5v6.sqlite"); +do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion); + +// moz_disabledHosts should now be empty after migration. +LoginTestUtils.assertDisabledHostsEqual([], getAllDisabledHostsFromMozStorage(dbConnection)); + +// Get all the other hosts currently saved in the permission manager. +let hostsInPermissionManager = getAllDisabledHostsFromPermissionManager(); + +// All disabledHosts should have migrated to the permission manager +LoginTestUtils.assertDisabledHostsEqual(disabledHosts, hostsInPermissionManager); + +// Remove all disabled hosts from the permission manager before test ends +for (let host of disabledHosts) { + setLoginSavingEnabled(host, true); +} + +/* ========== 12 ========== */ +testnum++; +testdesc = "Create nsILoginInfo instances for testing with"; + +testuser1 = new nsLoginInfo; +testuser1.init("http://dummyhost.mozilla.org", "", null, + "dummydude", "itsasecret", "put_user_here", "put_pw_here"); + + +/* + * ---------------------- DB Corruption ---------------------- + * Try to initialize with a corrupt database file. This should create a backup + * file, then upon next use create a new database file. + */ + +/* ========== 13 ========== */ +testnum++; +testdesc = "Corrupt database and backup"; + +const filename = "signons-c.sqlite"; +const filepath = OS.Path.join(OS.Constants.Path.profileDir, filename); + +yield OS.File.copy(do_get_file("data/corruptDB.sqlite").path, filepath); + +// will init mozStorage module with corrupt database, init should fail +Assert.throws( + () => reloadStorage(OS.Constants.Path.profileDir, filename), + /Initialization failed/); + +// check that the backup file exists +do_check_true(yield OS.File.exists(filepath + ".corrupt")); + +// check that the original corrupt file has been deleted +do_check_false(yield OS.File.exists(filepath)); + +// initialize the storage module again +storage = reloadStorage(OS.Constants.Path.profileDir, filename); + +// use the storage module again, should work now +storage.addLogin(testuser1); +checkStorageData(storage, [], [testuser1]); + +// check the file exists +var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); +file.initWithPath(OS.Constants.Path.profileDir); +file.append(filename); +do_check_true(file.exists()); + +deleteFile(OS.Constants.Path.profileDir, filename + ".corrupt"); +deleteFile(OS.Constants.Path.profileDir, filename); + +} catch (e) { + throw new Error("FAILED in test #" + testnum + " -- " + testdesc + ": " + e); +} + +}); diff --git a/toolkit/components/passwordmgr/test/unit/test_telemetry.js b/toolkit/components/passwordmgr/test/unit/test_telemetry.js new file mode 100644 index 000000000..1d8f80226 --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_telemetry.js @@ -0,0 +1,187 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the statistics and other counters reported through telemetry. + */ + +"use strict"; + +// Globals + +const MS_PER_DAY = 24 * 60 * 60 * 1000; + +// To prevent intermittent failures when the test is executed at a time that is +// very close to a day boundary, we make it deterministic by using a static +// reference date for all the time-based statistics. +const gReferenceTimeMs = new Date("2000-01-01T00:00:00").getTime(); + +// Returns a milliseconds value to use with nsILoginMetaInfo properties, falling +// approximately in the middle of the specified number of days before the +// reference time, where zero days indicates a time within the past 24 hours. +var daysBeforeMs = days => gReferenceTimeMs - (days + 0.5) * MS_PER_DAY; + +/** + * Contains metadata that will be attached to test logins in order to verify + * that the statistics collection is working properly. Most properties of the + * logins are initialized to the default test values already. + * + * If you update this data or any of the telemetry histograms it checks, you'll + * probably need to update the expected statistics in the test below. + */ +const StatisticsTestData = [ + { + timeLastUsed: daysBeforeMs(0), + }, + { + timeLastUsed: daysBeforeMs(1), + }, + { + timeLastUsed: daysBeforeMs(7), + formSubmitURL: null, + httpRealm: "The HTTP Realm", + }, + { + username: "", + timeLastUsed: daysBeforeMs(7), + }, + { + username: "", + timeLastUsed: daysBeforeMs(30), + }, + { + username: "", + timeLastUsed: daysBeforeMs(31), + }, + { + timeLastUsed: daysBeforeMs(365), + }, + { + username: "", + timeLastUsed: daysBeforeMs(366), + }, + { + // If the login was saved in the future, it is ignored for statistiscs. + timeLastUsed: daysBeforeMs(-1), + }, + { + timeLastUsed: daysBeforeMs(1000), + }, +]; + +/** + * Triggers the collection of those statistics that are not accumulated each + * time an action is taken, but are a static snapshot of the current state. + */ +function triggerStatisticsCollection() { + Services.obs.notifyObservers(null, "gather-telemetry", "" + gReferenceTimeMs); +} + +/** + * Tests the telemetry histogram with the given ID contains only the specified + * non-zero ranges, expressed in the format { range1: value1, range2: value2 }. + */ +function testHistogram(histogramId, expectedNonZeroRanges) { + let snapshot = Services.telemetry.getHistogramById(histogramId).snapshot(); + + // Compute the actual ranges in the format { range1: value1, range2: value2 }. + let actualNonZeroRanges = {}; + for (let [index, range] of snapshot.ranges.entries()) { + let value = snapshot.counts[index]; + if (value > 0) { + actualNonZeroRanges[range] = value; + } + } + + // These are stringified to visualize the differences between the values. + do_print("Testing histogram: " + histogramId); + do_check_eq(JSON.stringify(actualNonZeroRanges), + JSON.stringify(expectedNonZeroRanges)); +} + +// Tests + +/** + * Enable local telemetry recording for the duration of the tests, and prepare + * the test data that will be used by the following tests. + */ +add_task(function test_initialize() { + let oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + do_register_cleanup(function () { + Services.telemetry.canRecordExtended = oldCanRecord; + }); + + let uniqueNumber = 1; + for (let loginModifications of StatisticsTestData) { + loginModifications.hostname = `http://${uniqueNumber++}.example.com`; + Services.logins.addLogin(TestData.formLogin(loginModifications)); + } +}); + +/** + * Tests the collection of statistics related to login metadata. + */ +add_task(function test_logins_statistics() { + // Repeat the operation twice to test that histograms are not accumulated. + for (let repeating of [false, true]) { + triggerStatisticsCollection(); + + // Should record 1 in the bucket corresponding to the number of passwords. + testHistogram("PWMGR_NUM_SAVED_PASSWORDS", + { 10: 1 }); + + // Should record 1 in the bucket corresponding to the number of passwords. + testHistogram("PWMGR_NUM_HTTPAUTH_PASSWORDS", + { 1: 1 }); + + // For each saved login, should record 1 in the bucket corresponding to the + // age in days since the login was last used. + testHistogram("PWMGR_LOGIN_LAST_USED_DAYS", + { 0: 1, 1: 1, 7: 2, 29: 2, 356: 2, 750: 1 }); + + // Should record the number of logins without a username in bucket 0, and + // the number of logins with a username in bucket 1. + testHistogram("PWMGR_USERNAME_PRESENT", + { 0: 4, 1: 6 }); + } +}); + +/** + * Tests the collection of statistics related to hosts for which passowrd saving + * has been explicitly disabled. + */ +add_task(function test_disabledHosts_statistics() { + // Should record 1 in the bucket corresponding to the number of sites for + // which password saving is disabled. + Services.logins.setLoginSavingEnabled("http://www.example.com", false); + triggerStatisticsCollection(); + testHistogram("PWMGR_BLOCKLIST_NUM_SITES", { 1: 1 }); + + Services.logins.setLoginSavingEnabled("http://www.example.com", true); + triggerStatisticsCollection(); + testHistogram("PWMGR_BLOCKLIST_NUM_SITES", { 0: 1 }); +}); + +/** + * Tests the collection of statistics related to general settings. + */ +add_task(function test_settings_statistics() { + let oldRememberSignons = Services.prefs.getBoolPref("signon.rememberSignons"); + do_register_cleanup(function () { + Services.prefs.setBoolPref("signon.rememberSignons", oldRememberSignons); + }); + + // Repeat the operation twice per value to test that histograms are reset. + for (let remember of [false, true, false, true]) { + // This change should be observed immediately by the login service. + Services.prefs.setBoolPref("signon.rememberSignons", remember); + + triggerStatisticsCollection(); + + // Should record 1 in either bucket 0 or bucket 1 based on the preference. + testHistogram("PWMGR_SAVING_ENABLED", remember ? { 1: 1 } : { 0: 1 }); + } +}); diff --git a/toolkit/components/passwordmgr/test/unit/test_user_autocomplete_result.js b/toolkit/components/passwordmgr/test/unit/test_user_autocomplete_result.js new file mode 100644 index 000000000..e1d250a76 --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_user_autocomplete_result.js @@ -0,0 +1,488 @@ +XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper", + "resource://gre/modules/LoginHelper.jsm"); +Cu.import("resource://gre/modules/LoginManagerContent.jsm"); +var nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, "init"); + +const PREF_INSECURE_FIELD_WARNING_ENABLED = "security.insecure_field_warning.contextual.enabled"; +const PREF_INSECURE_AUTOFILLFORMS_ENABLED = "signon.autofillForms.http"; + +let matchingLogins = []; +matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null, + "", "emptypass1", "uname", "pword")); + +matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null, + "tempuser1", "temppass1", "uname", "pword")); + +matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null, + "testuser2", "testpass2", "uname", "pword")); + +matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null, + "testuser3", "testpass3", "uname", "pword")); + +matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null, + "zzzuser4", "zzzpass4", "uname", "pword")); + +let meta = matchingLogins[0].QueryInterface(Ci.nsILoginMetaInfo); +let dateAndTimeFormatter = new Intl.DateTimeFormat(undefined, + { day: "numeric", month: "short", year: "numeric" }); +let time = dateAndTimeFormatter.format(new Date(meta.timePasswordChanged)); +const LABEL_NO_USERNAME = "No username (" + time + ")"; + +let expectedResults = [ + { + insecureFieldWarningEnabled: true, + insecureAutoFillFormsEnabled: true, + isSecure: true, + isPasswordField: false, + matchingLogins: matchingLogins, + items: [{ + value: "", + label: LABEL_NO_USERNAME, + style: "login", + }, { + value: "tempuser1", + label: "tempuser1", + style: "login", + }, { + value: "testuser2", + label: "testuser2", + style: "login", + }, { + value: "testuser3", + label: "testuser3", + style: "login", + }, { + value: "zzzuser4", + label: "zzzuser4", + style: "login", + }] + }, + { + insecureFieldWarningEnabled: true, + insecureAutoFillFormsEnabled: true, + isSecure: false, + isPasswordField: false, + matchingLogins: matchingLogins, + items: [{ + value: "", + label: "This connection is not secure. Logins entered here could be compromised. Learn More", + style: "insecureWarning" + }, { + value: "", + label: LABEL_NO_USERNAME, + style: "login", + }, { + value: "tempuser1", + label: "tempuser1", + style: "login", + }, { + value: "testuser2", + label: "testuser2", + style: "login", + }, { + value: "testuser3", + label: "testuser3", + style: "login", + }, { + value: "zzzuser4", + label: "zzzuser4", + style: "login", + }] + }, + { + insecureFieldWarningEnabled: true, + insecureAutoFillFormsEnabled: true, + isSecure: true, + isPasswordField: true, + matchingLogins: matchingLogins, + items: [{ + value: "emptypass1", + label: LABEL_NO_USERNAME, + style: "login", + }, { + value: "temppass1", + label: "tempuser1", + style: "login", + }, { + value: "testpass2", + label: "testuser2", + style: "login", + }, { + value: "testpass3", + label: "testuser3", + style: "login", + }, { + value: "zzzpass4", + label: "zzzuser4", + style: "login", + }] + }, + { + insecureFieldWarningEnabled: true, + insecureAutoFillFormsEnabled: true, + isSecure: false, + isPasswordField: true, + matchingLogins: matchingLogins, + items: [{ + value: "", + label: "This connection is not secure. Logins entered here could be compromised. Learn More", + style: "insecureWarning" + }, { + value: "emptypass1", + label: LABEL_NO_USERNAME, + style: "login", + }, { + value: "temppass1", + label: "tempuser1", + style: "login", + }, { + value: "testpass2", + label: "testuser2", + style: "login", + }, { + value: "testpass3", + label: "testuser3", + style: "login", + }, { + value: "zzzpass4", + label: "zzzuser4", + style: "login", + }] + }, + { + insecureFieldWarningEnabled: false, + insecureAutoFillFormsEnabled: true, + isSecure: true, + isPasswordField: false, + matchingLogins: matchingLogins, + items: [{ + value: "", + label: LABEL_NO_USERNAME, + style: "login", + }, { + value: "tempuser1", + label: "tempuser1", + style: "login", + }, { + value: "testuser2", + label: "testuser2", + style: "login", + }, { + value: "testuser3", + label: "testuser3", + style: "login", + }, { + value: "zzzuser4", + label: "zzzuser4", + style: "login", + }] + }, + { + insecureFieldWarningEnabled: false, + insecureAutoFillFormsEnabled: true, + isSecure: false, + isPasswordField: false, + matchingLogins: matchingLogins, + items: [{ + value: "", + label: LABEL_NO_USERNAME, + style: "login", + }, { + value: "tempuser1", + label: "tempuser1", + style: "login", + }, { + value: "testuser2", + label: "testuser2", + style: "login", + }, { + value: "testuser3", + label: "testuser3", + style: "login", + }, { + value: "zzzuser4", + label: "zzzuser4", + style: "login", + }] + }, + { + insecureFieldWarningEnabled: false, + insecureAutoFillFormsEnabled: true, + isSecure: true, + isPasswordField: true, + matchingLogins: matchingLogins, + items: [{ + value: "emptypass1", + label: LABEL_NO_USERNAME, + style: "login", + }, { + value: "temppass1", + label: "tempuser1", + style: "login", + }, { + value: "testpass2", + label: "testuser2", + style: "login", + }, { + value: "testpass3", + label: "testuser3", + style: "login", + }, { + value: "zzzpass4", + label: "zzzuser4", + style: "login", + }] + }, + { + insecureFieldWarningEnabled: false, + insecureAutoFillFormsEnabled: true, + isSecure: false, + isPasswordField: true, + matchingLogins: matchingLogins, + items: [{ + value: "emptypass1", + label: LABEL_NO_USERNAME, + style: "login", + }, { + value: "temppass1", + label: "tempuser1", + style: "login", + }, { + value: "testpass2", + label: "testuser2", + style: "login", + }, { + value: "testpass3", + label: "testuser3", + style: "login", + }, { + value: "zzzpass4", + label: "zzzuser4", + style: "login", + }] + }, + { + insecureFieldWarningEnabled: true, + insecureAutoFillFormsEnabled: false, + isSecure: true, + isPasswordField: false, + matchingLogins: matchingLogins, + items: [{ + value: "", + label: LABEL_NO_USERNAME, + style: "login", + }, { + value: "tempuser1", + label: "tempuser1", + style: "login", + }, { + value: "testuser2", + label: "testuser2", + style: "login", + }, { + value: "testuser3", + label: "testuser3", + style: "login", + }, { + value: "zzzuser4", + label: "zzzuser4", + style: "login", + }] + }, + { + insecureFieldWarningEnabled: true, + insecureAutoFillFormsEnabled: false, + isSecure: false, + isPasswordField: false, + matchingLogins: matchingLogins, + items: [{ + value: "", + label: "This connection is not secure. Logins entered here could be compromised. Learn More", + style: "insecureWarning" + }, { + value: "", + label: LABEL_NO_USERNAME, + style: "login", + }, { + value: "tempuser1", + label: "tempuser1", + style: "login", + }, { + value: "testuser2", + label: "testuser2", + style: "login", + }, { + value: "testuser3", + label: "testuser3", + style: "login", + }, { + value: "zzzuser4", + label: "zzzuser4", + style: "login", + }] + }, + { + insecureFieldWarningEnabled: true, + insecureAutoFillFormsEnabled: false, + isSecure: true, + isPasswordField: true, + matchingLogins: matchingLogins, + items: [{ + value: "emptypass1", + label: LABEL_NO_USERNAME, + style: "login", + }, { + value: "temppass1", + label: "tempuser1", + style: "login", + }, { + value: "testpass2", + label: "testuser2", + style: "login", + }, { + value: "testpass3", + label: "testuser3", + style: "login", + }, { + value: "zzzpass4", + label: "zzzuser4", + style: "login", + }] + }, + { + insecureFieldWarningEnabled: true, + insecureAutoFillFormsEnabled: false, + isSecure: false, + isPasswordField: true, + matchingLogins: matchingLogins, + items: [{ + value: "", + label: "This connection is not secure. Logins entered here could be compromised. Learn More", + style: "insecureWarning" + }, { + value: "emptypass1", + label: LABEL_NO_USERNAME, + style: "login", + }, { + value: "temppass1", + label: "tempuser1", + style: "login", + }, { + value: "testpass2", + label: "testuser2", + style: "login", + }, { + value: "testpass3", + label: "testuser3", + style: "login", + }, { + value: "zzzpass4", + label: "zzzuser4", + style: "login", + }] + }, + { + insecureFieldWarningEnabled: false, + insecureAutoFillFormsEnabled: false, + isSecure: true, + isPasswordField: false, + matchingLogins: matchingLogins, + items: [{ + value: "", + label: LABEL_NO_USERNAME, + style: "login", + }, { + value: "tempuser1", + label: "tempuser1", + style: "login", + }, { + value: "testuser2", + label: "testuser2", + style: "login", + }, { + value: "testuser3", + label: "testuser3", + style: "login", + }, { + value: "zzzuser4", + label: "zzzuser4", + style: "login", + }] + }, + { + insecureFieldWarningEnabled: false, + insecureAutoFillFormsEnabled: false, + isSecure: false, + isPasswordField: false, + matchingLogins: matchingLogins, + items: [] + }, + { + insecureFieldWarningEnabled: false, + insecureAutoFillFormsEnabled: false, + isSecure: true, + isPasswordField: true, + matchingLogins: matchingLogins, + items: [{ + value: "emptypass1", + label: LABEL_NO_USERNAME, + style: "login", + }, { + value: "temppass1", + label: "tempuser1", + style: "login", + }, { + value: "testpass2", + label: "testuser2", + style: "login", + }, { + value: "testpass3", + label: "testuser3", + style: "login", + }, { + value: "zzzpass4", + label: "zzzuser4", + style: "login", + }] + }, + { + insecureFieldWarningEnabled: false, + insecureAutoFillFormsEnabled: false, + isSecure: false, + isPasswordField: true, + matchingLogins: matchingLogins, + items: [] + }, +]; + +add_task(function* test_all_patterns() { + LoginHelper.createLogger("UserAutoCompleteResult"); + expectedResults.forEach(pattern => { + Services.prefs.setBoolPref(PREF_INSECURE_FIELD_WARNING_ENABLED, + pattern.insecureFieldWarningEnabled); + Services.prefs.setBoolPref(PREF_INSECURE_AUTOFILLFORMS_ENABLED, + pattern.insecureAutoFillFormsEnabled); + let actual = new UserAutoCompleteResult("", pattern.matchingLogins, + { + isSecure: pattern.isSecure, + isPasswordField: pattern.isPasswordField + }); + pattern.items.forEach((item, index) => { + equal(actual.getValueAt(index), item.value); + equal(actual.getLabelAt(index), item.label); + equal(actual.getStyleAt(index), item.style); + }); + + if (pattern.items.length != 0) { + Assert.throws(() => actual.getValueAt(pattern.items.length), + /Index out of range\./); + + Assert.throws(() => actual.getLabelAt(pattern.items.length), + /Index out of range\./); + + Assert.throws(() => actual.removeValueAt(pattern.items.length, true), + /Index out of range\./); + } + }); +}); diff --git a/toolkit/components/passwordmgr/test/unit/xpcshell.ini b/toolkit/components/passwordmgr/test/unit/xpcshell.ini new file mode 100644 index 000000000..8f8c92a28 --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/xpcshell.ini @@ -0,0 +1,46 @@ +[DEFAULT] +head = head.js +tail = +support-files = data/** + +# Test JSON file access and import from SQLite, not applicable to Android. +[test_module_LoginImport.js] +skip-if = os == "android" +[test_module_LoginStore.js] +skip-if = os == "android" +[test_removeLegacySignonFiles.js] +skip-if = os == "android" + +# Test SQLite database backup and migration, applicable to Android only. +[test_storage_mozStorage.js] +skip-if = true || os != "android" # Bug 1171687: Needs fixing on Android + +# The following tests apply to any storage back-end. +[test_context_menu.js] +skip-if = os == "android" # The context menu isn't used on Android. +# LoginManagerContextMenu is only included for MOZ_BUILD_APP == 'browser'. +run-if = buildapp == "browser" +[test_dedupeLogins.js] +[test_disabled_hosts.js] +[test_getFormFields.js] +[test_getPasswordFields.js] +[test_getPasswordOrigin.js] +[test_isOriginMatching.js] +[test_legacy_empty_formSubmitURL.js] +[test_legacy_validation.js] +[test_logins_change.js] +[test_logins_decrypt_failure.js] +skip-if = os == "android" # Bug 1171687: Needs fixing on Android +[test_user_autocomplete_result.js] +skip-if = os == "android" +[test_logins_metainfo.js] +[test_logins_search.js] +[test_maybeImportLogin.js] +[test_notifications.js] +[test_OSCrypto_win.js] +skip-if = os != "win" +[test_recipes_add.js] +[test_recipes_content.js] +[test_search_schemeUpgrades.js] +[test_storage.js] +[test_telemetry.js] -- cgit v1.2.3