feat: Support Twilio Messaging Services (#4242)

This allows sending and receiving from multiple phone numbers using Twilio messaging services

Fixes: #4204
This commit is contained in:
Jordan Brough 2022-07-08 06:50:07 -06:00 committed by GitHub
parent fdf449dc87
commit 49d08a6773
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 379 additions and 105 deletions

View file

@ -78,7 +78,7 @@ gem 'wisper', '2.0.0'
# TODO: bump up gem to 2.0 # TODO: bump up gem to 2.0
gem 'facebook-messenger' gem 'facebook-messenger'
gem 'line-bot-api' gem 'line-bot-api'
gem 'twilio-ruby', '~> 5.32.0' gem 'twilio-ruby', '~> 5.66'
# twitty will handle subscription of twitter account events # twitty will handle subscription of twitter account events
# gem 'twitty', git: 'https://github.com/chatwoot/twitty' # gem 'twitty', git: 'https://github.com/chatwoot/twitty'
gem 'twitty' gem 'twitty'

View file

@ -582,8 +582,8 @@ GEM
activesupport activesupport
i18n i18n
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
twilio-ruby (5.32.0) twilio-ruby (5.66.0)
faraday (~> 1.0.0) faraday (>= 0.9, < 2.0)
jwt (>= 1.5, <= 2.5) jwt (>= 1.5, <= 2.5)
nokogiri (>= 1.6, < 2.0) nokogiri (>= 1.6, < 2.0)
twitty (0.1.4) twitty (0.1.4)
@ -730,7 +730,7 @@ DEPENDENCIES
squasher squasher
telephone_number telephone_number
time_diff time_diff
twilio-ruby (~> 5.32.0) twilio-ruby (~> 5.66)
twitty twitty
tzinfo-data tzinfo-data
uglifier uglifier

View file

@ -38,6 +38,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
@twilio_channel = Current.account.twilio_sms.create!( @twilio_channel = Current.account.twilio_sms.create!(
account_sid: permitted_params[:account_sid], account_sid: permitted_params[:account_sid],
auth_token: permitted_params[:auth_token], auth_token: permitted_params[:auth_token],
messaging_service_sid: permitted_params[:messaging_service_sid],
phone_number: phone_number, phone_number: phone_number,
medium: medium medium: medium
) )
@ -49,7 +50,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
def permitted_params def permitted_params
params.require(:twilio_channel).permit( params.require(:twilio_channel).permit(
:account_id, :phone_number, :account_sid, :auth_token, :name, :medium :account_id, :messaging_service_sid, :phone_number, :account_sid, :auth_token, :name, :medium
) )
end end
end end

View file

@ -7,7 +7,7 @@ class Twilio::CallbackController < ApplicationController
private private
def permitted_params def permitted_params # rubocop:disable Metrics/MethodLength
params.permit( params.permit(
:ApiVersion, :ApiVersion,
:SmsSid, :SmsSid,
@ -25,7 +25,8 @@ class Twilio::CallbackController < ApplicationController
:ToCountry, :ToCountry,
:FromState, :FromState,
:MediaUrl0, :MediaUrl0,
:MediaContentType0 :MediaContentType0,
:MessagingServiceSid
) )
end end
end end

View file

@ -12,7 +12,7 @@ export const getInboxClassByType = (type, phoneNumber) => {
return 'brand-twitter'; return 'brand-twitter';
case INBOX_TYPES.TWILIO: case INBOX_TYPES.TWILIO:
return phoneNumber.startsWith('whatsapp') return phoneNumber?.startsWith('whatsapp')
? 'brand-whatsapp' ? 'brand-whatsapp'
: 'brand-sms'; : 'brand-sms';

View file

@ -111,6 +111,12 @@
"PLACEHOLDER": "Please enter your Twilio Account SID", "PLACEHOLDER": "Please enter your Twilio Account SID",
"ERROR": "This field is required" "ERROR": "This field is required"
}, },
"MESSAGING_SERVICE_SID": {
"LABEL": "Messaging Service SID",
"PLACEHOLDER": "Please enter your Twilio Messaging Service SID",
"ERROR": "This field is required",
"USE_MESSAGING_SERVICE": "Use a Twilio Messaging Service"
},
"CHANNEL_TYPE": { "CHANNEL_TYPE": {
"LABEL": "Channel Type", "LABEL": "Channel Type",
"ERROR": "Please select your Channel Type" "ERROR": "Please select your Channel Type"

View file

@ -438,7 +438,11 @@ export default {
return this.$store.getters['inboxes/getInbox'](this.currentInboxId); return this.$store.getters['inboxes/getInbox'](this.currentInboxId);
}, },
inboxName() { inboxName() {
if (this.isATwilioSMSChannel || this.isAWhatsappChannel) { if (this.isATwilioSMSChannel || this.isATwilioWhatsappChannel) {
return `${this.inbox.name} (${this.inbox.messaging_service_sid ||
this.inbox.phone_number})`;
}
if (this.isAWhatsappChannel) {
return `${this.inbox.name} (${this.inbox.phone_number})`; return `${this.inbox.name} (${this.inbox.phone_number})`;
} }
if (this.isAnEmailChannel) { if (this.isAnEmailChannel) {

View file

@ -17,6 +17,26 @@
</div> </div>
<div class="medium-8 columns"> <div class="medium-8 columns">
<label
v-if="useMessagingService"
:class="{ error: $v.messagingServiceSID.$error }"
>
{{ $t('INBOX_MGMT.ADD.TWILIO.MESSAGING_SERVICE_SID.LABEL') }}
<input
v-model.trim="messagingServiceSID"
type="text"
:placeholder="
$t('INBOX_MGMT.ADD.TWILIO.MESSAGING_SERVICE_SID.PLACEHOLDER')
"
@blur="$v.messagingServiceSID.$touch"
/>
<span v-if="$v.messagingServiceSID.$error" class="message">{{
$t('INBOX_MGMT.ADD.TWILIO.MESSAGING_SERVICE_SID.ERROR')
}}</span>
</label>
</div>
<div v-if="!useMessagingService" class="medium-8 columns">
<label :class="{ error: $v.phoneNumber.$error }"> <label :class="{ error: $v.phoneNumber.$error }">
{{ $t('INBOX_MGMT.ADD.TWILIO.PHONE_NUMBER.LABEL') }} {{ $t('INBOX_MGMT.ADD.TWILIO.PHONE_NUMBER.LABEL') }}
<input <input
@ -31,6 +51,22 @@
</label> </label>
</div> </div>
<div class="medium-8 columns messagingServiceHelptext">
<label for="useMessagingService">
<input
id="useMessagingService"
v-model="useMessagingService"
type="checkbox"
class="checkbox"
/>
{{
$t(
'INBOX_MGMT.ADD.TWILIO.MESSAGING_SERVICE_SID.USE_MESSAGING_SERVICE'
)
}}
</label>
</div>
<div class="medium-8 columns"> <div class="medium-8 columns">
<label :class="{ error: $v.accountSID.$error }"> <label :class="{ error: $v.accountSID.$error }">
{{ $t('INBOX_MGMT.ADD.TWILIO.ACCOUNT_SID.LABEL') }} {{ $t('INBOX_MGMT.ADD.TWILIO.ACCOUNT_SID.LABEL') }}
@ -91,6 +127,8 @@ export default {
authToken: '', authToken: '',
medium: this.type, medium: this.type,
channelName: '', channelName: '',
messagingServiceSID: '',
useMessagingService: false,
phoneNumber: '', phoneNumber: '',
}; };
}, },
@ -99,12 +137,25 @@ export default {
uiFlags: 'inboxes/getUIFlags', uiFlags: 'inboxes/getUIFlags',
}), }),
}, },
validations: { validations() {
if (this.phoneNumber) {
return {
channelName: { required }, channelName: { required },
phoneNumber: { required, shouldStartWithPlusSign }, messagingServiceSID: {},
phoneNumber: { shouldStartWithPlusSign },
authToken: { required }, authToken: { required },
accountSID: { required }, accountSID: { required },
medium: { required }, medium: { required },
};
}
return {
channelName: { required },
messagingServiceSID: { required },
phoneNumber: {},
authToken: { required },
accountSID: { required },
medium: { required },
};
}, },
methods: { methods: {
async createChannel() { async createChannel() {
@ -122,7 +173,10 @@ export default {
medium: this.medium, medium: this.medium,
account_sid: this.accountSID, account_sid: this.accountSID,
auth_token: this.authToken, auth_token: this.authToken,
phone_number: `+${this.phoneNumber.replace(/\D/g, '')}`, messaging_service_sid: this.messagingServiceSID,
phone_number: this.messagingServiceSID
? null
: `+${this.phoneNumber.replace(/\D/g, '')}`,
}, },
} }
); );
@ -141,3 +195,13 @@ export default {
}, },
}; };
</script> </script>
<style lang="scss" scoped>
.messagingServiceHelptext {
margin-top: -10px;
margin-bottom: 15px;
.checkbox {
margin: 0px 4px;
}
}
</style>

