diff --git a/.rubocop.yml b/.rubocop.yml index 22e59629d..484e424ed 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -58,6 +58,7 @@ Metrics/BlockLength: Metrics/ModuleLength: Exclude: - lib/seeders/message_seeder.rb + - spec/support/slack_stubs.rb Rails/ApplicationController: Exclude: - 'app/controllers/api/v1/widget/messages_controller.rb' diff --git a/app/controllers/api/v1/integrations/webhooks_controller.rb b/app/controllers/api/v1/integrations/webhooks_controller.rb index 94373c92e..283be3d77 100644 --- a/app/controllers/api/v1/integrations/webhooks_controller.rb +++ b/app/controllers/api/v1/integrations/webhooks_controller.rb @@ -1,7 +1,15 @@ class Api::V1::Integrations::WebhooksController < ApplicationController def create - builder = Integrations::Slack::IncomingMessageBuilder.new(params) + builder = Integrations::Slack::IncomingMessageBuilder.new(permitted_params) response = builder.perform render json: response end + + private + + # TODO: This is a temporary solution to permit all params for slack unfurling job. + # We should only permit the params that we use. Handle all the params based on events and send it to the respective services. + def permitted_params + params.permit! + end end diff --git a/app/jobs/slack_unfurl_job.rb b/app/jobs/slack_unfurl_job.rb new file mode 100644 index 000000000..6829f7f49 --- /dev/null +++ b/app/jobs/slack_unfurl_job.rb @@ -0,0 +1,7 @@ +class SlackUnfurlJob < ApplicationJob + queue_as :low + + def perform(params, integration_hook) + Integrations::Slack::SlackLinkUnfurlService.new(params: params, integration_hook: integration_hook).perform + end +end diff --git a/config/initializers/monkey_patches/chat.rb b/config/initializers/monkey_patches/chat.rb new file mode 100644 index 000000000..c71d907c1 --- /dev/null +++ b/config/initializers/monkey_patches/chat.rb @@ -0,0 +1,21 @@ +module Slack + module Web + module Api + module Endpoints + module Chat + # TODO: Remove this monkey patch when PR for this issue https://github.com/slack-ruby/slack-ruby-client/issues/388 is merged + def chat_unfurl(options = {}) + if (options[:channel].nil? || options[:ts].nil?) && (options[:unfurl_id].nil? || options[:source].nil?) + raise ArgumentError, 'Either a combination of :channel and :ts or :unfurl_id and :source is required' + end + + raise ArgumentError, 'Required arguments :unfurls missing' if options[:unfurls].nil? + + options = options.merge(channel: conversations_id(options)['channel']['id']) if options[:channel] + post('chat.unfurl', options) + end + end + end + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 9a9b82ceb..dcec3e543 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -218,3 +218,12 @@ en: made_with: Made with header: go_to_homepage: Go to the main site + slack_unfurl: + fields: + name: Name + email: Email + phone_number: Phone + company_name: Company + inbox_name: Inbox + inbox_type: Inbox Type + button: Open conversation diff --git a/lib/integrations/slack/incoming_message_builder.rb b/lib/integrations/slack/incoming_message_builder.rb index 68bfa6e0b..09d137a4a 100644 --- a/lib/integrations/slack/incoming_message_builder.rb +++ b/lib/integrations/slack/incoming_message_builder.rb @@ -1,9 +1,10 @@ class Integrations::Slack::IncomingMessageBuilder include Integrations::Slack::SlackMessageHelper + attr_reader :params SUPPORTED_EVENT_TYPES = %w[event_callback url_verification].freeze - SUPPORTED_EVENTS = %w[message].freeze + SUPPORTED_EVENTS = %w[message link_shared].freeze SUPPORTED_MESSAGE_TYPES = %w[rich_text].freeze def initialize(params) @@ -17,6 +18,8 @@ class Integrations::Slack::IncomingMessageBuilder verify_hook elsif create_message? create_message + elsif link_shared? + SlackUnfurlJob.perform_later(params, integration_hook) end end @@ -68,6 +71,10 @@ class Integrations::Slack::IncomingMessageBuilder thread_timestamp_available? && supported_message? && integration_hook end + def link_shared? + params[:event][:type] == 'link_shared' && integration_hook + end + def message params[:event][:blocks]&.first end @@ -82,19 +89,6 @@ class Integrations::Slack::IncomingMessageBuilder @integration_hook ||= Integrations::Hook.find_by(reference_id: params[:event][:channel]) end - def conversation - @conversation ||= Conversation.where(identifier: params[:event][:thread_ts]).first - end - - def sender - user_email = slack_client.users_info(user: params[:event][:user])[:user][:profile][:email] - conversation.account.users.find_by(email: user_email) - end - - def private_note? - params[:event][:text].strip.downcase.starts_with?('note:', 'private:') - end - def slack_client @slack_client ||= Slack::Web::Client.new(token: @integration_hook.access_token) end diff --git a/lib/integrations/slack/link_unfurl_formatter.rb b/lib/integrations/slack/link_unfurl_formatter.rb new file mode 100644 index 000000000..5f045837b --- /dev/null +++ b/lib/integrations/slack/link_unfurl_formatter.rb @@ -0,0 +1,59 @@ +class Integrations::Slack::LinkUnfurlFormatter + pattr_initialize [:url!, :user_info!, :inbox_name!, :inbox_type!] + + def perform + return {} if url.blank? + + { + url => { + 'blocks' => preivew_blocks(user_info) + + open_conversation_button(url) + } + } + end + + private + + def preivew_blocks(user_info) + [ + { + 'type' => 'section', + 'fields' => [ + preview_field(I18n.t('slack_unfurl.fields.name'), user_info[:user_name]), + preview_field(I18n.t('slack_unfurl.fields.email'), user_info[:email]), + preview_field(I18n.t('slack_unfurl.fields.phone_number'), user_info[:phone_number]), + preview_field(I18n.t('slack_unfurl.fields.company_name'), user_info[:company_name]), + preview_field(I18n.t('slack_unfurl.fields.inbox_name'), inbox_name), + preview_field(I18n.t('slack_unfurl.fields.inbox_type'), inbox_type) + ] + } + ] + end + + def preview_field(label, value) + { + 'type' => 'mrkdwn', + 'text' => "*#{label}:*\n#{value}" + } + end + + def open_conversation_button(url) + [ + { + 'type' => 'actions', + 'elements' => [ + { + 'type' => 'button', + 'text' => { + 'type' => 'plain_text', + 'text' => I18n.t('slack_unfurl.button'), + 'emoji' => true + }, + 'url' => url, + 'action_id' => 'button-action' + } + ] + } + ] + end +end diff --git a/lib/integrations/slack/send_on_slack_service.rb b/lib/integrations/slack/send_on_slack_service.rb index d4889bc4c..7892ea92e 100644 --- a/lib/integrations/slack/send_on_slack_service.rb +++ b/lib/integrations/slack/send_on_slack_service.rb @@ -14,6 +14,12 @@ class Integrations::Slack::SendOnSlackService < Base::SendOnChannelService perform_reply end + def link_unfurl(event) + slack_client.chat_unfurl( + event + ) + end + private def valid_channel_for_slack? diff --git a/lib/integrations/slack/slack_link_unfurl_service.rb b/lib/integrations/slack/slack_link_unfurl_service.rb new file mode 100644 index 000000000..0d1dfdf8e --- /dev/null +++ b/lib/integrations/slack/slack_link_unfurl_service.rb @@ -0,0 +1,84 @@ +class Integrations::Slack::SlackLinkUnfurlService + pattr_initialize [:params!, :integration_hook!] + + def perform + event_links = params.dig(:event, :links) + return unless event_links + + event_links.each do |link_info| + url = link_info[:url] + # Unfurl only if the account id is same as the integration hook account id + unfurl_link(url) if url && valid_account?(url) + end + end + + def unfurl_link(url) + conversation = conversation_from_url(url) + return unless conversation + + send_unfurls(url, conversation) + end + + private + + def contact_attributes(conversation) + contact = conversation.contact + { + user_name: contact.name.presence || '---', + email: contact.email.presence || '---', + phone_number: contact.phone_number.presence || '---', + company_name: contact.additional_attributes&.dig('company_name').presence || '---' + } + end + + def generate_unfurls(url, user_info, inbox) + Integrations::Slack::LinkUnfurlFormatter.new( + url: url, + user_info: user_info, + inbox_name: inbox.name, + inbox_type: inbox.channel.name + ).perform + end + + def send_unfurls(url, conversation) + user_info = contact_attributes(conversation) + unfurls = generate_unfurls(url, user_info, conversation.inbox) + unfurl_params = { + unfurl_id: params.dig(:event, :unfurl_id), + source: params.dig(:event, :source), + unfurls: JSON.generate(unfurls) + } + + slack_service = Integrations::Slack::SendOnSlackService.new( + message: nil, + hook: integration_hook + ) + slack_service.link_unfurl(unfurl_params) + end + + def conversation_from_url(url) + conversation_id = extract_conversation_id(url) + find_conversation_by_id(conversation_id) if conversation_id + end + + def find_conversation_by_id(conversation_id) + Conversation.find_by(display_id: conversation_id, account_id: integration_hook.account_id) + end + + def valid_account?(url) + account_id = extract_account_id(url) + account_id == integration_hook.account_id.to_s + end + + def extract_account_id(url) + account_id_regex = %r{/accounts/(\d+)} + match_data = url.match(account_id_regex) + match_data[1] if match_data + end + + def extract_conversation_id(url) + conversation_id_regex = %r{/conversations/(\d+)} + match_data = url.match(conversation_id_regex) + match_data[1] if match_data + end +end diff --git a/lib/integrations/slack/slack_message_helper.rb b/lib/integrations/slack/slack_message_helper.rb index c4e16cbaf..9522112a8 100644 --- a/lib/integrations/slack/slack_message_helper.rb +++ b/lib/integrations/slack/slack_message_helper.rb @@ -62,4 +62,17 @@ module Integrations::Slack::SlackMessageHelper :file end end + + def conversation + @conversation ||= Conversation.where(identifier: params[:event][:thread_ts]).first + end + + def sender + user_email = slack_client.users_info(user: params[:event][:user])[:user][:profile][:email] + conversation.account.users.find_by(email: user_email) + end + + def private_note? + params[:event][:text].strip.downcase.starts_with?('note:', 'private:') + end end diff --git a/spec/jobs/slack_unfurl_job_spec.rb b/spec/jobs/slack_unfurl_job_spec.rb new file mode 100644 index 000000000..e60f370ad --- /dev/null +++ b/spec/jobs/slack_unfurl_job_spec.rb @@ -0,0 +1,38 @@ +require 'rails_helper' + +RSpec.describe SlackUnfurlJob do + subject(:job) { described_class.perform_later(params: link_shared, integration_hook: hook) } + + let(:account) { create(:account) } + let(:hook) { create(:integrations_hook, account: account) } + let(:inbox) { create(:inbox, account: account) } + let!(:conversation) { create(:conversation, inbox: inbox) } + + 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 'enqueues the job' do + expect { job }.to have_enqueued_job(described_class) + .on_queue('low') + end + + it 'calls the SlackLinkUnfurlService' do + slack_link_unfurl_service = instance_double(Integrations::Slack::SlackLinkUnfurlService) + + expect(Integrations::Slack::SlackLinkUnfurlService).to receive(:new) + .with(params: link_shared, integration_hook: hook) + .and_return(slack_link_unfurl_service) + expect(slack_link_unfurl_service).to receive(:perform) + described_class.perform_now(link_shared, hook) + end +end diff --git a/spec/lib/integrations/slack/incoming_message_builder_spec.rb b/spec/lib/integrations/slack/incoming_message_builder_spec.rb index 00a67df95..f1c1de7a5 100644 --- a/spec/lib/integrations/slack/incoming_message_builder_spec.rb +++ b/spec/lib/integrations/slack/incoming_message_builder_spec.rb @@ -3,6 +3,7 @@ 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', @@ -30,6 +31,8 @@ describe Integrations::Slack::IncomingMessageBuilder do 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 } @@ -145,5 +148,24 @@ describe Integrations::Slack::IncomingMessageBuilder do expect(conversation.messages.count).to eql(messages_count) 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, hook) + builder.perform + end + end end end diff --git a/spec/lib/integrations/slack/link_unfurl_formatter_spec.rb b/spec/lib/integrations/slack/link_unfurl_formatter_spec.rb new file mode 100644 index 000000000..12adc31ca --- /dev/null +++ b/spec/lib/integrations/slack/link_unfurl_formatter_spec.rb @@ -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 diff --git a/spec/lib/integrations/slack/send_on_slack_service_spec.rb b/spec/lib/integrations/slack/send_on_slack_service_spec.rb index d1704dcd2..aa0f37bde 100644 --- a/spec/lib/integrations/slack/send_on_slack_service_spec.rb +++ b/spec/lib/integrations/slack/send_on_slack_service_spec.rb @@ -18,12 +18,14 @@ describe Integrations::Slack::SendOnSlackService do 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') @@ -135,6 +137,18 @@ describe Integrations::Slack::SendOnSlackService do 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, diff --git a/spec/lib/integrations/slack/slack_link_unfurl_service_spec.rb b/spec/lib/integrations/slack/slack_link_unfurl_service_spec.rb new file mode 100644 index 000000000..0a8681672 --- /dev/null +++ b/spec/lib/integrations/slack/slack_link_unfurl_service_spec.rb @@ -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 diff --git a/spec/support/slack_stubs.rb b/spec/support/slack_stubs.rb index e37676c92..f7ca7706f 100644 --- a/spec/support/slack_stubs.rb +++ b/spec/support/slack_stubs.rb @@ -107,4 +107,14 @@ module SlackStubs channel: 'G01354F6A6Q' } end + + def link_shared_event + { + type: 'link_shared', + user: 'ULYPAKE5S', + source: 'conversations_history', + unfurl_id: 'C7NQEAE5Q.1695111587.937099.7e240338c6d2053fb49f56808e7c1f619f6ef317c39ebc59fc4af1cc30dce49b', + channel: 'G01354F6A6Q' + } + end end