# frozen_string_literal: true require 'rails_helper' RSpec.describe 'Twilio::VoiceController', type: :request do let(:account) { create(:account) } let(:channel) { create(:channel_twilio_sms, :with_voice, account: account, phone_number: '+15551230003') } let(:inbox) { channel.inbox } let(:digits) { channel.phone_number.delete_prefix('+') } before do allow(Twilio::VoiceWebhookSetupService).to receive(:new) .and_return(instance_double(Twilio::VoiceWebhookSetupService, perform: "AP#{SecureRandom.hex(16)}")) end describe 'POST /twilio/voice/call/:phone' do let(:call_sid) { 'CA_test_call_sid_123' } let(:from_number) { '+15550003333' } let(:to_number) { channel.phone_number } it 'invokes Voice::InboundCallBuilder for inbound calls and renders conference TwiML' do conversation = create(:conversation, account: account, inbox: inbox) contact = conversation.contact call = create( :call, account: account, inbox: inbox, conversation: conversation, contact: contact, provider_call_id: call_sid ) call.update!(conference_sid: call.default_conference_sid) expect(Voice::InboundCallBuilder).to receive(:perform!).with( inbox: inbox, call_sid: call_sid, caller: { source_ids: [from_number], contact_attributes: { name: from_number, phone_number: from_number } } ).and_return(call) post "/twilio/voice/call/#{digits}", params: { 'CallSid' => call_sid, 'From' => from_number, 'To' => to_number, 'Direction' => 'inbound' } expect(response).to have_http_status(:ok) expect(response.body).to include('') expect(response.body).to include('') expect(response.body).to include(call.conference_sid) end it 'looks up the Call when Twilio sends the outbound-api PSTN leg' do conversation = create(:conversation, account: account, inbox: inbox) call = create( :call, account: account, inbox: inbox, conversation: conversation, contact: conversation.contact, direction: :outgoing, provider_call_id: call_sid ) call.update!(conference_sid: call.default_conference_sid) post "/twilio/voice/call/#{digits}", params: { 'CallSid' => call_sid, 'From' => to_number, 'To' => from_number, 'Direction' => 'outbound-api' } expect(response).to have_http_status(:ok) expect(response.body).to include(call.conference_sid) expect(call.reload.parent_call_sid).to be_nil end it 'records the parent call SID when syncing outbound-dial legs' do parent_sid = 'CA_parent' child_sid = 'CA_child' conversation = create(:conversation, account: account, inbox: inbox) call = create( :call, account: account, inbox: inbox, conversation: conversation, contact: conversation.contact, direction: :outgoing, provider_call_id: parent_sid ) call.update!(conference_sid: call.default_conference_sid) post "/twilio/voice/call/#{digits}", params: { 'CallSid' => child_sid, 'ParentCallSid' => parent_sid, 'From' => to_number, 'To' => from_number, 'Direction' => 'outbound-dial' } expect(response).to have_http_status(:ok) expect(call.reload.parent_call_sid).to eq(parent_sid) end it 'raises not found when inbox is not present' do expect(Voice::InboundCallBuilder).not_to receive(:perform!) post '/twilio/voice/call/19998887777', params: { 'CallSid' => call_sid, 'From' => from_number, 'To' => to_number, 'Direction' => 'inbound' } expect(response).to have_http_status(:not_found) end it 'rejects the inbound contact leg without building a call when inbound calls are disabled' do channel.update!(provider_config: { 'inbound_calls_enabled' => false }) expect(Voice::InboundCallBuilder).not_to receive(:perform!) expect do post "/twilio/voice/call/#{digits}", params: { 'CallSid' => call_sid, 'From' => from_number, 'To' => to_number, 'Direction' => 'inbound' } end.not_to change(Call, :count) expect(response).to have_http_status(:ok) expect(response.body).to include(' call_sid, 'CallStatus' => 'completed') ) ).and_return(service_double) expect(service_double).to receive(:perform) post "/twilio/voice/status/#{digits}", params: { 'CallSid' => call_sid, 'CallStatus' => 'completed' } expect(response).to have_http_status(:no_content) end it 'raises not found when inbox is not present' do expect(Voice::StatusUpdateService).not_to receive(:new) post '/twilio/voice/status/18005550101', params: { 'CallSid' => call_sid, 'CallStatus' => 'busy' } expect(response).to have_http_status(:not_found) end end end