View file

@ -6,14 +6,16 @@
# account_sid :string not null # account_sid :string not null
# auth_token :string not null # auth_token :string not null
# medium :integer default("sms") # medium :integer default("sms")
# phone_number :string not null # messaging_service_sid :string
# phone_number :string
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# account_id :integer not null # account_id :integer not null
# #
# Indexes # Indexes
# #
# index_channel_twilio_sms_on_account_sid_and_phone_number (account_sid,phone_number) UNIQUE # index_channel_twilio_sms_on_account_id_and_phone_number (account_id,phone_number) UNIQUE
# index_channel_twilio_sms_on_messaging_service_sid (messaging_service_sid) UNIQUE
# index_channel_twilio_sms_on_phone_number (phone_number) UNIQUE # index_channel_twilio_sms_on_phone_number (phone_number) UNIQUE
# #
@ -24,8 +26,10 @@ class Channel::TwilioSms < ApplicationRecord
validates :account_sid, presence: true validates :account_sid, presence: true
validates :auth_token, presence: true validates :auth_token, presence: true
# NOTE: allowing nil for future when we suppor twilio messaging services
# https://github.com/chatwoot/chatwoot/pull/4242 # Must have _one_ of messaging_service_sid _or_ phone_number, and messaging_service_sid is preferred
validates :messaging_service_sid, uniqueness: true, presence: true, unless: :phone_number?
validates :phone_number, absence: true, if: :messaging_service_sid?
validates :phone_number, uniqueness: true, allow_nil: true validates :phone_number, uniqueness: true, allow_nil: true
enum medium: { sms: 0, whatsapp: 1 } enum medium: { sms: 0, whatsapp: 1 }
@ -37,4 +41,24 @@ class Channel::TwilioSms < ApplicationRecord
def messaging_window_enabled? def messaging_window_enabled?
medium == 'whatsapp' medium == 'whatsapp'
end end
def send_message(to:, body:, media_url: nil)
params = send_message_from.merge(to: to, body: body)
params[:media_url] = media_url if media_url.present?
client.messages.create(**params)
end
private
def client
::Twilio::REST::Client.new(account_sid, auth_token)
end
def send_message_from
if messaging_service_sid?
{ messaging_service_sid: messaging_service_sid }
else
{ from: phone_number }
end
end
end end

