Merge branch 'develop' into feat/github-integration
This commit is contained in:
@@ -17,40 +17,34 @@ describe Messages::Instagram::MessageBuilder do
|
||||
let!(:shared_reel_params) { build(:instagram_shared_reel_event).with_indifferent_access }
|
||||
let!(:instagram_story_reply_event) { build(:instagram_story_reply_event).with_indifferent_access }
|
||||
let!(:instagram_message_reply_event) { build(:instagram_message_reply_event).with_indifferent_access }
|
||||
let!(:contact) { create(:contact, id: 'Sender-id-1', name: 'Jane Dae') }
|
||||
let!(:contact_inbox) { create(:contact_inbox, contact_id: contact.id, inbox_id: instagram_inbox.id, source_id: 'Sender-id-1') }
|
||||
let(:conversation) do
|
||||
create(:conversation, account_id: account.id, inbox_id: instagram_inbox.id, contact_id: contact.id)
|
||||
end
|
||||
let(:message) do
|
||||
create(:message, account_id: account.id, inbox_id: instagram_inbox.id, conversation_id: conversation.id, message_type: 'outgoing',
|
||||
source_id: 'message-id-1')
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
before do
|
||||
instagram_channel.update(access_token: 'valid_instagram_token')
|
||||
|
||||
stub_request(:get, %r{https://graph\.instagram\.com/.*?/Sender-id-1\?.*})
|
||||
stub_request(:get, %r{https://graph\.instagram\.com/.*?/Sender-id-.*?\?.*})
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: {
|
||||
name: 'Jane',
|
||||
username: 'some_user_name',
|
||||
profile_pic: 'https://chatwoot-assets.local/sample.png',
|
||||
id: 'Sender-id-1',
|
||||
follower_count: 100,
|
||||
is_user_follow_business: true,
|
||||
is_business_follow_user: true,
|
||||
is_verified_user: false
|
||||
}.to_json,
|
||||
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 contact and message for the instagram direct inbox' do
|
||||
messaging = dm_params[:entry][0]['messaging'][0]
|
||||
contact_inbox
|
||||
create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
|
||||
described_class.new(messaging, instagram_inbox).perform
|
||||
|
||||
instagram_inbox.reload
|
||||
@@ -63,13 +57,15 @@ describe Messages::Instagram::MessageBuilder do
|
||||
end
|
||||
|
||||
it 'discard echo message already sent by chatwoot' do
|
||||
conversation
|
||||
message
|
||||
messaging = dm_params[:entry][0]['messaging'][0]
|
||||
contact = create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
|
||||
conversation = create(:conversation, account_id: account.id, inbox_id: instagram_inbox.id, contact_id: contact.id)
|
||||
create(:message, account_id: account.id, inbox_id: instagram_inbox.id, conversation_id: conversation.id, message_type: 'outgoing',
|
||||
source_id: 'message-id-1')
|
||||
|
||||
expect(instagram_inbox.conversations.count).to be 1
|
||||
expect(instagram_inbox.messages.count).to be 1
|
||||
|
||||
messaging = dm_params[:entry][0]['messaging'][0]
|
||||
messaging[:message][:mid] = 'message-id-1' # Set same source_id as the existing message
|
||||
described_class.new(messaging, instagram_inbox, outgoing_echo: true).perform
|
||||
|
||||
@@ -81,6 +77,7 @@ describe Messages::Instagram::MessageBuilder do
|
||||
|
||||
it 'discards duplicate messages from webhook events with the same message_id' do
|
||||
messaging = dm_params[:entry][0]['messaging'][0]
|
||||
create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
|
||||
described_class.new(messaging, instagram_inbox).perform
|
||||
|
||||
initial_message_count = instagram_inbox.messages.count
|
||||
@@ -93,6 +90,7 @@ describe Messages::Instagram::MessageBuilder do
|
||||
|
||||
it 'creates message for shared reel' do
|
||||
messaging = shared_reel_params[:entry][0]['messaging'][0]
|
||||
create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
|
||||
described_class.new(messaging, instagram_inbox).perform
|
||||
|
||||
message = instagram_inbox.messages.first
|
||||
@@ -103,7 +101,9 @@ describe Messages::Instagram::MessageBuilder do
|
||||
end
|
||||
|
||||
it 'creates message with story id' do
|
||||
story_source_id = instagram_story_reply_event[:entry][0]['messaging'][0]['message']['mid']
|
||||
messaging = instagram_story_reply_event[:entry][0]['messaging'][0]
|
||||
create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
|
||||
story_source_id = messaging['message']['mid']
|
||||
|
||||
stub_request(:get, %r{https://graph\.instagram\.com/.*?/#{story_source_id}\?.*})
|
||||
.to_return(
|
||||
@@ -121,7 +121,6 @@ describe Messages::Instagram::MessageBuilder do
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
|
||||
messaging = instagram_story_reply_event[:entry][0]['messaging'][0]
|
||||
described_class.new(messaging, instagram_inbox).perform
|
||||
|
||||
message = instagram_inbox.messages.first
|
||||
@@ -134,10 +133,13 @@ describe Messages::Instagram::MessageBuilder do
|
||||
it 'creates message with reply to mid' do
|
||||
# Create first message to ensure reply to is valid
|
||||
first_messaging = dm_params[:entry][0]['messaging'][0]
|
||||
sender_id = first_messaging['sender']['id']
|
||||
create_instagram_contact_for_sender(sender_id, instagram_inbox)
|
||||
described_class.new(first_messaging, instagram_inbox).perform
|
||||
|
||||
# Create second message with reply to mid
|
||||
# Create second message with reply to mid, using same sender_id
|
||||
messaging = instagram_message_reply_event[:entry][0]['messaging'][0]
|
||||
messaging['sender']['id'] = sender_id
|
||||
described_class.new(messaging, instagram_inbox).perform
|
||||
|
||||
first_message = instagram_inbox.messages.first
|
||||
@@ -148,12 +150,13 @@ describe Messages::Instagram::MessageBuilder do
|
||||
end
|
||||
|
||||
it 'handles deleted story' do
|
||||
story_source_id = story_mention_params[:entry][0][:messaging][0]['message']['mid']
|
||||
messaging = story_mention_params[:entry][0][:messaging][0]
|
||||
create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
|
||||
story_source_id = messaging['message']['mid']
|
||||
|
||||
stub_request(:get, %r{https://graph\.instagram\.com/.*?/#{story_source_id}\?.*})
|
||||
.to_return(status: 404, body: { error: { message: 'Story not found', code: 1_609_005 } }.to_json)
|
||||
|
||||
messaging = story_mention_params[:entry][0][:messaging][0]
|
||||
described_class.new(messaging, instagram_inbox).perform
|
||||
|
||||
message = instagram_inbox.messages.first
|
||||
@@ -163,11 +166,12 @@ describe Messages::Instagram::MessageBuilder do
|
||||
end
|
||||
|
||||
it 'does not create message for unsupported file type' do
|
||||
conversation
|
||||
messaging = story_mention_params[:entry][0][:messaging][0]
|
||||
contact = create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
|
||||
create(:conversation, account_id: account.id, inbox_id: instagram_inbox.id, contact_id: contact.id)
|
||||
|
||||
# try to create a message with unsupported file type
|
||||
story_mention_params[:entry][0][:messaging][0]['message']['attachments'][0]['type'] = 'unsupported_type'
|
||||
messaging = story_mention_params[:entry][0][:messaging][0]
|
||||
messaging['message']['attachments'][0]['type'] = 'unsupported_type'
|
||||
|
||||
described_class.new(messaging, instagram_inbox, outgoing_echo: false).perform
|
||||
|
||||
@@ -177,7 +181,11 @@ describe Messages::Instagram::MessageBuilder do
|
||||
end
|
||||
|
||||
it 'does not create message if the message is already exists' do
|
||||
message
|
||||
messaging = dm_params[:entry][0]['messaging'][0]
|
||||
contact = create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
|
||||
conversation = create(:conversation, account_id: account.id, inbox_id: instagram_inbox.id, contact_id: contact.id)
|
||||
create(:message, account_id: account.id, inbox_id: instagram_inbox.id, conversation_id: conversation.id, message_type: 'outgoing',
|
||||
source_id: 'message-id-1')
|
||||
|
||||
expect(instagram_inbox.conversations.count).to be 1
|
||||
expect(instagram_inbox.messages.count).to be 1
|
||||
@@ -194,7 +202,7 @@ describe Messages::Instagram::MessageBuilder do
|
||||
instagram_channel.update(access_token: 'invalid_token')
|
||||
|
||||
# Stub the request to return authorization error status
|
||||
stub_request(:get, %r{https://graph\.instagram\.com/.*?/Sender-id-1\?.*})
|
||||
stub_request(:get, %r{https://graph\.instagram\.com/.*?/Sender-id-.*?\?.*})
|
||||
.to_return(
|
||||
status: 401,
|
||||
body: { error: { message: 'unauthorized access token', code: 190 } }.to_json,
|
||||
@@ -218,6 +226,7 @@ describe Messages::Instagram::MessageBuilder do
|
||||
it 'creates a new conversation if existing conversation is not present' do
|
||||
initial_count = Conversation.count
|
||||
messaging = dm_params[:entry][0]['messaging'][0]
|
||||
create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
|
||||
|
||||
described_class.new(messaging, instagram_inbox).perform
|
||||
|
||||
@@ -226,21 +235,23 @@ describe Messages::Instagram::MessageBuilder do
|
||||
end
|
||||
|
||||
it 'will not create a new conversation if last conversation is not resolved' do
|
||||
messaging = dm_params[:entry][0]['messaging'][0]
|
||||
contact = create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
|
||||
existing_conversation = create(:conversation, account_id: account.id, inbox_id: instagram_inbox.id,
|
||||
contact_id: contact.id, status: :open)
|
||||
|
||||
messaging = dm_params[:entry][0]['messaging'][0]
|
||||
described_class.new(messaging, instagram_inbox).perform
|
||||
|
||||
expect(instagram_inbox.conversations.last.id).to eq(existing_conversation.id)
|
||||
end
|
||||
|
||||
it 'creates a new conversation if last conversation is resolved' do
|
||||
messaging = dm_params[:entry][0]['messaging'][0]
|
||||
contact = create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
|
||||
existing_conversation = create(:conversation, account_id: account.id, inbox_id: instagram_inbox.id,
|
||||
contact_id: contact.id, status: :resolved)
|
||||
|
||||
initial_count = Conversation.count
|
||||
messaging = dm_params[:entry][0]['messaging'][0]
|
||||
|
||||
described_class.new(messaging, instagram_inbox).perform
|
||||
|
||||
@@ -257,6 +268,7 @@ describe Messages::Instagram::MessageBuilder do
|
||||
it 'creates a new conversation if existing conversation is not present' do
|
||||
initial_count = Conversation.count
|
||||
messaging = dm_params[:entry][0]['messaging'][0]
|
||||
create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
|
||||
|
||||
described_class.new(messaging, instagram_inbox).perform
|
||||
|
||||
@@ -265,6 +277,8 @@ describe Messages::Instagram::MessageBuilder do
|
||||
end
|
||||
|
||||
it 'reopens last conversation if last conversation is resolved' do
|
||||
messaging = dm_params[:entry][0]['messaging'][0]
|
||||
contact = create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
|
||||
existing_conversation = create(:conversation, account_id: account.id, inbox_id: instagram_inbox.id,
|
||||
contact_id: contact.id, status: :resolved)
|
||||
|
||||
@@ -307,6 +321,7 @@ describe Messages::Instagram::MessageBuilder do
|
||||
|
||||
it 'saves story information when story mention is processed' do
|
||||
messaging = story_mention_params[:entry][0][:messaging][0]
|
||||
create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
|
||||
described_class.new(messaging, instagram_inbox).perform
|
||||
|
||||
message = instagram_inbox.messages.first
|
||||
@@ -328,6 +343,7 @@ describe Messages::Instagram::MessageBuilder do
|
||||
)
|
||||
|
||||
messaging = story_mention_params[:entry][0][:messaging][0]
|
||||
create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
|
||||
described_class.new(messaging, instagram_inbox).perform
|
||||
|
||||
message = instagram_inbox.messages.first
|
||||
|
||||
@@ -17,30 +17,23 @@ describe Messages::Instagram::Messenger::MessageBuilder do
|
||||
let!(:instagram_story_reply_event) { build(:instagram_story_reply_event).with_indifferent_access }
|
||||
let!(:instagram_message_reply_event) { build(:instagram_message_reply_event).with_indifferent_access }
|
||||
let(:fb_object) { double }
|
||||
let(:contact) { create(:contact, id: 'Sender-id-1', name: 'Jane Dae') }
|
||||
let(:contact_inbox) { create(:contact_inbox, contact_id: contact.id, inbox_id: instagram_messenger_inbox.id, source_id: 'Sender-id-1') }
|
||||
let(:conversation) do
|
||||
create(:conversation, account_id: account.id, inbox_id: instagram_messenger_inbox.id, contact_id: contact.id,
|
||||
additional_attributes: { type: 'instagram_direct_message', conversation_language: 'en' })
|
||||
end
|
||||
let(:message) do
|
||||
create(:message, account_id: account.id, inbox_id: instagram_messenger_inbox.id, conversation_id: conversation.id, message_type: 'outgoing',
|
||||
source_id: 'message-id-1')
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
it 'creates contact and message for the facebook inbox' do
|
||||
messaging = dm_params[:entry][0]['messaging'][0]
|
||||
sender_id = messaging['sender']['id']
|
||||
|
||||
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-1',
|
||||
id: sender_id,
|
||||
account_id: instagram_messenger_inbox.account_id,
|
||||
profile_pic: 'https://chatwoot-assets.local/sample.png'
|
||||
}.with_indifferent_access
|
||||
)
|
||||
messaging = dm_params[:entry][0]['messaging'][0]
|
||||
contact_inbox
|
||||
|
||||
create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
|
||||
described_class.new(messaging, instagram_messenger_inbox).perform
|
||||
|
||||
instagram_messenger_inbox.reload
|
||||
@@ -56,7 +49,13 @@ describe Messages::Instagram::Messenger::MessageBuilder do
|
||||
end
|
||||
|
||||
it 'discard echo message already sent by chatwoot' do
|
||||
message
|
||||
messaging = dm_params[:entry][0]['messaging'][0]
|
||||
sender_id = messaging['sender']['id']
|
||||
contact = create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
|
||||
conversation = create(:conversation, account_id: account.id, inbox_id: instagram_messenger_inbox.id, contact_id: contact.id,
|
||||
additional_attributes: { type: 'instagram_direct_message', conversation_language: 'en' })
|
||||
create(:message, account_id: account.id, inbox_id: instagram_messenger_inbox.id, conversation_id: conversation.id, message_type: 'outgoing',
|
||||
source_id: 'message-id-1')
|
||||
|
||||
expect(instagram_messenger_inbox.conversations.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.count).to be 1
|
||||
@@ -65,13 +64,11 @@ describe Messages::Instagram::Messenger::MessageBuilder do
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
{
|
||||
name: 'Jane',
|
||||
id: 'Sender-id-1',
|
||||
id: sender_id,
|
||||
account_id: instagram_messenger_inbox.account_id,
|
||||
profile_pic: 'https://chatwoot-assets.local/sample.png'
|
||||
}.with_indifferent_access
|
||||
)
|
||||
messaging = dm_params[:entry][0]['messaging'][0]
|
||||
contact_inbox
|
||||
described_class.new(messaging, instagram_messenger_inbox, outgoing_echo: true).perform
|
||||
|
||||
instagram_messenger_inbox.reload
|
||||
@@ -81,17 +78,20 @@ describe Messages::Instagram::Messenger::MessageBuilder do
|
||||
end
|
||||
|
||||
it 'creates message for shared reel' do
|
||||
messaging = shared_reel_params[:entry][0]['messaging'][0]
|
||||
sender_id = messaging['sender']['id']
|
||||
|
||||
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-1',
|
||||
id: sender_id,
|
||||
account_id: instagram_messenger_inbox.account_id,
|
||||
profile_pic: 'https://chatwoot-assets.local/sample.png'
|
||||
}.with_indifferent_access
|
||||
)
|
||||
messaging = shared_reel_params[:entry][0]['messaging'][0]
|
||||
contact_inbox
|
||||
|
||||
create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
|
||||
described_class.new(messaging, instagram_messenger_inbox).perform
|
||||
|
||||
message = instagram_messenger_channel.inbox.messages.first
|
||||
@@ -102,18 +102,20 @@ describe Messages::Instagram::Messenger::MessageBuilder do
|
||||
end
|
||||
|
||||
it 'creates message with for reply with story id' do
|
||||
messaging = instagram_story_reply_event[:entry][0]['messaging'][0]
|
||||
sender_id = messaging['sender']['id']
|
||||
|
||||
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-1',
|
||||
id: sender_id,
|
||||
account_id: instagram_messenger_inbox.account_id,
|
||||
profile_pic: 'https://chatwoot-assets.local/sample.png'
|
||||
}.with_indifferent_access
|
||||
)
|
||||
messaging = instagram_story_reply_event[:entry][0]['messaging'][0]
|
||||
contact_inbox
|
||||
|
||||
create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
|
||||
described_class.new(messaging, instagram_messenger_inbox).perform
|
||||
|
||||
message = instagram_messenger_channel.inbox.messages.first
|
||||
@@ -125,24 +127,26 @@ describe Messages::Instagram::Messenger::MessageBuilder do
|
||||
end
|
||||
|
||||
it 'creates message with for reply with mid' do
|
||||
# create first message to ensure reply to is valid
|
||||
first_message_data = dm_params[:entry][0]['messaging'][0]
|
||||
sender_id = first_message_data['sender']['id']
|
||||
|
||||
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-1',
|
||||
id: sender_id,
|
||||
account_id: instagram_messenger_inbox.account_id,
|
||||
profile_pic: 'https://chatwoot-assets.local/sample.png'
|
||||
}.with_indifferent_access
|
||||
)
|
||||
# create first message to ensure reply to is valid
|
||||
first_message = dm_params[:entry][0]['messaging'][0]
|
||||
contact_inbox
|
||||
described_class.new(first_message, instagram_messenger_inbox).perform
|
||||
|
||||
# create the second message with the reply to mid set
|
||||
create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
|
||||
described_class.new(first_message_data, instagram_messenger_inbox).perform
|
||||
|
||||
# create the second message with the reply to mid set, ensure same sender_id
|
||||
messaging = instagram_message_reply_event[:entry][0]['messaging'][0]
|
||||
contact_inbox
|
||||
|
||||
messaging['sender']['id'] = sender_id # Use the same sender_id
|
||||
described_class.new(messaging, instagram_messenger_inbox).perform
|
||||
first_message = instagram_messenger_channel.inbox.messages.first
|
||||
message = instagram_messenger_channel.inbox.messages.last
|
||||
@@ -153,14 +157,16 @@ describe Messages::Instagram::Messenger::MessageBuilder do
|
||||
end
|
||||
|
||||
it 'raises exception on deleted story' do
|
||||
messaging = story_mention_params[:entry][0][:messaging][0]
|
||||
sender_id = messaging['sender']['id']
|
||||
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_raise(Koala::Facebook::ClientError.new(
|
||||
190,
|
||||
'This Message has been deleted by the user or the business.'
|
||||
))
|
||||
|
||||
messaging = story_mention_params[:entry][0][:messaging][0]
|
||||
contact_inbox
|
||||
create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
|
||||
described_class.new(messaging, instagram_messenger_inbox, outgoing_echo: false).perform
|
||||
|
||||
instagram_messenger_inbox.reload
|
||||
@@ -180,22 +186,24 @@ describe Messages::Instagram::Messenger::MessageBuilder do
|
||||
end
|
||||
|
||||
it 'does not create message for unsupported file type' do
|
||||
# create a message with unsupported file type
|
||||
story_mention_params[:entry][0][:messaging][0]['message']['attachments'][0]['type'] = 'unsupported_type'
|
||||
messaging = story_mention_params[:entry][0][:messaging][0]
|
||||
sender_id = messaging['sender']['id']
|
||||
|
||||
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-1',
|
||||
id: sender_id,
|
||||
account_id: instagram_messenger_inbox.account_id,
|
||||
profile_pic: 'https://chatwoot-assets.local/sample.png'
|
||||
}.with_indifferent_access
|
||||
)
|
||||
|
||||
conversation
|
||||
|
||||
# create a message with unsupported file type
|
||||
story_mention_params[:entry][0][:messaging][0]['message']['attachments'][0]['type'] = 'unsupported_type'
|
||||
messaging = story_mention_params[:entry][0][:messaging][0]
|
||||
|
||||
contact = create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
|
||||
create(:conversation, account_id: account.id, inbox_id: instagram_messenger_inbox.id, contact_id: contact.id,
|
||||
additional_attributes: { type: 'instagram_direct_message', conversation_language: 'en' })
|
||||
described_class.new(messaging, instagram_messenger_inbox, outgoing_echo: false).perform
|
||||
|
||||
instagram_messenger_inbox.reload
|
||||
@@ -218,18 +226,22 @@ describe Messages::Instagram::Messenger::MessageBuilder do
|
||||
it 'creates a new conversation if existing conversation is not present' do
|
||||
inital_count = Conversation.count
|
||||
message = dm_params[:entry][0]['messaging'][0]
|
||||
contact_inbox
|
||||
sender_id = message['sender']['id']
|
||||
|
||||
create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
|
||||
described_class.new(message, instagram_messenger_inbox).perform
|
||||
|
||||
instagram_messenger_inbox.reload
|
||||
contact_inbox.reload
|
||||
|
||||
expect(instagram_messenger_inbox.conversations.count).to eq(1)
|
||||
expect(Conversation.count).to eq(inital_count + 1)
|
||||
end
|
||||
|
||||
it 'will not create a new conversation if last conversation is not resolved' do
|
||||
message = dm_params[:entry][0]['messaging'][0]
|
||||
sender_id = message['sender']['id']
|
||||
contact = create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
|
||||
|
||||
existing_conversation = create(
|
||||
:conversation,
|
||||
account_id: account.id,
|
||||
@@ -239,18 +251,18 @@ describe Messages::Instagram::Messenger::MessageBuilder do
|
||||
additional_attributes: { type: 'instagram_direct_message', conversation_language: 'en' }
|
||||
)
|
||||
|
||||
message = dm_params[:entry][0]['messaging'][0]
|
||||
contact_inbox
|
||||
|
||||
described_class.new(message, instagram_messenger_inbox).perform
|
||||
|
||||
instagram_messenger_inbox.reload
|
||||
contact_inbox.reload
|
||||
|
||||
expect(instagram_messenger_inbox.conversations.last.id).to eq(existing_conversation.id)
|
||||
end
|
||||
|
||||
it 'creates a new conversation if last conversation is resolved' do
|
||||
message = dm_params[:entry][0]['messaging'][0]
|
||||
sender_id = message['sender']['id']
|
||||
contact = create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
|
||||
|
||||
existing_conversation = create(
|
||||
:conversation,
|
||||
account_id: account.id,
|
||||
@@ -261,13 +273,9 @@ describe Messages::Instagram::Messenger::MessageBuilder do
|
||||
)
|
||||
|
||||
inital_count = Conversation.count
|
||||
message = dm_params[:entry][0]['messaging'][0]
|
||||
contact_inbox
|
||||
|
||||
described_class.new(message, instagram_messenger_inbox).perform
|
||||
|
||||
instagram_messenger_inbox.reload
|
||||
contact_inbox.reload
|
||||
|
||||
expect(instagram_messenger_inbox.conversations.last.id).not_to eq(existing_conversation.id)
|
||||
expect(Conversation.count).to eq(inital_count + 1)
|
||||
@@ -283,18 +291,22 @@ describe Messages::Instagram::Messenger::MessageBuilder do
|
||||
it 'creates a new conversation if existing conversation is not present' do
|
||||
inital_count = Conversation.count
|
||||
message = dm_params[:entry][0]['messaging'][0]
|
||||
contact_inbox
|
||||
sender_id = message['sender']['id']
|
||||
|
||||
create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
|
||||
described_class.new(message, instagram_messenger_inbox).perform
|
||||
|
||||
instagram_messenger_inbox.reload
|
||||
contact_inbox.reload
|
||||
|
||||
expect(instagram_messenger_inbox.conversations.count).to eq(1)
|
||||
expect(Conversation.count).to eq(inital_count + 1)
|
||||
end
|
||||
|
||||
it 'reopens last conversation if last conversation is resolved' do
|
||||
message = dm_params[:entry][0]['messaging'][0]
|
||||
sender_id = message['sender']['id']
|
||||
contact = create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
|
||||
|
||||
existing_conversation = create(
|
||||
:conversation,
|
||||
account_id: account.id,
|
||||
@@ -306,13 +318,9 @@ describe Messages::Instagram::Messenger::MessageBuilder do
|
||||
|
||||
inital_count = Conversation.count
|
||||
|
||||
message = dm_params[:entry][0]['messaging'][0]
|
||||
contact_inbox
|
||||
|
||||
described_class.new(message, instagram_messenger_inbox).perform
|
||||
|
||||
instagram_messenger_inbox.reload
|
||||
contact_inbox.reload
|
||||
|
||||
expect(instagram_messenger_inbox.conversations.last.id).to eq(existing_conversation.id)
|
||||
expect(Conversation.count).to eq(inital_count)
|
||||
@@ -344,7 +352,9 @@ describe Messages::Instagram::Messenger::MessageBuilder do
|
||||
allow(fb_object).to receive(:get_object).and_return(story_data)
|
||||
|
||||
messaging = story_mention_params[:entry][0][:messaging][0]
|
||||
contact_inbox
|
||||
sender_id = messaging['sender']['id']
|
||||
|
||||
create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
|
||||
builder = described_class.new(messaging, instagram_messenger_inbox)
|
||||
builder.perform
|
||||
|
||||
@@ -358,18 +368,20 @@ describe Messages::Instagram::Messenger::MessageBuilder do
|
||||
end
|
||||
|
||||
it 'handles story mentions specifically in the Instagram builder' do
|
||||
messaging = story_mention_params[:entry][0][:messaging][0]
|
||||
sender_id = messaging['sender']['id']
|
||||
|
||||
# First allow contact info fetch
|
||||
allow(fb_object).to receive(:get_object).and_return({
|
||||
name: 'Jane',
|
||||
id: 'Sender-id-1'
|
||||
id: sender_id
|
||||
}.with_indifferent_access)
|
||||
|
||||
# Then allow story data fetch
|
||||
allow(fb_object).to receive(:get_object).with(anything, fields: %w[story from])
|
||||
.and_return(story_data)
|
||||
|
||||
messaging = story_mention_params[:entry][0][:messaging][0]
|
||||
contact_inbox
|
||||
create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
|
||||
described_class.new(messaging, instagram_messenger_inbox).perform
|
||||
|
||||
message = instagram_messenger_inbox.messages.first
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
FactoryBot.define do
|
||||
factory :instagram_message_create_event, class: Hash do
|
||||
transient do
|
||||
ig_entry_id { SecureRandom.uuid }
|
||||
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
|
||||
end
|
||||
entry do
|
||||
[
|
||||
{
|
||||
'id': 'instagram-message-id-123',
|
||||
'id': ig_entry_id,
|
||||
'time': '2021-09-08T06:34:04+0000',
|
||||
'messaging': [
|
||||
{
|
||||
'sender': {
|
||||
'id': 'Sender-id-1'
|
||||
'id': sender_id
|
||||
},
|
||||
'recipient': {
|
||||
'id': 'chatwoot-app-user-id-1'
|
||||
@@ -27,15 +31,19 @@ FactoryBot.define do
|
||||
end
|
||||
|
||||
factory :instagram_message_standby_event, class: Hash do
|
||||
transient do
|
||||
ig_entry_id { SecureRandom.uuid }
|
||||
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
|
||||
end
|
||||
entry do
|
||||
[
|
||||
{
|
||||
'time': '2021-09-08T06:34:04+0000',
|
||||
'id': 'instagram-message-id-123',
|
||||
'id': ig_entry_id,
|
||||
'standby': [
|
||||
{
|
||||
'sender': {
|
||||
'id': 'Sender-id-1'
|
||||
'id': sender_id
|
||||
},
|
||||
'recipient': {
|
||||
'id': 'chatwoot-app-user-id-1'
|
||||
@@ -54,15 +62,19 @@ FactoryBot.define do
|
||||
end
|
||||
|
||||
factory :instagram_story_reply_event, class: Hash do
|
||||
transient do
|
||||
ig_entry_id { SecureRandom.uuid }
|
||||
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
|
||||
end
|
||||
entry do
|
||||
[
|
||||
{
|
||||
'id': 'instagram-message-id-123',
|
||||
'id': ig_entry_id,
|
||||
'time': '2021-09-08T06:34:04+0000',
|
||||
'messaging': [
|
||||
{
|
||||
'sender': {
|
||||
'id': 'Sender-id-1'
|
||||
'id': sender_id
|
||||
},
|
||||
'recipient': {
|
||||
'id': 'chatwoot-app-user-id-1'
|
||||
@@ -87,15 +99,19 @@ FactoryBot.define do
|
||||
end
|
||||
|
||||
factory :instagram_message_reply_event, class: Hash do
|
||||
transient do
|
||||
ig_entry_id { SecureRandom.uuid }
|
||||
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
|
||||
end
|
||||
entry do
|
||||
[
|
||||
{
|
||||
'id': 'instagram-message-id-123',
|
||||
'id': ig_entry_id,
|
||||
'time': '2021-09-08T06:35:04+0000',
|
||||
'messaging': [
|
||||
{
|
||||
'sender': {
|
||||
'id': 'Sender-id-1'
|
||||
'id': sender_id
|
||||
},
|
||||
'recipient': {
|
||||
'id': 'chatwoot-app-user-id-1'
|
||||
@@ -117,11 +133,14 @@ FactoryBot.define do
|
||||
end
|
||||
|
||||
factory :instagram_test_text_event, class: Hash do
|
||||
transient do
|
||||
ig_entry_id { SecureRandom.uuid }
|
||||
end
|
||||
entry do
|
||||
[
|
||||
{
|
||||
'time' => 1_661_141_837_537,
|
||||
'id' => '0',
|
||||
'id' => ig_entry_id,
|
||||
'messaging' => [
|
||||
{
|
||||
'sender' => {
|
||||
@@ -144,15 +163,19 @@ FactoryBot.define do
|
||||
end
|
||||
|
||||
factory :instagram_message_unsend_event, class: Hash do
|
||||
transient do
|
||||
ig_entry_id { SecureRandom.uuid }
|
||||
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
|
||||
end
|
||||
entry do
|
||||
[
|
||||
{
|
||||
'id': 'instagram-message-id-123',
|
||||
'id': ig_entry_id,
|
||||
'time': '2021-09-08T06:34:04+0000',
|
||||
'messaging': [
|
||||
{
|
||||
'sender': {
|
||||
'id': 'Sender-id-1'
|
||||
'id': sender_id
|
||||
},
|
||||
'recipient': {
|
||||
'id': 'chatwoot-app-user-id-1'
|
||||
@@ -171,15 +194,19 @@ FactoryBot.define do
|
||||
end
|
||||
|
||||
factory :instagram_message_attachment_event, class: Hash do
|
||||
transient do
|
||||
ig_entry_id { SecureRandom.uuid }
|
||||
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
|
||||
end
|
||||
entry do
|
||||
[
|
||||
{
|
||||
'id': 'instagram-message-id-1234',
|
||||
'id': ig_entry_id,
|
||||
'time': '2021-09-08T06:34:04+0000',
|
||||
'messaging': [
|
||||
{
|
||||
'sender': {
|
||||
'id': 'Sender-id-1'
|
||||
'id': sender_id
|
||||
},
|
||||
'recipient': {
|
||||
'id': 'chatwoot-app-user-id-1'
|
||||
@@ -205,15 +232,19 @@ FactoryBot.define do
|
||||
end
|
||||
|
||||
factory :instagram_shared_reel_event, class: Hash do
|
||||
transient do
|
||||
ig_entry_id { SecureRandom.uuid }
|
||||
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
|
||||
end
|
||||
entry do
|
||||
[
|
||||
{
|
||||
'id': 'instagram-message-id-1234',
|
||||
'id': ig_entry_id,
|
||||
'time': '2021-09-08T06:34:04+0000',
|
||||
'messaging': [
|
||||
{
|
||||
'sender': {
|
||||
'id': 'Sender-id-1'
|
||||
'id': sender_id
|
||||
},
|
||||
'recipient': {
|
||||
'id': 'chatwoot-app-user-id-1'
|
||||
@@ -241,15 +272,19 @@ FactoryBot.define do
|
||||
end
|
||||
|
||||
factory :instagram_story_mention_event, class: Hash do
|
||||
transient do
|
||||
ig_entry_id { SecureRandom.uuid }
|
||||
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
|
||||
end
|
||||
entry do
|
||||
[
|
||||
{
|
||||
'id': 'instagram-message-id-1234',
|
||||
'id': ig_entry_id,
|
||||
'time': '2021-09-08T06:34:04+0000',
|
||||
'messaging': [
|
||||
{
|
||||
'sender': {
|
||||
'id': 'Sender-id-1'
|
||||
'id': sender_id
|
||||
},
|
||||
'recipient': {
|
||||
'id': 'chatwoot-app-user-id-1'
|
||||
@@ -275,15 +310,19 @@ FactoryBot.define do
|
||||
end
|
||||
|
||||
factory :instagram_story_mention_event_with_echo, class: Hash do
|
||||
transient do
|
||||
ig_entry_id { SecureRandom.uuid }
|
||||
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
|
||||
end
|
||||
entry do
|
||||
[
|
||||
{
|
||||
'id': 'instagram-message-id-1234',
|
||||
'id': ig_entry_id,
|
||||
'time': '2021-09-08T06:34:04+0000',
|
||||
'messaging': [
|
||||
{
|
||||
'sender': {
|
||||
'id': 'Sender-id-1'
|
||||
'id': sender_id
|
||||
},
|
||||
'recipient': {
|
||||
'id': 'chatwoot-app-user-id-1'
|
||||
@@ -310,15 +349,19 @@ FactoryBot.define do
|
||||
end
|
||||
|
||||
factory :instagram_message_unsupported_event, class: Hash do
|
||||
transient do
|
||||
ig_entry_id { SecureRandom.uuid }
|
||||
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
|
||||
end
|
||||
entry do
|
||||
[
|
||||
{
|
||||
'id': 'instagram-message-unsupported-id-123',
|
||||
'id': ig_entry_id,
|
||||
'time': '2021-09-08T06:34:04+0000',
|
||||
'messaging': [
|
||||
{
|
||||
'sender': {
|
||||
'id': 'Sender-id-1'
|
||||
'id': sender_id
|
||||
},
|
||||
'recipient': {
|
||||
'id': 'chatwoot-app-user-id-1'
|
||||
@@ -337,15 +380,19 @@ FactoryBot.define do
|
||||
end
|
||||
|
||||
factory :messaging_seen_event, class: Hash do
|
||||
transient do
|
||||
ig_entry_id { SecureRandom.uuid }
|
||||
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
|
||||
end
|
||||
entry do
|
||||
[
|
||||
{
|
||||
'id': 'instagram-message-id-123',
|
||||
'id': ig_entry_id,
|
||||
'time': '2021-09-08T06:34:04+0000',
|
||||
'messaging': [
|
||||
{
|
||||
'sender': {
|
||||
'id': 'Sender-id-1'
|
||||
'id': sender_id
|
||||
},
|
||||
'recipient': {
|
||||
'id': 'chatwoot-app-user-id-1'
|
||||
|
||||
@@ -10,19 +10,10 @@ describe Webhooks::InstagramEventsJob do
|
||||
end
|
||||
|
||||
let!(:account) { create(:account) }
|
||||
let(:return_object) do
|
||||
{ name: 'Jane',
|
||||
id: 'Sender-id-1',
|
||||
account_id: instagram_messenger_inbox.account_id,
|
||||
profile_pic: 'https://chatwoot-assets.local/sample.png',
|
||||
username: 'some_user_name' }
|
||||
end
|
||||
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!(:instagram_channel) { create(:channel_instagram, account: account, instagram_id: 'chatwoot-app-user-id-1') }
|
||||
let!(:instagram_inbox) { create(:inbox, channel: instagram_channel, account: account, greeting_enabled: false) }
|
||||
|
||||
# Combined message events into one helper
|
||||
let(:message_events) do
|
||||
{
|
||||
@@ -37,6 +28,14 @@ describe Webhooks::InstagramEventsJob do
|
||||
}
|
||||
end
|
||||
|
||||
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(:fb_object) { double }
|
||||
@@ -47,8 +46,9 @@ describe Webhooks::InstagramEventsJob do
|
||||
|
||||
it 'creates incoming message in the instagram inbox' do
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
sender_id = message_events[:dm][:entry][0][:messaging][0][:sender][:id]
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
return_object.with_indifferent_access
|
||||
return_object_for(sender_id).with_indifferent_access
|
||||
)
|
||||
instagram_webhook.perform_now(message_events[:dm][:entry])
|
||||
|
||||
@@ -63,8 +63,9 @@ describe Webhooks::InstagramEventsJob do
|
||||
|
||||
it 'creates standby message in the instagram inbox' do
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
sender_id = message_events[:standby][:entry][0][:standby][0][:sender][:id]
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
return_object.with_indifferent_access
|
||||
return_object_for(sender_id).with_indifferent_access
|
||||
)
|
||||
instagram_webhook.perform_now(message_events[:standby][:entry])
|
||||
|
||||
@@ -82,10 +83,11 @@ describe Webhooks::InstagramEventsJob do
|
||||
it 'handle instagram unsend message event' do
|
||||
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)
|
||||
sender_id = message_events[:unsend][:entry][0][:messaging][0][:sender][:id]
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
{
|
||||
name: 'Jane',
|
||||
id: 'Sender-id-1',
|
||||
id: sender_id,
|
||||
account_id: instagram_messenger_inbox.account_id,
|
||||
profile_pic: 'https://chatwoot-assets.local/sample.png'
|
||||
}.with_indifferent_access
|
||||
@@ -104,8 +106,9 @@ describe Webhooks::InstagramEventsJob do
|
||||
|
||||
it 'creates incoming message with attachments in the instagram inbox' do
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
sender_id = message_events[:attachment][:entry][0][:messaging][0][:sender][:id]
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
return_object.with_indifferent_access
|
||||
return_object_for(sender_id).with_indifferent_access
|
||||
)
|
||||
instagram_webhook.perform_now(message_events[:attachment][:entry])
|
||||
|
||||
@@ -118,8 +121,9 @@ describe Webhooks::InstagramEventsJob do
|
||||
|
||||
it 'creates incoming message with attachments in the instagram inbox for story mention' do
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
sender_id = message_events[:story_mention][:entry][0][:messaging][0][:sender][:id]
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
return_object.with_indifferent_access,
|
||||
return_object_for(sender_id).with_indifferent_access,
|
||||
{ story:
|
||||
{
|
||||
mention: {
|
||||
@@ -165,8 +169,9 @@ describe Webhooks::InstagramEventsJob do
|
||||
|
||||
it 'handles unsupported message' do
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
sender_id = message_events[:unsupported][:entry][0][:messaging][0][:sender][:id]
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
return_object.with_indifferent_access
|
||||
return_object_for(sender_id).with_indifferent_access
|
||||
)
|
||||
|
||||
instagram_webhook.perform_now(message_events[:unsupported][:entry])
|
||||
@@ -184,19 +189,22 @@ describe Webhooks::InstagramEventsJob do
|
||||
before do
|
||||
instagram_channel.update(access_token: 'valid_instagram_token')
|
||||
|
||||
stub_request(:get, %r{https://graph\.instagram\.com/v22\.0/Sender-id-1\?.*})
|
||||
stub_request(:get, %r{https://graph\.instagram\.com/v22\.0/Sender-id-.*\?.*})
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: {
|
||||
name: 'Jane',
|
||||
username: 'some_user_name',
|
||||
profile_pic: 'https://chatwoot-assets.local/sample.png',
|
||||
id: 'Sender-id-1',
|
||||
follower_count: 100,
|
||||
is_user_follow_business: true,
|
||||
is_business_follow_user: true,
|
||||
is_verified_user: false
|
||||
}.to_json,
|
||||
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
|
||||
@@ -289,14 +297,15 @@ describe Webhooks::InstagramEventsJob 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)
|
||||
|
||||
sender_id = message_events[:dm][:entry][0][:messaging][0][:sender][:id]
|
||||
instagram_webhook.perform_now(message_events[:dm][:entry])
|
||||
|
||||
instagram_inbox.reload
|
||||
|
||||
expect(instagram_inbox.contacts.count).to be 1
|
||||
expect(instagram_inbox.contacts.last.name).to eq 'Unknown (IG: Sender-id-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-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
|
||||
|
||||
@@ -70,6 +70,7 @@ RSpec.configure do |config|
|
||||
config.include SlackStubs
|
||||
config.include FileUploadHelpers
|
||||
config.include CsvSpecHelpers
|
||||
config.include InstagramSpecHelpers
|
||||
config.include Devise::Test::IntegrationHelpers, type: :request
|
||||
config.include ActiveSupport::Testing::TimeHelpers
|
||||
config.include ActionCable::TestHelper
|
||||
|
||||
@@ -19,7 +19,7 @@ describe Whatsapp::OneoffCampaignService do
|
||||
'namespace' => '23423423_2342423_324234234_2343224',
|
||||
'category' => 'UTILITY',
|
||||
'language' => 'en',
|
||||
'processed_params' => { 'name' => 'John', 'ticket_id' => '2332' }
|
||||
'processed_params' => { 'body' => { 'name' => 'John', 'ticket_id' => '2332' } }
|
||||
}
|
||||
end
|
||||
|
||||
@@ -125,8 +125,13 @@ describe Whatsapp::OneoffCampaignService do
|
||||
namespace: '23423423_2342423_324234234_2343224',
|
||||
lang_code: 'en',
|
||||
parameters: array_including(
|
||||
hash_including(type: 'text', parameter_name: 'name', text: 'John'),
|
||||
hash_including(type: 'text', parameter_name: 'ticket_id', text: '2332')
|
||||
hash_including(
|
||||
type: 'body',
|
||||
parameters: array_including(
|
||||
hash_including(type: 'text', parameter_name: 'name', text: 'John'),
|
||||
hash_including(type: 'text', parameter_name: 'ticket_id', text: '2332')
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -165,19 +165,17 @@ describe Whatsapp::Providers::WhatsappCloudService do
|
||||
let(:template_body) do
|
||||
{
|
||||
messaging_product: 'whatsapp',
|
||||
recipient_type: 'individual', # Added recipient_type field
|
||||
to: '+123456789',
|
||||
type: 'template',
|
||||
template: {
|
||||
name: template_info[:name],
|
||||
language: {
|
||||
policy: 'deterministic',
|
||||
code: template_info[:lang_code]
|
||||
},
|
||||
components: [
|
||||
{ type: 'body',
|
||||
parameters: template_info[:parameters] }
|
||||
]
|
||||
},
|
||||
type: 'template'
|
||||
components: template_info[:parameters] # Changed to use parameters directly (enhanced format)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ describe Whatsapp::SendOnWhatsappService do
|
||||
namespace: '23423423_2342423_324234234_2343224',
|
||||
language: 'en_US',
|
||||
category: 'Marketing',
|
||||
processed_params: { '1' => '3' }
|
||||
processed_params: { 'body' => { '1' => '3' } }
|
||||
}
|
||||
|
||||
describe '#perform' do
|
||||
@@ -39,15 +39,16 @@ describe Whatsapp::SendOnWhatsappService do
|
||||
let(:named_template_body) do
|
||||
{
|
||||
messaging_product: 'whatsapp',
|
||||
recipient_type: 'individual',
|
||||
to: '123456789',
|
||||
type: 'template',
|
||||
template: {
|
||||
name: 'ticket_status_updated',
|
||||
language: { 'policy': 'deterministic', 'code': 'en_US' },
|
||||
components: [{ 'type': 'body',
|
||||
'parameters': [{ 'type': 'text', parameter_name: 'last_name', 'text': 'Dale' },
|
||||
{ 'type': 'text', parameter_name: 'ticket_id', 'text': '2332' }] }]
|
||||
},
|
||||
type: 'template'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
@@ -71,9 +72,33 @@ describe Whatsapp::SendOnWhatsappService do
|
||||
expect(message.reload.source_id).to eq('123456789')
|
||||
end
|
||||
|
||||
it 'marks message as failed when template name is blank' do
|
||||
processor = instance_double(Whatsapp::TemplateProcessorService)
|
||||
allow(Whatsapp::TemplateProcessorService).to receive(:new).and_return(processor)
|
||||
allow(processor).to receive(:call).and_return([nil, nil, nil, nil])
|
||||
|
||||
invalid_template_params = {
|
||||
name: '',
|
||||
namespace: 'test_namespace',
|
||||
language: 'en_US',
|
||||
category: 'UTILITY',
|
||||
processed_params: { '1' => 'test' }
|
||||
}
|
||||
|
||||
message = create(:message,
|
||||
additional_attributes: { template_params: invalid_template_params },
|
||||
conversation: conversation,
|
||||
message_type: :outgoing)
|
||||
|
||||
described_class.new(message: message).perform
|
||||
|
||||
expect(message.reload.status).to eq('failed')
|
||||
expect(message.reload.external_error).to eq('Template not found or invalid template name')
|
||||
end
|
||||
|
||||
it 'calls channel.send_template when after 24 hour limit' do
|
||||
message = create(:message, message_type: :outgoing, content: 'Your package has been shipped. It will be delivered in 3 business days.',
|
||||
conversation: conversation)
|
||||
conversation: conversation, additional_attributes: { template_params: template_params })
|
||||
|
||||
stub_request(:post, 'https://waba.360dialog.io/v1/messages')
|
||||
.with(
|
||||
@@ -107,12 +132,18 @@ describe Whatsapp::SendOnWhatsappService do
|
||||
name: 'ticket_status_updated',
|
||||
language: 'en_US',
|
||||
category: 'UTILITY',
|
||||
processed_params: { 'last_name' => 'Dale', 'ticket_id' => '2332' }
|
||||
processed_params: { 'body' => { 'last_name' => 'Dale', 'ticket_id' => '2332' } }
|
||||
}
|
||||
|
||||
stub_request(:post, "https://graph.facebook.com/v13.0/#{whatsapp_cloud_channel.provider_config['phone_number_id']}/messages")
|
||||
.with(
|
||||
:headers => { 'Content-Type' => 'application/json', 'Authorization' => "Bearer #{whatsapp_cloud_channel.provider_config['api_key']}" },
|
||||
:headers => {
|
||||
'Accept' => '*/*',
|
||||
'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
|
||||
'Content-Type' => 'application/json',
|
||||
'Authorization' => "Bearer #{whatsapp_cloud_channel.provider_config['api_key']}",
|
||||
'User-Agent' => 'Ruby'
|
||||
},
|
||||
:body => named_template_body.to_json
|
||||
).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' })
|
||||
message = create(:message,
|
||||
@@ -124,12 +155,44 @@ describe Whatsapp::SendOnWhatsappService do
|
||||
end
|
||||
|
||||
it 'calls channel.send_template when template has regexp characters' do
|
||||
message = create(
|
||||
:message,
|
||||
message_type: :outgoing,
|
||||
content: 'عميلنا العزيز الرجاء الرد على هذه الرسالة بكلمة *نعم* للرد على إستفساركم من قبل خدمة العملاء.',
|
||||
conversation: conversation
|
||||
)
|
||||
regexp_template_params = build_template_params('customer_yes_no', '2342384942_32423423_23423fdsdaf23', 'ar', {})
|
||||
arabic_content = 'عميلنا العزيز الرجاء الرد على هذه الرسالة بكلمة *نعم* للرد على إستفساركم من قبل خدمة العملاء.'
|
||||
message = create_message_with_template(arabic_content, regexp_template_params)
|
||||
stub_template_request(regexp_template_params, [])
|
||||
|
||||
described_class.new(message: message).perform
|
||||
expect(message.reload.source_id).to eq('123456789')
|
||||
end
|
||||
|
||||
it 'handles template with header parameters' do
|
||||
processed_params = {
|
||||
'body' => { '1' => '3' },
|
||||
'header' => { 'media_url' => 'https://example.com/image.jpg', 'media_type' => 'image' }
|
||||
}
|
||||
header_template_params = build_sample_template_params(processed_params)
|
||||
message = create_message_with_template('', header_template_params)
|
||||
|
||||
components = [
|
||||
{ type: 'header', parameters: [{ type: 'image', image: { link: 'https://example.com/image.jpg' } }] },
|
||||
{ type: 'body', parameters: [{ type: 'text', text: '3' }] }
|
||||
]
|
||||
stub_sample_template_request(components)
|
||||
|
||||
described_class.new(message: message).perform
|
||||
expect(message.reload.source_id).to eq('123456789')
|
||||
end
|
||||
|
||||
it 'handles empty processed_params gracefully' do
|
||||
empty_template_params = {
|
||||
name: 'sample_shipping_confirmation',
|
||||
namespace: '23423423_2342423_324234234_2343224',
|
||||
language: 'en_US',
|
||||
category: 'SHIPPING_UPDATE',
|
||||
processed_params: {}
|
||||
}
|
||||
|
||||
message = create(:message, additional_attributes: { template_params: empty_template_params },
|
||||
conversation: conversation, message_type: :outgoing)
|
||||
|
||||
stub_request(:post, 'https://waba.360dialog.io/v1/messages')
|
||||
.with(
|
||||
@@ -137,10 +200,10 @@ describe Whatsapp::SendOnWhatsappService do
|
||||
body: {
|
||||
to: '123456789',
|
||||
template: {
|
||||
name: 'customer_yes_no',
|
||||
namespace: '2342384942_32423423_23423fdsdaf23',
|
||||
language: { 'policy': 'deterministic', 'code': 'ar' },
|
||||
components: [{ 'type': 'body', 'parameters': [] }]
|
||||
name: 'sample_shipping_confirmation',
|
||||
namespace: '23423423_2342423_324234234_2343224',
|
||||
language: { 'policy': 'deterministic', 'code': 'en_US' },
|
||||
components: []
|
||||
},
|
||||
type: 'template'
|
||||
}.to_json
|
||||
@@ -149,6 +212,169 @@ describe Whatsapp::SendOnWhatsappService do
|
||||
described_class.new(message: message).perform
|
||||
expect(message.reload.source_id).to eq('123456789')
|
||||
end
|
||||
|
||||
it 'handles template with button parameters' do
|
||||
processed_params = {
|
||||
'body' => { '1' => '3' },
|
||||
'buttons' => [{ 'type' => 'url', 'parameter' => 'https://track.example.com/123' }]
|
||||
}
|
||||
button_template_params = build_sample_template_params(processed_params)
|
||||
message = create_message_with_template('', button_template_params)
|
||||
|
||||
components = [
|
||||
{ type: 'body', parameters: [{ type: 'text', text: '3' }] },
|
||||
{ type: 'button', sub_type: 'url', index: 0, parameters: [{ type: 'text', text: 'https://track.example.com/123' }] }
|
||||
]
|
||||
stub_sample_template_request(components)
|
||||
|
||||
described_class.new(message: message).perform
|
||||
expect(message.reload.source_id).to eq('123456789')
|
||||
end
|
||||
|
||||
it 'processes template parameters correctly via integration' do
|
||||
processed_params = {
|
||||
'body' => { '1' => '5' },
|
||||
'footer' => { 'text' => 'Thank you' }
|
||||
}
|
||||
complex_template_params = build_sample_template_params(processed_params)
|
||||
message = create_message_with_template('', complex_template_params)
|
||||
|
||||
components = [
|
||||
{ type: 'body', parameters: [{ type: 'text', text: '5' }] },
|
||||
{ type: 'footer', parameters: [{ type: 'text', text: 'Thank you' }] }
|
||||
]
|
||||
stub_sample_template_request(components)
|
||||
|
||||
expect { described_class.new(message: message).perform }.not_to raise_error
|
||||
expect(message.reload.source_id).to eq('123456789')
|
||||
end
|
||||
|
||||
it 'handles edge case with missing template gracefully' do
|
||||
# Test the service behavior when template is not found
|
||||
missing_template_params = {
|
||||
'name' => 'non_existent_template',
|
||||
'namespace' => 'missing_namespace',
|
||||
'language' => 'en_US',
|
||||
'category' => 'UTILITY',
|
||||
'processed_params' => { 'body' => { '1' => 'test' } }
|
||||
}
|
||||
|
||||
service = Whatsapp::TemplateProcessorService.new(
|
||||
channel: whatsapp_channel,
|
||||
template_params: missing_template_params
|
||||
)
|
||||
|
||||
expect { service.call }.not_to raise_error
|
||||
name, namespace, language, processed_params = service.call
|
||||
expect(name).to eq('non_existent_template')
|
||||
expect(namespace).to eq('missing_namespace')
|
||||
expect(language).to eq('en_US')
|
||||
expect(processed_params).to be_nil
|
||||
end
|
||||
|
||||
it 'handles template with blank parameter values correctly' do
|
||||
processed_params = {
|
||||
'body' => { '1' => '', '2' => 'valid_value', '3' => nil },
|
||||
'header' => { 'media_url' => '', 'media_type' => 'image' }
|
||||
}
|
||||
blank_values_template_params = build_sample_template_params(processed_params)
|
||||
message = create_message_with_template('', blank_values_template_params)
|
||||
|
||||
components = [{ type: 'body', parameters: [{ type: 'text', text: 'valid_value' }] }]
|
||||
stub_sample_template_request(components)
|
||||
|
||||
described_class.new(message: message).perform
|
||||
expect(message.reload.source_id).to eq('123456789')
|
||||
end
|
||||
|
||||
it 'handles nil template_params gracefully' do
|
||||
# Test service behavior when template_params is completely nil
|
||||
message = create(:message, additional_attributes: {},
|
||||
conversation: conversation, message_type: :outgoing)
|
||||
|
||||
# Should send regular message, not template
|
||||
stub_request(:post, 'https://waba.360dialog.io/v1/messages')
|
||||
.with(
|
||||
headers: headers,
|
||||
body: {
|
||||
to: '123456789',
|
||||
text: { body: message.content },
|
||||
type: 'text'
|
||||
}.to_json
|
||||
).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' })
|
||||
|
||||
expect { described_class.new(message: message).perform }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'processes template with rich text formatting' do
|
||||
processed_params = { 'body' => { '1' => '*Bold text* and _italic text_' } }
|
||||
rich_text_template_params = build_sample_template_params(processed_params)
|
||||
message = create_message_with_template('', rich_text_template_params)
|
||||
|
||||
components = [{ type: 'body', parameters: [{ type: 'text', text: '*Bold text* and _italic text_' }] }]
|
||||
stub_sample_template_request(components)
|
||||
|
||||
described_class.new(message: message).perform
|
||||
expect(message.reload.source_id).to eq('123456789')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_template_params(name, namespace, language, processed_params)
|
||||
{
|
||||
name: name,
|
||||
namespace: namespace,
|
||||
language: language,
|
||||
category: 'SHIPPING_UPDATE',
|
||||
processed_params: processed_params
|
||||
}
|
||||
end
|
||||
|
||||
def create_message_with_template(content, template_params)
|
||||
create(:message,
|
||||
message_type: :outgoing,
|
||||
content: content,
|
||||
conversation: conversation,
|
||||
additional_attributes: { template_params: template_params })
|
||||
end
|
||||
|
||||
def stub_template_request(template_params, components)
|
||||
stub_request(:post, 'https://waba.360dialog.io/v1/messages')
|
||||
.with(
|
||||
headers: headers,
|
||||
body: {
|
||||
to: '123456789',
|
||||
template: {
|
||||
name: template_params[:name],
|
||||
namespace: template_params[:namespace],
|
||||
language: { 'policy': 'deterministic', 'code': template_params[:language] },
|
||||
components: components
|
||||
},
|
||||
type: 'template'
|
||||
}.to_json
|
||||
).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' })
|
||||
end
|
||||
|
||||
def build_sample_template_params(processed_params)
|
||||
build_template_params('sample_shipping_confirmation', '23423423_2342423_324234234_2343224', 'en_US', processed_params)
|
||||
end
|
||||
|
||||
def stub_sample_template_request(components)
|
||||
stub_request(:post, 'https://waba.360dialog.io/v1/messages')
|
||||
.with(
|
||||
headers: headers,
|
||||
body: {
|
||||
to: '123456789',
|
||||
template: {
|
||||
name: 'sample_shipping_confirmation',
|
||||
namespace: '23423423_2342423_324234234_2343224',
|
||||
language: { 'policy': 'deterministic', 'code': 'en_US' },
|
||||
components: components
|
||||
},
|
||||
type: 'template'
|
||||
}.to_json
|
||||
).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Whatsapp::TemplateParameterConverterService do
|
||||
let(:template) do
|
||||
{
|
||||
'name' => 'test_template',
|
||||
'language' => 'en',
|
||||
'components' => [
|
||||
{
|
||||
'type' => 'BODY',
|
||||
'text' => 'Hello {{1}}, your order {{2}} is ready!'
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
let(:media_template) do
|
||||
{
|
||||
'name' => 'media_template',
|
||||
'language' => 'en',
|
||||
'components' => [
|
||||
{
|
||||
'type' => 'HEADER',
|
||||
'format' => 'IMAGE'
|
||||
},
|
||||
{
|
||||
'type' => 'BODY',
|
||||
'text' => 'Check out {{1}}!'
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
let(:button_template) do
|
||||
{
|
||||
'name' => 'button_template',
|
||||
'language' => 'en',
|
||||
'components' => [
|
||||
{
|
||||
'type' => 'BODY',
|
||||
'text' => 'Visit our website!'
|
||||
},
|
||||
{
|
||||
'type' => 'BUTTONS',
|
||||
'buttons' => [
|
||||
{
|
||||
'type' => 'URL',
|
||||
'url' => 'https://example.com/{{1}}'
|
||||
},
|
||||
{
|
||||
'type' => 'COPY_CODE'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
describe '#normalize_to_enhanced' do
|
||||
context 'when already enhanced format' do
|
||||
let(:enhanced_params) do
|
||||
{
|
||||
'processed_params' => {
|
||||
'body' => { '1' => 'John', '2' => 'Order123' }
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns unchanged' do
|
||||
converter = described_class.new(enhanced_params, template)
|
||||
result = converter.normalize_to_enhanced
|
||||
expect(result).to eq(enhanced_params)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when legacy array format' do
|
||||
let(:legacy_array_params) do
|
||||
{
|
||||
'processed_params' => %w[John Order123]
|
||||
}
|
||||
end
|
||||
|
||||
it 'converts to enhanced format' do
|
||||
converter = described_class.new(legacy_array_params, template)
|
||||
result = converter.normalize_to_enhanced
|
||||
|
||||
expect(result['processed_params']).to eq({
|
||||
'body' => { '1' => 'John', '2' => 'Order123' }
|
||||
})
|
||||
expect(result['format_version']).to eq('legacy')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when legacy flat hash format' do
|
||||
let(:legacy_hash_params) do
|
||||
{
|
||||
'processed_params' => { '1' => 'John', '2' => 'Order123' }
|
||||
}
|
||||
end
|
||||
|
||||
it 'converts to enhanced format' do
|
||||
converter = described_class.new(legacy_hash_params, template)
|
||||
result = converter.normalize_to_enhanced
|
||||
|
||||
expect(result['processed_params']).to eq({
|
||||
'body' => { '1' => 'John', '2' => 'Order123' }
|
||||
})
|
||||
expect(result['format_version']).to eq('legacy')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when legacy hash with all body parameters' do
|
||||
let(:legacy_hash_params) do
|
||||
{
|
||||
'processed_params' => {
|
||||
'1' => 'Product',
|
||||
'customer_name' => 'John'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'converts to enhanced format with body only' do
|
||||
converter = described_class.new(legacy_hash_params, media_template)
|
||||
result = converter.normalize_to_enhanced
|
||||
|
||||
expect(result['processed_params']).to eq({
|
||||
'body' => {
|
||||
'1' => 'Product',
|
||||
'customer_name' => 'John'
|
||||
}
|
||||
})
|
||||
expect(result['format_version']).to eq('legacy')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when invalid format' do
|
||||
let(:invalid_params) do
|
||||
{
|
||||
'processed_params' => 'invalid_string'
|
||||
}
|
||||
end
|
||||
|
||||
it 'raises ArgumentError' do
|
||||
expect do
|
||||
converter = described_class.new(invalid_params, template)
|
||||
converter.normalize_to_enhanced
|
||||
end.to raise_error(ArgumentError, /Unknown legacy format/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#enhanced_format?' do
|
||||
it 'returns true for valid enhanced format' do
|
||||
enhanced = { 'body' => { '1' => 'test' } }
|
||||
converter = described_class.new({}, template)
|
||||
expect(converter.send(:enhanced_format?, enhanced)).to be true
|
||||
end
|
||||
|
||||
it 'returns false for array' do
|
||||
converter = described_class.new({}, template)
|
||||
expect(converter.send(:enhanced_format?, ['test'])).to be false
|
||||
end
|
||||
|
||||
it 'returns false for flat hash' do
|
||||
converter = described_class.new({}, template)
|
||||
expect(converter.send(:enhanced_format?, { '1' => 'test' })).to be false
|
||||
end
|
||||
|
||||
it 'returns false for invalid structure' do
|
||||
invalid = { 'body' => 'not_a_hash' }
|
||||
converter = described_class.new({}, template)
|
||||
expect(converter.send(:enhanced_format?, invalid)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'simplified conversion methods' do
|
||||
describe '#convert_array_to_body_params' do
|
||||
it 'converts empty array' do
|
||||
converter = described_class.new({}, template)
|
||||
result = converter.send(:convert_array_to_body_params, [])
|
||||
expect(result).to eq({})
|
||||
end
|
||||
|
||||
it 'converts array to numbered body parameters' do
|
||||
converter = described_class.new({}, template)
|
||||
result = converter.send(:convert_array_to_body_params, %w[John Order123])
|
||||
expect(result).to eq({ '1' => 'John', '2' => 'Order123' })
|
||||
end
|
||||
end
|
||||
|
||||
describe '#convert_hash_to_body_params' do
|
||||
it 'converts hash to body parameters' do
|
||||
converter = described_class.new({}, template)
|
||||
result = converter.send(:convert_hash_to_body_params, { 'name' => 'John', 'order' => '123' })
|
||||
expect(result).to eq({ 'name' => 'John', 'order' => '123' })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
module InstagramSpecHelpers
|
||||
def create_instagram_contact_for_sender(sender_id, inbox)
|
||||
contact = Contact.find_by(identifier: sender_id)
|
||||
if contact.nil?
|
||||
contact = create(:contact, identifier: sender_id, name: 'Jane Dae')
|
||||
create(:contact_inbox, contact_id: contact.id, inbox_id: inbox.id, source_id: sender_id)
|
||||
end
|
||||
contact
|
||||
end
|
||||
|
||||
def instagram_user_response_object_for(sender_id, account_id)
|
||||
{
|
||||
name: 'Jane',
|
||||
id: sender_id,
|
||||
account_id: account_id,
|
||||
profile_pic: 'https://chatwoot-assets.local/sample.png',
|
||||
username: 'some_user_name'
|
||||
}
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user