mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 22:43:49 +08:00
6ab1a19e93
We're changing the implementation of trust levels to use groups. Part of this is to have site settings that reference trust levels use groups instead. It converts the min_trust_level_to_allow_invite site setting to invite_allowed_groups. Nothing much of note. This is used in one place and there's no fallout.
7306 lines
244 KiB
Ruby
7306 lines
244 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "rotp"
|
|
|
|
RSpec.describe UsersController do
|
|
fab!(:user)
|
|
fab!(:user1) { Fabricate(:user, username: "someusername") }
|
|
fab!(:another_user) { Fabricate(:user) }
|
|
fab!(:invitee) { Fabricate(:user) }
|
|
fab!(:inviter) { Fabricate(:user) }
|
|
|
|
fab!(:admin)
|
|
fab!(:moderator)
|
|
fab!(:inactive_user)
|
|
|
|
before { Group.refresh_automatic_groups! }
|
|
|
|
# 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
|
|
|
|
# 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))
|
|
user.trust_level = 1
|
|
user.save!
|
|
Group.refresh_automatic_groups!
|
|
|
|
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") }
|
|
|
|
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: 2, refresh_auto_groups: true)
|
|
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: 2, refresh_auto_groups: true)
|
|
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: 2, refresh_auto_groups: true)
|
|
sign_in(Fabricate(:user, trust_level: 2, refresh_auto_groups: true))
|
|
|
|
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: 2, refresh_auto_groups: true)
|
|
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: 2, refresh_auto_groups: true)
|
|
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: 2, refresh_auto_groups: true))
|
|
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)
|
|
|
|
before { sign_in(user) }
|
|
|
|
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.update!(trust_level: 3)
|
|
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 "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
|
|
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
|
|
context "when creation parameters are valid" do
|
|
it "creates a security key for the user" do
|
|
simulate_localhost_webauthn_challenge
|
|
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
|
|
simulate_localhost_webauthn_challenge
|
|
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
|
|
simulate_localhost_webauthn_challenge
|
|
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 { SiteSetting.enable_passkeys = true }
|
|
|
|
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
|
|
|
|
it "returns a successful response for the correct user" do
|
|
simulate_localhost_passkey_challenge
|
|
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)
|
|
simulate_localhost_passkey_challenge
|
|
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 "resopnds 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 "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) }
|
|
|
|
fab!(:private_message) do
|
|
Group.refresh_automatic_groups!
|
|
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
|