Feature: Slack integration (#783)
- Integrations architecture - Slack integration
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Integration Apps API', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
describe 'GET /api/v1/integrations/apps' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get api_v1_account_integrations_apps_url(account)
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
|
||||
it 'returns all the apps' do
|
||||
get api_v1_account_integrations_apps_url(account),
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
app = JSON.parse(response.body).first
|
||||
expect(app['id']).to eql('cw_slack')
|
||||
expect(app['name']).to eql('Slack')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/integrations/apps/:id' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get api_v1_account_integrations_app_url(account_id: account.id, id: 'cw_slack')
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
|
||||
it 'returns details of the app' do
|
||||
get api_v1_account_integrations_app_url(account_id: account.id, id: 'cw_slack'),
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
app = JSON.parse(response.body)
|
||||
expect(app['id']).to eql('cw_slack')
|
||||
expect(app['name']).to eql('Slack')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -7,6 +7,7 @@ FactoryBot.define do
|
||||
user_last_seen_at { Time.current }
|
||||
agent_last_seen_at { Time.current }
|
||||
locked { false }
|
||||
identifier { SecureRandom.hex }
|
||||
|
||||
after(:build) do |conversation|
|
||||
conversation.account ||= create(:account)
|
||||
|
||||
12
spec/factories/integrations/hooks.rb
Normal file
12
spec/factories/integrations/hooks.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
FactoryBot.define do
|
||||
factory :integrations_hook, class: 'Integrations::Hook' do
|
||||
status { 1 }
|
||||
inbox_id { 1 }
|
||||
account_id { 1 }
|
||||
app_id { 'cw_slack' }
|
||||
settings { 'MyText' }
|
||||
hook_type { 1 }
|
||||
access_token { SecureRandom.hex }
|
||||
reference_id { SecureRandom.hex }
|
||||
end
|
||||
end
|
||||
15
spec/jobs/hook_job_spec.rb
Normal file
15
spec/jobs/hook_job_spec.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe HookJob, type: :job do
|
||||
subject(:job) { described_class.perform_later(hook, message) }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let(:hook) { create(:integrations_hook, account: account) }
|
||||
let(:message) { create(:message) }
|
||||
|
||||
it 'queues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(hook, message)
|
||||
.on_queue('integrations')
|
||||
end
|
||||
end
|
||||
22
spec/lib/integrations/slack/hook_builder_spec.rb
Normal file
22
spec/lib/integrations/slack/hook_builder_spec.rb
Normal 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)
|
||||
builder.stub(:fetch_access_token) { 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
|
||||
45
spec/lib/integrations/slack/incoming_message_builder_spec.rb
Normal file
45
spec/lib/integrations/slack/incoming_message_builder_spec.rb
Normal file
@@ -0,0 +1,45 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Integrations::Slack::IncomingMessageBuilder do
|
||||
let(:account) { create(:account) }
|
||||
let(:message_params) { slack_message_stub }
|
||||
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]) }
|
||||
|
||||
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 'creates message' do
|
||||
messages_count = conversation.messages.count
|
||||
builder = described_class.new(message_params)
|
||||
builder.perform
|
||||
expect(conversation.messages.count).to eql(messages_count + 1)
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
30
spec/lib/integrations/slack/outgoing_message_builder_spec.rb
Normal file
30
spec/lib/integrations/slack/outgoing_message_builder_spec.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Integrations::Slack::OutgoingMessageBuilder do
|
||||
let(:account) { create(:account) }
|
||||
let!(:inbox) { create(:inbox, account: account) }
|
||||
let!(:contact) { create(:contact) }
|
||||
|
||||
let!(:hook) { create(:integrations_hook, account: account) }
|
||||
let!(:conversation) { create(:conversation, account: account, inbox: inbox, contact: contact) }
|
||||
let!(:message) { create(:message, account: account, inbox: inbox, conversation: conversation) }
|
||||
|
||||
describe '#perform' do
|
||||
it 'sent message to slack' do
|
||||
builder = described_class.new(hook, message)
|
||||
stub_request(:post, 'https://slack.com/api/chat.postMessage')
|
||||
.to_return(status: 200, body: '', headers: {})
|
||||
|
||||
# rubocop:disable RSpec/AnyInstance
|
||||
allow_any_instance_of(Slack::Web::Client).to receive(:chat_postMessage).with(
|
||||
channel: hook.reference_id,
|
||||
text: message.content,
|
||||
username: contact.name,
|
||||
thread_ts: conversation.identifier
|
||||
)
|
||||
# rubocop:enable RSpec/AnyInstance
|
||||
|
||||
builder.perform
|
||||
end
|
||||
end
|
||||
end
|
||||
32
spec/listeners/hook_listener_spec.rb
Normal file
32
spec/listeners/hook_listener_spec.rb
Normal file
@@ -0,0 +1,32 @@
|
||||
require 'rails_helper'
|
||||
describe HookListener do
|
||||
let(:listener) { described_class.instance }
|
||||
let!(:account) { create(:account) }
|
||||
let!(:user) { create(:user, account: account) }
|
||||
let!(:inbox) { create(:inbox, account: account) }
|
||||
let!(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user) }
|
||||
let!(:message) do
|
||||
create(:message, message_type: 'outgoing',
|
||||
account: account, inbox: inbox, conversation: conversation)
|
||||
end
|
||||
let!(:event) { Events::Base.new(event_name, Time.zone.now, message: message) }
|
||||
|
||||
describe '#message_created' do
|
||||
let(:event_name) { :'conversation.created' }
|
||||
|
||||
context 'when hook is not configured' do
|
||||
it 'does not trigger hook job' do
|
||||
expect(HookJob).to receive(:perform_later).exactly(0).times
|
||||
listener.message_created(event)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when hook is configured' do
|
||||
it 'triggers hook job' do
|
||||
hook = create(:integrations_hook, account: account)
|
||||
expect(HookJob).to receive(:perform_later).with(hook, message).once
|
||||
listener.message_created(event)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -29,6 +29,8 @@ RSpec.describe Inbox do
|
||||
it { is_expected.to have_many(:webhooks).dependent(:destroy) }
|
||||
|
||||
it { is_expected.to have_many(:events) }
|
||||
|
||||
it { is_expected.to have_many(:hooks) }
|
||||
end
|
||||
|
||||
describe '#add_member' do
|
||||
|
||||
12
spec/models/integrations/hook_spec.rb
Normal file
12
spec/models/integrations/hook_spec.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Integrations::Hook, type: :model do
|
||||
context 'with validations' do
|
||||
it { is_expected.to validate_presence_of(:app_id) }
|
||||
it { is_expected.to validate_presence_of(:account_id) }
|
||||
end
|
||||
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:account) }
|
||||
end
|
||||
end
|
||||
@@ -22,7 +22,9 @@ require 'sidekiq/testing'
|
||||
# directory. Alternatively, in the individual `*_spec.rb` files, manually
|
||||
# require only the support files necessary.
|
||||
#
|
||||
# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
|
||||
# rubocop:disable Rails/FilePath
|
||||
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
|
||||
# rubocop:enable Rails/FilePath
|
||||
|
||||
# Checks for pending migrations and applies them before tests are run.
|
||||
# If you are not using ActiveRecord, you can remove these lines.
|
||||
@@ -61,7 +63,7 @@ RSpec.configure do |config|
|
||||
config.filter_rails_from_backtrace!
|
||||
# arbitrary gems may also be filtered via:
|
||||
# config.filter_gems_from_backtrace("gem name")
|
||||
|
||||
config.include SlackStubs
|
||||
config.include Devise::Test::IntegrationHelpers, type: :request
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V1::Accounts::Integrations::Slacks', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:hook) { create(:integrations_hook, account: account) }
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/integrations/slack' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/slack", params: {}
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'creates hook' do
|
||||
hook_builder = Integrations::Slack::HookBuilder.new(account: account, code: SecureRandom.hex)
|
||||
hook_builder.stub(:fetch_access_token) { SecureRandom.hex }
|
||||
|
||||
expect(Integrations::Slack::HookBuilder).to receive(:new).and_return(hook_builder)
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/integrations/slack",
|
||||
params: { code: SecureRandom.hex },
|
||||
headers: agent.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
expect(json_response['app_id']).to eql('cw_slack')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /api/v1/accounts/{account.id}/integrations/slack/{id}' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
put "/api/v1/accounts/#{account.id}/integrations/slack/#{hook.id}", params: {}
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'updates hook' do
|
||||
channel_builder = Integrations::Slack::ChannelBuilder.new(hook: hook, channel: 'channel')
|
||||
channel_builder.stub(:perform)
|
||||
|
||||
expect(Integrations::Slack::ChannelBuilder).to receive(:new).and_return(channel_builder)
|
||||
|
||||
put "/api/v1/accounts/#{account.id}/integrations/slack/#{hook.id}",
|
||||
params: { channel: SecureRandom.hex },
|
||||
headers: agent.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
expect(json_response['app_id']).to eql('cw_slack')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/accounts/{account.id}/integrations/slack/{id}' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/slack/#{hook.id}", params: {}
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'deletes hook' do
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/slack/#{hook.id}",
|
||||
headers: agent.create_new_auth_token
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(Integrations::Hook.find_by(id: hook.id)).to be nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
15
spec/requests/api/v1/integrations/webhooks_request_spec.rb
Normal file
15
spec/requests/api/v1/integrations/webhooks_request_spec.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V1::Integrations::Webhooks', type: :request do
|
||||
describe 'POST /api/v1/integrations/webhooks' do
|
||||
it 'consumes webhook' do
|
||||
builder = Integrations::Slack::IncomingMessageBuilder.new({})
|
||||
builder.stub(:perform) { true }
|
||||
|
||||
expect(Integrations::Slack::IncomingMessageBuilder).to receive(:new).and_return(builder)
|
||||
|
||||
post '/api/v1/integrations/webhooks', params: {}
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,8 @@
|
||||
require 'simplecov'
|
||||
require 'webmock/rspec'
|
||||
|
||||
SimpleCov.start 'rails'
|
||||
WebMock.allow_net_connect!
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.expect_with :rspec do |expectations|
|
||||
|
||||
53
spec/support/slack_stubs.rb
Normal file
53
spec/support/slack_stubs.rb
Normal file
@@ -0,0 +1,53 @@
|
||||
module SlackStubs
|
||||
def slack_url_verification_stub
|
||||
{
|
||||
"token": 'Jhj5dZrVaK7ZwHHjRyZWjbDl',
|
||||
"challenge": '3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P',
|
||||
"type": 'url_verification'
|
||||
}
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def slack_message_stub
|
||||
{
|
||||
"token": '[FILTERED]',
|
||||
"team_id": 'TLST3048H',
|
||||
"api_app_id": 'A012S5UETV4',
|
||||
"event": {
|
||||
"client_msg_id": 'ffc6e64e-6f0c-4a3d-b594-faa6b44e48ab',
|
||||
"type": 'message',
|
||||
"text": 'this is test',
|
||||
"user": 'ULYPAKE5S',
|
||||
"ts": '1588623033.006000',
|
||||
"team": 'TLST3048H',
|
||||
"blocks": [
|
||||
{
|
||||
"type": 'rich_text',
|
||||
"block_id": 'jaIv3',
|
||||
"elements": [
|
||||
{
|
||||
"type": 'rich_text_section',
|
||||
"elements": [
|
||||
{
|
||||
"type": 'text',
|
||||
"text": 'this is test'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"thread_ts": '1588623023.005900',
|
||||
"channel": 'G01354F6A6Q',
|
||||
"event_ts": '1588623033.006000',
|
||||
"channel_type": 'group'
|
||||
},
|
||||
"type": 'event_callback',
|
||||
"event_id": 'Ev013QUX3WV6',
|
||||
"event_time": 1_588_623_033,
|
||||
"authed_users": '[FILTERED]',
|
||||
"webhook": {}
|
||||
}
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
end
|
||||
Reference in New Issue
Block a user