Initial commit: Add logistics and order_detail message types
Some checks failed
Lock Threads / action (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Run Linux nightly installer / nightly (push) Has been cancelled
Some checks failed
Lock Threads / action (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Run Linux nightly installer / nightly (push) Has been cancelled
- Add Logistics component with progress tracking - Add OrderDetail component for order information - Support data-driven steps and actions - Add blue color scale to widget SCSS - Fix node overflow and progress bar rendering issues - Add English translations for dashboard components Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
44
spec/jobs/webhooks/facebook_delivery_job_spec.rb
Normal file
44
spec/jobs/webhooks/facebook_delivery_job_spec.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Webhooks::FacebookDeliveryJob do
|
||||
include ActiveJob::TestHelper
|
||||
|
||||
let(:message) { 'test_message' }
|
||||
let(:parsed_message) { instance_double(Integrations::Facebook::MessageParser) }
|
||||
let(:delivery_status) { instance_double(Integrations::Facebook::DeliveryStatus) }
|
||||
|
||||
before do
|
||||
allow(Integrations::Facebook::MessageParser).to receive(:new).with(message).and_return(parsed_message)
|
||||
allow(Integrations::Facebook::DeliveryStatus).to receive(:new).with(params: parsed_message).and_return(delivery_status)
|
||||
allow(delivery_status).to receive(:perform)
|
||||
end
|
||||
|
||||
after do
|
||||
clear_enqueued_jobs
|
||||
end
|
||||
|
||||
describe '#perform_later' do
|
||||
it 'enqueues the job' do
|
||||
expect do
|
||||
described_class.perform_later(message)
|
||||
end.to have_enqueued_job(described_class).with(message).on_queue('low')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
it 'calls the MessageParser with the correct argument' do
|
||||
expect(Integrations::Facebook::MessageParser).to receive(:new).with(message)
|
||||
described_class.perform_now(message)
|
||||
end
|
||||
|
||||
it 'calls the DeliveryStatus with the correct argument' do
|
||||
expect(Integrations::Facebook::DeliveryStatus).to receive(:new).with(params: parsed_message)
|
||||
described_class.perform_now(message)
|
||||
end
|
||||
|
||||
it 'executes perform on the DeliveryStatus instance' do
|
||||
expect(delivery_status).to receive(:perform)
|
||||
described_class.perform_now(message)
|
||||
end
|
||||
end
|
||||
end
|
||||
51
spec/jobs/webhooks/facebook_events_job_spec.rb
Normal file
51
spec/jobs/webhooks/facebook_events_job_spec.rb
Normal file
@@ -0,0 +1,51 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Webhooks::FacebookEventsJob do
|
||||
subject(:job) { described_class.perform_later(params) }
|
||||
|
||||
let(:params) { { test: 'test' } }
|
||||
let(:parsed_response) { instance_double(Integrations::Facebook::MessageParser) }
|
||||
let(:lock_key_format) { Redis::Alfred::FACEBOOK_MESSAGE_MUTEX }
|
||||
let(:lock_key) { format(lock_key_format, sender_id: 'sender_id', recipient_id: 'recipient_id') } # Use real format if different
|
||||
|
||||
before do
|
||||
allow(Integrations::Facebook::MessageParser).to receive(:new).and_return(parsed_response)
|
||||
allow(parsed_response).to receive(:sender_id).and_return('sender_id')
|
||||
allow(parsed_response).to receive(:recipient_id).and_return('recipient_id')
|
||||
end
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(params)
|
||||
.on_queue('default')
|
||||
end
|
||||
|
||||
describe 'job execution' do
|
||||
let(:message_creator) { instance_double(Integrations::Facebook::MessageCreator) }
|
||||
|
||||
before do
|
||||
allow(Integrations::Facebook::MessageParser).to receive(:new).and_return(parsed_response)
|
||||
allow(Integrations::Facebook::MessageCreator).to receive(:new).with(parsed_response).and_return(message_creator)
|
||||
allow(message_creator).to receive(:perform)
|
||||
end
|
||||
|
||||
# ensures that the response is built
|
||||
it 'invokes the message parser and creator' do
|
||||
expect(Integrations::Facebook::MessageParser).to receive(:new).with(params)
|
||||
expect(Integrations::Facebook::MessageCreator).to receive(:new).with(parsed_response)
|
||||
expect(message_creator).to receive(:perform)
|
||||
|
||||
described_class.perform_now(params)
|
||||
end
|
||||
|
||||
# this test ensures that the process message function is indeed called
|
||||
it 'attempts to acquire a lock and then processes the message' do
|
||||
job_instance = described_class.new
|
||||
allow(job_instance).to receive(:process_message).with(parsed_response)
|
||||
|
||||
job_instance.perform(params)
|
||||
|
||||
expect(job_instance).to have_received(:process_message).with(parsed_response)
|
||||
end
|
||||
end
|
||||
end
|
||||
387
spec/jobs/webhooks/instagram_events_job_spec.rb
Normal file
387
spec/jobs/webhooks/instagram_events_job_spec.rb
Normal file
@@ -0,0 +1,387 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Webhooks::InstagramEventsJob do
|
||||
subject(:instagram_webhook) { described_class }
|
||||
|
||||
before do
|
||||
stub_request(:post, /graph\.facebook\.com/)
|
||||
stub_request(:get, 'https://www.example.com/test.jpeg')
|
||||
.to_return(status: 200, body: '', headers: {})
|
||||
stub_request(:get, 'https://lookaside.fbsbx.com/ig_messaging_cdn/?asset_id=17949487764033669&signature=test')
|
||||
.to_return(status: 200, body: '', headers: {})
|
||||
stub_request(:get, 'https://lookaside.fbsbx.com/ig_messaging_cdn/?asset_id=18091626484740369&signature=test')
|
||||
.to_return(status: 200, body: '', headers: {})
|
||||
end
|
||||
|
||||
let!(:account) { create(:account) }
|
||||
|
||||
def return_object_for(sender_id)
|
||||
{ name: 'Jane',
|
||||
id: sender_id,
|
||||
account_id: instagram_messenger_inbox.account_id,
|
||||
profile_pic: 'https://chatwoot-assets.local/sample.png',
|
||||
username: 'some_user_name' }
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'when handling messaging events for Instagram via Facebook page' do
|
||||
let!(:instagram_messenger_channel) { create(:channel_instagram_fb_page, account: account, instagram_id: 'chatwoot-app-user-id-1') }
|
||||
let!(:instagram_messenger_inbox) { create(:inbox, channel: instagram_messenger_channel, account: account, greeting_enabled: false) }
|
||||
let(:fb_object) { double }
|
||||
|
||||
it 'creates incoming message in the instagram inbox' do
|
||||
dm_event = build(:instagram_message_create_event).with_indifferent_access
|
||||
sender_id = dm_event[:entry][0][:messaging][0][:sender][:id]
|
||||
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
return_object_for(sender_id).with_indifferent_access
|
||||
)
|
||||
instagram_webhook.perform_now(dm_event[:entry])
|
||||
|
||||
expect(instagram_messenger_inbox.contacts.count).to be 1
|
||||
expect(instagram_messenger_inbox.contacts.last.additional_attributes['social_instagram_user_name']).to eq 'some_user_name'
|
||||
expect(instagram_messenger_inbox.conversations.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.last.content_attributes['is_unsupported']).to be_nil
|
||||
end
|
||||
|
||||
it 'creates standby message in the instagram inbox' do
|
||||
standby_event = build(:instagram_message_standby_event).with_indifferent_access
|
||||
sender_id = standby_event[:entry][0][:standby][0][:sender][:id]
|
||||
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
return_object_for(sender_id).with_indifferent_access
|
||||
)
|
||||
instagram_webhook.perform_now(standby_event[:entry])
|
||||
|
||||
expect(instagram_messenger_inbox.contacts.count).to be 1
|
||||
expect(instagram_messenger_inbox.contacts.last.additional_attributes['social_instagram_user_name']).to eq 'some_user_name'
|
||||
expect(instagram_messenger_inbox.conversations.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.count).to be 1
|
||||
|
||||
message = instagram_messenger_inbox.messages.last
|
||||
expect(message.content).to eq('This is the first standby message from the customer, after 24 hours.')
|
||||
end
|
||||
|
||||
it 'handle instagram unsend message event' do
|
||||
unsend_event = build(:instagram_message_unsend_event).with_indifferent_access
|
||||
sender_id = unsend_event[:entry][0][:messaging][0][:sender][:id]
|
||||
|
||||
message = create(:message, inbox_id: instagram_messenger_inbox.id, source_id: 'message-id-to-delete')
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
{
|
||||
name: 'Jane',
|
||||
id: sender_id,
|
||||
account_id: instagram_messenger_inbox.account_id,
|
||||
profile_pic: 'https://chatwoot-assets.local/sample.png'
|
||||
}.with_indifferent_access
|
||||
)
|
||||
message.attachments.new(file_type: :image, external_url: 'https://www.example.com/test.jpeg')
|
||||
|
||||
expect(instagram_messenger_inbox.messages.count).to be 1
|
||||
|
||||
instagram_webhook.perform_now(unsend_event[:entry])
|
||||
|
||||
expect(instagram_messenger_inbox.messages.last.content).to eq 'This message was deleted'
|
||||
expect(instagram_messenger_inbox.messages.last.deleted).to be true
|
||||
expect(instagram_messenger_inbox.messages.last.attachments.count).to be 0
|
||||
expect(instagram_messenger_inbox.messages.last.reload.deleted).to be true
|
||||
end
|
||||
|
||||
it 'creates incoming message with attachments in the instagram inbox' do
|
||||
attachment_event = build(:instagram_message_attachment_event).with_indifferent_access
|
||||
sender_id = attachment_event[:entry][0][:messaging][0][:sender][:id]
|
||||
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
return_object_for(sender_id).with_indifferent_access
|
||||
)
|
||||
instagram_webhook.perform_now(attachment_event[:entry])
|
||||
|
||||
expect(instagram_messenger_inbox.contacts.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.last.attachments.count).to be 1
|
||||
end
|
||||
|
||||
it 'creates incoming message with attachments in the instagram inbox for story mention' do
|
||||
story_mention_event = build(:instagram_story_mention_event).with_indifferent_access
|
||||
sender_id = story_mention_event[:entry][0][:messaging][0][:sender][:id]
|
||||
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
return_object_for(sender_id).with_indifferent_access,
|
||||
{ story:
|
||||
{
|
||||
mention: {
|
||||
link: 'https://www.example.com/test.jpeg',
|
||||
id: '17920786367196703'
|
||||
}
|
||||
},
|
||||
from: {
|
||||
username: 'Sender-id-1', id: 'Sender-id-1'
|
||||
},
|
||||
id: 'instagram-message-id-1234' }.with_indifferent_access
|
||||
)
|
||||
|
||||
instagram_webhook.perform_now(story_mention_event[:entry])
|
||||
|
||||
expect(instagram_messenger_inbox.messages.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.last.attachments.count).to be 1
|
||||
|
||||
attachment = instagram_messenger_inbox.messages.last.attachments.last
|
||||
expect(attachment.push_event_data[:data_url]).to eq(attachment.external_url)
|
||||
end
|
||||
|
||||
it 'creates incoming message with ig_story attachment in the instagram inbox' do
|
||||
ig_story_event = build(:instagram_ig_story_event).with_indifferent_access
|
||||
sender_id = ig_story_event[:entry][0][:messaging][0][:sender][:id]
|
||||
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
return_object_for(sender_id).with_indifferent_access
|
||||
)
|
||||
|
||||
instagram_webhook.perform_now(ig_story_event[:entry])
|
||||
|
||||
expect(instagram_messenger_inbox.messages.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.last.attachments.count).to be 1
|
||||
|
||||
message = instagram_messenger_inbox.messages.last
|
||||
attachment = message.attachments.last
|
||||
|
||||
expect(attachment.file_type).to eq 'ig_story'
|
||||
expect(attachment.external_url).to include 'lookaside.fbsbx.com'
|
||||
expect(message.content).to eq 'Shared story'
|
||||
expect(message.content_attributes['image_type']).to eq 'ig_story'
|
||||
end
|
||||
|
||||
it 'creates incoming message with ig_post attachment in the instagram inbox' do
|
||||
ig_post_event = build(:instagram_ig_post_event).with_indifferent_access
|
||||
sender_id = ig_post_event[:entry][0][:messaging][0][:sender][:id]
|
||||
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
return_object_for(sender_id).with_indifferent_access
|
||||
)
|
||||
|
||||
instagram_webhook.perform_now(ig_post_event[:entry])
|
||||
|
||||
expect(instagram_messenger_inbox.messages.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.last.attachments.count).to be 1
|
||||
|
||||
message = instagram_messenger_inbox.messages.last
|
||||
attachment = message.attachments.last
|
||||
|
||||
expect(attachment.file_type).to eq 'ig_post'
|
||||
expect(attachment.external_url).to include 'ig_messaging_cdn'
|
||||
expect(message.content).to eq 'Shared post'
|
||||
expect(message.content_attributes['image_type']).to eq 'ig_post'
|
||||
end
|
||||
|
||||
it 'does not create contact or messages when Facebook API call fails' do
|
||||
story_mention_echo_event = build(:instagram_story_mention_event_with_echo).with_indifferent_access
|
||||
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_raise(Koala::Facebook::ClientError)
|
||||
|
||||
instagram_webhook.perform_now(story_mention_echo_event[:entry])
|
||||
|
||||
expect(instagram_messenger_inbox.contacts.count).to be 0
|
||||
expect(instagram_messenger_inbox.contact_inboxes.count).to be 0
|
||||
expect(instagram_messenger_inbox.messages.count).to be 0
|
||||
end
|
||||
|
||||
it 'handle messaging_seen callback' do
|
||||
messaging_seen_event = build(:messaging_seen_event).with_indifferent_access
|
||||
|
||||
expect(Instagram::ReadStatusService).to receive(:new).with(params: messaging_seen_event[:entry][0][:messaging][0],
|
||||
channel: instagram_messenger_inbox.channel).and_call_original
|
||||
instagram_webhook.perform_now(messaging_seen_event[:entry])
|
||||
end
|
||||
|
||||
it 'handles unsupported message' do
|
||||
unsupported_event = build(:instagram_message_unsupported_event).with_indifferent_access
|
||||
sender_id = unsupported_event[:entry][0][:messaging][0][:sender][:id]
|
||||
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
return_object_for(sender_id).with_indifferent_access
|
||||
)
|
||||
|
||||
instagram_webhook.perform_now(unsupported_event[:entry])
|
||||
expect(instagram_messenger_inbox.contacts.count).to be 1
|
||||
expect(instagram_messenger_inbox.contacts.last.additional_attributes['social_instagram_user_name']).to eq 'some_user_name'
|
||||
expect(instagram_messenger_inbox.conversations.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.last.content_attributes['is_unsupported']).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when handling messaging events for Instagram via Instagram login' do
|
||||
let!(:instagram_channel) { create(:channel_instagram, account: account, instagram_id: 'chatwoot-app-user-id-1') }
|
||||
let!(:instagram_inbox) { instagram_channel.inbox }
|
||||
|
||||
before do
|
||||
instagram_channel.update(access_token: 'valid_instagram_token')
|
||||
|
||||
stub_request(:get, %r{https://graph\.instagram\.com/v22\.0/Sender-id-.*\?.*})
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: proc { |request|
|
||||
sender_id = request.uri.path.split('/').last.split('?').first
|
||||
{
|
||||
name: 'Jane',
|
||||
username: 'some_user_name',
|
||||
profile_pic: 'https://chatwoot-assets.local/sample.png',
|
||||
id: sender_id,
|
||||
follower_count: 100,
|
||||
is_user_follow_business: true,
|
||||
is_business_follow_user: true,
|
||||
is_verified_user: false
|
||||
}.to_json
|
||||
},
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
end
|
||||
|
||||
it 'creates incoming message with correct contact info in the instagram direct inbox' do
|
||||
dm_event = build(:instagram_message_create_event).with_indifferent_access
|
||||
instagram_webhook.perform_now(dm_event[:entry])
|
||||
expect(instagram_inbox.contacts.count).to eq 1
|
||||
expect(instagram_inbox.contacts.last.additional_attributes['social_instagram_user_name']).to eq 'some_user_name'
|
||||
expect(instagram_inbox.conversations.count).to eq 1
|
||||
expect(instagram_inbox.messages.count).to eq 1
|
||||
expect(instagram_inbox.messages.last.content_attributes['is_unsupported']).to be_nil
|
||||
end
|
||||
|
||||
it 'sets correct instagram attributes on contact' do
|
||||
dm_event = build(:instagram_message_create_event).with_indifferent_access
|
||||
instagram_webhook.perform_now(dm_event[:entry])
|
||||
instagram_inbox.reload
|
||||
|
||||
contact = instagram_inbox.contacts.last
|
||||
|
||||
expect(contact.additional_attributes['social_instagram_follower_count']).to eq 100
|
||||
expect(contact.additional_attributes['social_instagram_is_user_follow_business']).to be true
|
||||
expect(contact.additional_attributes['social_instagram_is_business_follow_user']).to be true
|
||||
expect(contact.additional_attributes['social_instagram_is_verified_user']).to be false
|
||||
end
|
||||
|
||||
it 'handle instagram unsend message event' do
|
||||
unsend_event = build(:instagram_message_unsend_event).with_indifferent_access
|
||||
|
||||
message = create(:message, inbox_id: instagram_inbox.id, source_id: 'message-id-to-delete', content: 'random_text')
|
||||
|
||||
# Create attachment correctly with account association
|
||||
message.attachments.create!(
|
||||
file_type: :image,
|
||||
external_url: 'https://www.example.com/test.jpeg',
|
||||
account_id: instagram_inbox.account_id
|
||||
)
|
||||
|
||||
expect(instagram_inbox.messages.count).to be 1
|
||||
|
||||
instagram_webhook.perform_now(unsend_event[:entry])
|
||||
|
||||
message.reload
|
||||
|
||||
expect(message.content).to eq 'This message was deleted'
|
||||
expect(message.deleted).to be true
|
||||
expect(message.attachments.count).to be 0
|
||||
end
|
||||
|
||||
it 'creates incoming message with attachments in the instagram direct inbox' do
|
||||
attachment_event = build(:instagram_message_attachment_event).with_indifferent_access
|
||||
instagram_webhook.perform_now(attachment_event[:entry])
|
||||
|
||||
expect(instagram_inbox.contacts.count).to be 1
|
||||
expect(instagram_inbox.messages.count).to be 1
|
||||
expect(instagram_inbox.messages.last.attachments.count).to be 1
|
||||
end
|
||||
|
||||
it 'handles unsupported message' do
|
||||
unsupported_event = build(:instagram_message_unsupported_event).with_indifferent_access
|
||||
instagram_webhook.perform_now(unsupported_event[:entry])
|
||||
expect(instagram_inbox.contacts.count).to be 1
|
||||
expect(instagram_inbox.contacts.last.additional_attributes['social_instagram_user_name']).to eq 'some_user_name'
|
||||
expect(instagram_inbox.conversations.count).to be 1
|
||||
expect(instagram_inbox.messages.count).to be 1
|
||||
expect(instagram_inbox.messages.last.content_attributes['is_unsupported']).to be true
|
||||
end
|
||||
|
||||
it 'creates incoming message with ig_story attachment in the instagram direct inbox' do
|
||||
ig_story_event = build(:instagram_ig_story_event).with_indifferent_access
|
||||
instagram_webhook.perform_now(ig_story_event[:entry])
|
||||
|
||||
expect(instagram_inbox.messages.count).to be 1
|
||||
expect(instagram_inbox.messages.last.attachments.count).to be 1
|
||||
|
||||
message = instagram_inbox.messages.last
|
||||
attachment = message.attachments.last
|
||||
|
||||
expect(attachment.file_type).to eq 'ig_story'
|
||||
expect(attachment.external_url).to include 'lookaside.fbsbx.com'
|
||||
expect(message.content).to eq 'Shared story'
|
||||
expect(message.content_attributes['image_type']).to eq 'ig_story'
|
||||
end
|
||||
|
||||
it 'creates incoming message with ig_post attachment in the instagram direct inbox' do
|
||||
ig_post_event = build(:instagram_ig_post_event).with_indifferent_access
|
||||
instagram_webhook.perform_now(ig_post_event[:entry])
|
||||
|
||||
expect(instagram_inbox.messages.count).to be 1
|
||||
expect(instagram_inbox.messages.last.attachments.count).to be 1
|
||||
|
||||
message = instagram_inbox.messages.last
|
||||
attachment = message.attachments.last
|
||||
|
||||
expect(attachment.file_type).to eq 'ig_post'
|
||||
expect(attachment.external_url).to include 'ig_messaging_cdn'
|
||||
expect(message.content).to eq 'Shared post'
|
||||
expect(message.content_attributes['image_type']).to eq 'ig_post'
|
||||
end
|
||||
|
||||
it 'does not create contact or messages when Instagram API call fails' do
|
||||
story_mention_echo_event = build(:instagram_story_mention_event_with_echo).with_indifferent_access
|
||||
|
||||
stub_request(:get, %r{https://graph\.instagram\.com/v22\.0/.*\?.*})
|
||||
.to_return(status: 401, body: { error: { message: 'Invalid OAuth access token' } }.to_json)
|
||||
|
||||
instagram_webhook.perform_now(story_mention_echo_event[:entry])
|
||||
|
||||
expect(instagram_inbox.contacts.count).to be 0
|
||||
expect(instagram_inbox.contact_inboxes.count).to be 0
|
||||
expect(instagram_inbox.messages.count).to be 0
|
||||
end
|
||||
|
||||
it 'handles messaging_seen callback' do
|
||||
messaging_seen_event = build(:messaging_seen_event).with_indifferent_access
|
||||
|
||||
expect(Instagram::ReadStatusService).to receive(:new).with(params: messaging_seen_event[:entry][0][:messaging][0],
|
||||
channel: instagram_inbox.channel).and_call_original
|
||||
instagram_webhook.perform_now(messaging_seen_event[:entry])
|
||||
end
|
||||
|
||||
it 'creates contact when Instagram API call returns `No matching Instagram user` (9010 error code)' do
|
||||
stub_request(:get, %r{https://graph\.instagram\.com/v22\.0/.*\?.*})
|
||||
.to_return(status: 401, body: { error: { message: 'No matching Instagram user', code: 9010 } }.to_json)
|
||||
|
||||
dm_event = build(:instagram_message_create_event).with_indifferent_access
|
||||
sender_id = dm_event[:entry][0][:messaging][0][:sender][:id]
|
||||
instagram_webhook.perform_now(dm_event[:entry])
|
||||
|
||||
expect(instagram_inbox.contacts.count).to be 1
|
||||
expect(instagram_inbox.contacts.last.name).to eq "Unknown (IG: #{sender_id})"
|
||||
expect(instagram_inbox.contacts.last.contact_inboxes.count).to be 1
|
||||
expect(instagram_inbox.contacts.last.contact_inboxes.first.source_id).to eq sender_id
|
||||
|
||||
expect(instagram_inbox.conversations.count).to eq 1
|
||||
expect(instagram_inbox.messages.count).to eq 1
|
||||
expect(instagram_inbox.messages.last.content_attributes['is_unsupported']).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
38
spec/jobs/webhooks/line_events_job_spec.rb
Normal file
38
spec/jobs/webhooks/line_events_job_spec.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Webhooks::LineEventsJob do
|
||||
subject(:job) { described_class.perform_later(params: params) }
|
||||
|
||||
let!(:line_channel) { create(:channel_line) }
|
||||
let!(:params) { { :line_channel_id => line_channel.line_channel_id, 'line' => { test: 'test' } } }
|
||||
let(:post_body) { params.to_json }
|
||||
let(:signature) { Base64.strict_encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), line_channel.line_channel_secret, post_body)) }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(params: params)
|
||||
.on_queue('default')
|
||||
end
|
||||
|
||||
context 'when invalid params' do
|
||||
it 'returns nil when no line_channel_id' do
|
||||
expect(described_class.perform_now(params: {})).to be_nil
|
||||
end
|
||||
|
||||
it 'returns nil when invalid bot_token' do
|
||||
expect(described_class.perform_now(params: { 'line_channel_id' => 'invalid_id', 'line' => { test: 'test' } })).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when valid params' do
|
||||
it 'calls Line::IncomingMessageService' do
|
||||
process_service = double
|
||||
allow(Line::IncomingMessageService).to receive(:new).and_return(process_service)
|
||||
allow(process_service).to receive(:perform)
|
||||
expect(Line::IncomingMessageService).to receive(:new).with(inbox: line_channel.inbox,
|
||||
params: params['line'].with_indifferent_access)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(params: params, post_body: post_body, signature: signature)
|
||||
end
|
||||
end
|
||||
end
|
||||
85
spec/jobs/webhooks/sms_events_job_spec.rb
Normal file
85
spec/jobs/webhooks/sms_events_job_spec.rb
Normal file
@@ -0,0 +1,85 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Webhooks::SmsEventsJob do
|
||||
subject(:job) { described_class.perform_later(params) }
|
||||
|
||||
let!(:sms_channel) { create(:channel_sms) }
|
||||
let!(:params) do
|
||||
{
|
||||
time: '2022-02-02T23:14:05.309Z',
|
||||
type: 'message-received',
|
||||
to: sms_channel.phone_number,
|
||||
description: 'Incoming message received',
|
||||
message: {
|
||||
'id': '3232420-2323-234324',
|
||||
'owner': sms_channel.phone_number,
|
||||
'applicationId': '2342349-324234d-32432432',
|
||||
'time': '2022-02-02T23:14:05.262Z',
|
||||
'segmentCount': 1,
|
||||
'direction': 'in',
|
||||
'to': [
|
||||
sms_channel.phone_number
|
||||
],
|
||||
'from': '+14234234234',
|
||||
'text': 'test message'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(params)
|
||||
.on_queue('default')
|
||||
end
|
||||
|
||||
context 'when invalid params' do
|
||||
it 'returns nil when no bot_token' do
|
||||
expect(described_class.perform_now({})).to be_nil
|
||||
end
|
||||
|
||||
it 'returns nil when invalid type' do
|
||||
expect(described_class.perform_now({ type: 'invalid' })).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when valid params' do
|
||||
it 'calls Sms::IncomingMessageService if the message type is message-received' do
|
||||
process_service = double
|
||||
allow(Sms::IncomingMessageService).to receive(:new).and_return(process_service)
|
||||
allow(process_service).to receive(:perform)
|
||||
expect(Sms::IncomingMessageService).to receive(:new).with(inbox: sms_channel.inbox,
|
||||
params: params[:message].with_indifferent_access)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(params)
|
||||
end
|
||||
|
||||
it 'calls Sms::DeliveryStatusService if the message type is message-delivered' do
|
||||
params[:type] = 'message-delivered'
|
||||
process_service = double
|
||||
allow(Sms::DeliveryStatusService).to receive(:new).and_return(process_service)
|
||||
allow(process_service).to receive(:perform)
|
||||
expect(Sms::DeliveryStatusService).to receive(:new).with(channel: sms_channel,
|
||||
params: params[:message].with_indifferent_access)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(params)
|
||||
end
|
||||
|
||||
it 'calls Sms::DeliveryStatusService if the message type is message-failed' do
|
||||
params[:type] = 'message-failed'
|
||||
process_service = double
|
||||
allow(Sms::DeliveryStatusService).to receive(:new).and_return(process_service)
|
||||
allow(process_service).to receive(:perform)
|
||||
expect(Sms::DeliveryStatusService).to receive(:new).with(channel: sms_channel,
|
||||
params: params[:message].with_indifferent_access)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(params)
|
||||
end
|
||||
|
||||
it 'does not call any service if the message type is not supported' do
|
||||
params[:type] = 'message-sent'
|
||||
expect(Sms::IncomingMessageService).not_to receive(:new)
|
||||
expect(Sms::DeliveryStatusService).not_to receive(:new)
|
||||
described_class.perform_now(params)
|
||||
end
|
||||
end
|
||||
end
|
||||
64
spec/jobs/webhooks/telegram_events_job_spec.rb
Normal file
64
spec/jobs/webhooks/telegram_events_job_spec.rb
Normal file
@@ -0,0 +1,64 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Webhooks::TelegramEventsJob do
|
||||
subject(:job) { described_class.perform_later(params) }
|
||||
|
||||
let!(:telegram_channel) { create(:channel_telegram) }
|
||||
let!(:params) { { :bot_token => telegram_channel.bot_token, 'telegram' => { test: 'test' } } }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(params)
|
||||
.on_queue('default')
|
||||
end
|
||||
|
||||
context 'when invalid params' do
|
||||
it 'returns nil when no bot_token' do
|
||||
expect(described_class.perform_now({})).to be_nil
|
||||
end
|
||||
|
||||
it 'logs a warning when channel is not found' do
|
||||
expect(Rails.logger).to receive(:warn).with('Telegram event discarded: Channel not found for bot_token: invalid')
|
||||
described_class.perform_now({ bot_token: 'invalid' })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when valid params' do
|
||||
it 'calls Telegram::IncomingMessageService' do
|
||||
process_service = double
|
||||
allow(Telegram::IncomingMessageService).to receive(:new).and_return(process_service)
|
||||
allow(process_service).to receive(:perform)
|
||||
expect(Telegram::IncomingMessageService).to receive(:new).with(inbox: telegram_channel.inbox,
|
||||
params: params['telegram'].with_indifferent_access)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(params.with_indifferent_access)
|
||||
end
|
||||
|
||||
it 'logs a warning and does not process events if account is suspended' do
|
||||
account = telegram_channel.account
|
||||
account.update!(status: :suspended)
|
||||
|
||||
process_service = double
|
||||
allow(Telegram::IncomingMessageService).to receive(:new).and_return(process_service)
|
||||
allow(process_service).to receive(:perform)
|
||||
|
||||
expect(Rails.logger).to receive(:warn).with("Telegram event discarded: Account #{account.id} is not active for channel #{telegram_channel.id}")
|
||||
expect(Telegram::IncomingMessageService).not_to receive(:new)
|
||||
described_class.perform_now(params.with_indifferent_access)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when update message params' do
|
||||
let!(:params) { { :bot_token => telegram_channel.bot_token, 'telegram' => { edited_message: 'test' } } }
|
||||
|
||||
it 'calls Telegram::UpdateMessageService' do
|
||||
process_service = double
|
||||
allow(Telegram::UpdateMessageService).to receive(:new).and_return(process_service)
|
||||
allow(process_service).to receive(:perform)
|
||||
expect(Telegram::UpdateMessageService).to receive(:new).with(inbox: telegram_channel.inbox,
|
||||
params: params['telegram'].with_indifferent_access)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(params.with_indifferent_access)
|
||||
end
|
||||
end
|
||||
end
|
||||
91
spec/jobs/webhooks/tiktok_events_job_spec.rb
Normal file
91
spec/jobs/webhooks/tiktok_events_job_spec.rb
Normal file
@@ -0,0 +1,91 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Webhooks::TiktokEventsJob do
|
||||
let(:account) { create(:account) }
|
||||
let!(:channel) { create(:channel_tiktok, account: account, business_id: 'biz-123') }
|
||||
let(:job) { described_class.new }
|
||||
|
||||
before do
|
||||
allow(job).to receive(:with_lock).and_yield
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
it 'processes im_receive_msg events via Tiktok::MessageService' do
|
||||
message_service = instance_double(Tiktok::MessageService, perform: true)
|
||||
allow(Tiktok::MessageService).to receive(:new).and_return(message_service)
|
||||
|
||||
event = {
|
||||
event: 'im_receive_msg',
|
||||
user_openid: 'biz-123',
|
||||
content: { conversation_id: 'tt-conv-1' }.to_json
|
||||
}
|
||||
|
||||
job.perform(event)
|
||||
|
||||
expect(Tiktok::MessageService).to have_received(:new).with(channel: channel, content: hash_including(conversation_id: 'tt-conv-1'))
|
||||
expect(message_service).to have_received(:perform)
|
||||
end
|
||||
|
||||
it 'processes im_mark_read_msg events via Tiktok::ReadStatusService' do
|
||||
read_status_service = instance_double(Tiktok::ReadStatusService, perform: true)
|
||||
allow(Tiktok::ReadStatusService).to receive(:new).and_return(read_status_service)
|
||||
|
||||
event = {
|
||||
event: 'im_mark_read_msg',
|
||||
user_openid: 'biz-123',
|
||||
content: { conversation_id: 'tt-conv-1', read: { last_read_timestamp: 1_700_000_000_000 }, from_user: { id: 'user-1' } }.to_json
|
||||
}
|
||||
|
||||
job.perform(event)
|
||||
|
||||
expect(Tiktok::ReadStatusService).to have_received(:new).with(channel: channel, content: hash_including(conversation_id: 'tt-conv-1'))
|
||||
expect(read_status_service).to have_received(:perform)
|
||||
end
|
||||
|
||||
it 'ignores unsupported event types' do
|
||||
allow(Tiktok::MessageService).to receive(:new)
|
||||
|
||||
event = {
|
||||
event: 'unknown_event',
|
||||
user_openid: 'biz-123',
|
||||
content: { conversation_id: 'tt-conv-1' }.to_json
|
||||
}
|
||||
|
||||
job.perform(event)
|
||||
|
||||
expect(Tiktok::MessageService).not_to have_received(:new)
|
||||
end
|
||||
|
||||
it 'does nothing when channel is missing' do
|
||||
allow(Tiktok::MessageService).to receive(:new)
|
||||
|
||||
event = {
|
||||
event: 'im_receive_msg',
|
||||
user_openid: 'biz-does-not-exist',
|
||||
content: { conversation_id: 'tt-conv-1' }.to_json
|
||||
}
|
||||
|
||||
job.perform(event)
|
||||
|
||||
expect(Tiktok::MessageService).not_to have_received(:new)
|
||||
end
|
||||
|
||||
it 'does nothing when account is inactive' do
|
||||
allow(Channel::Tiktok).to receive(:find_by).and_return(channel)
|
||||
allow(channel.account).to receive(:active?).and_return(false)
|
||||
|
||||
message_service = instance_double(Tiktok::MessageService, perform: true)
|
||||
allow(Tiktok::MessageService).to receive(:new).and_return(message_service)
|
||||
|
||||
event = {
|
||||
event: 'im_receive_msg',
|
||||
user_openid: 'biz-123',
|
||||
content: { conversation_id: 'tt-conv-1' }.to_json
|
||||
}
|
||||
|
||||
job.perform(event)
|
||||
|
||||
expect(Tiktok::MessageService).not_to have_received(:new)
|
||||
end
|
||||
end
|
||||
end
|
||||
26
spec/jobs/webhooks/twilio_delivery_status_job_spec.rb
Normal file
26
spec/jobs/webhooks/twilio_delivery_status_job_spec.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Webhooks::TwilioDeliveryStatusJob do
|
||||
subject(:job) { described_class.perform_later(params) }
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
'MessageSid' => 'SM123',
|
||||
'MessageStatus' => 'delivered',
|
||||
'AccountSid' => 'AC123'
|
||||
}
|
||||
end
|
||||
|
||||
it 'queues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(params)
|
||||
.on_queue('low')
|
||||
end
|
||||
|
||||
it 'calls the Twilio::DeliveryStatusService' do
|
||||
service = double
|
||||
expect(Twilio::DeliveryStatusService).to receive(:new).with(params: params).and_return(service)
|
||||
expect(service).to receive(:perform)
|
||||
described_class.new.perform(params)
|
||||
end
|
||||
end
|
||||
103
spec/jobs/webhooks/twilio_events_job_spec.rb
Normal file
103
spec/jobs/webhooks/twilio_events_job_spec.rb
Normal file
@@ -0,0 +1,103 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Webhooks::TwilioEventsJob do
|
||||
subject(:job) { described_class.perform_later(params) }
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
From: '+1234567890',
|
||||
To: '+0987654321',
|
||||
Body: 'Test message',
|
||||
AccountSid: 'AC123',
|
||||
SmsSid: 'SM123'
|
||||
}
|
||||
end
|
||||
|
||||
it 'queues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(params)
|
||||
.on_queue('low')
|
||||
end
|
||||
|
||||
it 'calls the Twilio::IncomingMessageService' do
|
||||
service = double
|
||||
expect(Twilio::IncomingMessageService).to receive(:new).with(params: params).and_return(service)
|
||||
expect(service).to receive(:perform)
|
||||
described_class.perform_now(params)
|
||||
end
|
||||
|
||||
context 'when Body parameter or MediaUrl0 is not present' do
|
||||
let(:params_without_body) do
|
||||
{
|
||||
From: '+1234567890',
|
||||
To: '+0987654321',
|
||||
AccountSid: 'AC123',
|
||||
SmsSid: 'SM123'
|
||||
}
|
||||
end
|
||||
|
||||
it 'does not process the event' do
|
||||
expect(Twilio::IncomingMessageService).not_to receive(:new)
|
||||
described_class.perform_now(params_without_body)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when Body parameter is present' do
|
||||
let(:params_with_body) do
|
||||
{
|
||||
From: '+1234567890',
|
||||
To: '+0987654321',
|
||||
Body: 'Test message',
|
||||
AccountSid: 'AC123',
|
||||
SmsSid: 'SM123'
|
||||
}
|
||||
end
|
||||
|
||||
it 'processes the event' do
|
||||
service = double
|
||||
expect(Twilio::IncomingMessageService).to receive(:new).with(params: params_with_body).and_return(service)
|
||||
expect(service).to receive(:perform)
|
||||
described_class.perform_now(params_with_body)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when MediaUrl0 parameter is present' do
|
||||
let(:params_with_media) do
|
||||
{
|
||||
From: '+1234567890',
|
||||
To: '+0987654321',
|
||||
MediaUrl0: 'https://example.com/media.jpg',
|
||||
AccountSid: 'AC123',
|
||||
SmsSid: 'SM123'
|
||||
}
|
||||
end
|
||||
|
||||
it 'processes the event' do
|
||||
service = double
|
||||
expect(Twilio::IncomingMessageService).to receive(:new).with(params: params_with_media).and_return(service)
|
||||
expect(service).to receive(:perform)
|
||||
described_class.perform_now(params_with_media)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when location message is present' do
|
||||
let(:params_with_location) do
|
||||
{
|
||||
From: 'whatsapp:+1234567890',
|
||||
To: 'whatsapp:+0987654321',
|
||||
MessageType: 'location',
|
||||
Latitude: '12.160894393921',
|
||||
Longitude: '75.265205383301',
|
||||
AccountSid: 'AC123',
|
||||
SmsSid: 'SM123'
|
||||
}
|
||||
end
|
||||
|
||||
it 'processes the location message' do
|
||||
service = double
|
||||
expect(Twilio::IncomingMessageService).to receive(:new).with(params: params_with_location).and_return(service)
|
||||
expect(service).to receive(:perform)
|
||||
described_class.perform_now(params_with_location)
|
||||
end
|
||||
end
|
||||
end
|
||||
246
spec/jobs/webhooks/whatsapp_events_job_spec.rb
Normal file
246
spec/jobs/webhooks/whatsapp_events_job_spec.rb
Normal file
@@ -0,0 +1,246 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Webhooks::WhatsappEventsJob do
|
||||
subject(:job) { described_class }
|
||||
|
||||
let(:channel) { create(:channel_whatsapp, provider: 'whatsapp_cloud', sync_templates: false, validate_provider_config: false) }
|
||||
let(:params) do
|
||||
{
|
||||
object: 'whatsapp_business_account',
|
||||
phone_number: channel.phone_number,
|
||||
entry: [{
|
||||
changes: [
|
||||
{
|
||||
value: {
|
||||
metadata: {
|
||||
phone_number_id: channel.provider_config['phone_number_id'],
|
||||
display_phone_number: channel.phone_number.delete('+')
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
end
|
||||
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('low')
|
||||
end
|
||||
|
||||
context 'when whatsapp_cloud provider' do
|
||||
it 'enqueue Whatsapp::IncomingMessageWhatsappCloudService' do
|
||||
allow(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new).and_return(process_service)
|
||||
expect(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new)
|
||||
job.perform_now(params)
|
||||
end
|
||||
|
||||
it 'will not enqueue message jobs based on phone number in the URL if the entry payload is not present' do
|
||||
params = {
|
||||
object: 'whatsapp_business_account',
|
||||
phone_number: channel.phone_number,
|
||||
entry: [{ changes: [{}] }]
|
||||
}
|
||||
allow(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new)
|
||||
allow(Whatsapp::IncomingMessageService).to receive(:new)
|
||||
|
||||
expect(Whatsapp::IncomingMessageWhatsappCloudService).not_to receive(:new)
|
||||
expect(Whatsapp::IncomingMessageService).not_to receive(:new)
|
||||
job.perform_now(params)
|
||||
end
|
||||
|
||||
it 'will not enqueue Whatsapp::IncomingMessageWhatsappCloudService if channel reauthorization required' do
|
||||
channel.prompt_reauthorization!
|
||||
allow(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new).and_return(process_service)
|
||||
expect(Whatsapp::IncomingMessageWhatsappCloudService).not_to receive(:new)
|
||||
job.perform_now(params)
|
||||
end
|
||||
|
||||
it 'will not enqueue if channel is not present' do
|
||||
allow(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new).and_return(process_service)
|
||||
allow(Whatsapp::IncomingMessageService).to receive(:new).and_return(process_service)
|
||||
|
||||
expect(Whatsapp::IncomingMessageWhatsappCloudService).not_to receive(:new)
|
||||
expect(Whatsapp::IncomingMessageService).not_to receive(:new)
|
||||
job.perform_now(phone_number: 'random_phone_number')
|
||||
end
|
||||
|
||||
it 'will not enqueue Whatsapp::IncomingMessageWhatsappCloudService if account is suspended' do
|
||||
account = channel.account
|
||||
account.update!(status: :suspended)
|
||||
allow(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new).and_return(process_service)
|
||||
allow(Whatsapp::IncomingMessageService).to receive(:new).and_return(process_service)
|
||||
|
||||
expect(Whatsapp::IncomingMessageWhatsappCloudService).not_to receive(:new)
|
||||
expect(Whatsapp::IncomingMessageService).not_to receive(:new)
|
||||
job.perform_now(params)
|
||||
end
|
||||
|
||||
it 'logs a warning when channel is inactive' do
|
||||
channel.prompt_reauthorization!
|
||||
allow(Rails.logger).to receive(:warn)
|
||||
|
||||
expect(Rails.logger).to receive(:warn).with("Inactive WhatsApp channel: #{channel.phone_number}")
|
||||
job.perform_now(params)
|
||||
end
|
||||
|
||||
it 'logs a warning with unknown phone number when channel does not exist' do
|
||||
unknown_phone = '+1234567890'
|
||||
allow(Rails.logger).to receive(:warn)
|
||||
|
||||
expect(Rails.logger).to receive(:warn).with("Inactive WhatsApp channel: unknown - #{unknown_phone}")
|
||||
job.perform_now(phone_number: unknown_phone)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when default provider' do
|
||||
it 'enqueue 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 'enqueue 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 'Ignore reaction type message and stop raising error' 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: {
|
||||
contacts: [{ profile: { name: 'Test Test' }, wa_id: '1111981136571' }],
|
||||
messages: [{
|
||||
from: '1111981136571', reaction: { emoji: '👍' }, timestamp: '1664799904', type: 'reaction'
|
||||
}],
|
||||
metadata: {
|
||||
phone_number_id: other_channel.provider_config['phone_number_id'],
|
||||
display_phone_number: other_channel.phone_number.delete('+')
|
||||
}
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}.with_indifferent_access
|
||||
expect do
|
||||
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: other_channel.inbox, params: wb_params).perform
|
||||
end.not_to change(Message, :count)
|
||||
end
|
||||
|
||||
it 'ignore reaction type message, would not create contact if the reaction is the first event' 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: {
|
||||
contacts: [{ profile: { name: 'Test Test' }, wa_id: '1111981136571' }],
|
||||
messages: [{
|
||||
from: '1111981136571', reaction: { emoji: '👍' }, timestamp: '1664799904', type: 'reaction'
|
||||
}],
|
||||
metadata: {
|
||||
phone_number_id: other_channel.provider_config['phone_number_id'],
|
||||
display_phone_number: other_channel.phone_number.delete('+')
|
||||
}
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}.with_indifferent_access
|
||||
expect do
|
||||
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: other_channel.inbox, params: wb_params).perform
|
||||
end.not_to change(Contact, :count)
|
||||
end
|
||||
|
||||
it 'ignore request_welcome type message, would not create contact or conversation' 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: {
|
||||
messages: [{
|
||||
from: '1111981136571', timestamp: '1664799904', type: 'request_welcome'
|
||||
}],
|
||||
metadata: {
|
||||
phone_number_id: other_channel.provider_config['phone_number_id'],
|
||||
display_phone_number: other_channel.phone_number.delete('+')
|
||||
}
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}.with_indifferent_access
|
||||
expect do
|
||||
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: other_channel.inbox, params: wb_params).perform
|
||||
end.not_to change(Contact, :count)
|
||||
|
||||
expect do
|
||||
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: other_channel.inbox, params: wb_params).perform
|
||||
end.not_to change(Conversation, :count)
|
||||
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
|
||||
Reference in New Issue
Block a user