mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-30 10:42:38 +00:00
393 lines
15 KiB
Ruby
393 lines
15 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
require Rails.root.join 'spec/models/concerns/liquidable_shared.rb'
|
|
|
|
RSpec.describe Message do
|
|
context 'with validations' do
|
|
it { is_expected.to validate_presence_of(:inbox_id) }
|
|
it { is_expected.to validate_presence_of(:conversation_id) }
|
|
it { is_expected.to validate_presence_of(:account_id) }
|
|
end
|
|
|
|
describe 'length validations' do
|
|
let(:message) { create(:message) }
|
|
|
|
context 'when it validates name length' do
|
|
it 'valid when within limit' do
|
|
message.content = 'a' * 120_000
|
|
expect(message.valid?).to be true
|
|
end
|
|
|
|
it 'invalid when crossed the limit' do
|
|
message.content = 'a' * 150_001
|
|
message.processed_message_content = 'a' * 150_001
|
|
message.valid?
|
|
|
|
expect(message.errors[:processed_message_content]).to include('is too long (maximum is 150000 characters)')
|
|
expect(message.errors[:content]).to include('is too long (maximum is 150000 characters)')
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'concerns' do
|
|
it_behaves_like 'liqudable'
|
|
end
|
|
|
|
describe 'message_filter_helpers' do
|
|
context 'when webhook_sendable?' do
|
|
[
|
|
{ type: :incoming, expected: true },
|
|
{ type: :outgoing, expected: true },
|
|
{ type: :template, expected: true },
|
|
{ type: :activity, expected: false }
|
|
].each do |scenario|
|
|
it "returns #{scenario[:expected]} for #{scenario[:type]} message" do
|
|
message = create(:message, message_type: scenario[:type])
|
|
expect(message.webhook_sendable?).to eq(scenario[:expected])
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#push_event_data' do
|
|
subject(:push_event_data) { message.push_event_data }
|
|
|
|
let(:message) { create(:message, echo_id: 'random-echo_id') }
|
|
|
|
let(:expected_data) do
|
|
{
|
|
|
|
account_id: message.account_id,
|
|
additional_attributes: message.additional_attributes,
|
|
content_attributes: message.content_attributes,
|
|
content_type: message.content_type,
|
|
content: message.content,
|
|
conversation_id: message.conversation.display_id,
|
|
created_at: message.created_at.to_i,
|
|
external_source_ids: message.external_source_ids,
|
|
id: message.id,
|
|
inbox_id: message.inbox_id,
|
|
message_type: message.message_type_before_type_cast,
|
|
private: message.private,
|
|
processed_message_content: message.processed_message_content,
|
|
sender_id: message.sender_id,
|
|
sender_type: message.sender_type,
|
|
source_id: message.source_id,
|
|
status: message.status,
|
|
updated_at: message.updated_at,
|
|
conversation: {
|
|
assignee_id: message.conversation.assignee_id,
|
|
contact_inbox: {
|
|
source_id: message.conversation.contact_inbox.source_id
|
|
},
|
|
last_activity_at: message.conversation.last_activity_at.to_i,
|
|
unread_count: message.conversation.unread_incoming_messages.count
|
|
},
|
|
sender: message.sender.push_event_data,
|
|
echo_id: 'random-echo_id'
|
|
}
|
|
end
|
|
|
|
it 'returns push event payload' do
|
|
expect(push_event_data).to eq(expected_data)
|
|
end
|
|
end
|
|
|
|
describe 'Check if message is a valid first reply' do
|
|
it 'is valid if it is outgoing' do
|
|
outgoing_message = create(:message, message_type: :outgoing)
|
|
expect(outgoing_message.valid_first_reply?).to be true
|
|
end
|
|
|
|
it 'is invalid if it is not outgoing' do
|
|
incoming_message = create(:message, message_type: :incoming)
|
|
expect(incoming_message.valid_first_reply?).to be false
|
|
|
|
activity_message = create(:message, message_type: :activity)
|
|
expect(activity_message.valid_first_reply?).to be false
|
|
|
|
template_message = create(:message, message_type: :template)
|
|
expect(template_message.valid_first_reply?).to be false
|
|
end
|
|
|
|
it 'is invalid if it is outgoing but private' do
|
|
conversation = create(:conversation)
|
|
|
|
outgoing_message = create(:message, message_type: :outgoing, conversation: conversation, private: true)
|
|
expect(outgoing_message.valid_first_reply?).to be false
|
|
|
|
# next message should be a valid reply
|
|
next_message = create(:message, message_type: :outgoing, conversation: conversation)
|
|
expect(next_message.valid_first_reply?).to be true
|
|
end
|
|
|
|
it 'is invalid if it is not the first reply' do
|
|
conversation = create(:conversation)
|
|
first_message = create(:message, message_type: :outgoing, conversation: conversation)
|
|
expect(first_message.valid_first_reply?).to be true
|
|
|
|
second_message = create(:message, message_type: :outgoing, conversation: conversation)
|
|
expect(second_message.valid_first_reply?).to be false
|
|
end
|
|
|
|
it 'is invalid if it is sent as campaign' do
|
|
conversation = create(:conversation)
|
|
campaign_message = create(:message, message_type: :outgoing, conversation: conversation, additional_attributes: { campaign_id: 1 })
|
|
expect(campaign_message.valid_first_reply?).to be false
|
|
|
|
second_message = create(:message, message_type: :outgoing, conversation: conversation)
|
|
expect(second_message.valid_first_reply?).to be true
|
|
end
|
|
|
|
it 'is invalid if it is sent by automation' do
|
|
conversation = create(:conversation)
|
|
automation_message = create(:message, message_type: :outgoing, conversation: conversation, content_attributes: { automation_rule_id: 1 })
|
|
expect(automation_message.valid_first_reply?).to be false
|
|
end
|
|
end
|
|
|
|
describe '#reopen_conversation' do
|
|
let(:conversation) { create(:conversation) }
|
|
let(:message) { build(:message, message_type: :incoming, conversation: conversation) }
|
|
|
|
it 'reopens resolved conversation when the message is from a contact' do
|
|
conversation.resolved!
|
|
message.save!
|
|
expect(message.conversation.open?).to be true
|
|
end
|
|
|
|
it 'reopens snoozed conversation when the message is from a contact' do
|
|
conversation.snoozed!
|
|
message.save!
|
|
expect(message.conversation.open?).to be true
|
|
end
|
|
|
|
it 'will not reopen if the conversation is muted' do
|
|
conversation.resolved!
|
|
conversation.mute!
|
|
message.save!
|
|
expect(message.conversation.open?).to be false
|
|
end
|
|
|
|
it 'will mark the conversation as pending if the agent bot is active' do
|
|
agent_bot = create(:agent_bot)
|
|
inbox = conversation.inbox
|
|
inbox.agent_bot = agent_bot
|
|
inbox.save!
|
|
conversation.resolved!
|
|
message.save!
|
|
expect(conversation.open?).to be false
|
|
expect(conversation.pending?).to be true
|
|
end
|
|
end
|
|
|
|
describe '#waiting since' do
|
|
let(:conversation) { create(:conversation) }
|
|
let(:agent) { create(:user, account: conversation.account) }
|
|
let(:message) { build(:message, conversation: conversation) }
|
|
|
|
it 'resets the waiting_since if an agent sent a reply' do
|
|
message.message_type = :outgoing
|
|
message.sender = agent
|
|
message.save!
|
|
|
|
expect(conversation.waiting_since).to be_nil
|
|
end
|
|
|
|
it 'sets the waiting_since if there is an incoming message' do
|
|
conversation.update(waiting_since: nil)
|
|
message.message_type = :incoming
|
|
message.save!
|
|
|
|
expect(conversation.waiting_since).not_to be_nil
|
|
end
|
|
|
|
it 'does not overwrite the previous value if there are newer messages' do
|
|
old_waiting_since = conversation.waiting_since
|
|
message.message_type = :incoming
|
|
message.save!
|
|
conversation.reload
|
|
|
|
expect(conversation.waiting_since).to eq old_waiting_since
|
|
end
|
|
end
|
|
|
|
context 'with webhook_data' do
|
|
it 'contains the message attachment when attachment is present' do
|
|
message = create(:message)
|
|
attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
|
|
attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
|
|
attachment.save!
|
|
expect(message.webhook_data.key?(:attachments)).to be true
|
|
end
|
|
|
|
it 'does not contain the message attachment when attachment is not present' do
|
|
message = create(:message)
|
|
expect(message.webhook_data.key?(:attachments)).to be false
|
|
end
|
|
end
|
|
|
|
context 'when message is created' do
|
|
let(:message) { build(:message, account: create(:account)) }
|
|
|
|
it 'updates conversation last_activity_at when created' do
|
|
message.save!
|
|
expect(message.created_at).to eq message.conversation.last_activity_at
|
|
end
|
|
|
|
it 'updates contact last_activity_at when created' do
|
|
expect { message.save! }.to(change { message.sender.last_activity_at })
|
|
end
|
|
|
|
it 'triggers ::MessageTemplates::HookExecutionService' do
|
|
hook_execution_service = double
|
|
allow(MessageTemplates::HookExecutionService).to receive(:new).and_return(hook_execution_service)
|
|
allow(hook_execution_service).to receive(:perform).and_return(true)
|
|
|
|
message.save!
|
|
|
|
expect(MessageTemplates::HookExecutionService).to have_received(:new).with(message: message)
|
|
expect(hook_execution_service).to have_received(:perform)
|
|
end
|
|
|
|
context 'with conversation continuity' do
|
|
it 'calls notify email method on after save for outgoing messages in website channel' do
|
|
allow(ConversationReplyEmailWorker).to receive(:perform_in).and_return(true)
|
|
message.message_type = 'outgoing'
|
|
message.save!
|
|
expect(ConversationReplyEmailWorker).to have_received(:perform_in)
|
|
end
|
|
|
|
it 'does not call notify email for website channel if continuity is disabled' do
|
|
message.inbox = create(:inbox, account: message.account,
|
|
channel: build(:channel_widget, account: message.account, continuity_via_email: false))
|
|
allow(ConversationReplyEmailWorker).to receive(:perform_in).and_return(true)
|
|
message.message_type = 'outgoing'
|
|
message.save!
|
|
expect(ConversationReplyEmailWorker).not_to have_received(:perform_in)
|
|
end
|
|
|
|
it 'wont call notify email method for private notes' do
|
|
message.private = true
|
|
allow(ConversationReplyEmailWorker).to receive(:perform_in).and_return(true)
|
|
message.save!
|
|
expect(ConversationReplyEmailWorker).not_to have_received(:perform_in)
|
|
end
|
|
|
|
it 'calls EmailReply worker if the channel is email' do
|
|
message.inbox = create(:inbox, account: message.account, channel: build(:channel_email, account: message.account))
|
|
allow(EmailReplyWorker).to receive(:perform_in).and_return(true)
|
|
message.message_type = 'outgoing'
|
|
message.content_attributes = { email: { text_content: { quoted: 'quoted text' } } }
|
|
message.save!
|
|
expect(EmailReplyWorker).to have_received(:perform_in).with(1.second, message.id)
|
|
end
|
|
|
|
it 'wont call notify email method unless its website or email channel' do
|
|
message.inbox = create(:inbox, account: message.account, channel: build(:channel_api, account: message.account))
|
|
allow(ConversationReplyEmailWorker).to receive(:perform_in).and_return(true)
|
|
message.save!
|
|
expect(ConversationReplyEmailWorker).not_to have_received(:perform_in)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when content_type is blank' do
|
|
let(:message) { build(:message, content_type: nil, account: create(:account)) }
|
|
|
|
it 'sets content_type as text' do
|
|
message.save!
|
|
expect(message.content_type).to eq 'text'
|
|
end
|
|
end
|
|
|
|
context 'when processed_message_content is blank' do
|
|
let(:message) { build(:message, content_type: :text, account: create(:account), content: 'Processed message content') }
|
|
|
|
it 'sets content_type as text' do
|
|
message.save!
|
|
expect(message.processed_message_content).to eq message.content
|
|
end
|
|
end
|
|
|
|
context 'when attachments size maximum' do
|
|
let(:message) { build(:message, content_type: nil, account: create(:account)) }
|
|
|
|
it 'add errors to message for attachment size is more than allowed limit' do
|
|
16.times.each do
|
|
attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
|
|
attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
|
|
end
|
|
|
|
expect(message.errors.messages).to eq({ attachments: ['exceeded maximum allowed'] })
|
|
end
|
|
end
|
|
|
|
context 'when email notifiable message' do
|
|
let(:message) { build(:message, content_type: nil, account: create(:account)) }
|
|
|
|
it 'return false if private message' do
|
|
message.private = true
|
|
message.message_type = 'outgoing'
|
|
expect(message.email_notifiable_message?).to be false
|
|
end
|
|
|
|
it 'return false if incoming message' do
|
|
message.private = false
|
|
message.message_type = 'incoming'
|
|
expect(message.email_notifiable_message?).to be false
|
|
end
|
|
|
|
it 'return false if activity message' do
|
|
message.private = false
|
|
message.message_type = 'activity'
|
|
expect(message.email_notifiable_message?).to be false
|
|
end
|
|
|
|
it 'return false if message type is template and content type is not input_csat or text' do
|
|
message.private = false
|
|
message.message_type = 'template'
|
|
message.content_type = 'incoming_email'
|
|
expect(message.email_notifiable_message?).to be false
|
|
end
|
|
|
|
it 'return true if not private and not incoming and message content type is input_csat or text' do
|
|
message.private = false
|
|
message.message_type = 'template'
|
|
message.content_type = 'text'
|
|
expect(message.email_notifiable_message?).to be true
|
|
end
|
|
end
|
|
|
|
context 'when facebook channel with unavailable story link' do
|
|
let(:instagram_message) { create(:message, :instagram_story_mention) }
|
|
|
|
before do
|
|
# stubbing the request to facebook api during the message creation
|
|
stub_request(:get, %r{https://graph.facebook.com/.*}).to_return(status: 200, body: {
|
|
story: { mention: { link: 'http://graph.facebook.com/test-story-mention', id: '17920786367196703' } },
|
|
from: { username: 'Sender-id-1', id: 'Sender-id-1' },
|
|
id: 'instagram-message-id-1234'
|
|
}.to_json, headers: {})
|
|
end
|
|
|
|
it 'keeps the attachment for deleted stories' do
|
|
expect(instagram_message.attachments.count).to eq 1
|
|
stub_request(:get, %r{https://graph.facebook.com/.*}).to_return(status: 404)
|
|
instagram_message.push_event_data
|
|
expect(instagram_message.reload.attachments.count).to eq 1
|
|
end
|
|
|
|
it 'keeps the attachment for expired stories' do
|
|
expect(instagram_message.attachments.count).to eq 1
|
|
# for expired stories, the link will be empty
|
|
stub_request(:get, %r{https://graph.facebook.com/.*}).to_return(status: 200, body: {
|
|
story: { mention: { link: '', id: '17920786367196703' } }
|
|
}.to_json, headers: {})
|
|
instagram_message.push_event_data
|
|
expect(instagram_message.reload.attachments.count).to eq 1
|
|
end
|
|
end
|
|
end
|