require 'rails_helper' describe Whatsapp::ZapiHandlers::ReceivedCallback do let!(:whatsapp_channel) do create(:channel_whatsapp, provider: 'zapi', validate_provider_config: false) end let(:inbox) { whatsapp_channel.inbox } let(:service) { Whatsapp::IncomingMessageZapiService.new(inbox: inbox, params: params) } describe '#set_contact with LID handling' do let(:base_params) do { type: 'ReceivedCallback', messageId: 'msg_123', momment: Time.current.to_i * 1000, fromMe: false, chatName: 'John Doe', text: { message: 'Hello' } } end context 'when creating a new contact' do context 'with LID only (no phone number available)' do let(:params) do base_params.merge( phone: '123456789@lid', chatLid: '123456789@lid' ) end it 'creates contact with identifier but no phone_number' do expect do service.perform end.to change(Contact, :count).by(1) contact = Contact.last expect(contact.identifier).to eq('123456789@lid') expect(contact.phone_number).to be_nil expect(contact.name).to eq('John Doe') end it 'creates contact_inbox with correct source_id' do service.perform contact_inbox = ContactInbox.last expect(contact_inbox.source_id).to eq('123456789') end end context 'with both phone number and LID' do let(:params) do base_params.merge( phone: '5511987654321', chatLid: '123456789@lid' ) end it 'creates contact with both identifier and phone_number' do expect do service.perform end.to change(Contact, :count).by(1) contact = Contact.last expect(contact.identifier).to eq('123456789@lid') expect(contact.phone_number).to eq('+5511987654321') expect(contact.name).to eq('John Doe') end it 'creates contact_inbox with LID as source_id' do service.perform contact_inbox = ContactInbox.last expect(contact_inbox.source_id).to eq('123456789') end end context 'when phone matches chatLid (both are LID)' do let(:params) do base_params.merge( phone: '123456789@lid', chatLid: '123456789@lid' ) end it 'creates contact with identifier but no phone_number' do service.perform contact = Contact.last expect(contact.identifier).to eq('123456789@lid') expect(contact.phone_number).to be_nil end end end context 'when updating an existing contact' do context 'when contact is created with LID only, now receives actual phone number' do let!(:existing_contact) do create(:contact, account: inbox.account, identifier: '123456789@lid', phone_number: nil, name: 'John Doe') end let!(:existing_contact_inbox) do create(:contact_inbox, inbox: inbox, contact: existing_contact, source_id: '123456789') end let(:params) do base_params.merge( phone: '5511987654321', chatLid: '123456789@lid' ) end it 'reuses existing contact and does not create a new one' do expect do service.perform end.not_to change(Contact, :count) end it 'reuses existing contact_inbox' do expect do service.perform end.not_to change(ContactInbox, :count) expect(ContactInbox.last.id).to eq(existing_contact_inbox.id) end it 'updates the contact phone_number when it becomes available' do service.perform expect(existing_contact.reload.phone_number).to eq('+5511987654321') end it 'creates message for the conversation' do expect do service.perform end.to change(Message, :count).by(1) message = Message.last expect(message.sender).to eq(existing_contact) end it 'does not overwrite phone_number if contact already has one' do existing_contact.update!(phone_number: '+5511999999999') service.perform expect(existing_contact.reload.phone_number).to eq('+5511999999999') end end context 'when contact is created with phone number, now receives LID' do let!(:existing_contact) do create(:contact, account: inbox.account, identifier: nil, phone_number: '+5511987654321', name: 'John Doe') end let!(:existing_contact_inbox) do create(:contact_inbox, inbox: inbox, contact: existing_contact, source_id: '5511987654321') end let(:params) do base_params.merge( phone: '123456789@lid', chatLid: '123456789@lid' ) end it 'creates a new contact with LID identifier' do # ContactInboxWithContactBuilder will not find by source_id (different) # Will not find by identifier (nil) # Will not find by phone_number (param has no phone_number) # So it creates a new contact expect do service.perform end.to change(Contact, :count).by(1) new_contact = Contact.last expect(new_contact.identifier).to eq('123456789@lid') expect(new_contact.phone_number).to be_nil expect(new_contact).not_to eq(existing_contact) end it 'creates a new contact_inbox with new source_id' do expect do service.perform end.to change(ContactInbox, :count).by(1) new_contact_inbox = ContactInbox.last expect(new_contact_inbox.source_id).to eq('123456789') expect(new_contact_inbox).not_to eq(existing_contact_inbox) end end context 'when contact exists with identifier and no phone, receives same LID' do let!(:existing_contact) do create(:contact, account: inbox.account, identifier: '123456789@lid', phone_number: nil, name: 'John Doe') end let(:params) do base_params.merge( phone: '123456789@lid', chatLid: '123456789@lid' ) end before do create(:contact_inbox, inbox: inbox, contact: existing_contact, source_id: '123456789') end it 'reuses existing contact' do expect do service.perform end.not_to change(Contact, :count) end it 'reuses existing contact_inbox' do expect do service.perform end.not_to change(ContactInbox, :count) end end context 'when updating contact name when name is raw phone number' do let!(:existing_contact) do create(:contact, account: inbox.account, identifier: '123456789@lid', phone_number: nil, name: '5511987654321') end let(:params) do base_params.merge( phone: '5511987654321', chatLid: '123456789@lid', chatName: 'John Updated' ) end before do create(:contact_inbox, inbox: inbox, contact: existing_contact, source_id: '123456789') end it 'updates contact name when it matches raw phone' do service.perform expect(existing_contact.reload.name).to eq('John Updated') end end context 'when updating contact name when name is LID' do let!(:existing_contact) do create(:contact, account: inbox.account, identifier: '123456789@lid', phone_number: nil, name: '123456789@lid') end let(:params) do base_params.merge( phone: '123456789@lid', chatLid: '123456789@lid', chatName: 'John Updated' ) end before do create(:contact_inbox, inbox: inbox, contact: existing_contact, source_id: '123456789') end it 'updates contact name when it matches the phone value' do service.perform expect(existing_contact.reload.name).to eq('John Updated') end end end context 'when handling contact name' do let(:params_with_lid) do base_params.merge( phone: '123456789@lid', chatLid: '123456789@lid' ) end context 'when senderName is present' do let(:params) { params_with_lid.merge(senderName: 'John Sender', chatName: 'John from Chat') } it 'uses senderName as contact name (higher priority)' do service.perform contact = Contact.last expect(contact.name).to eq('John Sender') end end context 'when senderName is absent but chatName is present' do let(:params) { params_with_lid.merge(senderName: nil, chatName: 'John from Chat') } it 'uses chatName as contact name' do service.perform contact = Contact.last expect(contact.name).to eq('John from Chat') end end context 'when both senderName and chatName are absent' do let(:params) { params_with_lid.merge(senderName: nil, chatName: nil) } it 'uses phone as contact name' do service.perform contact = Contact.last expect(contact.name).to eq('123456789@lid') end end end context 'when contact exists by phone but new LID provided' do let!(:existing_contact) do create(:contact, account: inbox.account, identifier: '999999999@lid', phone_number: '+5511987654321', name: 'Existing Contact') end let(:params) do base_params.merge( phone: '5511987654321', chatLid: '123456789@lid' ) end before do create(:contact_inbox, inbox: inbox, contact: existing_contact, source_id: '999999999') end it 'finds existing contact by phone_number' do expect do service.perform end.not_to change(Contact, :count) end it 'updates existing contact_inbox with new source_id' do expect do service.perform end.not_to change(ContactInbox, :count) existing_contact_inbox = existing_contact.contact_inboxes.find_by(inbox: inbox) expect(existing_contact_inbox.source_id).to eq('123456789') end end context 'when contact_inbox exists with phone as source_id (manual creation)' do let!(:existing_contact) do create(:contact, account: inbox.account, identifier: nil, phone_number: '+5511987654321', name: 'Manually Created Contact') end let!(:existing_contact_inbox) do create(:contact_inbox, inbox: inbox, contact: existing_contact, source_id: '5511987654321') end let(:params) do base_params.merge( phone: '5511987654321', chatLid: '123456789@lid' ) end it 'updates existing contact_inbox source_id from phone to LID' do service.perform expect(existing_contact_inbox.reload.source_id).to eq('123456789') end it 'updates existing contact identifier with LID' do service.perform expect(existing_contact.reload.identifier).to eq('123456789@lid') end it 'does not create a new contact_inbox' do expect do service.perform end.not_to change(ContactInbox, :count) end it 'does not create a new contact' do expect do service.perform end.not_to change(Contact, :count) end it 'reuses the existing contact for message' do service.perform message = Message.last expect(message.sender).to eq(existing_contact) end end context 'when handling avatar' do let(:params) do base_params.merge( phone: '123456789@lid', chatLid: '123456789@lid', senderPhoto: 'https://example.com/avatar.jpg' ) end it 'enqueues avatar update job when senderPhoto is present' do expect(Avatar::AvatarFromUrlJob).to receive(:perform_later) .with(kind_of(Contact), 'https://example.com/avatar.jpg') service.perform end it 'does not enqueue avatar update job when senderPhoto is missing' do params[:senderPhoto] = nil expect(Avatar::AvatarFromUrlJob).not_to receive(:perform_later) service.perform end it 'uses photo field as fallback when senderPhoto is not present' do params[:senderPhoto] = nil params[:photo] = 'https://example.com/photo.jpg' expect(Avatar::AvatarFromUrlJob).to receive(:perform_later) .with(kind_of(Contact), 'https://example.com/photo.jpg') service.perform end it 'does not enqueue job when URL is invalid' do params[:senderPhoto] = 'not-a-url' expect(Avatar::AvatarFromUrlJob).not_to receive(:perform_later) service.perform end end end describe '#process_received_callback' do let(:contact_phone) { '+5511987654321' } let(:message_id) { 'msg_123' } let(:contact) { create(:contact, phone_number: contact_phone, account: inbox.account) } let(:params) do { type: 'ReceivedCallback', messageId: message_id, momment: Time.current.to_i * 1000, phone: '5511987654321', chatLid: '5511987654321', fromMe: false, text: { message: 'Hello World' } } end it 'creates a new message when message does not exist' do expect do service.perform end.to change(Message, :count).by(1) message = Message.last expect(message.content).to eq('Hello World') expect(message.source_id).to eq(message_id) expect(message.message_type).to eq('incoming') end it 'does not create duplicate messages' do service.perform expect do service.perform end.not_to change(Message, :count) end it 'does not process messages with notification key' do notification_params = params.merge(notification: 'REVOKE') expect do Whatsapp::IncomingMessageZapiService.new(inbox: inbox, params: notification_params).perform end.not_to change(Message, :count) end it 'handles edited messages' do service.perform original_message = Message.last edited_params = params.merge( isEdit: true, text: { message: 'Hello World - Edited' } ) Whatsapp::IncomingMessageZapiService.new(inbox: inbox, params: edited_params).perform expect(original_message.reload.content).to eq('Hello World - Edited') expect(original_message.content_attributes['is_edited']).to be(true) expect(original_message.content_attributes['previous_content']).to eq('Hello World') end it 'calls channel received_messages method for incoming messages' do allow(inbox.channel).to receive(:received_messages) service.perform message = Message.last conversation = message.conversation expect(inbox.channel).to have_received(:received_messages).with([message], conversation) end context 'when processing image message' do let(:params) do { type: 'ReceivedCallback', messageId: 'img_123', momment: Time.current.to_i * 1000, phone: '5511987654321', chatLid: '5511987654321', fromMe: false, image: { caption: 'Check this image', imageUrl: 'https://example.com/image.jpg', mimeType: 'image/jpeg' } } end before do stub_request(:get, 'https://example.com/image.jpg') .to_return(status: 200, body: 'fake image data', headers: { 'Content-Type' => 'image/jpeg' }) end it 'creates message with image attachment' do expect do service.perform end.to change(Message, :count).by(1) message = Message.last expect(message.content).to eq('Check this image') expect(message.attachments.count).to eq(1) expect(message.attachments.first.file_type).to eq('image') end end context 'when processing audio message' do let(:params) do { type: 'ReceivedCallback', messageId: 'audio_123', momment: Time.current.to_i * 1000, phone: '5511987654321', chatLid: '5511987654321', fromMe: false, audio: { audioUrl: 'https://example.com/audio.mp3', mimeType: 'audio/mpeg' } } end before do stub_request(:get, 'https://example.com/audio.mp3') .to_return(status: 200, body: 'fake audio data', headers: { 'Content-Type' => 'audio/mpeg' }) end it 'creates message with audio attachment' do expect do service.perform end.to change(Message, :count).by(1) message = Message.last expect(message.attachments.count).to eq(1) expect(message.attachments.first.file_type).to eq('audio') end end context 'when processing video message' do let(:params) do { type: 'ReceivedCallback', messageId: 'video_123', momment: Time.current.to_i * 1000, phone: '5511987654321', chatLid: '5511987654321', fromMe: false, video: { caption: 'Check this video', videoUrl: 'https://example.com/video.mp4', mimeType: 'video/mp4' } } end before do stub_request(:get, 'https://example.com/video.mp4') .to_return(status: 200, body: 'fake video data', headers: { 'Content-Type' => 'video/mp4' }) end it 'creates message with video attachment' do expect do service.perform end.to change(Message, :count).by(1) message = Message.last expect(message.content).to eq('Check this video') expect(message.attachments.count).to eq(1) expect(message.attachments.first.file_type).to eq('video') end end context 'when processing document message' do let(:params) do { type: 'ReceivedCallback', messageId: 'doc_123', momment: Time.current.to_i * 1000, phone: '5511987654321', chatLid: '5511987654321', fromMe: false, document: { documentUrl: 'https://example.com/document.pdf', fileName: 'document.pdf', mimeType: 'application/pdf' } } end before do stub_request(:get, 'https://example.com/document.pdf') .to_return(status: 200, body: 'fake pdf data', headers: { 'Content-Type' => 'application/pdf' }) end it 'creates message with document attachment' do expect do service.perform end.to change(Message, :count).by(1) message = Message.last expect(message.content).to eq('document.pdf') expect(message.attachments.count).to eq(1) expect(message.attachments.first.file_type).to eq('file') end end context 'when processing unsupported message type' do let(:params) do { type: 'ReceivedCallback', messageId: 'unsupported_123', momment: Time.current.to_i * 1000, phone: '5511987654321', chatLid: '5511987654321', fromMe: false, data: 'some unsupported data' } end it 'creates message marked as unsupported' do expect do service.perform end.to change(Message, :count).by(1) message = Message.last expect(message.content).to be_blank expect(message.is_unsupported).to be(true) end end context 'when processing reaction message' do let(:contact_inbox) { create(:contact_inbox, inbox: inbox, contact: contact, source_id: '5511987654321') } let(:conversation) { create(:conversation, inbox: inbox, contact_inbox: contact_inbox) } let(:params) do { type: 'ReceivedCallback', messageId: 'reaction_123', momment: Time.current.to_i * 1000, phone: '5511987654321', chatLid: '5511987654321', fromMe: false, reaction: { value: '👍', referencedMessage: { messageId: 'original_123' } } } end before do create(:message, inbox: inbox, conversation: conversation, source_id: 'original_123') end it 'creates reaction message' do expect do service.perform end.to change(Message, :count).by(1) message = Message.last expect(message.content).to eq('👍') expect(message.content_attributes[:is_reaction]).to be(true) expect(message.content_attributes[:in_reply_to_external_id]).to eq('original_123') end it 'creates empty reaction message' do params[:reaction][:value] = '' expect do service.perform end.to change(Message, :count).by(1) message = Message.last expect(message.content).to eq('') expect(message.content_attributes[:is_reaction]).to be(true) end end context 'when processing sticker message' do let(:params) do { type: 'ReceivedCallback', messageId: 'sticker_123', momment: Time.current.to_i * 1000, phone: '5511987654321', chatLid: '5511987654321', fromMe: false, sticker: { stickerUrl: 'https://example.com/sticker.webp', mimeType: 'image/webp' } } end before do stub_request(:get, 'https://example.com/sticker.webp') .to_return(status: 200, body: 'fake sticker data', headers: { 'Content-Type' => 'image/webp' }) end it 'creates message with sticker attachment' do expect do service.perform end.to change(Message, :count).by(1) message = Message.last expect(message.attachments.count).to eq(1) expect(message.attachments.first.file_type).to eq('image') end end context 'when processing outgoing message' do let(:params) do { type: 'ReceivedCallback', messageId: 'outgoing_123', momment: Time.current.to_i * 1000, phone: '5511987654321', chatLid: '5511987654321', fromMe: true, text: { message: 'Outgoing message' } } end before do create(:account_user, account: inbox.account) end it 'creates outgoing message' do expect do service.perform end.to change(Message, :count).by(1) message = Message.last expect(message.content).to eq('Outgoing message') expect(message.message_type).to eq('outgoing') end it 'does not call channel received_messages method for outgoing messages' do allow(inbox.channel).to receive(:received_messages) service.perform expect(inbox.channel).not_to have_received(:received_messages) end end context 'when handling duplicated events' do let(:params) do { type: 'ReceivedCallback', messageId: 'duplicate_123', momment: Time.current.to_i * 1000, phone: '5511987654321', chatLid: '5511987654321', fromMe: false, text: { message: 'Duplicated event' } } end it 'does not create message if it is already being processed' do allow(Redis::Alfred).to receive(:get) .with(format_message_source_key('duplicate_123')) .and_return(true) expect do service.perform end.not_to change(Message, :count) end it 'caches and clears message source id in Redis' do allow(Redis::Alfred).to receive(:setex) allow(Redis::Alfred).to receive(:delete) service.perform expect(Redis::Alfred).to have_received(:setex) .with(format_message_source_key('duplicate_123'), true) expect(Redis::Alfred).to have_received(:delete) .with(format_message_source_key('duplicate_123')) end end context 'when attachment download fails' do let(:params) do { type: 'ReceivedCallback', messageId: 'img_fail_123', momment: Time.current.to_i * 1000, phone: '5511987654321', chatLid: '5511987654321', fromMe: false, image: { caption: 'Failed image', imageUrl: 'https://example.com/broken.jpg', mimeType: 'image/jpeg' } } end before do allow(Down).to receive(:download).and_raise(Down::ResponseError.new('Download failed')) allow(Rails.logger).to receive(:error) end it 'creates message marked as unsupported when download fails' do expect do service.perform end.to change(Message, :count).by(1) message = Message.last expect(message.is_unsupported).to be(true) expect(Rails.logger).to have_received(:error).with(/Failed to download attachment/) end it 'handles malformed attachment URLs gracefully' do allow(Down).to receive(:download).and_raise(Down::InvalidUrl.new('Invalid URL')) expect do service.perform end.to change(Message, :count).by(1) message = Message.last expect(message.is_unsupported).to be(true) end it 'handles network timeout errors' do allow(Down).to receive(:download).and_raise(Down::TimeoutError.new('Download timeout')) expect do service.perform end.to change(Message, :count).by(1) message = Message.last expect(message.is_unsupported).to be(true) end end context 'when contact name handling' do let(:params) do { type: 'ReceivedCallback', messageId: 'name_123', momment: Time.current.to_i * 1000, phone: '5511987654322', chatLid: '5511987654322', fromMe: false, senderName: 'John Doe from Z-API', text: { message: 'Hello with name' } } end it 'creates contact with sender name when provided' do service.perform contact = Contact.last expect(contact.name).to eq('John Doe from Z-API') end it 'uses phone number as name when sender name is not provided' do params.delete(:senderName) service.perform message = Message.last expect(message.sender.name).to eq('5511987654322') end end context 'when message should not be processed' do let(:params) do { type: 'ReceivedCallback', messageId: 'filtered_123', momment: Time.current.to_i * 1000, phone: '5511987654321', chatLid: '5511987654321', fromMe: false, text: { message: 'Filtered message' } } end it 'does not process group messages' do params[:isGroup] = true expect do service.perform end.not_to change(Message, :count) end it 'does not process newsletter messages' do params[:isNewsletter] = true expect do service.perform end.not_to change(Message, :count) end it 'does not process broadcast messages' do params[:broadcast] = true expect do service.perform end.not_to change(Message, :count) end it 'does not process status reply messages' do params[:isStatusReply] = true expect do service.perform end.not_to change(Message, :count) end end context 'when processing attachment with file extensions' do let(:params) do { type: 'ReceivedCallback', messageId: 'ext_123', momment: Time.current.to_i * 1000, phone: '5511987654321', chatLid: '5511987654321', fromMe: false, document: { fileName: 'report.xlsx', documentUrl: 'https://example.com/report.xlsx', mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' } } end before do stub_request(:get, 'https://example.com/report.xlsx') .to_return(status: 200, body: 'fake excel data', headers: { 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) end it 'preserves original filename and extension' do service.perform message = Message.last attachment = message.attachments.first expect(attachment.file.filename.to_s).to eq('report.xlsx') expect(attachment.file.content_type).to eq('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') end end context 'when processing contact message' do context 'with single phone number' do let(:params) do { type: 'ReceivedCallback', messageId: 'contact_123', momment: Time.current.to_i * 1000, phone: '1234567890', chatLid: '1234567890@lid', fromMe: false, contact: { displayName: 'Test Contact Name', vCard: "BEGIN:VCARD\nVERSION:3.0\nFN:Test Contact Name\nTEL;type=CELL:+1111111111\nEND:VCARD", phones: ['1111111111'] } } end it 'creates a message with contact attachment' do expect do service.perform end.to change(Message, :count).by(1) message = Message.last expect(message.content).to eq('Test Contact Name') expect(message.attachments.count).to eq(1) end it 'creates contact attachment with correct file_type' do service.perform attachment = Message.last.attachments.first expect(attachment.file_type).to eq('contact') end it 'stores phone number in fallback_title' do service.perform attachment = Message.last.attachments.first expect(attachment.fallback_title).to eq('1111111111') end it 'extracts firstName and lastName from displayName' do service.perform attachment = Message.last.attachments.first expect(attachment.meta['firstName']).to eq('Test') expect(attachment.meta['lastName']).to eq('Contact Name') end end context 'with multiple phone numbers' do let(:params) do { type: 'ReceivedCallback', messageId: 'contact_456', momment: Time.current.to_i * 1000, phone: '1234567890', chatLid: '1234567890@lid', fromMe: false, contact: { displayName: 'Sample User', vCard: "BEGIN:VCARD\nVERSION:3.0\nFN:Sample User\nTEL;type=CELL:+2222222222\nTEL;type=WORK:+3333333333\nEND:VCARD", phones: %w[2222222222 3333333333] } } end it 'creates a message for each phone number' do expect do service.perform end.to change(Message, :count).by(2) messages = Message.last(2) expect(messages.all? { |m| m.content == 'Sample User' }).to be(true) end it 'creates an attachment for each phone number' do service.perform messages = Message.last(2) expect(messages[0].attachments.count).to eq(1) expect(messages[1].attachments.count).to eq(1) end it 'stores different phone numbers in fallback_title' do service.perform attachments = Message.last(2).map { |m| m.attachments.first } expect(attachments[0].fallback_title).to eq('2222222222') expect(attachments[1].fallback_title).to eq('3333333333') end it 'uses the same source_id for all messages' do service.perform messages = Message.last(2) expect(messages.map(&:source_id).uniq).to eq(['contact_456']) end end context 'with no phone numbers' do let(:params) do { type: 'ReceivedCallback', messageId: 'contact_789', momment: Time.current.to_i * 1000, phone: '1234567890', chatLid: '1234567890@lid', fromMe: false, contact: { displayName: 'Test User', vCard: "BEGIN:VCARD\nVERSION:3.0\nFN:Test User\nEND:VCARD", phones: [] } } end it 'creates message with fallback text' do expect do service.perform end.to change(Message, :count).by(1) message = Message.last expect(message.content).to eq('Test User') end it 'creates attachment with "Phone number is not available" as fallback_title' do service.perform attachment = Message.last.attachments.first expect(attachment.fallback_title).to eq('Phone number is not available') end end context 'with single-word name' do let(:params) do { type: 'ReceivedCallback', messageId: 'contact_single', momment: Time.current.to_i * 1000, phone: '1234567890', chatLid: '1234567890@lid', fromMe: false, contact: { displayName: 'SingleName', vCard: "BEGIN:VCARD\nVERSION:3.0\nFN:SingleName\nTEL:+4444444444\nEND:VCARD", phones: ['4444444444'] } } end it 'extracts firstName only when no last name' do service.perform attachment = Message.last.attachments.first expect(attachment.meta['firstName']).to eq('SingleName') expect(attachment.meta.key?('lastName')).to be(false) end end context 'when contact message comes from user (outgoing)' do let(:params) do { type: 'ReceivedCallback', messageId: 'contact_outgoing', momment: Time.current.to_i * 1000, phone: '1234567890', chatLid: '1234567890@lid', fromMe: true, contact: { displayName: 'Outgoing Contact', vCard: "BEGIN:VCARD\nVERSION:3.0\nFN:Outgoing Contact\nTEL:+5555555555\nEND:VCARD", phones: ['5555555555'] } } end before do create(:account_user, account: inbox.account) end it 'creates outgoing message' do service.perform message = Message.last expect(message.message_type).to eq('outgoing') expect(message.sender_type).to eq('User') end it 'creates contact attachment for outgoing message' do service.perform message = Message.last expect(message.attachments.count).to eq(1) expect(message.attachments.first.file_type).to eq('contact') end end end end describe '#process_received_callback with locking' do let(:params) do { type: 'ReceivedCallback', messageId: 'msg_123', momment: Time.current.to_i * 1000, fromMe: false, chatName: 'John Doe', text: { message: 'Hello' }, phone: '5511987654321', chatLid: '123456789@lid' } end it 'acquires a lock on the contact phone number' do allow(Redis::Alfred).to receive(:set).and_return(true) allow(Redis::Alfred).to receive(:delete) service.perform expect(Redis::Alfred).to have_received(:set).with('ZAPI::CONTACT_LOCK::5511987654321', 1, nx: true, ex: 5.seconds) expect(Redis::Alfred).to have_received(:delete).with('ZAPI::CONTACT_LOCK::5511987654321') end it 'waits for the lock if it is already acquired' do allow(Redis::Alfred).to receive(:set).with('ZAPI::CONTACT_LOCK::5511987654321', 1, nx: true, ex: 5.seconds).and_return(false, true) allow(Redis::Alfred).to receive(:delete) allow(service).to receive(:sleep).with(0.1) service.perform expect(service).to have_received(:sleep).with(0.1).once end end private def format_message_source_key(message_id) format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: "#{inbox.id}_#{message_id}") end end