View file

@ -20,10 +20,9 @@ class Twilio::IncomingMessageService
private private
def twilio_inbox def twilio_inbox
@twilio_inbox ||= ::Channel::TwilioSms.find_by!( @twilio_inbox ||=
account_sid: params[:AccountSid], ::Channel::TwilioSms.find_by(messaging_service_sid: params[:MessagingServiceSid]) ||
phone_number: params[:To] ::Channel::TwilioSms.find_by!(account_sid: params[:AccountSid], phone_number: params[:To])
)
end end
def inbox def inbox

View file

@ -22,15 +22,7 @@ class Twilio::OneoffSmsCampaignService
campaign.account.contacts.tagged_with(audience_labels, any: true).each do |contact| campaign.account.contacts.tagged_with(audience_labels, any: true).each do |contact|
next if contact.phone_number.blank? next if contact.phone_number.blank?
send_message(to: contact.phone_number, from: channel.phone_number, content: campaign.message) channel.send_message(to: contact.phone_number, body: campaign.message)
end end
end end
def send_message(to:, from:, content:)
client.messages.create(body: content, from: from, to: to)
end
def client
::Twilio::REST::Client.new(channel.account_sid, channel.auth_token)
end
end end

View file

@ -7,7 +7,7 @@ class Twilio::SendOnTwilioService < Base::SendOnChannelService
def perform_reply def perform_reply
begin begin
twilio_message = client.messages.create(**message_params) twilio_message = channel.send_message(**message_params)
rescue Twilio::REST::TwilioError => e rescue Twilio::REST::TwilioError => e
ChatwootExceptionTracker.new(e, user: message.sender, account: message.account).capture_exception ChatwootExceptionTracker.new(e, user: message.sender, account: message.account).capture_exception
end end
@ -15,13 +15,11 @@ class Twilio::SendOnTwilioService < Base::SendOnChannelService
end end
def message_params def message_params
params = { {
body: message.content, body: message.content,
from: channel.phone_number, to: contact_inbox.source_id,
to: contact_inbox.source_id media_url: attachments
} }
params[:media_url] = attachments if message.attachments.present?
params
end end
def attachments def attachments
@ -39,8 +37,4 @@ class Twilio::SendOnTwilioService < Base::SendOnChannelService
def outgoing_message? def outgoing_message?
message.outgoing? || message.template? message.outgoing? || message.template?
end end
def client
::Twilio::REST::Client.new(channel.account_sid, channel.auth_token)
end
end end

