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>
104 lines
4.0 KiB
Ruby
104 lines
4.0 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
|
|
RSpec.describe Channel::TwilioSms do
|
|
let(:account) { create(:account) }
|
|
let(:twiml_app_sid) { 'AP1234567890abcdef' }
|
|
|
|
before do
|
|
allow(Twilio::VoiceWebhookSetupService).to receive(:new).and_return(instance_double(Twilio::VoiceWebhookSetupService, perform: twiml_app_sid))
|
|
end
|
|
|
|
describe 'factory' do
|
|
it 'has a valid :with_voice factory' do
|
|
channel = create(:channel_twilio_sms, :with_voice, account: account)
|
|
expect(channel).to be_valid
|
|
expect(channel.voice_enabled?).to be true
|
|
end
|
|
end
|
|
|
|
describe 'validations' do
|
|
it 'requires a phone number when voice is enabled' do
|
|
channel = build(:channel_twilio_sms, :with_voice, account: account, phone_number: nil)
|
|
channel.valid?
|
|
expect(channel.errors[:base]).to include('Voice calling requires a phone number and cannot be used with messaging service SID')
|
|
end
|
|
end
|
|
|
|
describe '#voice_enabled?' do
|
|
it 'returns true when voice_enabled is set' do
|
|
channel = create(:channel_twilio_sms, :with_voice, account: account)
|
|
expect(channel.voice_enabled?).to be true
|
|
end
|
|
|
|
it 'returns false by default' do
|
|
channel = create(:channel_twilio_sms, account: account)
|
|
expect(channel.voice_enabled?).to be false
|
|
end
|
|
end
|
|
|
|
describe '#voice_call_webhook_url' do
|
|
it 'returns the webhook URL based on phone number' do
|
|
channel = create(:channel_twilio_sms, :with_voice)
|
|
digits = channel.phone_number.delete_prefix('+')
|
|
expect(channel.voice_call_webhook_url).to include(digits)
|
|
end
|
|
end
|
|
|
|
describe '#voice_status_webhook_url' do
|
|
it 'returns the status webhook URL based on phone number' do
|
|
channel = create(:channel_twilio_sms, :with_voice)
|
|
digits = channel.phone_number.delete_prefix('+')
|
|
expect(channel.voice_status_webhook_url).to include(digits)
|
|
end
|
|
end
|
|
|
|
describe 'provisioning on create' do
|
|
it 'stores twiml_app_sid from the webhook setup service' do
|
|
stub_request(:get, %r{api\.twilio\.com/2010-04-01/Accounts/.*/IncomingPhoneNumbers\.json})
|
|
.to_return(status: 200, body: { incoming_phone_numbers: [{ capabilities: { 'voice' => true } }] }.to_json,
|
|
headers: { 'Content-Type' => 'application/json' })
|
|
channel = create(:channel_twilio_sms, :with_voice, twiml_app_sid: nil)
|
|
expect(channel.twiml_app_sid).to eq(twiml_app_sid)
|
|
end
|
|
end
|
|
|
|
describe 'teardown on disable' do
|
|
let(:channel) { create(:channel_twilio_sms, :with_voice, account: account) }
|
|
let(:app_context) { instance_double(Twilio::REST::Api::V2010::AccountContext::ApplicationContext) }
|
|
let(:twilio_client) { instance_double(Twilio::REST::Client) }
|
|
let(:numbers_list) { instance_double(Twilio::REST::Api::V2010::AccountContext::IncomingPhoneNumberList) }
|
|
|
|
before do
|
|
allow(Twilio::REST::Client).to receive(:new).and_return(twilio_client)
|
|
allow(twilio_client).to receive(:applications).with(channel.twiml_app_sid).and_return(app_context)
|
|
allow(app_context).to receive(:delete)
|
|
allow(twilio_client).to receive(:incoming_phone_numbers).and_return(numbers_list)
|
|
allow(numbers_list).to receive(:list).with(phone_number: channel.phone_number).and_return([])
|
|
end
|
|
|
|
it 'deletes the TwiML app and clears twiml_app_sid' do
|
|
original_twiml_sid = channel.twiml_app_sid
|
|
channel.update!(voice_enabled: false)
|
|
|
|
expect(twilio_client).to have_received(:applications).with(original_twiml_sid)
|
|
expect(app_context).to have_received(:delete)
|
|
expect(channel.reload.twiml_app_sid).to be_nil
|
|
end
|
|
|
|
it 'preserves api_key_sid and api_key_secret' do
|
|
channel.update!(voice_enabled: false)
|
|
expect(channel.reload.api_key_sid).to be_present
|
|
expect(channel.reload.api_key_secret).to be_present
|
|
end
|
|
|
|
it 'does not fail if Twilio API errors' do
|
|
allow(app_context).to receive(:delete).and_raise(StandardError.new('Not found'))
|
|
|
|
expect { channel.update!(voice_enabled: false) }.not_to raise_error
|
|
expect(channel.reload.twiml_app_sid).to be_nil
|
|
end
|
|
end
|
|
end
|