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

@@ -85,6 +85,10 @@ TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
TWITTER_ENVIRONMENT=
#slack
SLACK_CLIENT_ID=
SLACK_CLIENT_SECRET
### Change this env variable only if you are using a custom build mobile app
## Mobile app env variables
IOS_APP_ID=6C953F3RX2.com.chatwoot.app

View File

@@ -65,6 +65,8 @@ gem 'twilio-ruby', '~> 5.32.0'
gem 'twitty'
# facebook client
gem 'koala'
# slack client
gem 'slack-ruby-client'
# Random name generator
gem 'haikunator'
@@ -115,4 +117,5 @@ group :development, :test do
gem 'simplecov', '0.17.1', require: false
gem 'spring'
gem 'spring-watcher-listen'
gem 'webmock'
end

View File

@@ -143,6 +143,8 @@ GEM
descendants_tracker (~> 0.0.1)
concurrent-ruby (1.1.6)
connection_pool (2.2.2)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.6)
datetime_picker_rails (0.0.7)
momentjs-rails (>= 2.8.1)
@@ -191,6 +193,7 @@ GEM
ffi (1.12.2)
flag_shih_tzu (0.3.23)
foreman (0.87.1)
gli (2.19.0)
globalid (0.4.2)
activesupport (>= 4.2.0)
google-api-client (0.39.4)
@@ -225,6 +228,7 @@ GEM
activesupport (>= 5)
haikunator (1.1.0)
hana (1.3.6)
hashdiff (1.0.1)
hashie (4.1.0)
hkdf (0.3.0)
http-accept (1.7.0)
@@ -411,6 +415,7 @@ GEM
rubocop-rspec (1.39.0)
rubocop (>= 0.68.1)
ruby-progressbar (1.10.1)
safe_yaml (1.0.5)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
@@ -452,6 +457,13 @@ GEM
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
slack-ruby-client (0.14.6)
activesupport
faraday (>= 0.9)
faraday_middleware
gli
hashie
websocket-driver
spring (2.1.0)
spring-watcher-listen (2.0.1)
listen (>= 2.7, < 4.0)
@@ -507,6 +519,10 @@ GEM
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webmock (3.8.3)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webpacker (5.1.1)
activesupport (>= 5.2)
rack-proxy (>= 0.6.1)
@@ -583,6 +599,7 @@ DEPENDENCIES
shoulda-matchers
sidekiq
simplecov (= 0.17.1)
slack-ruby-client
spring
spring-watcher-listen
telegram-bot-ruby
@@ -594,6 +611,7 @@ DEPENDENCIES
uglifier
valid_email2
web-console
webmock
webpacker
webpush
wisper (= 2.0.0)

View File

@@ -0,0 +1,18 @@
class Api::V1::Accounts::Integrations::AppsController < Api::V1::Accounts::BaseController
before_action :fetch_apps, only: [:index]
before_action :fetch_app, only: [:show]
def index; end
def show; end
private
def fetch_apps
@apps = Integrations::App.all
end
def fetch_app
@app = Integrations::App.find(id: params[:id])
end
end

View File

@@ -0,0 +1,36 @@
class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::BaseController
before_action :fetch_hook, only: [:update, :destroy]
def create
builder = Integrations::Slack::HookBuilder.new(
account: current_account,
code: params[:code],
inbox_id: params[:inbox_id]
)
@hook = builder.perform
render json: @hook
end
def update
builder = Integrations::Slack::ChannelBuilder.new(
hook: @hook, channel: params[:channel]
)
builder.perform
render json: @hook
end
def destroy
@hook.destroy
head :ok
end
private
def fetch_hook
@hook = Integrations::Hook.find(params[:id])
end
end

View File

@@ -0,0 +1,7 @@
class Api::V1::Integrations::WebhooksController < ApplicationController
def create
builder = Integrations::Slack::IncomingMessageBuilder.new(params)
response = builder.perform
render json: response
end
end

View File

@@ -9,7 +9,7 @@ class AsyncDispatcher < BaseDispatcher
end
def listeners
listeners = [EventListener.instance, WebhookListener.instance]
listeners = [EventListener.instance, WebhookListener.instance, HookListener.instance]
listeners
end
end