View file

@ -4,6 +4,28 @@ class Twilio::WebhookSetupService
pattr_initialize [:inbox!] pattr_initialize [:inbox!]
def perform def perform
if channel.messaging_service_sid?
update_messaging_service
else
update_phone_number
end
rescue Twilio::REST::TwilioError => e
Rails.logger.error "TWILIO_FAILURE: #{e.message}"
end
private
def update_messaging_service
twilio_client
.messaging.services(channel.messaging_service_sid)
.update(
inbound_method: 'POST',
inbound_request_url: twilio_callback_index_url,
use_inbound_webhook_on_number: false
)
end
def update_phone_number
if phone_numbers.empty? if phone_numbers.empty?
Rails.logger.warn "TWILIO_PHONE_NUMBER_NOT_FOUND: #{channel.phone_number}" Rails.logger.warn "TWILIO_PHONE_NUMBER_NOT_FOUND: #{channel.phone_number}"
else else
@ -11,12 +33,8 @@ class Twilio::WebhookSetupService
.incoming_phone_numbers(phonenumber_sid) .incoming_phone_numbers(phonenumber_sid)
.update(sms_method: 'POST', sms_url: twilio_callback_index_url) .update(sms_method: 'POST', sms_url: twilio_callback_index_url)
end end
rescue Twilio::REST::TwilioError => e
Rails.logger.error "TWILIO_FAILURE: #{e.message}"
end end
private
def phonenumber_sid def phonenumber_sid
phone_numbers.first.sid phone_numbers.first.sid
end end

View file

@ -45,6 +45,7 @@ if resource.facebook?
end end
## Twilio Attributes ## Twilio Attributes
json.messaging_service_sid resource.channel.try(:messaging_service_sid)
json.phone_number resource.channel.try(:phone_number) json.phone_number resource.channel.try(:phone_number)
json.medium resource.channel.try(:medium) if resource.twilio? json.medium resource.channel.try(:medium) if resource.twilio?

View file

@ -0,0 +1,7 @@
class AddMessagingServiceSidToChannelTwilioSms < ActiveRecord::Migration[6.1]
def change
change_column_null :channel_twilio_sms, :phone_number, true
add_column :channel_twilio_sms, :messaging_service_sid, :string
add_index :channel_twilio_sms, [:messaging_service_sid], unique: true
end
end

View file

@ -290,14 +290,16 @@ ActiveRecord::Schema.define(version: 2022_07_06_085458) do
end end
create_table "channel_twilio_sms", force: :cascade do |t| create_table "channel_twilio_sms", force: :cascade do |t|
t.string "phone_number", null: false t.string "phone_number"
t.string "auth_token", null: false t.string "auth_token", null: false
t.string "account_sid", null: false t.string "account_sid", null: false
t.integer "account_id", null: false t.integer "account_id", null: false
t.datetime "created_at", precision: 6, null: false t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false
t.integer "medium", default: 0 t.integer "medium", default: 0
t.index ["account_sid", "phone_number"], name: "index_channel_twilio_sms_on_account_sid_and_phone_number", unique: true t.string "messaging_service_sid"
t.index ["account_id", "phone_number"], name: "index_channel_twilio_sms_on_account_id_and_phone_number", unique: true
t.index ["messaging_service_sid"], name: "index_channel_twilio_sms_on_messaging_service_sid", unique: true
t.index ["phone_number"], name: "index_channel_twilio_sms_on_phone_number", unique: true t.index ["phone_number"], name: "index_channel_twilio_sms_on_phone_number", unique: true
end end

