Merge branch 'develop' into feat/email-forwarding
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe CsatSurveyService do
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account, csat_survey_enabled: true) }
|
||||
let(:conversation) { create(:conversation, inbox: inbox, account: account, status: :resolved) }
|
||||
let(:service) { described_class.new(conversation: conversation) }
|
||||
|
||||
describe '#perform' do
|
||||
let(:csat_template) { instance_double(MessageTemplates::Template::CsatSurvey) }
|
||||
|
||||
before do
|
||||
allow(MessageTemplates::Template::CsatSurvey).to receive(:new).and_return(csat_template)
|
||||
allow(csat_template).to receive(:perform)
|
||||
allow(Conversations::ActivityMessageJob).to receive(:perform_later)
|
||||
end
|
||||
|
||||
context 'when CSAT survey should be sent' do
|
||||
before do
|
||||
allow(conversation).to receive(:can_reply?).and_return(true)
|
||||
end
|
||||
|
||||
it 'sends CSAT survey when within messaging window' do
|
||||
service.perform
|
||||
|
||||
expect(MessageTemplates::Template::CsatSurvey).to have_received(:new).with(conversation: conversation)
|
||||
expect(csat_template).to have_received(:perform)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when outside messaging window' do
|
||||
before do
|
||||
allow(conversation).to receive(:can_reply?).and_return(false)
|
||||
end
|
||||
|
||||
it 'creates activity message instead of sending survey' do
|
||||
service.perform
|
||||
|
||||
expect(Conversations::ActivityMessageJob).to have_received(:perform_later).with(
|
||||
conversation,
|
||||
hash_including(content: I18n.t('conversations.activity.csat.not_sent_due_to_messaging_window'))
|
||||
)
|
||||
expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when CSAT survey should not be sent' do
|
||||
it 'does nothing when conversation is not resolved' do
|
||||
conversation.update(status: :open)
|
||||
|
||||
service.perform
|
||||
|
||||
expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new)
|
||||
expect(Conversations::ActivityMessageJob).not_to have_received(:perform_later)
|
||||
end
|
||||
|
||||
it 'does nothing when CSAT survey is not enabled' do
|
||||
inbox.update(csat_survey_enabled: false)
|
||||
|
||||
service.perform
|
||||
|
||||
expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new)
|
||||
expect(Conversations::ActivityMessageJob).not_to have_received(:perform_later)
|
||||
end
|
||||
|
||||
it 'does nothing when CSAT already sent' do
|
||||
create(:message, conversation: conversation, content_type: :input_csat)
|
||||
|
||||
service.perform
|
||||
|
||||
expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new)
|
||||
expect(Conversations::ActivityMessageJob).not_to have_received(:perform_later)
|
||||
end
|
||||
|
||||
it 'does nothing for Twitter conversations' do
|
||||
twitter_channel = create(:channel_twitter_profile)
|
||||
twitter_inbox = create(:inbox, channel: twitter_channel, csat_survey_enabled: true)
|
||||
twitter_conversation = create(:conversation,
|
||||
inbox: twitter_inbox,
|
||||
status: :resolved,
|
||||
additional_attributes: { type: 'tweet' })
|
||||
twitter_service = described_class.new(conversation: twitter_conversation)
|
||||
|
||||
twitter_service.perform
|
||||
|
||||
expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new)
|
||||
expect(Conversations::ActivityMessageJob).not_to have_received(:perform_later)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -166,5 +166,33 @@ describe Facebook::SendOnFacebookService do
|
||||
expect(message.status).not_to eq('failed')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with input_select' do
|
||||
it 'if message with input_select is sent from chatwoot and is outgoing' do
|
||||
message = build(
|
||||
:message,
|
||||
message_type: 'outgoing',
|
||||
inbox: facebook_inbox,
|
||||
account: account,
|
||||
conversation: conversation,
|
||||
content_type: 'input_select',
|
||||
content_attributes: { 'items' => [{ 'title' => 'text 1', 'value' => 'value 1' }, { 'title' => 'text 2', 'value' => 'value 2' }] }
|
||||
)
|
||||
|
||||
described_class.new(message: message).perform
|
||||
expect(bot).to have_received(:deliver).with({
|
||||
recipient: { id: contact_inbox.source_id },
|
||||
message: {
|
||||
text: message.content,
|
||||
quick_replies: [
|
||||
{ content_type: 'text', payload: 'text 1', title: 'text 1' },
|
||||
{ content_type: 'text', payload: 'text 2', title: 'text 2' }
|
||||
]
|
||||
},
|
||||
messaging_type: 'MESSAGE_TAG',
|
||||
tag: 'ACCOUNT_UPDATE'
|
||||
}, { page_id: facebook_channel.page_id })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -93,6 +93,69 @@ describe Line::SendOnLineService do
|
||||
end
|
||||
end
|
||||
|
||||
context 'with message input_select' do
|
||||
let(:success_response) do
|
||||
{
|
||||
'message' => 'ok'
|
||||
}.to_json
|
||||
end
|
||||
|
||||
let(:expect_message) do
|
||||
{
|
||||
type: 'flex',
|
||||
altText: 'test',
|
||||
contents: {
|
||||
type: 'bubble',
|
||||
body: {
|
||||
type: 'box',
|
||||
layout: 'vertical',
|
||||
contents: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'test'
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
style: 'link',
|
||||
height: 'sm',
|
||||
action: {
|
||||
type: 'message',
|
||||
label: 'text 1',
|
||||
text: 'value 1'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
style: 'link',
|
||||
height: 'sm',
|
||||
action: {
|
||||
type: 'message',
|
||||
label: 'text 2',
|
||||
text: 'value 2'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'sends the message with input_select' do
|
||||
message = create(
|
||||
:message, message_type: :outgoing, content: 'test', content_type: 'input_select',
|
||||
content_attributes: { 'items' => [{ 'title' => 'text 1', 'value' => 'value 1' }, { 'title' => 'text 2', 'value' => 'value 2' }] },
|
||||
conversation: create(:conversation, inbox: line_channel.inbox)
|
||||
)
|
||||
|
||||
expect(line_client).to receive(:push_message).with(
|
||||
message.conversation.contact_inbox.source_id,
|
||||
expect_message
|
||||
).and_return(OpenStruct.new(code: '200', body: success_response))
|
||||
|
||||
described_class.new(message: message).perform
|
||||
end
|
||||
end
|
||||
|
||||
context 'with message attachments' do
|
||||
it 'sends the message with text and attachments' do
|
||||
attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Liquid::CampaignTemplateService do
|
||||
subject(:template_service) { described_class.new(campaign: campaign, contact: contact) }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let(:agent) { create(:user, account: account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:contact) { create(:contact, account: account, name: 'John Doe', phone_number: '+1234567890') }
|
||||
let(:campaign) { create(:campaign, account: account, inbox: inbox, sender: agent, message: message_content) }
|
||||
|
||||
describe '#call' do
|
||||
context 'with liquid template variables' do
|
||||
let(:message_content) { 'Hello {{contact.name}}, this is {{agent.name}} from {{account.name}}' }
|
||||
|
||||
it 'processes liquid template correctly' do
|
||||
result = template_service.call(message_content)
|
||||
agent_drop_name = UserDrop.new(agent).name
|
||||
contact_drop_name = ContactDrop.new(contact).name
|
||||
|
||||
expect(result).to eq("Hello #{contact_drop_name}, this is #{agent_drop_name} from #{account.name}")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with code blocks' do
|
||||
let(:message_content) { 'Check this code: `const x = {{contact.name}}`' }
|
||||
|
||||
it 'preserves code blocks without processing liquid' do
|
||||
result = template_service.call(message_content)
|
||||
|
||||
expect(result).to include('`const x = {{contact.name}}`')
|
||||
expect(result).not_to include(contact.name)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiline code blocks' do
|
||||
let(:message_content) do
|
||||
<<~MESSAGE
|
||||
Here's some code:
|
||||
```
|
||||
function greet() {
|
||||
return "Hello {{contact.name}}";
|
||||
}
|
||||
```
|
||||
MESSAGE
|
||||
end
|
||||
|
||||
it 'preserves multiline code blocks without processing liquid' do
|
||||
result = template_service.call(message_content)
|
||||
|
||||
expect(result).to include('{{contact.name}}')
|
||||
expect(result).not_to include(contact.name)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with malformed liquid syntax' do
|
||||
let(:message_content) { 'Hello {{contact.name missing closing braces' }
|
||||
|
||||
it 'returns original message when liquid parsing fails' do
|
||||
result = template_service.call(message_content)
|
||||
|
||||
expect(result).to eq(message_content)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid liquid tags' do
|
||||
let(:message_content) { 'Hello {% invalid_tag %} world' }
|
||||
|
||||
it 'returns original message when liquid parsing fails' do
|
||||
result = template_service.call(message_content)
|
||||
|
||||
expect(result).to eq(message_content)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with mixed content' do
|
||||
let(:message_content) { 'Hi {{contact.name}}, use this code: `{{agent.name}}` to contact {{agent.name}}' }
|
||||
|
||||
it 'processes liquid outside code blocks but preserves code blocks' do
|
||||
result = template_service.call(message_content)
|
||||
agent_drop_name = UserDrop.new(agent).name
|
||||
contact_drop_name = ContactDrop.new(contact).name
|
||||
|
||||
expect(result).to include("Hi #{contact_drop_name}")
|
||||
expect(result).to include("contact #{agent_drop_name}")
|
||||
expect(result).to include('`{{agent.name}}`')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with all drop types' do
|
||||
let(:message_content) do
|
||||
'Contact: {{contact.name}}, Agent: {{agent.name}}, Inbox: {{inbox.name}}, Account: {{account.name}}'
|
||||
end
|
||||
|
||||
it 'processes all available drops' do
|
||||
result = template_service.call(message_content)
|
||||
agent_drop_name = UserDrop.new(agent).name
|
||||
contact_drop_name = ContactDrop.new(contact).name
|
||||
|
||||
expect(result).to include("Contact: #{contact_drop_name}")
|
||||
expect(result).to include("Agent: #{agent_drop_name}")
|
||||
expect(result).to include("Inbox: #{inbox.name}")
|
||||
expect(result).to include("Account: #{account.name}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -111,69 +111,6 @@ describe MessageTemplates::HookExecutionService do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when CSAT Survey' do
|
||||
let(:csat_survey) { double }
|
||||
let(:conversation) { create(:conversation) }
|
||||
|
||||
before do
|
||||
allow(MessageTemplates::Template::CsatSurvey).to receive(:new).and_return(csat_survey)
|
||||
allow(csat_survey).to receive(:perform).and_return(true)
|
||||
create(:message, conversation: conversation, message_type: 'incoming')
|
||||
end
|
||||
|
||||
it 'calls ::MessageTemplates::Template::CsatSurvey when a conversation is resolved in an inbox with survey enabled' do
|
||||
conversation.inbox.update(csat_survey_enabled: true)
|
||||
|
||||
conversation.resolved!
|
||||
Conversations::ActivityMessageJob.perform_now(conversation,
|
||||
{ account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity,
|
||||
content: 'Conversation marked resolved!!' })
|
||||
|
||||
expect(MessageTemplates::Template::CsatSurvey).to have_received(:new).with(conversation: conversation)
|
||||
expect(csat_survey).to have_received(:perform)
|
||||
end
|
||||
|
||||
it 'will not call ::MessageTemplates::Template::CsatSurvey when Csat is not enabled' do
|
||||
conversation.inbox.update(csat_survey_enabled: false)
|
||||
|
||||
conversation.resolved!
|
||||
Conversations::ActivityMessageJob.perform_now(conversation,
|
||||
{ account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity,
|
||||
content: 'Conversation marked resolved!!' })
|
||||
|
||||
expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new).with(conversation: conversation)
|
||||
expect(csat_survey).not_to have_received(:perform)
|
||||
end
|
||||
|
||||
it 'will not call ::MessageTemplates::Template::CsatSurvey if its a tweet conversation' do
|
||||
twitter_channel = create(:channel_twitter_profile)
|
||||
twitter_inbox = create(:inbox, channel: twitter_channel)
|
||||
conversation = create(:conversation, inbox: twitter_inbox, additional_attributes: { type: 'tweet' })
|
||||
conversation.inbox.update(csat_survey_enabled: true)
|
||||
|
||||
conversation.resolved!
|
||||
Conversations::ActivityMessageJob.perform_now(conversation,
|
||||
{ account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity,
|
||||
content: 'Conversation marked resolved!!' })
|
||||
|
||||
expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new).with(conversation: conversation)
|
||||
expect(csat_survey).not_to have_received(:perform)
|
||||
end
|
||||
|
||||
it 'will not call ::MessageTemplates::Template::CsatSurvey if another Csat was already sent' do
|
||||
conversation.inbox.update(csat_survey_enabled: true)
|
||||
conversation.messages.create!(message_type: 'outgoing', content_type: :input_csat, account: conversation.account, inbox: conversation.inbox)
|
||||
|
||||
conversation.resolved!
|
||||
Conversations::ActivityMessageJob.perform_now(conversation,
|
||||
{ account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity,
|
||||
content: 'Conversation marked resolved!!' })
|
||||
|
||||
expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new).with(conversation: conversation)
|
||||
expect(csat_survey).not_to have_received(:perform)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is after working hours' do
|
||||
it 'calls ::MessageTemplates::Template::OutOfOffice' do
|
||||
contact = create(:contact)
|
||||
|
||||
@@ -43,5 +43,14 @@ describe Sms::OneoffSmsCampaignService do
|
||||
assert_requested(:post, 'https://messaging.bandwidth.com/api/v2/users/1/messages', times: 3)
|
||||
expect(campaign.reload.completed?).to be true
|
||||
end
|
||||
|
||||
it 'uses liquid template service to process campaign message' do
|
||||
contact = create(:contact, :with_phone_number, account: account)
|
||||
contact.update_labels([label1.title])
|
||||
|
||||
expect(Liquid::CampaignTemplateService).to receive(:new).with(campaign: campaign, contact: contact).and_call_original
|
||||
|
||||
sms_campaign_service.perform
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -179,6 +179,35 @@ describe Telegram::IncomingMessageService do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when valid video_note messages params' do
|
||||
it 'creates appropriate conversations, message and contacts' do
|
||||
allow(telegram_channel.inbox.channel).to receive(:get_telegram_file_path).and_return('https://chatwoot-assets.local/sample.mov')
|
||||
params = {
|
||||
'update_id' => 2_342_342_343_242,
|
||||
'message' => {
|
||||
'video_note' => {
|
||||
'duration' => 3,
|
||||
'length' => 240,
|
||||
'thumb' => {
|
||||
'file_id' => 'AAMCBQADGQEAA4ZhXd78Xz6_c6gCzbdIkgGiXJcwwwACqwMAAp3x8Fbhf3EWamgCWAEAB20AAyEE',
|
||||
'file_unique_id' => 'AQADqwMAAp3x8FZy',
|
||||
'file_size' => 11_462,
|
||||
'width' => 240,
|
||||
'height' => 240
|
||||
},
|
||||
'file_id' => 'DQACAgUAAxkBAAIBY2FdJlhf8PC2E3IalXSvXWO5m8GBAALJAwACwqHgVhb0truM0uhwIQQ',
|
||||
'file_unique_id' => 'AgADyQMAAsKh4FY',
|
||||
'file_size' => 132_446
|
||||
}
|
||||
}.merge(message_params)
|
||||
}.with_indifferent_access
|
||||
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
||||
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(Contact.all.first.name).to eq('Sojan Jose')
|
||||
expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('video')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when valid voice attachment params' do
|
||||
it 'creates appropriate conversations, message and contacts' do
|
||||
allow(telegram_channel.inbox.channel).to receive(:get_telegram_file_path).and_return('https://chatwoot-assets.local/sample.ogg')
|
||||
|
||||
@@ -60,5 +60,15 @@ describe Twilio::OneoffSmsCampaignService do
|
||||
sms_campaign_service.perform
|
||||
expect(campaign.reload.completed?).to be true
|
||||
end
|
||||
|
||||
it 'uses liquid template service to process campaign message' do
|
||||
contact = create(:contact, :with_phone_number, account: account)
|
||||
contact.update_labels([label1.title])
|
||||
|
||||
expect(Liquid::CampaignTemplateService).to receive(:new).with(campaign: campaign, contact: contact).and_call_original
|
||||
expect(twilio_messages).to receive(:create).once
|
||||
|
||||
sms_campaign_service.perform
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -51,16 +51,15 @@ describe Whatsapp::Providers::Whatsapp360DialogService do
|
||||
end
|
||||
|
||||
it 'calls message endpoints with list payload when number of items is greater than 3' do
|
||||
items = %w[Burito Pasta Sushi Salad].map { |i| { title: i, value: i } }
|
||||
message = create(:message, message_type: :outgoing, content: 'test', inbox: whatsapp_channel.inbox,
|
||||
content_type: 'input_select',
|
||||
content_attributes: {
|
||||
items: [
|
||||
{ title: 'Burito', value: 'Burito' },
|
||||
{ title: 'Pasta', value: 'Pasta' },
|
||||
{ title: 'Sushi', value: 'Sushi' },
|
||||
{ title: 'Salad', value: 'Salad' }
|
||||
]
|
||||
})
|
||||
content_type: 'input_select', content_attributes: { items: items })
|
||||
|
||||
expected_action = {
|
||||
button: I18n.t('conversations.messages.whatsapp.list_button_label'),
|
||||
sections: [{ rows: %w[Burito Pasta Sushi Salad].map { |i| { id: i, title: i } } }]
|
||||
}.to_json
|
||||
|
||||
stub_request(:post, 'https://waba.360dialog.io/v1/messages')
|
||||
.with(
|
||||
body: {
|
||||
@@ -70,9 +69,9 @@ describe Whatsapp::Providers::Whatsapp360DialogService do
|
||||
body: {
|
||||
text: 'test'
|
||||
},
|
||||
action: '{"button":"Choose an item","sections":[{"rows":[{"id":"Burito","title":"Burito"},' \
|
||||
'{"id":"Pasta","title":"Pasta"},{"id":"Sushi","title":"Sushi"},{"id":"Salad","title":"Salad"}]}]}'
|
||||
}, type: 'interactive'
|
||||
action: expected_action
|
||||
},
|
||||
type: 'interactive'
|
||||
}.to_json
|
||||
).to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
|
||||
expect(service.send_message('+123456789', message)).to eq 'message_id'
|
||||
|
||||
@@ -124,16 +124,15 @@ describe Whatsapp::Providers::WhatsappCloudService do
|
||||
end
|
||||
|
||||
it 'calls message endpoints with list payload when number of items is greater than 3' do
|
||||
items = %w[Burito Pasta Sushi Salad].map { |i| { title: i, value: i } }
|
||||
message = create(:message, message_type: :outgoing, content: 'test', inbox: whatsapp_channel.inbox,
|
||||
content_type: 'input_select',
|
||||
content_attributes: {
|
||||
items: [
|
||||
{ title: 'Burito', value: 'Burito' },
|
||||
{ title: 'Pasta', value: 'Pasta' },
|
||||
{ title: 'Sushi', value: 'Sushi' },
|
||||
{ title: 'Salad', value: 'Salad' }
|
||||
]
|
||||
})
|
||||
content_type: 'input_select', content_attributes: { items: items })
|
||||
|
||||
expected_action = {
|
||||
button: I18n.t('conversations.messages.whatsapp.list_button_label'),
|
||||
sections: [{ rows: %w[Burito Pasta Sushi Salad].map { |i| { id: i, title: i } } }]
|
||||
}.to_json
|
||||
|
||||
stub_request(:post, 'https://graph.facebook.com/v13.0/123456789/messages')
|
||||
.with(
|
||||
body: {
|
||||
@@ -143,9 +142,9 @@ describe Whatsapp::Providers::WhatsappCloudService do
|
||||
body: {
|
||||
text: 'test'
|
||||
},
|
||||
action: '{"button":"Choose an item","sections":[{"rows":[{"id":"Burito","title":"Burito"},' \
|
||||
'{"id":"Pasta","title":"Pasta"},{"id":"Sushi","title":"Sushi"},{"id":"Salad","title":"Salad"}]}]}'
|
||||
}, type: 'interactive'
|
||||
action: expected_action
|
||||
},
|
||||
type: 'interactive'
|
||||
}.to_json
|
||||
).to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
|
||||
expect(service.send_message('+123456789', message)).to eq 'message_id'
|
||||
|
||||
Reference in New Issue
Block a user