feat: Whatsapp embedded signup (#11612)

## Description

This PR introduces WhatsApp Embedded Signup functionality, enabling
users to connect their WhatsApp Business accounts through Meta's
streamlined OAuth flow without manual webhook configuration. This
significantly improves the user experience by automating the entire
setup process.

**Key Features:**

- Embedded signup flow using Facebook SDK and Meta's OAuth 2.0
- Automatic webhook registration and phone number configuration
- Enhanced provider selection UI with card-based design
- Real-time progress tracking during signup process
- Comprehensive error handling and user feedback


## Required Configuration

The following environment variables must be configured by administrators
before this feature can be used:
Super Admin Configuration (via
super_admin/app_config?config=whatsapp_embedded)

- `WHATSAPP_APP_ID`: The Facebook App ID for WhatsApp Business API
integration
- `WHATSAPP_CONFIGURATION_ID`: The Configuration ID for WhatsApp
Embedded Signup flow (obtained from Meta Developer Portal)
- `WHATSAPP_APP_SECRET`: The App Secret for WhatsApp Embedded Signup
flow (required for token exchange)
![Screenshot 2025-06-09 at 11 21
08 AM](https://github.com/user-attachments/assets/1615fb0d-27fc-4d9e-b193-9be7894ea93a)


## How Has This Been Tested?

#### Backend Tests (RSpec):

- Authentication validation for embedded signup endpoints
- Authorization code validation and error handling
- Missing business parameter validation
- Proper response format for configuration endpoint
- Unauthorized access prevention

#### Manual Test Cases:

- Complete embedded signup flow (happy path)
- Provider selection UI navigation
- Facebook authentication popup handling
- Error scenarios (cancelled auth, invalid business data, API failures)
- Configuration presence/absence behavior

## Related Screenshots:

![Screenshot 2025-06-09 at 7 48
18 PM](https://github.com/user-attachments/assets/34001425-df11-4d78-9424-334461e3178f)
![Screenshot 2025-06-09 at 7 48
22 PM](https://github.com/user-attachments/assets/c09f4964-3aba-4c39-9285-d1e8e37d0e33)
![Screenshot 2025-06-09 at 7 48
32 PM](https://github.com/user-attachments/assets/a34d5382-7a91-4e1c-906e-dc2d570c864a)
![Screenshot 2025-06-09 at 10 43
05 AM](https://github.com/user-attachments/assets/a15840d8-8223-4513-82e4-b08f23c95927)
![Screenshot 2025-06-09 at 10 42
56 AM](https://github.com/user-attachments/assets/8c345022-38b5-44c4-aba2-0cda81389c69)


Fixes
https://linear.app/chatwoot/issue/CW-2131/spec-for-whatsapp-cloud-channels-sign-in-with-facebook

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
Tanmay Deep Sharma
2025-07-15 11:37:06 +07:00
committed by GitHub
parent 4378506a35
commit 61d10044a0
58 changed files with 2384 additions and 63 deletions

View File

@@ -88,4 +88,4 @@ describe CsatSurveyService do
end
end
end
end
end

View File

@@ -171,4 +171,4 @@ RSpec.describe Linear::ActivityMessageService, type: :service do
end
end
end
end
end

View File

@@ -0,0 +1,119 @@
require 'rails_helper'
describe Whatsapp::ChannelCreationService do
let(:account) { create(:account) }
let(:waba_info) { { waba_id: 'test_waba_id', business_name: 'Test Business' } }
let(:phone_info) do
{
phone_number_id: 'test_phone_id',
phone_number: '+1234567890',
verified: true,
business_name: 'Test Business'
}
end
let(:access_token) { 'test_access_token' }
let(:service) { described_class.new(account, waba_info, phone_info, access_token) }
describe '#perform' do
before do
# Clean up any existing channels to avoid phone number conflicts
Channel::Whatsapp.destroy_all
# Stub the webhook setup service to prevent HTTP calls during tests
webhook_service = instance_double(Whatsapp::WebhookSetupService)
allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service)
allow(webhook_service).to receive(:perform)
# Stub the provider validation and sync_templates
allow(Channel::Whatsapp).to receive(:new).and_wrap_original do |method, *args|
channel = method.call(*args)
allow(channel).to receive(:validate_provider_config)
allow(channel).to receive(:sync_templates)
channel
end
end
context 'when channel does not exist' do
it 'creates a new channel' do
expect { service.perform }.to change(Channel::Whatsapp, :count).by(1)
end
it 'creates channel with correct attributes' do
channel = service.perform
expect(channel.phone_number).to eq('+1234567890')
expect(channel.provider).to eq('whatsapp_cloud')
expect(channel.provider_config['api_key']).to eq(access_token)
expect(channel.provider_config['phone_number_id']).to eq('test_phone_id')
expect(channel.provider_config['business_account_id']).to eq('test_waba_id')
expect(channel.provider_config['source']).to eq('embedded_signup')
end
it 'creates an inbox for the channel' do
channel = service.perform
inbox = channel.inbox
expect(inbox).not_to be_nil
expect(inbox.name).to eq('Test Business WhatsApp')
expect(inbox.account).to eq(account)
end
end
context 'when channel already exists' do
before do
create(:channel_whatsapp, account: account, phone_number: '+1234567890',
provider: 'whatsapp_cloud', sync_templates: false, validate_provider_config: false)
end
it 'raises an error' do
expect { service.perform }.to raise_error(/Channel already exists/)
end
end
context 'when required parameters are missing' do
it 'raises error when account is nil' do
service = described_class.new(nil, waba_info, phone_info, access_token)
expect { service.perform }.to raise_error(ArgumentError, 'Account is required')
end
it 'raises error when waba_info is nil' do
service = described_class.new(account, nil, phone_info, access_token)
expect { service.perform }.to raise_error(ArgumentError, 'WABA info is required')
end
it 'raises error when phone_info is nil' do
service = described_class.new(account, waba_info, nil, access_token)
expect { service.perform }.to raise_error(ArgumentError, 'Phone info is required')
end
it 'raises error when access_token is blank' do
service = described_class.new(account, waba_info, phone_info, '')
expect { service.perform }.to raise_error(ArgumentError, 'Access token is required')
end
end
context 'when business_name is in different places' do
context 'when business_name is only in phone_info' do
let(:waba_info) { { waba_id: 'test_waba_id' } }
it 'uses business_name from phone_info' do
channel = service.perform
expect(channel.inbox.name).to eq('Test Business WhatsApp')
end
end
context 'when business_name is only in waba_info' do
let(:phone_info) do
{
phone_number_id: 'test_phone_id',
phone_number: '+1234567890',
verified: true
}
end
it 'uses business_name from waba_info' do
channel = service.perform
expect(channel.inbox.name).to eq('Test Business WhatsApp')
end
end
end
end
end

View File

@@ -0,0 +1,127 @@
require 'rails_helper'
describe Whatsapp::EmbeddedSignupService do
let(:account) { create(:account) }
let(:code) { 'test_authorization_code' }
let(:business_id) { 'test_business_id' }
let(:waba_id) { 'test_waba_id' }
let(:phone_number_id) { 'test_phone_number_id' }
let(:service) do
described_class.new(
account: account,
code: code,
business_id: business_id,
waba_id: waba_id,
phone_number_id: phone_number_id
)
end
describe '#perform' do
let(:access_token) { 'test_access_token' }
let(:phone_info) do
{
phone_number_id: phone_number_id,
phone_number: '+1234567890',
verified: true,
business_name: 'Test Business'
}
end
let(:channel) { instance_double(Channel::Whatsapp) }
let(:token_exchange_service) { instance_double(Whatsapp::TokenExchangeService) }
let(:phone_info_service) { instance_double(Whatsapp::PhoneInfoService) }
let(:token_validation_service) { instance_double(Whatsapp::TokenValidationService) }
let(:channel_creation_service) { instance_double(Whatsapp::ChannelCreationService) }
before do
allow(GlobalConfig).to receive(:clear_cache)
allow(Whatsapp::TokenExchangeService).to receive(:new).with(code).and_return(token_exchange_service)
allow(token_exchange_service).to receive(:perform).and_return(access_token)
allow(Whatsapp::PhoneInfoService).to receive(:new)
.with(waba_id, phone_number_id, access_token).and_return(phone_info_service)
allow(phone_info_service).to receive(:perform).and_return(phone_info)
allow(Whatsapp::TokenValidationService).to receive(:new)
.with(access_token, waba_id).and_return(token_validation_service)
allow(token_validation_service).to receive(:perform)
allow(Whatsapp::ChannelCreationService).to receive(:new)
.with(account, { waba_id: waba_id, business_name: 'Test Business' }, phone_info, access_token)
.and_return(channel_creation_service)
allow(channel_creation_service).to receive(:perform).and_return(channel)
# Webhook setup is now handled in the channel after_create callback
# So we stub it at the model level
webhook_service = instance_double(Whatsapp::WebhookSetupService)
allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service)
allow(webhook_service).to receive(:perform)
end
it 'orchestrates all services in the correct order' do
expect(token_exchange_service).to receive(:perform).ordered
expect(phone_info_service).to receive(:perform).ordered
expect(token_validation_service).to receive(:perform).ordered
expect(channel_creation_service).to receive(:perform).ordered
result = service.perform
expect(result).to eq(channel)
end
context 'when required parameters are missing' do
it 'raises error when code is blank' do
service = described_class.new(
account: account,
code: '',
business_id: business_id,
waba_id: waba_id,
phone_number_id: phone_number_id
)
expect { service.perform }.to raise_error(ArgumentError, /Required parameters are missing: code/)
end
it 'raises error when business_id is blank' do
service = described_class.new(
account: account,
code: code,
business_id: '',
waba_id: waba_id,
phone_number_id: phone_number_id
)
expect { service.perform }.to raise_error(ArgumentError, /Required parameters are missing: business_id/)
end
it 'raises error when waba_id is blank' do
service = described_class.new(
account: account,
code: code,
business_id: business_id,
waba_id: '',
phone_number_id: phone_number_id
)
expect { service.perform }.to raise_error(ArgumentError, /Required parameters are missing: waba_id/)
end
it 'raises error when multiple parameters are blank' do
service = described_class.new(
account: account,
code: '',
business_id: '',
waba_id: waba_id,
phone_number_id: phone_number_id
)
expect { service.perform }.to raise_error(ArgumentError, /Required parameters are missing: code, business_id/)
end
end
context 'when any service fails' do
it 'logs and re-raises the error' do
allow(token_exchange_service).to receive(:perform).and_raise('Token error')
expect(Rails.logger).to receive(:error).with('[WHATSAPP] Embedded signup failed: Token error')
expect { service.perform }.to raise_error('Token error')
end
end
end
end

View File

@@ -0,0 +1,197 @@
require 'rails_helper'
describe Whatsapp::FacebookApiClient do
let(:access_token) { 'test_access_token' }
let(:api_client) { described_class.new(access_token) }
let(:api_version) { 'v22.0' }
let(:app_id) { 'test_app_id' }
let(:app_secret) { 'test_app_secret' }
before do
allow(GlobalConfigService).to receive(:load).with('WHATSAPP_API_VERSION', 'v22.0').and_return(api_version)
allow(GlobalConfigService).to receive(:load).with('WHATSAPP_APP_ID', '').and_return(app_id)
allow(GlobalConfigService).to receive(:load).with('WHATSAPP_APP_SECRET', '').and_return(app_secret)
end
describe '#exchange_code_for_token' do
let(:code) { 'test_code' }
context 'when successful' do
before do
stub_request(:get, "https://graph.facebook.com/#{api_version}/oauth/access_token")
.with(query: { client_id: app_id, client_secret: app_secret, code: code })
.to_return(
status: 200,
body: { access_token: 'new_token' }.to_json,
headers: { 'Content-Type' => 'application/json' }
)
end
it 'returns the response data' do
result = api_client.exchange_code_for_token(code)
expect(result['access_token']).to eq('new_token')
end
end
context 'when failed' do
before do
stub_request(:get, "https://graph.facebook.com/#{api_version}/oauth/access_token")
.with(query: { client_id: app_id, client_secret: app_secret, code: code })
.to_return(status: 400, body: { error: 'Invalid code' }.to_json)
end
it 'raises an error' do
expect { api_client.exchange_code_for_token(code) }.to raise_error(/Token exchange failed/)
end
end
end
describe '#fetch_phone_numbers' do
let(:waba_id) { 'test_waba_id' }
context 'when successful' do
before do
stub_request(:get, "https://graph.facebook.com/#{api_version}/#{waba_id}/phone_numbers")
.with(query: { access_token: access_token })
.to_return(
status: 200,
body: { data: [{ id: '123', display_phone_number: '1234567890' }] }.to_json,
headers: { 'Content-Type' => 'application/json' }
)
end
it 'returns the phone numbers data' do
result = api_client.fetch_phone_numbers(waba_id)
expect(result['data']).to be_an(Array)
expect(result['data'].first['id']).to eq('123')
end
end
context 'when failed' do
before do
stub_request(:get, "https://graph.facebook.com/#{api_version}/#{waba_id}/phone_numbers")
.with(query: { access_token: access_token })
.to_return(status: 403, body: { error: 'Access denied' }.to_json)
end
it 'raises an error' do
expect { api_client.fetch_phone_numbers(waba_id) }.to raise_error(/WABA phone numbers fetch failed/)
end
end
end
describe '#debug_token' do
let(:input_token) { 'test_input_token' }
let(:app_access_token) { "#{app_id}|#{app_secret}" }
context 'when successful' do
before do
stub_request(:get, "https://graph.facebook.com/#{api_version}/debug_token")
.with(query: { input_token: input_token, access_token: app_access_token })
.to_return(
status: 200,
body: { data: { app_id: app_id, is_valid: true } }.to_json,
headers: { 'Content-Type' => 'application/json' }
)
end
it 'returns the debug token data' do
result = api_client.debug_token(input_token)
expect(result['data']['is_valid']).to be(true)
end
end
context 'when failed' do
before do
stub_request(:get, "https://graph.facebook.com/#{api_version}/debug_token")
.with(query: { input_token: input_token, access_token: app_access_token })
.to_return(status: 400, body: { error: 'Invalid token' }.to_json)
end
it 'raises an error' do
expect { api_client.debug_token(input_token) }.to raise_error(/Token validation failed/)
end
end
end
describe '#register_phone_number' do
let(:phone_number_id) { 'test_phone_id' }
let(:pin) { '123456' }
context 'when successful' do
before do
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{phone_number_id}/register")
.with(
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' },
body: { messaging_product: 'whatsapp', pin: pin }.to_json
)
.to_return(
status: 200,
body: { success: true }.to_json,
headers: { 'Content-Type' => 'application/json' }
)
end
it 'returns success response' do
result = api_client.register_phone_number(phone_number_id, pin)
expect(result['success']).to be(true)
end
end
context 'when failed' do
before do
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{phone_number_id}/register")
.with(
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' },
body: { messaging_product: 'whatsapp', pin: pin }.to_json
)
.to_return(status: 400, body: { error: 'Registration failed' }.to_json)
end
it 'raises an error' do
expect { api_client.register_phone_number(phone_number_id, pin) }.to raise_error(/Phone registration failed/)
end
end
end
describe '#subscribe_waba_webhook' do
let(:waba_id) { 'test_waba_id' }
let(:callback_url) { 'https://example.com/webhook' }
let(:verify_token) { 'test_verify_token' }
context 'when successful' do
before do
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{waba_id}/subscribed_apps")
.with(
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' },
body: { override_callback_uri: callback_url, verify_token: verify_token }.to_json
)
.to_return(
status: 200,
body: { success: true }.to_json,
headers: { 'Content-Type' => 'application/json' }
)
end
it 'returns success response' do
result = api_client.subscribe_waba_webhook(waba_id, callback_url, verify_token)
expect(result['success']).to be(true)
end
end
context 'when failed' do
before do
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{waba_id}/subscribed_apps")
.with(
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' },
body: { override_callback_uri: callback_url, verify_token: verify_token }.to_json
)
.to_return(status: 400, body: { error: 'Webhook subscription failed' }.to_json)
end
it 'raises an error' do
expect { api_client.subscribe_waba_webhook(waba_id, callback_url, verify_token) }.to raise_error(/Webhook subscription failed/)
end
end
end
end

View File

@@ -0,0 +1,147 @@
require 'rails_helper'
describe Whatsapp::PhoneInfoService do
let(:waba_id) { 'test_waba_id' }
let(:phone_number_id) { 'test_phone_number_id' }
let(:access_token) { 'test_access_token' }
let(:service) { described_class.new(waba_id, phone_number_id, access_token) }
let(:api_client) { instance_double(Whatsapp::FacebookApiClient) }
before do
allow(Whatsapp::FacebookApiClient).to receive(:new).with(access_token).and_return(api_client)
end
describe '#perform' do
let(:phone_response) do
{
'data' => [
{
'id' => phone_number_id,
'display_phone_number' => '1234567890',
'verified_name' => 'Test Business',
'code_verification_status' => 'VERIFIED'
}
]
}
end
context 'when all parameters are valid' do
before do
allow(api_client).to receive(:fetch_phone_numbers).with(waba_id).and_return(phone_response)
end
it 'returns formatted phone info' do
result = service.perform
expect(result).to eq({
phone_number_id: phone_number_id,
phone_number: '+1234567890',
verified: true,
business_name: 'Test Business'
})
end
end
context 'when phone_number_id is not provided' do
let(:phone_number_id) { nil }
let(:phone_response) do
{
'data' => [
{
'id' => 'first_phone_id',
'display_phone_number' => '1234567890',
'verified_name' => 'Test Business',
'code_verification_status' => 'VERIFIED'
}
]
}
end
before do
allow(api_client).to receive(:fetch_phone_numbers).with(waba_id).and_return(phone_response)
end
it 'uses the first available phone number' do
result = service.perform
expect(result[:phone_number_id]).to eq('first_phone_id')
end
end
context 'when specific phone_number_id is not found' do
let(:phone_number_id) { 'different_id' }
let(:phone_response) do
{
'data' => [
{
'id' => 'available_phone_id',
'display_phone_number' => '9876543210',
'verified_name' => 'Different Business',
'code_verification_status' => 'VERIFIED'
}
]
}
end
before do
allow(api_client).to receive(:fetch_phone_numbers).with(waba_id).and_return(phone_response)
end
it 'uses the first available phone number as fallback' do
result = service.perform
expect(result[:phone_number_id]).to eq('available_phone_id')
expect(result[:phone_number]).to eq('+9876543210')
end
end
context 'when no phone numbers are available' do
let(:phone_response) { { 'data' => [] } }
before do
allow(api_client).to receive(:fetch_phone_numbers).with(waba_id).and_return(phone_response)
end
it 'raises an error' do
expect { service.perform }.to raise_error(/No phone numbers found for WABA/)
end
end
context 'when waba_id is blank' do
let(:waba_id) { '' }
it 'raises ArgumentError' do
expect { service.perform }.to raise_error(ArgumentError, 'WABA ID is required')
end
end
context 'when access_token is blank' do
let(:access_token) { '' }
it 'raises ArgumentError' do
expect { service.perform }.to raise_error(ArgumentError, 'Access token is required')
end
end
context 'when phone number has special characters' do
let(:phone_response) do
{
'data' => [
{
'id' => phone_number_id,
'display_phone_number' => '+1 (234) 567-8900',
'verified_name' => 'Test Business',
'code_verification_status' => 'VERIFIED'
}
]
}
end
before do
allow(api_client).to receive(:fetch_phone_numbers).with(waba_id).and_return(phone_response)
end
it 'sanitizes the phone number' do
result = service.perform
expect(result[:phone_number]).to eq('+12345678900')
end
end
end
end

View File

@@ -0,0 +1,45 @@
require 'rails_helper'
describe Whatsapp::TokenExchangeService do
let(:code) { 'test_authorization_code' }
let(:service) { described_class.new(code) }
let(:api_client) { instance_double(Whatsapp::FacebookApiClient) }
before do
allow(Whatsapp::FacebookApiClient).to receive(:new).and_return(api_client)
end
describe '#perform' do
context 'when code is valid' do
let(:token_response) { { 'access_token' => 'new_access_token' } }
before do
allow(api_client).to receive(:exchange_code_for_token).with(code).and_return(token_response)
end
it 'returns the access token' do
expect(service.perform).to eq('new_access_token')
end
end
context 'when code is blank' do
let(:service) { described_class.new('') }
it 'raises ArgumentError' do
expect { service.perform }.to raise_error(ArgumentError, 'Authorization code is required')
end
end
context 'when response has no access token' do
let(:token_response) { { 'error' => 'Invalid code' } }
before do
allow(api_client).to receive(:exchange_code_for_token).with(code).and_return(token_response)
end
it 'raises an error' do
expect { service.perform }.to raise_error(/No access token in response/)
end
end
end
end

View File

@@ -0,0 +1,99 @@
require 'rails_helper'
describe Whatsapp::TokenValidationService do
let(:access_token) { 'test_access_token' }
let(:waba_id) { 'test_waba_id' }
let(:service) { described_class.new(access_token, waba_id) }
let(:api_client) { instance_double(Whatsapp::FacebookApiClient) }
before do
allow(Whatsapp::FacebookApiClient).to receive(:new).with(access_token).and_return(api_client)
end
describe '#perform' do
context 'when token has access to WABA' do
let(:debug_response) do
{
'data' => {
'granular_scopes' => [
{
'scope' => 'whatsapp_business_management',
'target_ids' => [waba_id, 'another_waba_id']
}
]
}
}
end
before do
allow(api_client).to receive(:debug_token).with(access_token).and_return(debug_response)
end
it 'validates successfully' do
expect { service.perform }.not_to raise_error
end
end
context 'when token does not have access to WABA' do
let(:debug_response) do
{
'data' => {
'granular_scopes' => [
{
'scope' => 'whatsapp_business_management',
'target_ids' => ['different_waba_id']
}
]
}
}
end
before do
allow(api_client).to receive(:debug_token).with(access_token).and_return(debug_response)
end
it 'raises an error' do
expect { service.perform }.to raise_error(/Token does not have access to WABA/)
end
end
context 'when no WABA scope is found' do
let(:debug_response) do
{
'data' => {
'granular_scopes' => [
{
'scope' => 'some_other_scope',
'target_ids' => ['some_id']
}
]
}
}
end
before do
allow(api_client).to receive(:debug_token).with(access_token).and_return(debug_response)
end
it 'raises an error' do
expect { service.perform }.to raise_error('No WABA scope found in token')
end
end
context 'when access_token is blank' do
let(:access_token) { '' }
it 'raises ArgumentError' do
expect { service.perform }.to raise_error(ArgumentError, 'Access token is required')
end
end
context 'when waba_id is blank' do
let(:waba_id) { '' }
it 'raises ArgumentError' do
expect { service.perform }.to raise_error(ArgumentError, 'WABA ID is required')
end
end
end
end

View File

@@ -0,0 +1,119 @@
require 'rails_helper'
describe Whatsapp::WebhookSetupService do
let(:channel) do
create(:channel_whatsapp,
phone_number: '+1234567890',
provider_config: {
'phone_number_id' => 'test_phone_id',
'webhook_verify_token' => 'test_verify_token'
},
provider: 'whatsapp_cloud',
sync_templates: false,
validate_provider_config: false)
end
let(:waba_id) { 'test_waba_id' }
let(:access_token) { 'test_access_token' }
let(:service) { described_class.new(channel, waba_id, access_token) }
let(:api_client) { instance_double(Whatsapp::FacebookApiClient) }
before do
# Clean up any existing channels to avoid phone number conflicts
Channel::Whatsapp.destroy_all
allow(Whatsapp::FacebookApiClient).to receive(:new).and_return(api_client)
end
describe '#perform' do
context 'when all operations succeed' do
before do
allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456)
allow(api_client).to receive(:register_phone_number).with('123456789', 223_456)
allow(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, anything, 'test_verify_token')
.and_return({ 'success' => true })
allow(channel).to receive(:save!)
end
it 'registers the phone number' do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).to receive(:register_phone_number).with('123456789', 223_456)
service.perform
end
end
it 'sets up webhook subscription' do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token')
service.perform
end
end
end
context 'when phone registration fails' do
before do
allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456)
allow(api_client).to receive(:register_phone_number)
.and_raise('Registration failed')
allow(api_client).to receive(:subscribe_waba_webhook)
.and_return({ 'success' => true })
end
it 'continues with webhook setup' do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).to receive(:subscribe_waba_webhook)
expect { service.perform }.not_to raise_error
end
end
end
context 'when webhook setup fails' do
before do
allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456)
allow(api_client).to receive(:register_phone_number)
allow(api_client).to receive(:subscribe_waba_webhook)
.and_raise('Webhook failed')
end
it 'raises an error' do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect { service.perform }.to raise_error(/Webhook setup failed/)
end
end
end
context 'when required parameters are missing' do
it 'raises error when channel is nil' do
service = described_class.new(nil, waba_id, access_token)
expect { service.perform }.to raise_error(ArgumentError, 'Channel is required')
end
it 'raises error when waba_id is blank' do
service = described_class.new(channel, '', access_token)
expect { service.perform }.to raise_error(ArgumentError, 'WABA ID is required')
end
it 'raises error when access_token is blank' do
service = described_class.new(channel, waba_id, '')
expect { service.perform }.to raise_error(ArgumentError, 'Access token is required')
end
end
context 'when PIN already exists' do
before do
channel.provider_config['verification_pin'] = 123_456
allow(api_client).to receive(:register_phone_number)
allow(api_client).to receive(:subscribe_waba_webhook).and_return({ 'success' => true })
allow(channel).to receive(:save!)
end
it 'reuses existing PIN' do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).to receive(:register_phone_number).with('123456789', 123_456)
expect(SecureRandom).not_to receive(:random_number)
service.perform
end
end
end
end
end