View file

@ -20,7 +20,7 @@ RSpec.describe '/api/v1/accounts/{account.id}/channels/twilio_channel', type: :r
twilio_channel: { twilio_channel: {
account_sid: 'sid', account_sid: 'sid',
auth_token: 'token', auth_token: 'token',
phone_number: '+1234567890', messaging_service_sid: 'MGec8130512b5dd462cfe03095ec1342ed',
name: 'SMS Channel', name: 'SMS Channel',
medium: 'sms' medium: 'sms'
} }
@ -36,6 +36,34 @@ RSpec.describe '/api/v1/accounts/{account.id}/channels/twilio_channel', type: :r
context 'when user is logged in' do context 'when user is logged in' do
context 'with user as administrator' do context 'with user as administrator' do
it 'creates inbox and returns inbox object' do
allow(twilio_client).to receive(:messages).and_return(message_double)
allow(message_double).to receive(:list).and_return([])
post api_v1_account_channels_twilio_channel_path(account),
params: params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response['name']).to eq('SMS Channel')
expect(json_response['messaging_service_sid']).to eq('MGec8130512b5dd462cfe03095ec1342ed')
end
context 'with a phone number' do # rubocop:disable RSpec/NestedGroups
let(:params) do
{
twilio_channel: {
account_sid: 'sid',
auth_token: 'token',
phone_number: '+1234567890',
name: 'SMS Channel',
medium: 'sms'
}
}
end
it 'creates inbox and returns inbox object' do it 'creates inbox and returns inbox object' do
allow(twilio_client).to receive(:messages).and_return(message_double) allow(twilio_client).to receive(:messages).and_return(message_double)
allow(message_double).to receive(:list).and_return([]) allow(message_double).to receive(:list).and_return([])
@ -50,6 +78,7 @@ RSpec.describe '/api/v1/accounts/{account.id}/channels/twilio_channel', type: :r
expect(json_response['name']).to eq('SMS Channel') expect(json_response['name']).to eq('SMS Channel')
expect(json_response['phone_number']).to eq('+1234567890') expect(json_response['phone_number']).to eq('+1234567890')
end end
end
it 'return error if Twilio tokens are incorrect' do it 'return error if Twilio tokens are incorrect' do
allow(twilio_client).to receive(:messages).and_return(message_double) allow(twilio_client).to receive(:messages).and_return(message_double)

View file

@ -2,11 +2,16 @@ FactoryBot.define do
factory :channel_twilio_sms, class: 'Channel::TwilioSms' do factory :channel_twilio_sms, class: 'Channel::TwilioSms' do
auth_token { SecureRandom.uuid } auth_token { SecureRandom.uuid }
account_sid { SecureRandom.uuid } account_sid { SecureRandom.uuid }
sequence(:phone_number) { |n| "+123456789#{n}1" } messaging_service_sid { "MG#{Faker::Number.hexadecimal(digits: 32)}" }
medium { :sms } medium { :sms }
account account
after(:build) do |channel| after(:build) do |channel|
channel.inbox ||= create(:inbox, account: channel.account) channel.inbox ||= create(:inbox, account: channel.account)
end end
trait :with_phone_number do
sequence(:phone_number) { |n| "+123456789#{n}1" }
messaging_service_sid { nil }
end
end end
end end

View file

