discourse/spec/requests/users_controller_spec.rb
Osama Sayegh 976aca68f6
FEATURE: Restrict profile visibility of low-trust users (#29981)
We've seen in some communities abuse of user profile where bios and other fields are used in malicious ways, such as malware distribution. A common pattern between all the abuse cases we've seen is that the malicious actors tend to have 0 posts and have a low trust level.

To eliminate this abuse vector, or at least make it much less effective, we're making the following changes to user profiles:

1. Anonymous, TL0 and TL1 users cannot see any user profiles for users with 0 posts except for staff users
2. Anonymous and TL0 users can only see profiles of TL1 users and above

Users can always see their own profile, and they can still hide their profiles via the "Hide my public profile" preference. Staff can always see any user's profile.

Internal topic: t/142853.
2024-12-09 13:07:59 +03:00

7512 lines
252 KiB
Ruby

# frozen_string_literal: true
require "rotp"
RSpec.describe UsersController do
fab!(:user) { Fabricate(:user, refresh_auto_groups: true) }
fab!(:user1) do
Fabricate(:user, username: "someusername", refresh_auto_groups: true, created_at: 6.minutes.ago)
end
fab!(:another_user) { Fabricate(:user, refresh_auto_groups: true) }
fab!(:invitee) { Fabricate(:user) }
fab!(:inviter) { Fabricate(:user) }
fab!(:admin)
fab!(:moderator)
fab!(:inactive_user)
# Unfortunately, there are tests that depend on the user being created too
# late for fab! to work.
let(:user_deferred) { Fabricate(:user, refresh_auto_groups: true) }
describe "#full account registration flow" do
it "will correctly handle honeypot and challenge" do
get "/session/hp.json"
expect(response.status).to eq(200)
json = response.parsed_body
params = {
email: "jane@jane.com",
name: "jane",
username: "jane",
password_confirmation: json["value"],
challenge: json["challenge"].reverse,
password: SecureRandom.hex,
}
secure_session = SecureSession.new(session["secure_session_id"])
expect(secure_session[UsersController::HONEYPOT_KEY]).to eq(json["value"])
expect(secure_session[UsersController::CHALLENGE_KEY]).to eq(json["challenge"])
post "/u.json", params: params
expect(response.status).to eq(200)
jane = User.find_by(username: "jane")
expect(jane.email).to eq("jane@jane.com")
expect(secure_session[UsersController::HONEYPOT_KEY]).to eq(nil)
expect(secure_session[UsersController::CHALLENGE_KEY]).to eq(nil)
end
end
describe "#perform_account_activation" do
let(:email_token) { Fabricate(:email_token, user: user_deferred) }
before { UsersController.any_instance.stubs(:honeypot_or_challenge_fails?).returns(false) }
context "with invalid token" do
it "return success" do
put "/u/activate-account/invalid-token"
expect(response.status).to eq(422)
end
end
context "with valid token" do
context "with welcome message" do
it "enqueues a welcome message if the user object indicates so" do
SiteSetting.send_welcome_message = true
user_deferred.update(active: false)
put "/u/activate-account/#{email_token.token}"
expect(response.status).to eq(200)
expect(Jobs::SendSystemMessage.jobs.size).to eq(1)
expect(Jobs::SendSystemMessage.jobs.first["args"].first["message_type"]).to eq(
"welcome_user",
)
end
it "doesn't enqueue the welcome message if the object returns false" do
user_deferred.update(active: true)
put "/u/activate-account/#{email_token.token}"
expect(response.status).to eq(200)
expect(Jobs::SendSystemMessage.jobs.size).to eq(0)
end
end
context "with honeypot" do
it "raises an error if the honeypot is invalid" do
UsersController.any_instance.stubs(:honeypot_or_challenge_fails?).returns(true)
put "/u/activate-account/#{email_token.token}"
expect(response.status).to eq(403)
end
end
context "with response" do
it "correctly logs on user" do
email_token
events = DiscourseEvent.track_events { put "/u/activate-account/#{email_token.token}" }
expect(events.map { |event| event[:event_name] }).to contain_exactly(
:user_confirmed_email,
:user_first_logged_in,
:user_logged_in,
)
expect(response.status).to eq(200)
data = JSON.parse(response.body)
expect(data["needs_approval"]).to eq(false)
expect(session[:current_user_id]).to be_present
end
end
context "when user is not approved" do
before { SiteSetting.must_approve_users = true }
it "should return the right response" do
put "/u/activate-account/#{email_token.token}"
expect(response.status).to eq(200)
data = JSON.parse(response.body)
expect(data["needs_approval"]).to eq(true)
expect(session[:current_user_id]).to be_blank
end
end
context "when bootstrap mode is enabled" do
before { SiteSetting.bootstrap_mode_enabled = true }
it "adds the user to the user directory" do
token = Fabricate(:email_token, user: inactive_user)
expect do put "/u/activate-account/#{token.token}" end.to change {
DirectoryItem.where(user_id: inactive_user.id).count
}.by(DirectoryItem.period_types.count)
expect(response.status).to eq(200)
end
end
context "when bootstrap mode is disabled" do
before { SiteSetting.bootstrap_mode_enabled = false }
it "adds the user to the user directory" do
token = Fabricate(:email_token, user: inactive_user)
expect do put "/u/activate-account/#{token.token}" end.not_to change {
DirectoryItem.where(user_id: inactive_user.id).count
}
expect(response.status).to eq(200)
end
end
end
context "when cookies contains a destination URL" do
it "should redirect to the URL" do
destination_url = "http://thisisasite.com/somepath"
cookies[:destination_url] = destination_url
put "/u/activate-account/#{email_token.token}"
expect(response.status).to eq(200)
expect(response.parsed_body["redirect_to"]).to eq(destination_url)
end
end
context "when cookies does not contain a destination URL but users was invited to topic" do
let(:invite) { Fabricate(:invite) }
let(:topic) { Fabricate(:topic) }
before do
TopicInvite.create!(topic: topic, invite: invite)
Fabricate(:invited_user, invite: invite, user: email_token.user)
invite.reload
end
it "should redirect to the topic" do
put "/u/activate-account/#{email_token.token}"
expect(response.status).to eq(200)
expect(response.parsed_body["redirect_to"]).to eq(topic.relative_url)
end
end
end
describe "#password_reset" do
let(:token) { SecureRandom.hex }
context "when login is required" do
it "returns success" do
SiteSetting.login_required = true
get "/u/password-reset/#{token}"
expect(response.status).to eq(200)
expect(CGI.unescapeHTML(response.body)).to include(
I18n.t("password_reset.no_token", base_url: Discourse.base_url),
)
end
end
context "with missing token" do
it "disallows login" do
get "/u/password-reset/#{token}"
expect(response.status).to eq(200)
expect(CGI.unescapeHTML(response.body)).to include(
I18n.t("password_reset.no_token", base_url: Discourse.base_url),
)
expect(response.body).to_not have_tag(:script, with: { src: "/assets/application.js" })
expect(session[:current_user_id]).to be_blank
end
it "responds with proper error message" do
get "/u/password-reset/#{token}.json"
expect(response.status).to eq(200)
expect(response.parsed_body["message"]).to eq(
I18n.t("password_reset.no_token", base_url: Discourse.base_url),
)
expect(session[:current_user_id]).to be_blank
end
end
context "with invalid token" do
it "disallows login" do
get "/u/password-reset/ev!l_trout@!"
expect(response.status).to eq(200)
expect(CGI.unescapeHTML(response.body)).to include(
I18n.t("password_reset.no_token", base_url: Discourse.base_url),
)
expect(response.body).to_not have_tag(:script, with: { src: "/assets/application.js" })
expect(session[:current_user_id]).to be_blank
end
it "responds with proper error message" do
put "/u/password-reset/evil_trout!.json", params: { password: "awesomeSecretPassword" }
expect(response.status).to eq(200)
expect(response.parsed_body["message"]).to eq(
I18n.t("password_reset.no_token", base_url: Discourse.base_url),
)
expect(session[:current_user_id]).to be_blank
end
end
context "with valid token" do
let!(:user_auth_token) { UserAuthToken.generate!(user_id: user1.id) }
let!(:email_token) do
Fabricate(:email_token, user: user1, scope: EmailToken.scopes[:password_reset])
end
context "when rendered" do
it "renders referrer never on get requests" do
get "/u/password-reset/#{email_token.token}"
expect(response.status).to eq(200)
expect(response.body).to include('<meta name="referrer" content="never">')
end
end
it "returns success" do
events =
DiscourseEvent.track_events do
put "/u/password-reset/#{email_token.token}", params: { password: "hg9ow8yhg98o" }
end
expect(events.map { |event| event[:event_name] }).to contain_exactly(
:user_logged_in,
:user_first_logged_in,
:user_confirmed_email,
)
expect(response.status).to eq(200)
expect(response.body).to have_tag("div#data-preloaded") do |element|
json = JSON.parse(element.current_scope.attribute("data-preloaded").value)
expect(json["password_reset"]).to include(
'{"is_developer":false,"admin":false,"second_factor_required":false,"security_key_required":false,"backup_enabled":false,"multiple_second_factor_methods":false}',
)
end
expect(session["password-#{email_token.token}"]).to be_blank
expect(UserAuthToken.where(id: user_auth_token.id).count).to eq(0)
end
it "disallows double password reset" do
put "/u/password-reset/#{email_token.token}", params: { password: "hg9ow8yHG32O" }
put "/u/password-reset/#{email_token.token}", params: { password: "test123987AsdfXYZ" }
expect(user1.reload.confirm_password?("hg9ow8yHG32O")).to eq(true)
expect(user1.user_auth_tokens.count).to eq(1)
end
it "doesn't redirect to wizard on get" do
user1.update!(admin: true)
get "/u/password-reset/#{email_token.token}.json"
expect(response).not_to redirect_to(wizard_path)
end
it "redirects to the wizard if you're the first admin" do
user1.update!(admin: true)
get "/u/password-reset/#{email_token.token}"
put "/u/password-reset/#{email_token.token}",
params: {
password: "hg9ow8yhg98oadminlonger",
}
expect(response).to redirect_to(wizard_path)
end
it "sets the users timezone if the param is present" do
get "/u/password-reset/#{email_token.token}"
expect(user1.user_option.timezone).to eq(nil)
put "/u/password-reset/#{email_token.token}",
params: {
password: "hg9ow8yhg98oadminlonger",
timezone: "America/Chicago",
}
expect(user1.user_option.reload.timezone).to eq("America/Chicago")
end
it "deletes user associated accounts" do
SiteSetting.delete_associated_accounts_on_password_reset = true
UserAssociatedAccount.create(
user_id: user.id,
provider_uid: "example0",
provider_name: "facebook",
)
UserAssociatedAccount.create(
user_id: user1.id,
provider_uid: "example1",
provider_name: "facebook",
)
get "/u/password-reset/#{email_token.token}"
expect do
put "/u/password-reset/#{email_token.token}",
params: {
password: "hg9ow8yhg98oadminlonger",
}
end.to change { UserAssociatedAccount.count }.by(-1)
expect(UserAssociatedAccount.count).to eq(1)
end
it "logs the password change" do
get "/u/password-reset/#{email_token.token}"
expect do
put "/u/password-reset/#{email_token.token}",
params: {
password: "hg9ow8yhg98oadminlonger",
}
end.to change { UserHistory.count }.by(1)
user_history = UserHistory.last
expect(user_history.target_user_id).to eq(user1.id)
expect(user_history.action).to eq(UserHistory.actions[:change_password])
end
it "doesn't invalidate the token when loading the page" do
get "/u/password-reset/#{email_token.token}.json"
expect(response.status).to eq(200)
expect(email_token.reload.confirmed).to eq(false)
expect(UserAuthToken.where(id: user_auth_token.id).count).to eq(1)
end
context "with rate limiting" do
before { RateLimiter.enable }
it "rate limits reset passwords" do
freeze_time
6.times do
put "/u/password-reset/#{email_token.token}",
params: {
second_factor_token: 123_456,
second_factor_method: 1,
}
expect(response.status).to eq(200)
end
put "/u/password-reset/#{email_token.token}",
params: {
second_factor_token: 123_456,
second_factor_method: 1,
}
expect(response.status).to eq(429)
end
it "rate limits reset passwords by username" do
freeze_time
6.times do |x|
put "/u/password-reset/#{email_token.token}",
params: {
second_factor_token: 123_456,
second_factor_method: 1,
},
env: {
REMOTE_ADDR: "1.2.3.#{x}",
}
expect(response.status).to eq(200)
end
put "/u/password-reset/#{email_token.token}",
params: {
second_factor_token: 123_456,
second_factor_method: 1,
},
env: {
REMOTE_ADDR: "1.2.3.4",
}
expect(response.status).to eq(429)
end
end
context "when 2 factor authentication is required" do
fab!(:second_factor) { Fabricate(:user_second_factor_totp, user: user1) }
it "does not change with an invalid token" do
user1.user_auth_tokens.destroy_all
get "/u/password-reset/#{email_token.token}"
expect(response.body).to have_tag("div#data-preloaded") do |element|
json = JSON.parse(element.current_scope.attribute("data-preloaded").value)
expect(json["password_reset"]).to include(
'{"is_developer":false,"admin":false,"second_factor_required":true,"security_key_required":false,"backup_enabled":false,"multiple_second_factor_methods":false}',
)
end
put "/u/password-reset/#{email_token.token}",
params: {
password: "hg9ow8yHG32O",
second_factor_token: "000000",
second_factor_method: UserSecondFactor.methods[:totp],
}
expect(response.body).to include(I18n.t("login.invalid_second_factor_code"))
user1.reload
expect(user1.confirm_password?("hg9ow8yHG32O")).not_to eq(true)
expect(user1.user_auth_tokens.count).not_to eq(1)
end
it "changes password with valid 2-factor tokens" do
get "/u/password-reset/#{email_token.token}"
put "/u/password-reset/#{email_token.token}",
params: {
password: "hg9ow8yHG32O",
second_factor_token: ROTP::TOTP.new(second_factor.data).now,
second_factor_method: UserSecondFactor.methods[:totp],
}
user1.reload
expect(response.status).to eq(200)
expect(user1.confirm_password?("hg9ow8yHG32O")).to eq(true)
expect(user1.user_auth_tokens.count).to eq(1)
end
end
context "when security key authentication is required" do
let!(:user_security_key) do
Fabricate(
:user_security_key,
user: user1,
credential_id: valid_security_key_data[:credential_id],
public_key: valid_security_key_data[:public_key],
)
end
before do
simulate_localhost_webauthn_challenge
DiscourseWebauthn.stubs(:origin).returns("http://localhost:3000")
# store challenge in secure session by visiting the email login page
get "/u/password-reset/#{email_token.token}"
end
it "preloads with a security key challenge and allowed credential ids" do
expect(response.body).to have_tag("div#data-preloaded") do |element|
json = JSON.parse(element.current_scope.attribute("data-preloaded").value)
password_reset = JSON.parse(json["password_reset"])
expect(password_reset["challenge"]).not_to eq(nil)
expect(password_reset["allowed_credential_ids"]).to eq(
[user_security_key.credential_id],
)
expect(password_reset["security_key_required"]).to eq(true)
end
end
it "stages a webauthn challenge for the user" do
secure_session = SecureSession.new(session["secure_session_id"])
expect(DiscourseWebauthn.challenge(user1, secure_session)).not_to eq(nil)
end
it "changes password with valid security key challenge and authentication" do
put "/u/password-reset/#{email_token.token}.json",
params: {
password: "hg9ow8yHG32O",
second_factor_token: valid_security_key_auth_post_data,
second_factor_method: UserSecondFactor.methods[:security_key],
}
expect(response.status).to eq(200)
user1.reload
expect(user1.confirm_password?("hg9ow8yHG32O")).to eq(true)
expect(user1.user_auth_tokens.count).to eq(1)
end
it "does not change a password if a fake TOTP token is provided" do
put "/u/password-reset/#{email_token.token}.json",
params: {
password: "hg9ow8yHG32O",
second_factor_token: "blah",
second_factor_method: UserSecondFactor.methods[:security_key],
}
expect(response.status).to eq(200)
expect(user1.reload.confirm_password?("hg9ow8yHG32O")).to eq(false)
end
context "when security key authentication fails" do
it "shows an error message and does not change password" do
put "/u/password-reset/#{email_token.token}",
params: {
password: "hg9ow8yHG32O",
second_factor_token: {
signature: "bad",
clientData: "bad",
authenticatorData: "bad",
credentialId: "bad",
},
second_factor_method: UserSecondFactor.methods[:security_key],
}
expect(response.status).to eq(200)
expect(response.body).to include(I18n.t("webauthn.validation.not_found_error"))
expect(user1.reload.confirm_password?("hg9ow8yHG32O")).to eq(false)
end
end
end
end
context "with submit change" do
let(:email_token) do
Fabricate(:email_token, user: user1, scope: EmailToken.scopes[:password_reset])
end
it "fails when the password is blank" do
put "/u/password-reset/#{email_token.token}.json", params: { password: "" }
expect(response.status).to eq(200)
expect(response.parsed_body["errors"]).to be_present
expect(session[:current_user_id]).to be_blank
end
it "fails when the password is too long" do
put "/u/password-reset/#{email_token.token}.json",
params: {
password: ("x" * (User.max_password_length + 1)),
}
expect(response.status).to eq(200)
expect(response.parsed_body["errors"]).to be_present
expect(session[:current_user_id]).to be_blank
end
it "logs in the user" do
put "/u/password-reset/#{email_token.token}.json", params: { password: "ksjafh928r" }
expect(response.status).to eq(200)
expect(response.parsed_body["errors"]).to be_blank
expect(session[:current_user_id]).to be_present
end
it "doesn't log in the user when not approved" do
SiteSetting.must_approve_users = true
user1.update!(approved: false)
put "/u/password-reset/#{email_token.token}.json", params: { password: "ksjafh928r" }
expect(response.parsed_body["errors"]).to be_blank
expect(session[:current_user_id]).to be_blank
end
context "when in staff writes only mode" do
before { Discourse.enable_readonly_mode(Discourse::STAFF_WRITES_ONLY_MODE_KEY) }
it "allows staff to reset their password" do
admin = Fabricate(:admin)
email_token =
Fabricate(:email_token, user: admin, scope: EmailToken.scopes[:password_reset])
put "/u/password-reset/#{email_token.token}.json",
params: {
password: "hg9ow8yhg98oadminlonger",
}
expect(response.parsed_body["errors"]).to be_blank
expect(session[:current_user_id]).to eq(admin.id)
end
it "doesn't allow non-staff to reset their password" do
put "/u/password-reset/#{email_token.token}.json", params: { password: "ksjafh928r" }
expect(response.parsed_body["errors"]).to_not be_blank
expect(session[:current_user_id]).to be_blank
end
end
end
end
describe "#confirm_email_token" do
let!(:email_token) { Fabricate(:email_token, user: user1) }
it "token doesn't match any records" do
get "/u/confirm-email-token/#{SecureRandom.hex}.json"
expect(response.status).to eq(200)
expect(email_token.reload.confirmed).to eq(false)
end
it "token matches" do
get "/u/confirm-email-token/#{email_token.token}.json"
expect(response.status).to eq(200)
expect(email_token.reload.confirmed).to eq(true)
end
end
describe "#admin_login" do
it "enqueues mail with admin email and sso enabled" do
put "/u/admin-login", params: { email: admin.email }
expect(response.status).to eq(200)
expect(Jobs::CriticalUserEmail.jobs.size).to eq(1)
args = Jobs::CriticalUserEmail.jobs.first["args"].first
expect(args["user_id"]).to eq(admin.id)
end
it "passes through safe mode" do
put "/u/admin-login", params: { email: admin.email, use_safe_mode: true }
expect(response.status).to eq(200)
expect(Jobs::CriticalUserEmail.jobs.size).to eq(1)
args = Jobs::CriticalUserEmail.jobs.first["args"].first
expect(args["email_token"]).to end_with("?safe_mode=no_plugins,no_themes")
end
context "when email is incorrect" do
it "should return the right response" do
put "/u/admin-login", params: { email: "random" }
expect(response.status).to eq(200)
response_body = response.body
expect(response_body).to match(I18n.t("admin_login.errors.unknown_email_address"))
expect(response_body).to_not match(I18n.t("login.second_factor_description"))
end
end
end
describe "#toggle_anon" do
it "allows you to toggle anon if enabled" do
SiteSetting.allow_anonymous_posting = true
user = sign_in(Fabricate(:user, trust_level: TrustLevel[1]))
post "/u/toggle-anon.json"
expect(response.status).to eq(200)
expect(session[:current_user_id]).to eq(AnonymousShadowCreator.get(user).id)
post "/u/toggle-anon.json"
expect(response.status).to eq(200)
expect(session[:current_user_id]).to eq(user.id)
end
end
describe "#create" do
def honeypot_magic(params)
get "/session/hp.json"
json = response.parsed_body
params[:password_confirmation] = json["value"]
params[:challenge] = json["challenge"].reverse
params
end
before do
UsersController.any_instance.stubs(:honeypot_value).returns(nil)
UsersController.any_instance.stubs(:challenge_value).returns(nil)
SiteSetting.allow_new_registrations = true
@user = Fabricate.build(:user, email: "foobar@example.com", password: "strongpassword")
end
let(:post_user_params) do
{ name: @user.name, username: @user.username, password: "strongpassword", email: @user.email }
end
def post_user(extra_params = {})
post "/u.json", params: post_user_params.merge(extra_params)
end
context "when email params is missing" do
it "should raise the right error" do
post "/u.json",
params: {
name: @user.name,
username: @user.username,
password: "testing12352343",
}
expect(response.status).to eq(400)
end
end
context "when using an encoded email that decodes to an invalid email" do
it "blocks the registration" do
post_user(email: "=?x?q?hacker=40hackerdomain.com=3e=00?=osama@discourseemail.com")
expect(response.status).to eq(200)
expect(response.parsed_body["success"]).to eq(false)
expect(response.parsed_body["message"]).to eq("Primary email is invalid.")
expect(response.parsed_body["user_id"]).to be_blank
end
end
context "when using an encoded email that decodes to a valid email" do
it "accepts the registration" do
post_user(
email:
"=?utf-8?q?=6f=73=61=6d=61=2d=69=6e=2d=71=2d=65=6e=63=6f=64=69=6e=67?=@discourse.org",
)
expect(response.status).to eq(200)
expect(response.parsed_body["success"]).to eq(true)
expect(User.find_by(id: response.parsed_body["user_id"])).to be_present
end
end
context "when creating a user" do
it "sets the user locale to I18n.locale" do
SiteSetting.default_locale = "en"
I18n.stubs(:locale).returns(:fr)
post_user
expect(User.find_by(username: @user.username).locale).to eq("fr")
end
it "requires invite code when specified" do
expect(SiteSetting.require_invite_code).to eq(false)
SiteSetting.invite_code = "abc def"
expect(SiteSetting.require_invite_code).to eq(true)
post_user(invite_code: "abcd")
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["success"]).to eq(false)
# case insensitive and stripped of leading/ending spaces
post_user(invite_code: " AbC deF ")
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["success"]).to eq(true)
end
context "when timezone is provided as a guess on signup" do
it "sets the timezone" do
post_user(timezone: "Australia/Brisbane")
expect(response.status).to eq(200)
expect(User.find_by(username: @user.username).user_option.timezone).to eq(
"Australia/Brisbane",
)
end
end
context "with local logins disabled" do
before do
SiteSetting.enable_local_logins = false
SiteSetting.enable_google_oauth2_logins = true
end
it "blocks registration without authenticator information" do
post_user
expect(response.status).to eq(403)
end
it "blocks with a regular api key" do
api_key = Fabricate(:api_key, user: user1)
post "/u.json", params: post_user_params, headers: { HTTP_API_KEY: api_key.key }
expect(response.status).to eq(403)
end
it "works with an admin api key" do
api_key = Fabricate(:api_key, user: admin)
post "/u.json", params: post_user_params, headers: { HTTP_API_KEY: api_key.key }
expect(response.status).to eq(200)
end
end
context "with external_ids" do
fab!(:api_key, refind: false) { Fabricate(:api_key, user: admin) }
let(:plugin_auth_provider) do
authenticator_class =
Class.new(Auth::ManagedAuthenticator) do
def name
"pluginauth"
end
def enabled?
true
end
end
provider = Auth::AuthProvider.new
provider.authenticator = authenticator_class.new
provider
end
before { DiscoursePluginRegistry.register_auth_provider(plugin_auth_provider) }
after { DiscoursePluginRegistry.reset! }
it "creates User record" do
params = {
username: "foobar",
email: "test@example.com",
external_ids: {
"pluginauth" => "pluginauth_uid",
},
}
expect {
post "/u.json", params: params, headers: { HTTP_API_KEY: api_key.key }
}.to change { UserAssociatedAccount.count }.by(1).and change { User.count }.by(1)
expect(response.status).to eq(200)
user = User.last
user_associated_account = UserAssociatedAccount.last
expect(user.username).to eq("foobar")
expect(user.email).to eq("test@example.com")
expect(user.user_associated_account_ids).to contain_exactly(user_associated_account.id)
expect(user_associated_account.provider_name).to eq("pluginauth")
expect(user_associated_account.provider_uid).to eq("pluginauth_uid")
expect(user_associated_account.user_id).to eq(user.id)
end
it "returns error if external ID provider does not exist" do
params = {
username: "foobar",
email: "test@example.com",
external_ids: {
"pluginauth2" => "pluginauth_uid",
},
}
post "/u.json", params: params, headers: { HTTP_API_KEY: api_key.key }
expect(response.status).to eq(400)
end
end
end
context "when creating a non active user (unconfirmed email)" do
it "returns 403 forbidden when local logins are disabled" do
SiteSetting.enable_local_logins = false
post_user
expect(response.status).to eq(403)
end
it "returns an error when new registrations are disabled" do
SiteSetting.allow_new_registrations = false
post_user
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["success"]).to eq(false)
expect(json["message"]).to be_present
end
it "creates a user correctly" do
post_user
expect(response.status).to eq(200)
expect(response.parsed_body["active"]).to be_falsey
# should save user_created_message in session
expect(session["user_created_message"]).to be_present
expect(session[SessionController::ACTIVATE_USER_KEY]).to be_present
expect(Jobs::SendSystemMessage.jobs.size).to eq(0)
expect(Jobs::CriticalUserEmail.jobs.size).to eq(1)
args = Jobs::CriticalUserEmail.jobs.first["args"].first
expect(args["type"]).to eq("signup")
end
context "when `must approve users` site setting is enabled" do
before { SiteSetting.must_approve_users = true }
it "creates a user correctly" do
post_user
expect(response.status).to eq(200)
expect(response.parsed_body["active"]).to be_falsey
# should save user_created_message in session
expect(session["user_created_message"]).to be_present
expect(session[SessionController::ACTIVATE_USER_KEY]).to be_present
expect(Jobs::SendSystemMessage.jobs.size).to eq(0)
expect(Jobs::CriticalUserEmail.jobs.size).to eq(1)
args = Jobs::CriticalUserEmail.jobs.first["args"].first
expect(args["type"]).to eq("signup")
end
end
context "when normalize_emails is enabled" do
let(:email) { "jane+100@gmail.com" }
let(:dupe_email) { "jane+191@gmail.com" }
let!(:user) { Fabricate(:user, email: email, password: "strongpassword") }
before do
SiteSetting.hide_email_address_taken = true
SiteSetting.normalize_emails = true
end
it "sends an email to normalized email owner when hide_email_address_taken is enabled" do
expect do
expect_enqueued_with(
job: Jobs::CriticalUserEmail,
args: {
type: "account_exists",
user_id: user.id,
},
) do
post "/u.json",
params: {
name: "Jane Doe",
username: "janedoe9999",
password: "strongpassword",
email: dupe_email,
}
end
end.to_not change { User.count }
expect(response.status).to eq(200)
expect(session["user_created_message"]).to be_present
end
end
context "when users already exists with given email" do
let!(:existing) { Fabricate(:user, email: post_user_params[:email]) }
it "returns an error if hide_email_address_taken is disabled" do
SiteSetting.hide_email_address_taken = false
post_user
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["success"]).to eq(false)
expect(json["message"]).to be_present
end
it "returns success if hide_email_address_taken is enabled" do
SiteSetting.hide_email_address_taken = true
expect {
expect_enqueued_with(
job: Jobs::CriticalUserEmail,
args: {
type: "account_exists",
user_id: existing.id,
},
) { post_user }
}.to_not change { User.count }
expect(response.status).to eq(200)
expect(session["user_created_message"]).to be_present
json = response.parsed_body
expect(json["active"]).to be_falsey
expect(json["message"]).to eq(
I18n.t("login.activate_email", email: post_user_params[:email]),
)
expect(json["user_id"]).not_to be_present
existing.destroy!
expect { post_user }.to change { User.count }
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["active"]).to be_falsey
expect(json["message"]).to eq(
I18n.t("login.activate_email", email: post_user_params[:email]),
)
expect(json["user_id"]).not_to be_present
end
end
end
context "when creating as active" do
it "won't create the user as active" do
post "/u.json", params: post_user_params.merge(active: true)
expect(response.status).to eq(200)
expect(response.parsed_body["active"]).to be_falsey
end
context "with a regular api key" do
fab!(:api_key, refind: false) { Fabricate(:api_key, user: user1) }
it "won't create the user as active with a regular key" do
post "/u.json",
params: post_user_params.merge(active: true),
headers: {
HTTP_API_KEY: api_key.key,
}
expect(response.status).to eq(200)
expect(response.parsed_body["active"]).to be_falsey
end
end
context "with an admin api key" do
fab!(:api_key, refind: false) { Fabricate(:api_key, user: admin) }
it "creates the user as active with a an admin key" do
SiteSetting.send_welcome_message = true
SiteSetting.must_approve_users = true
# Sidekiq::Client.expects(:enqueue).never
post "/u.json",
params: post_user_params.merge(approved: true, active: true),
headers: {
HTTP_API_KEY: api_key.key,
}
expect(Jobs::CriticalUserEmail.jobs.size).to eq(0)
expect(Jobs::SendSystemMessage.jobs.size).to eq(0)
expect(response.status).to eq(200)
expect(response.parsed_body["active"]).to be_truthy
new_user = User.find(response.parsed_body["user_id"])
expect(new_user.active).to eq(true)
expect(new_user.approved).to eq(true)
expect(new_user.approved_by_id).to eq(admin.id)
expect(new_user.approved_at).to_not eq(nil)
expect(new_user.email_tokens.where(confirmed: true, email: new_user.email)).to exist
end
it "will create a reviewable when a user is created as active but not approved" do
Jobs.run_immediately!
SiteSetting.must_approve_users = true
post "/u.json",
params: post_user_params.merge(active: true),
headers: {
HTTP_API_KEY: api_key.key,
}
expect(response.status).to eq(200)
json = response.parsed_body
new_user = User.find(json["user_id"])
expect(json["active"]).to be_truthy
expect(new_user.approved).to eq(false)
expect(ReviewableUser.pending.find_by(target: new_user)).to be_present
end
it "won't create a reviewable when a user is not active" do
Jobs.run_immediately!
SiteSetting.must_approve_users = true
post "/u.json", params: post_user_params, headers: { HTTP_API_KEY: api_key.key }
expect(response.status).to eq(200)
json = response.parsed_body
new_user = User.find(json["user_id"])
expect(json["active"]).to eq(false)
expect(new_user.approved).to eq(false)
expect(ReviewableUser.pending.find_by(target: new_user)).to be_blank
end
it "won't create the developer as active" do
UsernameCheckerService.expects(:is_developer?).returns(true)
post "/u.json",
params: post_user_params.merge(active: true),
headers: {
HTTP_API_KEY: api_key.key,
}
expect(response.status).to eq(200)
expect(response.parsed_body["active"]).to be_falsy
end
it "won't set the new user's locale to the admin's locale" do
SiteSetting.allow_user_locale = true
admin.update!(locale: :fr)
post "/u.json",
params: post_user_params.merge(active: true),
headers: {
HTTP_API_KEY: api_key.key,
}
expect(response.status).to eq(200)
json = response.parsed_body
new_user = User.find(json["user_id"])
expect(new_user.locale).not_to eq("fr")
end
it "will auto approve user if the user email domain matches auto_approve_email_domains setting" do
Jobs.run_immediately!
SiteSetting.must_approve_users = true
SiteSetting.auto_approve_email_domains = "example.com"
post "/u.json",
params: post_user_params.merge(active: true),
headers: {
HTTP_API_KEY: api_key.key,
}
expect(response.status).to eq(200)
json = response.parsed_body
new_user = User.find(json["user_id"])
expect(json["active"]).to be_truthy
expect(new_user.approved).to be_truthy
expect(ReviewableUser.pending.find_by(target: new_user)).to be_blank
end
end
end
context "when creating as staged" do
it "won't create the user as staged" do
post "/u.json", params: post_user_params.merge(staged: true)
expect(response.status).to eq(200)
new_user = User.where(username: post_user_params[:username]).first
expect(new_user.staged?).to eq(false)
end
context "with a regular api key" do
fab!(:api_key, refind: false) { Fabricate(:api_key, user: user1) }
it "won't create the user as staged with a regular key" do
post "/u.json",
params: post_user_params.merge(staged: true),
headers: {
HTTP_API_KEY: api_key.key,
}
expect(response.status).to eq(200)
new_user = User.where(username: post_user_params[:username]).first
expect(new_user.staged?).to eq(false)
end
end
context "with an admin api key" do
fab!(:user) { admin }
fab!(:api_key, refind: false) { Fabricate(:api_key, user: user) }
it "creates the user as staged with a regular key" do
post "/u.json",
params: post_user_params.merge(staged: true),
headers: {
HTTP_API_KEY: api_key.key,
}
expect(response.status).to eq(200)
new_user = User.where(username: post_user_params[:username]).first
expect(new_user.staged?).to eq(true)
end
it "won't create the developer as staged" do
UsernameCheckerService.expects(:is_developer?).returns(true)
post "/u.json",
params: post_user_params.merge(staged: true),
headers: {
HTTP_API_KEY: api_key.key,
}
expect(response.status).to eq(200)
new_user = User.where(username: post_user_params[:username]).first
expect(new_user.staged?).to eq(false)
end
end
end
context "when creating an active user (confirmed email)" do
before { User.any_instance.stubs(:active?).returns(true) }
it "enqueues a welcome email" do
User.any_instance.expects(:enqueue_welcome_message).with("welcome_user")
post_user
expect(response.status).to eq(200)
# should save user_created_message in session
expect(session["user_created_message"]).to be_present
expect(session[SessionController::ACTIVATE_USER_KEY]).to be_present
end
it "shows the 'active' message" do
User.any_instance.expects(:enqueue_welcome_message)
post_user
expect(response.status).to eq(200)
expect(response.parsed_body["message"]).to eq(I18n.t "login.active")
end
it "should be logged in" do
User.any_instance.expects(:enqueue_welcome_message)
post_user
expect(response.status).to eq(200)
expect(session[:current_user_id]).to be_present
end
it "indicates the user is active in the response" do
User.any_instance.expects(:enqueue_welcome_message)
post_user
expect(response.status).to eq(200)
expect(response.parsed_body["active"]).to be_truthy
end
it 'doesn\'t succeed when new registrations are disabled' do
SiteSetting.allow_new_registrations = false
post_user
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["success"]).to eq(false)
expect(json["message"]).to be_present
end
context "with authentication records for" do
before do
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:twitter] = OmniAuth::AuthHash.new(
provider: "twitter",
uid: "123545",
info:
OmniAuth::AuthHash::InfoHash.new(
email: "osama@mail.com",
nickname: "testosama",
name: "Osama Test",
),
)
Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:twitter]
SiteSetting.enable_twitter_logins = true
get "/auth/twitter/callback.json"
end
after do
Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:twitter] = nil
OmniAuth.config.test_mode = false
end
it "should create twitter user info if required" do
post "/u.json",
params: {
name: "Test Osama",
username: "testosama",
password: "strongpassword",
email: "osama@mail.com",
}
expect(response.status).to eq(200)
expect(UserAssociatedAccount.where(provider_name: "twitter").count).to eq(1)
end
it "returns an error when email has been changed from the validated email address" do
post "/u.json",
params: {
name: "Test Osama",
username: "testosama",
password: "strongpassword",
email: "unvalidatedemail@mail.com",
}
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["success"]).to eq(false)
expect(json["message"]).to be_present
end
it "will create the user successfully if email validation is required" do
post "/u.json",
params: {
name: "Test Osama",
username: "testosama",
password: "strongpassword",
email: "osama@mail.com",
}
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["success"]).to eq(true)
end
it "doesn't use provided username/name if sso_overrides is enabled" do
SiteSetting.auth_overrides_username = true
SiteSetting.auth_overrides_name = true
post "/u.json",
params: {
username: "attemptednewname",
name: "Attempt At New Name",
password: "strongpassword",
email: "osama@mail.com",
}
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["success"]).to eq(true)
user = User.last
expect(user.username).to eq("testosama")
expect(user.name).to eq("Osama Test")
end
end
context "with no email in the auth payload" do
before do
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:twitter] = OmniAuth::AuthHash.new(
provider: "twitter",
uid: "123545",
info: OmniAuth::AuthHash::InfoHash.new(nickname: "testosama", name: "Osama Test"),
)
Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:twitter]
SiteSetting.enable_twitter_logins = true
get "/auth/twitter/callback.json"
end
after do
Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:twitter] = nil
OmniAuth.config.test_mode = false
end
it "will create the user successfully" do
Rails.application.env_config["omniauth.auth"].info.email = nil
post "/u.json",
params: {
name: "Test Osama",
username: "testosama",
password: "strongpassword",
email: "osama@mail.com",
}
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["success"]).to eq(true)
end
end
end
it "creates user successfully but doesn't activate the account" do
post_user
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["success"]).to eq(true)
expect(User.find_by(username: @user.username).active).to eq(false)
end
shared_examples "honeypot fails" do
it "should not create a new user" do
User.any_instance.expects(:enqueue_welcome_message).never
expect { post "/u.json", params: create_params }.to_not change { User.count }
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["success"]).to eq(true)
# should not change the session
expect(session["user_created_message"]).to be_blank
expect(session[SessionController::ACTIVATE_USER_KEY]).to be_blank
end
end
context "when honeypot value is wrong" do
before { UsersController.any_instance.stubs(:honeypot_value).returns("abc") }
let(:create_params) do
{
name: @user.name,
username: @user.username,
password: "strongpassword",
email: @user.email,
password_confirmation: "wrong",
}
end
include_examples "honeypot fails"
end
context "when challenge answer is wrong" do
before { UsersController.any_instance.stubs(:challenge_value).returns("abc") }
let(:create_params) do
{
name: @user.name,
username: @user.username,
password: "strongpassword",
email: @user.email,
challenge: "abc",
}
end
include_examples "honeypot fails"
end
context "when 'invite only' setting is enabled" do
before { SiteSetting.invite_only = true }
let(:create_params) do
{
name: @user.name,
username: @user.username,
password: "strongpassword",
email: @user.email,
}
end
include_examples "honeypot fails"
end
shared_examples "failed signup" do
it "should not create a new User" do
expect { post "/u.json", params: create_params }.to_not change { User.count }
expect(response.status).to eq(200)
end
it "should report failed" do
post "/u.json", params: create_params
json = response.parsed_body
expect(json["success"]).not_to eq(true)
# should not change the session
expect(session["user_created_message"]).to be_blank
expect(session[SessionController::ACTIVATE_USER_KEY]).to be_blank
end
end
context "when password is blank" do
let(:create_params) do
{ name: @user.name, username: @user.username, password: "", email: @user.email }
end
include_examples "failed signup"
end
context "when password is too long" do
let(:create_params) do
{
name: @user.name,
username: @user.username,
password: "x" * (User.max_password_length + 1),
email: @user.email,
}
end
include_examples "failed signup"
end
context "when password param is missing" do
let(:create_params) { { name: @user.name, username: @user.username, email: @user.email } }
include_examples "failed signup"
end
context "with a reserved username" do
let(:create_params) do
{ name: @user.name, username: "Reserved", email: @user.email, password: "strongpassword" }
end
before { SiteSetting.reserved_usernames = "a|reserved|b" }
include_examples "failed signup"
end
context "with a username that matches a user route" do
let(:create_params) do
{
name: @user.name,
username: "account-created",
email: @user.email,
password: "strongpassword",
}
end
include_examples "failed signup"
end
context "with a missing username" do
let(:create_params) { { name: @user.name, email: @user.email, password: "x" * 20 } }
it "should not create a new User" do
expect { post "/u.json", params: create_params }.to_not change { User.count }
expect(response.status).to eq(400)
end
end
context "when an Exception is raised" do
before { User.any_instance.stubs(:save).raises(ActiveRecord::StatementInvalid.new("Oh no")) }
let(:create_params) do
{
name: @user.name,
username: @user.username,
password: "strongpassword",
email: @user.email,
}
end
include_examples "failed signup"
end
context "with custom fields" do
fab!(:user_field)
fab!(:another_field) { Fabricate(:user_field) }
fab!(:optional_field) { Fabricate(:user_field, requirement: "optional") }
context "without a value for the fields" do
let(:create_params) do
{ name: @user.name, password: "watwatwat", username: @user.username, email: @user.email }
end
include_examples "failed signup"
end
context "with values for the fields" do
let(:update_user_url) { "/u/#{user1.username}.json" }
let(:field_id) { user_field.id.to_s }
before { sign_in(user1) }
context "with multple select fields" do
let(:valid_options) { %w[Axe Sword] }
fab!(:user_field) do
Fabricate(:user_field, field_type: "multiselect") do
user_field_options do
[
Fabricate(:user_field_option, value: "Axe"),
Fabricate(:user_field_option, value: "Sword"),
]
end
end
end
it "should allow single values and not just arrays" do
expect do
put update_user_url, params: { user_fields: { field_id => "Axe" } }
end.to change { user1.reload.user_fields[field_id] }.from(nil).to("Axe")
expect do
put update_user_url, params: { user_fields: { field_id => %w[Axe Juice Sword] } }
end.to change { user1.reload.user_fields[field_id] }.from("Axe").to(%w[Axe Sword])
end
it "shouldn't allow unregistered field values" do
expect do
put update_user_url, params: { user_fields: { field_id => %w[Juice] } }
end.not_to change { user1.reload.user_fields[field_id] }
end
it "should filter valid values" do
expect do
put update_user_url, params: { user_fields: { field_id => %w[Axe Juice Sword] } }
end.to change { user1.reload.user_fields[field_id] }.from(nil).to(valid_options)
end
it "allows registered field values" do
expect do
put update_user_url, params: { user_fields: { field_id => valid_options } }
end.to change { user1.reload.user_fields[field_id] }.from(nil).to(valid_options)
end
it "value can't be nil or empty if the field is required" do
put update_user_url, params: { user_fields: { field_id => valid_options } }
user_field.for_all_users!
expect do
put update_user_url, params: { user_fields: { field_id => nil } }
end.not_to change { user1.reload.user_fields[field_id] }
expect do
put update_user_url, params: { user_fields: { field_id => "" } }
end.not_to change { user1.reload.user_fields[field_id] }
end
it "value is required only on sign-up" do
user_field.on_signup!
expect do
put update_user_url, params: { user_fields: { field_id => "" } }
end.to change { user1.reload.user_fields[field_id] }.from(nil).to("")
put update_user_url, params: { user_fields: { field_id => valid_options } }
expect do
put update_user_url, params: { user_fields: { field_id => "" } }
end.not_to change { user1.reload.user_fields[field_id] }
end
it "value can nil or empty if the field is not required" do
put update_user_url, params: { user_fields: { field_id => valid_options } }
user_field.optional!
expect do
put update_user_url, params: { user_fields: { field_id => nil } }
end.to change { user1.reload.user_fields[field_id] }.from(valid_options).to(nil)
expect do
put update_user_url, params: { user_fields: { field_id => "" } }
end.to change { user1.reload.user_fields[field_id] }.from(nil).to("")
end
end
context "with dropdown fields" do
let(:valid_options) { ["Black Mesa", "Fox Hound"] }
fab!(:user_field) do
Fabricate(:user_field, field_type: "dropdown") do
user_field_options do
[
Fabricate(:user_field_option, value: "Black Mesa"),
Fabricate(:user_field_option, value: "Fox Hound"),
]
end
end
end
it "shouldn't allow unregistered field values" do
expect do
put update_user_url, params: { user_fields: { field_id => "Umbrella Corporation" } }
end.not_to change { user1.reload.user_fields[field_id] }
end
it "allows registered field values" do
expect do
put update_user_url, params: { user_fields: { field_id => valid_options.first } }
end.to change { user1.reload.user_fields[field_id] }.from(nil).to(valid_options.first)
end
it "value can't be nil if the field is required" do
put update_user_url, params: { user_fields: { field_id => valid_options.first } }
user_field.for_all_users!
expect do
put update_user_url, params: { user_fields: { field_id => nil } }
end.not_to change { user1.reload.user_fields[field_id] }
end
it "value can be set to nil if the field is not required" do
put update_user_url, params: { user_fields: { field_id => valid_options.last } }
user_field.optional!
expect do
put update_user_url, params: { user_fields: { field_id => nil } }
end.to change { user1.reload.user_fields[field_id] }.from(valid_options.last).to(nil)
end
end
let(:create_params) do
{
name: @user.name,
password: "suChS3cuRi7y",
username: @user.username,
email: @user.email,
user_fields: {
user_field.id.to_s => "value1",
another_field.id.to_s => "value2",
},
}
end
it "should succeed without the optional field" do
post "/u.json", params: create_params
expect(response.status).to eq(200)
inserted = User.find_by_email(@user.email)
expect(inserted).to be_present
expect(inserted.custom_fields).to be_present
expect(inserted.custom_fields["user_field_#{user_field.id}"]).to eq("value1")
expect(inserted.custom_fields["user_field_#{another_field.id}"]).to eq("value2")
expect(inserted.custom_fields["user_field_#{optional_field.id}"]).to be_blank
end
it "should succeed with the optional field" do
create_params[:user_fields][optional_field.id.to_s] = "value3"
post "/u.json", params: create_params.merge(create_params)
expect(response.status).to eq(200)
inserted = User.find_by_email(@user.email)
expect(inserted).to be_present
expect(inserted.custom_fields).to be_present
expect(inserted.custom_fields["user_field_#{user_field.id}"]).to eq("value1")
expect(inserted.custom_fields["user_field_#{another_field.id}"]).to eq("value2")
expect(inserted.custom_fields["user_field_#{optional_field.id}"]).to eq("value3")
end
it "trims excessively long fields" do
create_params[:user_fields][optional_field.id.to_s] = ("x" * 3000)
post "/u.json", params: create_params.merge(create_params)
expect(response.status).to eq(200)
inserted = User.find_by_email(@user.email)
val = inserted.custom_fields["user_field_#{optional_field.id}"]
expect(val.length).to eq(UserField.max_length)
end
end
end
context "with only optional custom fields" do
fab!(:user_field) { Fabricate(:user_field, requirement: "optional") }
context "without values for the fields" do
let(:create_params) do
{
name: @user.name,
password: "suChS3cuRi7y",
username: @user.username,
email: @user.email,
}
end
it "should succeed" do
post "/u.json", params: create_params
expect(response.status).to eq(200)
inserted = User.find_by_email(@user.email)
expect(inserted).to be_present
expect(inserted.custom_fields).not_to be_present
expect(inserted.custom_fields["user_field_#{user_field.id}"]).to be_blank
end
end
end
context "when taking over a staged account" do
before do
UsersController.any_instance.stubs(:honeypot_value).returns("abc")
UsersController.any_instance.stubs(:challenge_value).returns("efg")
SessionController.any_instance.stubs(:honeypot_value).returns("abc")
SessionController.any_instance.stubs(:challenge_value).returns("efg")
end
fab!(:staged) { Fabricate(:staged, email: "staged@account.com", active: true) }
it "succeeds" do
post "/u.json",
params:
honeypot_magic(email: staged.email, username: "zogstrip", password: "P4ssw0rd$$")
expect(response.status).to eq(200)
result = response.parsed_body
expect(result["success"]).to eq(true)
created_user = User.find_by_email(staged.email)
expect(created_user.staged).to eq(false)
expect(created_user.active).to eq(false)
expect(created_user.registration_ip_address).to be_present
expect(!!created_user.custom_fields["from_staged"]).to eq(true)
# do not allow emails changes please
put "/u/update-activation-email.json", params: { email: "bob@bob.com" }
created_user.reload
expect(created_user.email).to eq("staged@account.com")
expect(response.status).to eq(403)
end
it "works with custom fields" do
tennis_field = Fabricate(:user_field, show_on_profile: true, name: "Favorite tennis player")
post "/u.json",
params:
honeypot_magic(
email: staged.email,
username: "dude",
password: "P4ssw0rd$$",
user_fields: {
[tennis_field.id] => "Nadal",
},
)
expect(response.status).to eq(200)
result = response.parsed_body
expect(result["success"]).to eq(true)
created_user = User.find_by_email(staged.email)
expect(created_user.staged).to eq(false)
expect(created_user.active).to eq(false)
expect(created_user.registration_ip_address).to be_present
expect(!!created_user.custom_fields["from_staged"]).to eq(true)
expect(created_user.custom_fields["user_field_#{tennis_field.id}"]).to eq("Nadal")
end
end
end
describe "#username" do
it "raises an error when not logged in" do
put "/u/somename/preferences/username.json"
expect(response.status).to eq(403)
end
context "while logged in" do
let(:old_username) { "OrigUsername" }
let(:new_username) { "#{old_username}1234" }
fab!(:user) { Fabricate(:user, username: "OrigUsername", refresh_auto_groups: true) }
before do
user.username = old_username
sign_in(user)
end
it "raises an error without a new_username param" do
put "/u/#{user.username}/preferences/username.json", params: { username: user.username }
expect(response.status).to eq(400)
expect(user.reload.username).to eq(old_username)
end
it 'raises an error when you don\'t have permission to change the username' do
Guardian.any_instance.expects(:can_edit_username?).with(user).returns(false)
put "/u/#{user.username}/preferences/username.json", params: { new_username: new_username }
expect(response).to be_forbidden
expect(user.reload.username).to eq(old_username)
end
it "raises an error when change_username fails" do
put "/u/#{user.username}/preferences/username.json", params: { new_username: "@" }
expect(response.status).to eq(422)
body = response.parsed_body
expect(body["errors"].first).to include(
I18n.t("user.username.short", count: User.username_length.begin),
)
expect(user.reload.username).to eq(old_username)
end
it "should succeed in normal circumstances" do
put "/u/#{user.username}/preferences/username.json", params: { new_username: new_username }
expect(response.status).to eq(200)
expect(user.reload.username).to eq(new_username)
end
it "raises an error when the username clashes with an existing user route" do
put "/u/#{user.username}/preferences/username.json",
params: {
new_username: "account-created",
}
body = response.parsed_body
expect(body["errors"].first).to include(I18n.t("login.reserved_username"))
end
it "raises an error when the username is in the reserved list" do
SiteSetting.reserved_usernames = "reserved"
put "/u/#{user.username}/preferences/username.json", params: { new_username: "reserved" }
body = response.parsed_body
expect(body["errors"].first).to include(I18n.t("login.reserved_username"))
end
it "should fail if the user is old" do
# Older than the change period and >1 post
user.created_at = Time.now - (SiteSetting.username_change_period + 1).days
PostCreator.new(
user,
title: "This is a test topic",
raw: "This is a test this is a test",
).create
put "/u/#{user.username}/preferences/username.json", params: { new_username: new_username }
expect(response).to be_forbidden
expect(user.reload.username).to eq(old_username)
end
it "should create a staff action log when a staff member changes the username" do
acting_user = admin
sign_in(acting_user)
put "/u/#{user.username}/preferences/username.json", params: { new_username: new_username }
expect(response.status).to eq(200)
expect(
UserHistory.where(
action: UserHistory.actions[:change_username],
target_user_id: user.id,
acting_user_id: acting_user.id,
),
).to be_present
expect(user.reload.username).to eq(new_username)
end
it "should return a JSON response with the updated username" do
put "/u/#{user.username}/preferences/username.json", params: { new_username: new_username }
expect(response.parsed_body["username"]).to eq(new_username)
end
it "should respond with proper error message if auth_overrides_username is enabled" do
SiteSetting.discourse_connect_url = "http://someurl.com"
SiteSetting.enable_discourse_connect = true
SiteSetting.auth_overrides_username = true
acting_user = admin
sign_in(acting_user)
put "/u/#{user.username}/preferences/username.json", params: { new_username: new_username }
expect(response.status).to eq(422)
expect(response.parsed_body["errors"].first).to include(
I18n.t("errors.messages.auth_overrides_username"),
)
end
end
end
describe "#check_username" do
it "raises an error without any parameters" do
get "/u/check_username.json"
expect(response.status).to eq(400)
end
shared_examples "when username is unavailable" do
it "should return available as false in the JSON and return a suggested username" do
expect(response.status).to eq(200)
expect(response.parsed_body["available"]).to eq(false)
expect(response.parsed_body["suggestion"]).to be_present
end
end
shared_examples "when username is available" do
it "should return available in the JSON" do
expect(response.status).to eq(200)
expect(response.parsed_body["available"]).to eq(true)
end
end
it "returns nothing when given an email param but no username" do
get "/u/check_username.json", params: { email: "dood@example.com" }
expect(response.status).to eq(200)
end
context "when username is available" do
before { get "/u/check_username.json", params: { username: "BruceWayne" } }
include_examples "when username is available"
end
context "when username is unavailable" do
before { get "/u/check_username.json", params: { username: user1.username } }
include_examples "when username is unavailable"
end
shared_examples "checking an invalid username" do
it "should not return an available key but should return an error message" do
expect(response.status).to eq(200)
expect(response.parsed_body["available"]).to eq(nil)
expect(response.parsed_body["errors"]).to be_present
end
end
context "when has invalid characters" do
before { get "/u/check_username.json", params: { username: "bad username" } }
include_examples "checking an invalid username"
it "should return the invalid characters message" do
expect(response.status).to eq(200)
expect(response.parsed_body["errors"]).to include(I18n.t(:"user.username.characters"))
end
end
context "when is too long" do
before do
get "/u/check_username.json",
params: {
username: SecureRandom.alphanumeric(SiteSetting.max_username_length.to_i + 1),
}
end
include_examples "checking an invalid username"
it 'should return the "too long" message' do
expect(response.status).to eq(200)
expect(response.parsed_body["errors"]).to include(
I18n.t(:"user.username.long", count: SiteSetting.max_username_length),
)
end
end
describe "different case of existing username" do
context "when it's my username" do
fab!(:user) { Fabricate(:user, username: "hansolo") }
before do
sign_in(user)
get "/u/check_username.json", params: { username: "HanSolo" }
end
include_examples "when username is available"
end
context "when it's someone else's username" do
fab!(:user) { Fabricate(:user, username: "hansolo") }
fab!(:someone_else) { Fabricate(:user) }
before do
sign_in(someone_else)
get "/u/check_username.json", params: { username: "HanSolo" }
end
include_examples "when username is unavailable"
end
context "when an admin changing it for someone else" do
fab!(:user) { Fabricate(:user, username: "hansolo") }
before do
sign_in(admin)
get "/u/check_username.json", params: { username: "HanSolo", for_user_id: user.id }
end
include_examples "when username is available"
end
end
end
describe "#check_email" do
it "returns success if hide_email_address_taken is true" do
SiteSetting.hide_email_address_taken = true
get "/u/check_email.json", params: { email: user1.email }
expect(response.parsed_body["success"]).to be_present
end
it "returns success if email is empty" do
get "/u/check_email.json"
expect(response.parsed_body["success"]).to be_present
end
it "returns failure if email is not valid" do
get "/u/check_email.json", params: { email: "invalid" }
expect(response.parsed_body["failed"]).to be_present
end
it "returns failure if email exists" do
get "/u/check_email.json", params: { email: user1.email }
expect(response.parsed_body["failed"]).to be_present
get "/u/check_email.json", params: { email: user1.email.upcase }
expect(response.parsed_body["failed"]).to be_present
end
it "returns success if email does not exists" do
get "/u/check_email.json", params: { email: "available@example.com" }
expect(response.parsed_body["success"]).to be_present
end
it "return success if user email is taken by staged user" do
get "/u/check_email.json", params: { email: Fabricate(:staged).email }
expect(response.parsed_body["success"]).to be_present
end
end
describe "#invited" do
it "fails for anonymous users" do
get "/u/#{user1.username}/invited.json", params: { username: user1.username }
expect(response.status).to eq(403)
end
it "returns success" do
user = Fabricate(:user, trust_level: TrustLevel[2])
Fabricate(:invite, invited_by: user)
sign_in(user)
get "/u/#{user.username}/invited.json", params: { username: user.username }
expect(response.status).to eq(200)
expect(response.parsed_body["counts"]["pending"]).to eq(1)
expect(response.parsed_body["counts"]["total"]).to eq(1)
end
it "filters by all if viewing self" do
inviter = Fabricate(:user, trust_level: TrustLevel[2])
sign_in(inviter)
Fabricate(:invite, email: "billybob@example.com", invited_by: inviter)
redeemed_invite = Fabricate(:invite, email: "jimtom@example.com", invited_by: inviter)
Fabricate(:invited_user, invite: redeemed_invite, user: invitee)
get "/u/#{inviter.username}/invited.json", params: { filter: "pending", search: "billybob" }
expect(response.status).to eq(200)
invites = response.parsed_body["invites"]
expect(invites.size).to eq(1)
expect(invites.first).to include("email" => "billybob@example.com")
get "/u/#{inviter.username}/invited.json",
params: {
filter: "redeemed",
search: invitee.username,
}
expect(response.status).to eq(200)
invites = response.parsed_body["invites"]
expect(invites.size).to eq(1)
expect(invites[0]["user"]).to be_present
end
it "doesn't filter by email if another regular user" do
inviter = Fabricate(:user, trust_level: TrustLevel[2])
sign_in(Fabricate(:user, trust_level: TrustLevel[2]))
Fabricate(:invite, email: "billybob@example.com", invited_by: inviter)
redeemed_invite = Fabricate(:invite, email: "jimtom@example.com", invited_by: inviter)
Fabricate(:invited_user, invite: redeemed_invite, user: invitee)
get "/u/#{inviter.username}/invited.json", params: { filter: "pending", search: "billybob" }
expect(response.status).to eq(200)
invites = response.parsed_body["invites"]
expect(invites.size).to eq(0)
get "/u/#{inviter.username}/invited.json",
params: {
filter: "redeemed",
search: invitee.username,
}
expect(response.status).to eq(200)
invites = response.parsed_body["invites"]
expect(invites.size).to eq(1)
expect(invites[0]["user"]).to be_present
end
it "filters by email if staff" do
inviter = Fabricate(:user, trust_level: 2)
sign_in(moderator)
invite_1 = Fabricate(:invite, email: "billybob@example.com", invited_by: inviter)
invitee_1 = Fabricate(:user)
Fabricate(:invited_user, invite: invite_1, user: invitee_1)
invite_2 = Fabricate(:invite, email: "jimtom@example.com", invited_by: inviter)
invitee_2 = Fabricate(:user)
Fabricate(:invited_user, invite: invite_2, user: invitee_2)
get "/u/#{inviter.username}/invited.json", params: { search: "billybob" }
expect(response.status).to eq(200)
invites = response.parsed_body["invites"]
expect(invites.size).to eq(1)
expect(invites[0]["user"]).to include("id" => invitee_1.id)
end
context "with guest" do
context "with pending invites" do
it "does not return invites" do
Fabricate(:invite, invited_by: inviter)
get "/u/#{user1.username}/invited/pending.json"
expect(response.status).to eq(403)
end
end
context "with redeemed invites" do
it "returns invited_users" do
inviter = Fabricate(:user, trust_level: TrustLevel[2])
sign_in(inviter)
invite = Fabricate(:invite, invited_by: inviter)
_invited_user = Fabricate(:invited_user, invite: invite, user: invitee)
get "/u/#{inviter.username}/invited.json"
expect(response.status).to eq(200)
invites = response.parsed_body["invites"]
expect(invites.size).to eq(1)
expect(invites[0]).to include("id" => invite.id)
end
end
end
context "with authenticated user" do
context "with pending invites" do
context "with permission to see pending invites" do
it "returns invites" do
inviter = Fabricate(:user, trust_level: TrustLevel[2])
invite = Fabricate(:invite, invited_by: inviter)
sign_in(inviter)
get "/u/#{inviter.username}/invited/pending.json"
expect(response.status).to eq(200)
invites = response.parsed_body["invites"]
expect(invites.size).to eq(1)
expect(invites.first).to include("email" => invite.email)
expect(response.parsed_body["can_see_invite_details"]).to eq(true)
end
end
context "without permission to see pending invites" do
it "does not return invites" do
user = sign_in(Fabricate(:user))
Fabricate(:invite, invited_by: inviter)
stub_guardian(user) do |guardian|
guardian.stubs(:can_see_invite_details?).with(inviter).returns(false)
end
get "/u/#{inviter.username}/invited/pending.json"
expect(response.status).to eq(422)
end
end
context "with permission to see invite links" do
it "returns own invites" do
inviter = sign_in(Fabricate(:user, trust_level: TrustLevel[2]))
invite =
Fabricate(
:invite,
invited_by: inviter,
email: nil,
max_redemptions_allowed: 5,
expires_at: 1.month.from_now,
emailed_status: Invite.emailed_status_types[:not_required],
)
get "/u/#{inviter.username}/invited/pending.json"
expect(response.status).to eq(200)
invites = response.parsed_body["invites"]
expect(invites.size).to eq(1)
expect(invites.first).to include("id" => invite.id)
expect(response.parsed_body["can_see_invite_details"]).to eq(true)
end
it "allows admin to see invites" do
inviter = Fabricate(:user, trust_level: 2)
_admin = sign_in(Fabricate(:admin))
invite =
Fabricate(
:invite,
invited_by: inviter,
email: nil,
max_redemptions_allowed: 5,
expires_at: 1.month.from_now,
emailed_status: Invite.emailed_status_types[:not_required],
)
get "/u/#{inviter.username}/invited/pending.json"
expect(response.status).to eq(200)
invites = response.parsed_body["invites"]
expect(invites.size).to eq(1)
expect(invites.first).to include("id" => invite.id)
expect(response.parsed_body["can_see_invite_details"]).to eq(true)
end
end
context "without permission to see invite links" do
it "does not return invites" do
_user = Fabricate(:user, trust_level: 2)
inviter = admin
Fabricate(
:invite,
invited_by: inviter,
email: nil,
max_redemptions_allowed: 5,
expires_at: 1.month.from_now,
emailed_status: Invite.emailed_status_types[:not_required],
)
get "/u/#{inviter.username}/invited/pending.json"
expect(response.status).to eq(403)
end
end
end
context "with redeemed invites" do
it "returns invites" do
sign_in(moderator)
invite = Fabricate(:invite, invited_by: inviter)
Fabricate(:invited_user, invite: invite, user: invitee)
get "/u/#{inviter.username}/invited.json"
expect(response.status).to eq(200)
invites = response.parsed_body["invites"]
expect(invites.size).to eq(1)
expect(invites[0]).to include("id" => invite.id)
end
end
end
end
describe "#update" do
context "with guest" do
it "raises an error" do
put "/u/guest.json"
expect(response.status).to eq(403)
end
end
it "does not allow name to be updated if auth auth_overrides_name is enabled" do
SiteSetting.auth_overrides_name = true
sign_in(user1)
put "/u/#{user1.username}", params: { name: "test.test" }
expect(response.status).to eq(200)
expect(user1.reload.name).to_not eq("test.test")
end
context "when username contains a period" do
before { sign_in(user) }
fab!(:user) { Fabricate(:user, username: "test.test", name: "Test User") }
it "should be able to update a user" do
put "/u/#{user.username}", params: { name: "test.test" }
expect(response.status).to eq(200)
expect(user.reload.name).to eq("test.test")
end
end
context "as a staff user" do
context "with uneditable field" do
fab!(:user_field) { Fabricate(:user_field, editable: false) }
it "allows staff to edit the field" do
sign_in(admin)
put "/u/#{user.username}.json",
params: {
name: "Jim Tom",
title: "foobar",
user_fields: {
user_field.id.to_s => "happy",
},
}
expect(response.status).to eq(200)
user.reload
expect(user.user_fields[user_field.id.to_s]).to eq("happy")
expect(user.title).to eq("foobar")
end
end
end
context "with authenticated user" do
context "with permission to update" do
fab!(:upload)
fab!(:user) { Fabricate(:user, refresh_auto_groups: true) }
before do
User.set_callback(:create, :after, :ensure_in_trust_level_group)
sign_in(user)
end
after { User.skip_callback(:create, :after, :ensure_in_trust_level_group) }
it "allows the update" do
SiteSetting.tagging_enabled = true
user2 = Fabricate(:user)
user3 = Fabricate(:user)
tags = [Fabricate(:tag), Fabricate(:tag)]
tag_synonym = Fabricate(:tag, target_tag: tags[1])
put "/u/#{user.username}.json",
params: {
name: "Jim Tom",
muted_usernames: "#{user2.username},#{user3.username}",
watched_tags: "#{tags[0].name},#{tag_synonym.name}",
card_background_upload_url: upload.url,
profile_background_upload_url: upload.url,
}
expect(response.status).to eq(200)
expect(response.parsed_body["user"]["watched_tags"].count).to eq(2)
user.reload
expect(user.name).to eq "Jim Tom"
expect(user.muted_users.pluck(:username).sort).to eq [user2.username, user3.username].sort
expect(
TagUser.where(
user: user,
notification_level: TagUser.notification_levels[:watching],
).pluck(:tag_id),
).to contain_exactly(tags[0].id, tags[1].id)
theme = Fabricate(:theme, user_selectable: true)
put "/u/#{user.username}.json",
params: {
muted_usernames: "",
theme_ids: [theme.id],
email_level: UserOption.email_level_types[:always],
}
user.reload
expect(user.muted_users.pluck(:username).sort).to be_empty
expect(user.user_option.theme_ids).to eq([theme.id])
expect(user.user_option.email_level).to eq(UserOption.email_level_types[:always])
expect(user.profile_background_upload).to eq(upload)
expect(user.card_background_upload).to eq(upload)
end
it "does not allow updating attributes specific to user creation" do
put "/u/#{user.username}.json",
params: {
username: "jimtom2",
email: "newemail@example.com",
password: "123456789",
}
expect(response.status).to eq(200)
user.reload
expect(user.username).not_to eq "jimtop2"
expect(user.password).not_to eq "123456789"
expect(user.email).not_to eq "newemail@example.com"
end
it "updates watched tags in everyone tag group" do
SiteSetting.tagging_enabled = true
tags = [Fabricate(:tag), Fabricate(:tag)]
group =
Fabricate(:group, name: "group", mentionable_level: Group::ALIAS_LEVELS[:everyone])
tag_group = Fabricate(:tag_group, tags: tags)
Fabricate(:tag_group_permission, tag_group: tag_group, group: group)
tag_synonym = Fabricate(:tag, target_tag: tags[1])
put "/u/#{user.username}.json",
params: {
watched_tags: "#{tags[0].name},#{tag_synonym.name}",
}
expect(response.status).to eq(200)
expect(response.parsed_body["user"]["watched_tags"].count).to eq(2)
end
context "when a locale is chosen that differs from I18n.locale" do
before { SiteSetting.allow_user_locale = true }
it "updates the user's locale" do
I18n.locale = :fr
put "/u/#{user.username}.json", params: { locale: :fa_IR }
expect(user.reload.locale).to eq("fa_IR")
end
it "updates the title" do
BadgeGranter.enable_queue
user.update!(locale: :fr)
user.change_trust_level!(TrustLevel[4])
BadgeGranter.process_queue!
leader_title = I18n.t("badges.leader.name", locale: :fr)
put "/u/#{user.username}.json", params: { title: leader_title }
expect(user.reload.title).to eq(leader_title)
ensure
BadgeGranter.disable_queue
BadgeGranter.clear_queue!
end
end
context "with user fields" do
context "with an editable field" do
fab!(:user_field) { Fabricate(:user_field, requirement: "for_all_users") }
fab!(:optional_field) { Fabricate(:user_field, requirement: "optional") }
it "should update the user field" do
put "/u/#{user.username}.json",
params: {
name: "Jim Tom",
user_fields: {
user_field.id.to_s => "happy",
},
}
expect(response.status).to eq(200)
expect(user.user_fields[user_field.id.to_s]).to eq "happy"
end
it "cannot be updated to blank" do
put "/u/#{user.username}.json",
params: {
name: "Jim Tom",
user_fields: {
user_field.id.to_s => "",
},
}
expect(response.status).to eq(422)
expect(user.user_fields[user_field.id.to_s]).not_to eq("happy")
end
it "trims excessively large fields" do
put "/u/#{user.username}.json",
params: {
name: "Jim Tom",
user_fields: {
user_field.id.to_s => ("x" * 3000),
},
}
expect(user.user_fields[user_field.id.to_s].size).to eq(UserField.max_length)
end
it "should retain existing user fields" do
put "/u/#{user.username}.json",
params: {
name: "Jim Tom",
user_fields: {
user_field.id.to_s => "happy",
optional_field.id.to_s => "feet",
},
}
expect(response.status).to eq(200)
expect(user.user_fields[user_field.id.to_s]).to eq("happy")
expect(user.user_fields[optional_field.id.to_s]).to eq("feet")
put "/u/#{user.username}.json",
params: {
name: "Jim Tom",
user_fields: {
user_field.id.to_s => "sad",
},
}
expect(response.status).to eq(200)
user.reload
expect(user.user_fields[user_field.id.to_s]).to eq("sad")
expect(user.user_fields[optional_field.id.to_s]).to eq("feet")
end
end
context "with user_notification_schedule attributes" do
it "updates the user's notification schedule" do
params = {
user_notification_schedule: {
enabled: true,
day_0_start_time: 30,
day_0_end_time: 60,
day_1_start_time: 30,
day_1_end_time: 60,
day_2_start_time: 30,
day_2_end_time: 60,
day_3_start_time: 30,
day_3_end_time: 60,
day_4_start_time: 30,
day_4_end_time: 60,
day_5_start_time: 30,
day_5_end_time: 60,
day_6_start_time: 30,
day_6_end_time: 60,
},
}
put "/u/#{user.username}.json", params: params
user.reload
expect(user.user_notification_schedule.enabled).to eq(true)
expect(user.user_notification_schedule.day_0_start_time).to eq(30)
expect(user.user_notification_schedule.day_0_end_time).to eq(60)
expect(user.user_notification_schedule.day_6_start_time).to eq(30)
expect(user.user_notification_schedule.day_6_end_time).to eq(60)
end
end
context "with uneditable field" do
fab!(:user_field) { Fabricate(:user_field, editable: false) }
it "does not update the user field" do
put "/u/#{user.username}.json",
params: {
name: "Jim Tom",
user_fields: {
user_field.id.to_s => "happy",
},
}
expect(response.status).to eq(200)
expect(user.user_fields[user_field.id.to_s]).to be_blank
end
end
context "with custom_field" do
before do
plugin = Plugin::Instance.new
plugin.register_editable_user_custom_field :test2
plugin.register_editable_user_custom_field :test3, staff_only: true
end
after { DiscoursePluginRegistry.reset! }
it "only updates allowed user fields" do
put "/u/#{user.username}.json",
params: {
custom_fields: {
test1: :hello1,
test2: :hello2,
test3: :hello3,
},
}
expect(response.status).to eq(200)
expect(user.custom_fields["test1"]).to be_blank
expect(user.custom_fields["test2"]).to eq("hello2")
expect(user.custom_fields["test3"]).to be_blank
end
it "works alongside a user field" do
user_field = Fabricate(:user_field, editable: true)
put "/u/#{user.username}.json",
params: {
custom_fields: {
test1: :hello1,
test2: :hello2,
test3: :hello3,
},
user_fields: {
user_field.id.to_s => "happy",
},
}
expect(response.status).to eq(200)
expect(user.custom_fields["test1"]).to be_blank
expect(user.custom_fields["test2"]).to eq("hello2")
expect(user.custom_fields["test3"]).to eq(nil)
expect(user.user_fields[user_field.id.to_s]).to eq("happy")
end
it "works alongside a user field during creation" do
api_key = Fabricate(:api_key, user: admin)
user_field = Fabricate(:user_field, editable: true)
post "/u.json",
params: {
name: "Test User",
username: "testuser",
email: "user@mail.com",
password: "supersecure",
active: true,
custom_fields: {
test2: "custom field value",
},
user_fields: {
user_field.id.to_s => "user field value",
},
},
headers: {
HTTP_API_KEY: api_key.key,
}
expect(response.status).to eq(200)
u = User.find_by_email("user@mail.com")
val = u.custom_fields["user_field_#{user_field.id}"]
expect(val).to eq("user field value")
val = u.custom_fields["test2"]
expect(val).to eq("custom field value")
end
it "is secure when there are no registered editable fields" do
DiscoursePluginRegistry.reset!
put "/u/#{user.username}.json",
params: {
custom_fields: {
test1: :hello1,
test2: :hello2,
test3: :hello3,
},
}
expect(response.status).to eq(200)
expect(user.custom_fields["test1"]).to be_blank
expect(user.custom_fields["test2"]).to be_blank
expect(user.custom_fields["test3"]).to be_blank
put "/u/#{user.username}.json", params: { custom_fields: %w[arrayitem1 arrayitem2] }
expect(response.status).to eq(200)
end
it "allows staff to edit staff-editable fields" do
sign_in(admin)
put "/u/#{user.username}.json",
params: {
custom_fields: {
test1: :hello1,
test2: :hello2,
test3: :hello3,
},
}
expect(response.status).to eq(200)
expect(user.custom_fields["test1"]).to be_blank
expect(user.custom_fields["test2"]).to eq("hello2")
expect(user.custom_fields["test3"]).to eq("hello3")
end
end
end
it "returns user JSON" do
put "/u/#{user.username}.json"
json = response.parsed_body
expect(json["user"]["id"]).to eq user.id
end
context "with sidebar" do
before { SiteSetting.navigation_menu = "sidebar" }
it "does not remove category or tag sidebar section links when params are not present" do
Fabricate(:category_sidebar_section_link, user: user)
Fabricate(:tag_sidebar_section_link, user: user)
expect do
put "/u/#{user.username}.json"
expect(response.status).to eq(200)
end.to_not change { user.sidebar_section_links.count }
end
it "should allow user to remove all category sidebar section links" do
Fabricate(:category_sidebar_section_link, user: user)
expect do
put "/u/#{user.username}.json", params: { sidebar_category_ids: nil }
expect(response.status).to eq(200)
end.to change { user.sidebar_section_links.count }.from(1).to(0)
end
it "should allow user to only modify category sidebar section links for categories they have access to" do
category = Fabricate(:category)
group = Fabricate(:group)
restricted_category = Fabricate(:private_category, group: group)
category_sidebar_section_link = Fabricate(:category_sidebar_section_link, user: user)
put "/u/#{user.username}.json",
params: {
sidebar_category_ids: [category.id, restricted_category.id],
}
expect(response.status).to eq(200)
expect(user.sidebar_section_links.count).to eq(1)
expect(SidebarSectionLink.exists?(id: category_sidebar_section_link.id)).to eq(false)
sidebar_section_link = user.sidebar_section_links.first
expect(sidebar_section_link.linkable).to eq(category)
group.add(user)
expect do
put "/u/#{user.username}.json",
params: {
sidebar_category_ids: [category.id, restricted_category.id],
}
expect(response.status).to eq(200)
end.to change { user.sidebar_section_links.count }.from(1).to(2)
expect(SidebarSectionLink.exists?(user: user, linkable: restricted_category)).to eq(
true,
)
end
it "should allow user to remove all tag sidebar section links" do
SiteSetting.tagging_enabled = true
Fabricate(:tag_sidebar_section_link, user: user)
expect do
put "/u/#{user.username}.json", params: { sidebar_tag_names: nil }
expect(response.status).to eq(200)
end.to change { user.sidebar_section_links.count }.from(1).to(0)
end
it "should not allow user to add tag sidebar section links when tagging is disabled" do
SiteSetting.tagging_enabled = false
tag = Fabricate(:tag)
put "/u/#{user.username}.json", params: { sidebar_tag_names: [tag.name] }
expect(response.status).to eq(200)
expect(user.reload.sidebar_section_links.count).to eq(0)
end
it "should allow user to add tag sidebar section links only for tags that are visible to the user" do
SiteSetting.tagging_enabled = true
tag = Fabricate(:tag)
tag_sidebar_section_link = Fabricate(:tag_sidebar_section_link, user: user)
hidden_tag = Fabricate(:tag)
Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name])
put "/u/#{user.username}.json",
params: {
sidebar_tag_names: [tag.name, "somerandomtag", hidden_tag.name],
}
expect(response.status).to eq(200)
expect(user.sidebar_section_links.count).to eq(1)
expect(SidebarSectionLink.exists?(id: tag_sidebar_section_link.id)).to eq(false)
sidebar_section_link = user.sidebar_section_links.first
expect(sidebar_section_link.linkable).to eq(tag)
user.update!(admin: true)
expect do
put "/u/#{user.username}.json",
params: {
sidebar_tag_names: [tag.name, "somerandomtag", hidden_tag.name],
}
expect(response.status).to eq(200)
end.to change { user.sidebar_section_links.count }.from(1).to(2)
expect(SidebarSectionLink.exists?(user: user, linkable: hidden_tag)).to eq(true)
end
end
end
context "without permission to update" do
it "does not allow the update" do
user = Fabricate(:user, name: "Billy Bob")
sign_in(Fabricate(:user))
put "/u/#{user.username}.json", params: { name: "Jim Tom" }
expect(response).to be_forbidden
expect(user.reload.name).not_to eq "Jim Tom"
end
end
end
context "with external_ids" do
fab!(:api_key, refind: false) { Fabricate(:api_key, user: admin) }
let(:plugin_auth_provider) do
authenticator_class =
Class.new(Auth::ManagedAuthenticator) do
def name
"pluginauth"
end
def enabled?
true
end
end
provider = Auth::AuthProvider.new
provider.authenticator = authenticator_class.new
provider
end
before do
DiscoursePluginRegistry.register_auth_provider(plugin_auth_provider)
SiteSetting.discourse_connect_url = "http://localhost"
SiteSetting.enable_discourse_connect = true
end
after { DiscoursePluginRegistry.reset! }
it "can create UserAssociatedAccount records" do
params = { external_ids: { "pluginauth" => "pluginauth_uid" } }
expect {
put "/u/#{user.username}.json", params: params, headers: { HTTP_API_KEY: api_key.key }
}.to change { UserAssociatedAccount.count }.by(1)
expect(response.status).to eq(200)
user_associated_account = UserAssociatedAccount.last
expect(user.reload.user_associated_account_ids).to contain_exactly(
user_associated_account.id,
)
expect(user_associated_account.provider_name).to eq("pluginauth")
expect(user_associated_account.provider_uid).to eq("pluginauth_uid")
expect(user_associated_account.user_id).to eq(user.id)
end
it "can destroy UserAssociatedAccount records" do
user.user_associated_accounts.create!(
provider_name: "pluginauth",
provider_uid: "pluginauth_uid",
)
params = { external_ids: { "pluginauth" => nil } }
expect {
put "/u/#{user.username}.json", params: params, headers: { HTTP_API_KEY: api_key.key }
}.to change { UserAssociatedAccount.count }.by(-1)
expect(response.status).to eq(200)
expect(user.reload.user_associated_account_ids).to be_blank
end
it "can create SingleSignOnRecord records" do
params = { external_ids: { discourse_connect: "discourse_connect_uid" } }
expect {
put "/u/#{user.username}.json", params: params, headers: { HTTP_API_KEY: api_key.key }
}.to change { SingleSignOnRecord.count }.by(1)
expect(response.status).to eq(200)
single_sign_on_record = SingleSignOnRecord.last
expect(user.reload.single_sign_on_record).to eq(single_sign_on_record)
expect(single_sign_on_record.external_id).to eq("discourse_connect_uid")
end
it "can update SingleSignOnRecord records" do
user = Fabricate(:user)
SingleSignOnRecord.create!(
user_id: user.id,
external_id: "discourse_connect_uid",
last_payload: "discourse_connect_uid",
)
params = { external_ids: { discourse_connect: "discourse_connect_uid_2" } }
expect {
put "/u/#{user.username}.json", params: params, headers: { HTTP_API_KEY: api_key.key }
}.not_to change { SingleSignOnRecord.count }
expect(response.status).to eq(200)
expect(user.reload.single_sign_on_record.external_id).to eq("discourse_connect_uid_2")
end
it "can delete SingleSignOnRecord records" do
user = Fabricate(:user)
SingleSignOnRecord.create!(
user_id: user.id,
external_id: "discourse_connect_uid",
last_payload: "discourse_connect_uid",
)
params = { external_ids: { discourse_connect: nil } }
expect {
put "/u/#{user.username}.json", params: params, headers: { HTTP_API_KEY: api_key.key }
}.to change { SingleSignOnRecord.count }.by(-1)
expect(response.status).to eq(200)
expect(user.reload.single_sign_on_record).to be_blank
end
it "can update SingleSignOnRecord and UserAssociatedAccount records in a single call" do
user = Fabricate(:user)
user.user_associated_accounts.create!(
provider_name: "pluginauth",
provider_uid: "pluginauth_uid",
)
SingleSignOnRecord.create!(
user_id: user.id,
external_id: "discourse_connect_uid",
last_payload: "discourse_connect_uid",
)
params = {
external_ids: {
discourse_connect: "discourse_connect_uid_2",
pluginauth: "pluginauth_uid_2",
},
}
expect {
put "/u/#{user.username}.json", params: params, headers: { HTTP_API_KEY: api_key.key }
}.to change { SingleSignOnRecord.count + UserAssociatedAccount.count }.by(0)
expect(response.status).to eq(200)
expect(user.reload.single_sign_on_record.external_id).to eq("discourse_connect_uid_2")
user_associated_account = UserAssociatedAccount.last
expect(user.reload.user_associated_account_ids).to contain_exactly(
user_associated_account.id,
)
expect(user_associated_account.provider_name).to eq("pluginauth")
expect(user_associated_account.provider_uid).to eq("pluginauth_uid_2")
expect(user_associated_account.user_id).to eq(user.id)
end
it "returns error if external ID provider does not exist" do
params = { external_ids: { "pluginauth2" => "pluginauth_uid" } }
put "/u/#{user.username}.json", params: params, headers: { HTTP_API_KEY: api_key.key }
expect(response.status).to eq(400)
end
end
context "with user status" do
context "as a regular user" do
before do
SiteSetting.enable_user_status = true
sign_in(user)
end
it "sets user status" do
status = { emoji: "tooth", description: "off to dentist" }
put "/u/#{user.username}.json", params: { status: status }
expect(response.status).to eq(200)
user.reload
expect(user.user_status).not_to be_nil
expect(user.user_status.emoji).to eq(status[:emoji])
expect(user.user_status.description).to eq(status[:description])
end
it "updates user status" do
user.set_status!("off to dentist", "tooth")
user.reload
new_status = { emoji: "surfing_man", description: "surfing" }
put "/u/#{user.username}.json", params: { status: new_status }
expect(response.status).to eq(200)
user.reload
expect(user.user_status).not_to be_nil
expect(user.user_status.emoji).to eq(new_status[:emoji])
expect(user.user_status.description).to eq(new_status[:description])
end
it "clears user status" do
user.set_status!("off to dentist", "tooth")
user.reload
put "/u/#{user.username}.json", params: { status: nil }
expect(response.status).to eq(200)
user.reload
expect(user.user_status).to be_nil
end
it "can't set status of another user" do
put "/u/#{user1.username}.json",
params: {
status: {
emoji: "tooth",
description: "off to dentist",
},
}
expect(response.status).to eq(403)
user1.reload
expect(user1.user_status).to be_nil
end
it "can't update status of another user" do
old_status = { emoji: "tooth", description: "off to dentist" }
user1.set_status!(old_status[:description], old_status[:emoji])
user1.reload
new_status = { emoji: "surfing_man", description: "surfing" }
put "/u/#{user1.username}.json", params: { status: new_status }
expect(response.status).to eq(403)
user1.reload
expect(user1.user_status).not_to be_nil
expect(user1.user_status.emoji).to eq(old_status[:emoji])
expect(user1.user_status.description).to eq(old_status[:description])
end
it "can't clear status of another user" do
user1.set_status!("off to dentist", "tooth")
user1.reload
put "/u/#{user1.username}.json", params: { status: nil }
expect(response.status).to eq(403)
user1.reload
expect(user1.user_status).not_to be_nil
end
it "doesn't clear user status if it wasn't sent in the payload" do
new_status = { emoji: "tooth", description: "off to dentist" }
user.set_status!(new_status[:description], new_status[:emoji])
user.reload
put "/u/#{user.username}.json", params: { bio_raw: "new bio" }
expect(response.status).to eq(200)
user.reload
expect(user.user_status).not_to be_nil
expect(user.user_status.emoji).to eq(new_status[:emoji])
expect(user.user_status.description).to eq(new_status[:description])
end
context "when user status is disabled" do
before { SiteSetting.enable_user_status = false }
it "doesn't set user status" do
put "/u/#{user.username}.json",
params: {
status: {
emoji: "tooth",
description: "off to dentist",
},
}
expect(response.status).to eq(200)
user.reload
expect(user.user_status).to be_nil
end
it "doesn't update user status" do
old_status = { emoji: "tooth", description: "off to dentist" }
user.set_status!(old_status[:description], old_status[:emoji])
user.reload
new_status = { emoji: "surfing_man", description: "surfing" }
put "/u/#{user.username}.json", params: { status: new_status }
expect(response.status).to eq(200)
user.reload
expect(user.user_status).not_to be_nil
expect(user.user_status.emoji).to eq(old_status[:emoji])
expect(user.user_status.description).to eq(old_status[:description])
end
it "doesn't clear user status" do
user.set_status!("off to dentist", "tooth")
user.reload
put "/u/#{user.username}.json", params: { status: nil }
expect(response.status).to eq(200)
user.reload
expect(user.user_status).not_to be_nil
end
end
end
context "as a staff user" do
before do
SiteSetting.enable_user_status = true
sign_in(moderator)
end
it "sets another user's status" do
status = { emoji: "tooth", description: "off to dentist" }
put "/u/#{user.username}.json", params: { status: status }
expect(response.status).to eq(200)
user.reload
expect(user.user_status).not_to be_nil
expect(user.user_status.emoji).to eq(status[:emoji])
expect(user.user_status.description).to eq(status[:description])
end
it "updates another user's status" do
user.set_status!("off to dentist", "tooth")
user.reload
new_status = { emoji: "surfing_man", description: "surfing" }
put "/u/#{user.username}.json", params: { status: new_status }
expect(response.status).to eq(200)
user.reload
expect(user.user_status).not_to be_nil
expect(user.user_status.emoji).to eq(new_status[:emoji])
expect(user.user_status.description).to eq(new_status[:description])
end
it "clears another user's status" do
user.set_status!("off to dentist", "tooth")
user.reload
put "/u/#{user.username}.json", params: { status: nil }
expect(response.status).to eq(200)
user.reload
expect(user.user_status).to be_nil
end
end
end
context "when a plugin introduces a users_controller_update_user_params modifier" do
before { sign_in(user) }
after { DiscoursePluginRegistry.clear_modifiers! }
it "allows the plugin to modify the user params" do
block_called = false
plugin = Plugin::Instance.new
plugin.register_modifier(
:users_controller_update_user_params,
) do |result, current_user, params|
block_called = true
expect(current_user.id).to eq(user.id)
result[:location] = params[:plugin_location_alias]
result
end
put "/u/#{user.username}.json", params: { location: "abc", plugin_location_alias: "xyz" }
expect(response.status).to eq(200)
expect(user.reload.user_profile.location).to eq("xyz")
expect(block_called).to eq(true)
end
end
end
describe "#badge_title" do
fab!(:badge)
let(:user_badge) { BadgeGranter.grant(badge, user1) }
it "sets the user's title to the badge name if it is titleable" do
sign_in(user1)
put "/u/#{user1.username}/preferences/badge_title.json",
params: {
user_badge_id: user_badge.id,
}
expect(user1.reload.title).not_to eq(badge.display_name)
badge.update allow_title: true
put "/u/#{user1.username}/preferences/badge_title.json",
params: {
user_badge_id: user_badge.id,
}
expect(user1.reload.title).to eq(badge.display_name)
expect(user1.user_profile.granted_title_badge_id).to eq(badge.id)
badge.update allow_title: false
put "/u/#{user1.username}/preferences/badge_title.json",
params: {
user_badge_id: user_badge.id,
}
user1.reload
user1.user_profile.reload
expect(user1.title).to eq("")
expect(user1.user_profile.granted_title_badge_id).to eq(nil)
end
it "is not raising an erroring when user revokes title" do
sign_in(user1)
badge.update allow_title: true
put "/u/#{user1.username}/preferences/badge_title.json",
params: {
user_badge_id: user_badge.id,
}
put "/u/#{user1.username}/preferences/badge_title.json", params: { user_badge_id: 0 }
expect(response.status).to eq(200)
end
context "with overridden name" do
fab!(:badge) { Fabricate(:badge, name: "Demogorgon", allow_title: true) }
let(:user_badge) { BadgeGranter.grant(badge, user1) }
before do
I18n.backend.store_translations(:en, { badges: { demogorgon: { name: "D'Artagnan" } } })
TranslationOverride.upsert!("en", "badges.demogorgon.name", "Boss")
end
after { TranslationOverride.revert!("en", ["badges.demogorgon.name"]) }
it "uses the badge display name as user title" do
sign_in(user1)
put "/u/#{user1.username}/preferences/badge_title.json",
params: {
user_badge_id: user_badge.id,
}
expect(user1.reload.title).to eq(badge.display_name)
end
end
end
describe "#send_activation_email" do
before do
UsersController.any_instance.stubs(:honeypot_value).returns(nil)
UsersController.any_instance.stubs(:challenge_value).returns(nil)
end
let(:post_user) do
post "/u.json",
params: {
username: "osamatest",
password: "strongpassword",
email: "dsdsds@sasa.com",
}
User.find_by(username: "osamatest")
end
context "for an existing user" do
context "for an activated account with email confirmed" do
it "fails" do
user = post_user
email_token = Fabricate(:email_token, user: user).token
EmailToken.confirm(email_token)
post "/u/action/send_activation_email.json", params: { username: user.username }
expect(response.status).to eq(409)
expect(response.parsed_body["errors"]).to include(I18n.t("activation.activated"))
expect(session[SessionController::ACTIVATE_USER_KEY]).to eq(nil)
end
end
context "for an activated account with unconfirmed email" do
it "should send an email" do
user = post_user
user.update!(active: true)
Fabricate(:email_token, user: user)
expect_enqueued_with(
job: :critical_user_email,
args: {
type: :signup,
to_address: user.email,
},
) { post "/u/action/send_activation_email.json", params: { username: user.username } }
expect(response.status).to eq(200)
expect(session[SessionController::ACTIVATE_USER_KEY]).to eq(nil)
end
end
context "when approval is enabled" do
before { SiteSetting.must_approve_users = true }
it "should raise an error" do
user = post_user
user.update(active: true)
user.save!
Fabricate(:email_token, user: user1)
post "/u/action/send_activation_email.json", params: { username: user.username }
expect(response.status).to eq(403)
end
end
describe "when user does not have a valid session" do
it "should not be valid" do
post "/u/action/send_activation_email.json", params: { username: user.username }
expect(response.status).to eq(403)
end
it "should allow staff regardless" do
sign_in(admin)
user = Fabricate(:user, active: false)
post "/u/action/send_activation_email.json", params: { username: user.username }
expect(response.status).to eq(200)
end
end
context "with a valid email_token" do
it "should send the activation email" do
user = post_user
expect_enqueued_with(job: :critical_user_email, args: { type: :signup }) do
post "/u/action/send_activation_email.json", params: { username: user.username }
end
expect(response.status).to eq(200)
expect(session[SessionController::ACTIVATE_USER_KEY]).to eq(nil)
end
end
context "without an existing email_token" do
let(:user) { post_user }
before do
user.email_tokens.each { |t| t.destroy }
user.reload
end
it "should generate a new token" do
expect {
post "/u/action/send_activation_email.json", params: { username: user.username }
}.to change { user.reload.email_tokens.count }.by(1)
end
it "should send an email" do
expect do
post "/u/action/send_activation_email.json", params: { username: user.username }
end.to change { Jobs::CriticalUserEmail.jobs.size }.by(1)
expect(session[SessionController::ACTIVATE_USER_KEY]).to eq(nil)
end
end
end
context "when username does not exist" do
it "should not send an email" do
post "/u/action/send_activation_email.json", params: { username: "nopenopenopenope" }
expect(response.status).to eq(404)
expect(Jobs::CriticalUserEmail.jobs.size).to eq(0)
end
end
end
describe "#pick_avatar" do
it "raises an error when not logged in" do
put "/u/asdf/preferences/avatar/pick.json", params: { avatar_id: 1, type: "custom" }
expect(response.status).to eq(403)
end
context "while logged in" do
before { sign_in(user1) }
fab!(:upload) { Fabricate(:upload, user: user1) }
it "raises an error when you don't have permission to toggle the avatar" do
put "/u/#{another_user.username}/preferences/avatar/pick.json",
params: {
upload_id: upload.id,
type: "custom",
}
expect(response).to be_forbidden
end
it "raises an error when discourse_connect_overrides_avatar is disabled" do
SiteSetting.discourse_connect_overrides_avatar = true
put "/u/#{user1.username}/preferences/avatar/pick.json",
params: {
upload_id: upload.id,
type: "custom",
}
expect(response.status).to eq(422)
end
it "raises an error when selecting the custom/uploaded avatar and uploaded_avatars_allowed_groups is disabled" do
SiteSetting.uploaded_avatars_allowed_groups = ""
put "/u/#{user1.username}/preferences/avatar/pick.json",
params: {
upload_id: upload.id,
type: "custom",
}
expect(response.status).to eq(422)
end
it "raises an error when selecting the custom/uploaded avatar and uploaded_avatars_allowed_groups is admin" do
SiteSetting.uploaded_avatars_allowed_groups = "1"
put "/u/#{user1.username}/preferences/avatar/pick.json",
params: {
upload_id: upload.id,
type: "custom",
}
expect(response.status).to eq(422)
user1.update!(admin: true)
Group.refresh_automatic_groups!
put "/u/#{user1.username}/preferences/avatar/pick.json",
params: {
upload_id: upload.id,
type: "custom",
}
expect(response.status).to eq(200)
end
it "raises an error when selecting the custom/uploaded avatar and uploaded_avatars_allowed_groups is staff" do
SiteSetting.uploaded_avatars_allowed_groups = "3"
put "/u/#{user1.username}/preferences/avatar/pick.json",
params: {
upload_id: upload.id,
type: "custom",
}
expect(response.status).to eq(422)
user1.update!(moderator: true)
Group.refresh_automatic_groups!
put "/u/#{user1.username}/preferences/avatar/pick.json",
params: {
upload_id: upload.id,
type: "custom",
}
expect(response.status).to eq(200)
end
it "raises an error when selecting the custom/uploaded avatar and uploaded_avatars_allowed_groups is a trust level" do
SiteSetting.uploaded_avatars_allowed_groups = "13"
put "/u/#{user1.username}/preferences/avatar/pick.json",
params: {
upload_id: upload.id,
type: "custom",
}
expect(response.status).to eq(422)
user1.change_trust_level!(TrustLevel[3])
put "/u/#{user1.username}/preferences/avatar/pick.json",
params: {
upload_id: upload.id,
type: "custom",
}
expect(response.status).to eq(200)
end
it "ignores the upload if picking a system avatar" do
SiteSetting.uploaded_avatars_allowed_groups = ""
another_upload = Fabricate(:upload)
put "/u/#{user1.username}/preferences/avatar/pick.json",
params: {
upload_id: another_upload.id,
type: "system",
}
expect(response.status).to eq(200)
expect(user1.reload.uploaded_avatar_id).to eq(nil)
end
it "raises an error if the type is invalid" do
SiteSetting.uploaded_avatars_allowed_groups = ""
another_upload = Fabricate(:upload)
put "/u/#{user1.username}/preferences/avatar/pick.json",
params: {
upload_id: another_upload.id,
type: "x",
}
expect(response.status).to eq(422)
end
it "can successfully pick the system avatar" do
put "/u/#{user1.username}/preferences/avatar/pick.json"
expect(response.status).to eq(200)
expect(user1.reload.uploaded_avatar_id).to eq(nil)
end
it "disables the use_site_small_logo_as_system_avatar setting when picking an avatar for the system user" do
system_user = Discourse.system_user
SiteSetting.use_site_small_logo_as_system_avatar = true
another_upload = Fabricate(:upload, user: system_user)
sign_in(system_user)
put "/u/#{system_user.username}/preferences/avatar/pick.json",
params: {
upload_id: another_upload.id,
type: "uploaded",
}
expect(response.status).to eq(200)
expect(SiteSetting.use_site_small_logo_as_system_avatar).to eq(false)
end
it "can successfully pick a gravatar" do
user1.user_avatar.update_columns(gravatar_upload_id: upload.id)
put "/u/#{user1.username}/preferences/avatar/pick.json",
params: {
upload_id: upload.id,
type: "gravatar",
}
expect(response.status).to eq(200)
expect(user1.reload.uploaded_avatar_id).to eq(upload.id)
expect(user1.user_avatar.reload.gravatar_upload_id).to eq(upload.id)
end
it "can not pick uploads that were not created by user" do
upload2 = Fabricate(:upload)
put "/u/#{user1.username}/preferences/avatar/pick.json",
params: {
upload_id: upload2.id,
type: "custom",
}
expect(response.status).to eq(403)
end
it "can successfully pick a custom avatar" do
events =
DiscourseEvent.track_events do
put "/u/#{user1.username}/preferences/avatar/pick.json",
params: {
upload_id: upload.id,
type: "custom",
}
end
expect(events.map { |event| event[:event_name] }).to include(:user_updated)
expect(response.status).to eq(200)
expect(user1.reload.uploaded_avatar_id).to eq(upload.id)
expect(user1.user_avatar.reload.custom_upload_id).to eq(upload.id)
end
end
end
describe "#select_avatar" do
it "raises an error when not logged in" do
put "/u/asdf/preferences/avatar/select.json", params: { url: "https://meta.discourse.org" }
expect(response.status).to eq(403)
end
context "while logged in" do
before { sign_in(user1) }
fab!(:avatar1) { Fabricate(:upload) }
fab!(:avatar2) { Fabricate(:upload) }
let(:url) { "https://www.discourse.org" }
it "raises an error when url is blank" do
put "/u/#{user1.username}/preferences/avatar/select.json", params: { url: "" }
expect(response.status).to eq(422)
end
it "raises an error when selectable avatars is disabled" do
put "/u/#{user1.username}/preferences/avatar/select.json", params: { url: url }
expect(response.status).to eq(422)
end
context "when selectable avatars is enabled" do
before do
SiteSetting.selectable_avatars = [avatar1, avatar2]
SiteSetting.selectable_avatars_mode = "no_one"
end
it "raises an error when selectable avatars is empty" do
SiteSetting.selectable_avatars = ""
put "/u/#{user1.username}/preferences/avatar/select.json", params: { url: url }
expect(response.status).to eq(422)
end
context "when selectable avatars is properly setup" do
it "raises an error when url is not in selectable avatars list" do
put "/u/#{user1.username}/preferences/avatar/select.json", params: { url: url }
expect(response.status).to eq(422)
end
it "can successfully select an avatar" do
events =
DiscourseEvent.track_events do
put "/u/#{user1.username}/preferences/avatar/select.json",
params: {
url: avatar1.url,
}
end
expect(events.map { |event| event[:event_name] }).to include(:user_updated)
expect(response.status).to eq(200)
expect(user1.reload.uploaded_avatar_id).to eq(avatar1.id)
expect(user1.user_avatar.reload.custom_upload_id).to eq(avatar1.id)
end
it "can successfully select an avatar using a cooked URL" do
events =
DiscourseEvent.track_events do
put "/u/#{user1.username}/preferences/avatar/select.json",
params: {
url: UrlHelper.cook_url(avatar1.url),
}
end
expect(events.map { |event| event[:event_name] }).to include(:user_updated)
expect(response.status).to eq(200)
expect(user1.reload.uploaded_avatar_id).to eq(avatar1.id)
expect(user1.user_avatar.reload.custom_upload_id).to eq(avatar1.id)
end
it "disables the use_site_small_logo_as_system_avatar setting when picking an avatar for the system user" do
system_user = Discourse.system_user
SiteSetting.use_site_small_logo_as_system_avatar = true
sign_in(system_user)
put "/u/#{system_user.username}/preferences/avatar/select.json",
params: {
url: UrlHelper.cook_url(avatar1.url),
}
expect(response.status).to eq(200)
expect(SiteSetting.use_site_small_logo_as_system_avatar).to eq(false)
end
end
end
end
end
describe "#destroy_user_image" do
it "raises an error when not logged in" do
delete "/u/asdf/preferences/user_image.json", params: { type: "profile_background" }
expect(response.status).to eq(403)
end
context "while logged in" do
before { sign_in(user1) }
it 'raises an error when you don\'t have permission to clear the profile background' do
delete "/u/#{another_user.username}/preferences/user_image.json",
params: {
type: "profile_background",
}
expect(response).to be_forbidden
end
it "requires the `type` param" do
delete "/u/#{user1.username}/preferences/user_image.json"
expect(response.status).to eq(400)
end
it "only allows certain `types`" do
delete "/u/#{user1.username}/preferences/user_image.json", params: { type: "wat" }
expect(response.status).to eq(400)
end
it "can clear the profile background" do
delete "/u/#{user1.username}/preferences/user_image.json",
params: {
type: "profile_background",
}
expect(user1.reload.profile_background_upload).to eq(nil)
expect(response.status).to eq(200)
end
end
end
describe "#destroy" do
it "raises an error when not logged in" do
delete "/u/nobody.json"
expect(response.status).to eq(403)
end
context "while logged in" do
before { sign_in(user1) }
it "raises an error when you cannot delete your account" do
UserDestroyer.any_instance.expects(:destroy).never
stat = user1.user_stat
stat.post_count = 3
stat.save!
delete "/u/#{user1.username}.json"
expect(response).to be_forbidden
end
it "raises an error when you try to delete someone else's account" do
UserDestroyer.any_instance.expects(:destroy).never
delete "/u/#{another_user.username}.json"
expect(response).to be_forbidden
end
it "deletes your account when you're allowed to" do
UserDestroyer.any_instance.expects(:destroy).with(user1, anything).returns(user1)
delete "/u/#{user1.username}.json"
expect(response.status).to eq(200)
end
end
end
describe "#notification_level" do
it "raises an error when `notification_level` param is not a valid value" do
sign_in(user)
invalid_arg = "invalid"
put "/u/#{user.username}/notification_level.json", params: { notification_level: invalid_arg }
expect(response.status).to eq(422)
expect(response.parsed_body["errors"].first).to eq(
I18n.t("notification_level.invalid_value", value: invalid_arg),
)
end
end
describe "#ignore" do
it "raises an error when not logged in" do
put "/u/#{user1.username}/notification_level.json", params: { notification_level: "" }
expect(response.status).to eq(403)
end
context "while logged in" do
fab!(:user) { Fabricate(:user, trust_level: 2) }
before { sign_in(user) }
fab!(:ignored_user) { Fabricate(:ignored_user, user: user, ignored_user: another_user) }
fab!(:muted_user) { Fabricate(:muted_user, user: user, muted_user: another_user) }
context "when you can't change the notification" do
fab!(:staff_user) { admin }
it "ignoring includes a helpful error message" do
put "/u/#{staff_user.username}/notification_level.json",
params: {
notification_level: "ignore",
}
expect(response.status).to eq(422)
expect(response.parsed_body["errors"][0]).to eq(I18n.t("notification_level.ignore_error"))
end
it "muting includes a helpful error message" do
put "/u/#{staff_user.username}/notification_level.json",
params: {
notification_level: "mute",
}
expect(response.status).to eq(422)
expect(response.parsed_body["errors"][0]).to eq(I18n.t("notification_level.mute_error"))
end
end
context "when changing notification level to normal" do
it "changes notification level to normal" do
put "/u/#{another_user.username}/notification_level.json",
params: {
notification_level: "normal",
}
expect(IgnoredUser.count).to eq(0)
expect(MutedUser.count).to eq(0)
end
end
context "when changing notification level to mute" do
it "changes notification level to mute" do
put "/u/#{another_user.username}/notification_level.json",
params: {
notification_level: "mute",
}
expect(IgnoredUser.count).to eq(0)
expect(MutedUser.find_by(user_id: user.id, muted_user_id: another_user.id)).to be_present
end
end
context "when changing notification level to ignore" do
it "changes notification level to ignore" do
put "/u/#{another_user.username}/notification_level.json",
params: {
notification_level: "ignore",
expiring_at: 3.days.from_now,
}
expect(response.status).to eq(200)
expect(MutedUser.count).to eq(0)
expect(
IgnoredUser.find_by(user_id: user.id, ignored_user_id: another_user.id),
).to be_present
end
it "allows admin to change the ignore status for a source user" do
ignored_user.destroy!
sign_in(Fabricate(:user, admin: true))
put "/u/#{another_user.username}/notification_level.json",
params: {
notification_level: "ignore",
acting_user_id: user.id,
expiring_at: 3.days.from_now,
}
expect(response.status).to eq(200)
expect(
IgnoredUser.find_by(user_id: user.id, ignored_user_id: another_user.id),
).to be_present
end
it "does not allow a regular user to change the ignore status for anyone but themself" do
ignored_user.destroy!
acting_user = Fabricate(:user)
put "/u/#{another_user.username}/notification_level.json",
params: {
notification_level: "ignore",
acting_user_id: acting_user.id,
expiring_at: 3.days.from_now,
}
expect(response.status).to eq(422)
expect(
IgnoredUser.find_by(user_id: acting_user.id, ignored_user_id: another_user.id),
).to eq(nil)
put "/u/#{another_user.username}/notification_level.json",
params: {
notification_level: "ignore",
expiring_at: 3.days.from_now,
}
expect(response.status).to eq(200)
expect(
IgnoredUser.find_by(user_id: user.id, ignored_user_id: another_user.id),
).to be_present
end
context "when expiring_at param is set" do
it "changes notification level to ignore" do
freeze_time do
expiring_at = 3.days.from_now
put "/u/#{another_user.username}/notification_level.json",
params: {
notification_level: "ignore",
expiring_at: expiring_at,
}
ignored_user = IgnoredUser.find_by(user_id: user.id, ignored_user_id: another_user.id)
expect(ignored_user).to be_present
expect(ignored_user.expiring_at.to_i).to eq(expiring_at.to_i)
expect(MutedUser.count).to eq(0)
end
end
end
end
end
end
describe "for user with period in username" do
fab!(:user_with_period) { Fabricate(:user, username: "myname.test") }
it "still works" do
sign_in(user_with_period)
UserDestroyer
.any_instance
.expects(:destroy)
.with(user_with_period, anything)
.returns(user_with_period)
delete "/u/#{user_with_period.username}", xhr: true
expect(response.status).to eq(200)
end
end
describe "#my_redirect" do
it "redirects if the user is not logged in" do
get "/my/wat"
expect(response).to redirect_to("/login-preferences")
expect(response.cookies).to have_key("destination_url")
expect(response.cookies["destination_url"]).to eq("/my/wat")
expect(response.headers["X-Robots-Tag"]).to eq("noindex")
end
context "when the user is logged in" do
before { sign_in(user1) }
it "will not redirect to an invalid path" do
get "/my/wat/..password.txt"
expect(response).not_to be_redirect
end
it "will redirect to an valid path" do
get "/my/preferences"
expect(response).to redirect_to("/u/#{user1.username}/preferences")
end
it "permits forward slashes" do
get "/my/activity/posts"
expect(response).to redirect_to("/u/#{user1.username}/activity/posts")
end
it "correctly redirects for Unicode usernames" do
SiteSetting.unicode_usernames = true
user = sign_in(Fabricate(:unicode_user))
get "/my/preferences"
expect(response).to redirect_to("/u/#{user.encoded_username}/preferences")
end
end
end
describe "#check_emails" do
it "raises an error when not logged in" do
get "/u/zogstrip/emails.json"
expect(response.status).to eq(403)
end
context "while logged in" do
let(:sign_in_admin) { sign_in(admin) }
it "raises an error when you aren't allowed to check emails" do
sign_in(Fabricate(:user))
get "/u/#{Fabricate(:user).username}/emails.json"
expect(response).to be_forbidden
end
it "returns emails and associated_accounts for self" do
Fabricate(:email_change_request, user: user1)
sign_in(user)
get "/u/#{user.username}/emails.json"
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["email"]).to eq(user.email)
expect(json["secondary_emails"]).to eq(user.secondary_emails)
expect(json["unconfirmed_emails"]).to eq(user.unconfirmed_emails)
expect(json["associated_accounts"]).to eq([])
end
it "returns emails and associated_accounts when you're allowed to see them" do
Fabricate(:email_change_request, user: user1)
sign_in_admin
get "/u/#{user.username}/emails.json"
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["email"]).to eq(user.email)
expect(json["secondary_emails"]).to eq(user.secondary_emails)
expect(json["unconfirmed_emails"]).to eq(user.unconfirmed_emails)
expect(json["associated_accounts"]).to eq([])
end
it "works on inactive users" do
inactive_user = Fabricate(:user, active: false)
Fabricate(:email_change_request, user: inactive_user)
sign_in_admin
get "/u/#{inactive_user.username}/emails.json"
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["email"]).to eq(inactive_user.email)
expect(json["secondary_emails"]).to eq(inactive_user.secondary_emails)
expect(json["unconfirmed_emails"]).to eq(inactive_user.unconfirmed_emails)
expect(json["associated_accounts"]).to eq([])
end
end
end
describe "#check_sso_email" do
it "raises an error when not logged in" do
get "/u/zogstrip/sso-email.json"
expect(response.status).to eq(403)
end
context "while logged in" do
let(:sign_in_admin) { sign_in(admin) }
it "raises an error when you aren't allowed to check sso email" do
sign_in(Fabricate(:user))
get "/u/#{user1.username}/sso-email.json"
expect(response).to be_forbidden
end
it "returns emails and associated_accounts when you're allowed to see them" do
user1.single_sign_on_record =
SingleSignOnRecord.create(
user_id: user1.id,
external_email: "foobar@example.com",
external_id: "example",
last_payload: "looks good",
)
sign_in_admin
get "/u/#{user1.username}/sso-email.json"
expect(response.status).to eq(200)
expect(response.parsed_body["email"]).to eq("foobar@example.com")
end
end
end
describe "#check_sso_payload" do
it "raises an error when not logged in" do
get "/u/zogstrip/sso-payload.json"
expect(response.status).to eq(403)
end
context "while logged in" do
let(:sign_in_admin) { sign_in(admin) }
it "raises an error when you aren't allowed to check sso payload" do
sign_in(Fabricate(:user))
get "/u/#{user1.username}/sso-payload.json"
expect(response).to be_forbidden
end
it "returns SSO payload when you're allowed to see" do
user1.single_sign_on_record =
SingleSignOnRecord.create(
user_id: user1.id,
external_email: "foobar@example.com",
external_id: "example",
last_payload: "foobar",
)
sign_in_admin
get "/u/#{user1.username}/sso-payload.json"
expect(response.status).to eq(200)
expect(response.parsed_body["payload"]).to eq("foobar")
end
end
end
describe "#update_primary_email" do
let(:user_email) { user1.primary_email }
fab!(:other_email) { Fabricate(:secondary_email, user: user1) }
before do
SiteSetting.email_editable = true
sign_in(user1)
end
it "changes user's primary email" do
put "/u/#{user1.username}/preferences/primary-email.json", params: { email: user_email.email }
expect(response.status).to eq(200)
expect(user_email.reload.primary).to eq(true)
expect(other_email.reload.primary).to eq(false)
event =
DiscourseEvent
.track_events do
expect {
put "/u/#{user1.username}/preferences/primary-email.json",
params: {
email: other_email.email,
}
}.to change {
UserHistory.where(
action: UserHistory.actions[:update_email],
acting_user_id: user1.id,
).count
}.by(1)
end
.last
expect(response.status).to eq(200)
expect(user_email.reload.primary).to eq(false)
expect(other_email.reload.primary).to eq(true)
expect(event[:event_name]).to eq(:user_updated)
expect(event[:params].first).to eq(user1)
end
end
describe "#destroy_email" do
fab!(:user_email) { user1.primary_email }
fab!(:other_email) { Fabricate(:secondary_email, user: user1) }
before do
SiteSetting.email_editable = true
sign_in(user1)
end
it "can destroy secondary emails" do
delete "/u/#{user1.username}/preferences/email.json", params: { email: user_email.email }
expect(response.status).to eq(428)
expect(user1.reload.user_emails.pluck(:email)).to contain_exactly(
user_email.email,
other_email.email,
)
event =
DiscourseEvent
.track_events do
expect {
delete "/u/#{user1.username}/preferences/email.json",
params: {
email: other_email.email,
}
}.to change {
UserHistory.where(
action: UserHistory.actions[:destroy_email],
acting_user_id: user1.id,
).count
}.by(1)
end
.last
expect(response.status).to eq(200)
expect(user1.reload.user_emails.pluck(:email)).to contain_exactly(user_email.email)
expect(event[:event_name]).to eq(:user_updated)
expect(event[:params].first).to eq(user1)
end
it "can destroy unconfirmed emails" do
request_1 =
EmailChangeRequest.create!(
user: user1,
new_email: user_email.email,
change_state: EmailChangeRequest.states[:authorizing_new],
)
EmailChangeRequest.create!(
user: user1,
new_email: other_email.email,
change_state: EmailChangeRequest.states[:authorizing_new],
)
EmailChangeRequest.create!(
user: user1,
new_email: other_email.email,
change_state: EmailChangeRequest.states[:authorizing_new],
)
delete "/u/#{user1.username}/preferences/email.json", params: { email: other_email.email }
expect(user1.user_emails.pluck(:email)).to contain_exactly(
user_email.email,
other_email.email,
)
expect(user1.email_change_requests).to contain_exactly(request_1)
end
it "destroys associated email tokens and email change requests" do
new_email = "new.n.cool@example.com"
updater = EmailUpdater.new(guardian: user1.guardian, user: user1)
updater.change_to(new_email)
email_token = updater.change_req.new_email_token
expect(email_token).to be_present
delete "/u/#{user1.username}/preferences/email.json", params: { email: new_email }
expect(EmailToken.find_by(id: email_token.id)).to eq(nil)
expect(EmailChangeRequest.find_by(id: updater.change_req.id)).to eq(nil)
end
end
describe "#topic_tracking_state" do
context "when anon" do
it "raises an error on anon for topic_tracking_state" do
get "/u/#{user1.username}/topic-tracking-state.json"
expect(response.status).to eq(403)
end
end
context "when logged on" do
it "detects new topic" do
sign_in(user1)
topic = Fabricate(:topic)
get "/u/#{user1.username}/topic-tracking-state.json"
expect(response.status).to eq(200)
states = response.parsed_body
expect(states[0]["topic_id"]).to eq(topic.id)
end
end
end
describe "#summary" do
before do
user.user_stat.update!(post_count: 1)
user1.user_stat.update!(post_count: 1)
user_deferred.user_stat.update!(post_count: 1)
end
it "generates summary info" do
create_post(user: user)
get "/u/#{user.username_lower}/summary.json"
expect(response.headers["X-Robots-Tag"]).to eq("noindex")
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["user_summary"]["topic_count"]).to eq(1)
expect(json["user_summary"]["post_count"]).to eq(1)
end
context "when `hide_user_profiles_from_public` site setting is enabled" do
before { SiteSetting.hide_user_profiles_from_public = true }
it "returns 200 for logged in users" do
sign_in(Fabricate(:user))
get "/u/#{user.username_lower}/summary.json"
expect(response.status).to eq(200)
end
it "returns 403 for anonymous users" do
get "/u/#{user.username_lower}/summary.json"
expect(response.status).to eq(403)
end
end
context "when `hide_profile` user option is checked" do
before_all { user1.user_option.update_columns(hide_profile: true) }
it "returns 404" do
get "/u/#{user1.username_lower}/summary.json"
expect(response.status).to eq(404)
end
it "returns summary info if `allow_users_to_hide_profile` is false" do
SiteSetting.allow_users_to_hide_profile = false
get "/u/#{user1.username_lower}/summary.json"
expect(response.status).to eq(200)
end
end
context "with avatar flair in Most... sections" do
it "returns data for automatic groups flair" do
liker = Fabricate(:user, admin: true, moderator: true, trust_level: 1)
create_and_like_post(user_deferred, liker)
get "/u/#{user_deferred.username_lower}/summary.json"
json = response.parsed_body
expect(json["user_summary"]["most_liked_by_users"][0]["admin"]).to eq(true)
expect(json["user_summary"]["most_liked_by_users"][0]["moderator"]).to eq(true)
expect(json["user_summary"]["most_liked_by_users"][0]["trust_level"]).to eq(1)
end
it "returns data for flair when an icon is used" do
group =
Fabricate(
:group,
name: "Groupie",
flair_bg_color: "#111111",
flair_color: "#999999",
flair_icon: "icon",
)
liker = Fabricate(:user, flair_group: group, refresh_auto_groups: true)
create_and_like_post(user_deferred, liker)
get "/u/#{user_deferred.username_lower}/summary.json"
json = response.parsed_body
expect(json["user_summary"]["most_liked_by_users"][0]["flair_name"]).to eq("Groupie")
expect(json["user_summary"]["most_liked_by_users"][0]["flair_url"]).to eq("icon")
expect(json["user_summary"]["most_liked_by_users"][0]["flair_bg_color"]).to eq("#111111")
expect(json["user_summary"]["most_liked_by_users"][0]["flair_color"]).to eq("#999999")
end
it "returns data for flair when an image is used" do
upload = Fabricate(:upload)
group = Fabricate(:group, name: "Groupie", flair_bg_color: "#111111", flair_upload: upload)
liker = Fabricate(:user, flair_group: group)
create_and_like_post(user_deferred, liker)
get "/u/#{user_deferred.username_lower}/summary.json"
json = response.parsed_body
expect(json["user_summary"]["most_liked_by_users"][0]["flair_name"]).to eq("Groupie")
expect(json["user_summary"]["most_liked_by_users"][0]["flair_url"]).to eq(upload.url)
expect(json["user_summary"]["most_liked_by_users"][0]["flair_bg_color"]).to eq("#111111")
end
def create_and_like_post(likee, liker)
UserActionManager.enable
post = create_post(user: likee)
PostActionCreator.like(liker, post)
end
end
end
describe "#confirm_admin" do
it "fails without a valid token" do
get "/u/confirm-admin/invalid-token.json"
expect(response).not_to be_successful
end
it "fails with a missing token" do
get "/u/confirm-admin/a0a0a0a0a0.json"
expect(response).to_not be_successful
end
it "succeeds with a valid code as anonymous" do
ac = AdminConfirmation.new(user1, admin)
ac.create_confirmation
get "/u/confirm-admin/#{ac.token}.json"
expect(response.status).to eq(200)
user1.reload
expect(user1.admin?).to eq(false)
end
it "succeeds with a valid code when logged in as that user" do
sign_in(admin)
ac = AdminConfirmation.new(user1, admin)
ac.create_confirmation
get "/u/confirm-admin/#{ac.token}.json", params: { token: ac.token }
expect(response.status).to eq(200)
user1.reload
expect(user1.admin?).to eq(false)
end
it "fails if you're logged in as a different account" do
sign_in(admin)
ac = AdminConfirmation.new(user1, Fabricate(:admin))
ac.create_confirmation
get "/u/confirm-admin/#{ac.token}.json"
expect(response).to_not be_successful
user1.reload
expect(user1.admin?).to eq(false)
end
describe "post" do
it "gives the user admin access when POSTed" do
ac = AdminConfirmation.new(user1, admin)
ac.create_confirmation
post "/u/confirm-admin/#{ac.token}.json"
expect(response.status).to eq(200)
user1.reload
expect(user1.admin?).to eq(true)
end
end
end
describe "#update_activation_email" do
before do
UsersController.any_instance.stubs(:honeypot_value).returns(nil)
UsersController.any_instance.stubs(:challenge_value).returns(nil)
end
let(:post_user) do
post "/u.json",
params: {
username: "osamatest",
password: "strongpassword",
email: "osama@example.com",
}
user = User.where(username: "osamatest").first
user.active = false
user.save!
user
end
context "with a session variable" do
it "raises an error with an invalid session value" do
post_user
post "/u.json",
params: {
username: "osamatest2",
password: "strongpassword2",
email: "osama22@example.com",
}
user = User.where(username: "osamatest2").first
user.destroy
put "/u/update-activation-email.json", params: { email: "osamaupdated@example.com" }
expect(response.status).to eq(403)
end
it "raises an error for an active user" do
user = post_user
user.update(active: true)
user.save!
put "/u/update-activation-email.json", params: { email: "osama@example.com" }
expect(response.status).to eq(403)
end
it "raises an error when logged in" do
sign_in(moderator)
post_user
put "/u/update-activation-email.json", params: { email: "updatedemail@example.com" }
expect(response.status).to eq(403)
end
it "raises an error when the new email is taken" do
active_user = Fabricate(:user)
post_user
put "/u/update-activation-email.json", params: { email: active_user.email }
expect(response.status).to eq(422)
end
it "raises an error when the email is blocklisted" do
post_user
SiteSetting.blocked_email_domains = "example.com"
put "/u/update-activation-email.json", params: { email: "test@example.com" }
expect(response.status).to eq(422)
end
it "can be updated" do
user = post_user
token = user.email_tokens.first
put "/u/update-activation-email.json", params: { email: "updatedemail@example.com" }
expect(response.status).to eq(200)
user.reload
expect(user.email).to eq("updatedemail@example.com")
expect(
user.email_tokens.where(email: "updatedemail@example.com", expired: false),
).to be_present
expect(EmailToken.find_by(id: token.id)).to eq(nil)
end
it "tells the user to slow down after many requests" do
RateLimiter.enable
freeze_time
user = post_user
token = user.email_tokens.first
6.times do |n|
put "/u/update-activation-email.json",
params: {
email: "updatedemail#{n}@example.com",
},
env: {
REMOTE_ADDR: "1.2.3.#{n}",
}
end
expect(response.status).to eq(429)
end
end
context "with a username and password" do
it "raises an error with an invalid username" do
put "/u/update-activation-email.json",
params: {
username: "eviltrout",
password: "invalid-password",
email: "updatedemail@example.com",
}
expect(response.status).to eq(403)
end
it "raises an error with an invalid password" do
put "/u/update-activation-email.json",
params: {
username: inactive_user.username,
password: "invalid-password",
email: "updatedemail@example.com",
}
expect(response.status).to eq(403)
end
it "raises an error for an active user" do
put "/u/update-activation-email.json",
params: {
username: Fabricate(:walter_white).username,
password: "letscook",
email: "updatedemail@example.com",
}
expect(response.status).to eq(403)
end
it "raises an error when logged in" do
sign_in(moderator)
put "/u/update-activation-email.json",
params: {
username: inactive_user.username,
password: "qwerqwer123",
email: "updatedemail@example.com",
}
expect(response.status).to eq(403)
end
it "raises an error when the new email is taken" do
put "/u/update-activation-email.json",
params: {
username: inactive_user.username,
password: "qwerqwer123",
email: user.email,
}
expect(response.status).to eq(422)
end
it "can be updated" do
user = inactive_user
token = user.email_tokens.first
put "/u/update-activation-email.json",
params: {
username: user.username,
password: "qwerqwer123",
email: "updatedemail@example.com",
}
expect(response.status).to eq(200)
user.reload
expect(user.email).to eq("updatedemail@example.com")
expect(
user.email_tokens.where(email: "updatedemail@example.com", expired: false),
).to be_present
expect(EmailToken.find_by(id: token.id)).to eq(nil)
end
it "tells the user to slow down after many requests" do
RateLimiter.enable
freeze_time
user = inactive_user
token = user.email_tokens.first
6.times do |n|
put "/u/update-activation-email.json",
params: {
username: user.username,
password: "qwerqwer123",
email: "updatedemail#{n}@example.com",
},
env: {
REMOTE_ADDR: "1.2.3.#{n}",
}
end
expect(response.status).to eq(429)
end
end
end
describe "#show" do
context "when anon" do
let(:user) { Discourse.system_user }
it "returns success" do
get "/u/#{user.username}.json"
expect(response.status).to eq(200)
parsed = response.parsed_body["user"]
expect(parsed["username"]).to eq(user.username)
expect(parsed["profile_hidden"]).to be_blank
expect(parsed["trust_level"]).to be_present
expect(response.headers["X-Robots-Tag"]).to eq("noindex")
end
it "returns a hidden profile" do
user.user_option.update_column(:hide_profile, true)
get "/u/#{user.username}.json"
expect(response.status).to eq(200)
parsed = response.parsed_body["user"]
expect(parsed["username"]).to eq(user.username)
expect(parsed["profile_hidden"]).to eq(true)
expect(parsed["trust_level"]).to be_blank
expect(response.headers["X-Robots-Tag"]).to eq("noindex")
end
it "should 403 for anonymous user when profiles are hidden" do
SiteSetting.hide_user_profiles_from_public = true
get "/u/#{user.username}.json"
expect(response.headers["X-Robots-Tag"]).to eq("noindex")
expect(response).to have_http_status(:forbidden)
get "/u/#{user.username}/messages.json"
expect(response).to have_http_status(:forbidden)
end
it "should 403 correctly for crawlers when profiles are hidden" do
SiteSetting.hide_user_profiles_from_public = true
get "/u/#{user.username}", headers: { "User-Agent" => "Googlebot" }
expect(response).to have_http_status(:forbidden)
expect(response.body).to have_tag("body.crawler")
expect(response.headers["X-Robots-Tag"]).to eq("noindex")
end
describe "user profile views" do
it "should track a user profile view for an anon user" do
get "/"
UserProfileView.expects(:add).with(another_user.user_profile.id, request.remote_ip, nil)
get "/u/#{another_user.username}.json"
end
it "skips tracking" do
UserProfileView.expects(:add).never
get "/u/#{user.username}.json", params: { skip_track_visit: true }
end
end
end
context "when logged in" do
before { sign_in(user1) }
it "returns success" do
get "/u/#{user1.username}.json"
expect(response.status).to eq(200)
expect(response.headers["X-Robots-Tag"]).to eq("noindex")
json = response.parsed_body
expect(json["user"]["has_title_badges"]).to eq(false)
end
it "returns not found when the username doesn't exist" do
get "/u/madeuppity.json"
expect(response).not_to be_successful
expect(response.headers["X-Robots-Tag"]).to eq("noindex")
end
it "returns not found when the user is inactive" do
inactive = Fabricate(:user, active: false)
get "/u/#{inactive.username}.json"
expect(response).not_to be_successful
expect(response.headers["X-Robots-Tag"]).to eq("noindex")
end
it "returns success when show_inactive_accounts is true and user is logged in" do
SiteSetting.show_inactive_accounts = true
inactive = Fabricate(:user, active: false)
get "/u/#{inactive.username}.json"
expect(response.status).to eq(200)
end
it "raises an error on invalid access" do
Guardian.any_instance.expects(:can_see?).with(user1).returns(false)
get "/u/#{user1.username}.json"
expect(response).to be_forbidden
expect(response.headers["X-Robots-Tag"]).to eq("noindex")
end
describe "user profile views" do
it "should track a user profile view for a signed in user" do
UserProfileView.expects(:add).with(
another_user.user_profile.id,
request.remote_ip,
user1.id,
)
get "/u/#{another_user.username}.json"
end
it "should not track a user profile view for a user viewing his own profile" do
UserProfileView.expects(:add).never
get "/u/#{user1.username}.json"
end
it "skips tracking" do
UserProfileView.expects(:add).never
get "/u/#{user1.username}.json", params: { skip_track_visit: true }
end
end
context "when fetching a user by external_id" do
before { user1.create_single_sign_on_record(external_id: "997", last_payload: "") }
it "returns fetch for a matching external_id" do
get "/u/by-external/997.json"
expect(response.status).to eq(200)
expect(response.parsed_body["user"]["username"]).to eq(user1.username)
end
it "returns not found when external_id doesn't match" do
get "/u/by-external/99.json"
expect(response).not_to be_successful
end
context "for an external provider" do
before do
sign_in(admin)
SiteSetting.enable_google_oauth2_logins = true
UserAssociatedAccount.create!(
user: user1,
provider_uid: "myuid",
provider_name: "google_oauth2",
)
end
it "doesn't work for non-admin" do
sign_in(user1)
get "/u/by-external/google_oauth2/myuid.json"
expect(response.status).to eq(403)
end
it "can fetch the user" do
get "/u/by-external/google_oauth2/myuid.json"
expect(response.status).to eq(200)
expect(response.parsed_body["user"]["username"]).to eq(user1.username)
end
it "fails for disabled provider" do
SiteSetting.enable_google_oauth2_logins = false
get "/u/by-external/google_oauth2/myuid.json"
expect(response.status).to eq(404)
end
it "returns 404 for missing user" do
get "/u/by-external/google_oauth2/myotheruid.json"
expect(response.status).to eq(404)
end
end
end
describe "include_post_count_for" do
fab!(:topic)
before_all do
Fabricate(:post, user: user1, topic: topic)
Fabricate(:post, user: admin, topic: topic)
Fabricate(:post, user: admin, topic: topic, post_type: Post.types[:whisper])
end
it "includes only visible posts" do
get "/u/#{admin.username}.json", params: { include_post_count_for: topic.id }
topic_post_count = response.parsed_body.dig("user", "topic_post_count")
expect(topic_post_count[topic.id.to_s]).to eq(1)
end
it "doesn't include the post count when the signed in user doesn't have access" do
c = Fabricate(:category, read_restricted: true)
topic.update(category_id: c.id)
expect(Guardian.new(user1).can_see?(topic)).to eq(false)
get "/u/#{admin.username}.json", params: { include_post_count_for: topic.id }
topic_post_count = response.parsed_body.dig("user", "topic_post_count")
expect(topic_post_count).to eq(nil)
end
it "includes all post types for staff members" do
SiteSetting.whispers_allowed_groups = "#{Group::AUTO_GROUPS[:staff]}"
sign_in(admin)
get "/u/#{admin.username}.json", params: { include_post_count_for: topic.id }
topic_post_count = response.parsed_body.dig("user", "topic_post_count")
expect(topic_post_count[topic.id.to_s]).to eq(2)
end
end
end
it "should be able to view a user" do
get "/u/#{user1.username}"
expect(response.status).to eq(200)
expect(response.body).to include(user1.username)
end
it "should not be able to view a private user profile" do
user1.user_profile.update!(bio_raw: "Hello world!")
user1.user_option.update!(hide_profile: true)
get "/u/#{user1.username}"
expect(response.status).to eq(200)
expect(response.body).not_to include("Hello world!")
end
describe "when username contains a period" do
before_all { user1.update!(username: "test.test") }
it "should be able to view a user" do
get "/u/#{user1.username}"
expect(response.status).to eq(200)
expect(response.body).to include(user1.username)
end
end
end
describe "#show_card" do
context "when anon" do
let(:user) { Discourse.system_user }
it "returns success" do
get "/u/#{user.username}/card.json"
expect(response.status).to eq(200)
parsed = response.parsed_body["user"]
expect(parsed["username"]).to eq(user.username)
expect(parsed["profile_hidden"]).to be_blank
expect(parsed["trust_level"]).to be_present
end
it "should have http status 403 for anonymous user when profiles are hidden" do
SiteSetting.hide_user_profiles_from_public = true
get "/u/#{user.username}/card.json"
expect(response).to have_http_status(:forbidden)
end
end
context "when logged in" do
before { sign_in(user1) }
it "works correctly" do
get "/u/#{user.username}/card.json"
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["user"]["associated_accounts"]).to eq(nil) # Not serialized in card
expect(json["user"]["username"]).to eq(user.username)
end
it "returns not found when the username doesn't exist" do
get "/u/madeuppity/card.json"
expect(response).not_to be_successful
end
it "returns partial response when inactive user" do
user.update!(active: false)
get "/u/#{user.username}/card.json"
expect(response).to be_successful
expect(response.parsed_body["user"]["inactive"]).to eq(true)
end
it "raises an error on invalid access" do
Guardian.any_instance.expects(:can_see?).with(user).returns(false)
get "/u/#{user.username}/card.json"
expect(response).to be_forbidden
end
context "when hidden users" do
before { user.user_option.update!(hide_profile: true) }
it "returns the correct partial response when the user has messages enabled" do
user.user_option.update!(allow_private_messages: true)
get "/u/#{user.username}/card.json"
expect(response).to be_successful
expect(response.parsed_body["user"]["profile_hidden"]).to eq(true)
expect(response.parsed_body["user"]["can_send_private_message_to_user"]).to eq(true)
end
it "returns the correct partial response when the user has messages disabled" do
user.user_option.update!(allow_private_messages: false)
get "/u/#{user.username}/card.json"
expect(response).to be_successful
expect(response.parsed_body["user"]["profile_hidden"]).to eq(true)
expect(response.parsed_body["user"]["can_send_private_message_to_user"]).to eq(false)
end
end
end
end
describe "#cards" do
fab!(:user) { Discourse.system_user }
fab!(:user2) { Fabricate(:user) }
before { user2.user_stat.update!(post_count: 1) }
it "returns success" do
get "/user-cards.json?user_ids=#{user.id},#{user2.id}"
expect(response.status).to eq(200)
parsed = response.parsed_body["users"]
expect(parsed.map { |u| u["username"] }).to contain_exactly(user.username, user2.username)
end
it "should have http status 403 for anonymous user when profiles are hidden" do
SiteSetting.hide_user_profiles_from_public = true
get "/user-cards.json?user_ids=#{user.id},#{user2.id}"
expect(response).to have_http_status(:forbidden)
end
context "when `hide_profile` user option is checked" do
before { user2.user_option.update_columns(hide_profile: true) }
it "does not include hidden profiles" do
get "/user-cards.json?user_ids=#{user.id},#{user2.id}"
expect(response.status).to eq(200)
parsed = response.parsed_body["users"]
expect(parsed.map { |u| u["username"] }).to contain_exactly(user.username)
end
it "does include hidden profiles when `allow_users_to_hide_profile` is false" do
SiteSetting.allow_users_to_hide_profile = false
get "/user-cards.json?user_ids=#{user.id},#{user2.id}"
expect(response.status).to eq(200)
parsed = response.parsed_body["users"]
expect(parsed.map { |u| u["username"] }).to contain_exactly(user.username, user2.username)
end
end
end
describe "#badges" do
it "renders fine by default" do
get "/u/#{user1.username}/badges"
expect(response.status).to eq(200)
end
it "fails if badges are disabled" do
SiteSetting.enable_badges = false
get "/u/#{user1.username}/badges"
expect(response.status).to eq(404)
end
end
describe "#account_created" do
it "returns a message when no session is present" do
get "/u/account-created"
expect(response.status).to eq(200)
body = response.body
expect(body).to match(I18n.t("activation.missing_session"))
end
it "redirects when the user is logged in" do
sign_in(user1)
get "/u/account-created"
expect(response).to redirect_to("/")
end
context "when cookies contains a destination URL" do
it "should redirect to the URL" do
sign_in(user1)
destination_url = "http://thisisasite.com/somepath"
cookies[:destination_url] = destination_url
get "/u/account-created"
expect(response).to redirect_to(destination_url)
end
end
context "when the user account is created" do
include ApplicationHelper
it "returns the message when set in the session" do
user1 = create_user
get "/u/account-created"
expect(response.status).to eq(200)
expect(response.body).to have_tag("div#data-preloaded") do |element|
json = JSON.parse(element.current_scope.attribute("data-preloaded").value)
expect(json["accountCreated"]).to include(
"{\"message\":\"#{I18n.t("login.activate_email", email: user1.email).gsub!("</", "<\\/")}\",\"show_controls\":true,\"username\":\"#{user1.username}\",\"email\":\"#{user1.email}\"}",
)
end
end
end
end
describe "#search_users" do
fab!(:topic)
let(:user) { Fabricate :user, username: "joecabot", name: "Lawrence Tierney" }
let(:post1) { Fabricate(:post, user: user, topic: topic) }
let(:staged_user) { Fabricate(:user, staged: true) }
before do
SearchIndexer.enable
post1
end
it "searches when provided the term only" do
get "/u/search/users.json", params: { term: user.name.split(" ").last }
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["users"].map { |u| u["username"] }).to include(user.username)
end
context "when searching usernames" do
it "searches when provided a list of usernames" do
users = Fabricate.times(3, :user)
get "/u/search/users.json", params: { usernames: users.map(&:username).join(",") }
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["users"].map { |u| u["username"] }).to match_array(users.map(&:username))
end
it "searches groups if include_groups = true" do
users = Fabricate.times(3, :user)
group = Fabricate(:group)
sign_in(user)
get "/u/search/users.json",
params: {
usernames: [group.name, users.first.username].join(","),
include_groups: true,
}
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["users"].map { |u| u["username"] }).to contain_exactly(users.first.username)
expect(json["groups"].map { |u| u["name"] }).to contain_exactly(group.name)
end
end
it "searches when provided the topic only" do
get "/u/search/users.json", params: { topic_id: topic.id }
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["users"].map { |u| u["username"] }).to include(user.username)
end
it "searches when provided the term and topic" do
get "/u/search/users.json", params: { term: user.name.split(" ").last, topic_id: topic.id }
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["users"].map { |u| u["username"] }).to include(user.username)
end
it "searches only for users who have access to private topic" do
searching_user = Fabricate(:user)
privileged_user =
Fabricate(:user, trust_level: 4, username: "joecabit", name: "Lawrence Tierney")
privileged_group = Fabricate(:group)
privileged_group.add(searching_user)
privileged_group.add(privileged_user)
privileged_group.save
category = Fabricate(:category)
category.set_permissions(privileged_group => :readonly)
category.save
private_topic = Fabricate(:topic, category: category)
sign_in(searching_user)
get "/u/search/users.json",
params: {
term: user.name.split(" ").last,
topic_id: private_topic.id,
topic_allowed_users: "true",
}
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["users"].map { |u| u["username"] }).to_not include(user.username)
expect(json["users"].map { |u| u["username"] }).to include(privileged_user.username)
end
it "interprets blank category id correctly" do
pm_topic = Fabricate(:private_message_post).topic
sign_in(pm_topic.user)
get "/u/search/users.json", params: { term: "", topic_id: pm_topic.id, category_id: "" }
expect(response.status).to eq(200)
end
describe "when limit params is invalid" do
include_examples "invalid limit params",
"/u/search/users.json",
described_class::SEARCH_USERS_LIMIT
end
context "when `enable_names` is true" do
before { SiteSetting.enable_names = true }
it "returns names" do
get "/u/search/users.json", params: { term: user.name }
json = response.parsed_body
expect(json["users"].map { |u| u["name"] }).to include(user.name)
end
end
context "when `enable_names` is false" do
before { SiteSetting.enable_names = false }
it "returns names" do
get "/u/search/users.json", params: { term: user.name }
json = response.parsed_body
expect(json["users"].map { |u| u["name"] }).not_to include(user.name)
end
end
context "with groups" do
fab!(:mentionable_group) do
Fabricate(
:group,
mentionable_level: Group::ALIAS_LEVELS[:everyone],
messageable_level: Group::ALIAS_LEVELS[:nobody],
visibility_level: Group.visibility_levels[:public],
name: "aaa1bbb",
)
end
fab!(:mentionable_group_2) do
Fabricate(
:group,
mentionable_level: Group::ALIAS_LEVELS[:everyone],
messageable_level: Group::ALIAS_LEVELS[:nobody],
visibility_level: Group.visibility_levels[:logged_on_users],
name: "bbb1aaa",
)
end
fab!(:messageable_group) do
Fabricate(
:group,
mentionable_level: Group::ALIAS_LEVELS[:nobody],
messageable_level: Group::ALIAS_LEVELS[:everyone],
visibility_level: Group.visibility_levels[:logged_on_users],
name: "ccc1aaa",
)
end
fab!(:private_group) do
Fabricate(
:group,
mentionable_level: Group::ALIAS_LEVELS[:members_mods_and_admins],
messageable_level: Group::ALIAS_LEVELS[:members_mods_and_admins],
visibility_level: Group.visibility_levels[:members],
name: "ddd1aaa",
)
end
describe "when signed in" do
before { sign_in(user) }
it "correctly sorts on prefix" do
get "/u/search/users.json", params: { include_groups: "true", term: "bbb" }
expect(response.status).to eq(200)
groups = response.parsed_body["groups"]
expect(groups.map { |g| g["name"] }).to eq(%w[bbb1aaa aaa1bbb])
end
it "does not search for groups if there is no term" do
get "/u/search/users.json", params: { include_groups: "true" }
expect(response.status).to eq(200)
groups = response.parsed_body["groups"]
expect(groups).to eq(nil)
end
it "only returns visible groups" do
get "/u/search/users.json", params: { include_groups: "true", term: "a" }
expect(response.status).to eq(200)
groups = response.parsed_body["groups"]
expect(groups.map { |group| group["name"] }).to_not include(private_group.name)
end
it "allows plugins to register custom groups filter" do
get "/u/search/users.json", params: { include_groups: "true", term: "a" }
expect(response.status).to eq(200)
groups = response.parsed_body["groups"]
expect(groups.count).to eq(6)
plugin = Plugin::Instance.new
plugin.register_groups_callback_for_users_search_controller_action(
:admins_filter,
) { |original_groups, user| original_groups.where(name: "admins") }
get "/u/search/users.json",
params: {
include_groups: "true",
admins_filter: "true",
term: "a",
}
expect(response.status).to eq(200)
groups = response.parsed_body["groups"]
expect(groups).to eq([{ "name" => "admins", "full_name" => nil }])
DiscoursePluginRegistry.reset!
end
it "allows plugins to use apply modifiers to the groups filter" do
get "/u/search/users.json", params: { include_groups: "true", term: "a" }
expect(response.status).to eq(200)
initial_groups = response.parsed_body["groups"]
expect(initial_groups.count).to eq(6)
Plugin::Instance
.new
.register_modifier(:groups_for_users_search) do |groups|
groups.where(name: initial_groups.first["name"])
end
get "/u/search/users.json", params: { include_groups: "true", term: "a" }
expect(response.status).to eq(200)
expect(response.parsed_body["groups"].count).to eq(1)
DiscoursePluginRegistry.reset!
end
it "works when the modifier to the groups filter introduces a join with a conflicting name fields like `id` for example" do
%i[
include_groups
include_mentionable_groups
include_messageable_groups
].each do |param_name|
get "/u/search/users.json", params: { param_name => "true", :term => "a" }
expect(response.status).to eq(200)
Plugin::Instance
.new
.register_modifier(:groups_for_users_search) do |groups|
# a join with a conflicting name field (id) is introduced here
# we expect the query to work correctly
groups.left_joins(:users).where(users: { admin: true })
end
get "/u/search/users.json", params: { param_name => "true", :term => "a" }
expect(response.status).to eq(200) # the conflict would cause a 500 error
DiscoursePluginRegistry.reset!
end
end
it "doesn't search for groups" do
get "/u/search/users.json",
params: {
include_mentionable_groups: "false",
include_messageable_groups: "false",
term: "a",
}
expect(response.status).to eq(200)
expect(response.parsed_body).not_to have_key(:groups)
end
it "searches for messageable groups" do
get "/u/search/users.json",
params: {
include_mentionable_groups: "false",
include_messageable_groups: "true",
term: "a",
}
expect(response.status).to eq(200)
expect(response.parsed_body["groups"].map { |group| group["name"] }).to contain_exactly(
messageable_group.name,
Group.find(Group::AUTO_GROUPS[:moderators]).name,
)
end
it "searches for mentionable groups" do
get "/u/search/users.json",
params: {
include_messageable_groups: "false",
include_mentionable_groups: "true",
term: "a",
}
expect(response.status).to eq(200)
groups = response.parsed_body["groups"]
expect(groups.map { |group| group["name"] }).to contain_exactly(
mentionable_group.name,
mentionable_group_2.name,
)
end
end
describe "when not signed in" do
it "should not include mentionable/messageable groups" do
get "/u/search/users.json",
params: {
include_mentionable_groups: "false",
include_messageable_groups: "false",
term: "a",
}
expect(response.status).to eq(200)
expect(response.parsed_body).not_to have_key(:groups)
get "/u/search/users.json",
params: {
include_mentionable_groups: "false",
include_messageable_groups: "true",
term: "a",
}
expect(response.status).to eq(200)
expect(response.parsed_body).not_to have_key(:groups)
get "/u/search/users.json",
params: {
include_messageable_groups: "false",
include_mentionable_groups: "true",
term: "a",
}
expect(response.status).to eq(200)
expect(response.parsed_body).not_to have_key(:groups)
end
end
describe "when searching by group name" do
fab!(:exclusive_group) { Fabricate(:group) }
it "return results if the user is a group member" do
exclusive_group.add(user)
get "/u/search/users.json", params: { group: exclusive_group.name, term: user.username }
expect(users_found).to contain_exactly(user.username)
end
it "does not return results if the user is not a group member" do
get "/u/search/users.json", params: { group: exclusive_group.name, term: user.username }
expect(users_found).to be_empty
end
it "returns results if the user is member of one of the groups" do
exclusive_group.add(user)
get "/u/search/users.json",
params: {
groups: [exclusive_group.name],
term: user.username,
}
expect(users_found).to contain_exactly(user.username)
end
it "does not return results if the user is not a member of the groups" do
get "/u/search/users.json",
params: {
groups: [exclusive_group.name],
term: user.username,
}
expect(users_found).to be_empty
end
def users_found
response.parsed_body["users"].map { |u| u["username"] }
end
end
end
context "with `include_staged_users`" do
it "includes staged users when the param is true" do
get "/u/search/users.json", params: { term: staged_user.name, include_staged_users: true }
json = response.parsed_body
expect(json["users"].map { |u| u["name"] }).to include(staged_user.name)
end
it "doesn't include staged users when the param is not passed" do
get "/u/search/users.json", params: { term: staged_user.name }
json = response.parsed_body
expect(json["users"].map { |u| u["name"] }).not_to include(staged_user.name)
end
it "doesn't include staged users when the param explicitly set to false" do
get "/u/search/users.json", params: { term: staged_user.name, include_staged_users: false }
json = response.parsed_body
expect(json["users"].map { |u| u["name"] }).not_to include(staged_user.name)
end
end
context "with `last_seen_users`" do
it "returns results when the param is true" do
get "/u/search/users.json", params: { last_seen_users: true }
json = response.parsed_body
expect(json["users"]).not_to be_empty
end
it "respects limit parameter at the same time" do
limit = 3
get "/u/search/users.json", params: { last_seen_users: true, limit: limit }
json = response.parsed_body
expect(json["users"]).not_to be_empty
expect(json["users"].size).to eq(limit)
end
end
it "returns avatar_template" do
get "/u/search/users.json", params: { term: user.username }
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["users"][0]).to have_key("avatar_template")
expect(json["users"][0]["avatar_template"]).to eq(
"/letter_avatar_proxy/v4/letter/j/f475e1/{size}.png",
)
end
describe "#status" do
it "returns user status if enabled in site settings" do
SiteSetting.enable_user_status = true
emoji = "tooth"
description = "off to dentist"
user.set_status!(description, emoji)
get "/u/search/users.json", params: { term: user.name }
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["users"][0]).to have_key("status")
expect(json["users"][0]["status"]["description"]).to eq(description)
expect(json["users"][0]["status"]["emoji"]).to eq(emoji)
end
it "doesn't return user status if disabled in site settings" do
SiteSetting.enable_user_status = false
user.set_status!("off to dentist", "tooth")
get "/u/search/users.json", params: { term: user.name }
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["users"][0]).not_to have_key("status")
end
end
end
describe "#email_login" do
before { SiteSetting.enable_local_logins_via_email = true }
it "enqueues the right email" do
post "/u/email-login.json", params: { login: user1.email }
expect(response.status).to eq(200)
expect(response.parsed_body["user_found"]).to eq(true)
job_args = Jobs::CriticalUserEmail.jobs.last["args"].first
expect(job_args["user_id"]).to eq(user1.id)
expect(job_args["type"]).to eq("email_login")
expect(EmailToken.hash_token(job_args["email_token"])).to eq(
user1.email_tokens.last.token_hash,
)
end
describe "when enable_local_logins_via_email is disabled" do
before { SiteSetting.enable_local_logins_via_email = false }
it "should return the right response" do
post "/u/email-login.json", params: { login: user1.email }
expect(response.status).to eq(404)
end
end
describe "when username or email is not valid" do
it "should not enqueue the email to login" do
post "/u/email-login.json", params: { login: "@random" }
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["user_found"]).to eq(false)
expect(json["hide_taken"]).to eq(false)
expect(Jobs::CriticalUserEmail.jobs).to eq([])
end
end
describe "when hide_email_address_taken is true" do
it "should return the right response" do
SiteSetting.hide_email_address_taken = true
post "/u/email-login.json", params: { login: user1.email }
expect(response.status).to eq(200)
json = response.parsed_body
expect(json.has_key?("user_found")).to eq(false)
expect(json["hide_taken"]).to eq(true)
end
end
describe "when user is already logged in" do
it "should redirect to the root path" do
sign_in(user1)
post "/u/email-login.json", params: { login: user1.email }
expect(response).to redirect_to("/")
end
end
end
describe "#create_second_factor_totp" do
context "when not logged in" do
it "should return the right response" do
post "/users/second_factors.json", params: { password: "wrongpassword" }
expect(response.status).to eq(403)
end
end
context "when logged in" do
before { sign_in(user1) }
describe "create 2fa request" do
it "fails on incorrect password" do
ApplicationController
.any_instance
.expects(:secure_session)
.returns("confirmed-session-#{user1.id}" => "false")
post "/users/create_second_factor_totp.json"
expect(response.status).to eq(403)
end
describe "when local logins are disabled" do
it "should return the right response" do
SiteSetting.enable_local_logins = false
post "/users/create_second_factor_totp.json"
expect(response.status).to eq(404)
end
end
describe "when SSO is enabled" do
it "should return the right response" do
SiteSetting.discourse_connect_url = "http://someurl.com"
SiteSetting.enable_discourse_connect = true
post "/users/create_second_factor_totp.json"
expect(response.status).to eq(404)
end
end
it "succeeds on correct password" do
ApplicationController
.any_instance
.stubs(:secure_session)
.returns("confirmed-session-#{user1.id}" => "true")
post "/users/create_second_factor_totp.json"
expect(response.status).to eq(200)
response_body = response.parsed_body
expect(response_body["key"]).to be_present
expect(response_body["qr"]).to be_present
end
it "raises an error for a user created > 5 mins ago without a confirmed session" do
post "/users/create_second_factor_totp.json"
expect(response.status).to eq(403)
end
it "does not require confirming session for a user created < 5 mins ago" do
user1.update(created_at: Time.now.utc - 4.minutes)
post "/users/create_second_factor_totp.json"
expect(response.status).to eq(200)
end
end
end
end
describe "#enable_second_factor_totp" do
before { sign_in(user1) }
def create_totp
stub_secure_session_confirmed
post "/users/create_second_factor_totp.json"
end
it "creates a totp for the user successfully" do
create_totp
staged_totp_key = read_secure_session["staged-totp-#{user1.id}"]
token = ROTP::TOTP.new(staged_totp_key).now
post "/users/enable_second_factor_totp.json",
params: {
name: "test",
second_factor_token: token,
}
expect(response.status).to eq(200)
expect(user1.user_second_factors.count).to eq(1)
end
it "rate limits by IP address" do
RateLimiter.enable
create_totp
staged_totp_key = read_secure_session["staged-totp-#{user1.id}"]
token = ROTP::TOTP.new(staged_totp_key).now
7.times do |x|
post "/users/enable_second_factor_totp.json",
params: {
name: "test",
second_factor_token: token,
}
end
expect(response.status).to eq(429)
end
it "rate limits by username" do
RateLimiter.enable
create_totp
staged_totp_key = read_secure_session["staged-totp-#{user1.id}"]
token = ROTP::TOTP.new(staged_totp_key).now
7.times do |x|
post "/users/enable_second_factor_totp.json",
params: {
name: "test",
second_factor_token: token,
},
env: {
REMOTE_ADDR: "1.2.3.#{x}",
}
end
expect(response.status).to eq(429)
end
context "when an incorrect token is provided" do
before do
create_totp
post "/users/enable_second_factor_totp.json",
params: {
name: "test",
second_factor_token: "123456",
}
end
it "shows a helpful error message to the user" do
expect(response.parsed_body["error"]).to eq(I18n.t("login.invalid_second_factor_code"))
end
end
context "when a name is not provided" do
before do
create_totp
post "/users/enable_second_factor_totp.json", params: { second_factor_token: "123456" }
end
it "shows a helpful error message to the user" do
expect(response.parsed_body["error"]).to eq(I18n.t("login.missing_second_factor_name"))
end
end
context "when a token is not provided" do
before do
create_totp
post "/users/enable_second_factor_totp.json", params: { name: "test" }
end
it "shows a helpful error message to the user" do
expect(response.parsed_body["error"]).to eq(I18n.t("login.missing_second_factor_code"))
end
end
it "doesn't allow creating too many TOTPs" do
Fabricate(:user_second_factor_totp, user: user1)
create_totp
staged_totp_key = read_secure_session["staged-totp-#{user1.id}"]
token = ROTP::TOTP.new(staged_totp_key).now
stub_const(UserSecondFactor, "MAX_TOTPS_PER_USER", 1) do
post "/users/enable_second_factor_totp.json",
params: {
name: "test",
second_factor_token: token,
}
end
expect(response.status).to eq(422)
expect(response.parsed_body["errors"]).to include(I18n.t("login.too_many_authenticators"))
expect(user1.user_second_factors.count).to eq(1)
end
it "doesn't allow the TOTP name to exceed the limit" do
create_totp
staged_totp_key = read_secure_session["staged-totp-#{user1.id}"]
token = ROTP::TOTP.new(staged_totp_key).now
post "/users/enable_second_factor_totp.json",
params: {
name: "a" * (UserSecondFactor::MAX_NAME_LENGTH + 1),
second_factor_token: token,
}
expect(response.status).to eq(422)
expect(response.parsed_body["errors"]).to include(
"Name is too long (maximum is 300 characters)",
)
expect(user1.user_second_factors.count).to eq(0)
end
end
describe "#update_second_factor" do
fab!(:user_second_factor) { Fabricate(:user_second_factor_totp, user: user1) }
context "when not logged in" do
it "should return the right response" do
put "/users/second_factor.json"
expect(response.status).to eq(403)
end
end
context "when logged in" do
before { sign_in(user1) }
context "when user has totp setup" do
context "when token is missing" do
it "returns the right response" do
put "/users/second_factor.json",
params: {
disable: "true",
second_factor_target: UserSecondFactor.methods[:totp],
id: user_second_factor.id,
}
expect(response.status).to eq(403)
end
end
context "when token is valid" do
before { stub_secure_session_confirmed }
it "should allow second factor for the user to be renamed" do
put "/users/second_factor.json",
params: {
name: "renamed",
second_factor_target: UserSecondFactor.methods[:totp],
id: user_second_factor.id,
}
expect(response.status).to eq(200)
expect(user1.reload.user_second_factors.totps.first.name).to eq("renamed")
end
it "should allow second factor for the user to be disabled" do
put "/users/second_factor.json",
params: {
disable: "true",
second_factor_target: UserSecondFactor.methods[:totp],
id: user_second_factor.id,
}
expect(response.status).to eq(200)
expect(user1.reload.user_second_factors.totps.first).to eq(nil)
end
end
end
context "when user is updating backup codes" do
context "when token is missing" do
it "returns the right response" do
put "/users/second_factor.json",
params: {
second_factor_target: UserSecondFactor.methods[:backup_codes],
}
expect(response.status).to eq(403)
end
end
context "when token is valid" do
before do
ApplicationController
.any_instance
.stubs(:secure_session)
.returns("confirmed-session-#{user1.id}" => "true")
end
it "should allow second factor backup for the user to be disabled" do
put "/users/second_factor.json",
params: {
second_factor_target: UserSecondFactor.methods[:backup_codes],
disable: "true",
}
expect(response.status).to eq(200)
expect(user1.reload.user_second_factors.backup_codes).to be_empty
end
end
end
end
end
describe "#create_second_factor_backup" do
fab!(:user_second_factor) { Fabricate(:user_second_factor_totp, user: user1) }
context "when not logged in" do
it "should return the right response" do
put "/users/second_factors_backup.json",
params: {
second_factor_token: "wrongtoken",
second_factor_method: UserSecondFactor.methods[:totp],
}
expect(response.status).to eq(403)
end
end
context "when logged in" do
before { sign_in(user1) }
describe "create 2fa request" do
it "fails on incorrect password" do
ApplicationController
.any_instance
.expects(:secure_session)
.returns("confirmed-session-#{user1.id}" => "false")
put "/users/second_factors_backup.json"
expect(response.status).to eq(403)
end
describe "when local logins are disabled" do
it "should return the right response" do
SiteSetting.enable_local_logins = false
put "/users/second_factors_backup.json"
expect(response.status).to eq(404)
end
end
describe "when SSO is enabled" do
it "should return the right response" do
SiteSetting.discourse_connect_url = "http://someurl.com"
SiteSetting.enable_discourse_connect = true
put "/users/second_factors_backup.json"
expect(response.status).to eq(404)
end
end
it "succeeds on correct password" do
ApplicationController
.any_instance
.expects(:secure_session)
.returns("confirmed-session-#{user1.id}" => "true")
put "/users/second_factors_backup.json"
expect(response.status).to eq(200)
response_body = response.parsed_body
# we use SecureRandom.hex(16) for backup codes, ensure this continues to be the case
expect(response_body["backup_codes"].map(&:length)).to eq([32] * 10)
end
end
end
end
describe "#create_second_factor_security_key" do
it "stores the challenge in the session and returns challenge data, user id, and supported algorithms" do
create_second_factor_security_key
secure_session = read_secure_session
response_parsed = response.parsed_body
expect(response_parsed["challenge"]).to eq(DiscourseWebauthn.challenge(user1, secure_session))
expect(response_parsed["rp_id"]).to eq(DiscourseWebauthn.rp_id)
expect(response_parsed["rp_name"]).to eq(DiscourseWebauthn.rp_name)
expect(response_parsed["user_secure_id"]).to eq(
user1.reload.create_or_fetch_secure_identifier,
)
expect(response_parsed["supported_algorithms"]).to eq(
::DiscourseWebauthn::SUPPORTED_ALGORITHMS,
)
end
it "doesn't create a challenge if the user has the maximum number allowed of security keys" do
Fabricate(:user_security_key_with_random_credential, user: user1)
stub_const(UserSecurityKey, "MAX_KEYS_PER_USER", 1) { create_second_factor_security_key }
expect(response.status).to eq(422)
expect(response.parsed_body["errors"]).to include(I18n.t("login.too_many_security_keys"))
end
context "if the user has security key credentials already" do
fab!(:user_security_key) { Fabricate(:user_security_key_with_random_credential, user: user1) }
it "returns those existing active credentials" do
create_second_factor_security_key
response_parsed = response.parsed_body
expect(response_parsed["existing_active_credential_ids"]).to eq(
[user_security_key.credential_id],
)
end
end
end
describe "#register_second_factor_security_key" do
before do
simulate_localhost_webauthn_challenge
DiscourseWebauthn.stubs(:origin).returns("http://localhost:3000")
end
context "when creation parameters are valid" do
it "creates a security key for the user" do
create_second_factor_security_key
_response_parsed = response.parsed_body
post "/u/register_second_factor_security_key.json",
params: valid_security_key_create_post_data
expect(user1.security_keys.count).to eq(1)
expect(user1.security_keys.last.credential_id).to eq(
valid_security_key_create_post_data[:rawId],
)
expect(user1.security_keys.last.name).to eq(valid_security_key_create_post_data[:name])
end
it "doesn't allow creating too many security keys" do
create_second_factor_security_key
_response_parsed = response.parsed_body
Fabricate(:user_security_key_with_random_credential, user: user1)
stub_const(UserSecurityKey, "MAX_KEYS_PER_USER", 1) do
post "/u/register_second_factor_security_key.json",
params: valid_security_key_create_post_data
end
expect(response.status).to eq(422)
expect(response.parsed_body["errors"]).to include(I18n.t("login.too_many_security_keys"))
expect(user1.security_keys.count).to eq(1)
end
it "doesn't allow the security key name to exceed the limit" do
create_second_factor_security_key
_response_parsed = response.parsed_body
post "/u/register_second_factor_security_key.json",
params:
valid_security_key_create_post_data.merge(
name: "a" * (UserSecurityKey::MAX_NAME_LENGTH + 1),
)
expect(response.status).to eq(422)
expect(response.parsed_body["errors"]).to include(
"Name is too long (maximum is 300 characters)",
)
expect(user1.security_keys.count).to eq(0)
end
end
context "when the creation parameters are invalid" do
it "shows a security key error and does not create a key" do
create_second_factor_security_key
_response_parsed = response.parsed_body
post "/u/register_second_factor_security_key.json",
params: {
id: "bad id",
rawId: "bad rawId",
type: "public-key",
attestation: "bad attestation",
clientData: Base64.encode64('{"bad": "json"}'),
name: "My Bad Key",
}
expect(user1.security_keys.count).to eq(0)
expect(response.parsed_body["error"]).to eq(
I18n.t("webauthn.validation.invalid_type_error"),
)
end
end
end
describe "#disable_second_factor" do
context "when logged in with secure session" do
before do
sign_in(user1)
stub_secure_session_confirmed
end
context "when user has a registered totp and security key" do
before do
_totp_second_factor = Fabricate(:user_second_factor_totp, user: user1)
_security_key_second_factor =
Fabricate(
:user_security_key,
user: user1,
factor_type: UserSecurityKey.factor_types[:second_factor],
)
Fabricate(:passkey_with_random_credential, user: user1)
end
it "should disable all totp and security keys (but not passkeys)" do
expect_enqueued_with(
job: :critical_user_email,
args: {
type: :account_second_factor_disabled,
user_id: user1.id,
},
) do
put "/u/disable_second_factor.json"
expect(response.status).to eq(200)
expect(user1.reload.user_second_factors).to be_empty
expect(user1.second_factor_security_keys).to be_empty
expect(user1.security_keys.length).to eq(1)
expect(user1.security_keys[0].factor_type).to eq(
UserSecurityKey.factor_types[:first_factor],
)
expect(user1.passkey_credential_ids.length).to eq(1)
end
end
end
end
end
describe "#create_passkey" do
before do
SiteSetting.enable_passkeys = true
stub_secure_session_confirmed
end
it "fails if user is not logged in" do
post "/u/create_passkey.json"
expect(response.status).to eq(403)
end
it "stores the challenge in the session and returns challenge data, user id, and supported algorithms" do
sign_in(user1)
post "/u/create_passkey.json"
secure_session = read_secure_session
response_parsed = response.parsed_body
expect(response_parsed["challenge"]).to eq(DiscourseWebauthn.challenge(user1, secure_session))
expect(response_parsed["rp_id"]).to eq(DiscourseWebauthn.rp_id)
expect(response_parsed["rp_name"]).to eq(DiscourseWebauthn.rp_name)
expect(response_parsed["user_secure_id"]).to eq(user1.reload.secure_identifier)
expect(response_parsed["supported_algorithms"]).to eq(
::DiscourseWebauthn::SUPPORTED_ALGORITHMS,
)
end
context "when user has a passkey" do
fab!(:user_security_key) { Fabricate(:passkey_with_random_credential, user: user1) }
it "returns existing active credentials" do
sign_in(user1)
post "/u/create_passkey.json"
response_parsed = response.parsed_body
expect(response_parsed["existing_passkey_credential_ids"]).to eq(
[user_security_key.credential_id],
)
end
end
end
describe "#rename_passkey" do
before { SiteSetting.enable_passkeys = true }
it "fails if no user is logged in" do
put "/u/rename_passkey/NONE.json"
expect(response.status).to eq(403)
end
it "fails if no name parameter is provided" do
sign_in(user1)
put "/u/rename_passkey/ID.json"
expect(response.status).to eq(400)
expect(response.parsed_body["errors"][0]).to eq(
"param is missing or the value is empty: name",
)
end
it "fails if key is invalid" do
sign_in(user1)
put "/u/rename_passkey/ID.json", params: { name: "new name" }
expect(response.status).to eq(400)
expect(response.parsed_body["errors"][0]).to include(
"You supplied invalid parameters to the request: id",
)
end
context "with an existing passkey" do
fab!(:passkey) do
Fabricate(:passkey_with_random_credential, user: user1, name: "original name")
end
it "renames the key" do
sign_in(user1)
put "/u/rename_passkey/#{passkey.id}.json", params: { name: "new name" }
response_parsed = response.parsed_body
expect(response.status).to eq(200)
expect(passkey.reload.name).to eq("new name")
end
it "does not let an admin delete a passkey associated with user1" do
sign_in(admin)
put "/u/rename_passkey/#{passkey.id}.json", params: { name: "new name" }
expect(passkey.reload.name).to eq("original name")
end
end
end
describe "#delete_passkey" do
before { SiteSetting.enable_passkeys = true }
fab!(:passkey) { Fabricate(:passkey_with_random_credential, user: user1) }
it "fails if user does not have a confirmed session" do
sign_in(user1)
delete "/u/delete_passkey/#{passkey.id}.json"
expect(response.status).to eq(403)
end
context "with a confirmed session" do
before { stub_secure_session_confirmed }
it "fails if user is not logged in" do
delete "/u/delete_passkey/#{passkey.id}.json"
expect(response.status).to eq(403)
end
it "deletes the key" do
sign_in(user1)
delete "/u/delete_passkey/#{passkey.id}.json"
expect(response.status).to eq(200)
expect(user1.passkey_credential_ids).to eq([])
end
it "does not let an admin delete a passkey associated with user1" do
sign_in(admin)
delete "/u/delete_passkey/#{passkey.id}.json"
expect(response.status).to eq(200)
expect(user1.passkey_credential_ids[0]).to eq(passkey.credential_id)
end
end
end
describe "#register_passkey" do
before do
SiteSetting.enable_passkeys = true
DiscourseWebauthn.stubs(:origin).returns("http://localhost:3000")
end
it "fails if user is not logged in" do
stub_secure_session_confirmed
post "/u/register_passkey.json"
expect(response.status).to eq(403)
end
it "fails if session is not confirmed" do
sign_in(user1)
post "/u/register_passkey.json"
expect(response.status).to eq(403)
end
context "with a valid key" do
let(:attestation) do
"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAICRXq4sFZ9XpWZOzfJ8EguJmoEPMzNVyFMUWQfT5u1QzpQECAyYgASFYILjOiAHAwNrXkCk/tmyYRiE87QyV/15wUvhcXhr1JfwtIlggClQywgQvSxTsqV/FSK0cNHTTmuwfzzREqE6eLDmPxmI="
end
let(:valid_client_param) { passkey_client_data_param("webauthn.create") }
let(:invalid_client_param) { passkey_client_data_param("webauthn.get") }
before do
sign_in(user1)
stub_secure_session_confirmed
simulate_localhost_passkey_challenge
end
it "registers the passkey" do
post "/u/register_passkey.json",
params: {
name: "My Passkey",
attestation: attestation,
clientData: Base64.encode64(valid_client_param.to_json),
}
expect(response.status).to eq(200)
expect(response.parsed_body["name"]).to eq("My Passkey")
expect(user1.passkey_credential_ids).to eq([valid_passkey_data[:credential_id]])
end
it "does not register a passkey with the wrong webauthn type" do
post "/u/register_passkey.json",
params: {
name: "My Passkey",
attestation: attestation,
clientData: Base64.encode64(invalid_client_param.to_json),
}
expect(response.status).to eq(401)
expect(response.parsed_body["errors"][0]).to eq(
I18n.t("webauthn.validation.invalid_type_error"),
)
end
end
end
describe "#revoke_account" do
it "errors for unauthorised users" do
post "/u/#{user1.username}/preferences/revoke-account.json",
params: {
provider_name: "facebook",
}
expect(response.status).to eq(403)
sign_in(another_user)
post "/u/#{user1.username}/preferences/revoke-account.json",
params: {
provider_name: "facebook",
}
expect(response.status).to eq(403)
end
context "while logged in" do
before { sign_in(user1) }
it "returns an error when there is no matching account" do
post "/u/#{user1.username}/preferences/revoke-account.json",
params: {
provider_name: "facebook",
}
expect(response.status).to eq(404)
end
context "with fake provider" do
let(:authenticator) do
Class
.new(Auth::Authenticator) do
attr_accessor :can_revoke
def name
"testprovider"
end
def enabled?
true
end
def description_for_user(user)
"an account"
end
def can_revoke?
can_revoke
end
def revoke(user, skip_remote: false)
true
end
end
.new
end
before do
DiscoursePluginRegistry.register_auth_provider(
Auth::AuthProvider.new(authenticator: authenticator),
)
end
after { DiscoursePluginRegistry.reset! }
it "returns an error when revoking is not allowed" do
authenticator.can_revoke = false
post "/u/#{user1.username}/preferences/revoke-account.json",
params: {
provider_name: "testprovider",
}
expect(response.status).to eq(404)
authenticator.can_revoke = true
post "/u/#{user1.username}/preferences/revoke-account.json",
params: {
provider_name: "testprovider",
}
expect(response.status).to eq(200)
end
it "works" do
authenticator.can_revoke = true
post "/u/#{user1.username}/preferences/revoke-account.json",
params: {
provider_name: "testprovider",
}
expect(response.status).to eq(200)
end
end
end
end
describe "#revoke_auth_token" do
context "while logged in" do
before { 2.times { sign_in(user1) } }
it "logs user out" do
ids = user1.user_auth_tokens.order(:created_at).pluck(:id)
post "/u/#{user1.username}/preferences/revoke-auth-token.json", params: { token_id: ids[0] }
expect(response.status).to eq(200)
user1.user_auth_tokens.reload
expect(user1.user_auth_tokens.count).to eq(1)
expect(user1.user_auth_tokens.first.id).to eq(ids[1])
end
it "checks if token exists" do
ids = user1.user_auth_tokens.order(:created_at).pluck(:id)
post "/u/#{user1.username}/preferences/revoke-auth-token.json", params: { token_id: ids[0] }
expect(response.status).to eq(200)
post "/u/#{user1.username}/preferences/revoke-auth-token.json", params: { token_id: ids[0] }
expect(response.status).to eq(400)
end
it "does not let user log out of current session" do
token = UserAuthToken.generate!(user_id: user1.id)
cookie =
create_auth_cookie(
token: token.unhashed_auth_token,
user_id: user1.id,
trust_level: user1.trust_level,
issued_at: 5.minutes.ago,
)
post "/u/#{user1.username}/preferences/revoke-auth-token.json",
params: {
token_id: token.id,
},
headers: {
"HTTP_COOKIE" => "_t=#{cookie}",
}
expect(token.reload.id).to be_present
expect(response.status).to eq(400)
end
it "logs user out from everywhere if token_id is not present" do
post "/u/#{user1.username}/preferences/revoke-auth-token.json"
expect(response.status).to eq(200)
expect(user1.user_auth_tokens.count).to eq(0)
end
end
end
describe "#list_second_factors" do
let(:user) { user1 }
before { sign_in(user) }
context "when SSO is enabled" do
before do
SiteSetting.discourse_connect_url = "https://discourse.test/sso"
SiteSetting.enable_discourse_connect = true
end
it "does not allow access" do
post "/u/second_factors.json"
expect(response.status).to eq(404)
end
end
context "when local logins are not enabled" do
before { SiteSetting.enable_local_logins = false }
it "does not allow access" do
post "/u/second_factors.json"
expect(response.status).to eq(404)
end
end
context "when the site settings allow second factors" do
before do
SiteSetting.enable_local_logins = true
SiteSetting.enable_discourse_connect = false
end
context "when the session is unconfirmed" do
it "returns unconfirmed session response" do
post "/u/second_factors.json"
expect(response.status).to eq(200)
response_body = response.parsed_body
expect(response_body["unconfirmed_session"]).to eq(true)
end
end
context "when the session is confirmed" do
fab!(:user) { Fabricate(:user, password: "acoolpassword") }
it "returns a list of enabled totps and security_key second factors" do
totp_second_factor = Fabricate(:user_second_factor_totp, user: user)
security_key_second_factor =
Fabricate(
:user_security_key,
user: user,
factor_type: UserSecurityKey.factor_types[:second_factor],
)
post "/u/confirm-session.json", params: { password: "acoolpassword" }
post "/u/second_factors.json"
expect(response.status).to eq(200)
response_body = response.parsed_body
expect(response_body["totps"].map { |second_factor| second_factor["id"] }).to include(
totp_second_factor.id,
)
expect(
response_body["security_keys"].map { |second_factor| second_factor["id"] },
).to include(security_key_second_factor.id)
end
end
end
end
describe "#confirm_session" do
let(:user) { user1 }
let(:password) { "test" }
before { sign_in(user) }
context "when SSO is enabled" do
before do
SiteSetting.discourse_connect_url = "https://discourse.test/sso"
SiteSetting.enable_discourse_connect = true
end
it "does not allow access" do
post "/u/confirm-session.json", params: { password: password }
expect(response.status).to eq(404)
end
end
context "when local logins are not enabled" do
before { SiteSetting.enable_local_logins = false }
it "does not allow access" do
post "/u/confirm-session.json", params: { password: password }
expect(response.status).to eq(404)
end
end
context "when the site settings allow local logins" do
before do
SiteSetting.enable_local_logins = true
SiteSetting.enable_discourse_connect = false
end
context "when params are incorrect" do
it "returns 400 response if no password or passkey is supplied" do
post "/u/confirm-session.json"
expect(response.status).to eq(400)
expect(response.parsed_body["errors"][0]).to include("Missing password or passkey")
end
it "returns incorrect response on a wrong password" do
post "/u/confirm-session.json", params: { password: password }
expect(response.status).to eq(200)
expect(response.parsed_body["error"]).to eq("Incorrect password or passkey")
end
end
context "when the password is correct" do
fab!(:user2) { Fabricate(:user, password: "8555039dd212cc66ec68") }
it "returns a successful response" do
sign_in(user2)
post "/u/confirm-session.json", params: { password: "8555039dd212cc66ec68" }
expect(response.status).to eq(200)
expect(response.parsed_body["error"]).to eq(nil)
end
end
context "with an invalid passkey" do
it "returns invalid response" do
post "/u/confirm-session.json", params: { publicKeyCredential: "someboringstring" }
expect(response.status).to eq(401)
json = response.parsed_body
expect(json["errors"][0]).to eq(
I18n.t("webauthn.validation.malformed_public_key_credential_error"),
)
end
end
context "with a valid passkey" do
fab!(:user2) { Fabricate(:user) }
let!(:passkey) do
Fabricate(
:user_security_key,
credential_id: valid_passkey_data[:credential_id],
public_key: valid_passkey_data[:public_key],
user: user1,
factor_type: UserSecurityKey.factor_types[:first_factor],
last_used: nil,
name: "passkey",
)
end
before do
DiscourseWebauthn.stubs(:origin).returns("http://localhost:3000")
simulate_localhost_passkey_challenge
end
it "returns a successful response for the correct user" do
user1.create_or_fetch_secure_identifier
post "/u/confirm-session.json",
params: {
publicKeyCredential:
valid_passkey_auth_data.merge(
{ userHandle: Base64.strict_encode64(user1.secure_identifier) },
),
}
expect(response.status).to eq(200)
expect(response.parsed_body["error"]).to eq(nil)
end
it "returns invalid response when key belongs to a different user" do
sign_in(user2)
user2.create_or_fetch_secure_identifier
post "/u/confirm-session.json",
params: {
publicKeyCredential:
valid_passkey_auth_data.merge(
{ userHandle: Base64.strict_encode64(user2.secure_identifier) },
),
}
expect(response.status).to eq(401)
json = response.parsed_body
expect(json["errors"][0]).to eq(I18n.t("webauthn.validation.ownership_error"))
end
end
end
end
describe "#trusted_session" do
it "returns 403 for anons" do
get "/u/trusted-session.json"
expect(response.status).to eq(403)
end
it "responds with a 'failed' result by default" do
sign_in(user1)
get "/u/trusted-session.json"
expect(response.status).to eq(200)
expect(response.parsed_body["failed"]).to eq("FAILED")
end
it "responds with a 'success' result if user was recently created" do
sign_in(user1)
user1.update(created_at: Time.now.utc - 4.minutes)
get "/u/trusted-session.json"
expect(response.status).to eq(200)
expect(response.parsed_body["success"]).to eq("OK")
end
it "response with 'success' on a confirmed session" do
user2 = Fabricate(:user, password: "8555039dd212cc66ec68")
sign_in(user2)
post "/u/confirm-session.json", params: { password: "8555039dd212cc66ec68" }
expect(response.status).to eq(200)
get "/u/trusted-session.json"
expect(response.status).to eq(200)
expect(response.parsed_body["success"]).to eq("OK")
end
end
describe "#feature_topic" do
fab!(:topic)
fab!(:other_topic) { Fabricate(:topic) }
fab!(:private_message) { Fabricate(:private_message_topic, user: another_user) }
fab!(:category) { Fabricate(:category_with_definition) }
describe "site setting enabled" do
before { SiteSetting.allow_featured_topic_on_user_profiles = true }
it "requires the user to be logged in" do
put "/u/#{user1.username}/feature-topic.json", params: { topic_id: topic.id }
expect(response.status).to eq(403)
end
it "returns an error if the user tries to set for another user" do
sign_in(user1)
topic.update(user_id: another_user.id)
put "/u/#{another_user.username}/feature-topic.json", params: { topic_id: topic.id }
expect(response.status).to eq(403)
end
it "returns an error if the topic is a PM" do
sign_in(another_user)
put "/u/#{another_user.username}/feature-topic.json",
params: {
topic_id: private_message.id,
}
expect(response.status).to eq(403)
end
it "returns an error if the topic is not visible" do
sign_in(user1)
topic.update_status("visible", false, user1)
put "/u/#{user1.username}/feature-topic.json", params: { topic_id: topic.id }
expect(response.status).to eq(403)
end
it "returns an error if the topic's category is read_restricted" do
sign_in(user1)
category.set_permissions({})
topic.update(category_id: category.id)
put "/u/#{another_user.username}/feature-topic.json", params: { topic_id: topic.id }
expect(response.status).to eq(403)
end
it "sets featured_topic correctly for user created topic" do
sign_in(user1)
topic.update(user_id: user1.id)
put "/u/#{user1.username}/feature-topic.json", params: { topic_id: topic.id }
expect(response.status).to eq(200)
expect(user1.user_profile.featured_topic).to eq topic
end
it "sets featured_topic correctly for non-user-created topic" do
sign_in(user1)
put "/u/#{user1.username}/feature-topic.json", params: { topic_id: other_topic.id }
expect(response.status).to eq(200)
expect(user1.user_profile.featured_topic).to eq other_topic
end
describe "site setting disabled" do
before { SiteSetting.allow_featured_topic_on_user_profiles = false }
it "does not allow setting featured_topic for user_profiles" do
sign_in(user1)
topic.update(user_id: user1.id)
put "/u/#{user1.username}/feature-topic.json", params: { topic_id: topic.id }
expect(response.status).to eq(403)
end
end
end
end
describe "#clear_featured_topic" do
fab!(:topic)
it "requires the user to be logged in" do
put "/u/#{user1.username}/clear-featured-topic.json"
expect(response.status).to eq(403)
end
it "returns an error if the current user does not have access" do
sign_in(user1)
topic.update(user_id: another_user.id)
put "/u/#{another_user.username}/clear-featured-topic.json"
expect(response.status).to eq(403)
end
it "clears the user_profiles featured_topic correctly" do
sign_in(user1)
topic.update(user: user1)
put "/u/#{user1.username}/clear-featured-topic.json"
expect(response.status).to eq(200)
expect(user1.user_profile.featured_topic).to eq nil
end
end
describe "#bookmarks" do
before do
register_test_bookmarkable
TopicUser.change(user1.id, bookmark1.bookmarkable.topic_id, total_msecs_viewed: 1)
TopicUser.change(user1.id, bookmark2.bookmarkable_id, total_msecs_viewed: 1)
Fabricate(:post, topic: bookmark2.bookmarkable)
bookmark3 && bookmark4
end
after { DiscoursePluginRegistry.reset! }
let(:bookmark1) { Fabricate(:bookmark, user: user1, bookmarkable: Fabricate(:post)) }
let(:bookmark2) { Fabricate(:bookmark, user: user1, bookmarkable: Fabricate(:topic)) }
let(:bookmark3) { Fabricate(:bookmark, user: user1, bookmarkable: Fabricate(:user)) }
let(:bookmark4) { Fabricate(:bookmark) }
it "returns a list of serialized bookmarks for the user" do
sign_in(user1)
get "/u/#{user1.username}/bookmarks.json"
expect(response.status).to eq(200)
expect(
response.parsed_body["user_bookmark_list"]["bookmarks"].map { |b| b["id"] },
).to match_array([bookmark1.id, bookmark2.id, bookmark3.id])
end
it "returns a list of serialized bookmarks for the user including custom registered bookmarkables" do
sign_in(user1)
bookmark3.bookmarkable.user_profile.update!(bio_raw: "<p>Something cooked</p>")
bookmark3.bookmarkable.user_profile.rebake!
get "/u/#{user1.username}/bookmarks.json"
expect(response.status).to eq(200)
response_bookmarks = response.parsed_body["user_bookmark_list"]["bookmarks"]
expect(response_bookmarks.map { |b| b["id"] }).to match_array(
[bookmark1.id, bookmark2.id, bookmark3.id],
)
expect(response_bookmarks.find { |b| b["id"] == bookmark3.id }["excerpt"]).to eq(
"Something cooked",
)
end
it "returns an .ics file of bookmark reminders for the user in date order" do
bookmark1.update!(name: nil, reminder_at: 1.day.from_now)
bookmark2.update!(name: "Some bookmark note", reminder_at: 1.week.from_now)
bookmark3.update!(name: nil, reminder_at: 2.weeks.from_now)
sign_in(user1)
get "/u/#{user1.username}/bookmarks.ics"
expect(response.status).to eq(200)
expect(response.body).to eq(<<~ICS)
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Discourse//#{Discourse.current_hostname}//#{Discourse.full_version}//EN
BEGIN:VEVENT
UID:bookmark_reminder_##{bookmark1.id}@#{Discourse.current_hostname}
DTSTAMP:#{bookmark1.updated_at.strftime(I18n.t("datetime_formats.formats.calendar_ics"))}
DTSTART:#{bookmark1.reminder_at_ics}
DTEND:#{bookmark1.reminder_at_ics(offset: 1.hour)}
SUMMARY:#{bookmark1.bookmarkable.topic.title}
DESCRIPTION:#{bookmark1.bookmarkable.full_url}
URL:#{bookmark1.bookmarkable.full_url}
END:VEVENT
BEGIN:VEVENT
UID:bookmark_reminder_##{bookmark2.id}@#{Discourse.current_hostname}
DTSTAMP:#{bookmark2.updated_at.strftime(I18n.t("datetime_formats.formats.calendar_ics"))}
DTSTART:#{bookmark2.reminder_at_ics}
DTEND:#{bookmark2.reminder_at_ics(offset: 1.hour)}
SUMMARY:Some bookmark note
DESCRIPTION:#{bookmark2.bookmarkable.url}
URL:#{bookmark2.bookmarkable.url}
END:VEVENT
BEGIN:VEVENT
UID:bookmark_reminder_##{bookmark3.id}@#{Discourse.current_hostname}
DTSTAMP:#{bookmark3.updated_at.strftime(I18n.t("datetime_formats.formats.calendar_ics"))}
DTSTART:#{bookmark3.reminder_at_ics}
DTEND:#{bookmark3.reminder_at_ics(offset: 1.hour)}
SUMMARY:#{bookmark3.bookmarkable.username}
DESCRIPTION:#{Discourse.base_url}/u/#{bookmark3.bookmarkable.username}
URL:#{Discourse.base_url}/u/#{bookmark3.bookmarkable.username}
END:VEVENT
END:VCALENDAR
ICS
end
it "does not show another user's bookmarks" do
sign_in(Fabricate(:user))
get "/u/#{bookmark3.user.username}/bookmarks.json"
expect(response.status).to eq(403)
end
it "shows a helpful message if no bookmarks are found" do
bookmark1.destroy
bookmark2.destroy
bookmark3.destroy
sign_in(user1)
get "/u/#{user1.username}/bookmarks.json"
expect(response.status).to eq(200)
expect(response.parsed_body["bookmarks"]).to eq([])
end
it "shows a helpful message if no bookmarks are found for the search" do
sign_in(user1)
get "/u/#{user1.username}/bookmarks.json", params: { q: "badsearch" }
expect(response.status).to eq(200)
expect(response.parsed_body["bookmarks"]).to eq([])
end
describe "when limit params is invalid" do
before { sign_in(user1) }
include_examples "invalid limit params",
"/u/someusername/bookmarks.json",
described_class::BOOKMARKS_LIMIT
end
end
describe "#bookmarks excerpts" do
fab!(:user)
let!(:topic) { Fabricate(:topic, user: user) }
let!(:post) { Fabricate(:post, topic: topic) }
let!(:bookmark) { Fabricate(:bookmark, name: "Test", user: user, bookmarkable: topic) }
it "uses the first post of the topic for the bookmarks excerpt" do
TopicUser.change(
user.id,
bookmark.bookmarkable.id,
{ last_read_post_number: post.post_number },
)
sign_in(user)
get "/u/#{user.username}/bookmarks.json"
expect(response.status).to eq(200)
bookmark_list = response.parsed_body["user_bookmark_list"]["bookmarks"]
expected_excerpt = PrettyText.excerpt(topic.first_post.cooked, 300, keep_emoji_images: true)
expect(bookmark_list.first["excerpt"]).to eq(expected_excerpt)
end
describe "bookmarkable_url" do
context "with the link_to_first_unread_post option" do
it "is a full topic URL to the first unread post in the topic when the option is set" do
TopicUser.change(
user.id,
bookmark.bookmarkable.id,
{ last_read_post_number: post.post_number },
)
sign_in(user)
get "/u/#{user.username}/user-menu-bookmarks.json"
expect(response.status).to eq(200)
bookmark_list = response.parsed_body["bookmarks"]
expect(bookmark_list.first["bookmarkable_url"]).to end_with(
"/t/#{topic.slug}/#{topic.id}/#{post.post_number + 1}",
)
end
it "is a full topic URL to the first post in the topic when the option isn't set" do
TopicUser.change(
user.id,
bookmark.bookmarkable.id,
{ last_read_post_number: post.post_number },
)
sign_in(user)
get "/u/#{user.username}/bookmarks.json"
expect(response.status).to eq(200)
bookmark_list = response.parsed_body["user_bookmark_list"]["bookmarks"]
expect(bookmark_list.first["bookmarkable_url"]).to end_with(
"/t/#{topic.slug}/#{topic.id}",
)
end
end
end
end
describe "#private_message_topic_tracking_state" do
fab!(:user_2) { Fabricate(:user, refresh_auto_groups: true) }
fab!(:private_message) do
create_post(
user: user1,
target_usernames: [user_2.username],
archetype: Archetype.private_message,
).topic
end
before { sign_in(user_2) }
it "does not allow an unauthorized user to access the state of another user" do
get "/u/#{user1.username}/private-message-topic-tracking-state.json"
expect(response.status).to eq(403)
end
it "returns the right response" do
get "/u/#{user_2.username}/private-message-topic-tracking-state.json"
expect(response.status).to eq(200)
topic_state = response.parsed_body.first
expect(topic_state["topic_id"]).to eq(private_message.id)
expect(topic_state["highest_post_number"]).to eq(1)
expect(topic_state["last_read_post_number"]).to eq(nil)
expect(topic_state["notification_level"]).to eq(NotificationLevels.all[:watching])
expect(topic_state["group_ids"]).to eq([])
end
end
describe "#reset_recent_searches" do
it "does nothing for anon" do
delete "/u/recent-searches.json"
expect(response.status).to eq(403)
end
it "works for logged in user" do
freeze_time
sign_in(user1)
delete "/u/recent-searches.json"
expect(response.status).to eq(200)
user1.reload
expect(user1.user_option.oldest_search_log_date).to be_within(5.seconds).of(1.second.ago)
end
end
describe "#recent_searches" do
it "does nothing for anon" do
get "/u/recent-searches.json"
expect(response.status).to eq(403)
end
it "works for logged in user" do
freeze_time
sign_in(user1)
SiteSetting.log_search_queries = true
user1.user_option.update!(oldest_search_log_date: nil)
get "/u/recent-searches.json"
expect(response.status).to eq(200)
expect(response.parsed_body["recent_searches"]).to eq([])
SearchLog.create!(
term: "old one",
user_id: user1.id,
search_type: 1,
ip_address: "192.168.0.1",
created_at: 5.minutes.ago,
)
SearchLog.create!(
term: "also old",
user_id: user1.id,
search_type: 1,
ip_address: "192.168.0.1",
created_at: 15.minutes.ago,
)
get "/u/recent-searches.json"
expect(response.status).to eq(200)
expect(response.parsed_body["recent_searches"]).to eq(["old one", "also old"])
user1.user_option.update!(oldest_search_log_date: 20.minutes.ago)
get "/u/recent-searches.json"
expect(response.status).to eq(200)
expect(response.parsed_body["recent_searches"]).to eq(["old one", "also old"])
user1.user_option.update!(oldest_search_log_date: 10.seconds.ago)
get "/u/recent-searches.json"
expect(response.status).to eq(200)
expect(response.parsed_body["recent_searches"]).to eq([])
SearchLog.create!(
term: "new search",
user_id: user1.id,
search_type: 1,
ip_address: "192.168.0.1",
created_at: 2.seconds.ago,
)
get "/u/recent-searches.json"
expect(response.status).to eq(200)
expect(response.parsed_body["recent_searches"]).to eq(["new search"])
end
it "shows an error message when log_search_queries are off" do
sign_in(user1)
SiteSetting.log_search_queries = false
get "/u/recent-searches.json"
expect(response.status).to eq(403)
expect(response.parsed_body["error"]).to eq(I18n.t("user_activity.no_log_search_queries"))
end
end
describe "#user_menu_bookmarks" do
fab!(:post)
fab!(:topic) { Fabricate(:post).topic }
fab!(:bookmark_with_reminder) { Fabricate(:bookmark, user: user, bookmarkable: post) }
fab!(:bookmark_without_reminder) { Fabricate(:bookmark, user: user, bookmarkable: topic) }
before do
TopicUser.change(user.id, post.topic.id, total_msecs_viewed: 1)
TopicUser.change(user.id, topic.id, total_msecs_viewed: 1)
BookmarkReminderNotificationHandler.new(bookmark_with_reminder).send_notification
end
context "when logged out" do
it "responds with 404" do
get "/u/#{user.username}/user-menu-bookmarks"
expect(response.status).to eq(404)
end
end
context "when logged in" do
before { sign_in(user) }
it "responds with 403 when requesting bookmarks list of another user" do
get "/u/#{user1.username}/user-menu-bookmarks"
expect(response.status).to eq(403)
end
it "sends an array of unread bookmark_reminder notifications" do
bookmark_with_reminder2 = Fabricate(:bookmark, user: user, bookmarkable: Fabricate(:post))
TopicUser.change(user.id, bookmark_with_reminder2.bookmarkable.topic, total_msecs_viewed: 1)
BookmarkReminderNotificationHandler.new(bookmark_with_reminder2).send_notification
user
.notifications
.where(notification_type: Notification.types[:bookmark_reminder])
.where("data::json ->> 'bookmark_id' = ?", bookmark_with_reminder2.id.to_s)
.first
.update!(read: true)
get "/u/#{user.username}/user-menu-bookmarks"
expect(response.status).to eq(200)
notifications = response.parsed_body["notifications"]
expect(notifications.size).to eq(1)
expect(notifications.first["data"]["bookmark_id"]).to eq(bookmark_with_reminder.id)
end
it "responds with an array of bookmarks that are not associated with any of the unread bookmark_reminder notifications" do
get "/u/#{user.username}/user-menu-bookmarks"
expect(response.status).to eq(200)
bookmarks = response.parsed_body["bookmarks"]
expect(bookmarks.size).to eq(1)
expect(bookmarks.first["id"]).to eq(bookmark_without_reminder.id)
bookmark_reminder =
user
.notifications
.where(notification_type: Notification.types[:bookmark_reminder])
.where("data::json ->> 'bookmark_id' = ?", bookmark_with_reminder.id.to_s)
.first
bookmark_reminder.update!(read: true)
get "/u/#{user.username}/user-menu-bookmarks"
expect(response.status).to eq(200)
bookmarks = response.parsed_body["bookmarks"]
expect(bookmarks.map { |bookmark| bookmark["id"] }).to contain_exactly(
bookmark_with_reminder.id,
bookmark_without_reminder.id,
)
data = bookmark_reminder.data_hash
data.delete(:bookmark_id)
bookmark_reminder.update!(data: data.to_json, read: false)
get "/u/#{user.username}/user-menu-bookmarks"
expect(response.status).to eq(200)
notifications = response.parsed_body["notifications"]
expect(notifications.size).to eq(0)
bookmarks = response.parsed_body["bookmarks"]
expect(bookmarks.map { |bookmark| bookmark["id"] }).to contain_exactly(
bookmark_with_reminder.id,
bookmark_without_reminder.id,
)
end
it "fills up the remaining of the USER_MENU_LIST_LIMIT limit with bookmarks" do
bookmark2 = Fabricate(:bookmark, user: user, bookmarkable: Fabricate(:post, topic: topic))
stub_const(UsersController, "USER_MENU_LIST_LIMIT", 2) do
get "/u/#{user.username}/user-menu-bookmarks"
end
expect(response.status).to eq(200)
notifications = response.parsed_body["notifications"]
expect(notifications.size).to eq(1)
bookmarks = response.parsed_body["bookmarks"]
expect(bookmarks.size).to eq(1)
stub_const(UsersController, "USER_MENU_LIST_LIMIT", 3) do
get "/u/#{user.username}/user-menu-bookmarks"
end
expect(response.status).to eq(200)
notifications = response.parsed_body["notifications"]
expect(notifications.size).to eq(1)
bookmarks = response.parsed_body["bookmarks"]
expect(bookmarks.size).to eq(2)
BookmarkReminderNotificationHandler.new(bookmark2).send_notification
stub_const(UsersController, "USER_MENU_LIST_LIMIT", 3) do
get "/u/#{user.username}/user-menu-bookmarks"
end
expect(response.status).to eq(200)
notifications = response.parsed_body["notifications"]
expect(notifications.size).to eq(2)
bookmarks = response.parsed_body["bookmarks"]
expect(bookmarks.size).to eq(1)
end
it "does not return any unread notifications for bookmarks that the user no longer has access to" do
bookmark_with_reminder2 = Fabricate(:bookmark, user: user, bookmarkable: Fabricate(:post))
TopicUser.change(user.id, bookmark_with_reminder2.bookmarkable.topic, total_msecs_viewed: 1)
BookmarkReminderNotificationHandler.new(bookmark_with_reminder2).send_notification
bookmark_with_reminder2.bookmarkable.topic.update!(
archetype: Archetype.private_message,
category: nil,
)
get "/u/#{user.username}/user-menu-bookmarks"
expect(response.status).to eq(200)
notifications = response.parsed_body["notifications"]
expect(notifications.size).to eq(1)
expect(notifications.first["data"]["bookmark_id"]).to eq(bookmark_with_reminder.id)
end
it "shows unread notifications even if the bookmark has been deleted if they have bookmarkable data" do
bookmark_with_reminder.destroy!
get "/u/#{user.username}/user-menu-bookmarks"
expect(response.status).to eq(200)
notifications = response.parsed_body["notifications"]
expect(notifications.size).to eq(1)
expect(notifications.first["data"]["bookmark_id"]).to eq(bookmark_with_reminder.id)
end
it "does not show unread notifications if the bookmark has been deleted if they only have the bookmark_id data" do
notif =
Notification.find_by(
topic: bookmark_with_reminder.bookmarkable.topic,
post_number: bookmark_with_reminder.bookmarkable.post_number,
)
new_data = notif.data_hash
new_data.delete(:bookmarkable_type)
new_data.delete(:bookmarkable_id)
notif.update!(data: JSON.dump(new_data))
bookmark_with_reminder.destroy!
get "/u/#{user.username}/user-menu-bookmarks"
expect(response.status).to eq(200)
notifications = response.parsed_body["notifications"]
expect(notifications.size).to eq(0)
end
context "with `show_user_menu_avatars` setting enabled" do
before { SiteSetting.show_user_menu_avatars = true }
it "serializes acting_user_avatar into notifications" do
get "/u/#{user.username}/user-menu-bookmarks"
expect(response.status).to eq(200)
first_notification = response.parsed_body["notifications"].first
expect(first_notification["acting_user_avatar_template"]).to be_present
end
end
end
end
describe "#user_menu_messages" do
fab!(:group1) { Fabricate(:group, has_messages: true, users: [user]) }
fab!(:group2) { Fabricate(:group, has_messages: true, users: [user, user1]) }
fab!(:group3) { Fabricate(:group, has_messages: true, users: [user1]) }
fab!(:message_without_notification) { Fabricate(:private_message_post, recipient: user).topic }
fab!(:message_with_read_notification) do
Fabricate(:private_message_post, recipient: user).topic
end
fab!(:message_with_unread_notification) do
Fabricate(:private_message_post, recipient: user).topic
end
fab!(:archived_message) { Fabricate(:private_message_post, recipient: user).topic }
fab!(:group_message1) { Fabricate(:group_private_message_post, recipients: group1).topic }
fab!(:group_message2) { Fabricate(:group_private_message_post, recipients: group2).topic }
fab!(:group_message3) { Fabricate(:group_private_message_post, recipients: group3).topic }
fab!(:archived_group_message1) do
Fabricate(:group_private_message_post, recipients: group1).topic
end
fab!(:archived_group_message2) do
Fabricate(:group_private_message_post, recipients: group2).topic
end
fab!(:user1_message_without_notification) do
Fabricate(:private_message_post, recipient: user1).topic
end
fab!(:user1_message_with_read_notification) do
Fabricate(:private_message_post, recipient: user1).topic
end
fab!(:user1_message_with_unread_notification) do
Fabricate(:private_message_post, recipient: user1).topic
end
fab!(:user1_archived_message) { Fabricate(:private_message_post, recipient: user1).topic }
fab!(:unread_pm_notification) do
Fabricate(
:private_message_notification,
read: false,
user: user,
topic: message_with_unread_notification,
created_at: 4.minutes.ago,
)
end
fab!(:read_pm_notification) do
Fabricate(
:private_message_notification,
read: true,
user: user,
topic: message_with_read_notification,
)
end
fab!(:unread_group_message_summary_notification) do
Fabricate(
:notification,
read: false,
user: user,
notification_type: Notification.types[:group_message_summary],
created_at: 2.minutes.ago,
)
end
fab!(:read_group_message_summary_notification) do
Fabricate(
:notification,
read: true,
user: user,
notification_type: Notification.types[:group_message_summary],
created_at: 1.minutes.ago,
)
end
fab!(:user1_unread_pm_notification) do
Fabricate(
:private_message_notification,
read: false,
user: user1,
topic: user1_message_with_unread_notification,
)
end
fab!(:user1_read_pm_notification) do
Fabricate(
:private_message_notification,
read: true,
user: user1,
topic: user1_message_with_read_notification,
)
end
fab!(:user1_unread_group_message_summary_notification) do
Fabricate(
:notification,
read: false,
user: user1,
notification_type: Notification.types[:group_message_summary],
)
end
fab!(:user1_read_group_message_summary_notification) do
Fabricate(
:notification,
read: true,
user: user1,
notification_type: Notification.types[:group_message_summary],
)
end
before do
UserArchivedMessage.archive!(user.id, archived_message)
UserArchivedMessage.archive!(user1.id, user1_archived_message)
GroupArchivedMessage.archive!(group1.id, archived_group_message1)
GroupArchivedMessage.archive!(group2.id, archived_group_message2)
end
context "when logged out" do
it "responds with 404" do
get "/u/#{user.username}/user-menu-private-messages"
expect(response.status).to eq(404)
end
end
context "when logged in" do
before { sign_in(user) }
it "responds with 403 when requesting messages list of another user" do
get "/u/#{user1.username}/user-menu-private-messages"
expect(response.status).to eq(403)
end
it "responds with 403 if personal_message_enabled_groups does not include the user and the user isn't staff" do
SiteSetting.personal_message_enabled_groups = Group::AUTO_GROUPS[:trust_level_4]
user.update(trust_level: 1)
get "/u/#{user.username}/user-menu-private-messages"
expect(response.status).to eq(403)
end
it "sends an array of unread private_message notifications" do
get "/u/#{user.username}/user-menu-private-messages"
expect(response.status).to eq(200)
unread_notifications = response.parsed_body["unread_notifications"]
expect(unread_notifications.map { |notification| notification["id"] }).to eq(
[unread_pm_notification.id, unread_group_message_summary_notification.id],
)
end
it "sends an array of read group_message_summary notifications" do
read_group_message_summary_notification2 =
Fabricate(
:notification,
read: true,
user: user,
notification_type: Notification.types[:group_message_summary],
created_at: 5.minutes.ago,
)
get "/u/#{user.username}/user-menu-private-messages"
expect(response.status).to eq(200)
read_notifications = response.parsed_body["read_notifications"]
expect(read_notifications.map { |notification| notification["id"] }).to eq(
[read_group_message_summary_notification.id, read_group_message_summary_notification2.id],
)
end
it "responds with an array of personal messages and user watching group messages that are not associated with any of the unread private_message notifications" do
group_message1.update!(bumped_at: 1.minutes.ago)
message_without_notification.update!(bumped_at: 3.minutes.ago)
group_message2.update!(bumped_at: 6.minutes.ago)
message_with_read_notification.update!(bumped_at: 10.minutes.ago)
read_group_message_summary_notification.destroy!
TopicUser.create!(
user: user,
topic: group_message1,
notification_level: TopicUser.notification_levels[:watching],
)
TopicUser.create!(
user: user,
topic: group_message2,
notification_level: TopicUser.notification_levels[:regular],
)
get "/u/#{user.username}/user-menu-private-messages"
expect(response.status).to eq(200)
topics = response.parsed_body["topics"]
expect(topics.map { |topic| topic["id"] }).to eq(
[group_message1.id, message_without_notification.id, message_with_read_notification.id],
)
end
it "fills up the remaining of the USER_MENU_LIST_LIMIT limit with PM topics" do
stub_const(UsersController, "USER_MENU_LIST_LIMIT", 3) do
get "/u/#{user.username}/user-menu-private-messages"
end
expect(response.status).to eq(200)
unread_notifications = response.parsed_body["unread_notifications"]
expect(unread_notifications.size).to eq(2)
topics = response.parsed_body["topics"]
read_notifications = response.parsed_body["read_notifications"]
expect(topics.size).to eq(1)
expect(read_notifications.size).to eq(1)
message2 = Fabricate(:private_message_post, recipient: user).topic
Fabricate(:private_message_notification, read: false, user: user, topic: message2)
stub_const(UsersController, "USER_MENU_LIST_LIMIT", 2) do
get "/u/#{user.username}/user-menu-private-messages"
end
expect(response.status).to eq(200)
unread_notifications = response.parsed_body["unread_notifications"]
expect(unread_notifications.size).to eq(2)
topics = response.parsed_body["topics"]
read_notifications = response.parsed_body["read_notifications"]
expect(topics.size).to eq(0)
expect(read_notifications.size).to eq(0)
end
end
end
def create_second_factor_security_key
sign_in(user1)
stub_secure_session_confirmed
post "/u/create_second_factor_security_key.json"
end
def stub_secure_session_confirmed
UsersController.any_instance.stubs(:secure_session_confirmed?).returns(true)
end
end