Voice calling is now a capability on the existing TwilioSms rather than a separate Voice model. A single Twilio phone number handles both SMS and voice calls through one inbox. Fixes https://linear.app/chatwoot/issue/CW-6683/add-voice-calling-as-a-capability-on-twilio-sms-channel and https://linear.app/chatwoot/issue/PLA-120/add-the-support-for-sms **What changed** - Replaced Channel::Voice with voice_enabled flag on Channel::TwilioSms - Added voice_enabled, twiml_app_sid, api_key_secret columns to channel_twilio_sms table - Dropped channel_voice table (no production data) - All voice logic lives in Enterprise layer via prepend_mod_with('Channel::TwilioSms') - Added Voice settings tab on Twilio SMS inbox settings to enable/disable voice - Validates Twilio number voice capability before provisioning - Teardown service cleans up TwiML app and credentials when voice is disabled - Frontend voice detection uses isVoiceCallEnabled() / getVoiceCallProvider() helpers — extensible to future providers - Gated by channel_voice feature flag **How to test** 1. Enable feature flag: Account.find(<id>).enable_features('channel_voice') 2. Create voice inbox: Inboxes → Voice tile → enter Twilio credentials → verify incoming/outgoing calls and SMS work 3. Enable voice on existing SMS inbox: Inboxes → select Twilio SMS inbox → Voice tab → toggle on → provide API key credentials → verify calls work 4. Disable voice: Voice tab → toggle off → verify TwiML app is deleted, credentials cleared, SMS still works 5. Re-enable voice: Toggle on again → must provide api_key_secret again → new TwiML app provisioned --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
91 lines
3.5 KiB
Ruby
91 lines
3.5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
|
|
RSpec.describe Twilio::VoiceWebhookSetupService do
|
|
let(:account_sid) { 'ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' }
|
|
let(:auth_token) { 'auth_token_123' }
|
|
let(:api_key_sid) { 'SKaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' }
|
|
let(:api_key_secret) { 'api_key_secret_123' }
|
|
let(:phone_number) { '+15551230001' }
|
|
let(:frontend_url) { 'https://app.chatwoot.test' }
|
|
let(:account) { create(:account) }
|
|
|
|
let(:channel) do
|
|
build(:channel_twilio_sms, :with_voice,
|
|
account: account,
|
|
phone_number: phone_number,
|
|
account_sid: account_sid,
|
|
auth_token: auth_token,
|
|
api_key_sid: api_key_sid,
|
|
api_key_secret: api_key_secret)
|
|
end
|
|
|
|
let(:twilio_base_url) { "https://api.twilio.com/2010-04-01/Accounts/#{account_sid}" }
|
|
let(:incoming_numbers_url) { "#{twilio_base_url}/IncomingPhoneNumbers.json" }
|
|
let(:applications_url) { "#{twilio_base_url}/Applications.json" }
|
|
let(:phone_number_sid) { 'PN123' }
|
|
let(:phone_number_url) { "#{twilio_base_url}/IncomingPhoneNumbers/#{phone_number_sid}.json" }
|
|
|
|
before do
|
|
# Token validation using Account SID + Auth Token
|
|
stub_request(:get, /#{Regexp.escape(incoming_numbers_url)}.*/)
|
|
.with(basic_auth: [account_sid, auth_token])
|
|
.to_return(status: 200,
|
|
body: { incoming_phone_numbers: [], meta: { key: 'incoming_phone_numbers' } }.to_json,
|
|
headers: { 'Content-Type' => 'application/json' })
|
|
|
|
# Number lookup using API Key SID/Secret
|
|
stub_request(:get, /#{Regexp.escape(incoming_numbers_url)}.*/)
|
|
.with(basic_auth: [api_key_sid, api_key_secret])
|
|
.to_return(status: 200,
|
|
body: { incoming_phone_numbers: [{ sid: phone_number_sid }], meta: { key: 'incoming_phone_numbers' } }.to_json,
|
|
headers: { 'Content-Type' => 'application/json' })
|
|
|
|
# TwiML App create (voice only)
|
|
stub_request(:post, applications_url)
|
|
.with(basic_auth: [api_key_sid, api_key_secret])
|
|
.to_return(status: 201,
|
|
body: { sid: 'AP123' }.to_json,
|
|
headers: { 'Content-Type' => 'application/json' })
|
|
|
|
# Incoming Phone Number webhook update
|
|
stub_request(:post, phone_number_url)
|
|
.with(basic_auth: [api_key_sid, api_key_secret])
|
|
.to_return(status: 200,
|
|
body: { sid: phone_number_sid }.to_json,
|
|
headers: { 'Content-Type' => 'application/json' })
|
|
end
|
|
|
|
it 'creates a TwiML App and configures number webhooks with correct URLs' do
|
|
with_modified_env FRONTEND_URL: frontend_url do
|
|
service = described_class.new(channel: channel)
|
|
sid = service.perform
|
|
|
|
expect(sid).to eq('AP123')
|
|
|
|
expected_voice_url = channel.voice_call_webhook_url
|
|
expected_status_url = channel.voice_status_webhook_url
|
|
|
|
# Assert TwiML App creation body includes voice URL and POST method
|
|
expect(
|
|
a_request(:post, applications_url)
|
|
.with(body: hash_including('VoiceUrl' => expected_voice_url, 'VoiceMethod' => 'POST'))
|
|
).to have_been_made
|
|
|
|
# Assert number webhook update body includes both URLs and POST methods
|
|
expect(
|
|
a_request(:post, phone_number_url)
|
|
.with(
|
|
body: hash_including(
|
|
'VoiceUrl' => expected_voice_url,
|
|
'VoiceMethod' => 'POST',
|
|
'StatusCallback' => expected_status_url,
|
|
'StatusCallbackMethod' => 'POST'
|
|
)
|
|
)
|
|
).to have_been_made
|
|
end
|
|
end
|
|
end
|