DEV: properly namespace chat (#20690)

This commit main goal was to comply with Zeitwerk and properly rely on autoloading. To achieve this, most resources have been namespaced under the `Chat` module.

- Given all models are now namespaced with `Chat::` and would change the stored types in DB when using polymorphism or STI (single table inheritance), this commit uses various Rails methods to ensure proper class is loaded and the stored name in DB is unchanged, eg: `Chat::Message` model will be stored as `"ChatMessage"`, and `"ChatMessage"` will correctly load `Chat::Message` model.
- Jobs are now using constants only, eg: `Jobs::Chat::Foo` and should only be enqueued this way

Notes:
- This commit also used this opportunity to limit the number of registered css files in plugin.rb
- `discourse_dev` support has been removed within this commit and will be reintroduced later

<!-- NOTE: All pull requests should have tests (rspec in Ruby, qunit in JavaScript). If your code does not include test coverage, please include an explanation of why it was omitted. -->
This commit is contained in:
Joffrey JAFFEUX
2023-03-17 14:24:38 +01:00
committed by GitHub
parent 74349e17c9
commit 12a18d4d55
343 changed files with 9077 additions and 8745 deletions

View File

@ -0,0 +1,285 @@
# frozen_string_literal: true
require "rails_helper"
describe Chat::Mailer do
fab!(:chatters_group) { Fabricate(:group) }
fab!(:sender) { Fabricate(:user, group_ids: [chatters_group.id]) }
fab!(:user_1) { Fabricate(:user, group_ids: [chatters_group.id], last_seen_at: 15.minutes.ago) }
fab!(:chat_channel) { Fabricate(:category_channel) }
fab!(:chat_message) { Fabricate(:chat_message, user: sender, chat_channel: chat_channel) }
fab!(:user_1_chat_channel_membership) do
Fabricate(
:user_chat_channel_membership,
user: user_1,
chat_channel: chat_channel,
last_read_message_id: nil,
)
end
fab!(:private_chat_channel) do
Group.refresh_automatic_groups!
Chat::DirectMessageChannelCreator.create!(acting_user: sender, target_users: [sender, user_1])
end
before do
SiteSetting.chat_enabled = true
SiteSetting.chat_allowed_groups = chatters_group.id
Fabricate(:user_chat_channel_membership, user: sender, chat_channel: chat_channel)
end
def assert_summary_skipped
expect(
job_enqueued?(job: :user_email, args: { type: "chat_summary", user_id: user_1.id }),
).to eq(false)
end
def assert_only_queued_once
expect_job_enqueued(job: :user_email, args: { type: "chat_summary", user_id: user_1.id })
expect(Jobs::UserEmail.jobs.size).to eq(1)
end
describe "for chat mentions" do
fab!(:mention) { Fabricate(:chat_mention, user: user_1, chat_message: chat_message) }
it "skips users without chat access" do
chatters_group.remove(user_1)
described_class.send_unread_mentions_summary
assert_summary_skipped
end
it "skips users with summaries disabled" do
user_1.user_option.update(chat_email_frequency: UserOption.chat_email_frequencies[:never])
described_class.send_unread_mentions_summary
assert_summary_skipped
end
it "skips a job if the user haven't read the channel since the last summary" do
user_1_chat_channel_membership.update!(last_unread_mention_when_emailed_id: chat_message.id)
described_class.send_unread_mentions_summary
assert_summary_skipped
end
it "skips without chat enabled" do
user_1.user_option.update(
chat_enabled: false,
chat_email_frequency: UserOption.chat_email_frequencies[:when_away],
)
described_class.send_unread_mentions_summary
assert_summary_skipped
end
it "queues a job for users that was mentioned and never read the channel before" do
described_class.send_unread_mentions_summary
assert_only_queued_once
end
it "skips the job when the user was mentioned but already read the message" do
user_1_chat_channel_membership.update!(last_read_message_id: chat_message.id)
described_class.send_unread_mentions_summary
assert_summary_skipped
end
it "skips the job when the user is not following a public channel anymore" do
user_1_chat_channel_membership.update!(
last_read_message_id: chat_message.id - 1,
following: false,
)
described_class.send_unread_mentions_summary
assert_summary_skipped
end
it "doesn’t skip the job when the user is not following a direct channel" do
private_chat_channel
.user_chat_channel_memberships
.where(user_id: user_1.id)
.update!(last_read_message_id: chat_message.id - 1, following: false)
described_class.send_unread_mentions_summary
assert_only_queued_once
end
it "skips users with unread messages from a different channel" do
user_1_chat_channel_membership.update!(last_read_message_id: chat_message.id)
second_channel = Fabricate(:category_channel)
Fabricate(
:user_chat_channel_membership,
user: user_1,
chat_channel: second_channel,
last_read_message_id: chat_message.id - 1,
)
described_class.send_unread_mentions_summary
assert_summary_skipped
end
it "only queues the job once for users who are member of multiple groups with chat access" do
chatters_group_2 = Fabricate(:group, users: [user_1])
SiteSetting.chat_allowed_groups = [chatters_group, chatters_group_2].map(&:id).join("|")
described_class.send_unread_mentions_summary
assert_only_queued_once
end
it "skips users when the mention was deleted" do
chat_message.trash!
described_class.send_unread_mentions_summary
assert_summary_skipped
end
it "queues the job if the user has unread mentions and already read all the messages in the previous summary" do
user_1_chat_channel_membership.update!(
last_read_message_id: chat_message.id,
last_unread_mention_when_emailed_id: chat_message.id,
)
unread_message = Fabricate(:chat_message, chat_channel: chat_channel, user: sender)
Fabricate(:chat_mention, user: user_1, chat_message: unread_message)
described_class.send_unread_mentions_summary
expect_job_enqueued(job: :user_email, args: { type: "chat_summary", user_id: user_1.id })
expect(Jobs::UserEmail.jobs.size).to eq(1)
end
it "skips users who were seen recently" do
user_1.update!(last_seen_at: 2.minutes.ago)
described_class.send_unread_mentions_summary
assert_summary_skipped
end
it "doesn't mix mentions from other users" do
mention.destroy!
user_2 = Fabricate(:user, groups: [chatters_group], last_seen_at: 20.minutes.ago)
Fabricate(
:user_chat_channel_membership,
user: user_2,
chat_channel: chat_channel,
last_read_message_id: nil,
)
new_message = Fabricate(:chat_message, chat_channel: chat_channel, user: sender)
Fabricate(:chat_mention, user: user_2, chat_message: new_message)
described_class.send_unread_mentions_summary
expect(
job_enqueued?(job: :user_email, args: { type: "chat_summary", user_id: user_1.id }),
).to eq(false)
expect_job_enqueued(job: :user_email, args: { type: "chat_summary", user_id: user_2.id })
expect(Jobs::UserEmail.jobs.size).to eq(1)
end
it "skips users when the message is older than 1 week" do
chat_message.update!(created_at: 1.5.weeks.ago)
described_class.send_unread_mentions_summary
assert_summary_skipped
end
it "queues a job when the chat_allowed_groups is set to everyone" do
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
described_class.send_unread_mentions_summary
assert_only_queued_once
end
describe "update the user membership after we send the email" do
before { Jobs.run_immediately! }
it "doesn't send the same summary the summary again if the user haven't read any channel messages since the last one" do
user_1_chat_channel_membership.update!(last_read_message_id: chat_message.id - 1)
described_class.send_unread_mentions_summary
expect(user_1_chat_channel_membership.reload.last_unread_mention_when_emailed_id).to eq(
chat_message.id,
)
another_channel_message = Fabricate(:chat_message, chat_channel: chat_channel, user: sender)
Fabricate(:chat_mention, user: user_1, chat_message: another_channel_message)
expect { described_class.send_unread_mentions_summary }.not_to change(
Jobs::UserEmail.jobs,
:size,
)
end
it "only updates the last_message_read_when_emailed_id on the channel with unread mentions" do
another_channel = Fabricate(:category_channel)
another_channel_message =
Fabricate(:chat_message, chat_channel: another_channel, user: sender)
Fabricate(:chat_mention, user: user_1, chat_message: another_channel_message)
another_channel_membership =
Fabricate(
:user_chat_channel_membership,
user: user_1,
chat_channel: another_channel,
last_read_message_id: another_channel_message.id,
)
user_1_chat_channel_membership.update!(last_read_message_id: chat_message.id - 1)
described_class.send_unread_mentions_summary
expect(user_1_chat_channel_membership.reload.last_unread_mention_when_emailed_id).to eq(
chat_message.id,
)
expect(another_channel_membership.reload.last_unread_mention_when_emailed_id).to be_nil
end
end
end
describe "for direct messages" do
before { Fabricate(:chat_message, user: sender, chat_channel: private_chat_channel) }
it "queue a job when the user has unread private mentions" do
described_class.send_unread_mentions_summary
assert_only_queued_once
end
it "only queues the job once when the user has mentions and private messages" do
Fabricate(:chat_mention, user: user_1, chat_message: chat_message)
described_class.send_unread_mentions_summary
assert_only_queued_once
end
it "doesn't mix or update mentions from other users when joining tables" do
user_2 = Fabricate(:user, groups: [chatters_group], last_seen_at: 20.minutes.ago)
user_2_membership =
Fabricate(
:user_chat_channel_membership,
user: user_2,
chat_channel: chat_channel,
last_read_message_id: chat_message.id,
)
Fabricate(:chat_mention, user: user_2, chat_message: chat_message)
described_class.send_unread_mentions_summary
assert_only_queued_once
expect(user_2_membership.reload.last_unread_mention_when_emailed_id).to be_nil
end
end
end

View File

@ -0,0 +1,952 @@
# frozen_string_literal: true
require "rails_helper"
describe Chat::MessageCreator do
fab!(:admin1) { Fabricate(:admin) }
fab!(:admin2) { Fabricate(:admin) }
fab!(:user1) { Fabricate(:user, group_ids: [Group::AUTO_GROUPS[:everyone]]) }
fab!(:user2) { Fabricate(:user) }
fab!(:user3) { Fabricate(:user) }
fab!(:user4) { Fabricate(:user) }
fab!(:admin_group) do
Fabricate(
:public_group,
users: [admin1, admin2],
mentionable_level: Group::ALIAS_LEVELS[:everyone],
)
end
fab!(:user_group) do
Fabricate(
:public_group,
users: [user1, user2, user3],
mentionable_level: Group::ALIAS_LEVELS[:everyone],
)
end
fab!(:user_without_memberships) { Fabricate(:user) }
fab!(:public_chat_channel) { Fabricate(:category_channel) }
fab!(:dm_chat_channel) do
Fabricate(
:direct_message_channel,
chatable: Fabricate(:direct_message, users: [user1, user2, user3]),
)
end
let(:direct_message_channel) do
Chat::DirectMessageChannelCreator.create!(acting_user: user1, target_users: [user1, user2])
end
before do
SiteSetting.chat_enabled = true
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
SiteSetting.chat_duplicate_message_sensitivity = 0
# Create channel memberships
[admin1, admin2, user1, user2, user3].each do |user|
Fabricate(:user_chat_channel_membership, chat_channel: public_chat_channel, user: user)
end
Group.refresh_automatic_groups!
direct_message_channel
end
describe "Integration tests with jobs running immediately" do
before { Jobs.run_immediately! }
it "errors when length is less than `chat_minimum_message_length`" do
SiteSetting.chat_minimum_message_length = 10
creator =
described_class.create(chat_channel: public_chat_channel, user: user1, content: "2 short")
expect(creator.failed?).to eq(true)
expect(creator.error.message).to match(
I18n.t(
"chat.errors.minimum_length_not_met",
{ count: SiteSetting.chat_minimum_message_length },
),
)
end
it "errors when length is greater than `chat_maximum_message_length`" do
SiteSetting.chat_maximum_message_length = 100
creator =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "a really long and in depth message that is just too detailed" * 100,
)
expect(creator.failed?).to eq(true)
expect(creator.error.message).to match(
I18n.t("chat.errors.message_too_long", { count: SiteSetting.chat_maximum_message_length }),
)
end
it "allows message creation when length is less than `chat_minimum_message_length` when upload is present" do
upload = Fabricate(:upload, user: user1)
SiteSetting.chat_minimum_message_length = 10
expect {
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "2 short",
upload_ids: [upload.id],
)
}.to change { Chat::Message.count }.by(1)
end
it "creates messages for users who can see the channel" do
expect {
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "this is a message",
)
}.to change { Chat::Message.count }.by(1)
end
it "updates the channel’s last message date" do
previous_last_message_sent_at = public_chat_channel.last_message_sent_at
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "this is a message",
)
expect(previous_last_message_sent_at).to be < public_chat_channel.reload.last_message_sent_at
end
it "sets the last_editor_id to the user who created the message" do
message =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "this is a message",
).chat_message
expect(message.last_editor_id).to eq(user1.id)
end
it "publishes a DiscourseEvent for new messages" do
events =
DiscourseEvent.track_events do
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "this is a message",
)
end
expect(events.map { _1[:event_name] }).to include(:chat_message_created)
end
it "creates mentions and mention notifications for public chat" do
message =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content:
"this is a @#{user1.username} message with @system @mentions @#{user2.username} and @#{user3.username}",
).chat_message
# a mention for the user himself wasn't created
user1_mention = user1.chat_mentions.where(chat_message: message).first
expect(user1_mention).to be_nil
system_user_mention = Discourse.system_user.chat_mentions.where(chat_message: message).first
expect(system_user_mention).to be_nil
user2_mention = user2.chat_mentions.where(chat_message: message).first
expect(user2_mention).to be_present
expect(user2_mention.notification).to be_present
user3_mention = user3.chat_mentions.where(chat_message: message).first
expect(user3_mention).to be_present
expect(user3_mention.notification).to be_present
end
it "mentions are case insensitive" do
expect {
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "Hey @#{user2.username.upcase}",
)
}.to change { user2.chat_mentions.count }.by(1)
end
it "notifies @all properly" do
expect {
described_class.create(chat_channel: public_chat_channel, user: user1, content: "@all")
}.to change { Chat::Mention.count }.by(4)
Chat::UserChatChannelMembership.where(
user: user2,
chat_channel: public_chat_channel,
).update_all(following: false)
expect {
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "again! @all",
)
}.to change { Chat::Mention.count }.by(3)
end
it "notifies @here properly" do
admin1.update(last_seen_at: 1.year.ago)
admin2.update(last_seen_at: 1.year.ago)
user1.update(last_seen_at: Time.now)
user2.update(last_seen_at: Time.now)
user3.update(last_seen_at: Time.now)
expect {
described_class.create(chat_channel: public_chat_channel, user: user1, content: "@here")
}.to change { Chat::Mention.count }.by(2)
end
it "doesn't sent double notifications when '@here' is mentioned" do
user2.update(last_seen_at: Time.now)
expect {
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "@here @#{user2.username}",
)
}.to change { user2.chat_mentions.count }.by(1)
end
it "notifies @here plus other mentions" do
admin1.update(last_seen_at: Time.now)
admin2.update(last_seen_at: 1.year.ago)
user1.update(last_seen_at: 1.year.ago)
user2.update(last_seen_at: 1.year.ago)
user3.update(last_seen_at: 1.year.ago)
expect {
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "@here plus @#{user3.username}",
)
}.to change { user3.chat_mentions.count }.by(1)
end
it "doesn't create mention notifications for users without a membership record" do
message =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "hello @#{user_without_memberships.username}",
).chat_message
mention = user_without_memberships.chat_mentions.where(chat_message: message).first
expect(mention.notification).to be_nil
end
it "doesn't create mention notifications for users who cannot chat" do
new_group = Group.create
SiteSetting.chat_allowed_groups = new_group.id
message =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "hi @#{user2.username} @#{user3.username}",
).chat_message
user2_mention = user2.chat_mentions.where(chat_message: message).first
expect(user2_mention.notification).to be_nil
user3_mention = user2.chat_mentions.where(chat_message: message).first
expect(user3_mention.notification).to be_nil
end
it "doesn't create mentions for users with chat disabled" do
user2.user_option.update(chat_enabled: false)
message =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "hi @#{user2.username}",
).chat_message
mention = user2.chat_mentions.where(chat_message: message).first
expect(mention).to be_nil
end
it "creates only mention notifications for users with access in private chat" do
message =
described_class.create(
chat_channel: direct_message_channel,
user: user1,
content: "hello there @#{user2.username} and @#{user3.username}",
).chat_message
# Only user2 should be notified
user2_mention = user2.chat_mentions.where(chat_message: message).first
expect(user2_mention.notification).to be_present
user3_mention = user3.chat_mentions.where(chat_message: message).first
expect(user3_mention.notification).to be_nil
end
it "creates a mention for group users even if they're not participating in private chat" do
expect {
described_class.create(
chat_channel: direct_message_channel,
user: user1,
content: "hello there @#{user_group.name}",
)
}.to change { user2.chat_mentions.count }.by(1).and change { user3.chat_mentions.count }.by(1)
end
it "creates a mention notifications only for group users that are participating in private chat" do
message =
described_class.create(
chat_channel: direct_message_channel,
user: user1,
content: "hello there @#{user_group.name}",
).chat_message
user2_mention = user2.chat_mentions.where(chat_message: message).first
expect(user2_mention.notification).to be_present
user3_mention = user3.chat_mentions.where(chat_message: message).first
expect(user3_mention.notification).to be_nil
end
it "publishes inaccessible mentions when user isn't aren't a part of the channel" do
Chat::Publisher.expects(:publish_inaccessible_mentions).once
described_class.create(
chat_channel: public_chat_channel,
user: admin1,
content: "hello @#{user4.username}",
)
end
it "publishes inaccessible mentions when user doesn't have chat access" do
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:staff]
Chat::Publisher.expects(:publish_inaccessible_mentions).once
described_class.create(
chat_channel: public_chat_channel,
user: admin1,
content: "hello @#{user3.username}",
)
end
it "doesn't publish inaccessible mentions when user is following channel" do
Chat::Publisher.expects(:publish_inaccessible_mentions).never
described_class.create(
chat_channel: public_chat_channel,
user: admin1,
content: "hello @#{admin2.username}",
)
end
it "creates mentions for suspended users" do
user2.update(suspended_till: Time.now + 10.years)
message =
described_class.create(
chat_channel: direct_message_channel,
user: user1,
content: "hello @#{user2.username}",
).chat_message
mention = user2.chat_mentions.where(chat_message: message).first
expect(mention).to be_present
end
it "does not create mention notifications for suspended users" do
user2.update(suspended_till: Time.now + 10.years)
message =
described_class.create(
chat_channel: direct_message_channel,
user: user1,
content: "hello @#{user2.username}",
).chat_message
mention = user2.chat_mentions.where(chat_message: message).first
expect(mention.notification).to be_nil
end
context "when ignore_channel_wide_mention is enabled" do
before { user2.user_option.update(ignore_channel_wide_mention: true) }
it "when mentioning @all creates a mention without notification" do
message =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "hi! @all",
).chat_message
mention = user2.chat_mentions.where(chat_message: message).first
expect(mention).to be_present
expect(mention.notification).to be_nil
end
it "when mentioning @here creates a mention without notification" do
user2.update(last_seen_at: Time.now)
message =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "@here",
).chat_message
mention = user2.chat_mentions.where(chat_message: message).first
expect(mention).to be_present
expect(mention.notification).to be_nil
end
end
describe "replies" do
fab!(:reply_message) do
Fabricate(:chat_message, chat_channel: public_chat_channel, user: user2)
end
fab!(:unrelated_message_1) { Fabricate(:chat_message, chat_channel: public_chat_channel) }
fab!(:unrelated_message_2) { Fabricate(:chat_message, chat_channel: public_chat_channel) }
it "links the message that the user is replying to" do
message =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "this is a message",
in_reply_to_id: reply_message.id,
).chat_message
expect(message.in_reply_to_id).to eq(reply_message.id)
end
it "creates a thread and includes the original message and the reply" do
message = nil
expect {
message =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "this is a message",
in_reply_to_id: reply_message.id,
).chat_message
}.to change { Chat::Thread.count }.by(1)
expect(message.reload.thread).not_to eq(nil)
expect(message.in_reply_to.thread).to eq(message.thread)
expect(message.thread.original_message).to eq(reply_message)
expect(message.thread.original_message_user).to eq(reply_message.user)
end
context "when the thread_id is provided" do
fab!(:existing_thread) { Fabricate(:chat_thread, channel: public_chat_channel) }
it "does not create a thread when one is passed in" do
message = nil
expect {
message =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "this is a message",
thread_id: existing_thread.id,
).chat_message
}.not_to change { Chat::Thread.count }
expect(message.reload.thread).to eq(existing_thread)
end
it "errors when the thread ID is for a different channel" do
other_channel_thread = Fabricate(:chat_thread, channel: Fabricate(:chat_channel))
result =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "this is a message",
thread_id: other_channel_thread.id,
)
expect(result.error.message).to eq(I18n.t("chat.errors.thread_invalid_for_channel"))
end
it "errors when the thread does not match the in_reply_to thread" do
reply_message.update!(thread: existing_thread)
result =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "this is a message",
in_reply_to_id: reply_message.id,
thread_id: Fabricate(:chat_thread, channel: public_chat_channel).id,
)
expect(result.error.message).to eq(I18n.t("chat.errors.thread_does_not_match_parent"))
end
it "errors when the root message does not have a thread ID" do
reply_message.update!(thread: nil)
result =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "this is a message",
in_reply_to_id: reply_message.id,
thread_id: existing_thread.id,
)
expect(result.error.message).to eq(I18n.t("chat.errors.thread_does_not_match_parent"))
end
end
context "for missing root messages" do
fab!(:original_message) do
Fabricate(
:chat_message,
chat_channel: public_chat_channel,
user: user2,
created_at: 1.day.ago,
)
end
before { reply_message.update!(in_reply_to: original_message) }
it "raises an error when the root message has been trashed" do
original_message.trash!
result =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "this is a message",
in_reply_to_id: reply_message.id,
)
expect(result.error.message).to eq(I18n.t("chat.errors.original_message_not_found"))
end
it "uses the next message in the chain as the root when the root is deleted" do
original_message.destroy!
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "this is a message",
in_reply_to_id: reply_message.id,
)
expect(reply_message.reload.thread).not_to eq(nil)
end
end
context "when there is an existing reply chain" do
fab!(:old_message_1) do
Fabricate(
:chat_message,
chat_channel: public_chat_channel,
user: user1,
created_at: 6.hours.ago,
)
end
fab!(:old_message_2) do
Fabricate(
:chat_message,
chat_channel: public_chat_channel,
user: user2,
in_reply_to: old_message_1,
created_at: 4.hours.ago,
)
end
fab!(:old_message_3) do
Fabricate(
:chat_message,
chat_channel: public_chat_channel,
user: user1,
in_reply_to: old_message_2,
created_at: 1.hour.ago,
)
end
before do
reply_message.update!(
created_at: old_message_3.created_at + 1.hour,
in_reply_to: old_message_3,
)
end
it "creates a thread and updates all the messages in the chain" do
thread_count = Chat::Thread.count
message =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "this is a message",
in_reply_to_id: reply_message.id,
).chat_message
expect(Chat::Thread.count).to eq(thread_count + 1)
expect(message.reload.thread).not_to eq(nil)
expect(message.reload.in_reply_to.thread).to eq(message.thread)
expect(old_message_1.reload.thread).to eq(message.thread)
expect(old_message_2.reload.thread).to eq(message.thread)
expect(old_message_3.reload.thread).to eq(message.thread)
expect(message.thread.chat_messages.count).to eq(5)
message =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "this is a message",
in_reply_to_id: reply_message.id,
).chat_message
end
context "when a thread already exists and the thread_id is passed in" do
let!(:last_message) do
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "this is a message",
in_reply_to_id: reply_message.id,
).chat_message
end
let!(:existing_thread) { last_message.reload.thread }
it "does not create a new thread" do
thread_count = Chat::Thread.count
message =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "this is a message again",
in_reply_to_id: last_message.id,
thread_id: existing_thread.id,
).chat_message
expect(Chat::Thread.count).to eq(thread_count)
expect(message.reload.thread).to eq(existing_thread)
expect(message.reload.in_reply_to.thread).to eq(existing_thread)
expect(message.thread.chat_messages.count).to eq(6)
end
it "errors when the thread does not match the root thread" do
old_message_1.update!(thread: Fabricate(:chat_thread, channel: public_chat_channel))
result =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "this is a message",
in_reply_to_id: reply_message.id,
thread_id: existing_thread.id,
)
expect(result.error.message).to eq(I18n.t("chat.errors.thread_does_not_match_parent"))
end
it "errors when the root message does not have a thread ID" do
old_message_1.update!(thread: nil)
result =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "this is a message",
in_reply_to_id: reply_message.id,
thread_id: existing_thread.id,
)
expect(result.error.message).to eq(I18n.t("chat.errors.thread_does_not_match_parent"))
end
end
context "when there are hundreds of messages in a reply chain already" do
before do
previous_message = nil
1000.times do |i|
previous_message =
Fabricate(
:chat_message,
chat_channel: public_chat_channel,
user: [user1, user2].sample,
in_reply_to: previous_message,
created_at: i.hours.ago,
)
end
@last_message_in_chain = previous_message
end
xit "works" do
thread_count = Chat::Thread.count
message = nil
puts Benchmark.measure {
message =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "this is a message",
in_reply_to_id: @last_message_in_chain.id,
).chat_message
}
expect(Chat::Thread.count).to eq(thread_count + 1)
expect(message.reload.thread).not_to eq(nil)
expect(message.reload.in_reply_to.thread).to eq(message.thread)
expect(message.thread.chat_messages.count).to eq(1001)
end
end
context "if the root message alread had a thread" do
fab!(:old_thread) { Fabricate(:chat_thread, original_message: old_message_1) }
fab!(:incorrect_thread) { Fabricate(:chat_thread, channel: public_chat_channel) }
before do
old_message_1.update!(thread: old_thread)
old_message_3.update!(thread: incorrect_thread)
end
it "does not change any messages in the chain, assumes they have the correct thread ID" do
thread_count = Chat::Thread.count
message =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "this is a message",
in_reply_to_id: reply_message.id,
).chat_message
expect(Chat::Thread.count).to eq(thread_count)
expect(message.reload.thread).to eq(old_thread)
expect(message.reload.in_reply_to.thread).to eq(old_thread)
expect(old_message_1.reload.thread).to eq(old_thread)
expect(old_message_2.reload.thread).to eq(old_thread)
expect(old_message_3.reload.thread).to eq(incorrect_thread)
expect(message.thread.chat_messages.count).to eq(4)
end
end
end
end
describe "group mentions" do
it "creates chat mentions for group mentions where the group is mentionable" do
expect {
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "hello @#{admin_group.name}",
)
}.to change { admin1.chat_mentions.count }.by(1).and change {
admin2.chat_mentions.count
}.by(1)
end
it "doesn't mention users twice if they are direct mentioned and group mentioned" do
expect {
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "hello @#{admin_group.name} @#{admin1.username} and @#{admin2.username}",
)
}.to change { admin1.chat_mentions.count }.by(1).and change {
admin2.chat_mentions.count
}.by(1)
end
it "creates chat mentions for group mentions and direct mentions" do
expect {
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "hello @#{admin_group.name} @#{user2.username}",
)
}.to change { admin1.chat_mentions.count }.by(1).and change {
admin2.chat_mentions.count
}.by(1).and change { user2.chat_mentions.count }.by(1)
end
it "creates chat mentions for group mentions and direct mentions" do
expect {
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "hello @#{admin_group.name} @#{user_group.name}",
)
}.to change { admin1.chat_mentions.count }.by(1).and change {
admin2.chat_mentions.count
}.by(1).and change { user2.chat_mentions.count }.by(1).and change {
user3.chat_mentions.count
}.by(1)
end
it "doesn't create chat mentions for group mentions where the group is un-mentionable" do
admin_group.update(mentionable_level: Group::ALIAS_LEVELS[:nobody])
expect {
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "hello @#{admin_group.name}",
)
}.not_to change { Chat::Mention.count }
end
end
describe "push notifications" do
before do
Chat::UserChatChannelMembership.where(
user: user1,
chat_channel: public_chat_channel,
).update(
mobile_notification_level: Chat::UserChatChannelMembership::NOTIFICATION_LEVELS[:always],
)
PresenceChannel.clear_all!
end
it "sends a push notification to watching users who are not in chat" do
PostAlerter.expects(:push_notification).once
described_class.create(chat_channel: public_chat_channel, user: user2, content: "Beep boop")
end
it "does not send a push notification to watching users who are in chat" do
PresenceChannel.new("/chat/online").present(user_id: user1.id, client_id: 1)
PostAlerter.expects(:push_notification).never
described_class.create(chat_channel: public_chat_channel, user: user2, content: "Beep boop")
end
end
describe "with uploads" do
fab!(:upload1) { Fabricate(:upload, user: user1) }
fab!(:upload2) { Fabricate(:upload, user: user1) }
fab!(:private_upload) { Fabricate(:upload, user: user2) }
it "can attach 1 upload to a new message" do
expect {
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "Beep boop",
upload_ids: [upload1.id],
)
}.to not_change { chat_upload_count([upload1]) }.and change {
UploadReference.where(upload_id: upload1.id).count
}.by(1)
end
it "can attach multiple uploads to a new message" do
expect {
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "Beep boop",
upload_ids: [upload1.id, upload2.id],
)
}.to not_change { chat_upload_count([upload1, upload2]) }.and change {
UploadReference.where(upload_id: [upload1.id, upload2.id]).count
}.by(2)
end
it "filters out uploads that weren't uploaded by the user" do
expect {
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "Beep boop",
upload_ids: [private_upload.id],
)
}.not_to change { chat_upload_count([private_upload]) }
end
it "doesn't attach uploads when `chat_allow_uploads` is false" do
SiteSetting.chat_allow_uploads = false
expect {
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "Beep boop",
upload_ids: [upload1.id],
)
}.to not_change { chat_upload_count([upload1]) }.and not_change {
UploadReference.where(upload_id: upload1.id).count
}
end
end
end
it "destroys draft after message was created" do
Chat::Draft.create!(user: user1, chat_channel: public_chat_channel, data: "{}")
expect do
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "Hi @#{user2.username}",
)
end.to change { Chat::Draft.count }.by(-1)
end
describe "watched words" do
fab!(:watched_word) { Fabricate(:watched_word) }
it "errors when a blocked word is present" do
creator =
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "bad word - #{watched_word.word}",
)
expect(creator.failed?).to eq(true)
expect(creator.error.message).to match(
I18n.t("contains_blocked_word", { word: watched_word.word }),
)
end
end
describe "channel statuses" do
def create_message(user)
described_class.create(chat_channel: public_chat_channel, user: user, content: "test message")
end
context "when channel is closed" do
before { public_chat_channel.update(status: :closed) }
it "errors when trying to create the message for non-staff" do
creator = create_message(user1)
expect(creator.failed?).to eq(true)
expect(creator.error.message).to eq(
I18n.t("chat.errors.channel_new_message_disallowed.closed"),
)
end
it "does not error when trying to create a message for staff" do
expect { create_message(admin1) }.to change { Chat::Message.count }.by(1)
end
end
context "when channel is read_only" do
before { public_chat_channel.update(status: :read_only) }
it "errors when trying to create the message for all users" do
creator = create_message(user1)
expect(creator.failed?).to eq(true)
expect(creator.error.message).to eq(
I18n.t("chat.errors.channel_new_message_disallowed.read_only"),
)
creator = create_message(admin1)
expect(creator.failed?).to eq(true)
expect(creator.error.message).to eq(
I18n.t("chat.errors.channel_new_message_disallowed.read_only"),
)
end
end
context "when channel is archived" do
before { public_chat_channel.update(status: :archived) }
it "errors when trying to create the message for all users" do
creator = create_message(user1)
expect(creator.failed?).to eq(true)
expect(creator.error.message).to eq(
I18n.t("chat.errors.channel_new_message_disallowed.archived"),
)
creator = create_message(admin1)
expect(creator.failed?).to eq(true)
expect(creator.error.message).to eq(
I18n.t("chat.errors.channel_new_message_disallowed.archived"),
)
end
end
end
# TODO (martin) Remove this when we remove ChatUpload completely, 2023-04-01
def chat_upload_count(uploads)
DB.query_single(
"SELECT COUNT(*) FROM chat_uploads WHERE upload_id IN (#{uploads.map(&:id).join(",")})",
).first
end
end

