# frozen_string_literal: true require "rotp" shared_examples "forgot password scenarios" do let(:login_modal) { PageObjects::Modals::Login.new } let(:user_preferences_security_page) { PageObjects::Pages::UserPreferencesSecurity.new } fab!(:user) { Fabricate(:user, username: "john", password: "supersecurepassword") } fab!(:password_reset_token) do Fabricate( :email_token, user:, scope: EmailToken.scopes[:password_reset], email: user.email, ).token end let(:user_menu) { PageObjects::Components::UserMenu.new } let(:user_reset_password_page) { PageObjects::Pages::UserResetPassword.new } def visit_reset_password_link visit("/u/password-reset/#{password_reset_token}") end def create_user_security_key(user) # testing the 2FA flow requires a user that was created > 5 minutes ago user.update!(created_at: 6.minutes.ago) sign_in(user) user_preferences_security_page.visit(user) user_preferences_security_page.visit_second_factor("supersecurepassword") find(".security-key .new-security-key").click expect(user_preferences_security_page).to have_css("input#security-key-name") find(".d-modal__body input#security-key-name").fill_in(with: "First Key") find(".add-security-key").click expect(user_preferences_security_page).to have_css(".security-key .second-factor-item") user_menu.sign_out end context "when user does not have any multi-factor authentication configured" do it "should allow a user to reset their password" do visit_reset_password_link user_reset_password_page.fill_in_new_password("newsuperpassword").submit_new_password expect(user_reset_password_page).to have_logged_in_user end end context "when user has multi-factor authentication configured" do context "when user only has TOTP configured" do fab!(:user_second_factor_totp) { Fabricate(:user_second_factor_totp, user:) } it "should allow a user to reset password with TOTP" do visit_reset_password_link expect(user_reset_password_page).to have_no_toggle_button_to_second_factor_form user_reset_password_page .fill_in_totp(ROTP::TOTP.new(user_second_factor_totp.data).now) .submit_totp .fill_in_new_password("newsuperpassword") .submit_new_password expect(user_reset_password_page).to have_logged_in_user end end context "when user only has security key configured" do before do @authenticator = page.driver.browser.add_virtual_authenticator( Selenium::WebDriver::VirtualAuthenticatorOptions.new, ) create_user_security_key(user) end after { @authenticator.remove! } it "should allow a user to reset password with a security key" do visit_reset_password_link expect(user_reset_password_page).to have_no_toggle_button_to_second_factor_form user_reset_password_page.submit_security_key user_reset_password_page.fill_in_new_password("newsuperpassword").submit_new_password expect(user_reset_password_page).to have_logged_in_user end end context "when user has TOTP and backup codes configured" do fab!(:user_second_factor_backup) { Fabricate(:user_second_factor_backup, user:) } fab!(:user_second_factor_totp) { Fabricate(:user_second_factor_totp, user:) } it "should allow a user to reset password with backup code" do visit_reset_password_link user_reset_password_page .use_backup_codes .fill_in_backup_code("iAmValidBackupCode") .submit_backup_code .fill_in_new_password("newsuperpassword") .submit_new_password expect(user_reset_password_page).to have_logged_in_user end end context "when user has security key and backup codes configured" do fab!(:user_second_factor_backup) { Fabricate(:user_second_factor_backup, user:) } before do @authenticator = page.driver.browser.add_virtual_authenticator( Selenium::WebDriver::VirtualAuthenticatorOptions.new, ) create_user_security_key(user) end after { @authenticator.remove! } it "should allow a user to reset password with backup code instead of security key" do visit_reset_password_link user_reset_password_page.try_another_way expect(user_reset_password_page).to have_no_toggle_button_in_second_factor_form user_reset_password_page .fill_in_backup_code("iAmValidBackupCode") .submit_backup_code .fill_in_new_password("newsuperpassword") .submit_new_password expect(user_reset_password_page).to have_logged_in_user end end context "when user has TOTP, security key and backup codes configured" do fab!(:user_second_factor_totp) { Fabricate(:user_second_factor_totp, user:) } fab!(:user_second_factor_backup) { Fabricate(:user_second_factor_backup, user:) } before do @authenticator = page.driver.browser.add_virtual_authenticator( Selenium::WebDriver::VirtualAuthenticatorOptions.new, ) create_user_security_key(user) end after { @authenticator.remove! } it "should allow a user to toggle from security key to TOTP and between TOTP and backup codes" do visit_reset_password_link user_reset_password_page.try_another_way expect(user_reset_password_page).to have_totp_description user_reset_password_page.use_backup_codes expect(user_reset_password_page).to have_backup_codes_description user_reset_password_page.use_totp expect(user_reset_password_page).to have_totp_description end end context "when user has TOTP and security key configured but no backup codes" do fab!(:user_second_factor_totp) { Fabricate(:user_second_factor_totp, user:) } before do @authenticator = page.driver.browser.add_virtual_authenticator( Selenium::WebDriver::VirtualAuthenticatorOptions.new, ) create_user_security_key(user) end after { @authenticator.remove! } it "should allow a user to reset password with TOTP instead of security key" do visit_reset_password_link user_reset_password_page.try_another_way expect(user_reset_password_page).to have_no_toggle_button_in_second_factor_form user_reset_password_page .fill_in_totp(ROTP::TOTP.new(user_second_factor_totp.data).now) .submit_totp .fill_in_new_password("newsuperpassword") .submit_new_password expect(user_reset_password_page).to have_logged_in_user end end end end describe "User resetting password", type: :system do context "when desktop" do include_examples "forgot password scenarios" end context "when mobile", mobile: true do include_examples "forgot password scenarios" end end