11
app/jobs/hook_job.rb Normal file
View File

@@ -0,0 +1,11 @@
class HookJob < ApplicationJob
queue_as :integrations
def perform(hook, message)
return unless hook.slack?
Integrations::Slack::OutgoingMessageBuilder.perform(hook, message)
rescue StandardError => e
Raven.capture_exception(e)
end
end

View File

@@ -0,0 +1,10 @@
class HookListener < BaseListener
def message_created(event)
message = extract_message_and_account(event)[0]
return unless message.reportable?
message.account.hooks.each do |hook|
HookJob.perform_later(hook, message)
end
end
end

View File

@@ -47,6 +47,7 @@ class Account < ApplicationRecord
has_many :webhooks, dependent: :destroy
has_many :labels, dependent: :destroy
has_many :notification_settings, dependent: :destroy
has_many :hooks, dependent: :destroy, class_name: 'Integrations::Hook'
has_flags ACCOUNT_SETTINGS_FLAGS.merge(column: 'settings_flags').merge(DEFAULT_QUERY_SETTING)
enum locale: LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h

View File

@@ -5,6 +5,7 @@
# id :integer not null, primary key
# additional_attributes :jsonb
# agent_last_seen_at :datetime
# identifier :string
# locked :boolean default(FALSE)
# status :integer default("open"), not null
# user_last_seen_at :datetime

View File

@@ -42,6 +42,7 @@ class Inbox < ApplicationRecord
has_one :agent_bot_inbox, dependent: :destroy
has_one :agent_bot, through: :agent_bot_inbox
has_many :webhooks, dependent: :destroy
has_many :hooks, dependent: :destroy, class_name: 'Integrations::Hook'
after_destroy :delete_round_robin_agents

View File

@@ -0,0 +1,5 @@
module Integrations
def self.table_name_prefix
'integrations_'
end
end

View File

@@ -0,0 +1,51 @@
class Integrations::App
attr_accessor :params
def initialize(params)
@params = params
end
def id
params[:id]
end
def name
params[:name]
end
def description
params[:description]
end
def logo
params[:logo]
end
def fields
params[:fields]
end
def button
params[:button]
end
def enabled?(account)
account.hooks.where(app_id: id).exists?
end
class << self
def apps
Hashie::Mash.new(APPS_CONFIG)
end
def all
apps.values.each_with_object([]) do |app, result|
result << new(app)
end
end
def find(params)
all.detect { |app| app.id == params[:id] }
end
end
end

View File

@@ -0,0 +1,36 @@
# == Schema Information
#
# Table name: integrations_hooks
#
# id :bigint not null, primary key
# access_token :string
# hook_type :integer default("account")
# settings :text
# status :integer default("disabled")
# created_at :datetime not null
# updated_at :datetime not null
# account_id :integer
# app_id :string
# inbox_id :integer
# reference_id :string
#
class Integrations::Hook < ApplicationRecord
validates :account_id, presence: true
validates :app_id, presence: true
enum status: { disabled: 0, enabled: 1 }
belongs_to :account
belongs_to :inbox, optional: true
has_secure_token :access_token
enum hook_type: { account: 0, inbox: 1 }
def app
@app ||= Integrations::App.find(id: app_id)
end
def slack?
app_id == 'cw_slack'
end
end

View File

@@ -0,0 +1,7 @@
json.array! @apps do |app|
json.id app.id
json.name app.name
json.logo app.logo
json.enabled app.enabled?(@current_account)
json.button app.button
end

View File

@@ -0,0 +1,7 @@
json.id @app.id
json.name @app.name
json.logo @app.logo
json.description @app.description
json.fields @app.fields
json.enabled @app.enabled?(@current_account)
json.button @app.button

View File

@@ -0,0 +1 @@
APPS_CONFIG = YAML.load_file(File.join(Rails.root, 'config/integration', 'apps.yml'))

View File