View File

@ -0,0 +1,74 @@
# frozen_string_literal: true
require "rails_helper"
describe Chat::MessageRateLimiter do
fab!(:user) { Fabricate(:user, trust_level: 3) }
let(:limiter) { described_class.new(user) }
before do
freeze_time
RateLimiter.enable
SiteSetting.chat_allowed_messages_for_trust_level_0 = 1
SiteSetting.chat_allowed_messages_for_other_trust_levels = 2
SiteSetting.chat_auto_silence_duration = 30
end
after { limiter.clear! }
it "does nothing when rate limits are not exceeded" do
limiter.run!
expect(user.reload.silenced?).to be false
end
it "silences the user for the correct amount of time when they exceed the limit" do
2.times do
limiter.run!
expect(user.reload.silenced?).to be false
end
expect { limiter.run! }.to raise_error(RateLimiter::LimitExceeded)
expect(user.reload.silenced?).to be true
expect(user.silenced_till).to be_within(0.1).of(30.minutes.from_now)
end
it "silences the user correctly based on trust level" do
user.update(trust_level: 0) # Should only be able to run once without hitting limit
limiter.run!
expect(user.reload.silenced?).to be false
expect { limiter.run! }.to raise_error(RateLimiter::LimitExceeded)
expect(user.reload.silenced?).to be true
end
it "doesn't hit limit if site setting for allowed messages equals 0" do
SiteSetting.chat_allowed_messages_for_other_trust_levels = 0
5.times do
limiter.run!
expect(user.reload.silenced?).to be false
end
end
it "doesn't silence the user even when the limit is broken if auto_silence_duration is set to 0" do
SiteSetting.chat_allowed_messages_for_other_trust_levels = 1
SiteSetting.chat_auto_silence_duration = 0
limiter.run!
expect(user.reload.silenced?).to be false
expect { limiter.run! }.to raise_error(RateLimiter::LimitExceeded)
expect(user.reload.silenced?).to be false
end
it "logs a staff action when the user is silenced" do
SiteSetting.chat_allowed_messages_for_other_trust_levels = 1
limiter.run!
expect { limiter.run! }.to raise_error(RateLimiter::LimitExceeded).and change {
UserHistory.where(
target_user: user,
acting_user: Discourse.system_user,
action: UserHistory.actions[:silence_user],
).count
}.by(1)
end
end

