Initial commit: Add logistics and order_detail message types
Some checks failed
Lock Threads / action (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Run Linux nightly installer / nightly (push) Has been cancelled

- Add Logistics component with progress tracking
- Add OrderDetail component for order information
- Support data-driven steps and actions
- Add blue color scale to widget SCSS
- Fix node overflow and progress bar rendering issues
- Add English translations for dashboard components

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Liang XJ
2026-01-26 11:16:56 +08:00
commit 092fb2e083
7646 changed files with 975643 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
require 'rails_helper'
describe Integrations::Slack::HookBuilder do
let(:account) { create(:account) }
let(:code) { SecureRandom.hex }
let(:token) { SecureRandom.hex }
describe '#perform' do
it 'creates hook' do
hooks_count = account.hooks.count
builder = described_class.new(account: account, code: code)
allow(builder).to receive(:fetch_access_token).and_return(token)
builder.perform
expect(account.hooks.count).to eql(hooks_count + 1)
hook = account.hooks.last
expect(hook.access_token).to eql(token)
end
end
end

View File

@@ -0,0 +1,194 @@
require 'rails_helper'
describe Integrations::Slack::IncomingMessageBuilder do
let(:account) { create(:account) }
let(:message_params) { slack_message_stub }
let(:builder) { described_class.new(hook: hook) }
let(:private_message_params) do
{
team_id: 'TLST3048H',
api_app_id: 'A012S5UETV4',
event: message_event.merge({ text: 'pRivate: A private note message' }),
type: 'event_callback',
event_time: 1_588_623_033
}
end
let(:sub_type_message) do
{
team_id: 'TLST3048H',
api_app_id: 'A012S5UETV4',
event: message_event.merge({ type: 'message', subtype: 'bot_message' }),
type: 'event_callback',
event_time: 1_588_623_033
}
end
let(:message_without_user) do
{
team_id: 'TLST3048H',
api_app_id: 'A012S5UETV4',
event: message_event.merge({ type: 'message', user: nil }),
type: 'event_callback',
event_time: 1_588_623_033
}
end
let(:slack_client) { double }
let(:link_unfurl_service) { double }
let(:message_with_attachments) { slack_attachment_stub }
let(:message_without_thread_ts) { slack_message_stub_without_thread_ts }
let(:verification_params) { slack_url_verification_stub }
let!(:hook) { create(:integrations_hook, account: account, reference_id: message_params[:event][:channel]) }
let!(:conversation) { create(:conversation, identifier: message_params[:event][:thread_ts]) }
before do
stub_request(:get, 'https://chatwoot-assets.local/sample.png').to_return(
status: 200,
body: File.read('spec/assets/sample.png'),
headers: {}
)
end
describe '#perform' do
context 'when url verification' do
it 'return challenge code as response' do
builder = described_class.new(verification_params)
response = builder.perform
expect(response[:challenge]).to eql(verification_params[:challenge])
end
end
context 'when message creation' do
it 'doesnot create message if thread info is missing' do
messages_count = conversation.messages.count
builder = described_class.new(message_without_thread_ts)
builder.perform
expect(conversation.messages.count).to eql(messages_count)
end
it 'does not create message if message already exists' do
expect(hook).not_to be_nil
messages_count = conversation.messages.count
builder = described_class.new(message_params)
allow(builder).to receive(:sender).and_return(nil)
2.times.each { builder.perform }
expect(conversation.messages.count).to eql(messages_count + 1)
expect(conversation.messages.last.content).to eql('this is test https://chatwoot.com Hey @Sojan Test again')
end
it 'creates message' do
expect(hook).not_to be_nil
messages_count = conversation.messages.count
builder = described_class.new(message_params)
allow(builder).to receive(:sender).and_return(nil)
builder.perform
expect(conversation.messages.count).to eql(messages_count + 1)
expect(conversation.messages.last.content).to eql('this is test https://chatwoot.com Hey @Sojan Test again')
end
it 'creates a private note' do
expect(hook).not_to be_nil
messages_count = conversation.messages.count
builder = described_class.new(private_message_params)
allow(builder).to receive(:sender).and_return(nil)
builder.perform
expect(conversation.messages.count).to eql(messages_count + 1)
expect(conversation.messages.last.content).to eql('pRivate: A private note message')
expect(conversation.messages.last.private).to be(true)
end
it 'does not create message for invalid event type' do
messages_count = conversation.messages.count
message_params[:type] = 'invalid_event_type'
builder = described_class.new(message_params)
builder.perform
expect(conversation.messages.count).to eql(messages_count)
end
it 'does not create message for invalid event name' do
messages_count = conversation.messages.count
message_params[:event][:type] = 'invalid_event_name'
builder = described_class.new(message_params)
builder.perform
expect(conversation.messages.count).to eql(messages_count)
end
it 'does not create message for message sub type events' do
messages_count = conversation.messages.count
builder = described_class.new(sub_type_message)
builder.perform
expect(conversation.messages.count).to eql(messages_count)
end
it 'does not create message if user is missing' do
messages_count = conversation.messages.count
builder = described_class.new(message_without_user)
builder.perform
expect(conversation.messages.count).to eql(messages_count)
end
it 'does not create message for invalid event type and event files is not present' do
messages_count = conversation.messages.count
message_with_attachments[:event][:files] = nil
builder = described_class.new(message_with_attachments)
allow(builder).to receive(:sender).and_return(nil)
builder.perform
expect(conversation.messages.count).to eql(messages_count)
end
it 'saves attachment if params files present' do
expect(hook).not_to be_nil
messages_count = conversation.messages.count
builder = described_class.new(message_with_attachments)
allow(builder).to receive(:sender).and_return(nil)
builder.perform
expect(conversation.messages.count).to eql(messages_count + 1)
expect(conversation.messages.last.content).to eql('this is test https://chatwoot.com Hey @Sojan Test again')
expect(conversation.messages.last.attachments).to be_any
end
it 'ignore message if it is postback of CW attachment message' do
expect(hook).not_to be_nil
messages_count = conversation.messages.count
message_with_attachments[:event][:text] = 'Attached File!'
builder = described_class.new(message_with_attachments)
allow(builder).to receive(:sender).and_return(nil)
builder.perform
expect(conversation.messages.count).to eql(messages_count)
end
it 'handles different file types correctly' do
expect(hook).not_to be_nil
video_attachment_params = message_with_attachments.deep_dup
video_attachment_params[:event][:files][0][:filetype] = 'mp4'
video_attachment_params[:event][:files][0][:mimetype] = 'video/mp4'
builder = described_class.new(video_attachment_params)
allow(builder).to receive(:sender).and_return(nil)
expect { builder.perform }.not_to raise_error
expect(conversation.messages.last.attachments).to be_any
end
end
context 'when link shared' do
let(:link_shared) do
{
team_id: 'TLST3048H',
api_app_id: 'A012S5UETV4',
event: link_shared_event.merge({ links: [{ url: "https://qa.chatwoot.com/app/accounts/1/conversations/#{conversation.display_id}",
domain: 'qa.chatwoot.com' }] }),
type: 'event_callback',
event_time: 1_588_623_033
}
end
it 'unfurls link' do
builder = described_class.new(link_shared)
expect(SlackUnfurlJob).to receive(:perform_later).with(link_shared)
builder.perform
end
end
end
end

View File

@@ -0,0 +1,61 @@
require 'rails_helper'
describe Integrations::Slack::LinkUnfurlFormatter do
let(:contact) { create(:contact) }
let(:inbox) { create(:inbox) }
let(:url) { 'https://example.com/app/accounts/1/conversations/100' }
describe '#perform' do
context 'when unfurling a URL' do
let(:user_info) do
{
user_name: contact.name,
email: contact.email,
phone_number: '---',
company_name: '---'
}
end
let(:expected_payload) do
{
url => {
'blocks' => [
{
'type' => 'section',
'fields' => [
{ 'type' => 'mrkdwn', 'text' => "*Name:*\n#{contact.name}" },
{ 'type' => 'mrkdwn', 'text' => "*Email:*\n#{contact.email}" },
{ 'type' => 'mrkdwn', 'text' => "*Phone:*\n---" },
{ 'type' => 'mrkdwn', 'text' => "*Company:*\n---" },
{ 'type' => 'mrkdwn', 'text' => "*Inbox:*\n#{inbox.name}" },
{ 'type' => 'mrkdwn', 'text' => "*Inbox Type:*\n#{inbox.channel_type}" }
]
},
{
'type' => 'actions',
'elements' => [
{
'type' => 'button',
'text' => { 'type' => 'plain_text', 'text' => 'Open conversation', 'emoji' => true },
'url' => url,
'action_id' => 'button-action'
}
]
}
]
}
}
end
it 'returns expected unfurl blocks when the URL is not blank' do
formatter = described_class.new(url: url, user_info: user_info, inbox_name: inbox.name, inbox_type: inbox.channel_type)
expect(formatter.perform).to eq(expected_payload)
end
it 'returns an empty hash when the URL is blank' do
formatter = described_class.new(url: nil, user_info: user_info, inbox_name: inbox.name, inbox_type: inbox.channel_type)
expect(formatter.perform).to eq({})
end
end
end
end

View File

@@ -0,0 +1,406 @@
require 'rails_helper'
describe Integrations::Slack::SendOnSlackService do
let!(:contact) { create(:contact) }
let(:channel_email) { create(:channel_email) }
let!(:conversation) { create(:conversation, inbox: channel_email.inbox, contact: contact, identifier: nil) }
let(:account) { conversation.account }
let!(:hook) { create(:integrations_hook, account: account) }
let!(:message) do
create(:message, account: conversation.account, inbox: conversation.inbox, conversation: conversation)
end
let!(:template_message) do
create(:message, account: conversation.account, inbox: conversation.inbox, conversation: conversation, message_type: :template)
end
let(:slack_message) { double }
let(:file_attachment) { double }
let(:slack_message_content) { double }
let(:slack_client) { double }
let(:builder) { described_class.new(message: message, hook: hook) }
let(:link_builder) { described_class.new(message: nil, hook: hook) }
let(:conversation_link) do
"<#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/conversations/#{conversation.display_id}|Click here> to view the conversation."
end
before do
allow(builder).to receive(:slack_client).and_return(slack_client)
allow(link_builder).to receive(:slack_client).and_return(slack_client)
allow(slack_message).to receive(:[]).with('ts').and_return('12345.6789')
allow(slack_message).to receive(:[]).with('message').and_return(slack_message_content)
allow(slack_message_content).to receive(:[]).with('ts').and_return('6789.12345')
allow(slack_message_content).to receive(:[]).with('thread_ts').and_return('12345.6789')
end
describe '#perform' do
context 'without identifier' do
it 'updates slack thread id in conversation' do
inbox = conversation.inbox
expect(slack_client).to receive(:chat_postMessage).with(
channel: hook.reference_id,
text: "\n*Inbox:* #{inbox.name} (#{inbox.inbox_type})\n#{conversation_link}\n\n#{message.content}",
username: "#{message.sender.name} (Contact)",
thread_ts: nil,
icon_url: anything,
unfurl_links: false
).and_return(slack_message)
builder.perform
expect(conversation.reload.identifier).to eq '12345.6789'
end
context 'with subject line in email' do
let(:message) do
create(:message,
content_attributes: { 'email': { 'subject': 'Sample subject line' } },
content: 'Sample Body',
account: conversation.account,
inbox: conversation.inbox, conversation: conversation)
end
it 'creates slack message with subject line' do
inbox = conversation.inbox
expect(slack_client).to receive(:chat_postMessage).with(
channel: hook.reference_id,
text: "\n*Inbox:* #{inbox.name} (#{inbox.inbox_type})\n#{conversation_link}\n" \
"*Subject:* Sample subject line\n\n\n#{message.content}",
username: "#{message.sender.name} (Contact)",
thread_ts: nil,
icon_url: anything,
unfurl_links: false
).and_return(slack_message)
builder.perform
expect(conversation.reload.identifier).to eq '12345.6789'
end
end
end
context 'with identifier' do
before do
conversation.update!(identifier: 'random_slack_thread_ts')
end
it 'sent message to slack' do
expect(slack_client).to receive(:chat_postMessage).with(
channel: hook.reference_id,
text: message.content,
username: "#{message.sender.name} (Contact)",
thread_ts: conversation.identifier,
icon_url: anything,
unfurl_links: true
).and_return(slack_message)
builder.perform
expect(message.external_source_id_slack).to eq 'cw-origin-6789.12345'
end
it 'sent message will send to the the previous thread if the slack disconnects and connects to a same channel.' do
allow(slack_message).to receive(:[]).with('message').and_return({ 'thread_ts' => conversation.identifier })
expect(slack_client).to receive(:chat_postMessage).with(
channel: hook.reference_id,
text: message.content,
username: "#{message.sender.name} (Contact)",
thread_ts: conversation.identifier,
icon_url: anything,
unfurl_links: true
).and_return(slack_message)
builder.perform
expect(conversation.identifier).to eq 'random_slack_thread_ts'
end
it 'sent message will create a new thread if the slack disconnects and connects to a different channel' do
allow(slack_message).to receive(:[]).with('message').and_return({ 'thread_ts' => nil })
allow(slack_message).to receive(:[]).with('ts').and_return('1691652432.896169')
hook.update!(reference_id: 'C12345')
expect(slack_client).to receive(:chat_postMessage).with(
channel: 'C12345',
text: message.content,
username: "#{message.sender.name} (Contact)",
thread_ts: conversation.identifier,
icon_url: anything,
unfurl_links: true
).and_return(slack_message)
builder.perform
expect(hook.reload.reference_id).to eq 'C12345'
expect(conversation.identifier).to eq '1691652432.896169'
end
it 'sent lnk unfurl to slack' do
unflur_payload = { :channel => 'channel',
:ts => 'timestamp',
:unfurls =>
{ :'https://qa.chatwoot.com/app/accounts/1/conversations/1' =>
{ :blocks => [{ :type => 'section',
:text => { :type => 'plain_text', :text => 'This is a plain text section block.', :emoji => true } }] } } }
allow(slack_client).to receive(:chat_unfurl).with(unflur_payload)
link_builder.link_unfurl(unflur_payload)
expect(slack_client).to have_received(:chat_unfurl).with(unflur_payload)
end
it 'sent attachment on slack' do
expect(slack_client).to receive(:chat_postMessage).with(
channel: hook.reference_id,
text: message.content,
username: "#{message.sender.name} (Contact)",
thread_ts: conversation.identifier,
icon_url: anything,
unfurl_links: true
).and_return(slack_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')
expect(slack_client).to receive(:files_upload_v2).with(
files: [{
filename: attachment.file.filename.to_s,
content: anything,
title: attachment.file.filename.to_s
}],
channel_id: hook.reference_id,
thread_ts: conversation.identifier,
initial_comment: 'Attached File!'
).and_return(file_attachment)
message.save!
builder.perform
expect(message.external_source_id_slack).to eq 'cw-origin-6789.12345'
expect(message.attachments).to be_any
end
it 'sent multiple attachments on slack' do
expect(slack_client).to receive(:chat_postMessage).and_return(slack_message)
attachment1 = message.attachments.new(account_id: message.account_id, file_type: :image)
attachment1.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
attachment2 = message.attachments.new(account_id: message.account_id, file_type: :image)
attachment2.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'logo.png', content_type: 'image/png')
expected_files = [
{ filename: 'avatar.png', content: anything, title: 'avatar.png' },
{ filename: 'logo.png', content: anything, title: 'logo.png' }
]
expect(slack_client).to receive(:files_upload_v2).with(
files: expected_files,
channel_id: hook.reference_id,
thread_ts: conversation.identifier,
initial_comment: 'Attached File!'
).and_return(file_attachment)
message.save!
builder.perform
expect(message.attachments.count).to eq 2
end
it 'streams attachment blobs and uploads only once' do
expect(slack_client).to receive(:chat_postMessage).and_return(slack_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')
blob = attachment.file.blob
allow(blob).to receive(:open).and_call_original
expect(blob).to receive(:open).and_call_original
expect(slack_client).to receive(:files_upload_v2).once.and_return(file_attachment)
message.save!
builder.perform
end
it 'handles file upload errors gracefully' do
expect(slack_client).to receive(:chat_postMessage).with(
channel: hook.reference_id,
text: message.content,
username: "#{message.sender.name} (Contact)",
thread_ts: conversation.identifier,
icon_url: anything,
unfurl_links: true
).and_return(slack_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')
expect(slack_client).to receive(:files_upload_v2).with(
files: [{
filename: attachment.file.filename.to_s,
content: anything,
title: attachment.file.filename.to_s
}],
channel_id: hook.reference_id,
thread_ts: conversation.identifier,
initial_comment: 'Attached File!'
).and_raise(Slack::Web::Api::Errors::SlackError.new('File upload failed'))
expect(Rails.logger).to receive(:error).with('Failed to upload files: File upload failed')
message.save!
builder.perform
expect(message.external_source_id_slack).to eq 'cw-origin-6789.12345'
end
it 'will not call file_upload if attachment does not have a file (e.g facebook - fallback type)' do
expect(slack_client).to receive(:chat_postMessage).with(
channel: hook.reference_id,
text: message.content,
username: "#{message.sender.name} (Contact)",
thread_ts: conversation.identifier,
icon_url: anything,
unfurl_links: true
).and_return(slack_message)
message.attachments.new(account_id: message.account_id, file_type: :fallback)
expect(slack_client).not_to receive(:files_upload)
message.save!
builder.perform
expect(message.external_source_id_slack).to eq 'cw-origin-6789.12345'
expect(message.attachments).to be_any
end
it 'sent a template message on slack' do
builder = described_class.new(message: template_message, hook: hook)
allow(builder).to receive(:slack_client).and_return(slack_client)
template_message.update!(sender: nil)
expect(slack_client).to receive(:chat_postMessage).with(
channel: hook.reference_id,
text: template_message.content,
username: 'Bot',
thread_ts: conversation.identifier,
icon_url: anything,
unfurl_links: true
).and_return(slack_message)
builder.perform
expect(template_message.external_source_id_slack).to eq 'cw-origin-6789.12345'
end
it 'sent a activity message on slack' do
template_message.update!(message_type: :activity)
template_message.update!(sender: nil)
builder = described_class.new(message: template_message, hook: hook)
allow(builder).to receive(:slack_client).and_return(slack_client)
expect(slack_client).to receive(:chat_postMessage).with(
channel: hook.reference_id,
text: "_#{template_message.content}_",
username: 'System',
thread_ts: conversation.identifier,
icon_url: anything,
unfurl_links: true
).and_return(slack_message)
builder.perform
expect(template_message.external_source_id_slack).to eq 'cw-origin-6789.12345'
end
it 'disables hook on Slack AccountInactive error' do
expect(slack_client).to receive(:chat_postMessage).with(
channel: hook.reference_id,
text: message.content,
username: "#{message.sender.name} (Contact)",
thread_ts: conversation.identifier,
icon_url: anything,
unfurl_links: true
).and_raise(Slack::Web::Api::Errors::AccountInactive.new('Account disconnected'))
allow(hook).to receive(:prompt_reauthorization!)
builder.perform
expect(hook).to be_disabled
expect(hook).to have_received(:prompt_reauthorization!)
end
it 'disables hook on Slack MissingScope error' do
expect(slack_client).to receive(:chat_postMessage).with(
channel: hook.reference_id,
text: message.content,
username: "#{message.sender.name} (Contact)",
thread_ts: conversation.identifier,
icon_url: anything,
unfurl_links: true
).and_raise(Slack::Web::Api::Errors::MissingScope.new('Account disconnected'))
allow(hook).to receive(:prompt_reauthorization!)
builder.perform
expect(hook).to be_disabled
expect(hook).to have_received(:prompt_reauthorization!)
end
it 'logs MissingScope error during link unfurl' do
unflur_payload = { channel: 'channel', ts: 'timestamp', unfurls: {} }
error = Slack::Web::Api::Errors::MissingScope.new('Missing required scope')
expect(slack_client).to receive(:chat_unfurl)
.with(unflur_payload)
.and_raise(error)
expect(Rails.logger).to receive(:warn).with('Slack: Missing scope error: Missing required scope')
link_builder.link_unfurl(unflur_payload)
end
end
context 'when message contains mentions' do
it 'sends formatted message to slack along with inbox name when identifier not present' do
inbox = conversation.inbox
message.update!(content: "Hi [@#{contact.name}](mention://user/#{contact.id}/#{contact.name}), welcome to Chatwoot!")
formatted_message_text = message.content.gsub(RegexHelper::MENTION_REGEX, '\1')
expect(slack_client).to receive(:chat_postMessage).with(
channel: hook.reference_id,
text: "\n*Inbox:* #{inbox.name} (#{inbox.inbox_type})\n#{conversation_link}\n\n#{formatted_message_text}",
username: "#{message.sender.name} (Contact)",
thread_ts: nil,
icon_url: anything,
unfurl_links: false
).and_return(slack_message)
builder.perform
end
it 'sends formatted message to slack when identifier is present' do
conversation.update!(identifier: 'random_slack_thread_ts')
message.update!(content: "Hi [@#{contact.name}](mention://user/#{contact.id}/#{contact.name}), welcome to Chatwoot!")
formatted_message_text = message.content.gsub(RegexHelper::MENTION_REGEX, '\1')
expect(slack_client).to receive(:chat_postMessage).with(
channel: hook.reference_id,
text: formatted_message_text,
username: "#{message.sender.name} (Contact)",
thread_ts: 'random_slack_thread_ts',
icon_url: anything,
unfurl_links: true
).and_return(slack_message)
builder.perform
end
it 'will not throw error if message content is nil' do
message.update!(content: nil)
conversation.update!(identifier: 'random_slack_thread_ts')
expect { builder.perform }.not_to raise_error
end
end
end
end

View File

@@ -0,0 +1,163 @@
require 'rails_helper'
describe Integrations::Slack::SlackLinkUnfurlService do
let!(:contact) { create(:contact, name: 'Contact 1', email: nil, phone_number: nil) }
let(:channel_email) { create(:channel_email) }
let!(:conversation) { create(:conversation, inbox: channel_email.inbox, contact: contact, identifier: nil) }
let(:account) { conversation.account }
let!(:hook) { create(:integrations_hook, account: account) }
describe '#perform' do
context 'when the event does not contain any link' do
let(:link_shared) do
{
team_id: 'TLST3048H',
api_app_id: 'A012S5UETV4',
event: link_shared_event.merge(links: []),
type: 'event_callback',
event_time: 1_588_623_033
}
end
let(:link_builder) { described_class.new(params: link_shared, integration_hook: hook) }
it 'does not send a POST request to Slack API' do
result = link_builder.perform
expect(result).to eq([])
end
end
context 'when the event link contains the account id which does not match the integration hook account id' do
let(:link_shared) do
{
team_id: 'TLST3048H',
api_app_id: 'A012S5UETV4',
event: link_shared_event.merge(links: [{
url: "https://qa.chatwoot.com/app/accounts/1212/conversations/#{conversation.display_id}",
domain: 'qa.chatwoot.com'
}], channel: 'G054F6A6Q'),
type: 'event_callback',
event_time: 1_588_623_033
}
end
let(:link_builder) { described_class.new(params: link_shared, integration_hook: hook) }
it 'does not send a POST request to Slack API' do
link_builder.perform
expect(link_builder).not_to receive(:unfurl_link)
end
end
context 'when the event link contains the conversation id which does not belong to the account' do
let(:link_shared) do
{
team_id: 'TLST3048H',
api_app_id: 'A012S5UETV4',
event: link_shared_event.merge(links: [{
url: 'https://qa.chatwoot.com/app/accounts/1/conversations/1213',
domain: 'qa.chatwoot.com'
}], channel: 'G054F6A6Q'),
type: 'event_callback',
event_time: 1_588_623_033
}
end
let(:link_builder) { described_class.new(params: link_shared, integration_hook: hook) }
it 'does not send a POST request to Slack API' do
link_builder.perform
expect(link_builder).not_to receive(:unfurl_link)
end
end
context 'when the event contains containing single link' do
let(:link_shared) do
{
team_id: 'TLST3048H',
api_app_id: 'A012S5UETV4',
event: link_shared_event.merge(links: [{
url: "https://qa.chatwoot.com/app/accounts/1/conversations/#{conversation.display_id}",
domain: 'qa.chatwoot.com'
}]),
type: 'event_callback',
event_time: 1_588_623_033
}
end
let(:link_builder) { described_class.new(params: link_shared, integration_hook: hook) }
it 'sends a POST unfurl request to Slack' do
expected_body = {
'source' => 'conversations_history',
'unfurl_id' => 'C7NQEAE5Q.1695111587.937099.7e240338c6d2053fb49f56808e7c1f619f6ef317c39ebc59fc4af1cc30dce49b',
'unfurls' => '{"https://qa.chatwoot.com/app/accounts/1/conversations/1":' \
'{"blocks":[{' \
'"type":"section",' \
'"fields":[{' \
'"type":"mrkdwn",' \
"\"text\":\"*Name:*\\n#{contact.name}\"}," \
'{"type":"mrkdwn","text":"*Email:*\\n---"},' \
'{"type":"mrkdwn","text":"*Phone:*\\n---"},' \
'{"type":"mrkdwn","text":"*Company:*\\n---"},' \
"{\"type\":\"mrkdwn\",\"text\":\"*Inbox:*\\n#{channel_email.inbox.name}\"}," \
"{\"type\":\"mrkdwn\",\"text\":\"*Inbox Type:*\\n#{channel_email.inbox.channel.name}\"}]}," \
'{"type":"actions","elements":[{' \
'"type":"button",' \
'"text":{"type":"plain_text","text":"Open conversation","emoji":true},' \
'"url":"https://qa.chatwoot.com/app/accounts/1/conversations/1",' \
'"action_id":"button-action"}]}]}}'
}
stub_request(:post, 'https://slack.com/api/chat.unfurl')
.with(body: expected_body)
.to_return(status: 200, body: '', headers: {})
result = link_builder.perform
expect(result).to eq([{ url: 'https://qa.chatwoot.com/app/accounts/1/conversations/1', domain: 'qa.chatwoot.com' }])
end
end
context 'when the event contains containing multiple links' do
let(:link_shared_1) do
{
team_id: 'TLST3048H',
api_app_id: 'A012S5UETV4',
event: link_shared_event.merge(links: [{
url: "https://qa.chatwoot.com/app/accounts/1/conversations/#{conversation.display_id}",
domain: 'qa.chatwoot.com'
},
{
url: "https://qa.chatwoot.com/app/accounts/1/conversations/#{conversation.display_id}",
domain: 'qa.chatwoot.com'
}]),
type: 'event_callback',
event_time: 1_588_623_033
}
end
let(:link_builder) { described_class.new(params: link_shared_1, integration_hook: hook) }
it('sends multiple POST unfurl request to Slack') do
expected_body = {
'source' => 'conversations_history',
'unfurl_id' => 'C7NQEAE5Q.1695111587.937099.7e240338c6d2053fb49f56808e7c1f619f6ef317c39ebc59fc4af1cc30dce49b',
'unfurls' => '{"https://qa.chatwoot.com/app/accounts/1/conversations/1":' \
'{"blocks":[{' \
'"type":"section",' \
'"fields":[{' \
'"type":"mrkdwn",' \
"\"text\":\"*Name:*\\n#{contact.name}\"}," \
'{"type":"mrkdwn","text":"*Email:*\\n---"},' \
'{"type":"mrkdwn","text":"*Phone:*\\n---"},' \
'{"type":"mrkdwn","text":"*Company:*\\n---"},' \
"{\"type\":\"mrkdwn\",\"text\":\"*Inbox:*\\n#{channel_email.inbox.name}\"}," \
"{\"type\":\"mrkdwn\",\"text\":\"*Inbox Type:*\\n#{channel_email.inbox.channel.name}\"}]}," \
'{"type":"actions","elements":[{' \
'"type":"button",' \
'"text":{"type":"plain_text","text":"Open conversation","emoji":true},' \
'"url":"https://qa.chatwoot.com/app/accounts/1/conversations/1",' \
'"action_id":"button-action"}]}]}}'
}
stub_request(:post, 'https://slack.com/api/chat.unfurl')
.with(body: expected_body)
.to_return(status: 200, body: '', headers: {})
expect { link_builder.perform }.not_to raise_error
end
end
end
end