mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 02:11:08 -06:00
d643294c9d
There was a race condition when 2 invites existed for 1 user where in some cases data from both invites would be used for the redeem. Depending on DB ordering. Fix is to delete duplicate invites earlier in the process prior to `redeem_from_email` being called.
500 lines
15 KiB
Ruby
500 lines
15 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
|
|
describe Invite do
|
|
|
|
it { is_expected.to validate_presence_of :invited_by_id }
|
|
|
|
it { is_expected.to rate_limit }
|
|
|
|
let(:iceking) { 'iceking@adventuretime.ooo' }
|
|
|
|
context 'user validators' do
|
|
fab!(:coding_horror) { Fabricate(:coding_horror) }
|
|
fab!(:user) { Fabricate(:user) }
|
|
let(:invite) { Invite.create(email: user.email, invited_by: coding_horror) }
|
|
|
|
it "should not allow an invite with the same email as an existing user" do
|
|
expect(invite).not_to be_valid
|
|
end
|
|
|
|
it "should not allow a user to invite themselves" do
|
|
expect(invite.email_already_exists).to eq(true)
|
|
end
|
|
|
|
end
|
|
|
|
context 'email validators' do
|
|
fab!(:coding_horror) { Fabricate(:coding_horror) }
|
|
|
|
it "should not allow an invite with unformatted email address" do
|
|
expect {
|
|
Fabricate(:invite, email: "John Doe <john.doe@example.com>")
|
|
}.to raise_error(ActiveRecord::RecordInvalid)
|
|
end
|
|
|
|
it "should not allow an invite with blacklisted email" do
|
|
invite = Invite.create(email: "test@mailinator.com", invited_by: coding_horror)
|
|
expect(invite).not_to be_valid
|
|
end
|
|
|
|
it "should allow an invite with non-blacklisted email" do
|
|
invite = Fabricate(:invite, email: "test@mail.com", invited_by: coding_horror)
|
|
expect(invite).to be_valid
|
|
end
|
|
|
|
end
|
|
|
|
context '#create' do
|
|
|
|
context 'saved' do
|
|
subject { Fabricate(:invite) }
|
|
|
|
it "works" do
|
|
expect(subject.invite_key).to be_present
|
|
expect(subject.email_already_exists).to eq(false)
|
|
end
|
|
|
|
it 'should store a lower case version of the email' do
|
|
expect(subject.email).to eq(iceking)
|
|
end
|
|
end
|
|
|
|
context 'to a topic' do
|
|
fab!(:topic) { Fabricate(:topic) }
|
|
let(:inviter) { topic.user }
|
|
|
|
context 'email' do
|
|
it 'enqueues a job to email the invite' do
|
|
expect do
|
|
Invite.invite_by_email(iceking, inviter, topic)
|
|
end.to change { Jobs::InviteEmail.jobs.size }
|
|
end
|
|
end
|
|
|
|
context 'destroyed' do
|
|
it "can invite the same user after their invite was destroyed" do
|
|
Invite.invite_by_email(iceking, inviter, topic).destroy!
|
|
invite = Invite.invite_by_email(iceking, inviter, topic)
|
|
expect(invite).to be_present
|
|
end
|
|
end
|
|
|
|
context 'after created' do
|
|
let(:invite) { Invite.invite_by_email(iceking, inviter, topic) }
|
|
|
|
it 'belongs to the topic' do
|
|
expect(topic.invites).to eq([invite])
|
|
expect(invite.topics).to eq([topic])
|
|
end
|
|
|
|
context 'when added by another user' do
|
|
fab!(:coding_horror) { Fabricate(:coding_horror) }
|
|
|
|
let(:new_invite) do
|
|
Invite.invite_by_email(iceking, coding_horror, topic)
|
|
end
|
|
|
|
it 'returns a different invite' do
|
|
expect(new_invite).not_to eq(invite)
|
|
expect(new_invite.invite_key).not_to eq(invite.invite_key)
|
|
expect(new_invite.topics).to eq([topic])
|
|
end
|
|
|
|
end
|
|
|
|
context 'when adding a duplicate' do
|
|
it 'returns the original invite' do
|
|
%w{
|
|
iceking@adventuretime.ooo
|
|
iceking@ADVENTURETIME.ooo
|
|
ICEKING@adventuretime.ooo
|
|
}.each do |email|
|
|
expect(Invite.invite_by_email(
|
|
email, inviter, topic
|
|
)).to eq(invite)
|
|
end
|
|
end
|
|
|
|
it 'updates timestamp of existing invite' do
|
|
invite.update!(created_at: 10.days.ago)
|
|
|
|
resend_invite = Invite.invite_by_email(
|
|
'iceking@adventuretime.ooo', inviter, topic
|
|
)
|
|
|
|
expect(resend_invite.created_at).to be_within(1.minute).of(Time.zone.now)
|
|
end
|
|
|
|
it 'returns a new invite if the other has expired' do
|
|
SiteSetting.invite_expiry_days = 1
|
|
invite.update!(created_at: 2.days.ago)
|
|
|
|
new_invite = Invite.invite_by_email(
|
|
'iceking@adventuretime.ooo', inviter, topic
|
|
)
|
|
|
|
expect(new_invite).not_to eq(invite)
|
|
expect(new_invite).not_to be_expired
|
|
end
|
|
end
|
|
|
|
context 'when adding to another topic' do
|
|
fab!(:another_topic) { Fabricate(:topic, user: topic.user) }
|
|
|
|
it 'should be the same invite' do
|
|
new_invite = Invite.invite_by_email(iceking, inviter, another_topic)
|
|
expect(new_invite).to eq(invite)
|
|
expect(another_topic.invites).to eq([invite])
|
|
expect(invite.topics).to match_array([topic, another_topic])
|
|
end
|
|
end
|
|
|
|
it 'correctly marks invite as sent via email' do
|
|
expect(invite.via_email).to eq(true)
|
|
|
|
Invite.invite_by_email(iceking, inviter, topic)
|
|
expect(invite.reload.via_email).to eq(true)
|
|
end
|
|
|
|
it 'does not mark invite as sent via email after generating invite link' do
|
|
expect(invite.via_email).to eq(true)
|
|
|
|
Invite.generate_invite_link(iceking, inviter, topic)
|
|
expect(invite.reload.via_email).to eq(false)
|
|
|
|
Invite.invite_by_email(iceking, inviter, topic)
|
|
expect(invite.reload.via_email).to eq(false)
|
|
|
|
Invite.generate_invite_link(iceking, inviter, topic)
|
|
expect(invite.reload.via_email).to eq(false)
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'an existing user' do
|
|
fab!(:topic) { Fabricate(:topic, category_id: nil, archetype: 'private_message') }
|
|
fab!(:coding_horror) { Fabricate(:coding_horror) }
|
|
|
|
it "works" do
|
|
expect do
|
|
Invite.invite_by_email(coding_horror.email, topic.user, topic)
|
|
end.to raise_error(Invite::UserExists)
|
|
end
|
|
|
|
end
|
|
|
|
context 'a staged user' do
|
|
it 'creates an invite for a staged user' do
|
|
Fabricate(:staged, email: 'staged@account.com')
|
|
invite = Invite.invite_by_email('staged@account.com', Fabricate(:coding_horror))
|
|
|
|
expect(invite).to be_valid
|
|
expect(invite.email).to eq('staged@account.com')
|
|
end
|
|
end
|
|
|
|
context '.redeem' do
|
|
|
|
fab!(:invite) { Fabricate(:invite) }
|
|
|
|
it 'creates a notification for the invitee' do
|
|
expect { invite.redeem }.to change(Notification, :count)
|
|
end
|
|
|
|
it 'wont redeem an expired invite' do
|
|
SiteSetting.invite_expiry_days = 10
|
|
invite.update_column(:created_at, 20.days.ago)
|
|
expect(invite.redeem).to be_blank
|
|
end
|
|
|
|
it 'wont redeem a deleted invite' do
|
|
invite.destroy
|
|
expect(invite.redeem).to be_blank
|
|
end
|
|
|
|
it "won't redeem an invalidated invite" do
|
|
invite.invalidated_at = 1.day.ago
|
|
expect(invite.redeem).to be_blank
|
|
end
|
|
|
|
context "deletes duplicate invites" do
|
|
fab!(:another_user) { Fabricate(:user) }
|
|
|
|
it 'delete duplicate invite' do
|
|
another_invite = Fabricate(:invite, email: invite.email, invited_by: another_user)
|
|
invite.redeem
|
|
duplicate_invite = Invite.find_by(id: another_invite.id)
|
|
expect(duplicate_invite).to be_nil
|
|
end
|
|
|
|
it 'does not delete already redeemed invite' do
|
|
redeemed_invite = Fabricate(:invite, email: invite.email, invited_by: another_user, redeemed_at: 1.day.ago)
|
|
invite.redeem
|
|
used_invite = Invite.find_by(id: redeemed_invite.id)
|
|
expect(used_invite).not_to be_nil
|
|
end
|
|
|
|
end
|
|
|
|
context "as a moderator" do
|
|
it "will give the user a moderator flag" do
|
|
invite.invited_by = Fabricate(:admin)
|
|
invite.moderator = true
|
|
invite.save
|
|
|
|
user = invite.redeem
|
|
expect(user).to be_moderator
|
|
end
|
|
|
|
it "will not give the user a moderator flag if the inviter is not staff" do
|
|
invite.moderator = true
|
|
invite.save
|
|
|
|
user = invite.redeem
|
|
expect(user).not_to be_moderator
|
|
end
|
|
end
|
|
|
|
context "when inviting to groups" do
|
|
it "add the user to the correct groups" do
|
|
group = Fabricate(:group)
|
|
invite.invited_groups.build(group_id: group.id)
|
|
invite.save
|
|
|
|
user = invite.redeem
|
|
expect(user.groups.count).to eq(1)
|
|
end
|
|
end
|
|
|
|
context "invite trust levels" do
|
|
it "returns the trust level in default_invitee_trust_level" do
|
|
SiteSetting.default_invitee_trust_level = TrustLevel[3]
|
|
expect(invite.redeem.trust_level).to eq(TrustLevel[3])
|
|
end
|
|
end
|
|
|
|
context 'inviting when must_approve_users? is enabled' do
|
|
it 'correctly activates accounts' do
|
|
invite.invited_by = Fabricate(:admin)
|
|
SiteSetting.must_approve_users = true
|
|
user = invite.redeem
|
|
expect(user.approved?).to eq(true)
|
|
end
|
|
end
|
|
|
|
context 'simple invite' do
|
|
|
|
let!(:user) { invite.redeem }
|
|
|
|
it 'works correctly' do
|
|
expect(user.is_a?(User)).to eq(true)
|
|
expect(user.send_welcome_message).to eq(true)
|
|
expect(user.trust_level).to eq(SiteSetting.default_invitee_trust_level)
|
|
end
|
|
|
|
context 'after redeeming' do
|
|
before do
|
|
invite.reload
|
|
end
|
|
|
|
it 'works correctly' do
|
|
# has set the user_id attribute
|
|
expect(invite.user).to eq(user)
|
|
|
|
# returns true for redeemed
|
|
expect(invite).to be_redeemed
|
|
end
|
|
|
|
context 'again' do
|
|
it 'will not redeem twice' do
|
|
expect(invite.redeem).to be_blank
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
context 'invited to topics' do
|
|
fab!(:tl2_user) { Fabricate(:user, trust_level: 2) }
|
|
fab!(:topic) { Fabricate(:private_message_topic, user: tl2_user) }
|
|
|
|
let!(:invite) do
|
|
topic.invite(topic.user, 'jake@adventuretime.ooo')
|
|
Invite.find_by(invited_by_id: topic.user)
|
|
end
|
|
|
|
context 'redeem topic invite' do
|
|
it 'adds the user to the topic_users' do
|
|
user = invite.redeem
|
|
topic.reload
|
|
expect(topic.allowed_users.include?(user)).to eq(true)
|
|
expect(Guardian.new(user).can_see?(topic)).to eq(true)
|
|
end
|
|
end
|
|
|
|
context 'invited by another user to the same topic' do
|
|
fab!(:another_tl2_user) { Fabricate(:user, trust_level: 2) }
|
|
let!(:another_invite) { topic.invite(another_tl2_user, 'jake@adventuretime.ooo') }
|
|
let!(:user) { invite.redeem }
|
|
|
|
it 'adds the user to the topic_users' do
|
|
topic.reload
|
|
expect(topic.allowed_users.include?(user)).to eq(true)
|
|
end
|
|
end
|
|
|
|
context 'invited by another user to a different topic' do
|
|
let!(:user) { invite.redeem }
|
|
fab!(:another_tl2_user) { Fabricate(:user, trust_level: 2) }
|
|
fab!(:another_topic) { Fabricate(:topic, user: another_tl2_user) }
|
|
|
|
it 'adds the user to the topic_users of the first topic' do
|
|
expect(another_topic.invite(another_tl2_user, user.username)).to be_truthy # invited via username
|
|
expect(topic.allowed_users.include?(user)).to eq(true)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '.find_all_invites_from' do
|
|
context 'with user that has invited' do
|
|
it 'returns invites' do
|
|
inviter = Fabricate(:user)
|
|
invite = Fabricate(:invite, invited_by: inviter)
|
|
|
|
invites = Invite.find_all_invites_from(inviter)
|
|
|
|
expect(invites).to include invite
|
|
end
|
|
end
|
|
|
|
context 'with user that has not invited' do
|
|
it 'does not return invites' do
|
|
user = Fabricate(:user)
|
|
Fabricate(:invite)
|
|
|
|
invites = Invite.find_all_invites_from(user)
|
|
|
|
expect(invites).to be_empty
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '.find_pending_invites_from' do
|
|
it 'returns pending invites only' do
|
|
inviter = Fabricate(:user)
|
|
Fabricate(
|
|
:invite,
|
|
invited_by: inviter,
|
|
user_id: 123,
|
|
email: 'redeemed@example.com'
|
|
)
|
|
|
|
pending_invite = Fabricate(
|
|
:invite,
|
|
invited_by: inviter,
|
|
user_id: nil,
|
|
email: 'pending@example.com'
|
|
)
|
|
|
|
invites = Invite.find_pending_invites_from(inviter)
|
|
|
|
expect(invites.length).to eq(1)
|
|
expect(invites.first).to eq pending_invite
|
|
|
|
expect(Invite.find_pending_invites_count(inviter)).to eq(1)
|
|
end
|
|
end
|
|
|
|
describe '.find_redeemed_invites_from' do
|
|
it 'returns redeemed invites only' do
|
|
inviter = Fabricate(:user)
|
|
Fabricate(
|
|
:invite,
|
|
invited_by: inviter,
|
|
user_id: nil,
|
|
email: 'pending@example.com'
|
|
)
|
|
|
|
redeemed_invite = Fabricate(
|
|
:invite,
|
|
invited_by: inviter,
|
|
user_id: 123,
|
|
email: 'redeemed@example.com'
|
|
)
|
|
|
|
invites = Invite.find_redeemed_invites_from(inviter)
|
|
|
|
expect(invites.length).to eq(1)
|
|
expect(invites.first).to eq redeemed_invite
|
|
|
|
expect(Invite.find_redeemed_invites_count(inviter)).to eq(1)
|
|
end
|
|
end
|
|
|
|
describe '.invalidate_for_email' do
|
|
let(:email) { 'invite.me@example.com' }
|
|
subject { described_class.invalidate_for_email(email) }
|
|
|
|
it 'returns nil if there is no invite for the given email' do
|
|
expect(subject).to eq(nil)
|
|
end
|
|
|
|
it 'sets the matching invite to be invalid' do
|
|
invite = Fabricate(:invite, invited_by: Fabricate(:user), user_id: nil, email: email)
|
|
expect(subject).to eq(invite)
|
|
expect(subject.link_valid?).to eq(false)
|
|
expect(subject).to be_valid
|
|
end
|
|
|
|
it 'sets the matching invite to be invalid without being case-sensitive' do
|
|
invite = Fabricate(:invite, invited_by: Fabricate(:user), user_id: nil, email: 'invite.me2@Example.COM')
|
|
result = described_class.invalidate_for_email('invite.me2@EXAMPLE.com')
|
|
expect(result).to eq(invite)
|
|
expect(result.link_valid?).to eq(false)
|
|
expect(result).to be_valid
|
|
end
|
|
end
|
|
|
|
describe '.redeem_from_email' do
|
|
fab!(:inviter) { Fabricate(:user) }
|
|
fab!(:invite) { Fabricate(:invite, invited_by: inviter, email: 'test@example.com', user_id: nil) }
|
|
fab!(:user) { Fabricate(:user, email: invite.email) }
|
|
|
|
it 'redeems the invite from email' do
|
|
Invite.redeem_from_email(user.email)
|
|
invite.reload
|
|
expect(invite).to be_redeemed
|
|
end
|
|
|
|
it 'does not redeem the invite if email does not match' do
|
|
Invite.redeem_from_email('test24@example.com')
|
|
invite.reload
|
|
expect(invite).not_to be_redeemed
|
|
end
|
|
|
|
end
|
|
|
|
describe '.rescind_all_expired_invites_from' do
|
|
it 'removes all expired invites sent by a user' do
|
|
SiteSetting.invite_expiry_days = 1
|
|
user = Fabricate(:user)
|
|
invite_1 = Fabricate(:invite, invited_by: user)
|
|
invite_2 = Fabricate(:invite, invited_by: user)
|
|
expired_invite = Fabricate(:invite, invited_by: user)
|
|
expired_invite.update!(created_at: 2.days.ago)
|
|
Invite.rescind_all_expired_invites_from(user)
|
|
invite_1.reload
|
|
invite_2.reload
|
|
expired_invite.reload
|
|
expect(invite_1.deleted_at).to eq(nil)
|
|
expect(invite_2.deleted_at).to eq(nil)
|
|
expect(expired_invite.deleted_at).to be_present
|
|
end
|
|
end
|
|
end
|