feat: Support for Whatsapp Cloud API (#4938)
Ability to configure Whatsapp Cloud API Inboxes fixes: #4712
This commit is contained in:
@@ -41,6 +41,25 @@ RSpec.describe 'Inboxes API', type: :request do
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(JSON.parse(response.body, symbolize_names: true)[:payload].size).to eq(1)
|
||||
end
|
||||
|
||||
context 'when provider_config' do
|
||||
let(:inbox) { create(:channel_whatsapp, account: account, sync_templates: false, validate_provider_config: false).inbox }
|
||||
|
||||
it 'returns provider config attributes for admin' do
|
||||
get "/api/v1/accounts/#{account.id}/inboxes",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
expect(JSON.parse(response.body)['payload'].last.key?('provider_config')).to eq(true)
|
||||
end
|
||||
|
||||
it 'will not return provider config for agent' do
|
||||
get "/api/v1/accounts/#{account.id}/inboxes",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(JSON.parse(response.body)['payload'].last.key?('provider_config')).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Webhooks::InstagramController', type: :request do
|
||||
describe 'GET /webhooks/verify' do
|
||||
it 'returns 401 when valid params are not present' do
|
||||
get '/webhooks/instagram/verify'
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
|
||||
it 'returns 401 when invalid params' do
|
||||
with_modified_env IG_VERIFY_TOKEN: '123456' do
|
||||
get '/webhooks/instagram/verify', params: { 'hub.challenge' => '123456', 'hub.mode' => 'subscribe', 'hub.verify_token' => 'invalid' }
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns challenge when valid params' do
|
||||
with_modified_env IG_VERIFY_TOKEN: '123456' do
|
||||
get '/webhooks/instagram/verify', params: { 'hub.challenge' => '123456', 'hub.mode' => 'subscribe', 'hub.verify_token' => '123456' }
|
||||
expect(response.body).to include '123456'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /webhooks/instagram' do
|
||||
let!(:dm_params) { build(:instagram_message_create_event).with_indifferent_access }
|
||||
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Webhooks::WhatsappController', type: :request do
|
||||
let(:channel) { create(:channel_whatsapp, provider: 'whatsapp_cloud', sync_templates: false, validate_provider_config: false) }
|
||||
|
||||
describe 'GET /webhooks/verify' do
|
||||
it 'returns 401 when valid params are not present' do
|
||||
get "/webhooks/whatsapp/#{channel.phone_number}"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns 401 when invalid params' do
|
||||
get "/webhooks/whatsapp/#{channel.phone_number}",
|
||||
params: { 'hub.challenge' => '123456', 'hub.mode' => 'subscribe', 'hub.verify_token' => 'invalid' }
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns challenge when valid params' do
|
||||
get "/webhooks/whatsapp/#{channel.phone_number}",
|
||||
params: { 'hub.challenge' => '123456', 'hub.mode' => 'subscribe', 'hub.verify_token' => channel.provider_config['webhook_verify_token'] }
|
||||
expect(response.body).to include '123456'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /webhooks/whatsapp/{:phone_number}' do
|
||||
it 'call the whatsapp events job with the params' do
|
||||
allow(Webhooks::WhatsappEventsJob).to receive(:perform_later)
|
||||
|
||||
@@ -36,11 +36,17 @@ FactoryBot.define do
|
||||
|
||||
transient do
|
||||
sync_templates { true }
|
||||
validate_provider_config { true }
|
||||
end
|
||||
|
||||
before(:create) do |channel_whatsapp, options|
|
||||
# since factory already has the required message templates, we just need to bypass it getting updated
|
||||
channel_whatsapp.define_singleton_method(:sync_templates) { return } unless options.sync_templates
|
||||
channel_whatsapp.define_singleton_method(:validate_provider_config) { return } unless options.validate_provider_config
|
||||
if channel_whatsapp.provider == 'whatsapp_cloud'
|
||||
channel_whatsapp.provider_config = { 'api_key' => 'test_key', 'phone_number_id' => '123456789', 'business_account_id' => '123456789',
|
||||
'webhook_verify_token': 'test_token' }
|
||||
end
|
||||
end
|
||||
|
||||
after(:create) do |channel_whatsapp|
|
||||
|
||||
91
spec/jobs/webhooks/whatsapp_events_job_spec.rb
Normal file
91
spec/jobs/webhooks/whatsapp_events_job_spec.rb
Normal file
@@ -0,0 +1,91 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Webhooks::WhatsappEventsJob, type: :job do
|
||||
subject(:job) { described_class }
|
||||
|
||||
let(:channel) { create(:channel_whatsapp, provider: 'whatsapp_cloud', sync_templates: false, validate_provider_config: false) }
|
||||
let(:params) { { phone_number: channel.phone_number } }
|
||||
let(:process_service) { double }
|
||||
|
||||
before do
|
||||
allow(process_service).to receive(:perform)
|
||||
end
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job.perform_later(params) }.to have_enqueued_job(described_class)
|
||||
.with(params)
|
||||
.on_queue('default')
|
||||
end
|
||||
|
||||
context 'when whatsapp_cloud provider' do
|
||||
it 'enques Whatsapp::IncomingMessageWhatsappCloudService' do
|
||||
allow(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new).and_return(process_service)
|
||||
expect(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new)
|
||||
job.perform_now(params)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when default provider' do
|
||||
it 'enques Whatsapp::IncomingMessageService' do
|
||||
stub_request(:post, 'https://waba.360dialog.io/v1/configs/webhook')
|
||||
channel.update(provider: 'default')
|
||||
allow(Whatsapp::IncomingMessageService).to receive(:new).and_return(process_service)
|
||||
expect(Whatsapp::IncomingMessageService).to receive(:new)
|
||||
job.perform_now(params)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when whatsapp business params' do
|
||||
it 'enques Whatsapp::IncomingMessageWhatsappCloudService based on the number in payload' do
|
||||
other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false,
|
||||
validate_provider_config: false)
|
||||
wb_params = {
|
||||
phone_number: channel.phone_number,
|
||||
object: 'whatsapp_business_account',
|
||||
entry: [
|
||||
{
|
||||
changes: [
|
||||
{
|
||||
value: {
|
||||
metadata: {
|
||||
phone_number_id: other_channel.provider_config['phone_number_id'],
|
||||
display_phone_number: other_channel.phone_number.delete('+')
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
allow(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new).and_return(process_service)
|
||||
expect(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new).with(inbox: other_channel.inbox, params: wb_params)
|
||||
job.perform_now(wb_params)
|
||||
end
|
||||
|
||||
it 'will not enque Whatsapp::IncomingMessageWhatsappCloudService when invalid phone number id' do
|
||||
other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false,
|
||||
validate_provider_config: false)
|
||||
wb_params = {
|
||||
phone_number: channel.phone_number,
|
||||
object: 'whatsapp_business_account',
|
||||
entry: [
|
||||
{
|
||||
changes: [
|
||||
{
|
||||
value: {
|
||||
metadata: {
|
||||
phone_number_id: 'random phone number id',
|
||||
display_phone_number: other_channel.phone_number.delete('+')
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
allow(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new).and_return(process_service)
|
||||
expect(Whatsapp::IncomingMessageWhatsappCloudService).not_to receive(:new).with(inbox: other_channel.inbox, params: wb_params)
|
||||
job.perform_now(wb_params)
|
||||
end
|
||||
end
|
||||
end
|
||||
23
spec/models/channel/whatsapp_spec.rb
Normal file
23
spec/models/channel/whatsapp_spec.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Channel::Whatsapp do
|
||||
describe 'validate_provider_config' do
|
||||
let(:channel) { build(:channel_whatsapp, provider: 'whatsapp_cloud', account: create(:account)) }
|
||||
|
||||
it 'validates false when provider config is wrong' do
|
||||
stub_request(:get, 'https://graph.facebook.com/v14.0//message_templates?access_token=test_key').to_return(status: 401)
|
||||
expect(channel.save).to eq(false)
|
||||
end
|
||||
|
||||
it 'validates true when provider config is right' do
|
||||
stub_request(:get, 'https://graph.facebook.com/v14.0//message_templates?access_token=test_key')
|
||||
.to_return(status: 200,
|
||||
body: { data: [{
|
||||
id: '123456789', name: 'test_template'
|
||||
}] }.to_json)
|
||||
expect(channel.save).to eq(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
## the specs are covered in send in spec/services/whatsapp/send_on_whatsapp_service_spec.rb
|
||||
116
spec/services/whatsapp/providers/whatsapp_cloud_service_spec.rb
Normal file
116
spec/services/whatsapp/providers/whatsapp_cloud_service_spec.rb
Normal file
@@ -0,0 +1,116 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Whatsapp::Providers::WhatsappCloudService do
|
||||
subject(:service) { described_class.new(whatsapp_channel: whatsapp_channel) }
|
||||
|
||||
let(:whatsapp_channel) { create(:channel_whatsapp, provider: 'whatsapp_cloud', validate_provider_config: false, sync_templates: false) }
|
||||
let(:message) { create(:message, message_type: :outgoing, content: 'test', inbox: whatsapp_channel.inbox) }
|
||||
let(:response_headers) { { 'Content-Type' => 'application/json' } }
|
||||
let(:whatsapp_response) { { messages: [{ id: 'message_id' }] } }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'https://graph.facebook.com/v14.0/123456789/message_templates?access_token=test_key')
|
||||
end
|
||||
|
||||
describe '#send_message' do
|
||||
context 'when called' do
|
||||
it 'calls message endpoints for normal messages' do
|
||||
stub_request(:post, 'https://graph.facebook.com/v13.0/123456789/messages')
|
||||
.with(
|
||||
body: {
|
||||
messaging_product: 'whatsapp',
|
||||
to: '+123456789',
|
||||
text: { body: message.content },
|
||||
type: 'text'
|
||||
}.to_json
|
||||
)
|
||||
.to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
|
||||
expect(service.send_message('+123456789', message)).to eq 'message_id'
|
||||
end
|
||||
|
||||
it 'calls message endpoints for attachment message messages' do
|
||||
attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
|
||||
attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png')
|
||||
|
||||
stub_request(:post, 'https://graph.facebook.com/v13.0/123456789/messages')
|
||||
.with(
|
||||
body: hash_including({
|
||||
messaging_product: 'whatsapp',
|
||||
to: '+123456789',
|
||||
type: 'image'
|
||||
})
|
||||
)
|
||||
.to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
|
||||
expect(service.send_message('+123456789', message)).to eq 'message_id'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#send_template' do
|
||||
let(:template_info) do
|
||||
{
|
||||
name: 'test_template',
|
||||
namespace: 'test_namespace',
|
||||
lang_code: 'en_US',
|
||||
parameters: [{ type: 'text', text: 'test' }]
|
||||
}
|
||||
end
|
||||
|
||||
let(:template_body) do
|
||||
{
|
||||
messaging_product: 'whatsapp',
|
||||
to: '+123456789',
|
||||
template: {
|
||||
name: template_info[:name],
|
||||
language: {
|
||||
policy: 'deterministic',
|
||||
code: template_info[:lang_code]
|
||||
},
|
||||
components: [
|
||||
{ type: 'body',
|
||||
parameters: template_info[:parameters] }
|
||||
]
|
||||
},
|
||||
type: 'template'
|
||||
}
|
||||
end
|
||||
|
||||
context 'when called' do
|
||||
it 'calls message endpoints with template params for template messages' do
|
||||
stub_request(:post, 'https://graph.facebook.com/v13.0/123456789/messages')
|
||||
.with(
|
||||
body: template_body.to_json
|
||||
)
|
||||
.to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
|
||||
|
||||
expect(service.send_template('+123456789', template_info)).to eq('message_id')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#sync_templates' do
|
||||
context 'when called' do
|
||||
it 'updated the message templates' do
|
||||
stub_request(:get, 'https://graph.facebook.com/v14.0/123456789/message_templates?access_token=test_key')
|
||||
.to_return(status: 200, headers: response_headers, body: { data: [{ id: '123456789', name: 'test_template' }] }.to_json)
|
||||
expect(subject.sync_templates).to eq(true)
|
||||
expect(whatsapp_channel.reload.message_templates).to eq([{ id: '123456789', name: 'test_template' }.stringify_keys])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#validate_provider_config' do
|
||||
context 'when called' do
|
||||
it 'returns true if valid' do
|
||||
stub_request(:get, 'https://graph.facebook.com/v14.0/123456789/message_templates?access_token=test_key')
|
||||
expect(subject.validate_provider_config?).to eq(true)
|
||||
expect(whatsapp_channel.errors.present?).to eq(false)
|
||||
end
|
||||
|
||||
it 'returns false if invalid' do
|
||||
stub_request(:get, 'https://graph.facebook.com/v14.0/123456789/message_templates?access_token=test_key').to_return(status: 401)
|
||||
expect(subject.validate_provider_config?).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user