0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-08 17:53:55 +08:00
discourse/spec/system/code_signup_spec.rb
Keegan George 8b280eb7b0
FEATURE: Sign up with a one-time email code (#40909)
Previously, signing up required picking a username and password and
confirming through an activation email, even on sites that only want a
minimal email-based signup.

This change builds on the email code login flow so that, when
`enable_local_logins_via_code` is enabled, new users can sign up with
just an email address and a one-time code: they enter an email, confirm
the code, fill in any required signup fields, and land on an
account-ready step where they pick a username and avatar for their new
passwordless account. Account creation only happens after the code
proves ownership of the inbox, and it respects
`allow_new_registrations`, `invite_only`, `require_invite_code`,
`must_approve_users`, and screened/blocked email rules.

Requires changes from #40804 


https://github.com/user-attachments/assets/2969db68-af55-4d5e-9587-37262776632f




<details>
<summary>Screenshots</summary>

**Desktop**

| Step | Foundation · Light | Foundation · Dark | Horizon · Light |
Horizon · Dark |
|---|---|---|---|---|
| Email entry | <img width="1400" height="1200"
alt="desktop-foundation-light-code-signup-email-step"
src="https://github.com/user-attachments/assets/db92c4a2-9f90-4f61-9c32-5d2be3e7f211"
/> | <img width="1400" height="1200"
alt="desktop-foundation-dark-code-signup-email-step"
src="https://github.com/user-attachments/assets/72a5a416-cabd-4958-99da-1b766db0eceb"
/> | <img width="1400" height="1200"
alt="desktop-horizon-light-code-signup-email-step"
src="https://github.com/user-attachments/assets/2bed0aa0-3883-45cb-959f-73b1ace9628b"
/> | <img width="1400" height="1200"
alt="desktop-horizon-dark-code-signup-email-step"
src="https://github.com/user-attachments/assets/ad2269a0-2bc8-4a6d-9dc7-e1462aab0596"
/> |
| Code entry | <img width="1400" height="1200"
alt="desktop-foundation-light-code-signup-code-step"
src="https://github.com/user-attachments/assets/3b651100-a788-4562-9dd5-92f15a86eaef"
/> | <img width="1400" height="1200"
alt="desktop-foundation-dark-code-signup-code-step"
src="https://github.com/user-attachments/assets/db13d098-e7d1-4d4f-a0c5-deb201a5f909"
/> | <img width="1400" height="1200"
alt="desktop-horizon-light-code-signup-code-step"
src="https://github.com/user-attachments/assets/37f9f251-d0ed-4665-8f53-25224b11e026"
/> | <img width="1400" height="1200"
alt="desktop-horizon-dark-code-signup-code-step"
src="https://github.com/user-attachments/assets/362e273b-d524-480b-9b07-5fe95fcb16d4"
/> |
| Required fields | <img width="1400" height="1200"
alt="desktop-foundation-light-code-signup-user-fields-step"
src="https://github.com/user-attachments/assets/f81cc094-fef2-4c45-8b09-759f64bafd36"
/> | <img width="1400" height="1200"
alt="desktop-foundation-dark-code-signup-user-fields-step"
src="https://github.com/user-attachments/assets/8aec56a1-80cd-4179-b071-bf8faf55d591"
/> | <img width="1400" height="1200"
alt="desktop-horizon-light-code-signup-user-fields-step"
src="https://github.com/user-attachments/assets/ce7e6f13-b684-49b6-98b2-e1481fdfb43c"
/> | <img width="1400" height="1200"
alt="desktop-horizon-dark-code-signup-user-fields-step"
src="https://github.com/user-attachments/assets/b253e9d9-0d7d-4482-93b6-fdbb767762d9"
/> |
| Account ready | <img width="1400" height="1200"
alt="desktop-foundation-light-code-signup-complete-step"
src="https://github.com/user-attachments/assets/7282a6a5-ddce-45fb-ac18-4f9eecc45fce"
/> | <img width="1400" height="1200"
alt="desktop-foundation-dark-code-signup-complete-step"
src="https://github.com/user-attachments/assets/bf573b5f-e431-4c6d-8616-ed4df1324488"
/> | <img width="1400" height="1200"
alt="desktop-horizon-light-code-signup-complete-step"
src="https://github.com/user-attachments/assets/114cc19d-1f90-4a26-be5c-ce28637ae852"
/> | <img width="1400" height="1200"
alt="desktop-horizon-dark-code-signup-complete-step"
src="https://github.com/user-attachments/assets/1494835e-4ecf-4fd1-acc8-1816167917f1"
/> |

**Mobile**

| Step | Foundation · Light | Foundation · Dark | Horizon · Light |
Horizon · Dark |
|---|---|---|---|---|
| Email entry | <img width="1170" height="3600"
alt="mobile-foundation-light-code-signup-email-step"
src="https://github.com/user-attachments/assets/10d77db9-b83f-4e26-85e8-0fad4bb37812"
/> | <img width="1170" height="3600"
alt="mobile-foundation-dark-code-signup-email-step"
src="https://github.com/user-attachments/assets/73225116-0d69-4c58-9a58-17803e9685be"
/> | <img width="1170" height="3600"
alt="mobile-horizon-light-code-signup-email-step"
src="https://github.com/user-attachments/assets/8654ffcc-9694-4364-97b3-eb867ffb3ef6"
/> | <img width="1170" height="3600"
alt="mobile-horizon-dark-code-signup-email-step"
src="https://github.com/user-attachments/assets/c6254023-d711-4d09-b010-d0662aeafdbf"
/> |
| Code entry | <img width="1170" height="3600"
alt="mobile-foundation-light-code-signup-code-step"
src="https://github.com/user-attachments/assets/b2603114-a2b8-4d12-9343-ce6626fbca16"
/> | <img width="1170" height="3600"
alt="mobile-foundation-dark-code-signup-code-step"
src="https://github.com/user-attachments/assets/cefd4e19-460d-4149-916b-d007cdcf0ae3"
/> | <img width="1170" height="3600"
alt="mobile-horizon-light-code-signup-code-step"
src="https://github.com/user-attachments/assets/eeba6cbf-edc7-4ab9-b90f-cf77cefc788b"
/> | <img width="1170" height="3600"
alt="mobile-horizon-dark-code-signup-code-step"
src="https://github.com/user-attachments/assets/739de191-8363-4de2-9f1e-4afd41acbafd"
/> |
| Required fields | <img width="1170" height="3600"
alt="mobile-foundation-light-code-signup-user-fields-step"
src="https://github.com/user-attachments/assets/4f2697aa-9df0-4dc9-9cf9-012f5a8cc15c"
/> | <img width="1170" height="3600"
alt="mobile-foundation-dark-code-signup-user-fields-step"
src="https://github.com/user-attachments/assets/e9ba5131-89dc-432e-a96d-4c4bf7f98b4f"
/> | <img width="1170" height="3600"
alt="mobile-horizon-light-code-signup-user-fields-step"
src="https://github.com/user-attachments/assets/de39ae14-b076-4096-bbed-11f0a98f9dfd"
/> | <img width="1170" height="3600"
alt="mobile-horizon-dark-code-signup-user-fields-step"
src="https://github.com/user-attachments/assets/58ee1450-2b01-47cd-8d49-6eebc6fd60a1"
/> |
| Account ready | <img width="1170" height="3600"
alt="mobile-foundation-light-code-signup-complete-step"
src="https://github.com/user-attachments/assets/08598aad-cb5a-4f4c-bf18-c911504dc55c"
/> | <img width="1170" height="3600"
alt="mobile-foundation-dark-code-signup-complete-step"
src="https://github.com/user-attachments/assets/b78b39aa-a7c7-435a-baab-f5d21c4dbada"
/> | <img width="1170" height="3600"
alt="mobile-horizon-light-code-signup-complete-step"
src="https://github.com/user-attachments/assets/86d01a27-a49b-4d3a-81cc-2b25fb13b7d9"
/> | <img width="1170" height="3600"
alt="mobile-horizon-dark-code-signup-complete-step"
src="https://github.com/user-attachments/assets/f3935eae-fda5-42f7-9c9e-7e0d5b13b6b8"
/> |

</details>
2026-06-23 11:30:18 -07:00

182 lines
6.2 KiB
Ruby
Vendored

# frozen_string_literal: true
describe "Sign up via email code" do
include ThemeScreenshotMarker
before do
SiteSetting.enable_local_logins_via_email = true
SiteSetting.enable_local_logins_via_code = true
Jobs.run_immediately!
end
def fill_code(code)
find(".d-otp-input").fill_in(with: code)
end
def latest_emailed_code(email)
wait_for(timeout: 10) { ActionMailer::Base.deliveries.count != 0 }
mail = ActionMailer::Base.deliveries.last
expect(mail.to).to contain_exactly(email)
mail.subject[/(\d{6})/, 1]
end
def submit_email(email)
find(".code-login-form__email-step input[type='email']").fill_in(with: email)
find(".code-login-form__continue").click
expect(page).to have_css(".code-login-form__code-step")
end
def pick_username(name)
fill_in("code-login-username", with: name)
expect(page).to have_no_css(".code-login-form__continue-to-site[disabled]")
end
it "creates a passwordless account, picks a username, and logs in" do
visit("/signup")
expect(page).to have_css(".code-login-form__email-step")
expect(page).to have_content(I18n.t("js.code_login.signup_title"))
screenshot_marker(label: "code-signup-email-step")
submit_email("new.person@example.com")
screenshot_marker(label: "code-signup-code-step")
fill_code(latest_emailed_code("new.person@example.com"))
expect(page).to have_css(".code-login-form__complete-step")
screenshot_marker(label: "code-signup-complete-step")
# A username must be picked before the account can be used.
expect(page).to have_css(".code-login-form__continue-to-site[disabled]")
pick_username("new-person")
find(".code-login-form__continue-to-site").click
expect(page).to have_css(".header-dropdown-toggle.current-user")
user = User.find_by_email("new.person@example.com")
expect(user).to be_active
expect(user.username).to eq("new-person")
expect(user.user_password).to be_nil
end
it "blocks continuing while the picked username is taken" do
Fabricate(:user, username: "takenname")
visit("/signup")
submit_email("new.person@example.com")
fill_code(latest_emailed_code("new.person@example.com"))
expect(page).to have_css(".code-login-form__complete-step")
fill_in("code-login-username", with: "takenname")
expect(page).to have_css(".code-login-form__error", text: "username")
expect(page).to have_css(".code-login-form__continue-to-site[disabled]")
end
it "prefills the username when email-based suggestions are enabled" do
SiteSetting.use_email_for_username_and_name_suggestions = true
visit("/signup")
submit_email("jane@example.com")
fill_code(latest_emailed_code("jane@example.com"))
expect(page).to have_css(".code-login-form__complete-step")
expect(find("#code-login-username").value).to eq("jane")
end
it "keeps the generated username when usernames can't be changed" do
SiteSetting.username_change_period = 0
visit("/signup")
submit_email("locked.name@example.com")
fill_code(latest_emailed_code("locked.name@example.com"))
expect(page).to have_css(".code-login-form__complete-step")
expect(page).to have_no_css("#code-login-username")
find(".code-login-form__continue-to-site").click
expect(page).to have_css(".header-dropdown-toggle.current-user")
expect(User.find_by_email("locked.name@example.com")).to be_present
end
it "opens the avatar picker before continuing" do
visit("/signup")
submit_email("avatar.person@example.com")
fill_code(latest_emailed_code("avatar.person@example.com"))
expect(page).to have_css(".code-login-form__complete-step")
find(".code-login-form__avatar").click
expect(page).to have_css(".avatar-selector-modal")
end
it "shows an error for an incorrect code" do
visit("/signup")
submit_email("new.person@example.com")
correct_code = latest_emailed_code("new.person@example.com")
fill_code(correct_code == "000000" ? "000001" : "000000")
expect(page).to have_css(
".code-login-form__error",
text: I18n.t("email_login_code.invalid_code"),
)
expect(page).to have_no_css(".header-dropdown-toggle.current-user")
expect(User.find_by_email("new.person@example.com")).to be_nil
end
it "does not create an account when registrations are disabled" do
SiteSetting.allow_new_registrations = false
visit("/signup")
submit_email("new.person@example.com")
# No code is sent when registrations are closed, so any code is rejected.
fill_code("000000")
expect(page).to have_css(".code-login-form__error")
expect(page).to have_no_css(".header-dropdown-toggle.current-user")
expect(User.find_by_email("new.person@example.com")).to be_nil
end
it "shows a pending-approval message when users must be approved" do
SiteSetting.must_approve_users = true
visit("/signup")
submit_email("approve.me@example.com")
fill_code(latest_emailed_code("approve.me@example.com"))
expect(page).to have_css(".code-login-form__error", text: I18n.t("login.not_approved"))
expect(page).to have_no_css(".header-dropdown-toggle.current-user")
user = User.find_by_email("approve.me@example.com")
expect(user).not_to be_approved
expect(ReviewableUser.pending.find_by(target: user)).to be_present
end
context "with required user fields" do
fab!(:user_field) { Fabricate(:user_field, name: "Occupation") }
it "collects the fields after the code is verified" do
visit("/signup")
submit_email("fields.person@example.com")
fill_code(latest_emailed_code("fields.person@example.com"))
expect(page).to have_css(".code-login-form__user-fields-step")
screenshot_marker(label: "code-signup-user-fields-step")
find(".user-field-occupation input").fill_in(with: "Dev")
find(".code-login-form__user-fields-step .code-login-form__verify").click
expect(page).to have_css(".code-login-form__complete-step")
pick_username("fields-person")
find(".code-login-form__continue-to-site").click
expect(page).to have_css(".header-dropdown-toggle.current-user")
user = User.find_by_email("fields.person@example.com")
expect(user.username).to eq("fields-person")
expect(user.custom_fields["user_field_#{user_field.id}"]).to eq("Dev")
end
end
end