Feature: Slack integration (#783)

- Integrations architecture
- Slack integration
This commit is contained in:
Subin T P
2020-06-12 23:12:47 +05:30
committed by GitHub
parent 4f3b483066
commit ed1c871633
44 changed files with 867 additions and 7 deletions

View File

@@ -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

View File

@@ -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)

View 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

View 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

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)
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

View 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

View 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

View 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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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|

View 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