mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 18:03:43 +08:00
974b3a2a6f
When making sensitive changes to an account (adding 2FA or passkeys), we require users to confirm their password. This is to prevent an attacker from adding 2FA to an account they have access to. However, on newly created accounts, we should not require this, it's an extra step and it doesn't provide extra security (since the account was just created). This commit makes it so that we don't require session confirmation for accounts created less than 5 minutes ago.
7338 lines
245 KiB
Ruby
7338 lines
245 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) { Fabricate(:admin, refresh_auto_groups: true) }
|
|
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-tooken"
|
|
expect(response.status).to eq(200)
|
|
expect(flash[:error]).to be_present
|
|
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)
|
|
expect(flash[:error]).to be_blank
|
|
expect(session[:current_user_id]).to be_present
|
|
|
|
expect(CGI.unescapeHTML(response.body)).to_not include(
|
|
I18n.t("activation.approval_required"),
|
|
)
|
|
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)
|
|
|
|
expect(CGI.unescapeHTML(response.body)).to include(I18n.t("activation.approval_required"))
|
|
|
|
expect(response.body).to_not have_tag(:script, with: { src: "/assets/application.js" })
|
|
|
|
expect(flash[:error]).to be_blank
|
|
expect(session[:current_user_id]).to be_blank
|
|
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).to redirect_to(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).to redirect_to(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 "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 }
|
|
|
|
use_redis_snapshotting
|
|
|
|
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
|
|
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 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, required: false) }
|
|
|
|
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.update!(required: true)
|
|
|
|
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 can nil or empty if the field is not required" do
|
|
put update_user_url, params: { user_fields: { field_id => valid_options } }
|
|
|
|
user_field.update!(required: false)
|
|
|
|
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.update!(required: true)
|
|
|
|
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.update!(required: false)
|
|
|
|
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, required: false) }
|
|
|
|
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", min: 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", max: 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)
|
|
fab!(:optional_field) { Fabricate(:user_field, required: false) }
|
|
|
|
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 { TranslationOverride.upsert!("en", "badges.demogorgon.name", "Boss") }
|
|
|
|
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(Time.now) 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
|
|
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(0)
|
|
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_and_presence` user option is checked" do
|
|
before_all { user1.user_option.update_columns(hide_profile_and_presence: 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
|
|
use_redis_snapshotting
|
|
|
|
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
|
|
end
|
|
|
|
it "returns a hidden profile" do
|
|
user.user_option.update_column(:hide_profile_and_presence, 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
|
|
end
|
|
|
|
it "should redirect to login page for anonymous user when profiles are hidden" do
|
|
SiteSetting.hide_user_profiles_from_public = true
|
|
get "/u/#{user.username}.json"
|
|
expect(response).to have_http_status(:forbidden)
|
|
get "/u/#{user.username}/messages.json"
|
|
expect(response).to have_http_status(:forbidden)
|
|
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
|
|
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
|
|
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
|
|
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_and_presence: 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 "returns partial response when hidden users" do
|
|
user.user_option.update!(hide_profile_and_presence: true)
|
|
get "/u/#{user.username}/card.json"
|
|
expect(response).to be_successful
|
|
expect(response.parsed_body["user"]["profile_hidden"]).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
|
|
end
|
|
end
|
|
|
|
describe "#cards" do
|
|
fab!(:user) { Discourse.system_user }
|
|
fab!(:user2) { Fabricate(:user) }
|
|
|
|
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_and_presence` user option is checked" do
|
|
before { user2.user_option.update_columns(hide_profile_and_presence: 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) { Fabricate :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) }
|
|
|
|
use_redis_snapshotting
|
|
|
|
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 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
|
|
|
|
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
|