discourse/spec/requests/invites_controller_spec.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

667 lines
23 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
require 'rails_helper'
describe InvitesController do
fab!(:admin) { Fabricate(:admin) }
fab!(:trust_level_4) { Fabricate(:trust_level_4) }
context 'show' do
fab!(:invite) { Fabricate(:invite) }
fab!(:user) { Fabricate(:coding_horror) }
it 'does not work for logged in users' do
sign_in(Fabricate(:user))
get "/invites/#{invite.invite_key}"
expect(response.status).to eq(200)
body = response.body
expect(CGI.unescapeHTML(body)).to include(I18n.t("login.already_logged_in"))
end
it "returns error if invite not found" do
get "/invites/nopeNOPEnope"
expect(response.status).to eq(200)
body = response.body
expect(body).to_not have_tag(:script, with: { src: '/assets/application.js' })
expect(CGI.unescapeHTML(body)).to include(I18n.t('invite.not_found', base_url: Discourse.base_url))
end
it "returns error if invite expired" do
invite.update(expires_at: 1.day.ago)
get "/invites/#{invite.invite_key}"
expect(response.status).to eq(200)
body = response.body
expect(body).to_not have_tag(:script, with: { src: '/assets/application.js' })
expect(CGI.unescapeHTML(body)).to include(I18n.t('invite.expired', base_url: Discourse.base_url))
end
it "renders the accept invite page if invite exists" do
get "/invites/#{invite.invite_key}"
expect(response.status).to eq(200)
body = response.body
expect(body).to have_tag(:script, with: { src: '/assets/application.js' })
expect(CGI.unescapeHTML(body)).to_not include(I18n.t('invite.not_found_template', site_name: SiteSetting.title, base_url: Discourse.base_url))
end
it "returns error if invite has already been redeemed" do
Fabricate(:invited_user, invite: invite, user: Fabricate(:user))
get "/invites/#{invite.invite_key}"
expect(response.status).to eq(200)
body = response.body
expect(body).to_not have_tag(:script, with: { src: '/assets/application.js' })
expect(CGI.unescapeHTML(body)).to include(I18n.t('invite.not_found_template', site_name: SiteSetting.title, base_url: Discourse.base_url))
end
end
context '#destroy' do
it 'requires you to be logged in' do
delete "/invites.json",
params: { email: 'jake@adventuretime.ooo' }
expect(response.status).to eq(403)
end
context 'while logged in' do
let!(:user) { sign_in(Fabricate(:user)) }
let!(:invite) { Fabricate(:invite, invited_by: user) }
fab!(:another_invite) { Fabricate(:invite, email: 'anotheremail@address.com') }
it 'raises an error when the id is missing' do
delete "/invites.json"
expect(response.status).to eq(400)
end
it "raises an error when the id cannot be found" do
delete "/invites.json", params: { id: 848 }
expect(response.status).to eq(400)
end
it 'raises an error when the invite is not yours' do
delete "/invites.json", params: { id: another_invite.id }
expect(response.status).to eq(400)
end
it "destroys the invite" do
delete "/invites.json", params: { id: invite.id }
expect(response.status).to eq(200)
invite.reload
expect(invite.trashed?).to be_truthy
end
end
end
context '#create' do
it 'requires you to be logged in' do
post "/invites.json", params: { email: 'jake@adventuretime.ooo' }
expect(response.status).to eq(403)
end
context 'while logged in' do
let(:email) { 'jake@adventuretime.ooo' }
it "fails if you can't invite to the forum" do
sign_in(Fabricate(:user))
post "/invites.json", params: { email: email }
expect(response).to be_forbidden
end
it "fails for normal user if invite email already exists" do
user = sign_in(trust_level_4)
invite = Invite.invite_by_email("invite@example.com", user)
post "/invites.json", params: { email: invite.email }
expect(response.status).to eq(422)
json = response.parsed_body
expect(json["failed"]).to be_present
end
it "allows admins to invite to groups" do
group = Fabricate(:group)
sign_in(admin)
post "/invites.json", params: { email: email, group_ids: [group.id] }
expect(response.status).to eq(200)
expect(Invite.find_by(email: email).invited_groups.count).to eq(1)
end
it 'allows group owners to invite to groups' do
group = Fabricate(:group)
user = sign_in(Fabricate(:user))
user.update!(trust_level: TrustLevel[2])
group.add_owner(user)
post "/invites.json", params: { email: email, group_ids: [group.id] }
expect(response.status).to eq(200)
expect(Invite.find_by(email: email).invited_groups.count).to eq(1)
end
it "does not allow admins to send multiple invites to same email" do
user = sign_in(admin)
invite = Invite.invite_by_email("invite@example.com", user)
post "/invites.json", params: { email: invite.email }
expect(response.status).to eq(422)
end
it "responds with error message in case of validation failure" do
sign_in(admin)
post "/invites.json", params: { email: "test@mailinator.com" }
expect(response.status).to eq(422)
json = response.parsed_body
expect(json["errors"]).to be_present
end
end
end
describe "#create_invite_link" do
describe 'single use invite link' do
it 'requires you to be logged in' do
post "/invites/link.json", params: {
email: 'jake@adventuretime.ooo'
}
expect(response.status).to eq(403)
end
context 'while logged in' do
let(:email) { 'jake@adventuretime.ooo' }
it "fails if you can't invite to the forum" do
sign_in(Fabricate(:user))
post "/invites/link.json", params: { email: email }
2020-07-13 09:22:04 -04:00
expect(response.status).to eq(422)
end
it "fails for normal user if invite email already exists" do
user = sign_in(trust_level_4)
invite = Invite.invite_by_email("invite@example.com", user)
post "/invites/link.json", params: {
email: invite.email
}
expect(response.status).to eq(422)
end
it "returns the right response when topic_id is invalid" do
sign_in(trust_level_4)
post "/invites/link.json", params: {
email: email, topic_id: -9999
}
2020-07-13 09:22:04 -04:00
expect(response.status).to eq(422)
end
it "verifies that inviter is authorized to invite new user to a group-private topic" do
group = Fabricate(:group)
private_category = Fabricate(:private_category, group: group)
group_private_topic = Fabricate(:topic, category: private_category)
sign_in(trust_level_4)
post "/invites/link.json", params: {
email: email, topic_id: group_private_topic.id
}
2020-07-13 09:22:04 -04:00
expect(response.status).to eq(422)
end
it "allows admins to invite to groups" do
group = Fabricate(:group)
sign_in(admin)
post "/invites/link.json", params: {
email: email, group_ids: [group.id]
}
expect(response.status).to eq(200)
expect(Invite.find_by(email: email).invited_groups.count).to eq(1)
end
it "allows multiple group invite" do
Fabricate(:group, name: "security")
Fabricate(:group, name: "support")
sign_in(admin)
post "/invites/link.json", params: {
email: email, group_names: "security,support"
}
expect(response.status).to eq(200)
expect(Invite.find_by(email: email).invited_groups.count).to eq(2)
end
end
end
describe 'multiple use invite link' do
it 'requires you to be logged in' do
post "/invites/link.json", params: {
max_redemptions_allowed: 5
}
expect(response).to be_forbidden
end
context 'while logged in' do
it "fails for non-staff users" do
sign_in(trust_level_4)
post "/invites/link.json", params: {
max_redemptions_allowed: 5
}
2020-07-13 09:22:04 -04:00
expect(response.status).to eq(422)
end
it "allows staff to invite to groups" do
moderator = Fabricate(:moderator)
sign_in(moderator)
group = Fabricate(:group)
group.add_owner(moderator)
post "/invites/link.json", params: {
max_redemptions_allowed: 5,
group_ids: [group.id]
}
expect(response.status).to eq(200)
expect(Invite.multiple_use_invites.last.invited_groups.count).to eq(1)
end
it "allows multiple group invite" do
Fabricate(:group, name: "security")
Fabricate(:group, name: "support")
sign_in(admin)
post "/invites/link.json", params: {
max_redemptions_allowed: 5,
group_names: "security,support"
}
expect(response.status).to eq(200)
expect(Invite.multiple_use_invites.last.invited_groups.count).to eq(2)
end
end
end
end
context '#perform_accept_invitation' do
context 'with an invalid invite id' do
it "redirects to the root and doesn't change the session" do
put "/invites/show/doesntexist.json"
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["success"]).to eq(false)
2019-07-19 07:09:44 -04:00
expect(json["message"]).to eq(I18n.t('invite.not_found_json'))
expect(session[:current_user_id]).to be_blank
end
end
context 'with an invalid invite record' do
let(:invite) { Fabricate(:invite) }
it "responds with error message" do
invite.update_attribute(:email, "John Doe <john.doe@example.com>")
put "/invites/show/#{invite.invite_key}.json"
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["success"]).to eq(false)
expect(json["message"]).to eq(I18n.t('invite.error_message'))
expect(session[:current_user_id]).to be_blank
end
end
context 'with a deleted invite' do
fab!(:topic) { Fabricate(:topic) }
let(:invite) do
Invite.invite_by_email("iceking@adventuretime.ooo", topic.user, topic)
end
before do
invite.destroy!
end
it "redirects to the root" do
put "/invites/show/#{invite.invite_key}.json"
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["success"]).to eq(false)
2019-07-19 07:09:44 -04:00
expect(json["message"]).to eq(I18n.t('invite.not_found_json'))
expect(session[:current_user_id]).to be_blank
end
end
context 'with an expired invite' do
fab!(:invite_link) { Fabricate(:invite, email: nil, max_redemptions_allowed: 5, expires_at: 1.day.ago, emailed_status: Invite.emailed_status_types[:not_required]) }
it "response is not successful" do
put "/invites/show/#{invite_link.invite_key}.json"
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["success"]).to eq(false)
expect(json["message"]).to eq(I18n.t('invite.not_found_json'))
expect(session[:current_user_id]).to be_blank
end
end
context 'with a valid invite id' do
fab!(:topic) { Fabricate(:topic) }
let(:invite) do
Invite.invite_by_email("iceking@adventuretime.ooo", topic.user, topic)
end
it 'redeems the invite' do
put "/invites/show/#{invite.invite_key}.json"
invite.reload
expect(invite.redeemed?).to be_truthy
end
context 'when redeem returns a user' do
fab!(:user) { Fabricate(:coding_horror) }
context 'success' do
it 'logs in the user' do
events = DiscourseEvent.track_events do
put "/invites/show/#{invite.invite_key}.json"
end
expect(events.map { |event| event[:event_name] }).to include(
:user_logged_in, :user_first_logged_in
)
invite.reload
expect(response.status).to eq(200)
expect(session[:current_user_id]).to eq(invite.invited_users.first.user_id)
expect(invite.redeemed?).to be_truthy
user = User.find(invite.invited_users.first.user_id)
expect(user.ip_address).to be_present
expect(user.registration_ip_address).to be_present
end
it 'redirects to the first topic the user was invited to' do
put "/invites/show/#{invite.invite_key}.json"
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["success"]).to eq(true)
expect(json["redirect_to"]).to eq(topic.relative_url)
end
context "if a timezone guess is provided" do
it "sets the timezone of the user in user_options" do
put "/invites/show/#{invite.invite_key}.json", params: { timezone: "Australia/Melbourne" }
expect(response.status).to eq(200)
invite.reload
user = User.find(invite.invited_users.first.user_id)
expect(user.user_option.timezone).to eq("Australia/Melbourne")
end
end
end
context 'failure' do
it "doesn't log in the user if there's a validation error" do
put "/invites/show/#{invite.invite_key}.json", params: { password: "password" }
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["success"]).to eq(false)
expect(json["errors"]["password"]).to be_present
end
end
context '.post_process_invite' do
it 'sends a welcome message if set' do
user.send_welcome_message = true
put "/invites/show/#{invite.invite_key}.json"
expect(response.status).to eq(200)
expect(response.parsed_body["success"]).to eq(true)
expect(Jobs::SendSystemMessage.jobs.size).to eq(1)
end
it 'refreshes automatic groups if staff' do
topic.user.grant_admin!
invite.update!(moderator: true)
put "/invites/show/#{invite.invite_key}.json"
expect(response.status).to eq(200)
expect(invite.invited_users.first.user.groups.pluck(:name)).to contain_exactly("moderators", "staff")
end
context "without password" do
it "sends password reset email" do
put "/invites/show/#{invite.invite_key}.json"
expect(response.status).to eq(200)
expect(Jobs::InvitePasswordInstructionsEmail.jobs.size).to eq(1)
expect(Jobs::CriticalUserEmail.jobs.size).to eq(0)
end
it "does not send password reset email if sso is enabled" do
invite # create the invite before enabling SSO
FEATURE: Rename 'Discourse SSO' to DiscourseConnect (#11978) The 'Discourse SSO' protocol is being rebranded to DiscourseConnect. This should help to reduce confusion when 'SSO' is used in the generic sense. This commit aims to: - Rename `sso_` site settings. DiscourseConnect specific ones are prefixed `discourse_connect_`. Generic settings are prefixed `auth_` - Add (server-side-only) backwards compatibility for the old setting names, with deprecation notices - Copy `site_settings` database records to the new names - Rename relevant translation keys - Update relevant translations This commit does **not** aim to: - Rename any Ruby classes or methods. This might be done in a future commit - Change any URLs. This would break existing integrations - Make any changes to the protocol. This would break existing integrations - Change any functionality. Further normalization across DiscourseConnect and other auth methods will be done separately The risks are: - There is no backwards compatibility for site settings on the client-side. Accessing auth-related site settings in Javascript is fairly rare, and an error on the client side would not be security-critical. - If a plugin is monkey-patching parts of the auth process, changes to locale keys could cause broken error messages. This should also be unlikely. The old site setting names remain functional, so security-related overrides will remain working. A follow-up commit will be made with a post-deploy migration to delete the old `site_settings` rows.
2021-02-08 05:04:33 -05:00
SiteSetting.discourse_connect_url = "https://www.example.com/sso"
SiteSetting.enable_discourse_connect = true
put "/invites/show/#{invite.invite_key}.json"
expect(response.status).to eq(200)
expect(Jobs::InvitePasswordInstructionsEmail.jobs.size).to eq(0)
expect(Jobs::CriticalUserEmail.jobs.size).to eq(0)
end
it "does not send password reset email if local login is disabled" do
invite # create the invite before enabling SSO
SiteSetting.enable_local_logins = false
put "/invites/show/#{invite.invite_key}.json"
expect(response.status).to eq(200)
expect(Jobs::InvitePasswordInstructionsEmail.jobs.size).to eq(0)
expect(Jobs::CriticalUserEmail.jobs.size).to eq(0)
end
end
context "with password" do
context "user was invited via email" do
before { invite.update_column(:emailed_status, Invite.emailed_status_types[:pending]) }
it "doesn't send an activation email and activates the user" do
expect do
put "/invites/show/#{invite.invite_key}.json", params: { password: "verystrongpassword" }
end.to change { UserAuthToken.count }.by(1)
expect(response.status).to eq(200)
expect(response.parsed_body["success"]).to eq(true)
expect(Jobs::InvitePasswordInstructionsEmail.jobs.size).to eq(0)
expect(Jobs::CriticalUserEmail.jobs.size).to eq(0)
invited_user = User.find_by_email(invite.email)
expect(invited_user.active).to eq(true)
expect(invited_user.email_confirmed?).to eq(true)
end
end
context "user was invited via link" do
before { invite.update_column(:emailed_status, Invite.emailed_status_types[:not_required]) }
it "sends an activation email and doesn't activate the user" do
expect do
put "/invites/show/#{invite.invite_key}.json", params: { password: "verystrongpassword" }
end.not_to change { UserAuthToken.count }
expect(response.status).to eq(200)
expect(response.parsed_body["success"]).to eq(true)
expect(response.parsed_body["message"]).to eq(I18n.t("invite.confirm_email"))
invited_user = User.find_by_email(invite.email)
expect(invited_user.active).to eq(false)
expect(invited_user.email_confirmed?).to eq(false)
expect(Jobs::InvitePasswordInstructionsEmail.jobs.size).to eq(0)
expect(Jobs::CriticalUserEmail.jobs.size).to eq(1)
tokens = EmailToken.where(user_id: invited_user.id, confirmed: false, expired: false).pluck(:token)
expect(tokens.size).to eq(1)
job_args = Jobs::CriticalUserEmail.jobs.first["args"].first
expect(job_args["type"]).to eq("signup")
expect(job_args["user_id"]).to eq(invited_user.id)
expect(job_args["email_token"]).to eq(tokens.first)
end
end
end
end
end
end
context 'with multiple use invite link' do
fab!(:invite_link) { Fabricate(:invite, email: nil, max_redemptions_allowed: 5, expires_at: 1.month.from_now, emailed_status: Invite.emailed_status_types[:not_required]) }
it "sends an activation email and doesn't activate the user" do
expect do
put "/invites/show/#{invite_link.invite_key}.json", params: { email: "foobar@example.com", password: "verystrongpassword" }
end.not_to change { UserAuthToken.count }
expect(response.status).to eq(200)
expect(response.parsed_body["success"]).to eq(true)
expect(response.parsed_body["message"]).to eq(I18n.t("invite.confirm_email"))
invite_link.reload
expect(invite_link.redemption_count).to eq(1)
invited_user = User.find_by_email("foobar@example.com")
expect(invited_user.active).to eq(false)
expect(invited_user.email_confirmed?).to eq(false)
expect(Jobs::InvitePasswordInstructionsEmail.jobs.size).to eq(0)
expect(Jobs::CriticalUserEmail.jobs.size).to eq(1)
tokens = EmailToken.where(user_id: invited_user.id, confirmed: false, expired: false).pluck(:token)
expect(tokens.size).to eq(1)
job_args = Jobs::CriticalUserEmail.jobs.first["args"].first
expect(job_args["type"]).to eq("signup")
expect(job_args["user_id"]).to eq(invited_user.id)
expect(job_args["email_token"]).to eq(tokens.first)
end
end
context 'new registrations are disabled' do
fab!(:topic) { Fabricate(:topic) }
let(:invite) do
Invite.invite_by_email("iceking@adventuretime.ooo", topic.user, topic)
end
before { SiteSetting.allow_new_registrations = false }
it "doesn't redeem the invite" do
put "/invites/show/#{invite.invite_key}.json"
expect(response.status).to eq(200)
invite.reload
expect(invite.invited_users).to be_blank
expect(invite.redeemed?).to be_falsey
expect(response.body).to include(I18n.t("login.new_registrations_disabled"))
end
end
context 'user is already logged in' do
fab!(:topic) { Fabricate(:topic) }
let(:invite) do
Invite.invite_by_email("iceking@adventuretime.ooo", topic.user, topic)
end
let!(:user) { sign_in(Fabricate(:user)) }
it "doesn't redeem the invite" do
put "/invites/show/#{invite.invite_key}.json", params: { id: invite.invite_key }
expect(response.status).to eq(200)
invite.reload
expect(invite.invited_users).to be_blank
expect(invite.redeemed?).to be_falsey
expect(response.body).to include(I18n.t("login.already_logged_in", current_user: user.username))
end
end
end
context '#resend_invite' do
it 'requires you to be logged in' do
post "/invites/reinvite.json", params: { email: 'first_name@example.com' }
expect(response.status).to eq(403)
end
context 'while logged in' do
let!(:user) { sign_in(Fabricate(:user)) }
let!(:invite) { Fabricate(:invite, invited_by: user) }
fab!(:another_invite) { Fabricate(:invite, email: 'last_name@example.com') }
it 'raises an error when the email is missing' do
post "/invites/reinvite.json"
expect(response.status).to eq(400)
end
it "raises an error when the email cannot be found" do
post "/invites/reinvite.json", params: { email: 'first_name@example.com' }
expect(response.status).to eq(400)
end
it 'raises an error when the invite is not yours' do
post "/invites/reinvite.json", params: { email: another_invite.email }
expect(response.status).to eq(400)
end
it "resends the invite" do
post "/invites/reinvite.json", params: { email: invite.email }
expect(response.status).to eq(200)
expect(Jobs::InviteEmail.jobs.size).to eq(1)
end
end
end
context '#upload_csv' do
it 'requires you to be logged in' do
post "/invites/upload_csv.json"
expect(response.status).to eq(403)
end
context 'while logged in' do
let(:csv_file) { File.new("#{Rails.root}/spec/fixtures/csv/discourse.csv") }
let(:file) do
Rack::Test::UploadedFile.new(File.open(csv_file))
end
let(:filename) { 'discourse.csv' }
it "fails if you can't bulk invite to the forum" do
sign_in(Fabricate(:user))
post "/invites/upload_csv.json", params: { file: file, name: filename }
expect(response.status).to eq(403)
end
it "allows admin to bulk invite" do
sign_in(admin)
post "/invites/upload_csv.json", params: { file: file, name: filename }
expect(response.status).to eq(200)
expect(Jobs::BulkInvite.jobs.size).to eq(1)
end
2019-06-12 05:05:21 -04:00
it "sends limited invites at a time" do
SiteSetting.max_bulk_invites = 3
sign_in(admin)
2019-06-12 05:05:21 -04:00
post "/invites/upload_csv.json", params: { file: file, name: filename }
expect(response.status).to eq(422)
expect(Jobs::BulkInvite.jobs.size).to eq(1)
json = response.parsed_body
2019-06-12 05:05:21 -04:00
expect(json["errors"][0]).to eq(I18n.t("bulk_invite.max_rows", max_bulk_invites: SiteSetting.max_bulk_invites))
end
end
end
end