* fix: update merge method to deep_merge for scheduled message metadata and adjust error handling in WhatsApp service specs * fix: update error expectation syntax in WhatsappZapiService specs * fix: update due_for_sending expectation to compare message IDs
833 lines
30 KiB
Ruby
833 lines
30 KiB
Ruby
require 'rails_helper'
|
|
|
|
describe Whatsapp::Providers::WhatsappZapiService do
|
|
subject(:service) { described_class.new(whatsapp_channel: whatsapp_channel) }
|
|
|
|
let(:whatsapp_channel) do
|
|
create(:channel_whatsapp, provider: 'zapi', validate_provider_config: false).tap do |channel|
|
|
channel.provider_config = {
|
|
'instance_id' => 'test_instance',
|
|
'token' => 'test_token',
|
|
'client_token' => 'test_client_token'
|
|
}
|
|
channel.save!
|
|
end
|
|
end
|
|
let(:message) { create(:message, inbox: whatsapp_channel.inbox, source_id: 'msg_123', content_attributes: { external_created_at: 123 }) }
|
|
|
|
let(:test_send_phone_number) { '551187654321' }
|
|
let(:api_instance_path) { "#{described_class::API_BASE_PATH}/instances/#{whatsapp_channel.provider_config['instance_id']}" }
|
|
let(:api_instance_path_with_token) { "#{api_instance_path}/token/#{whatsapp_channel.provider_config['token']}" }
|
|
|
|
def stub_headers(extra_headers = {})
|
|
{
|
|
'Accept' => '*/*',
|
|
'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
|
|
'Content-Type' => 'application/json',
|
|
'Client-Token' => 'test_client_token',
|
|
'User-Agent' => 'Ruby'
|
|
}.merge(extra_headers)
|
|
end
|
|
|
|
describe '#validate_provider_config?' do
|
|
context 'when response is successful' do
|
|
it 'returns true' do
|
|
stub_request(:get, "#{api_instance_path_with_token}/status")
|
|
.with(headers: stub_headers)
|
|
.to_return(status: 200, body: '', headers: {})
|
|
|
|
expect(service.validate_provider_config?).to be(true)
|
|
end
|
|
end
|
|
|
|
context 'when response is unsuccessful' do
|
|
it 'logs the error and returns false' do
|
|
stub_request(:get, "#{api_instance_path_with_token}/status")
|
|
.with(headers: stub_headers)
|
|
.to_return(status: 400, body: 'error message', headers: {})
|
|
|
|
allow(Rails.logger).to receive(:error)
|
|
|
|
expect(service.validate_provider_config?).to be(false)
|
|
expect(Rails.logger).to have_received(:error).with('error message')
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#setup_channel_provider' do
|
|
context 'when response is successful' do
|
|
it 'sets up the webhook and returns true' do
|
|
stub_request(:put, "#{api_instance_path_with_token}/update-every-webhooks")
|
|
.with(
|
|
headers: stub_headers,
|
|
body: {
|
|
value: whatsapp_channel.inbox.callback_webhook_url,
|
|
notifySentByMe: true
|
|
}.to_json
|
|
)
|
|
.to_return(status: 200)
|
|
|
|
expect(service.setup_channel_provider).to be(true)
|
|
end
|
|
|
|
it 'schedules QR code job when connection is blank' do
|
|
whatsapp_channel.update!(provider_connection: {})
|
|
|
|
stub_request(:put, "#{api_instance_path_with_token}/update-every-webhooks")
|
|
.with(headers: stub_headers)
|
|
.to_return(status: 200)
|
|
|
|
service.setup_channel_provider
|
|
|
|
expect(Channels::Whatsapp::ZapiQrCodeJob).to have_been_enqueued.with(whatsapp_channel)
|
|
end
|
|
|
|
it 'schedules QR code job when connection is closed' do
|
|
whatsapp_channel.update!(provider_connection: { 'connection' => 'close' })
|
|
|
|
stub_request(:put, "#{api_instance_path_with_token}/update-every-webhooks")
|
|
.with(headers: stub_headers)
|
|
.to_return(status: 200)
|
|
|
|
service.setup_channel_provider
|
|
|
|
expect(Channels::Whatsapp::ZapiQrCodeJob).to have_been_enqueued.with(whatsapp_channel)
|
|
end
|
|
|
|
it 'does not schedule QR code job when connection is not closed' do
|
|
whatsapp_channel.update!(provider_connection: { 'connection' => 'open' })
|
|
|
|
stub_request(:put, "#{api_instance_path_with_token}/update-every-webhooks")
|
|
.with(headers: stub_headers)
|
|
.to_return(status: 200)
|
|
|
|
service.setup_channel_provider
|
|
|
|
expect(Channels::Whatsapp::ZapiQrCodeJob).not_to have_been_enqueued
|
|
end
|
|
end
|
|
|
|
context 'when response is unsuccessful' do
|
|
it 'raises ProviderUnavailableError' do
|
|
stub_request(:put, "#{api_instance_path_with_token}/update-every-webhooks")
|
|
.with(headers: stub_headers)
|
|
.to_return(status: 400, body: 'error message', headers: {})
|
|
|
|
allow(Rails.logger).to receive(:error)
|
|
|
|
expect do
|
|
service.setup_channel_provider
|
|
end.to(raise_error { |error| expect(error.class.name).to eq('Whatsapp::Providers::WhatsappZapiService::ProviderUnavailableError') })
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#disconnect_channel_provider' do
|
|
context 'when response is successful' do
|
|
it 'disconnects the whatsapp connection' do
|
|
stub_request(:get, "#{api_instance_path_with_token}/disconnect")
|
|
.with(headers: stub_headers)
|
|
.to_return(status: 200)
|
|
|
|
expect(service.disconnect_channel_provider).to be(true)
|
|
end
|
|
end
|
|
|
|
context 'when response is unsuccessful' do
|
|
it 'raises ProviderUnavailableError' do
|
|
stub_request(:get, "#{api_instance_path_with_token}/disconnect")
|
|
.with(headers: stub_headers)
|
|
.to_return(status: 400, body: 'error message', headers: {})
|
|
|
|
allow(Rails.logger).to receive(:error)
|
|
|
|
expect do
|
|
service.disconnect_channel_provider
|
|
end.to(raise_error { |error| expect(error.class.name).to eq('Whatsapp::Providers::WhatsappZapiService::ProviderUnavailableError') })
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#qr_code_image' do
|
|
context 'when response indicates connected' do
|
|
it 'updates provider connection to open and returns nil' do
|
|
stub_request(:get, "#{api_instance_path_with_token}/qr-code/image")
|
|
.with(headers: stub_headers)
|
|
.to_return(status: 200, body: { connected: true }.to_json)
|
|
|
|
expect(whatsapp_channel).to receive(:update_provider_connection!).with(connection: 'open')
|
|
|
|
result = service.qr_code_image
|
|
|
|
expect(result).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'when response is successful and not connected' do
|
|
it 'returns the QR code value' do
|
|
qr_code_value = 'base64_qr_code_data'
|
|
response_body = { 'value' => qr_code_value }
|
|
stub_request(:get, "#{api_instance_path_with_token}/qr-code/image")
|
|
.with(headers: stub_headers)
|
|
.to_return(status: 200, body: response_body.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
|
|
result = service.qr_code_image
|
|
|
|
expect(result).to eq(qr_code_value)
|
|
end
|
|
end
|
|
|
|
context 'when response is unsuccessful' do
|
|
it 'logs error and returns nil' do
|
|
stub_request(:get, "#{api_instance_path_with_token}/qr-code/image")
|
|
.with(headers: stub_headers)
|
|
.to_return(status: 400, body: 'error message', headers: {})
|
|
|
|
allow(Rails.logger).to receive(:error)
|
|
|
|
result = service.qr_code_image
|
|
|
|
expect(result).to be_nil
|
|
expect(Rails.logger).to have_received(:error).with('error message')
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#read_messages' do
|
|
let(:first_message) { create(:message, source_id: 'msg_first') }
|
|
let(:second_message) { create(:message, source_id: 'msg_second') }
|
|
let(:messages) { [first_message, second_message] }
|
|
|
|
it 'enqueues a job for each message' do
|
|
service.read_messages(messages, recipient_id: "+#{test_send_phone_number}")
|
|
|
|
expect(Channels::Whatsapp::ZapiReadMessageJob).to have_been_enqueued.with(whatsapp_channel, test_send_phone_number, first_message.source_id)
|
|
expect(Channels::Whatsapp::ZapiReadMessageJob).to have_been_enqueued.with(whatsapp_channel, test_send_phone_number, second_message.source_id)
|
|
end
|
|
|
|
it 'returns true' do
|
|
result = service.read_messages(messages, recipient_id: "+#{test_send_phone_number}")
|
|
|
|
expect(result).to be(true)
|
|
end
|
|
end
|
|
|
|
describe '#send_read_message' do
|
|
let(:phone) { test_send_phone_number }
|
|
let(:message_source_id) { 'msg_123' }
|
|
|
|
context 'when response is successful' do
|
|
it 'sends a read message request to Z-API' do
|
|
stub_request(:post, "#{api_instance_path_with_token}/read-message")
|
|
.with(
|
|
headers: stub_headers,
|
|
body: { phone: phone, messageId: message_source_id }.to_json
|
|
)
|
|
.to_return(status: 200)
|
|
|
|
result = service.send_read_message(phone, message_source_id)
|
|
|
|
expect(result).to be(true)
|
|
end
|
|
end
|
|
|
|
context 'when response is unsuccessful' do
|
|
it 'logs the error and returns false' do
|
|
stub_request(:post, "#{api_instance_path_with_token}/read-message")
|
|
.with(headers: stub_headers)
|
|
.to_return(status: 400, body: 'error message')
|
|
|
|
allow(Rails.logger).to receive(:error)
|
|
|
|
result = service.send_read_message(phone, message_source_id)
|
|
|
|
expect(result).to be(false)
|
|
expect(Rails.logger).to have_received(:error).with('error message')
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#on_whatsapp' do
|
|
let(:phone_number) { '+123456789' }
|
|
|
|
context 'when response is successful' do
|
|
it 'checks if phone number exists on WhatsApp' do
|
|
response_body = { 'exists' => true, 'phone' => '123456789', 'lid' => 'some_lid' }
|
|
stub_request(:get, "#{api_instance_path_with_token}/phone-exists/123456789")
|
|
.with(headers: stub_headers)
|
|
.to_return(status: 200, body: response_body.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
|
|
result = service.on_whatsapp(phone_number)
|
|
|
|
expect(result).to eq(response_body)
|
|
end
|
|
|
|
it 'returns default response when parsed_response is nil' do
|
|
stub_request(:get, "#{api_instance_path_with_token}/phone-exists/123456789")
|
|
.with(headers: stub_headers)
|
|
.to_return(status: 200, body: '')
|
|
|
|
result = service.on_whatsapp(phone_number)
|
|
|
|
expect(result).to eq({ 'exists' => false, 'phone' => nil, 'lid' => nil })
|
|
end
|
|
end
|
|
|
|
context 'when response is unsuccessful' do
|
|
it 'raises ProviderUnavailableError' do
|
|
stub_request(:get, "#{api_instance_path_with_token}/phone-exists/123456789")
|
|
.with(headers: stub_headers)
|
|
.to_return(status: 400, body: 'error message', headers: {})
|
|
|
|
allow(Rails.logger).to receive(:error)
|
|
|
|
expect do
|
|
service.on_whatsapp(phone_number)
|
|
end.to(raise_error { |error| expect(error.class.name).to eq('Whatsapp::Providers::WhatsappZapiService::ProviderUnavailableError') })
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#delete_message' do
|
|
let(:outgoing_message) { create(:message, inbox: whatsapp_channel.inbox, source_id: 'msg_456', message_type: :outgoing) }
|
|
let(:incoming_message) { create(:message, inbox: whatsapp_channel.inbox, source_id: 'msg_789', message_type: :incoming) }
|
|
|
|
context 'when deleting an outgoing message' do
|
|
it 'sends delete request with owner true' do
|
|
stub_request(:delete, "#{api_instance_path_with_token}/messages")
|
|
.with(
|
|
headers: stub_headers,
|
|
query: { messageId: outgoing_message.source_id, phone: test_send_phone_number, owner: 'true' }
|
|
)
|
|
.to_return(status: 204, body: '{}')
|
|
|
|
result = service.delete_message("+#{test_send_phone_number}", outgoing_message)
|
|
|
|
expect(result).to be(true)
|
|
end
|
|
end
|
|
|
|
context 'when deleting an incoming message' do
|
|
it 'sends delete request with owner false' do
|
|
stub_request(:delete, "#{api_instance_path_with_token}/messages")
|
|
.with(
|
|
headers: stub_headers,
|
|
query: { messageId: incoming_message.source_id, phone: test_send_phone_number, owner: 'false' }
|
|
)
|
|
.to_return(status: 204, body: '{}')
|
|
|
|
result = service.delete_message("+#{test_send_phone_number}", incoming_message)
|
|
|
|
expect(result).to be(true)
|
|
end
|
|
end
|
|
|
|
context 'when response is unsuccessful' do
|
|
it 'raises ProviderUnavailableError and logs the error' do
|
|
stub_request(:delete, "#{api_instance_path_with_token}/messages")
|
|
.with(
|
|
headers: stub_headers,
|
|
query: { messageId: outgoing_message.source_id, phone: test_send_phone_number, owner: 'true' }
|
|
)
|
|
.to_return(status: 400, body: 'error message')
|
|
|
|
allow(Rails.logger).to receive(:error)
|
|
|
|
expect do
|
|
service.delete_message("+#{test_send_phone_number}", outgoing_message)
|
|
end.to(raise_error { |error| expect(error.class.name).to eq('Whatsapp::Providers::WhatsappZapiService::ProviderUnavailableError') })
|
|
|
|
expect(Rails.logger).to have_received(:error).with('error message')
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#send_message' do
|
|
let(:request_path) { "#{api_instance_path_with_token}/send-text" }
|
|
let(:result_body) { { 'messageId' => 'msg_123' } }
|
|
|
|
context 'when message is unsupported' do
|
|
it 'updates the message with is_unsupported attribute' do
|
|
unsupported_message = create(:message, content: nil)
|
|
|
|
result = service.send_message(test_send_phone_number, unsupported_message)
|
|
|
|
expect(unsupported_message.reload.is_unsupported).to be(true)
|
|
expect(result).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'when message is input_csat' do
|
|
before do
|
|
allow(message).to receive(:outgoing_content).and_return('Please rate us http://example.com/survey')
|
|
message.content_type = :input_csat
|
|
end
|
|
|
|
it 'sends the message with survey url' do
|
|
stub_request(:post, request_path)
|
|
.with(
|
|
headers: stub_headers,
|
|
body: {
|
|
phone: test_send_phone_number,
|
|
message: 'Please rate us http://example.com/survey'
|
|
}.to_json
|
|
)
|
|
.to_return(status: 200, body: result_body.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
|
|
result = service.send_message("+#{test_send_phone_number}", message)
|
|
|
|
expect(result).to eq('msg_123')
|
|
end
|
|
end
|
|
|
|
context 'when message is a text' do
|
|
it 'sends the text message' do
|
|
stub_request(:post, request_path)
|
|
.with(
|
|
headers: stub_headers,
|
|
body: {
|
|
phone: test_send_phone_number,
|
|
message: message.content
|
|
}.to_json
|
|
)
|
|
.to_return(status: 200, body: result_body.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
|
|
result = service.send_message("+#{test_send_phone_number}", message)
|
|
|
|
expect(result).to eq('msg_123')
|
|
end
|
|
|
|
it 'includes zapi_args when present' do
|
|
message.update!(content_attributes: { zapi_args: { delayMessage: 1000 } })
|
|
|
|
stub_request(:post, request_path)
|
|
.with(
|
|
headers: stub_headers,
|
|
body: {
|
|
phone: test_send_phone_number,
|
|
message: message.content,
|
|
delayMessage: 1000
|
|
}.to_json
|
|
)
|
|
.to_return(status: 200, body: result_body.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
|
|
result = service.send_message("+#{test_send_phone_number}", message)
|
|
|
|
expect(result).to eq('msg_123')
|
|
end
|
|
|
|
context 'when request is unsuccessful' do
|
|
it 'updates message status and raises ProviderUnavailableError' do
|
|
error_response = { 'error' => 'Failed to send message' }
|
|
stub_request(:post, request_path)
|
|
.with(
|
|
headers: stub_headers,
|
|
body: {
|
|
phone: test_send_phone_number,
|
|
message: message.content
|
|
}.to_json
|
|
)
|
|
.to_return(status: 400, body: error_response.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
|
|
allow(Rails.logger).to receive(:error)
|
|
|
|
expect do
|
|
service.send_message("+#{test_send_phone_number}", message)
|
|
end.to(raise_error { |error| expect(error.class.name).to eq('Whatsapp::Providers::WhatsappZapiService::ProviderUnavailableError') })
|
|
|
|
expect(message.reload.status).to eq('failed')
|
|
expect(message.external_error).to eq('Failed to send message')
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when message is a reply to another message' do
|
|
let(:inbox) { whatsapp_channel.inbox }
|
|
let(:account_user) { create(:account_user, account: inbox.account) }
|
|
let(:contact) { create(:contact, account: inbox.account, name: 'John Doe', phone_number: "+#{test_send_phone_number}") }
|
|
let(:conversation) do
|
|
contact_inbox = create(:contact_inbox, inbox: inbox, contact: contact, source_id: test_send_phone_number)
|
|
create(:conversation, inbox: inbox, contact_inbox: contact_inbox)
|
|
end
|
|
|
|
it 'sends text reply with messageId parameter' do
|
|
original_message = create(:message, inbox: inbox, conversation: conversation, sender: contact,
|
|
message_type: 'incoming', source_id: 'original_zapi_msg_123', content: 'Original text')
|
|
reply_message = create(:message, inbox: inbox, conversation: conversation, sender: account_user,
|
|
content: 'Reply text', content_attributes: { in_reply_to_external_id: original_message.source_id })
|
|
|
|
stub_request(:post, request_path)
|
|
.with(
|
|
headers: stub_headers,
|
|
body: {
|
|
phone: test_send_phone_number,
|
|
message: 'Reply text',
|
|
messageId: 'original_zapi_msg_123'
|
|
}.to_json
|
|
)
|
|
.to_return(status: 200, body: result_body.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
|
|
result = service.send_message("+#{test_send_phone_number}", reply_message)
|
|
|
|
expect(result).to eq('msg_123')
|
|
end
|
|
|
|
it 'sends image reply with messageId parameter' do
|
|
base64_image = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII='
|
|
buffer = "data:image/png;base64,#{base64_image}"
|
|
|
|
original_message = create(:message, inbox: inbox, conversation: conversation, sender: contact,
|
|
message_type: 'incoming', source_id: 'original_zapi_img_456', content: 'Check this')
|
|
reply_message = create(:message, inbox: inbox, conversation: conversation, sender: account_user,
|
|
content: 'Nice!', content_attributes: { in_reply_to_external_id: original_message.source_id })
|
|
reply_message.attachments.create!(account_id: reply_message.account_id, file_type: 'image',
|
|
file: { io: StringIO.new(Base64.decode64(base64_image)), filename: 'reply.png' })
|
|
|
|
stub_request(:post, "#{api_instance_path_with_token}/send-image")
|
|
.with(
|
|
headers: stub_headers,
|
|
body: {
|
|
phone: test_send_phone_number,
|
|
image: buffer,
|
|
caption: 'Nice!',
|
|
messageId: 'original_zapi_img_456'
|
|
}.to_json
|
|
)
|
|
.to_return(status: 200, body: result_body.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
|
|
result = service.send_message("+#{test_send_phone_number}", reply_message)
|
|
|
|
expect(result).to eq('msg_123')
|
|
end
|
|
|
|
it 'sends message without messageId when in_reply_to_external_id is blank' do
|
|
regular_message = create(:message, inbox: inbox, conversation: conversation, sender: account_user, content: 'Regular message')
|
|
|
|
stub_request(:post, request_path)
|
|
.with(
|
|
headers: stub_headers,
|
|
body: {
|
|
phone: test_send_phone_number,
|
|
message: 'Regular message'
|
|
}.to_json
|
|
)
|
|
.to_return(status: 200, body: result_body.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
|
|
result = service.send_message("+#{test_send_phone_number}", regular_message)
|
|
|
|
expect(result).to eq('msg_123')
|
|
end
|
|
end
|
|
|
|
context 'when message is an image file' do
|
|
let(:base64_image) { 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=' }
|
|
let(:buffer) { "data:image/png;base64,#{base64_image}" }
|
|
|
|
before do
|
|
message.attachments.create!(
|
|
account_id: message.account_id,
|
|
file_type: 'image',
|
|
file: {
|
|
io: StringIO.new(Base64.decode64(base64_image)),
|
|
filename: 'image.png'
|
|
}
|
|
)
|
|
end
|
|
|
|
it 'sends the image message' do
|
|
stub_request(:post, "#{api_instance_path_with_token}/send-image")
|
|
.with(
|
|
headers: stub_headers,
|
|
body: {
|
|
phone: test_send_phone_number,
|
|
image: buffer,
|
|
caption: message.content
|
|
}.to_json
|
|
)
|
|
.to_return(status: 200, body: result_body.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
|
|
result = service.send_message("+#{test_send_phone_number}", message)
|
|
|
|
expect(result).to eq('msg_123')
|
|
end
|
|
end
|
|
|
|
context 'when message is an audio file' do
|
|
let(:base64_audio) { 'UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=' }
|
|
|
|
before do
|
|
message.attachments.create!(
|
|
account_id: message.account_id,
|
|
file_type: 'audio',
|
|
file: {
|
|
io: StringIO.new(Base64.decode64(base64_audio)),
|
|
filename: 'audio.wav'
|
|
}
|
|
)
|
|
end
|
|
|
|
it 'sends the audio message' do
|
|
stub_request(:post, "#{api_instance_path_with_token}/send-audio")
|
|
.with(
|
|
headers: stub_headers,
|
|
body: hash_including({
|
|
phone: test_send_phone_number,
|
|
waveform: true
|
|
})
|
|
)
|
|
.to_return(status: 200, body: result_body.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
|
|
result = service.send_message("+#{test_send_phone_number}", message)
|
|
|
|
expect(result).to eq('msg_123')
|
|
end
|
|
end
|
|
|
|
context 'when message is a video file' do
|
|
let(:base64_video) { 'AAABAAIADAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' }
|
|
|
|
before do
|
|
message.attachments.create!(
|
|
account_id: message.account_id,
|
|
file_type: 'video',
|
|
file: {
|
|
io: StringIO.new(Base64.decode64(base64_video)),
|
|
filename: 'video.mp4'
|
|
}
|
|
)
|
|
end
|
|
|
|
it 'sends the video message' do
|
|
stub_request(:post, "#{api_instance_path_with_token}/send-video")
|
|
.with(
|
|
headers: stub_headers,
|
|
body: hash_including({
|
|
phone: test_send_phone_number,
|
|
caption: message.content
|
|
})
|
|
)
|
|
.to_return(status: 200, body: result_body.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
|
|
result = service.send_message("+#{test_send_phone_number}", message)
|
|
|
|
expect(result).to eq('msg_123')
|
|
end
|
|
end
|
|
|
|
context 'when message is a document file' do
|
|
let(:base64_document) { 'JVBERi0xLjQKJcOkw7zDtsOgw7MKMSAwIG9iagp' }
|
|
|
|
before do
|
|
message.attachments.create!(
|
|
account_id: message.account_id,
|
|
file_type: 'file',
|
|
file: {
|
|
io: StringIO.new(Base64.decode64(base64_document)),
|
|
filename: 'document.pdf'
|
|
}
|
|
)
|
|
end
|
|
|
|
it 'sends the document message' do
|
|
stub_request(:post, "#{api_instance_path_with_token}/send-document/pdf")
|
|
.with(
|
|
headers: stub_headers,
|
|
body: hash_including({
|
|
phone: test_send_phone_number,
|
|
fileName: 'document.pdf',
|
|
caption: message.content
|
|
})
|
|
)
|
|
.to_return(status: 200, body: result_body.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
|
|
result = service.send_message("+#{test_send_phone_number}", message)
|
|
|
|
expect(result).to eq('msg_123')
|
|
end
|
|
|
|
it 'handles missing file extension gracefully' do
|
|
message.attachments.first.file.blob.update!(filename: 'document')
|
|
|
|
stub_request(:post, "#{api_instance_path_with_token}/send-document/bin")
|
|
.with(
|
|
headers: stub_headers,
|
|
body: hash_including({
|
|
phone: test_send_phone_number,
|
|
fileName: 'document',
|
|
caption: message.content
|
|
})
|
|
)
|
|
.to_return(status: 200, body: result_body.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
|
|
allow(Rails.logger).to receive(:warn)
|
|
|
|
result = service.send_message("+#{test_send_phone_number}", message)
|
|
|
|
expect(result).to eq('msg_123')
|
|
expect(Rails.logger).to have_received(:warn).with(/Missing file extension/)
|
|
end
|
|
end
|
|
|
|
context 'when message is a reply' do
|
|
let(:conversation) { create(:conversation, inbox: whatsapp_channel.inbox) }
|
|
let(:original_message) { create(:message, inbox: whatsapp_channel.inbox, source_id: 'original_msg_123', conversation: conversation) }
|
|
let(:reply_message) do
|
|
create(:message,
|
|
inbox: whatsapp_channel.inbox,
|
|
content: 'This is a reply',
|
|
source_id: 'reply_msg_123',
|
|
conversation: conversation,
|
|
content_attributes: {
|
|
external_created_at: 123,
|
|
in_reply_to: original_message.id
|
|
})
|
|
end
|
|
|
|
it 'sends the reply message with messageId parameter' do
|
|
stub_request(:post, "#{api_instance_path_with_token}/send-text")
|
|
.with(
|
|
headers: stub_headers,
|
|
body: {
|
|
phone: test_send_phone_number,
|
|
message: reply_message.content,
|
|
messageId: original_message.source_id
|
|
}.to_json
|
|
)
|
|
.to_return(status: 200, body: result_body.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
|
|
result = service.send_message("+#{test_send_phone_number}", reply_message)
|
|
|
|
expect(result).to eq('msg_123')
|
|
end
|
|
end
|
|
|
|
context 'when message is a reaction' do
|
|
let(:conversation) { create(:conversation, inbox: whatsapp_channel.inbox) }
|
|
let(:original_message) { create(:message, inbox: whatsapp_channel.inbox, source_id: 'original_msg_456', conversation: conversation) }
|
|
let(:reaction_message) do
|
|
create(:message,
|
|
inbox: whatsapp_channel.inbox,
|
|
content: '👍',
|
|
source_id: 'reaction_msg_789',
|
|
conversation: conversation,
|
|
content_attributes: {
|
|
is_reaction: true,
|
|
external_created_at: 123,
|
|
in_reply_to: original_message.id
|
|
})
|
|
end
|
|
|
|
it 'sends the reaction message' do
|
|
stub_request(:post, "#{api_instance_path_with_token}/send-reaction")
|
|
.with(
|
|
headers: stub_headers,
|
|
body: {
|
|
phone: test_send_phone_number,
|
|
reaction: reaction_message.content,
|
|
messageId: original_message.source_id
|
|
}.to_json
|
|
)
|
|
.to_return(status: 200, body: result_body.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
|
|
result = service.send_message("+#{test_send_phone_number}", reaction_message)
|
|
|
|
expect(result).to eq('msg_123')
|
|
end
|
|
end
|
|
|
|
context 'when attachment exceeds size limits' do
|
|
it 'rejects image files larger than 5MB' do
|
|
message.attachments.create!(
|
|
account_id: message.account_id,
|
|
file_type: 'image',
|
|
file: {
|
|
io: StringIO.new('fake image content'),
|
|
filename: 'large_image.png'
|
|
}
|
|
)
|
|
|
|
attachment = message.attachments.first
|
|
allow(attachment.file).to receive(:byte_size).and_return(6.megabytes)
|
|
|
|
result = service.send_message("+#{test_send_phone_number}", message)
|
|
|
|
expect(message.reload.status).to eq('failed')
|
|
expect(message.external_error).to eq('File too large')
|
|
expect(result).to be_nil
|
|
end
|
|
|
|
it 'rejects audio files larger than 16MB' do
|
|
message.attachments.create!(
|
|
account_id: message.account_id,
|
|
file_type: 'audio',
|
|
file: {
|
|
io: StringIO.new('fake audio content'),
|
|
filename: 'large_audio.mp3'
|
|
}
|
|
)
|
|
|
|
attachment = message.attachments.first
|
|
allow(attachment.file).to receive(:byte_size).and_return(17.megabytes)
|
|
|
|
result = service.send_message("+#{test_send_phone_number}", message)
|
|
|
|
expect(message.reload.status).to eq('failed')
|
|
expect(message.external_error).to eq('File too large')
|
|
expect(result).to be_nil
|
|
end
|
|
|
|
it 'rejects video files larger than 16MB' do
|
|
message.attachments.create!(
|
|
account_id: message.account_id,
|
|
file_type: 'video',
|
|
file: {
|
|
io: StringIO.new('fake video content'),
|
|
filename: 'large_video.mp4'
|
|
}
|
|
)
|
|
|
|
attachment = message.attachments.first
|
|
allow(attachment.file).to receive(:byte_size).and_return(17.megabytes)
|
|
|
|
result = service.send_message("+#{test_send_phone_number}", message)
|
|
|
|
expect(message.reload.status).to eq('failed')
|
|
expect(message.external_error).to eq('File too large')
|
|
expect(result).to be_nil
|
|
end
|
|
|
|
it 'rejects document files larger than 100MB' do
|
|
message.attachments.create!(
|
|
account_id: message.account_id,
|
|
file_type: 'file',
|
|
file: {
|
|
io: StringIO.new('fake document content'),
|
|
filename: 'large_document.pdf'
|
|
}
|
|
)
|
|
|
|
attachment = message.attachments.first
|
|
allow(attachment.file).to receive(:byte_size).and_return(101.megabytes)
|
|
|
|
result = service.send_message("+#{test_send_phone_number}", message)
|
|
|
|
expect(message.reload.status).to eq('failed')
|
|
expect(message.external_error).to eq('File too large')
|
|
expect(result).to be_nil
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#send_template' do
|
|
it 'is not implemented yet (returns nil)' do
|
|
expect(service.send_template('+123456789', {})).to be_nil
|
|
end
|
|
end
|
|
|
|
describe '#sync_templates' do
|
|
it 'is not implemented yet (returns nil)' do
|
|
expect(service.sync_templates).to be_nil
|
|
end
|
|
end
|
|
end
|