diff --git a/app/assets/javascripts/discourse/controllers/invite.js.es6 b/app/assets/javascripts/discourse/controllers/invite.js.es6 index 1b750a18659..287f209e15c 100644 --- a/app/assets/javascripts/discourse/controllers/invite.js.es6 +++ b/app/assets/javascripts/discourse/controllers/invite.js.es6 @@ -289,7 +289,7 @@ export default Ember.Controller.extend(ModalFunctionality, { model.setProperties({ saving: true, error: false }); - const onerror = function(e) { + const onerror = e => { if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) { self.set("errorMessage", e.jqXHR.responseJSON.errors[0]); } else { diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index a647d466f79..bf44a9746b1 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -555,7 +555,7 @@ class TopicsController < ApplicationController )) end - guardian.ensure_can_invite_to!(topic, groups) + guardian.ensure_can_invite_to!(topic) group_ids = groups.map(&:id) begin @@ -568,7 +568,25 @@ class TopicsController < ApplicationController render json: success_json end else - render json: failed_json, status: 422 + json = failed_json + + unless topic.private_message? + group_names = topic.category + .visible_group_names(current_user) + .where(automatic: false) + .pluck(:name) + .join(", ") + + if group_names.present? + json.merge!(errors: [ + I18n.t("topic_invite.failed_to_invite", + group_names: group_names + ) + ]) + end + end + + render json: json, status: 422 end rescue Topic::UserExists => e render json: { errors: [e.message] }, status: 422 diff --git a/app/models/category.rb b/app/models/category.rb index e857f3dcbdf..0f168f21541 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -475,6 +475,10 @@ class Category < ActiveRecord::Base self.name_lower = name.downcase if self.name end + def visible_group_names(user) + self.groups.visible_groups(user) + end + def secure_group_ids if self.read_restricted? groups.pluck("groups.id") diff --git a/app/models/invite.rb b/app/models/invite.rb index b613e22c5e6..44f505b1c7d 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -57,19 +57,6 @@ class Invite < ActiveRecord::Base InviteRedeemer.new(self, username, name, password, user_custom_fields).redeem unless expired? || destroyed? || !link_valid? end - def self.extend_permissions(topic, user, invited_by) - if topic.private_message? - topic.grant_permission_to_user(user.email) - elsif topic.category && topic.category.groups.any? - if Guardian.new(invited_by).can_invite_via_email?(topic) - (topic.category.groups - user.groups).each do |group| - group.add(user) - GroupActionLogger.new(Discourse.system_user, group).log_add_user_to_group(user) - end - end - end - end - def self.invite_by_email(email, invited_by, topic = nil, group_ids = nil, custom_message = nil) create_invite_by_email(email, invited_by, topic: topic, @@ -103,8 +90,10 @@ class Invite < ActiveRecord::Base lower_email = Email.downcase(email) if user = find_user_by_email(lower_email) - extend_permissions(topic, user, invited_by) if topic - raise UserExists.new I18n.t("invite.user_exists", email: lower_email, username: user.username) + raise UserExists.new(I18n.t("invite.user_exists", + email: lower_email, + username: user.username + )) end invite = Invite.with_deleted @@ -134,14 +123,10 @@ class Invite < ActiveRecord::Base if group_ids.present? group_ids = group_ids - invite.invited_groups.pluck(:group_id) + group_ids.each do |group_id| invite.invited_groups.create!(group_id: group_id) end - else - if topic && topic.category && Guardian.new(invited_by).can_invite_to?(topic) - group_ids = topic.category.groups.where(automatic: false).pluck(:id) - invite.invited_groups.pluck(:group_id) - group_ids.each { |group_id| invite.invited_groups.create!(group_id: group_id) } - end end Jobs.enqueue(:invite_email, invite_id: invite.id) if send_email diff --git a/app/models/topic.rb b/app/models/topic.rb index e2e0fb449c8..0f727c93ba7 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -840,62 +840,29 @@ class Topic < ActiveRecord::Base def invite(invited_by, username_or_email, group_ids = nil, custom_message = nil) target_user = User.find_by_username_or_email(username_or_email) guardian = Guardian.new(invited_by) + is_email = username_or_email =~ /^.+@.+$/ - if target_user && topic_allowed_users.where(user_id: target_user.id).exists? - raise UserExists.new(I18n.t("topic_invite.user_exists")) - end - - return true if target_user && invite_existing_muted?(target_user, invited_by) - - if private_message? && target_user && !guardian.can_send_private_message?(target_user) - raise UserExists.new(I18n.t("activerecord.errors.models.topic.attributes.base.cant_send_pm")) - end - - if target_user && private_message? && topic_allowed_users.create!(user_id: target_user.id) - rate_limit_topic_invitation(invited_by) - add_small_action(invited_by, "invited_user", target_user.username) - - create_invite_notification!( - target_user, - Notification.types[:invited_to_private_message], - invited_by.username - ) - - true - elsif username_or_email =~ /^.+@.+$/ && guardian.can_invite_via_email?(self) - - if target_user - rate_limit_topic_invitation(invited_by) - Invite.extend_permissions(self, target_user, invited_by) - - create_invite_notification!( - target_user, - Notification.types[:invited_to_topic], - invited_by.username - ) - else - invite_by_email(invited_by, username_or_email, group_ids, custom_message) + if target_user + if topic_allowed_users.exists?(user_id: target_user.id) + raise UserExists.new(I18n.t("topic_invite.user_exists")) end - true - elsif target_user && - rate_limit_topic_invitation(invited_by) && - topic_allowed_users.create!(user_id: target_user.id) + if invite_existing_muted?(target_user, invited_by) + return true + end - create_invite_notification!( - target_user, - Notification.types[:invited_to_topic], - invited_by.username + if private_message? + !!invite_to_private_message(invited_by, target_user, guardian) + else + !!invite_to_topic(invited_by, target_user, group_ids, guardian) + end + elsif is_email && guardian.can_invite_via_email?(self) + !!Invite.invite_by_email( + username_or_email, invited_by, self, group_ids, custom_message ) - - true end end - def invite_by_email(invited_by, email, group_ids = nil, custom_message = nil) - Invite.invite_by_email(email, invited_by, self, group_ids, custom_message) - end - def invite_existing_muted?(target_user, invited_by) if invited_by.id && MutedUser.where(user_id: target_user.id, muted_user_id: invited_by.id) @@ -1398,6 +1365,55 @@ class Topic < ActiveRecord::Base private + def invite_to_private_message(invited_by, target_user, guardian) + if !guardian.can_send_private_message?(target_user) + raise UserExists.new(I18n.t( + "activerecord.errors.models.topic.attributes.base.cant_send_pm" + )) + end + + Topic.transaction do + rate_limit_topic_invitation(invited_by) + topic_allowed_users.create!(user_id: target_user.id) + add_small_action(invited_by, "invited_user", target_user.username) + + create_invite_notification!( + target_user, + Notification.types[:invited_to_private_message], + invited_by.username + ) + end + end + + def invite_to_topic(invited_by, target_user, group_ids, guardian) + Topic.transaction do + rate_limit_topic_invitation(invited_by) + + if group_ids + ( + self.category.groups.where(id: group_ids).where(automatic: false) - + target_user.groups.where(automatic: false) + ).each do |group| + if guardian.can_edit_group?(group) + group.add(target_user) + + GroupActionLogger + .new(invited_by, group) + .log_add_user_to_group(target_user) + end + end + end + + if Guardian.new(target_user).can_see_topic?(self) + create_invite_notification!( + target_user, + Notification.types[:invited_to_topic], + invited_by.username + ) + end + end + end + def update_category_topic_count_by(num) if category_id.present? Category.where(['id = ?', category_id]).update_all("topic_count = topic_count " + (num > 0 ? '+' : '') + "#{num}") diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index bed4fd8003d..70a8c9292db 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -190,6 +190,7 @@ en: error: "There was an error uploading that file. Please try again later." topic_invite: + failed_to_invite: "The user cannot be invited into this topic without a group membership in either one of the following groups: %{group_names}." user_exists: "Sorry, that user has already been invited. You may only invite a user to a topic once." backup: diff --git a/lib/guardian.rb b/lib/guardian.rb index 3f7a96eba36..630ed38ffe5 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -265,19 +265,25 @@ class Guardian def can_invite_to?(object, groups = nil) return false unless authenticated? - return true if is_admin? + is_topic = object.is_a?(Topic) + return true if is_admin? && !is_topic return false if (SiteSetting.max_invites_per_day.to_i == 0 && !is_staff?) return false unless can_see?(object) return false if groups.present? - if object.is_a?(Topic) && object.private_message? - return false unless SiteSetting.enable_personal_messages? - return false if object.reached_recipients_limit? && !is_staff? - end + if is_topic + if object.private_message? + return true if is_admin? + return false unless SiteSetting.enable_personal_messages? + return false if object.reached_recipients_limit? && !is_staff? + end - if object.is_a?(Topic) && object.category - if object.category.groups.any? - return true if object.category.groups.all? { |g| can_edit_group?(g) } + if (category = object.category) && category.read_restricted + if (groups = category.groups&.where(automatic: false))&.any? + return groups.any? { |g| can_edit_group?(g) } ? true : false + else + return false + end end end diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb index fc620ecc039..c7604c85b77 100644 --- a/spec/components/guardian_spec.rb +++ b/spec/components/guardian_spec.rb @@ -550,8 +550,8 @@ describe Guardian do expect(Guardian.new(user).can_invite_to?(private_topic)).to be_falsey end - it 'returns true for admin on private topic' do - expect(Guardian.new(admin).can_invite_to?(private_topic)).to be_truthy + it 'returns false for admin on private topic' do + expect(Guardian.new(admin).can_invite_to?(private_topic)).to be(false) end it 'returns true for a group owner' do @@ -562,6 +562,49 @@ describe Guardian do SiteSetting.enable_personal_messages = false expect(Guardian.new(trust_level_2).can_invite_to?(topic)).to be_truthy end + + describe 'for a private category for automatic and non-automatic group' do + let(:automatic_group) { Fabricate(:group, automatic: true) } + let(:group) { Fabricate(:group) } + + let(:category) do + Fabricate(:category, read_restricted: true).tap do |category| + category.groups << automatic_group + category.groups << group + end + end + + let(:topic) { Fabricate(:topic, category: category) } + + it 'should return true for an admin user' do + expect(Guardian.new(admin).can_invite_to?(topic)).to eq(true) + end + + it 'should return true for a group owner' do + expect(Guardian.new(group_owner).can_invite_to?(topic)).to eq(true) + end + + it 'should return false for a normal user' do + expect(Guardian.new(user).can_invite_to?(topic)).to eq(false) + end + end + + describe 'for a private category for automatic groups' do + let(:group) { Fabricate(:group, automatic: true) } + + let(:category) do + Fabricate(:private_category, group: group, read_restricted: true) + end + + let(:group_owner) { Fabricate(:user).tap { |user| group.add_owner(user) } } + let(:topic) { Fabricate(:topic, category: category) } + + it 'should return false for all type of users' do + expect(Guardian.new(admin).can_invite_to?(topic)).to eq(false) + expect(Guardian.new(group_owner).can_invite_to?(topic)).to eq(false) + expect(Guardian.new(user).can_invite_to?(topic)).to eq(false) + end + end end describe "private messages" do diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb index 95edd750c54..33a49cb1f4e 100644 --- a/spec/models/invite_spec.rb +++ b/spec/models/invite_spec.rb @@ -59,37 +59,38 @@ describe Invite do context 'email' do it 'enqueues a job to email the invite' do - Jobs.expects(:enqueue).with(:invite_email, has_key(:invite_id)) - topic.invite_by_email(inviter, iceking) + 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 = topic.invite_by_email(inviter, iceking) - invite.destroy - invite = topic.invite_by_email(inviter, iceking) + 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 - before do - @invite = topic.invite_by_email(inviter, iceking) - end + 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]) + expect(topic.invites).to eq([invite]) + expect(invite.topics).to eq([topic]) end context 'when added by another user' do let(:coding_horror) { Fabricate(:coding_horror) } - let(:new_invite) { topic.invite_by_email(coding_horror, iceking) } + + 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).not_to eq(invite) + expect(new_invite.invite_key).not_to eq(invite.invite_key) expect(new_invite.topics).to eq([topic]) end @@ -97,24 +98,36 @@ describe Invite do context 'when adding a duplicate' do it 'returns the original invite' do - expect(topic.invite_by_email(inviter, 'iceking@adventuretime.ooo')).to eq(@invite) - expect(topic.invite_by_email(inviter, 'iceking@ADVENTURETIME.ooo')).to eq(@invite) - expect(topic.invite_by_email(inviter, 'ICEKING@adventuretime.ooo')).to eq(@invite) + %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.created_at = 10.days.ago - @invite.save - resend_invite = topic.invite_by_email(inviter, 'iceking@adventuretime.ooo') + 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.created_at = 2.days.ago - @invite.save - new_invite = topic.invite_by_email(inviter, 'iceking@adventuretime.ooo') - expect(new_invite).not_to eq(@invite) + 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 @@ -123,64 +136,24 @@ describe Invite do let!(:another_topic) { Fabricate(:topic, user: topic.user) } it 'should be the same invite' do - @new_invite = another_topic.invite_by_email(inviter, iceking) - expect(@new_invite).to eq(@invite) - expect(another_topic.invites).to eq([@invite]) - expect(@invite.topics).to match_array([topic, another_topic]) + 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 end end end - context 'to a group-private topic' do - let(:group) { Fabricate(:group) } - let(:private_category) { Fabricate(:private_category, group: group) } - let(:group_private_topic) { Fabricate(:topic, category: private_category) } - let(:inviter) { group_private_topic.user } - - before do - group.add_owner(inviter) - @invite = group_private_topic.invite_by_email(inviter, iceking) - end - - it 'should add the groups to the invite' do - expect(@invite.groups).to eq([group]) - end - - context 'when duplicated' do - it 'should not duplicate the groups' do - expect(group_private_topic.invite_by_email(inviter, iceking)).to eq(@invite) - expect(@invite.groups).to eq([group]) - end - end - - it 'verifies that inviter is authorized to invite user to a topic' do - tl2_user = Fabricate(:user, trust_level: 2) - - invite = group_private_topic.invite_by_email(tl2_user, 'foo@bar.com') - expect(invite.groups.count).to eq(0) - end - - context 'automatic groups' do - it 'should not add invited user to automatic groups' do - group.update!(automatic: true) - expect(group_private_topic.invite_by_email(Fabricate(:admin), iceking).groups.count).to eq(0) - end - end - end - context 'an existing user' do let(:topic) { Fabricate(:topic, category_id: nil, archetype: 'private_message') } let(:coding_horror) { Fabricate(:coding_horror) } it "works" do - # doesn't create an invite - expect { topic.invite_by_email(topic.user, coding_horror.email) }.to raise_error(Invite::UserExists) - - # gives the user permission to access the topic - expect(topic.allowed_users.include?(coding_horror)).to eq(true) + expect do + Invite.invite_by_email(coding_horror.email, topic.user, topic) + end.to raise_error(Invite::UserExists) end end @@ -353,7 +326,6 @@ describe Invite do 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) - expect(another_topic.allowed_users.include?(user)).to eq(true) end end end diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index 5e28d0d618a..6e25df69fa9 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -608,35 +608,85 @@ describe Topic do end describe 'public topic' do - def expect_the_right_notification_to_be_created + def expect_the_right_notification_to_be_created(inviter, invitee) notification = Notification.last expect(notification.notification_type) .to eq(Notification.types[:invited_to_topic]) - expect(notification.user).to eq(another_user) + expect(notification.user).to eq(invitee) expect(notification.topic).to eq(topic) notification_data = JSON.parse(notification.data) expect(notification_data["topic_title"]).to eq(topic.title) - expect(notification_data["display_username"]).to eq(user.username) + expect(notification_data["display_username"]).to eq(inviter.username) end describe 'by username' do it 'should invite user into a topic' do topic.invite(user, another_user.username) - - expect(topic.reload.allowed_users.last).to eq(another_user) - expect_the_right_notification_to_be_created + expect_the_right_notification_to_be_created(user, another_user) end end describe 'by email' do it 'should be able to invite a user' do expect(topic.invite(user, another_user.email)).to eq(true) - expect(topic.reload.allowed_users.last).to eq(another_user) - expect_the_right_notification_to_be_created + expect_the_right_notification_to_be_created(user, another_user) + end + + describe 'when topic belongs to a private category' do + let(:group) { Fabricate(:group) } + + let(:category) do + Fabricate(:category, groups: [group]).tap do |category| + category.set_permissions(group => :full) + category.save! + end + end + + let(:topic) { Fabricate(:topic, category: category) } + let(:inviter) { Fabricate(:user).tap { |user| group.add_owner(user) } } + let(:invitee) { Fabricate(:user) } + + describe 'as a group owner' do + it 'should be able to invite a user' do + expect do + expect(topic.invite(inviter, invitee.email, [group.id])) + .to eq(true) + end.to change { Notification.count } & + change { GroupHistory.count } + + expect_the_right_notification_to_be_created(inviter, invitee) + + group_history = GroupHistory.last + + expect(group_history.acting_user).to eq(inviter) + expect(group_history.target_user).to eq(invitee) + + expect(group_history.action).to eq( + GroupHistory.actions[:add_user_to_group] + ) + end + + describe 'when group ids are not given' do + it 'should not invite the user' do + expect do + expect(topic.invite(inviter, invitee.email)).to eq(false) + end.to_not change { Notification.count } + end + end + end + + describe 'as a normal user' do + it 'should not be able to invite a user' do + expect do + expect(topic.invite(Fabricate(:user), invitee.email, [group.id])) + .to eq(false) + end.to_not change { Notification.count } + end + end end context "for a muted topic" do @@ -2004,34 +2054,6 @@ describe Topic do expect(topic.save).to eq(true) end - context 'invite by group manager' do - let(:group_manager) { Fabricate(:user) } - let(:group) { Fabricate(:group).tap { |g| g.add_owner(group_manager) } } - let(:private_category) { Fabricate(:private_category, group: group) } - let(:group_private_topic) { Fabricate(:topic, category: private_category, user: group_manager) } - - context 'to an email' do - let(:randolph) { 'randolph@duke.ooo' } - - it "should attach group to the invite" do - group_private_topic.invite(group_manager, randolph) - expect(Invite.last.groups).to eq([group]) - end - end - - # should work for an existing user - give access, send notification - context 'to an existing user' do - let(:walter) { Fabricate(:walter_white) } - - it "should add user to the group" do - expect(Guardian.new(walter).can_see?(group_private_topic)).to be_falsey - group_private_topic.invite(group_manager, walter.email) - expect(walter.groups).to include(group) - expect(Guardian.new(walter).can_see?(group_private_topic)).to be_truthy - end - end - end - it "Correctly sets #message_archived?" do topic = Fabricate(:private_message_topic) user = topic.user diff --git a/spec/requests/invites_controller_spec.rb b/spec/requests/invites_controller_spec.rb index 4b4586ce134..9663b889e9d 100644 --- a/spec/requests/invites_controller_spec.rb +++ b/spec/requests/invites_controller_spec.rb @@ -215,9 +215,13 @@ describe InvitesController do context 'with a deleted invite' do let(:topic) { Fabricate(:topic) } - let(:invite) { topic.invite_by_email(topic.user, "iceking@adventuretime.ooo") } + + let(:invite) do + Invite.invite_by_email("iceking@adventuretime.ooo", topic.user, topic) + end + before do - invite.destroy + invite.destroy! end it "redirects to the root" do @@ -233,7 +237,9 @@ describe InvitesController do context 'with a valid invite id' do let(:topic) { Fabricate(:topic) } - let(:invite) { topic.invite_by_email(topic.user, "iceking@adventuretime.ooo") } + 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" @@ -323,7 +329,11 @@ describe InvitesController do context 'new registrations are disabled' do let(:topic) { Fabricate(:topic) } - let(:invite) { topic.invite_by_email(topic.user, "iceking@adventuretime.ooo") } + + 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 @@ -338,7 +348,11 @@ describe InvitesController do context 'user is already logged in' do let(:topic) { Fabricate(:topic) } - let(:invite) { topic.invite_by_email(topic.user, "iceking@adventuretime.ooo") } + + 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 diff --git a/spec/requests/topics_controller_spec.rb b/spec/requests/topics_controller_spec.rb index 94c9a010055..9726b22374f 100644 --- a/spec/requests/topics_controller_spec.rb +++ b/spec/requests/topics_controller_spec.rb @@ -2010,14 +2010,46 @@ RSpec.describe TopicsController do let(:recipient) { 'jake@adventuretime.ooo' } it "should attach group to the invite" do - post "/t/#{group_private_topic.id}/invite.json", params: { - user: recipient + user: recipient, + group_ids: "#{group.id},123" } expect(response.status).to eq(200) expect(Invite.find_by(email: recipient).groups).to eq([group]) end + + describe 'when group is available to automatic groups only' do + before do + group.update!(automatic: true) + end + + it 'should return the right response' do + post "/t/#{group_private_topic.id}/invite.json", params: { + user: Fabricate(:user) + } + + expect(response.status).to eq(403) + end + end + + describe 'when user is not part of the required group' do + it 'should return the right response' do + post "/t/#{group_private_topic.id}/invite.json", params: { + user: Fabricate(:user) + } + + expect(response.status).to eq(422) + + response_body = JSON.parse(response.body) + + expect(response_body["errors"]).to eq([ + I18n.t("topic_invite.failed_to_invite", + group_names: group.name + ) + ]) + end + end end describe 'when topic id is invalid' do