discourse/spec/system/user_page/user_preferences_security_spec.rb
Chris Alberti b724181bdf
DEV: Update verbiage around remove password modal, swap button for a link (#32338)
Simplified the language around remove password to be more clear, swapped out the button for a link.
2025-04-16 14:37:54 -05:00

155 lines
5.2 KiB
Ruby

# frozen_string_literal: true
describe "User preferences | Security", type: :system do
fab!(:password) { "kungfukenny" }
fab!(:email) { "email@user.com" }
fab!(:user) { Fabricate(:user, email: email, password: password) }
let(:user_preferences_security_page) { PageObjects::Pages::UserPreferencesSecurity.new }
let(:user_menu) { PageObjects::Components::UserMenu.new }
before do
user.activate
# testing the enforced 2FA flow requires a user that was created > 5 minutes ago
user.created_at = 6.minutes.ago
user.save!
sign_in(user)
# system specs run on their own host + port
DiscourseWebauthn.stubs(:origin).returns(current_host + ":" + Capybara.server_port.to_s)
end
shared_examples "security keys" do
it "adds a 2FA security key and logs in with it" do
options = ::Selenium::WebDriver::VirtualAuthenticatorOptions.new
authenticator = page.driver.browser.add_virtual_authenticator(options)
user_preferences_security_page.visit(user)
user_preferences_security_page.visit_second_factor(user, password)
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
# login flow
find(".d-header .login-button").click
find("input#login-account-name").fill_in(with: user.username)
find("input#login-account-password").fill_in(with: password)
find("#login-button.btn-primary").click
find("#security-key .btn-primary").click
expect(page).to have_css(".header-dropdown-toggle.current-user")
ensure
# clear authenticator (otherwise it will interfere with other tests)
authenticator&.remove!
end
end
shared_examples "passkeys" do
before { SiteSetting.enable_passkeys = true }
it "adds a passkey, removes user password, logs in with passkey" do
options =
::Selenium::WebDriver::VirtualAuthenticatorOptions.new(
user_verification: true,
user_verified: true,
resident_key: true,
)
authenticator = page.driver.browser.add_virtual_authenticator(options)
page.driver.browser.manage.add_cookie(
domain: Discourse.current_hostname,
name: "destination_url",
value: "/new",
path: "/",
)
user_preferences_security_page.visit(user)
find(".pref-passkeys__add .btn").click
expect(user_preferences_security_page).to have_css("input#password")
find(".dialog-body input#password").fill_in(with: password)
find(".confirm-session .btn-primary").click
expect(user_preferences_security_page).to have_css(".rename-passkey__form")
find(".dialog-close").click
expect(user_preferences_security_page).to have_css(".pref-passkeys__rows .row")
select_kit = PageObjects::Components::SelectKit.new(".passkey-options-dropdown")
select_kit.expand
select_kit.select_row_by_name("Delete")
# confirm deletion screen shown without requiring session confirmation
# since this was already done when adding the passkey
expect(user_preferences_security_page).to have_css(".dialog-footer .btn-danger")
# close the dialog (don't delete the key, we need it to login in the next step)
find(".dialog-close").click
find("#remove-password-link").click
# already confirmed session for the passkey, so this will go straight for the confirmation dialog
find(".dialog-footer .btn-danger").click
expect(user_preferences_security_page).to have_no_css("#remove-password-link")
user_menu.sign_out
# login with the key we just created
# this triggers the conditional UI for passkeys
# which uses the virtual authenticator
find(".d-header .login-button").click
expect(page).to have_css(".header-dropdown-toggle.current-user")
# ensures that we are redirected to the destination_url cookie
expect(page.driver.current_url).to include("/new")
ensure
# clear authenticator (otherwise it will interfere with other tests)
authenticator&.remove!
end
end
shared_examples "enforced second factor" do
it "allows user to add 2FA" do
SiteSetting.enforce_second_factor = "all"
visit("/")
expect(page).to have_selector(
".alert-error",
text: "You are required to enable two-factor authentication before accessing this site.",
)
expect(page).to have_css(".user-preferences .totp")
expect(page).to have_css(".user-preferences .security-key")
find(".user-preferences .totp .btn.new-totp").click
find(".dialog-body input#password").fill_in(with: password)
find(".confirm-session .btn-primary").click
expect(page).to have_css(".qr-code")
end
end
context "when desktop" do
include_examples "security keys"
include_examples "passkeys"
include_examples "enforced second factor"
end
context "when mobile", mobile: true do
include_examples "security keys"
include_examples "passkeys"
include_examples "enforced second factor"
end
end