@@ -0,0 +1,6 @@
slack:
id: cw_slack
name: Slack
logo: https://a.slack-edge.com/80588/marketing/img/media-kit/img-logos@2x.png
description: "'Be less busy' - Slack is the chat tool that brings all your communication together in one place. By integrating Slack with SupportBee, you can get notified in your Slack channels for important events in your support desk"
button: <a href="https://slack.com/oauth/v2/authorize?scope=incoming-webhook,commands,chat:write&client_id=706921004289.1094198503990"><img alt=""Add to Slack"" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" /></a>

View File

@@ -87,18 +87,26 @@ Rails.application.routes.draw do
end
resource :notification_settings, only: [:show, :update]
resources :webhooks, except: [:show]
resources :webhooks, except: [:show]
namespace :integrations do
resources :apps, only: [:index, :show]
resources :slack, only: [:create, :update, :destroy]
end
end
end
# end of account scoped api routes
# ----------------------------------
namespace :integrations do
resources :webhooks, only: [:create]
end
resource :profile, only: [:show, :update]
resource :notification_subscriptions, only: [:create]
resources :agent_bots, only: [:index]
namespace :widget do
resources :events, only: [:create]
resources :messages, only: [:index, :create, :update]

View File

@@ -13,6 +13,7 @@
:queues:
- [low, 1]
- [webhooks, 1]
- [integrations, 2]
- [bots, 1]
- [active_storage_analysis, 1]
- [action_mailbox_incineration, 1]

View File

@@ -0,0 +1,15 @@
class CreateIntegrationsHooks < ActiveRecord::Migration[6.0]
def change
create_table :integrations_hooks do |t|
t.integer :status, default: 0
t.integer :inbox_id
t.integer :account_id
t.string :app_id
t.text :settings
t.integer :hook_type, default: 0
t.string :reference_id
t.string :access_token
t.timestamps
end
end
end

View File

@@ -0,0 +1,5 @@
class AddReferenceIdToConversation < ActiveRecord::Migration[6.0]
def change
add_column :conversations, :reference_id, :string
end
end

View File

@@ -0,0 +1,5 @@
class RenameReferenceId < ActiveRecord::Migration[6.0]
def change
rename_column :conversations, :reference_id, :identifier
end
end

View File

@@ -10,10 +10,9 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_06_07_140737) do
ActiveRecord::Schema.define(version: 2020_06_10_143132) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
enable_extension "pgcrypto"
enable_extension "plpgsql"
@@ -206,6 +205,7 @@ ActiveRecord::Schema.define(version: 2020_06_07_140737) do
t.boolean "locked", default: false
t.jsonb "additional_attributes"
t.bigint "contact_inbox_id"
t.string "identifier"
t.uuid "uuid", default: -> { "gen_random_uuid()" }, null: false
t.index ["account_id", "display_id"], name: "index_conversations_on_account_id_and_display_id", unique: true
t.index ["account_id"], name: "index_conversations_on_account_id"
@@ -228,6 +228,17 @@ ActiveRecord::Schema.define(version: 2020_06_07_140737) do
t.index ["user_id"], name: "index_events_on_user_id"
end
create_table "hooks_inbox_apps", force: :cascade do |t|
t.integer "inbox_id"
t.integer "agent_id"
t.integer "account_id"
t.string "app_slug"
t.string "status"
t.text "settings"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "inbox_members", id: :serial, force: :cascade do |t|
t.integer "user_id", null: false
t.integer "inbox_id", null: false
@@ -257,6 +268,19 @@ ActiveRecord::Schema.define(version: 2020_06_07_140737) do
t.index ["name", "created_at"], name: "index_installation_configs_on_name_and_created_at", unique: true
end
create_table "integrations_hooks", force: :cascade do |t|
t.integer "status", default: 0
t.integer "inbox_id"
t.integer "account_id"
t.string "app_id"
t.text "settings"
t.integer "hook_type", default: 0
t.string "reference_id"
t.string "access_token"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "labels", force: :cascade do |t|
t.string "title"
t.text "description"
@@ -357,9 +381,11 @@ ActiveRecord::Schema.define(version: 2020_06_07_140737) do
t.index ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context"
t.index ["taggable_id", "taggable_type", "tagger_id", "context"], name: "taggings_idy"
t.index ["taggable_id"], name: "index_taggings_on_taggable_id"
t.index ["taggable_type", "taggable_id"], name: "index_taggings_on_taggable_type_and_taggable_id"
t.index ["taggable_type"], name: "index_taggings_on_taggable_type"
t.index ["tagger_id", "tagger_type"], name: "index_taggings_on_tagger_id_and_tagger_type"
t.index ["tagger_id"], name: "index_taggings_on_tagger_id"
t.index ["tagger_type", "tagger_id"], name: "index_taggings_on_tagger_type_and_tagger_id"
end
create_table "tags", id: :serial, force: :cascade do |t|

