discourse/plugins/chat/spec/components/chat_message_updater_spec.rb
Martin Brennan 0924f874bd
DEV: Use UploadReference instead of ChatUpload in chat (#19947)
We've had the UploadReference table for some time now in core,
but it was added after ChatUpload was and chat was just never
moved over to this new system.

This commit changes all chat code dealing with uploads to create/
update/delete/query UploadReference records instead of ChatUpload
records for consistency. At a later date we will drop the ChatUpload
table, but for now keeping it for data backup.

The migration + post migration are the same, we need both in case
any chat uploads are added/removed during deploy.
2023-01-24 13:28:21 +10:00

599 lines
20 KiB
Ruby

# frozen_string_literal: true
require "rails_helper"
describe Chat::ChatMessageUpdater 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::ChatMessageCreator.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::ChatMessageUpdater.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",
{ minimum: 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::ChatMessageUpdater.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", { maximum: 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::ChatMessageUpdater.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::ChatMessageUpdater.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::ChatMessageUpdater.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::ChatMessageUpdater.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::ChatMessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: message + " editedddd",
)
}.not_to change { ChatMention.count }
end
it "doesn't create mentions for users without access" do
message = "ping"
chat_message = create_chat_message(user1, message, public_chat_channel)
expect {
Chat::ChatMessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: message + " @#{user_without_memberships.username}",
)
}.not_to change { ChatMention.count }
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::ChatMessageUpdater.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::ChatMessageUpdater.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 "does not create new mentions in direct message for users who don't have access" do
chat_message = create_chat_message(user1, "ping nobody", @direct_message_channel)
expect {
Chat::ChatMessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: "ping @#{admin1.username}",
)
}.not_to change { ChatMention.count }
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::ChatMessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: "ping @#{admin_group.name}",
)
}.to change { ChatMention.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::ChatMessageUpdater.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::ChatMessageUpdater.update(
guardian: guardian,
chat_message: chat_message,
new_content: "ping nobody anymore!",
)
}.to change { ChatMention.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::ChatMessageUpdater.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::ChatMessageUpdater.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::ChatMessageUpdater.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::ChatMessageUpdater.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::ChatMessageUpdater.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::ChatMessageUpdater.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::ChatMessageUpdater.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::ChatMessageUpdater.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::ChatMessageUpdater.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::ChatMessageUpdater.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::ChatMessageUpdater.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::ChatMessageUpdater.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::ChatMessageCreator.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::ChatMessageUpdater.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",
status: public_chat_channel.status_name,
),
)
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",
status: public_chat_channel.status_name,
),
)
updater = update_message(admin1)
expect(updater.failed?).to eq(true)
expect(updater.error.message).to eq(
I18n.t(
"chat.errors.channel_modify_message_disallowed",
status: public_chat_channel.status_name,
),
)
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",
status: public_chat_channel.status_name,
),
)
updater = update_message(admin1)
expect(updater.failed?).to eq(true)
expect(updater.error.message).to eq(
I18n.t(
"chat.errors.channel_modify_message_disallowed",
status: public_chat_channel.status_name,
),
)
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