mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 19:48:08 +00:00
Previously, email replies were handled inside workers. There was no execution logs. This meant if emails silently failed (as reported by a customer), we had no way to trace where the issue happened, the only assumption was “no error = mail sent.” By moving email handling into jobs, we now have proper execution logs for each attempt. This makes it easier to debug delivery issues and would have better visibility when investigating customer reports. Fixes https://linear.app/chatwoot/issue/CW-5538/emails-are-not-sentdelivered-to-the-contact --------- Co-authored-by: Sojan Jose <sojan@pepalo.com> Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
764 lines
28 KiB
Ruby
764 lines
28 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
require Rails.root.join 'spec/models/concerns/liquidable_shared.rb'
|
|
|
|
RSpec.describe Message do
|
|
before do
|
|
# rubocop:disable RSpec/AnyInstance
|
|
allow_any_instance_of(described_class).to receive(:reindex_for_search).and_return(true)
|
|
# rubocop:enable RSpec/AnyInstance
|
|
end
|
|
|
|
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
|
|
|
|
it 'adds error in case of message flooding' do
|
|
with_modified_env 'CONVERSATION_MESSAGE_PER_MINUTE_LIMIT': '2' do
|
|
conversation = message.conversation
|
|
create(:message, conversation: conversation)
|
|
conv_new_message = build(:message, conversation: message.conversation)
|
|
|
|
expect(conv_new_message.valid?).to be false
|
|
expect(conv_new_message.errors[:base]).to eq(['Too many messages'])
|
|
end
|
|
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
|
|
},
|
|
sentiment: {},
|
|
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 'message create event' do
|
|
let!(:conversation) { create(:conversation) }
|
|
|
|
before do
|
|
conversation.reload
|
|
end
|
|
|
|
it 'updates the conversation first reply created at if it is the first outgoing message' do
|
|
expect(conversation.first_reply_created_at).to be_nil
|
|
expect(conversation.waiting_since).to eq conversation.created_at
|
|
|
|
outgoing_message = create(:message, message_type: :outgoing, conversation: conversation)
|
|
|
|
expect(conversation.first_reply_created_at).to eq outgoing_message.created_at
|
|
expect(conversation.waiting_since).to be_nil
|
|
end
|
|
|
|
it 'does not update the conversation first reply created at if the message is incoming' do
|
|
expect(conversation.first_reply_created_at).to be_nil
|
|
expect(conversation.waiting_since).to eq conversation.created_at
|
|
|
|
create(:message, message_type: :incoming, conversation: conversation)
|
|
|
|
expect(conversation.first_reply_created_at).to be_nil
|
|
expect(conversation.waiting_since).to eq conversation.created_at
|
|
end
|
|
|
|
it 'does not update the conversation first reply created at if the message is template' do
|
|
expect(conversation.first_reply_created_at).to be_nil
|
|
expect(conversation.waiting_since).to eq conversation.created_at
|
|
|
|
create(:message, message_type: :template, conversation: conversation)
|
|
|
|
expect(conversation.first_reply_created_at).to be_nil
|
|
expect(conversation.waiting_since).to eq conversation.created_at
|
|
end
|
|
|
|
it 'does not update the conversation first reply created at if the message is activity' do
|
|
expect(conversation.first_reply_created_at).to be_nil
|
|
expect(conversation.waiting_since).to eq conversation.created_at
|
|
|
|
create(:message, message_type: :activity, conversation: conversation)
|
|
|
|
expect(conversation.first_reply_created_at).to be_nil
|
|
expect(conversation.waiting_since).to eq conversation.created_at
|
|
end
|
|
|
|
it 'does not update the conversation first reply created at if the message is a private message' do
|
|
expect(conversation.first_reply_created_at).to be_nil
|
|
expect(conversation.waiting_since).to eq conversation.created_at
|
|
|
|
create(:message, message_type: :outgoing, conversation: conversation, private: true)
|
|
|
|
expect(conversation.first_reply_created_at).to be_nil
|
|
expect(conversation.waiting_since).to eq conversation.created_at
|
|
|
|
next_message = create(:message, message_type: :outgoing, conversation: conversation)
|
|
expect(conversation.first_reply_created_at).to eq next_message.created_at
|
|
expect(conversation.waiting_since).to be_nil
|
|
end
|
|
|
|
it 'does not update first reply if the message is sent as campaign' do
|
|
expect(conversation.first_reply_created_at).to be_nil
|
|
expect(conversation.waiting_since).to eq conversation.created_at
|
|
|
|
create(:message, message_type: :outgoing, conversation: conversation, additional_attributes: { campaign_id: 1 })
|
|
|
|
expect(conversation.first_reply_created_at).to be_nil
|
|
expect(conversation.waiting_since).to eq conversation.created_at
|
|
end
|
|
|
|
it 'does not update first reply if the message is sent by automation' do
|
|
expect(conversation.first_reply_created_at).to be_nil
|
|
expect(conversation.waiting_since).to eq conversation.created_at
|
|
|
|
create(:message, message_type: :outgoing, conversation: conversation, content_attributes: { automation_rule_id: 1 })
|
|
|
|
expect(conversation.first_reply_created_at).to be_nil
|
|
expect(conversation.waiting_since).to eq conversation.created_at
|
|
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
|
|
|
|
it 'uses outgoing_content for webhook content' do
|
|
message = create(:message, content: 'Test content')
|
|
expect(message).to receive(:outgoing_content).and_return('Outgoing test content')
|
|
|
|
webhook_data = message.webhook_data
|
|
expect(webhook_data[:content]).to eq('Outgoing test content')
|
|
end
|
|
|
|
it 'includes CSAT survey link in webhook content for input_csat messages' do
|
|
inbox = create(:inbox, channel: create(:channel_api))
|
|
conversation = create(:conversation, inbox: inbox)
|
|
message = create(:message, conversation: conversation, content_type: 'input_csat', content: 'Rate your experience')
|
|
|
|
expect(message.outgoing_content).to include('survey/responses/')
|
|
expect(message.webhook_data[:content]).to include('survey/responses/')
|
|
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
|
|
let(:inbox_with_continuity) do
|
|
create(:inbox, account: message.account,
|
|
channel: build(:channel_widget, account: message.account, continuity_via_email: true))
|
|
end
|
|
|
|
it 'schedules email notification for outgoing messages in website channel' do
|
|
message.inbox = inbox_with_continuity
|
|
message.conversation.update!(inbox: inbox_with_continuity)
|
|
message.conversation.contact.update!(email: 'test@example.com')
|
|
message.message_type = 'outgoing'
|
|
|
|
# Perform jobs inline to test full integration
|
|
perform_enqueued_jobs do
|
|
message.save!
|
|
end
|
|
|
|
# Verify the email worker is eventually scheduled through the service
|
|
jobs_for_conversation_count = ConversationReplyEmailWorker.jobs.count { |job| job['args'].first == message.conversation.id }
|
|
expect(jobs_for_conversation_count).to eq(1)
|
|
end
|
|
|
|
it 'does not schedule email for website channel if continuity is disabled' do
|
|
inbox_without_continuity = create(:inbox, account: message.account,
|
|
channel: build(:channel_widget, account: message.account, continuity_via_email: false))
|
|
message.inbox = inbox_without_continuity
|
|
message.conversation.update!(inbox: inbox_without_continuity)
|
|
message.conversation.contact.update!(email: 'test@example.com')
|
|
message.message_type = 'outgoing'
|
|
|
|
initial_job_count = ConversationReplyEmailWorker.jobs.count { |job| job['args'].first == message.conversation.id }
|
|
|
|
perform_enqueued_jobs do
|
|
message.save!
|
|
end
|
|
|
|
# No new jobs should be scheduled for this conversation
|
|
jobs_for_conversation_count = ConversationReplyEmailWorker.jobs.count { |job| job['args'].first == message.conversation.id }
|
|
expect(jobs_for_conversation_count).to eq(initial_job_count)
|
|
end
|
|
|
|
it 'does not schedule email for private notes' do
|
|
message.inbox = inbox_with_continuity
|
|
message.conversation.update!(inbox: inbox_with_continuity)
|
|
message.conversation.contact.update!(email: 'test@example.com')
|
|
message.private = true
|
|
message.message_type = 'outgoing'
|
|
|
|
initial_job_count = ConversationReplyEmailWorker.jobs.count { |job| job['args'].first == message.conversation.id }
|
|
|
|
perform_enqueued_jobs do
|
|
message.save!
|
|
end
|
|
|
|
# No new jobs should be scheduled for this conversation
|
|
jobs_for_conversation_count = ConversationReplyEmailWorker.jobs.count { |job| job['args'].first == message.conversation.id }
|
|
expect(jobs_for_conversation_count).to eq(initial_job_count)
|
|
end
|
|
|
|
it 'calls SendReplyJob for all channels' do
|
|
allow(SendReplyJob).to receive(:perform_later).and_return(true)
|
|
message.message_type = 'outgoing'
|
|
message.save!
|
|
expect(SendReplyJob).to have_received(:perform_later).with(message.id)
|
|
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
|
|
|
|
describe '#ensure_in_reply_to' do
|
|
let(:conversation) { create(:conversation) }
|
|
let(:message) { create(:message, conversation: conversation, source_id: 12_345) }
|
|
|
|
context 'when in_reply_to is present' do
|
|
let(:content_attributes) { { in_reply_to: message.id } }
|
|
let(:new_message) { build(:message, conversation: conversation, content_attributes: content_attributes) }
|
|
|
|
it 'sets in_reply_to_external_id based on the source_id of the referenced message' do
|
|
new_message.send(:ensure_in_reply_to)
|
|
expect(new_message.content_attributes[:in_reply_to_external_id]).to eq(message.source_id)
|
|
end
|
|
end
|
|
|
|
context 'when in_reply_to is not present' do
|
|
let(:content_attributes) { { in_reply_to_external_id: message.source_id } }
|
|
let(:new_message) { build(:message, conversation: conversation, content_attributes: content_attributes) }
|
|
|
|
it 'sets in_reply_to based on the source_id of the referenced message' do
|
|
new_message.send(:ensure_in_reply_to)
|
|
expect(new_message.content_attributes[:in_reply_to]).to eq(message.id)
|
|
end
|
|
end
|
|
|
|
context 'when the referenced message is not found' do
|
|
let(:content_attributes) { { in_reply_to: message.id + 1 } }
|
|
let(:new_message) { build(:message, conversation: conversation, content_attributes: content_attributes) }
|
|
|
|
it 'does not set in_reply_to_external_id' do
|
|
new_message.send(:ensure_in_reply_to)
|
|
expect(new_message.content_attributes[:in_reply_to_external_id]).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'when the source message is not found' do
|
|
let(:content_attributes) { { in_reply_to_external_id: 'source-id-that-does-not-exist' } }
|
|
let(:new_message) { build(:message, conversation: conversation, content_attributes: content_attributes) }
|
|
|
|
it 'does not set in_reply_to' do
|
|
new_message.send(:ensure_in_reply_to)
|
|
expect(new_message.content_attributes[:in_reply_to]).to be_nil
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#content' do
|
|
let(:conversation) { create(:conversation) }
|
|
|
|
context 'when message is not input_csat' do
|
|
let(:message) { create(:message, conversation: conversation, content_type: 'text', content: 'Regular message') }
|
|
|
|
it 'returns original content' do
|
|
expect(message.content).to eq('Regular message')
|
|
end
|
|
end
|
|
|
|
context 'when message is input_csat' do
|
|
let(:message) { create(:message, conversation: conversation, content_type: 'input_csat', content: 'Rate your experience') }
|
|
|
|
context 'when inbox is web widget' do
|
|
before do
|
|
allow(message.inbox).to receive(:web_widget?).and_return(true)
|
|
end
|
|
|
|
it 'returns original content without survey URL' do
|
|
expect(message.content).to eq('Rate your experience')
|
|
end
|
|
end
|
|
|
|
context 'when inbox is not web widget' do
|
|
before do
|
|
allow(message.inbox).to receive(:web_widget?).and_return(false)
|
|
end
|
|
|
|
it 'returns only the stored content (clean for dashboard)' do
|
|
expect(message.content).to eq('Rate your experience')
|
|
end
|
|
|
|
it 'returns only the base content without URL when survey_url stored separately' do
|
|
message.content_attributes = { 'survey_url' => 'https://app.chatwoot.com/survey/responses/12345' }
|
|
expect(message.content).to eq('Rate your experience')
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#outgoing_content' do
|
|
let(:conversation) { create(:conversation) }
|
|
let(:message) { create(:message, conversation: conversation, content_type: 'text', content: 'Regular message') }
|
|
|
|
it 'delegates to MessageContentPresenter' do
|
|
presenter = instance_double(MessageContentPresenter)
|
|
allow(MessageContentPresenter).to receive(:new).with(message).and_return(presenter)
|
|
allow(presenter).to receive(:outgoing_content).and_return('Presented content')
|
|
|
|
expect(message.outgoing_content).to eq('Presented content')
|
|
expect(MessageContentPresenter).to have_received(:new).with(message)
|
|
expect(presenter).to have_received(:outgoing_content)
|
|
end
|
|
end
|
|
|
|
describe '#auto_reply_email?' do
|
|
context 'when message is not an incoming email and inbox is not email' do
|
|
let(:conversation) { create(:conversation) }
|
|
let(:message) { create(:message, conversation: conversation, message_type: :outgoing) }
|
|
|
|
it 'returns false' do
|
|
expect(message.auto_reply_email?).to be false
|
|
end
|
|
end
|
|
|
|
context 'when message is an incoming email' do
|
|
let(:email_channel) { create(:channel_email) }
|
|
let(:email_inbox) { create(:inbox, channel: email_channel) }
|
|
let(:conversation) { create(:conversation, inbox: email_inbox) }
|
|
|
|
it 'returns false when auto_reply is not set to true' do
|
|
message = create(
|
|
:message,
|
|
conversation: conversation,
|
|
message_type: :incoming,
|
|
content_type: 'incoming_email',
|
|
content_attributes: {}
|
|
)
|
|
expect(message.auto_reply_email?).to be false
|
|
end
|
|
|
|
it 'returns true when auto_reply is set to true' do
|
|
message = create(
|
|
:message,
|
|
conversation: conversation,
|
|
message_type: :incoming,
|
|
content_type: 'incoming_email',
|
|
content_attributes: { email: { auto_reply: true } }
|
|
)
|
|
expect(message.auto_reply_email?).to be true
|
|
end
|
|
end
|
|
|
|
context 'when inbox is email' do
|
|
let(:email_channel) { create(:channel_email) }
|
|
let(:email_inbox) { create(:inbox, channel: email_channel) }
|
|
let(:conversation) { create(:conversation, inbox: email_inbox) }
|
|
|
|
it 'returns false when auto_reply is not set to true' do
|
|
message = create(
|
|
:message,
|
|
conversation: conversation,
|
|
message_type: :outgoing,
|
|
content_attributes: {}
|
|
)
|
|
expect(message.auto_reply_email?).to be false
|
|
end
|
|
|
|
it 'returns true when auto_reply is set to true' do
|
|
message = create(
|
|
:message,
|
|
conversation: conversation,
|
|
message_type: :outgoing,
|
|
content_attributes: { email: { auto_reply: true } }
|
|
)
|
|
expect(message.auto_reply_email?).to be true
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#should_index?' do
|
|
let(:account) { create(:account) }
|
|
let(:conversation) { create(:conversation, account: account) }
|
|
let(:message) { create(:message, conversation: conversation, account: account) }
|
|
|
|
before do
|
|
allow(ChatwootApp).to receive(:advanced_search_allowed?).and_return(true)
|
|
account.enable_features('advanced_search_indexing')
|
|
end
|
|
|
|
context 'when advanced search is not allowed globally' do
|
|
before do
|
|
allow(ChatwootApp).to receive(:advanced_search_allowed?).and_return(false)
|
|
end
|
|
|
|
it 'returns false' do
|
|
expect(message.should_index?).to be false
|
|
end
|
|
end
|
|
|
|
context 'when advanced search feature is not enabled for account on chatwoot cloud' do
|
|
before do
|
|
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
|
|
account.disable_features('advanced_search_indexing')
|
|
end
|
|
|
|
it 'returns false' do
|
|
expect(message.should_index?).to be false
|
|
end
|
|
end
|
|
|
|
context 'when advanced search feature is not enabled for account on self-hosted' do
|
|
before do
|
|
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(false)
|
|
account.disable_features('advanced_search_indexing')
|
|
end
|
|
|
|
it 'returns true' do
|
|
expect(message.should_index?).to be true
|
|
end
|
|
end
|
|
|
|
context 'when message type is not incoming or outgoing' do
|
|
before do
|
|
message.message_type = 'activity'
|
|
end
|
|
|
|
it 'returns false' do
|
|
expect(message.should_index?).to be false
|
|
end
|
|
end
|
|
|
|
context 'when all conditions are met' do
|
|
it 'returns true for incoming message' do
|
|
message.message_type = 'incoming'
|
|
expect(message.should_index?).to be true
|
|
end
|
|
|
|
it 'returns true for outgoing message' do
|
|
message.message_type = 'outgoing'
|
|
expect(message.should_index?).to be true
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#reindex_for_search callback' do
|
|
let(:account) { create(:account) }
|
|
let(:conversation) { create(:conversation, account: account) }
|
|
|
|
before do
|
|
allow(ChatwootApp).to receive(:advanced_search_allowed?).and_return(true)
|
|
account.enable_features('advanced_search_indexing')
|
|
end
|
|
|
|
context 'when message should be indexed' do
|
|
it 'calls reindex_for_search for incoming message on create' do
|
|
message = build(:message, conversation: conversation, account: account, message_type: :incoming)
|
|
expect(message).to receive(:reindex_for_search)
|
|
message.save!
|
|
end
|
|
|
|
it 'calls reindex_for_search for outgoing message on update' do
|
|
# rubocop:disable RSpec/AnyInstance
|
|
allow_any_instance_of(described_class).to receive(:reindex_for_search).and_return(true)
|
|
# rubocop:enable RSpec/AnyInstance
|
|
message = create(:message, conversation: conversation, account: account, message_type: :outgoing)
|
|
expect(message).to receive(:reindex_for_search).and_return(true)
|
|
message.update!(content: 'Updated content')
|
|
end
|
|
end
|
|
|
|
context 'when message should not be indexed' do
|
|
it 'does not call reindex_for_search for activity message' do
|
|
message = build(:message, conversation: conversation, account: account, message_type: :activity)
|
|
expect(message).not_to receive(:reindex_for_search)
|
|
message.save!
|
|
end
|
|
|
|
it 'does not call reindex_for_search for unpaid account on cloud' do
|
|
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
|
|
account.disable_features('advanced_search_indexing')
|
|
message = build(:message, conversation: conversation, account: account, message_type: :incoming)
|
|
expect(message).not_to receive(:reindex_for_search)
|
|
message.save!
|
|
end
|
|
|
|
it 'does not call reindex_for_search when advanced search is not allowed' do
|
|
allow(ChatwootApp).to receive(:advanced_search_allowed?).and_return(false)
|
|
message = build(:message, conversation: conversation, account: account, message_type: :incoming)
|
|
expect(message).not_to receive(:reindex_for_search)
|
|
message.save!
|
|
end
|
|
end
|
|
end
|
|
end
|