Files
discourse/spec/system/forgot_password_spec.rb
Alan Guo Xiang Tan ac26a52c6d DEV: Improve PageObjects::Pages::UserPreferencesSecurity#visit_second_factor (#32017)
This commit improves said method to ensure that user is redirected to
the right page before returning.

### Reviewer notes

Example of test flakiness:
https://github.com/discourse/discourse/actions/runs/14081653020/job/39435797236

```
Failure/Error: raise capybara_timeout_error

CapybaraTimeoutExtension::CapybaraTimedOut:
  This spec passed, but capybara waited for the full wait duration (10s) at least once. This will slow down the test suite. Beware of negating the result of selenium's RSpec matchers.

[Screenshot Image]: /__w/discourse/discourse/tmp/capybara/failures_r_spec_example_groups_user_resetting_password_when_desktop_when_user_has_multi_factor_authentication_configured_when_user_has_security_key_and_backup_codes_configured_should_allow_a_user_to_reset_pass_261.png

~~~~~~~ JS LOGS ~~~~~~~
~~~~~ END JS LOGS ~~~~~

Shared Example Group: "forgot password scenarios" called from ./spec/system/forgot_password_spec.rb:213

./spec/rails_helper.rb:426:in `block (3 levels) in <top (required)>'
./spec/rails_helper.rb:619:in `block (3 levels) in <top (required)>'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/benchmark-0.4.0/lib/benchmark.rb:304:in `measure'
./spec/rails_helper.rb:619:in `block (2 levels) in <top (required)>'
./spec/rails_helper.rb:580:in `block (3 levels) in <top (required)>'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/timeout-0.4.3/lib/timeout.rb:185:in `block in timeout'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/timeout-0.4.3/lib/timeout.rb:192:in `timeout'
./spec/rails_helper.rb:570:in `block (2 levels) in <top (required)>'
./spec/rails_helper.rb:527:in `block (2 levels) in <top (required)>'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/webmock-3.25.1/lib/webmock/rspec.rb:39:in `block (2 levels) in <top (required)>'
```
2025-03-26 22:11:03 +08:00

220 lines
6.9 KiB
Ruby

# frozen_string_literal: true
require "rotp"
shared_examples "forgot password scenarios" do
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(user, "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
describe "when desktop" do
include_examples "forgot password scenarios"
end
describe "when mobile", mobile: true do
include_examples "forgot password scenarios"
end
end