# frozen_string_literal: true require "rails_helper" describe ChatMessage do fab!(:message) { Fabricate(:chat_message, message: "hey friend, what's up?!") } it { is_expected.to have_many(:chat_mentions).dependent(:destroy) } describe ".cook" do it "does not support HTML tags" do cooked = ChatMessage.cook("
<h1>test</h1>
") end it "does not support headings" do cooked = ChatMessage.cook("## heading 2") expect(cooked).to eq("## heading 2
") end it "does not support horizontal rules" do cooked = ChatMessage.cook("---") expect(cooked).to eq("---
") end it "supports backticks rule" do cooked = ChatMessage.cook("`test`") expect(cooked).to eq("test
something = test
COOKED
end
it "supports fence rule with language support" do
cooked = ChatMessage.cook(<<~RAW)
```ruby
Widget.triangulate(argument: "no u")
```
RAW
expect(cooked).to eq(<<~COOKED.chomp)
Widget.triangulate(argument: "no u")
COOKED
end
it "supports code rule" do
cooked = ChatMessage.cook(" something = test")
expect(cooked).to eq("something = test\n
")
end
it "supports blockquote rule" do
cooked = ChatMessage.cook("> a quote")
expect(cooked).to eq("\n") end it "supports quote bbcode" do topic = Fabricate(:topic, title: "Some quotable topic") post = Fabricate(:post, topic: topic) SiteSetting.external_system_avatars_enabled = false avatar_src = "//test.localhost#{User.system_avatar_template(post.user.username).gsub("{size}", "40")}" cooked = ChatMessage.cook(<<~RAW) [quote="#{post.user.username}, post:#{post.post_number}, topic:#{topic.id}"] Mark me...this will go down in history. [/quote] RAW expect(cooked).to eq(<<~COOKED.chomp) COOKED end it "supports chat quote bbcode" do chat_channel = Fabricate(:category_channel, name: "testchannel") user = Fabricate(:user, username: "chatbbcodeuser") user2 = Fabricate(:user, username: "otherbbcodeuser") avatar_src = "//test.localhost#{User.system_avatar_template(user.username).gsub("{size}", "40")}" avatar_src2 = "//test.localhost#{User.system_avatar_template(user2.username).gsub("{size}", "40")}" msg1 = Fabricate( :chat_message, chat_channel: chat_channel, message: "this is the first message", user: user, ) msg2 = Fabricate( :chat_message, chat_channel: chat_channel, message: "and another cool one", user: user2, ) other_messages_to_quote = [msg1, msg2] cooked = ChatMessage.cook( ChatTranscriptService.new( chat_channel, Fabricate(:user), messages_or_ids: other_messages_to_quote.map(&:id), ).generate_markdown, ) expect(cooked).to eq(<<~COOKED.chomp)a quote
\n
test
bold
") end it "supports link markdown rule" do chat_message = Fabricate(:chat_message, message: "[test link](https://www.example.com)") expect(chat_message.cooked).to eq( "", ) end it "supports table markdown plugin" do cooked = ChatMessage.cook(<<~RAW) | Command | Description | | --- | --- | | git status | List all new or modified files | RAW expected = <<~COOKEDCommand | Description |
---|---|
git status | List all new or modified files |
@mention
") end it "supports category-hashtag plugin" do # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites SiteSetting.enable_experimental_hashtag_autocomplete = false category = Fabricate(:category) cooked = ChatMessage.cook("##{category.slug}") expect(cooked).to eq( "", ) end it "supports hashtag-autocomplete plugin" do SiteSetting.chat_enabled = true SiteSetting.enable_experimental_hashtag_autocomplete = true category = Fabricate(:category) user = Fabricate(:user) cooked = ChatMessage.cook("##{category.slug}", user_id: user.id) expect(cooked).to eq( "", ) end it "supports censored plugin" do watched_word = Fabricate(:watched_word, action: WatchedWord.actions[:censor]) cooked = ChatMessage.cook(watched_word.word) expect(cooked).to eq("■■■■■
") end it "includes links in pretty text excerpt if the raw message is a single link and the PrettyText excerpt is blank" do message = Fabricate.build( :chat_message, message: "https://twitter.com/EffinBirds/status/1518743508378697729", ) expect(message.excerpt).to eq("https://twitter.com/EffinBirds/status/1518743508378697729") message = Fabricate.build( :chat_message, message: "https://twitter.com/EffinBirds/status/1518743508378697729", cooked: <<~COOKED, \n COOKED ) expect(message.excerpt).to eq("https://twitter.com/EffinBirds/status/1518743508378697729") message = Fabricate.build( :chat_message, message: "wow check out these birbs https://twitter.com/EffinBirds/status/1518743508378697729", ) expect(message.excerpt).to eq( "wow check out these birbs https://twitter.com/Effi...", ) end it "returns an empty string if PrettyText.excerpt returns empty string" do message = Fabricate(:chat_message, message: <<~MSG) [quote="martin, post:30, topic:3179, full:true"] This is a real **quote** topic with some *markdown* in it I can quote. [/quote] MSG expect(message.excerpt).to eq("") end it "excerpts upload file name if message is empty" do gif = Fabricate(:upload, original_filename: "cat.gif", width: 400, height: 300, extension: "gif") message = Fabricate(:chat_message, message: "") UploadReference.create(target: message, upload: gif) expect(message.excerpt).to eq "cat.gif" end it "supports autolink with <>" do cooked = ChatMessage.cook("https://github.com/discourse/discourse-chat/pull/468
", ) end it "supports lists" do cooked = ChatMessage.cook(<<~MSG) wow look it's a list * item 1 * item 2 MSG expect(cooked).to eq(<<~HTML.chomp)wow look it's a list
this is a replace test
HTML end it "supports spoilers" do if SiteSetting.respond_to?(:spoiler_enabled) && SiteSetting.spoiler_enabled cooked = ChatMessage.cook("[spoiler]the planet of the apes was earth all along[/spoiler]") expect(cooked).to eq( "the planet of the apes was earth all along
\n<h1>@#{user.username}</h1>
") end end end describe ".to_markdown" do it "renders the message without uploads" do expect(message.to_markdown).to eq("hey friend, what's up?!") end it "renders the message with uploads" do image = Fabricate( :upload, original_filename: "test_image.jpg", width: 400, height: 300, extension: "jpg", ) image2 = Fabricate(:upload, original_filename: "meme.jpg", width: 10, height: 10, extension: "jpg") UploadReference.create!(target: message, upload: image) UploadReference.create!(target: message, upload: image2) expect(message.to_markdown).to eq(<<~MSG.chomp) hey friend, what's up?! ![test_image.jpg|400x300](#{image.short_url}) ![meme.jpg|10x10](#{image2.short_url}) MSG end end describe ".push_notification_excerpt" do it "truncates to 400 characters" do message = ChatMessage.new(message: "Hello, World!" * 40) expect(message.push_notification_excerpt.size).to eq(400) end it "encodes emojis" do message = ChatMessage.new(message: ":grinning:") expect(message.push_notification_excerpt).to eq("😀") end end describe "blocking duplicate messages" do fab!(:channel) { Fabricate(:chat_channel, user_count: 10) } fab!(:user1) { Fabricate(:user) } fab!(:user2) { Fabricate(:user) } before { SiteSetting.chat_duplicate_message_sensitivity = 1 } it "blocks duplicate messages for the message, channel user, and message age requirements" do Fabricate(:chat_message, message: "this is duplicate", chat_channel: channel, user: user1) message = ChatMessage.new(message: "this is duplicate", chat_channel: channel, user: user2) message.validate_message(has_uploads: false) expect(message.errors.full_messages).to include(I18n.t("chat.errors.duplicate_message")) end end describe "#destroy" do it "nullify messages with in_reply_to_id to this destroyed message" do message_1 = Fabricate(:chat_message) message_2 = Fabricate(:chat_message, in_reply_to_id: message_1.id) message_3 = Fabricate(:chat_message, in_reply_to_id: message_2.id) expect(message_2.in_reply_to_id).to eq(message_1.id) message_1.destroy! expect(message_2.reload.in_reply_to_id).to be_nil expect(message_3.reload.in_reply_to_id).to eq(message_2.id) end it "destroys chat_message_revisions" do message_1 = Fabricate(:chat_message) revision_1 = Fabricate(:chat_message_revision, chat_message: message_1) message_1.destroy! expect { revision_1.reload }.to raise_error(ActiveRecord::RecordNotFound) end it "destroys chat_message_reactions" do message_1 = Fabricate(:chat_message) reaction_1 = Fabricate(:chat_message_reaction, chat_message: message_1) message_1.destroy! expect { reaction_1.reload }.to raise_error(ActiveRecord::RecordNotFound) end it "destroys chat_mention" do message_1 = Fabricate(:chat_message) notification = Fabricate(:notification) mention_1 = Fabricate(:chat_mention, chat_message: message_1, notification: notification) message_1.destroy! expect { mention_1.reload }.to raise_error(ActiveRecord::RecordNotFound) end it "destroys chat_webhook_event" do message_1 = Fabricate(:chat_message) webhook_1 = Fabricate(:chat_webhook_event, chat_message: message_1) message_1.destroy! expect { webhook_1.reload }.to raise_error(ActiveRecord::RecordNotFound) end it "destroys upload_references and chat_uploads" do message_1 = Fabricate(:chat_message) upload_reference_1 = Fabricate(:upload_reference, target: message_1) upload_1 = Fabricate(:upload) # 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(#{upload_1.id}, #{message_1.id}, NOW(), NOW()) SQL message_1.destroy! expect(DB.query("SELECT * FROM chat_uploads WHERE upload_id = #{upload_1.id}")).to eq([]) expect { upload_reference_1.reload }.to raise_error(ActiveRecord::RecordNotFound) end describe "bookmarks" do before { Bookmark.register_bookmarkable(ChatMessageBookmarkable) } it "destroys bookmarks" do message_1 = Fabricate(:chat_message) bookmark_1 = Fabricate(:bookmark, bookmarkable: message_1) message_1.destroy! expect { bookmark_1.reload }.to raise_error(ActiveRecord::RecordNotFound) end end end describe "#rebake!" do fab!(:chat_message) { Fabricate(:chat_message) } describe "hashtags" do fab!(:category) { Fabricate(:category) } fab!(:group) { Fabricate(:group) } fab!(:secure_category) { Fabricate(:private_category, group: group) } before do SiteSetting.chat_enabled = true SiteSetting.enable_experimental_hashtag_autocomplete = true SiteSetting.suppress_secured_categories_from_admin = true end it "keeps the same hashtags the user has permission to after rebake" do group.add(chat_message.user) chat_message.update!( message: "this is the message ##{category.slug} ##{secure_category.slug} ##{chat_message.chat_channel.slug}", ) chat_message.cook chat_message.save! expect(chat_message.reload.cooked).to include(secure_category.name) chat_message.rebake! expect(chat_message.reload.cooked).to include(secure_category.name) end end end describe "#attach_uploads" do fab!(:chat_message) { Fabricate(:chat_message) } fab!(:upload_1) { Fabricate(:upload) } fab!(:upload_2) { Fabricate(:upload) } it "creates an UploadReference record for the provided uploads" do chat_message.attach_uploads([upload_1, upload_2]) upload_references = UploadReference.where(upload_id: [upload_1, upload_2]) expect(chat_upload_count([upload_1, upload_2])).to eq(0) expect(upload_references.count).to eq(2) expect(upload_references.map(&:target_id).uniq).to eq([chat_message.id]) expect(upload_references.map(&:target_type).uniq).to eq(["ChatMessage"]) end it "does nothing if the message record is new" do expect { ChatMessage.new.attach_uploads([upload_1, upload_2]) }.to not_change { chat_upload_count }.and not_change { UploadReference.count } end it "does nothing for an empty uploads array" do expect { chat_message.attach_uploads([]) }.to not_change { chat_upload_count }.and not_change { UploadReference.count } 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