View File

@ -0,0 +1,586 @@
# frozen_string_literal: true
require "rails_helper"
describe Chat::MessageUpdater do
let(:guardian) { Guardian.new(user1) }
fab!(:admin1) { Fabricate(:admin) }
fab!(:admin2) { Fabricate(:admin) }
fab!(:user1) { Fabricate(:user) }
fab!(:user2) { Fabricate(:user) }
fab!(:user3) { Fabricate(:user) }
fab!(:user4) { Fabricate(:user) }
fab!(:admin_group) do
Fabricate(
:public_group,
users: [admin1, admin2],
mentionable_level: Group::ALIAS_LEVELS[:everyone],
)
end
fab!(:user_without_memberships) { Fabricate(:user) }
fab!(:public_chat_channel) { Fabricate(:category_channel) }
before do
SiteSetting.chat_enabled = true
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
SiteSetting.chat_duplicate_message_sensitivity = 0
Jobs.run_immediately!
[admin1, admin2, user1, user2, user3, user4].each do |user|
Fabricate(:user_chat_channel_membership, chat_channel: public_chat_channel, user: user)
end
Group.refresh_automatic_groups!
@direct_message_channel =
Chat::DirectMessageChannelCreator.create!(acting_user: user1, target_users: [user1, user2])
end
def create_chat_message(user, message, channel, upload_ids: nil)
creator =
Chat::MessageCreator.create(
chat_channel: channel,
user: user,
in_reply_to_id: nil,
content: message,
upload_ids: upload_ids,
)
creator.chat_message
end
it "errors when length is less than `chat_minimum_message_length`" do
SiteSetting.chat_minimum_message_length = 10
og_message = "This won't be changed!"
chat_message = create_chat_message(user1, og_message, public_chat_channel)
new_message = "2 short"
updater =
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: new_message,
)
expect(updater.failed?).to eq(true)
expect(updater.error.message).to match(
I18n.t(
"chat.errors.minimum_length_not_met",
{ count: SiteSetting.chat_minimum_message_length },
),
)
expect(chat_message.reload.message).to eq(og_message)
end
it "errors when length is greater than `chat_maximum_message_length`" do
SiteSetting.chat_maximum_message_length = 100
og_message = "This won't be changed!"
chat_message = create_chat_message(user1, og_message, public_chat_channel)
new_message = "2 long" * 100
updater =
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: new_message,
)
expect(updater.failed?).to eq(true)
expect(updater.error.message).to match(
I18n.t("chat.errors.message_too_long", { count: SiteSetting.chat_maximum_message_length }),
)
expect(chat_message.reload.message).to eq(og_message)
end
it "errors if a user other than the message user is trying to edit the message" do
og_message = "This won't be changed!"
chat_message = create_chat_message(user1, og_message, public_chat_channel)
new_message = "2 short"
updater =
Chat::MessageUpdater.update(
guardian: Guardian.new(Fabricate(:user)),
chat_message: chat_message,
new_content: new_message,
)
expect(updater.failed?).to eq(true)
expect(updater.error).to match(Discourse::InvalidAccess)
end
it "it updates a messages content" do
chat_message = create_chat_message(user1, "This will be changed", public_chat_channel)
new_message = "Change to this!"
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: new_message,
)
expect(chat_message.reload.message).to eq(new_message)
end
it "publishes a DiscourseEvent for updated messages" do
chat_message = create_chat_message(user1, "This will be changed", public_chat_channel)
events =
DiscourseEvent.track_events do
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: "Change to this!",
)
end
expect(events.map { _1[:event_name] }).to include(:chat_message_edited)
end
it "creates mention notifications for unmentioned users" do
chat_message = create_chat_message(user1, "This will be changed", public_chat_channel)
expect {
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content:
"this is a message with @system @mentions @#{user2.username} and @#{user3.username}",
)
}.to change { user2.chat_mentions.count }.by(1).and change { user3.chat_mentions.count }.by(1)
end
it "doesn't create mentions for already mentioned users" do
message = "ping @#{user2.username} @#{user3.username}"
chat_message = create_chat_message(user1, message, public_chat_channel)
expect {
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: message + " editedddd",
)
}.not_to change { Chat::Mention.count }
end
it "doesn't create mention notification for users without access" do
message = "ping"
chat_message = create_chat_message(user1, message, public_chat_channel)
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: message + " @#{user_without_memberships.username}",
)
mention = user_without_memberships.chat_mentions.where(chat_message: chat_message).first
expect(mention.notification).to be_nil
end
it "destroys mention notifications that should be removed" do
chat_message =
create_chat_message(user1, "ping @#{user2.username} @#{user3.username}", public_chat_channel)
expect {
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: "ping @#{user3.username}",
)
}.to change { user2.chat_mentions.count }.by(-1).and not_change { user3.chat_mentions.count }
end
it "creates new, leaves existing, and removes old mentions all at once" do
chat_message =
create_chat_message(user1, "ping @#{user2.username} @#{user3.username}", public_chat_channel)
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: "ping @#{user3.username} @#{user4.username}",
)
expect(user2.chat_mentions.where(chat_message: chat_message)).not_to be_present
expect(user3.chat_mentions.where(chat_message: chat_message)).to be_present
expect(user4.chat_mentions.where(chat_message: chat_message)).to be_present
end
it "doesn't create mention notification in direct message for users without access" do
message = create_chat_message(user1, "ping nobody", @direct_message_channel)
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: message,
new_content: "ping @#{admin1.username}",
)
mention = admin1.chat_mentions.where(chat_message: message).first
expect(mention.notification).to be_nil
end
describe "group mentions" do
it "creates group mentions on update" do
chat_message = create_chat_message(user1, "ping nobody", public_chat_channel)
expect {
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: "ping @#{admin_group.name}",
)
}.to change { Chat::Mention.where(chat_message: chat_message).count }.by(2)
expect(admin1.chat_mentions.where(chat_message: chat_message)).to be_present
expect(admin2.chat_mentions.where(chat_message: chat_message)).to be_present
end
it "doesn't duplicate mentions when the user is already direct mentioned and then group mentioned" do
chat_message = create_chat_message(user1, "ping @#{admin2.username}", public_chat_channel)
expect {
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: "ping @#{admin_group.name} @#{admin2.username}",
)
}.to change { admin1.chat_mentions.count }.by(1).and not_change { admin2.chat_mentions.count }
end
it "deletes old mentions when group mention is removed" do
chat_message = create_chat_message(user1, "ping @#{admin_group.name}", public_chat_channel)
expect {
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: "ping nobody anymore!",
)
}.to change { Chat::Mention.where(chat_message: chat_message).count }.by(-2)
expect(admin1.chat_mentions.where(chat_message: chat_message)).not_to be_present
expect(admin2.chat_mentions.where(chat_message: chat_message)).not_to be_present
end
end
it "creates a chat_message_revision record and sets last_editor_id for the message" do
old_message = "It's a thrsday!"
new_message = "It's a thursday!"
chat_message = create_chat_message(user1, old_message, public_chat_channel)
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: new_message,
)
revision = chat_message.revisions.last
expect(revision.old_message).to eq(old_message)
expect(revision.new_message).to eq(new_message)
expect(revision.user_id).to eq(guardian.user.id)
expect(chat_message.reload.last_editor_id).to eq(guardian.user.id)
end
describe "duplicates" do
fab!(:upload1) { Fabricate(:upload, user: user1) }
fab!(:upload2) { Fabricate(:upload, user: user1) }
before do
SiteSetting.chat_duplicate_message_sensitivity = 1.0
public_chat_channel.update!(user_count: 50)
end
it "errors when editing the message to be the same as one that was posted recently" do
chat_message_1 = create_chat_message(user1, "this is some chat message", public_chat_channel)
chat_message_2 =
create_chat_message(
Fabricate(:user),
"another different chat message here",
public_chat_channel,
)
chat_message_1.update!(created_at: 30.seconds.ago)
chat_message_2.update!(created_at: 20.seconds.ago)
updater =
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message_1,
new_content: "another different chat message here",
)
expect(updater.failed?).to eq(true)
expect(updater.error.message).to eq(I18n.t("chat.errors.duplicate_message"))
end
it "does not count the message as a duplicate when editing leaves the message the same but changes uploads" do
chat_message =
create_chat_message(
user1,
"this is some chat message",
public_chat_channel,
upload_ids: [upload1.id, upload2.id],
)
chat_message.update!(created_at: 30.seconds.ago)
updater =
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: "this is some chat message",
upload_ids: [upload2.id],
)
expect(updater.failed?).to eq(false)
expect(chat_message.reload.uploads.count).to eq(1)
end
end
describe "uploads" do
fab!(:upload1) { Fabricate(:upload, user: user1) }
fab!(:upload2) { Fabricate(:upload, user: user1) }
it "does nothing if the passed in upload_ids match the existing upload_ids" do
chat_message =
create_chat_message(
user1,
"something",
public_chat_channel,
upload_ids: [upload1.id, upload2.id],
)
expect {
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: "I guess this is different",
upload_ids: [upload2.id, upload1.id],
)
}.to not_change { chat_upload_count }.and not_change { UploadReference.count }
end
it "removes uploads that should be removed" do
chat_message =
create_chat_message(
user1,
"something",
public_chat_channel,
upload_ids: [upload1.id, upload2.id],
)
# TODO (martin) Remove this when we remove ChatUpload completely, 2023-04-01
DB.exec(<<~SQL)
INSERT INTO chat_uploads(upload_id, chat_message_id, created_at, updated_at)
VALUES(#{upload1.id}, #{chat_message.id}, NOW(), NOW())
SQL
DB.exec(<<~SQL)
INSERT INTO chat_uploads(upload_id, chat_message_id, created_at, updated_at)
VALUES(#{upload2.id}, #{chat_message.id}, NOW(), NOW())
SQL
expect {
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: "I guess this is different",
upload_ids: [upload1.id],
)
}.to change { chat_upload_count([upload2]) }.by(-1).and change {
UploadReference.where(upload_id: upload2.id).count
}.by(-1)
end
it "removes all uploads if they should be removed" do
chat_message =
create_chat_message(
user1,
"something",
public_chat_channel,
upload_ids: [upload1.id, upload2.id],
)
# TODO (martin) Remove this when we remove ChatUpload completely, 2023-04-01
DB.exec(<<~SQL)
INSERT INTO chat_uploads(upload_id, chat_message_id, created_at, updated_at)
VALUES(#{upload1.id}, #{chat_message.id}, NOW(), NOW())
SQL
DB.exec(<<~SQL)
INSERT INTO chat_uploads(upload_id, chat_message_id, created_at, updated_at)
VALUES(#{upload2.id}, #{chat_message.id}, NOW(), NOW())
SQL
expect {
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: "I guess this is different",
upload_ids: [],
)
}.to change { chat_upload_count([upload1, upload2]) }.by(-2).and change {
UploadReference.where(target: chat_message).count
}.by(-2)
end
it "adds one upload if none exist" do
chat_message = create_chat_message(user1, "something", public_chat_channel)
expect {
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: "I guess this is different",
upload_ids: [upload1.id],
)
}.to not_change { chat_upload_count([upload1]) }.and change {
UploadReference.where(target: chat_message).count
}.by(1)
end
it "adds multiple uploads if none exist" do
chat_message = create_chat_message(user1, "something", public_chat_channel)
expect {
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: "I guess this is different",
upload_ids: [upload1.id, upload2.id],
)
}.to not_change { chat_upload_count([upload1, upload2]) }.and change {
UploadReference.where(target: chat_message).count
}.by(2)
end
it "doesn't remove existing uploads when upload ids that do not exist are passed in" do
chat_message =
create_chat_message(user1, "something", public_chat_channel, upload_ids: [upload1.id])
expect {
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: "I guess this is different",
upload_ids: [0],
)
}.to not_change { chat_upload_count }.and not_change {
UploadReference.where(target: chat_message).count
}
end
it "doesn't add uploads if `chat_allow_uploads` is false" do
SiteSetting.chat_allow_uploads = false
chat_message = create_chat_message(user1, "something", public_chat_channel)
expect {
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: "I guess this is different",
upload_ids: [upload1.id, upload2.id],
)
}.to not_change { chat_upload_count([upload1, upload2]) }.and not_change {
UploadReference.where(target: chat_message).count
}
end
it "doesn't remove existing uploads if `chat_allow_uploads` is false" do
SiteSetting.chat_allow_uploads = false
chat_message =
create_chat_message(
user1,
"something",
public_chat_channel,
upload_ids: [upload1.id, upload2.id],
)
expect {
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: "I guess this is different",
upload_ids: [],
)
}.to not_change { chat_upload_count }.and not_change {
UploadReference.where(target: chat_message).count
}
end
it "updates if upload is present even if length is less than `chat_minimum_message_length`" do
chat_message =
create_chat_message(
user1,
"something",
public_chat_channel,
upload_ids: [upload1.id, upload2.id],
)
SiteSetting.chat_minimum_message_length = 10
new_message = "hi :)"
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: new_message,
upload_ids: [upload1.id],
)
expect(chat_message.reload.message).to eq(new_message)
end
end
describe "watched words" do
fab!(:watched_word) { Fabricate(:watched_word) }
it "errors when a blocked word is present" do
chat_message = create_chat_message(user1, "something", public_chat_channel)
creator =
Chat::MessageCreator.create(
chat_channel: public_chat_channel,
user: user1,
content: "bad word - #{watched_word.word}",
)
expect(creator.failed?).to eq(true)
expect(creator.error.message).to match(
I18n.t("contains_blocked_word", { word: watched_word.word }),
)
end
end
describe "channel statuses" do
fab!(:message) { Fabricate(:chat_message, user: user1, chat_channel: public_chat_channel) }
def update_message(user)
message.update(user: user)
Chat::MessageUpdater.update(
guardian: Guardian.new(user),
chat_message: message,
new_content: "I guess this is different",
)
end
context "when channel is closed" do
before { public_chat_channel.update(status: :closed) }
it "errors when trying to update the message for non-staff" do
updater = update_message(user1)
expect(updater.failed?).to eq(true)
expect(updater.error.message).to eq(
I18n.t("chat.errors.channel_modify_message_disallowed.closed"),
)
end
it "does not error when trying to create a message for staff" do
update_message(admin1)
expect(message.reload.message).to eq("I guess this is different")
end
end
context "when channel is read_only" do
before { public_chat_channel.update(status: :read_only) }
it "errors when trying to update the message for all users" do
updater = update_message(user1)
expect(updater.failed?).to eq(true)
expect(updater.error.message).to eq(
I18n.t("chat.errors.channel_modify_message_disallowed.read_only"),
)
updater = update_message(admin1)
expect(updater.failed?).to eq(true)
expect(updater.error.message).to eq(
I18n.t("chat.errors.channel_modify_message_disallowed.read_only"),
)
end
end
context "when channel is archived" do
before { public_chat_channel.update(status: :archived) }
it "errors when trying to update the message for all users" do
updater = update_message(user1)
expect(updater.failed?).to eq(true)
expect(updater.error.message).to eq(
I18n.t("chat.errors.channel_modify_message_disallowed.archived"),
)
updater = update_message(admin1)
expect(updater.failed?).to eq(true)
expect(updater.error.message).to eq(
I18n.t("chat.errors.channel_modify_message_disallowed.archived"),
)
end
end
end
# TODO (martin) Remove this when we remove ChatUpload completely, 2023-04-01
def chat_upload_count(uploads = nil)
return DB.query_single("SELECT COUNT(*) FROM chat_uploads").first if !uploads
DB.query_single(
"SELECT COUNT(*) FROM chat_uploads WHERE upload_id IN (#{uploads.map(&:id).join(",")})",
).first
end
end

