iachat/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb
2026-01-24 23:37:21 -03:00

1218 lines
45 KiB
Ruby

require 'rails_helper'
describe Whatsapp::Providers::WhatsappBaileysService do
subject(:service) { described_class.new(whatsapp_channel: whatsapp_channel) }
let(:whatsapp_channel) { create(:channel_whatsapp, provider: 'baileys', validate_provider_config: false) }
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(:test_send_jid) { '551187654321@s.whatsapp.net' }
before do
stub_const('Whatsapp::Providers::WhatsappBaileysService::DEFAULT_CLIENT_NAME', 'chatwoot-test')
end
describe '.status' do
context 'when DEFAULT_URL or DEFAULT_API_KEY are missing' do
it 'raises ProviderUnavailableError when DEFAULT_URL is blank' do
stub_const('Whatsapp::Providers::WhatsappBaileysService::DEFAULT_URL', '')
stub_const('Whatsapp::Providers::WhatsappBaileysService::DEFAULT_API_KEY', 'test_key')
expect do
described_class.status
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError,
'Missing BAILEYS_PROVIDER_DEFAULT_URL or BAILEYS_PROVIDER_DEFAULT_API_KEY setup')
end
it 'raises ProviderUnavailableError when DEFAULT_API_KEY is blank' do
stub_const('Whatsapp::Providers::WhatsappBaileysService::DEFAULT_URL', 'http://test.com')
stub_const('Whatsapp::Providers::WhatsappBaileysService::DEFAULT_API_KEY', '')
expect do
described_class.status
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError,
'Missing BAILEYS_PROVIDER_DEFAULT_URL or BAILEYS_PROVIDER_DEFAULT_API_KEY setup')
end
it 'raises ProviderUnavailableError when both are blank' do
stub_const('Whatsapp::Providers::WhatsappBaileysService::DEFAULT_URL', nil)
stub_const('Whatsapp::Providers::WhatsappBaileysService::DEFAULT_API_KEY', nil)
expect do
described_class.status
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError,
'Missing BAILEYS_PROVIDER_DEFAULT_URL or BAILEYS_PROVIDER_DEFAULT_API_KEY setup')
end
end
context 'when DEFAULT_URL and DEFAULT_API_KEY are present' do
before do
stub_const('Whatsapp::Providers::WhatsappBaileysService::DEFAULT_URL', 'http://test.com')
stub_const('Whatsapp::Providers::WhatsappBaileysService::DEFAULT_API_KEY', 'test_key')
end
context 'when response is successful' do
it 'returns the status response with symbolized keys' do
stub_request(:get, 'http://test.com/status')
.with(headers: { 'x-api-key' => 'test_key' })
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: { packageInfo: { version: '1.0.0' } }.to_json
)
result = described_class.status
expect(result).to eq({ packageInfo: { version: '1.0.0' } })
end
end
context 'when response is unsuccessful' do
it 'logs the error and raises ProviderUnavailableError' do
stub_request(:get, 'http://test.com/status')
.with(headers: { 'x-api-key' => 'test_key' })
.to_return(
status: 500,
body: 'Internal Server Error',
headers: {}
)
allow(Rails.logger).to receive(:error)
expect do
described_class.status
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError, 'Baileys API is unavailable')
expect(Rails.logger).to have_received(:error).with('Internal Server Error')
end
end
end
end
describe '#setup_channel_provider' do
context 'when response is successful' do
it 'calls the connection endpoint' do
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
.with(
headers: stub_headers(whatsapp_channel),
body: {
clientName: 'chatwoot-test',
webhookUrl: whatsapp_channel.inbox.callback_webhook_url,
webhookVerifyToken: whatsapp_channel.provider_config['webhook_verify_token'],
includeMedia: false
}.to_json
)
.to_return(status: 200)
response = service.setup_channel_provider
expect(response).to be(true)
end
end
context 'when response is unsuccessful' do
it 'raises ProviderUnavailableError and logs the error' do
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
.with(
headers: stub_headers(whatsapp_channel),
body: {
clientName: 'chatwoot-test',
webhookUrl: whatsapp_channel.inbox.callback_webhook_url,
webhookVerifyToken: whatsapp_channel.provider_config['webhook_verify_token'],
includeMedia: false
}.to_json
)
.to_return(
status: 400,
body: 'error message',
headers: {}
)
allow(Rails.logger).to receive(:error)
expect do
service.setup_channel_provider
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
expect(Rails.logger).to have_received(:error).with('error message').twice
end
end
end
describe '#disconnect_channel_provider' do
context 'when response is successful' do
it 'disconnects the whatsapp connection' do
stub_request(:delete, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
.with(headers: stub_headers(whatsapp_channel))
.to_return(status: 200)
response = service.disconnect_channel_provider
expect(response).to be(true)
end
end
context 'when response is unsuccessful' do
it 'raises ProviderUnavailableError and logs the error' do
# Stub the failing request
stub_request(:delete, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
.with(headers: stub_headers(whatsapp_channel))
.to_return(
status: 400,
body: 'error message',
headers: {}
)
# Stub the reconnection attempt
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
.to_return(status: 200)
allow(Rails.logger).to receive(:error)
expect do
service.disconnect_channel_provider
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
expect(Rails.logger).to have_received(:error).with('error message')
end
end
end
describe '#send_message' do
let(:request_path) { "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/send-message" }
let(:result_body) { { 'data' => { 'key' => { 'id' => 'msg_123' } } } }
context 'when message is unsupported' do
it 'updates the message with content attribute is_unsupported' do
unsupported_message = create(:message, content: nil)
service.send_message(test_send_phone_number, unsupported_message)
expect(unsupported_message.is_unsupported).to be(true)
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(whatsapp_channel),
body: {
jid: test_send_jid,
messageContent: { text: 'Please rate us http://example.com/survey' }
}.to_json
)
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: result_body.to_json
)
result = service.send_message(test_send_phone_number, message)
expect(result).to eq('msg_123')
end
end
context 'when message has attachment' do
let(:base64_image) { 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=' }
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 attachment message' do
stub_request(:post, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
jid: test_send_jid,
messageContent: { fileName: 'image.png', caption: message.content, image: base64_image }
}.to_json
)
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: result_body.to_json
)
result = service.send_message(test_send_phone_number, message)
expect(result).to eq('msg_123')
end
it 'omits caption if message content is empty' do
message.update!(content: nil)
stub_request(:post, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
jid: test_send_jid,
messageContent: { fileName: 'image.png', image: base64_image }
}.to_json
)
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: result_body.to_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, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
jid: test_send_jid,
messageContent: { fileName: 'audio.wav', caption: message.content, audio: base64_audio }
}.to_json
)
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: result_body.to_json
)
result = service.send_message(test_send_phone_number, message)
expect(result).to eq('msg_123')
end
it 'sends message with ptt true if message is recorded audio' do
message.attachments.first.update!(meta: { is_recorded_audio: true })
stub_request(:post, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
jid: test_send_jid,
messageContent: { fileName: 'audio.wav', caption: message.content, audio: base64_audio, ptt: true }
}.to_json
)
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: result_body.to_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 message' do
stub_request(:post, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
jid: test_send_jid,
messageContent: { text: message.content }
}.to_json
)
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: result_body.to_json
)
result = service.send_message(test_send_phone_number, message)
expect(result).to eq('msg_123')
end
it 'updates the message external_created_at' do
stub_request(:post, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
jid: test_send_jid,
messageContent: { text: message.content }
}.to_json
)
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: { 'data' => { 'key' => { 'id' => 'msg_123' }, 'messageTimestamp' => 1_748_003_165 } }.to_json
)
service.send_message(test_send_phone_number, message)
expect(message.reload.content_attributes['external_created_at']).to eq(1_748_003_165)
end
end
context 'when message is a reaction' 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 the reaction message for outgoing message' do
message = create(:message, inbox: inbox, conversation: conversation, sender: account_user, message_type: 'outgoing', source_id: 'msg_123')
reaction = create(:message, inbox: inbox, conversation: conversation, sender: account_user, content: '👍',
content_attributes: { is_reaction: true, in_reply_to: message.id })
stub_request(:post, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
jid: test_send_jid,
messageContent: { react: { key: { id: message.source_id,
remoteJid: test_send_jid,
fromMe: true },
text: '👍' } }
}.to_json
)
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: { 'data' => { 'key' => { 'id' => 'reaction_123' } } }.to_json
)
result = service.send_message(test_send_phone_number, reaction)
expect(result).to eq('reaction_123')
end
it 'sends the reaction message for incoming message' do
message = create(:message, inbox: inbox, conversation: conversation, sender: contact, source_id: 'msg_123')
reaction = create(:message, inbox: inbox, conversation: conversation, sender: account_user, content: '👍',
content_attributes: { is_reaction: true, in_reply_to: message.id })
stub_request(:post, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
jid: test_send_jid,
messageContent: { react: { key: { id: message.source_id,
remoteJid: test_send_jid,
fromMe: false },
text: '👍' } }
}.to_json
)
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: { 'data' => { 'key' => { 'id' => 'reaction_123' } } }.to_json
)
result = service.send_message(test_send_phone_number, reaction)
expect(result).to eq('reaction_123')
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 to outgoing message with quotedMessage' do
original_message = create(:message, inbox: inbox, conversation: conversation, sender: account_user,
message_type: 'outgoing', source_id: 'original_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(whatsapp_channel),
body: {
jid: test_send_jid,
messageContent: {
text: 'Reply text',
quotedMessage: {
key: {
id: 'original_msg_123',
remoteJid: test_send_jid,
fromMe: true
},
message: { conversation: 'Original text' }
}
}
}.to_json
)
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: result_body.to_json
)
result = service.send_message(test_send_phone_number, reply_message)
expect(result).to eq('msg_123')
end
it 'sends text reply to incoming message with quotedMessage' do
original_message = create(:message, inbox: inbox, conversation: conversation, sender: contact,
message_type: 'incoming', source_id: 'incoming_msg_456', content: 'Incoming text')
reply_message = create(:message, inbox: inbox, conversation: conversation, sender: account_user,
content: 'Reply to incoming', content_attributes: { in_reply_to_external_id: original_message.source_id })
stub_request(:post, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
jid: test_send_jid,
messageContent: {
text: 'Reply to incoming',
quotedMessage: {
key: {
id: 'incoming_msg_456',
remoteJid: test_send_jid,
fromMe: false
},
message: { conversation: 'Incoming text' }
}
}
}.to_json
)
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: result_body.to_json
)
result = service.send_message(test_send_phone_number, reply_message)
expect(result).to eq('msg_123')
end
it 'sends reply to message with image attachment' do
original_message = create(:message, inbox: inbox, conversation: conversation, sender: contact,
message_type: 'incoming', source_id: 'image_msg_789', content: 'Check this image')
original_message.attachments.create!(account_id: original_message.account_id, file_type: 'image',
file: { io: StringIO.new('fake'), filename: 'image.png' })
reply_message = create(:message, inbox: inbox, conversation: conversation, sender: account_user,
content: 'Nice image!', content_attributes: { in_reply_to_external_id: original_message.source_id })
stub_request(:post, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
jid: test_send_jid,
messageContent: {
text: 'Nice image!',
quotedMessage: {
key: {
id: 'image_msg_789',
remoteJid: test_send_jid,
fromMe: false
},
message: { imageMessage: { caption: 'Check this image' } }
}
}
}.to_json
)
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: result_body.to_json
)
result = service.send_message(test_send_phone_number, reply_message)
expect(result).to eq('msg_123')
end
it 'sends message without quotedMessage 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(whatsapp_channel),
body: {
jid: test_send_jid,
messageContent: { text: 'Regular message' }
}.to_json
)
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: result_body.to_json
)
result = service.send_message(test_send_phone_number, regular_message)
expect(result).to eq('msg_123')
end
end
context 'when request is unsuccessful' do
it 'raises ProviderUnavailableError' do
stub_request(:post, request_path)
.to_return(
status: 400,
headers: { 'Content-Type' => 'application/json' },
body: result_body.to_json
)
# Stub the reconnection attempt
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
.to_return(status: 200)
expect do
service.send_message(test_send_phone_number, message)
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
end
end
end
describe '#media_url' do
it 'returns the media url' do
media_id = '12345'
expected_url = "#{whatsapp_channel.provider_config['provider_url']}/media/#{media_id}"
expect(service.media_url(media_id)).to eq(expected_url)
end
end
describe '#api_headers' do
it 'returns the headers' do
expect(service.api_headers).to eq('x-api-key' => 'test_key', 'Content-Type' => 'application/json')
end
end
describe '#validate_provider_config?' do
context 'when response is successful' do
it 'returns true' do
stub_request(:get, "#{whatsapp_channel.provider_config['provider_url']}/status/auth")
.with(headers: stub_headers(whatsapp_channel))
.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, "#{whatsapp_channel.provider_config['provider_url']}/status/auth")
.with(headers: stub_headers(whatsapp_channel))
.to_return(status: 400, body: 'error message', headers: {})
allow(Rails.logger).to receive(:error).with('error message')
expect(service.validate_provider_config?).to be(false)
expect(Rails.logger).to have_received(:error)
end
end
end
describe '#read_messages' do
it 'send read messages request' do
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/read-messages")
.with(
headers: stub_headers(whatsapp_channel),
body: { keys: [{ id: message.source_id, remoteJid: test_send_jid, fromMe: false }] }.to_json
).to_return(status: 200, body: '', headers: {})
result = service.read_messages([message], recipient_id: test_send_phone_number)
expect(result).to be(true)
end
context 'when request is unsuccessful' do
it 'raises ProviderUnavailableError' do
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/read-messages")
.with(
headers: stub_headers(whatsapp_channel),
body: { keys: [{ id: message.source_id, remoteJid: test_send_jid, fromMe: false }] }.to_json
).to_return(status: 400, body: 'error message', headers: {})
# Stub the reconnection attempt
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
.to_return(status: 200)
expect do
service.read_messages([message], recipient_id: test_send_phone_number)
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
end
end
end
describe '#unread_message' do
it 'send unread message request' do
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/chat-modify")
.with(
headers: stub_headers(whatsapp_channel),
body: {
jid: test_send_jid,
mod: {
markRead: false,
lastMessages: [
{
key: { id: 'msg_123', remoteJid: test_send_jid, fromMe: false },
messageTimestamp: 123
}
]
}
}.to_json
).to_return(status: 200)
result = service.unread_message(test_send_phone_number, message)
expect(result).to be(true)
end
context 'when request is unsuccessful' do
it 'raises ProviderUnavailableError' do
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/chat-modify")
.with(
headers: stub_headers(whatsapp_channel),
body: {
jid: test_send_jid,
mod: {
markRead: false,
lastMessages: [
{
key: { id: 'msg_123', remoteJid: test_send_jid, fromMe: false },
messageTimestamp: 123
}
]
}
}.to_json
).to_return(status: 400, body: 'error message', headers: {})
# Stub the reconnection attempt
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
.to_return(status: 200)
expect do
service.unread_message(test_send_phone_number, message)
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
end
end
end
describe '#received_messages' do
it 'send received messages request' do
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/send-receipts")
.with(
headers: stub_headers(whatsapp_channel),
body: {
keys: [{ id: message.source_id, remoteJid: test_send_jid, fromMe: false }]
}.to_json
).to_return(status: 200)
result = service.received_messages(test_send_phone_number, [message])
expect(result).to be(true)
end
context 'when request is unsuccessful' do
it 'raises ProviderUnavailableError' do
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/send-receipts")
.with(
headers: stub_headers(whatsapp_channel),
body: {
keys: [{ id: message.source_id, remoteJid: test_send_jid, fromMe: false }]
}.to_json
).to_return(status: 400, body: 'error message', headers: {})
# Stub the reconnection attempt
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
.to_return(status: 200)
expect do
service.received_messages(test_send_phone_number, [message])
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
end
end
end
describe '#toggle_typing_status' do
let(:request_path) { "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/presence" }
it 'calls presence endpoint for typing on' do
request = stub_request(:patch, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
toJid: test_send_jid,
type: 'composing'
}.to_json
)
.to_return(status: 200)
service.toggle_typing_status(Events::Types::CONVERSATION_TYPING_ON, recipient_id: test_send_phone_number)
expect(request).to have_been_requested
end
it 'calls presence endpoint for recording' do
request = stub_request(:patch, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
toJid: test_send_jid,
type: 'recording'
}.to_json
)
.to_return(status: 200)
service.toggle_typing_status(Events::Types::CONVERSATION_RECORDING, recipient_id: test_send_phone_number)
expect(request).to have_been_requested
end
it 'calls presence endpoint for typing off' do
request = stub_request(:patch, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
toJid: test_send_jid,
type: 'paused'
}.to_json
)
.to_return(status: 200)
service.toggle_typing_status(Events::Types::CONVERSATION_TYPING_OFF, recipient_id: test_send_phone_number)
expect(request).to have_been_requested
end
context 'when request is unsuccessful' do
it 'raises ProviderUnavailableError and logs the error' do
stub_request(:patch, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
toJid: test_send_jid,
type: 'composing'
}.to_json
)
.to_return(
status: 400,
body: 'error message',
headers: {}
)
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
.to_return(status: 200)
allow(Rails.logger).to receive(:error)
expect do
service.toggle_typing_status(Events::Types::CONVERSATION_TYPING_ON, recipient_id: test_send_phone_number)
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
expect(Rails.logger).to have_received(:error).with('error message')
end
end
end
describe '#update_presence' do
let(:request_path) { "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/presence" }
it 'calls presence endpoint' do
request = stub_request(:patch, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
type: 'available'
}.to_json
)
.to_return(status: 200)
service.update_presence('online')
expect(request).to have_been_requested
end
context 'when request is unsuccessful' do
it 'raises ProviderUnavailableError and logs the error' do
stub_request(:patch, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
type: 'available'
}.to_json
)
.to_return(
status: 400,
body: 'error message',
headers: {}
)
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
.to_return(status: 200)
allow(Rails.logger).to receive(:error)
expect do
service.update_presence('online')
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
expect(Rails.logger).to have_received(:error).with('error message')
end
end
end
describe '#on_whatsapp' do
let(:request_path) { "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/on-whatsapp" }
let(:phone_number) { '+123456789' }
context 'when response is successful' do
it 'requests whatsapp check' do
stub_request(:post, request_path)
.with(headers: stub_headers(whatsapp_channel), body: { jids: ["#{phone_number.delete('+')}@s.whatsapp.net"] }.to_json)
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: [{ jid: "#{phone_number.delete('+')}@s.whatsapp.net", exists: true }].to_json
)
response = service.on_whatsapp(phone_number)
expect(response).to eq({ 'jid' => "#{phone_number.delete('+')}@s.whatsapp.net", 'exists' => true })
end
it 'returns default check response' do
stub_request(:post, request_path)
.with(headers: stub_headers(whatsapp_channel), body: { jids: ["#{phone_number.delete('+')}@s.whatsapp.net"] }.to_json)
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: [].to_json
)
response = service.on_whatsapp(phone_number)
expect(response).to eq({ 'jid' => "#{phone_number.delete('+')}@s.whatsapp.net", 'exists' => false })
end
end
context 'when response is unsuccessful' do
it 'raises ProviderUnavailableError and logs the error' do
stub_request(:post, request_path)
.with(headers: stub_headers(whatsapp_channel), body: { jids: ["#{phone_number.delete('+')}@s.whatsapp.net"] }.to_json)
.to_return(
status: 400,
body: 'error message',
headers: {}
)
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
.to_return(status: 200)
allow(Rails.logger).to receive(:error)
expect do
service.on_whatsapp(phone_number)
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
expect(Rails.logger).to have_received(:error).with('error message')
end
end
end
describe '#delete_message' do
let(:request_path) { "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/messages" }
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 fromMe true' do
stub_request(:delete, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
jid: test_send_jid,
key: {
id: outgoing_message.source_id,
remoteJid: test_send_jid,
fromMe: true
}
}.to_json
)
.to_return(status: 200, 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 fromMe false' do
stub_request(:delete, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
jid: test_send_jid,
key: {
id: incoming_message.source_id,
remoteJid: test_send_jid,
fromMe: false
}
}.to_json
)
.to_return(status: 200, 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, request_path)
.with(headers: stub_headers(whatsapp_channel))
.to_return(status: 400, body: 'error message')
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
.to_return(status: 200)
allow(Rails.logger).to receive(:error)
expect do
service.delete_message(test_send_phone_number, outgoing_message)
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
expect(Rails.logger).to have_received(:error).with('error message')
end
end
end
context 'when environment variable BAILEYS_PROVIDER_DEFAULT_URL is set' do
it 'uses the base url from the environment variable' do
stub_const('Whatsapp::Providers::WhatsappBaileysService::DEFAULT_URL', 'http://test.com')
whatsapp_channel.update!(provider_config: {})
expect(service.send(:provider_url)).to eq('http://test.com')
end
end
context 'when environment variable BAILEYS_PROVIDER_DEFAULT_API_KEY is set' do
it 'uses the API key from the environment variable' do
stub_const('Whatsapp::Providers::WhatsappBaileysService::DEFAULT_API_KEY', 'key')
whatsapp_channel.update!(provider_config: {})
expect(service.send(:api_key)).to eq('key')
end
end
describe 'error handling' do
describe '#handle_channel_error' do
it 'updates provider connection to close' do
whatsapp_channel.update!(provider_connection: { 'connection' => 'open' })
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
.with(
headers: stub_headers(whatsapp_channel),
body: {
clientName: 'chatwoot-test',
webhookUrl: whatsapp_channel.inbox.callback_webhook_url,
webhookVerifyToken: whatsapp_channel.provider_config['webhook_verify_token'],
includeMedia: false
}.to_json
)
.to_return(status: 200)
service.send(:handle_channel_error)
expect(whatsapp_channel.reload.provider_connection['connection']).to eq('close')
end
it 'attempts to reconnect by calling setup_channel_provider' do
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
.with(
headers: stub_headers(whatsapp_channel),
body: {
clientName: 'chatwoot-test',
webhookUrl: whatsapp_channel.inbox.callback_webhook_url,
webhookVerifyToken: whatsapp_channel.provider_config['webhook_verify_token'],
includeMedia: false
}.to_json
)
.to_return(status: 200)
service.send(:handle_channel_error)
expect(WebMock).to have_requested(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
end
it 'logs error and does not raise when reconnection fails' do
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
.with(
headers: stub_headers(whatsapp_channel),
body: {
clientName: 'chatwoot-test',
webhookUrl: whatsapp_channel.inbox.callback_webhook_url,
webhookVerifyToken: whatsapp_channel.provider_config['webhook_verify_token'],
includeMedia: false
}.to_json
)
.to_return(status: 400, body: 'reconnection failed')
allow(Rails.logger).to receive(:error)
expect { service.send(:handle_channel_error) }.not_to raise_error
expect(Rails.logger).to have_received(:error).with(/Failed to reconnect channel after error/)
end
it 'prevents infinite loop with @handling_error flag' do
service.instance_variable_set(:@handling_error, true)
expect(HTTParty).not_to receive(:post)
service.send(:handle_channel_error)
expect(whatsapp_channel.reload.provider_connection['connection']).to eq('close')
end
end
describe 'error handling wrapper' do
context 'when send_message fails' do
it 'calls handle_channel_error and re-raises the error' do
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/send-message")
.to_return(status: 500, body: 'server error')
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
.to_return(status: 200)
whatsapp_channel.update!(provider_connection: { 'connection' => 'open' })
expect do
service.send_message(test_send_phone_number, message)
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
expect(whatsapp_channel.reload.provider_connection['connection']).to eq('close')
expect(WebMock).to have_requested(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
end
end
context 'when setup_channel_provider fails' do
it 'calls handle_channel_error and re-raises the error' do
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
.to_return(status: 500, body: 'server error')
whatsapp_channel.update!(provider_connection: { 'connection' => 'open' })
allow(Rails.logger).to receive(:error)
expect do
service.setup_channel_provider
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
expect(whatsapp_channel.reload.provider_connection['connection']).to eq('close')
expect(Rails.logger).to have_received(:error).with(/Failed to reconnect channel after error/)
end
end
context 'when toggle_typing_status fails' do
it 'calls handle_channel_error and re-raises the error' do
stub_request(:patch, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/presence")
.to_return(status: 500, body: 'server error')
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
.to_return(status: 200)
whatsapp_channel.update!(provider_connection: { 'connection' => 'open' })
expect do
service.toggle_typing_status(Events::Types::CONVERSATION_TYPING_ON, recipient_id: test_send_phone_number)
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
expect(whatsapp_channel.reload.provider_connection['connection']).to eq('close')
expect(WebMock).to have_requested(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
end
end
end
end
describe '#get_profile_pic' do
let(:test_jid) { '551187654321@s.whatsapp.net' }
context 'when response is successful' do
it 'returns the profile picture URL data' do
stub_request(:get, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/profile-picture-url")
.with(
headers: stub_headers(whatsapp_channel),
query: { jid: test_jid }
)
.to_return(
status: 200,
body: { data: { profilePictureUrl: 'https://pps.whatsapp.net/v/t61.24694-24/avatar.jpg', jid: test_jid } }.to_json
)
result = service.get_profile_pic(test_jid)
expect(result).to eq({
'data' => {
'profilePictureUrl' => 'https://pps.whatsapp.net/v/t61.24694-24/avatar.jpg',
'jid' => test_jid
}
})
end
end
context 'when response fails' do
it 'returns nil when profile picture not found (404)' do
stub_request(:get, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/profile-picture-url")
.with(
headers: stub_headers(whatsapp_channel),
query: { jid: test_jid }
)
.to_return(
status: 404,
body: { error: 'Profile picture not found' }.to_json
)
result = service.get_profile_pic(test_jid)
expect(result).to be_nil
end
end
end
def stub_headers(channel)
{
'Content-Type' => 'application/json',
'x-api-key' => channel.provider_config['api_key']
}
end
end