mirror of
https://github.com/discourse/discourse.git
synced 2025-01-18 10:52:45 +08:00
FIX: leaving a group channel should destroy membership (#24631)
In other kind of channels we will only unfollow but for group channels we don't want people to keep appearing in members list. This commit also creates appropriate services: - `Chat::LeaveChannel` - `Chat::UnfollowChannel` And dedicated endpoint for unfollow: `DELETE /chat/api/channels/:id/memberships/me/follows`
This commit is contained in:
parent
11636f8736
commit
384a8b17a1
|
@ -12,10 +12,6 @@ class Chat::Api::ChannelsCurrentUserMembershipController < Chat::Api::ChannelsCo
|
|||
end
|
||||
|
||||
def destroy
|
||||
render_serialized(
|
||||
channel_from_params.remove(current_user),
|
||||
Chat::UserChannelMembershipSerializer,
|
||||
root: "membership",
|
||||
)
|
||||
with_service(Chat::LeaveChannel) { on_model_not_found(:channel) { raise Discourse::NotFound } }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Chat::Api::ChannelsCurrentUserMembershipFollowsController < Chat::Api::ChannelsController
|
||||
def destroy
|
||||
with_service(Chat::UnfollowChannel) do
|
||||
on_success do
|
||||
render_serialized(
|
||||
result.membership,
|
||||
Chat::UserChannelMembershipSerializer,
|
||||
root: "membership",
|
||||
)
|
||||
end
|
||||
on_model_not_found(:channel) { raise Discourse::NotFound }
|
||||
end
|
||||
end
|
||||
end
|
55
plugins/chat/app/services/chat/leave_channel.rb
Normal file
55
plugins/chat/app/services/chat/leave_channel.rb
Normal file
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
# Service responsible to flag a message.
|
||||
#
|
||||
# @example
|
||||
# ::Chat::LeaveChannel.call(
|
||||
# guardian: guardian,
|
||||
# channel_id: 1,
|
||||
# )
|
||||
#
|
||||
class LeaveChannel
|
||||
include Service::Base
|
||||
|
||||
# @!method call(guardian:, channel_id:,)
|
||||
# @param [Guardian] guardian
|
||||
# @param [Integer] channel_id of the channel
|
||||
|
||||
# @return [Service::Base::Context]
|
||||
contract
|
||||
model :channel
|
||||
step :leave
|
||||
step :recompute_users_count
|
||||
|
||||
# @!visibility private
|
||||
class Contract
|
||||
attribute :channel_id, :integer
|
||||
validates :channel_id, presence: true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_channel(contract:, **)
|
||||
Chat::Channel.find_by(id: contract.channel_id)
|
||||
end
|
||||
|
||||
def leave(channel:, guardian:, **)
|
||||
ActiveRecord::Base.transaction do
|
||||
if channel.direct_message_channel? && channel.chatable&.group
|
||||
channel.membership_for(guardian.user)&.destroy!
|
||||
channel.chatable.direct_message_users.where(user_id: guardian.user.id).destroy_all
|
||||
else
|
||||
channel.remove(guardian.user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def recompute_users_count(channel:, **)
|
||||
channel.update!(
|
||||
user_count: ::Chat::ChannelMembershipsQuery.count(channel),
|
||||
user_count_stale: false,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
40
plugins/chat/app/services/chat/unfollow_channel.rb
Normal file
40
plugins/chat/app/services/chat/unfollow_channel.rb
Normal file
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
# Service responsible to flag a message.
|
||||
#
|
||||
# @example
|
||||
# ::Chat::UnfollowChannel.call(
|
||||
# guardian: guardian,
|
||||
# channel_id: 1,
|
||||
# )
|
||||
#
|
||||
class UnfollowChannel
|
||||
include Service::Base
|
||||
|
||||
# @!method call(guardian:, channel_id:,)
|
||||
# @param [Guardian] guardian
|
||||
# @param [Integer] channel_id of the channel
|
||||
|
||||
# @return [Service::Base::Context]
|
||||
contract
|
||||
model :channel
|
||||
step :unfollow
|
||||
|
||||
# @!visibility private
|
||||
class Contract
|
||||
attribute :channel_id, :integer
|
||||
validates :channel_id, presence: true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_channel(contract:, **)
|
||||
Chat::Channel.find_by(id: contract.channel_id)
|
||||
end
|
||||
|
||||
def unfollow(channel:, guardian:, **)
|
||||
context.membership = channel.remove(guardian.user)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -131,7 +131,7 @@ export default class ChatChannelRow extends Component {
|
|||
}
|
||||
|
||||
get leaveDirectMessageLabel() {
|
||||
return I18n.t("chat.direct_messages.leave");
|
||||
return I18n.t("chat.direct_messages.close");
|
||||
}
|
||||
|
||||
get leaveChannelLabel() {
|
||||
|
|
|
@ -29,12 +29,14 @@ const NOTIFICATION_LEVELS = [
|
|||
export default class ChatAboutScreen extends Component {
|
||||
@service chatApi;
|
||||
@service chatGuardian;
|
||||
@service chatChannelsManager;
|
||||
@service currentUser;
|
||||
@service siteSettings;
|
||||
@service dialog;
|
||||
@service modal;
|
||||
@service site;
|
||||
@service toasts;
|
||||
@service router;
|
||||
|
||||
notificationLevels = NOTIFICATION_LEVELS;
|
||||
|
||||
|
@ -309,6 +311,12 @@ export default class ChatAboutScreen extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
onLeaveChannel(channel) {
|
||||
this.chatChannelsManager.remove(channel);
|
||||
return this.router.transitionTo("chat");
|
||||
}
|
||||
|
||||
@action
|
||||
onEditChannelDescription() {
|
||||
return this.modal.show(ChatModalEditChannelDescription, {
|
||||
|
@ -573,6 +581,7 @@ export default class ChatAboutScreen extends Component {
|
|||
<:action>
|
||||
<ToggleChannelMembershipButton
|
||||
@channel={{@channel}}
|
||||
@onLeave={{this.onLeaveChannel}}
|
||||
@options={{hash
|
||||
joinClass="btn-primary"
|
||||
leaveClass="btn-danger"
|
||||
|
|
|
@ -6,10 +6,13 @@ import DButton from "discourse/components/d-button";
|
|||
import concatClass from "discourse/helpers/concat-class";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export default class ToggleChannelMembershipButton extends Component {
|
||||
@service chat;
|
||||
@service chatApi;
|
||||
|
||||
@tracked isLoading = false;
|
||||
onToggle = null;
|
||||
|
||||
options = {};
|
||||
|
||||
constructor() {
|
||||
|
@ -54,7 +57,7 @@ export default class ToggleChannelMembershipButton extends Component {
|
|||
return this.chat
|
||||
.followChannel(this.args.channel)
|
||||
.then(() => {
|
||||
this.onToggle?.();
|
||||
this.args.onJoin?.(this.args.channel);
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => {
|
||||
|
@ -67,22 +70,22 @@ export default class ToggleChannelMembershipButton extends Component {
|
|||
}
|
||||
|
||||
@action
|
||||
onLeaveChannel() {
|
||||
async onLeaveChannel() {
|
||||
this.isLoading = true;
|
||||
|
||||
return this.chat
|
||||
.unfollowChannel(this.args.channel)
|
||||
.then(() => {
|
||||
this.onToggle?.();
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => {
|
||||
if (this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (this.args.channel.chatable.group) {
|
||||
await this.chatApi.leaveChannel(this.args.channel.id);
|
||||
} else {
|
||||
await this.chat.unfollowChannel(this.args.channel);
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
});
|
||||
this.args.onLeave?.(this.args.channel);
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
|
|
|
@ -211,7 +211,7 @@ export default {
|
|||
suffixCSSClass = "urgent";
|
||||
hoverType = "icon";
|
||||
hoverValue = "times";
|
||||
hoverTitle = I18n.t("chat.direct_messages.leave");
|
||||
hoverTitle = I18n.t("chat.direct_messages.close");
|
||||
|
||||
constructor({ channel, chatService, currentUser }) {
|
||||
super(...arguments);
|
||||
|
|
|
@ -34,6 +34,13 @@ export default function withChatChannel(extendedClass) {
|
|||
let { channelTitle } = this.paramsFor(this.routeName);
|
||||
|
||||
if (channelTitle && channelTitle !== model.slugifiedTitle) {
|
||||
if (this.routeName === "chat.channel.info") {
|
||||
return this.router.replaceWith(
|
||||
"chat.channel.info",
|
||||
...model.routeModels
|
||||
);
|
||||
}
|
||||
|
||||
const messageId = this.paramsFor("chat.channel.near-message").messageId;
|
||||
const threadId = this.paramsFor("chat.channel.thread").threadId;
|
||||
|
||||
|
|
|
@ -293,9 +293,19 @@ export default class ChatApi extends Service {
|
|||
* @returns {Promise}
|
||||
*/
|
||||
unfollowChannel(channelId) {
|
||||
return this.#deleteRequest(`/channels/${channelId}/memberships/me`).then(
|
||||
(result) => UserChatChannelMembership.create(result.membership)
|
||||
);
|
||||
return this.#deleteRequest(
|
||||
`/channels/${channelId}/memberships/me/follows`
|
||||
).then((result) => UserChatChannelMembership.create(result.membership));
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the membership of current user on a channel.
|
||||
*
|
||||
* @param {number} channelId - The ID of the channel.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
leaveChannel(channelId) {
|
||||
return this.#deleteRequest(`/channels/${channelId}/memberships/me`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -77,13 +77,15 @@ export default class ChatChannelsManager extends Service {
|
|||
}
|
||||
|
||||
async unfollow(model) {
|
||||
this.chatSubscriptionsManager.stopChannelSubscription(model);
|
||||
|
||||
return this.chatApi.unfollowChannel(model.id).then((membership) => {
|
||||
model.currentUserMembership = membership;
|
||||
|
||||
try {
|
||||
this.chatSubscriptionsManager.stopChannelSubscription(model);
|
||||
model.currentUserMembership = await this.chatApi.unfollowChannel(
|
||||
model.id
|
||||
);
|
||||
return model;
|
||||
});
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@debounce(300)
|
||||
|
|
|
@ -495,6 +495,7 @@ en:
|
|||
new: "Create a personal chat"
|
||||
create: "Go"
|
||||
leave: "Leave this personal chat"
|
||||
close: "Close this personal chat"
|
||||
cannot_create: "Sorry, you cannot send direct messages."
|
||||
|
||||
incoming_webhooks:
|
||||
|
|
|
@ -22,6 +22,9 @@ Chat::Engine.routes.draw do
|
|||
get "/channels/:channel_id/memberships" => "channels_memberships#index"
|
||||
post "/channels/:channel_id/memberships" => "channels_memberships#create"
|
||||
delete "/channels/:channel_id/memberships/me" => "channels_current_user_membership#destroy"
|
||||
delete "/channels/:channel_id/memberships/me/follows" =>
|
||||
"channels_current_user_membership_follows#destroy"
|
||||
put "/channels/:channel_id/memberships/me" => "channels_current_user_membership#update"
|
||||
post "/channels/:channel_id/memberships/me" => "channels_current_user_membership#create"
|
||||
put "/channels/:channel_id/notifications-settings/me" =>
|
||||
"channels_current_user_notifications_settings#update"
|
||||
|
|
|
@ -105,46 +105,19 @@ describe Chat::Api::ChannelsCurrentUserMembershipController do
|
|||
end
|
||||
|
||||
describe "#destroy" do
|
||||
context "when an existing membership exists" do
|
||||
before { channel_1.add(current_user) }
|
||||
describe "success" do
|
||||
it "works" do
|
||||
delete "/chat/api/channels/#{channel_1.id}/memberships/me"
|
||||
|
||||
it "enforces 'following' to false" do
|
||||
membership_record = channel_1.membership_for(current_user)
|
||||
|
||||
expect { delete "/chat/api/channels/#{channel_1.id}/memberships/me" }.to change {
|
||||
membership_record.reload.following
|
||||
}.to(false).from(true)
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body["membership"]["following"]).to eq(false)
|
||||
expect(response.parsed_body["membership"]["chat_channel_id"]).to eq(channel_1.id)
|
||||
expect(response.parsed_body["membership"]["user"]["id"]).to eq(current_user.id)
|
||||
end
|
||||
end
|
||||
|
||||
context "when current user can’t see the channel" do
|
||||
fab!(:channel_2) { Fabricate(:private_category_channel, group: Fabricate(:group)) }
|
||||
context "when channel is not found" do
|
||||
it "returns a 404" do
|
||||
delete "/chat/api/channels/-999/memberships/me"
|
||||
|
||||
it "fails" do
|
||||
expect { delete "/chat/api/channels/#{channel_2.id}/memberships/me" }.not_to change {
|
||||
Chat::UserChatChannelMembership.where(user_id: current_user.id).count
|
||||
}
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the channel is a direct message channel" do
|
||||
fab!(:dm_channel_1) { Fabricate(:direct_message_channel, users: [current_user]) }
|
||||
|
||||
it "enforces 'following' to false" do
|
||||
membership_record = dm_channel_1.membership_for(current_user)
|
||||
|
||||
expect { delete "/chat/api/channels/#{dm_channel_1.id}/memberships/me" }.to change {
|
||||
membership_record.reload.following
|
||||
}.to(false).from(true)
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body["membership"]["following"]).to eq(false)
|
||||
expect(response.parsed_body["membership"]["chat_channel_id"]).to eq(dm_channel_1.id)
|
||||
expect(response.parsed_body["membership"]["user"]["id"]).to eq(current_user.id)
|
||||
end
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
describe Chat::Api::ChannelsCurrentUserMembershipController do
|
||||
fab!(:current_user) { Fabricate(:user) }
|
||||
fab!(:channel_1) { Fabricate(:category_channel) }
|
||||
|
||||
before do
|
||||
channel_1.add(current_user)
|
||||
SiteSetting.chat_enabled = true
|
||||
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
|
||||
sign_in(current_user)
|
||||
end
|
||||
|
||||
describe "#destroy" do
|
||||
describe "success" do
|
||||
it "works" do
|
||||
delete "/chat/api/channels/#{channel_1.id}/memberships/me/follows"
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(channel_1.membership_for(current_user).following).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
context "when channel is not found" do
|
||||
it "returns a 404" do
|
||||
delete "/chat/api/channels/-999/memberships/me/follows"
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
116
plugins/chat/spec/services/chat/leave_channel_spec.rb
Normal file
116
plugins/chat/spec/services/chat/leave_channel_spec.rb
Normal file
|
@ -0,0 +1,116 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Chat::LeaveChannel do
|
||||
describe described_class::Contract, type: :model do
|
||||
subject(:contract) { described_class.new }
|
||||
|
||||
it { is_expected.to validate_presence_of(:channel_id) }
|
||||
end
|
||||
|
||||
describe ".call" do
|
||||
subject(:result) { described_class.call(params) }
|
||||
|
||||
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||
fab!(:current_user) { Fabricate(:user) }
|
||||
|
||||
let(:guardian) { Guardian.new(current_user) }
|
||||
let(:channel_id) { channel_1.id }
|
||||
|
||||
before { SiteSetting.direct_message_enabled_groups = Group::AUTO_GROUPS[:everyone] }
|
||||
|
||||
let(:params) { { guardian: guardian, channel_id: channel_id } }
|
||||
|
||||
context "when all steps pass" do
|
||||
context "when category channel" do
|
||||
context "with existing membership" do
|
||||
before do
|
||||
channel_1.add(current_user)
|
||||
Chat::Channel.ensure_consistency!
|
||||
end
|
||||
|
||||
it "unfollows the channel" do
|
||||
membership = channel_1.membership_for(current_user)
|
||||
|
||||
expect { result }.to change { membership.reload.following }.from(true).to(false)
|
||||
end
|
||||
|
||||
it "recomputes user count" do
|
||||
expect { result }.to change { channel_1.reload.user_count }.from(1).to(0)
|
||||
end
|
||||
end
|
||||
|
||||
context "with no existing membership" do
|
||||
it "does nothing" do
|
||||
expect { result }.to_not change { Chat::UserChatChannelMembership }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when group channel" do
|
||||
context "with existing membership" do
|
||||
fab!(:channel_1) do
|
||||
Fabricate(:direct_message_channel, group: true, users: [current_user, Fabricate(:user)])
|
||||
end
|
||||
|
||||
before { Chat::Channel.ensure_consistency! }
|
||||
|
||||
it "leaves the channel" do
|
||||
membership = channel_1.membership_for(current_user)
|
||||
|
||||
result
|
||||
|
||||
expect(Chat::UserChatChannelMembership.exists?(membership.id)).to eq(false)
|
||||
expect(
|
||||
channel_1.chatable.direct_message_users.where(user_id: current_user.id).exists?,
|
||||
).to eq(false)
|
||||
end
|
||||
|
||||
it "recomputes user count" do
|
||||
expect { result }.to change { channel_1.reload.user_count }.from(2).to(1)
|
||||
end
|
||||
end
|
||||
|
||||
context "with no existing membership" do
|
||||
it "does nothing" do
|
||||
expect { result }.to_not change { Chat::UserChatChannelMembership }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when direct channel" do
|
||||
context "with existing membership" do
|
||||
fab!(:channel_1) do
|
||||
Fabricate(:direct_message_channel, users: [current_user, Fabricate(:user)])
|
||||
end
|
||||
|
||||
before { Chat::Channel.ensure_consistency! }
|
||||
|
||||
it "unfollows the channel" do
|
||||
membership = channel_1.membership_for(current_user)
|
||||
|
||||
expect { result }.to change { membership.reload.following }.from(true).to(false)
|
||||
expect(
|
||||
channel_1.chatable.direct_message_users.where(user_id: current_user.id).exists?,
|
||||
).to eq(true)
|
||||
end
|
||||
|
||||
it "recomputes user count" do
|
||||
expect { result }.to_not change { channel_1.reload.user_count }
|
||||
end
|
||||
end
|
||||
|
||||
context "with no existing membership" do
|
||||
it "does nothing" do
|
||||
expect { result }.to_not change { Chat::UserChatChannelMembership }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when channel is not found" do
|
||||
before { params[:channel_id] = -999 }
|
||||
|
||||
it { is_expected.to fail_to_find_a_model(:channel) }
|
||||
end
|
||||
end
|
||||
end
|
47
plugins/chat/spec/services/chat/unfollow_channel_spec.rb
Normal file
47
plugins/chat/spec/services/chat/unfollow_channel_spec.rb
Normal file
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Chat::UnfollowChannel do
|
||||
describe described_class::Contract, type: :model do
|
||||
subject(:contract) { described_class.new }
|
||||
|
||||
it { is_expected.to validate_presence_of(:channel_id) }
|
||||
end
|
||||
|
||||
describe ".call" do
|
||||
subject(:result) { described_class.call(params) }
|
||||
|
||||
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||
fab!(:current_user) { Fabricate(:user) }
|
||||
|
||||
let(:guardian) { Guardian.new(current_user) }
|
||||
let(:channel_id) { channel_1.id }
|
||||
|
||||
before { SiteSetting.direct_message_enabled_groups = Group::AUTO_GROUPS[:everyone] }
|
||||
|
||||
let(:params) { { guardian: guardian, channel_id: channel_id } }
|
||||
|
||||
context "when all steps pass" do
|
||||
context "with existing membership" do
|
||||
before { channel_1.add(current_user) }
|
||||
|
||||
it "unfollows the channel" do
|
||||
membership = channel_1.membership_for(current_user)
|
||||
|
||||
expect { result }.to change { membership.reload.following }.from(true).to(false)
|
||||
end
|
||||
end
|
||||
|
||||
context "with no existing membership" do
|
||||
it "does nothing" do
|
||||
expect { result }.to_not change { Chat::UserChatChannelMembership }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when channel is not found" do
|
||||
before { params[:channel_id] = -999 }
|
||||
|
||||
it { is_expected.to fail_to_find_a_model(:channel) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -154,6 +154,37 @@ RSpec.describe "Channel - Info - Settings page", type: :system do
|
|||
expect(toasts).to have_success(I18n.t("js.saved"))
|
||||
}.to change { membership.reload.mobile_notification_level }.from("mention").to("never")
|
||||
end
|
||||
|
||||
it "can unfollow channel" do
|
||||
membership = channel_1.membership_for(current_user)
|
||||
|
||||
chat_page.visit_channel_settings(channel_1)
|
||||
click_button(I18n.t("js.chat.channel_settings.leave_channel"))
|
||||
|
||||
expect(page).to have_current_path("/chat/browse/open")
|
||||
expect(membership.reload.following).to eq(false)
|
||||
end
|
||||
|
||||
context "when group channel" do
|
||||
fab!(:channel_1) do
|
||||
Fabricate(:direct_message_channel, group: true, users: [current_user, Fabricate(:user)])
|
||||
end
|
||||
|
||||
before { channel_1.add(current_user) }
|
||||
|
||||
it "can leave channel" do
|
||||
membership = channel_1.membership_for(current_user)
|
||||
|
||||
chat_page.visit_channel_settings(channel_1)
|
||||
click_button(I18n.t("js.chat.channel_settings.leave_channel"))
|
||||
|
||||
expect(page).to have_current_path("/chat/browse/open")
|
||||
expect(Chat::UserChatChannelMembership.exists?(membership.id)).to eq(false)
|
||||
expect(
|
||||
channel_1.chatable.direct_message_users.where(user_id: current_user.id).exists?,
|
||||
).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "as staff" do
|
||||
|
|
Loading…
Reference in New Issue
Block a user