View File

@ -0,0 +1,72 @@
# frozen_string_literal: true
require "rails_helper"
describe Chat::Seeder do
fab!(:staff_category) { Fabricate(:private_category, name: "Staff", group: Group[:staff]) }
fab!(:general_category) { Fabricate(:category, name: "General") }
fab!(:staff_user1) do
Fabricate(:user, last_seen_at: 1.minute.ago, groups: [Group[:staff], Group[:everyone]])
end
fab!(:staff_user2) do
Fabricate(:user, last_seen_at: 1.minute.ago, groups: [Group[:staff], Group[:everyone]])
end
fab!(:regular_user) { Fabricate(:user, last_seen_at: 1.minute.ago, groups: [Group[:everyone]]) }
before do
SiteSetting.staff_category_id = staff_category.id
SiteSetting.general_category_id = general_category.id
Jobs.run_immediately!
end
def assert_channel_was_correctly_seeded(channel, group)
expect(channel).to be_present
expect(channel.auto_join_users).to eq(true)
expected_members_count = GroupUser.where(group: group).count
memberships_count =
Chat::UserChatChannelMembership.automatic.where(chat_channel: channel, following: true).count
expect(memberships_count).to eq(expected_members_count)
end
it "seeds default channels" do
Chat::Seeder.new.execute
staff_channel = Chat::Channel.find_by(chatable_id: staff_category)
general_channel = Chat::Channel.find_by(chatable_id: general_category)
assert_channel_was_correctly_seeded(staff_channel, Group[:staff])
assert_channel_was_correctly_seeded(general_channel, Group[:everyone])
expect(staff_category.custom_fields[Chat::HAS_CHAT_ENABLED]).to eq(true)
expect(general_category.reload.custom_fields[Chat::HAS_CHAT_ENABLED]).to eq(true)
expect(SiteSetting.needs_chat_seeded).to eq(false)
end
it "applies a name to the general category channel" do
expected_name = general_category.name
Chat::Seeder.new.execute
general_channel = Chat::Channel.find_by(chatable_id: general_category)
expect(general_channel.name).to eq(expected_name)
end
it "applies a name to the staff category channel" do
expected_name = staff_category.name
Chat::Seeder.new.execute
staff_channel = Chat::Channel.find_by(chatable_id: staff_category)
expect(staff_channel.name).to eq(expected_name)
end
it "does nothing when 'SiteSetting.needs_chat_seeded' is false" do
SiteSetting.needs_chat_seeded = false
expect { Chat::Seeder.new.execute }.not_to change { Chat::Channel.count }
end
end