iachat/spec/services/whatsapp/contact_inbox_consolidation_service_spec.rb
Gabriel Jablonski ec8366aabd
fix(whatsapp): fix conversation duplication on race condition (#193)
* fix(whatsapp): update message source ID handling and improve Redis key formatting

* fix(whatsapp): prevent updating contact_inbox on identifier conflict

* fix(whatsapp): refactor conversation routing tests to use shared examples for better clarity and maintainability
2026-01-24 18:13:35 -03:00

171 lines
7.6 KiB
Ruby

require 'rails_helper'
describe Whatsapp::ContactInboxConsolidationService do
let!(:whatsapp_channel) do
create(:channel_whatsapp, provider: 'baileys', validate_provider_config: false)
end
let(:inbox) { whatsapp_channel.inbox }
let(:phone) { '5511912345678' }
let(:lid) { '12345678' }
let(:identifier) { "#{lid}@lid" }
describe '#perform' do
context 'when phone is blank' do
it 'does nothing' do
service = described_class.new(inbox: inbox, phone: nil, lid: lid, identifier: identifier)
expect { service.perform }.not_to change(ContactInbox, :count)
end
end
context 'when lid is blank' do
it 'does nothing' do
service = described_class.new(inbox: inbox, phone: phone, lid: nil, identifier: identifier)
expect { service.perform }.not_to change(ContactInbox, :count)
end
end
context 'when phone and lid are the same' do
it 'does nothing' do
contact = create(:contact, account: inbox.account, phone_number: "+#{phone}")
create(:contact_inbox, inbox: inbox, contact: contact, source_id: phone)
service = described_class.new(inbox: inbox, phone: phone, lid: phone, identifier: identifier)
expect { service.perform }.not_to change(ContactInbox, :count)
end
end
context 'when no phone-based contact_inbox exists' do
it 'does nothing' do
service = described_class.new(inbox: inbox, phone: phone, lid: lid, identifier: identifier)
expect { service.perform }.not_to change(ContactInbox, :count)
end
end
context 'when only phone-based contact_inbox exists' do
let!(:contact) { create(:contact, account: inbox.account, phone_number: "+#{phone}") }
let!(:phone_contact_inbox) { create(:contact_inbox, inbox: inbox, contact: contact, source_id: phone) }
it 'migrates the contact_inbox from phone to lid' do
service = described_class.new(inbox: inbox, phone: phone, lid: lid, identifier: identifier)
service.perform
expect(phone_contact_inbox.reload.source_id).to eq(lid)
expect(contact.reload.identifier).to eq(identifier)
expect(contact.phone_number).to eq("+#{phone}")
end
context 'when there is an identifier conflict with a different contact' do
let!(:conflicting_contact) { create(:contact, account: inbox.account, identifier: identifier) } # rubocop:disable RSpec/LetSetup
it 'does not migrate' do
service = described_class.new(inbox: inbox, phone: phone, lid: lid, identifier: identifier)
service.perform
expect(phone_contact_inbox.reload.source_id).to eq(phone)
end
end
context 'when there is a phone conflict with a different contact' do
it 'does not migrate when another contact already has this phone number' do
# Create contact without phone, then create conflicting contact with the phone
contact.update!(phone_number: nil)
create(:contact, account: inbox.account, phone_number: "+#{phone}")
service = described_class.new(inbox: inbox, phone: phone, lid: lid, identifier: identifier)
service.perform
expect(phone_contact_inbox.reload.source_id).to eq(phone)
end
end
end
context 'when both phone and lid contact_inboxes exist for the same contact' do
let!(:contact) { create(:contact, account: inbox.account, phone_number: "+#{phone}", identifier: identifier) }
let!(:lid_contact_inbox) { create(:contact_inbox, inbox: inbox, contact: contact, source_id: lid) }
let!(:phone_contact_inbox) { create(:contact_inbox, inbox: inbox, contact: contact, source_id: phone) }
it 'consolidates by moving conversations and deleting phone-based contact_inbox' do
conversation = create(:conversation, inbox: inbox, contact: contact, contact_inbox: phone_contact_inbox)
service = described_class.new(inbox: inbox, phone: phone, lid: lid, identifier: identifier)
service.perform
expect(conversation.reload.contact_inbox_id).to eq(lid_contact_inbox.id)
expect(ContactInbox.exists?(phone_contact_inbox.id)).to be(false)
expect(inbox.contact_inboxes.where(contact: contact).count).to eq(1)
end
it 'handles multiple conversations on the phone-based contact_inbox' do
conversation1 = create(:conversation, inbox: inbox, contact: contact, contact_inbox: phone_contact_inbox)
conversation2 = create(:conversation, inbox: inbox, contact: contact, contact_inbox: phone_contact_inbox)
conversation3 = create(:conversation, inbox: inbox, contact: contact, contact_inbox: lid_contact_inbox)
service = described_class.new(inbox: inbox, phone: phone, lid: lid, identifier: identifier)
service.perform
expect(conversation1.reload.contact_inbox_id).to eq(lid_contact_inbox.id)
expect(conversation2.reload.contact_inbox_id).to eq(lid_contact_inbox.id)
expect(conversation3.reload.contact_inbox_id).to eq(lid_contact_inbox.id)
expect(ContactInbox.exists?(phone_contact_inbox.id)).to be(false)
end
end
context 'when phone and lid contact_inboxes belong to different contacts' do
let!(:contact1) { create(:contact, account: inbox.account, phone_number: "+#{phone}") }
let!(:contact2) { create(:contact, account: inbox.account, identifier: identifier) }
let!(:phone_contact_inbox) { create(:contact_inbox, inbox: inbox, contact: contact1, source_id: phone) }
let!(:lid_contact_inbox) { create(:contact_inbox, inbox: inbox, contact: contact2, source_id: lid) }
it 'does not consolidate different contacts' do
service = described_class.new(inbox: inbox, phone: phone, lid: lid, identifier: identifier)
service.perform
expect(ContactInbox.exists?(phone_contact_inbox.id)).to be(true)
expect(ContactInbox.exists?(lid_contact_inbox.id)).to be(true)
expect(phone_contact_inbox.reload.source_id).to eq(phone)
end
end
context 'when contact exists by phone but has contact_inbox with different source_id' do
let!(:contact) { create(:contact, account: inbox.account, phone_number: "+#{phone}") }
let!(:old_contact_inbox) { create(:contact_inbox, inbox: inbox, contact: contact, source_id: '999999999') }
it 'updates the existing contact_inbox source_id to lid' do
service = described_class.new(inbox: inbox, phone: phone, lid: lid, identifier: identifier)
expect { service.perform }.not_to change(ContactInbox, :count)
expect(old_contact_inbox.reload.source_id).to eq(lid)
expect(contact.reload.identifier).to eq(identifier)
end
context 'when a lid contact_inbox already exists' do
let!(:lid_contact_inbox) { create(:contact_inbox, inbox: inbox, contact: contact, source_id: lid) } # rubocop:disable RSpec/LetSetup
it 'does not update to avoid duplicate' do
service = described_class.new(inbox: inbox, phone: phone, lid: lid, identifier: identifier)
service.perform
expect(old_contact_inbox.reload.source_id).to eq('999999999')
end
end
context 'when another contact already has the same identifier' do
let!(:conflicting_contact) { create(:contact, account: inbox.account, identifier: identifier) } # rubocop:disable RSpec/LetSetup
it 'does not update to avoid identifier conflict' do
service = described_class.new(inbox: inbox, phone: phone, lid: lid, identifier: identifier)
service.perform
expect(old_contact_inbox.reload.source_id).to eq('999999999')
expect(contact.reload.identifier).not_to eq(identifier)
end
end
end
end
end