@ -3,6 +3,7 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Channel::TwilioSms do RSpec.describe Channel::TwilioSms do
describe '#has_24_hour_messaging_window?' do
context 'with medium whatsapp' do context 'with medium whatsapp' do
let!(:whatsapp_channel) { create(:channel_twilio_sms, medium: :whatsapp) } let!(:whatsapp_channel) { create(:channel_twilio_sms, medium: :whatsapp) }
@ -22,4 +23,54 @@ RSpec.describe Channel::TwilioSms do
expect(sms_channel.medium).to eq 'sms' expect(sms_channel.medium).to eq 'sms'
end end
end end
end
describe '#send_message' do
let(:channel) { create(:channel_twilio_sms) }
let(:twilio_client) { instance_double(Twilio::REST::Client) }
let(:twilio_messages) { double }
before do
allow(::Twilio::REST::Client).to receive(:new).and_return(twilio_client)
allow(twilio_client).to receive(:messages).and_return(twilio_messages)
end
it 'sends via twilio client' do
expect(twilio_messages).to receive(:create).with(
messaging_service_sid: channel.messaging_service_sid,
to: '+15555550111',
body: 'hello world'
).once
channel.send_message(to: '+15555550111', body: 'hello world')
end
context 'with a "from" phone number' do
let(:channel) { create(:channel_twilio_sms, :with_phone_number) }
it 'sends via twilio client' do
expect(twilio_messages).to receive(:create).with(
from: channel.phone_number,
to: '+15555550111',
body: 'hello world'
).once
channel.send_message(to: '+15555550111', body: 'hello world')
end
end
context 'with media urls' do
it 'supplies a media url' do
expect(twilio_messages).to receive(:create).with(
messaging_service_sid: channel.messaging_service_sid,
to: '+15555550111',
body: 'hello world',
media_url: ['https://example.com/1.jpg']
).once
channel.send_message(to: '+15555550111', body: 'hello world', media_url: ['https://example.com/1.jpg'])
end
end
end
end end

View file

@ -3,7 +3,7 @@ require 'rails_helper'
describe Twilio::IncomingMessageService do describe Twilio::IncomingMessageService do
let!(:account) { create(:account) } let!(:account) { create(:account) }
let!(:twilio_sms) do let!(:twilio_sms) do
create(:channel_twilio_sms, account: account, phone_number: '+1234567890', account_sid: 'ACxxx', create(:channel_twilio_sms, account: account, account_sid: 'ACxxx',
inbox: create(:inbox, account: account, greeting_enabled: false)) inbox: create(:inbox, account: account, greeting_enabled: false))
end end
let!(:contact) { create(:contact, account: account, phone_number: '+12345') } let!(:contact) { create(:contact, account: account, phone_number: '+12345') }
@ -16,7 +16,7 @@ describe Twilio::IncomingMessageService do
SmsSid: 'SMxx', SmsSid: 'SMxx',
From: '+12345', From: '+12345',
AccountSid: 'ACxxx', AccountSid: 'ACxxx',
To: '+1234567890', MessagingServiceSid: twilio_sms.messaging_service_sid,
Body: 'testing3' Body: 'testing3'
} }
@ -29,7 +29,39 @@ describe Twilio::IncomingMessageService do
SmsSid: 'SMxx', SmsSid: 'SMxx',
From: '+123456', From: '+123456',
AccountSid: 'ACxxx', AccountSid: 'ACxxx',
To: '+1234567890', MessagingServiceSid: twilio_sms.messaging_service_sid,
Body: 'new conversation'
}
described_class.new(params: params).perform
expect(Conversation.count).to eq(2)
end
context 'with a phone number' do
let!(:twilio_sms) do
create(:channel_twilio_sms, :with_phone_number, account: account, account_sid: 'ACxxx',
inbox: create(:inbox, account: account, greeting_enabled: false))
end
it 'creates a new message in existing conversation' do
params = {
SmsSid: 'SMxx',
From: '+12345',
AccountSid: 'ACxxx',
To: twilio_sms.phone_number,
Body: 'testing3'
}
described_class.new(params: params).perform
expect(conversation.reload.messages.last.content).to eq('testing3')
end
it 'creates a new conversation' do
params = {
SmsSid: 'SMxx',
From: '+123456',
AccountSid: 'ACxxx',
To: twilio_sms.phone_number,
Body: 'new conversation' Body: 'new conversation'
} }
@ -37,4 +69,5 @@ describe Twilio::IncomingMessageService do
expect(Conversation.count).to eq(2) expect(Conversation.count).to eq(2)
end end
end end
end
end end

View file