View File

@@ -0,0 +1,33 @@
class Integrations::Slack::ChannelBuilder
attr_reader :params, :channel
def initialize(params)
@params = params
end
def perform
create_channel
update_reference_id
end
private
def hook
@hook ||= params[:hook]
end
def slack_client
Slack.configure do |config|
config.token = hook.access_token
end
Slack::Web::Client.new
end
def create_channel
@channel = slack_client.conversations_create(name: params[:channel])
end
def update_reference_id
@hook.reference_id = channel['channel']['id']
end
end

View File

@@ -0,0 +1,42 @@
class Integrations::Slack::HookBuilder
attr_reader :params
def initialize(params)
@params = params
end
def perform
token = fetch_access_token
hook = account.hooks.new(
access_token: token,
status: 'enabled',
inbox_id: params[:inbox_id],
hook_type: hook_type,
app_id: 'cw_slack'
)
hook.save!
hook
end
private
def account
params[:account]
end
def hook_type
params[:inbox_id] ? 'inbox' : 'account'
end
def fetch_access_token
client = Slack::Web::Client.new
client.oauth_access(
client_id: ENV.fetch('SLACK_CLIENT_ID', 'TEST_CLIENT_ID'),
client_secret: ENV.fetch('SLACK_CLIENT_SECRET', 'TEST_CLIENT_SECRET'),
code: params[:code]
)['bot']['bot_access_token']
end
end

View File

@@ -0,0 +1,78 @@
class Integrations::Slack::IncomingMessageBuilder
attr_reader :params
SUPPORTED_EVENT_TYPES = %w[event_callback url_verification].freeze
SUPPORTED_EVENTS = %w[message].freeze
SUPPORTED_MESSAGE_TYPES = %w[rich_text].freeze
def initialize(params)
@params = params
end
def perform
return unless valid_event?
if hook_verification?
verify_hook
elsif create_message?
create_message
end
end
private
def valid_event?
supported_event_type? && supported_event?
end
def supported_event_type?
SUPPORTED_EVENT_TYPES.include?(params[:type])
end
def supported_event?
hook_verification? || SUPPORTED_EVENTS.include?(params[:event][:type])
end
def supported_message?
SUPPORTED_MESSAGE_TYPES.include?(message[:type])
end
def hook_verification?
params[:type] == 'url_verification'
end
def create_message?
supported_message? && integration_hook
end
def message
params[:event][:blocks].first
end
def verify_hook
{
challenge: params[:challenge]
}
end
def integration_hook
@integration_hook ||= Integrations::Hook.where(reference_id: params[:event][:channel])
end
def conversation
@conversation ||= Conversation.where(identifier: params[:event][:thread_ts]).first
end
def create_message
return unless conversation
conversation.messages.create(
message_type: 0,
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
content: message[:elements].first[:elements].first[:text]
)
{ status: 'success' }
end
end

View File

@@ -0,0 +1,50 @@
class Integrations::Slack::OutgoingMessageBuilder
attr_reader :hook, :message
def self.perform(hook, message)
new(hook, message).perform
end
def initialize(hook, message)
@hook = hook
@message = message
end
def perform
send_message
update_reference_id
end
private
def conversation
@conversation ||= message.conversation
end
def contact
@contact ||= conversation.contact
end
def send_message
@slack_message = slack_client.chat_postMessage(
channel: hook.reference_id,
text: message.content,
username: contact.try(:name),
thread_ts: conversation.identifier
)
end
def update_reference_id
return if conversation.identifier
conversation.identifier = @slack_message['ts']
conversation.save!
end
def slack_client
Slack.configure do |config|
config.token = hook.access_token
end
Slack::Web::Client.new
end
end

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