@ -38,12 +38,21 @@ describe Twilio::OneoffSmsCampaignService do
contact_with_label1.update_labels([label1.title]) contact_with_label1.update_labels([label1.title])
contact_with_label2.update_labels([label2.title]) contact_with_label2.update_labels([label2.title])
contact_with_both_labels.update_labels([label1.title, label2.title]) contact_with_both_labels.update_labels([label1.title, label2.title])
expect(twilio_messages).to receive(:create).with(body: campaign.message, expect(twilio_messages).to receive(:create).with(
from: twilio_sms.phone_number, to: contact_with_label1.phone_number).once body: campaign.message,
expect(twilio_messages).to receive(:create).with(body: campaign.message, messaging_service_sid: twilio_sms.messaging_service_sid,
from: twilio_sms.phone_number, to: contact_with_label2.phone_number).once to: contact_with_label1.phone_number
expect(twilio_messages).to receive(:create).with(body: campaign.message, ).once
from: twilio_sms.phone_number, to: contact_with_both_labels.phone_number).once expect(twilio_messages).to receive(:create).with(
body: campaign.message,
messaging_service_sid: twilio_sms.messaging_service_sid,
to: contact_with_label2.phone_number
).once
expect(twilio_messages).to receive(:create).with(
body: campaign.message,
messaging_service_sid: twilio_sms.messaging_service_sid,
to: contact_with_both_labels.phone_number
).once
sms_campaign_service.perform sms_campaign_service.perform
expect(campaign.reload.completed?).to eq true expect(campaign.reload.completed?).to eq true

View file

@ -3,18 +3,51 @@ require 'rails_helper'
describe Twilio::WebhookSetupService do describe Twilio::WebhookSetupService do
include Rails.application.routes.url_helpers include Rails.application.routes.url_helpers
let(:channel_twilio_sms) { create(:channel_twilio_sms) }
let(:twilio_client) { instance_double(::Twilio::REST::Client) } let(:twilio_client) { instance_double(::Twilio::REST::Client) }
before do
allow(::Twilio::REST::Client).to receive(:new).and_return(twilio_client)
end
describe '#perform' do
context 'with a messaging service sid' do
let(:channel_twilio_sms) { create(:channel_twilio_sms) }
let(:messaging) { instance_double(Twilio::REST::Messaging) }
let(:services) { instance_double(Twilio::REST::Messaging::V1::ServiceContext) }
before do
allow(twilio_client).to receive(:messaging).and_return(messaging)
allow(messaging).to receive(:services).with(channel_twilio_sms.messaging_service_sid).and_return(services)
allow(services).to receive(:update)
end
it 'updates the messaging service' do
described_class.new(inbox: channel_twilio_sms.inbox).perform
expect(services).to have_received(:update)
end
it 'does not raise if TwilioError is thrown' do
expect(services).to receive(:update).and_raise(Twilio::REST::TwilioError)
expect do
described_class.new(inbox: channel_twilio_sms.inbox).perform
end.not_to raise_error
end
end
context 'with a phone number' do
let(:channel_twilio_sms) { create(:channel_twilio_sms, :with_phone_number) }
let(:phone_double) { instance_double('phone_double') } let(:phone_double) { instance_double('phone_double') }
let(:phone_record_double) { instance_double('phone_record_double') } let(:phone_record_double) { instance_double('phone_record_double') }
before do before do
allow(::Twilio::REST::Client).to receive(:new).and_return(twilio_client)
allow(phone_double).to receive(:update) allow(phone_double).to receive(:update)
allow(phone_record_double).to receive(:sid).and_return('1234') allow(phone_record_double).to receive(:sid).and_return('1234')
end end
describe '#perform' do
it 'logs error if phone_number is not found' do it 'logs error if phone_number is not found' do
allow(twilio_client).to receive(:incoming_phone_numbers).and_return(phone_double) allow(twilio_client).to receive(:incoming_phone_numbers).and_return(phone_double)
allow(phone_double).to receive(:list).and_return([]) allow(phone_double).to receive(:list).and_return([])
@ -36,7 +69,7 @@ describe Twilio::WebhookSetupService do
) )
end end
it 'doesnot call update if TwilioError is thrown' do it 'does not call update if TwilioError is thrown' do
allow(twilio_client).to receive(:incoming_phone_numbers).and_return(phone_double) allow(twilio_client).to receive(:incoming_phone_numbers).and_return(phone_double)
allow(phone_double).to receive(:list).and_raise(Twilio::REST::TwilioError) allow(phone_double).to receive(:list).and_raise(Twilio::REST::TwilioError)
@ -45,4 +78,5 @@ describe Twilio::WebhookSetupService do
expect(phone_double).not_to have_received(:update) expect(phone_double).not_to have_received(:update)
end end
end end
end
end end