Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Mailbox::ConversationFinder do
|
||||
let(:account) { create(:account) }
|
||||
let(:email_channel) { create(:channel_email, email: 'test@example.com', account: account) }
|
||||
let(:conversation) { create(:conversation, inbox: email_channel.inbox, account: account) }
|
||||
let(:mail) { Mail.new }
|
||||
|
||||
describe '#find' do
|
||||
context 'when receiver uuid strategy finds conversation' do
|
||||
before do
|
||||
conversation.update!(uuid: '12345678-1234-1234-1234-123456789012')
|
||||
mail.to = 'reply+12345678-1234-1234-1234-123456789012@example.com'
|
||||
end
|
||||
|
||||
it 'returns the conversation' do
|
||||
finder = described_class.new(mail)
|
||||
expect(finder.find).to eq(conversation)
|
||||
end
|
||||
|
||||
it 'logs which strategy succeeded' do
|
||||
allow(Rails.logger).to receive(:info)
|
||||
finder = described_class.new(mail)
|
||||
finder.find
|
||||
|
||||
expect(Rails.logger).to have_received(:info).with('Conversation found via receiver_uuid_strategy strategy')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when in_reply_to strategy finds conversation' do
|
||||
before do
|
||||
conversation.update!(uuid: '12345678-1234-1234-1234-123456789012')
|
||||
mail.in_reply_to = 'conversation/12345678-1234-1234-1234-123456789012/messages/123@example.com'
|
||||
end
|
||||
|
||||
it 'returns the conversation' do
|
||||
finder = described_class.new(mail)
|
||||
expect(finder.find).to eq(conversation)
|
||||
end
|
||||
|
||||
it 'logs which strategy succeeded' do
|
||||
allow(Rails.logger).to receive(:info)
|
||||
finder = described_class.new(mail)
|
||||
finder.find
|
||||
|
||||
expect(Rails.logger).to have_received(:info).with('Conversation found via in_reply_to_strategy strategy')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when references strategy finds conversation' do
|
||||
before do
|
||||
conversation.update!(uuid: '12345678-1234-1234-1234-123456789012')
|
||||
mail.to = 'test@example.com'
|
||||
mail.references = 'conversation/12345678-1234-1234-1234-123456789012/messages/123@example.com'
|
||||
end
|
||||
|
||||
it 'returns the conversation' do
|
||||
finder = described_class.new(mail)
|
||||
expect(finder.find).to eq(conversation)
|
||||
end
|
||||
|
||||
it 'logs which strategy succeeded' do
|
||||
allow(Rails.logger).to receive(:info)
|
||||
finder = described_class.new(mail)
|
||||
finder.find
|
||||
|
||||
expect(Rails.logger).to have_received(:info).with('Conversation found via references_strategy strategy')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no strategy finds conversation' do
|
||||
# With NewConversationStrategy in default strategies, this scenario only happens
|
||||
# when using custom strategies that exclude NewConversationStrategy
|
||||
let(:finding_strategies) do
|
||||
[
|
||||
Mailbox::ConversationFinderStrategies::ReceiverUuidStrategy,
|
||||
Mailbox::ConversationFinderStrategies::InReplyToStrategy,
|
||||
Mailbox::ConversationFinderStrategies::ReferencesStrategy
|
||||
]
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
finder = described_class.new(mail, strategies: finding_strategies)
|
||||
expect(finder.find).to be_nil
|
||||
end
|
||||
|
||||
it 'logs that no conversation was found' do
|
||||
allow(Rails.logger).to receive(:error)
|
||||
finder = described_class.new(mail, strategies: finding_strategies)
|
||||
finder.find
|
||||
|
||||
expect(Rails.logger).to have_received(:error).with('No conversation found via any strategy (NewConversationStrategy missing?)')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with custom strategies' do
|
||||
let(:custom_strategy_class) do
|
||||
Class.new(Mailbox::ConversationFinderStrategies::BaseStrategy) do
|
||||
def find
|
||||
# Always return nil for testing
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'uses provided strategies instead of defaults' do
|
||||
finder = described_class.new(mail, strategies: [custom_strategy_class])
|
||||
expect(finder.find).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with strategy execution order' do
|
||||
before do
|
||||
conversation.update!(uuid: '12345678-1234-1234-1234-123456789012')
|
||||
|
||||
# Set up mail so all strategies could match
|
||||
mail.to = 'reply+12345678-1234-1234-1234-123456789012@example.com'
|
||||
mail.in_reply_to = 'conversation/12345678-1234-1234-1234-123456789012/messages/123@example.com'
|
||||
mail.references = 'conversation/12345678-1234-1234-1234-123456789012/messages/456@example.com'
|
||||
end
|
||||
|
||||
it 'returns conversation from first matching strategy' do
|
||||
allow(Rails.logger).to receive(:info)
|
||||
finder = described_class.new(mail)
|
||||
result = finder.find
|
||||
|
||||
expect(result).to eq(conversation)
|
||||
# Should only log the first strategy that succeeded (ReceiverUuidStrategy)
|
||||
expect(Rails.logger).to have_received(:info).once.with('Conversation found via receiver_uuid_strategy strategy')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,118 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Mailbox::ConversationFinderStrategies::InReplyToStrategy do
|
||||
let(:account) { create(:account) }
|
||||
let(:email_channel) { create(:channel_email, email: 'test@example.com', account: account) }
|
||||
let(:conversation) { create(:conversation, inbox: email_channel.inbox, account: account) }
|
||||
let(:mail) { Mail.new }
|
||||
|
||||
describe '#find' do
|
||||
context 'when in_reply_to has message-specific pattern' do
|
||||
before do
|
||||
conversation.update!(uuid: '12345678-1234-1234-1234-123456789012')
|
||||
mail.in_reply_to = 'conversation/12345678-1234-1234-1234-123456789012/messages/123@example.com'
|
||||
end
|
||||
|
||||
it 'extracts UUID and returns conversation' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to eq(conversation)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when in_reply_to has conversation fallback pattern' do
|
||||
before do
|
||||
conversation.update!(uuid: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee')
|
||||
mail.in_reply_to = "account/#{account.id}/conversation/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee@example.com"
|
||||
end
|
||||
|
||||
it 'extracts UUID and returns conversation' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to eq(conversation)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when in_reply_to matches message source_id' do
|
||||
let(:message) do
|
||||
conversation.messages.create!(
|
||||
source_id: 'original-message-id@example.com',
|
||||
account_id: account.id,
|
||||
message_type: 'outgoing',
|
||||
inbox_id: email_channel.inbox.id,
|
||||
content: 'Original message'
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
message # Create the message
|
||||
mail.in_reply_to = 'original-message-id@example.com'
|
||||
end
|
||||
|
||||
it 'finds conversation from message source_id' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to eq(conversation)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when in_reply_to has multiple values' do
|
||||
let(:message) do
|
||||
conversation.messages.create!(
|
||||
source_id: 'message-123@example.com',
|
||||
account_id: account.id,
|
||||
message_type: 'outgoing',
|
||||
inbox_id: email_channel.inbox.id,
|
||||
content: 'Test message'
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
message # Create the message
|
||||
mail.in_reply_to = ['some-other-id@example.com', 'message-123@example.com']
|
||||
end
|
||||
|
||||
it 'finds conversation from any in_reply_to value' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to eq(conversation)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when in_reply_to is blank' do
|
||||
it 'returns nil' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when in_reply_to does not match any pattern or source_id' do
|
||||
before do
|
||||
mail.in_reply_to = 'random-message-id@gmail.com'
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when UUID exists but conversation does not' do
|
||||
before do
|
||||
mail.in_reply_to = 'conversation/99999999-9999-9999-9999-999999999999/messages/123@example.com'
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with malformed in_reply_to pattern' do
|
||||
before do
|
||||
mail.in_reply_to = 'conversation/not-a-uuid/messages/123@example.com'
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,161 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Mailbox::ConversationFinderStrategies::NewConversationStrategy do
|
||||
let(:account) { create(:account) }
|
||||
let(:email_channel) { create(:channel_email, account: account) }
|
||||
let(:mail) { Mail.new }
|
||||
|
||||
before do
|
||||
mail.to = [email_channel.email]
|
||||
mail.from = 'sender@example.com'
|
||||
mail.subject = 'Test Subject'
|
||||
mail.message_id = '<test@example.com>'
|
||||
end
|
||||
|
||||
describe '#find' do
|
||||
context 'when channel is found' do
|
||||
context 'with new contact' do
|
||||
it 'builds a new conversation with new contact' do
|
||||
strategy = described_class.new(mail)
|
||||
|
||||
expect do
|
||||
conversation = strategy.find
|
||||
expect(conversation).to be_a(Conversation)
|
||||
expect(conversation.new_record?).to be(true) # Not persisted yet
|
||||
expect(conversation.inbox).to eq(email_channel.inbox)
|
||||
expect(conversation.account).to eq(account)
|
||||
end.to not_change(Conversation, :count) # No conversation created yet
|
||||
.and change(Contact, :count).by(1) # Contact is created
|
||||
.and change(ContactInbox, :count).by(1)
|
||||
end
|
||||
|
||||
it 'sets conversation attributes correctly' do
|
||||
strategy = described_class.new(mail)
|
||||
conversation = strategy.find
|
||||
|
||||
expect(conversation.additional_attributes['source']).to eq('email')
|
||||
expect(conversation.additional_attributes['mail_subject']).to eq('Test Subject')
|
||||
expect(conversation.additional_attributes['initiated_at']).to have_key('timestamp')
|
||||
end
|
||||
|
||||
it 'sets contact attributes correctly' do
|
||||
strategy = described_class.new(mail)
|
||||
conversation = strategy.find
|
||||
|
||||
expect(conversation.contact.email).to eq('sender@example.com')
|
||||
expect(conversation.contact.name).to eq('sender')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with existing contact' do
|
||||
let!(:existing_contact) { create(:contact, email: 'sender@example.com', account: account) }
|
||||
|
||||
before do
|
||||
create(:contact_inbox, contact: existing_contact, inbox: email_channel.inbox)
|
||||
end
|
||||
|
||||
it 'builds conversation with existing contact' do
|
||||
strategy = described_class.new(mail)
|
||||
|
||||
expect do
|
||||
conversation = strategy.find
|
||||
expect(conversation).to be_a(Conversation)
|
||||
expect(conversation.new_record?).to be(true) # Not persisted yet
|
||||
expect(conversation.contact).to eq(existing_contact)
|
||||
end.to not_change(Conversation, :count) # No conversation created yet
|
||||
.and not_change(Contact, :count)
|
||||
.and not_change(ContactInbox, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when mail has In-Reply-To header' do
|
||||
before do
|
||||
mail['In-Reply-To'] = '<previous-message@example.com>'
|
||||
end
|
||||
|
||||
it 'stores in_reply_to in additional_attributes' do
|
||||
strategy = described_class.new(mail)
|
||||
conversation = strategy.find
|
||||
|
||||
expect(conversation.additional_attributes['in_reply_to']).to eq('<previous-message@example.com>')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when mail is auto reply' do
|
||||
before do
|
||||
mail['X-Autoreply'] = 'yes'
|
||||
end
|
||||
|
||||
it 'marks conversation as auto_reply' do
|
||||
strategy = described_class.new(mail)
|
||||
conversation = strategy.find
|
||||
|
||||
expect(conversation.additional_attributes['auto_reply']).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sender has name in From header' do
|
||||
before do
|
||||
mail.from = 'John Doe <john@example.com>'
|
||||
end
|
||||
|
||||
it 'uses sender name from mail' do
|
||||
strategy = described_class.new(mail)
|
||||
conversation = strategy.find
|
||||
|
||||
expect(conversation.contact.name).to eq('John Doe')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when channel is not found' do
|
||||
before do
|
||||
mail.to = ['nonexistent@example.com']
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
strategy = described_class.new(mail)
|
||||
|
||||
expect do
|
||||
result = strategy.find
|
||||
expect(result).to be_nil
|
||||
end.not_to change(Conversation, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when contact creation fails' do
|
||||
before do
|
||||
builder = instance_double(ContactInboxWithContactBuilder)
|
||||
allow(ContactInboxWithContactBuilder).to receive(:new).and_return(builder)
|
||||
allow(builder).to receive(:perform).and_raise(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
|
||||
it 'rolls back the transaction' do
|
||||
strategy = described_class.new(mail)
|
||||
|
||||
expect do
|
||||
strategy.find
|
||||
end.to raise_error(ActiveRecord::RecordInvalid)
|
||||
.and not_change(Conversation, :count)
|
||||
.and not_change(Contact, :count)
|
||||
.and not_change(ContactInbox, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation creation fails' do
|
||||
before do
|
||||
# Make conversation build fail with invalid attributes
|
||||
allow(Conversation).to receive(:new).and_return(Conversation.new)
|
||||
end
|
||||
|
||||
it 'returns invalid conversation object' do
|
||||
strategy = described_class.new(mail)
|
||||
|
||||
conversation = strategy.find
|
||||
expect(conversation).to be_a(Conversation)
|
||||
expect(conversation.new_record?).to be(true)
|
||||
expect(conversation.valid?).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,99 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Mailbox::ConversationFinderStrategies::ReceiverUuidStrategy do
|
||||
let(:account) { create(:account) }
|
||||
let(:email_channel) { create(:channel_email, email: 'test@example.com', account: account) }
|
||||
let(:conversation) { create(:conversation, inbox: email_channel.inbox, account: account) }
|
||||
let(:mail) { Mail.new }
|
||||
|
||||
describe '#find' do
|
||||
context 'when mail has valid reply+uuid format' do
|
||||
before do
|
||||
conversation.update!(uuid: '12345678-1234-1234-1234-123456789012')
|
||||
mail.to = 'reply+12345678-1234-1234-1234-123456789012@example.com'
|
||||
end
|
||||
|
||||
it 'returns the conversation' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to eq(conversation)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when mail has uppercase UUID' do
|
||||
before do
|
||||
conversation.update!(uuid: '12345678-1234-1234-1234-123456789012')
|
||||
mail.to = 'reply+12345678-1234-1234-1234-123456789012@EXAMPLE.COM'
|
||||
end
|
||||
|
||||
it 'returns the conversation (case-insensitive matching)' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to eq(conversation)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when mail has multiple recipients with valid UUID' do
|
||||
before do
|
||||
conversation.update!(uuid: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee')
|
||||
mail.to = ['other@example.com', 'reply+aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee@example.com']
|
||||
end
|
||||
|
||||
it 'extracts UUID from any recipient' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to eq(conversation)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when UUID does not exist in database' do
|
||||
before do
|
||||
mail.to = 'reply+99999999-9999-9999-9999-999999999999@example.com'
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when mail has no recipients' do
|
||||
it 'returns nil' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when mail recipient has malformed UUID' do
|
||||
before do
|
||||
mail.to = 'reply+not-a-valid-uuid@example.com'
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when mail recipient has no reply+ prefix' do
|
||||
before do
|
||||
conversation.update!(uuid: '12345678-1234-1234-1234-123456789012')
|
||||
mail.to = 'test+12345678-1234-1234-1234-123456789012@example.com'
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when mail recipient has additional text after UUID' do
|
||||
before do
|
||||
conversation.update!(uuid: '12345678-1234-1234-1234-123456789012')
|
||||
mail.to = 'reply+12345678-1234-1234-1234-123456789012-extra@example.com'
|
||||
end
|
||||
|
||||
it 'returns nil (UUID must be exact)' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,208 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Mailbox::ConversationFinderStrategies::ReferencesStrategy do
|
||||
let(:account) { create(:account) }
|
||||
let(:email_channel) { create(:channel_email, email: 'test@example.com', account: account) }
|
||||
let(:conversation) { create(:conversation, inbox: email_channel.inbox, account: account) }
|
||||
let(:mail) { Mail.new }
|
||||
|
||||
describe '#find' do
|
||||
context 'when references has message-specific pattern' do
|
||||
before do
|
||||
conversation.update!(uuid: '12345678-1234-1234-1234-123456789012')
|
||||
mail.to = 'test@example.com'
|
||||
mail.references = 'conversation/12345678-1234-1234-1234-123456789012/messages/123@example.com'
|
||||
end
|
||||
|
||||
it 'extracts UUID and returns conversation' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to eq(conversation)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when references has conversation fallback pattern' do
|
||||
before do
|
||||
conversation.update!(uuid: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee')
|
||||
mail.to = 'test@example.com'
|
||||
mail.references = "account/#{account.id}/conversation/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee@example.com"
|
||||
end
|
||||
|
||||
it 'extracts UUID and returns conversation' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to eq(conversation)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when references matches message source_id' do
|
||||
let(:message) do
|
||||
conversation.messages.create!(
|
||||
source_id: 'original-message-id@example.com',
|
||||
account_id: account.id,
|
||||
message_type: 'outgoing',
|
||||
inbox_id: email_channel.inbox.id,
|
||||
content: 'Original message'
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
message # Create the message
|
||||
mail.to = 'test@example.com'
|
||||
mail.references = 'original-message-id@example.com'
|
||||
end
|
||||
|
||||
it 'finds conversation from message source_id' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to eq(conversation)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when references has multiple values' do
|
||||
before do
|
||||
conversation.update!(uuid: '12345678-1234-1234-1234-123456789012')
|
||||
mail.to = 'test@example.com'
|
||||
mail.references = [
|
||||
'some-random-message@gmail.com',
|
||||
'conversation/12345678-1234-1234-1234-123456789012/messages/123@example.com',
|
||||
'another-message@outlook.com'
|
||||
]
|
||||
end
|
||||
|
||||
it 'finds conversation from any reference' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to eq(conversation)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when references is blank' do
|
||||
it 'returns nil' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when references does not match any pattern or source_id' do
|
||||
before do
|
||||
mail.references = 'random-message-id@gmail.com'
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with channel validation' do
|
||||
context 'when conversation belongs to the correct channel' do
|
||||
before do
|
||||
conversation.update!(uuid: '12345678-1234-1234-1234-123456789012')
|
||||
mail.to = 'test@example.com'
|
||||
mail.references = 'conversation/12345678-1234-1234-1234-123456789012/messages/123@example.com'
|
||||
end
|
||||
|
||||
it 'returns the conversation' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to eq(conversation)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation belongs to a different channel' do
|
||||
let(:other_email_channel) { create(:channel_email, email: 'other@example.com', account: account) }
|
||||
let(:other_conversation) do
|
||||
create(
|
||||
:conversation,
|
||||
inbox: other_email_channel.inbox,
|
||||
account: account
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
other_conversation.update!(uuid: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee')
|
||||
# Mail is addressed to test@example.com but references conversation from other@example.com
|
||||
mail.to = 'test@example.com'
|
||||
mail.references = 'conversation/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/messages/456@example.com'
|
||||
end
|
||||
|
||||
it 'returns nil (prevents cross-channel hijacking)' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when channel cannot be determined from mail' do
|
||||
before do
|
||||
conversation.update!(uuid: '12345678-1234-1234-1234-123456789012')
|
||||
mail.to = 'unknown@example.com' # Email not associated with any channel
|
||||
mail.references = 'conversation/12345678-1234-1234-1234-123456789012/messages/123@example.com'
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when mail has multiple recipients including correct channel' do
|
||||
before do
|
||||
conversation.update!(uuid: '12345678-1234-1234-1234-123456789012')
|
||||
mail.to = ['other@example.com', 'test@example.com']
|
||||
mail.references = 'conversation/12345678-1234-1234-1234-123456789012/messages/123@example.com'
|
||||
end
|
||||
|
||||
it 'finds the correct channel and returns conversation' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to eq(conversation)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when UUID exists but conversation does not' do
|
||||
before do
|
||||
mail.to = 'test@example.com'
|
||||
mail.references = 'conversation/99999999-9999-9999-9999-999999999999/messages/123@example.com'
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with malformed references pattern' do
|
||||
before do
|
||||
mail.references = 'conversation/not-a-uuid/messages/123@example.com'
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when first reference fails channel validation but second succeeds' do
|
||||
let(:other_email_channel) { create(:channel_email, email: 'other@example.com', account: account) }
|
||||
let(:other_conversation) do
|
||||
create(
|
||||
:conversation,
|
||||
inbox: other_email_channel.inbox,
|
||||
account: account
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
conversation.update!(uuid: '12345678-1234-1234-1234-123456789012')
|
||||
other_conversation.update!(uuid: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee')
|
||||
|
||||
mail.to = 'test@example.com'
|
||||
mail.references = [
|
||||
'conversation/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/messages/456@example.com', # Wrong channel
|
||||
'conversation/12345678-1234-1234-1234-123456789012/messages/123@example.com' # Correct channel
|
||||
]
|
||||
end
|
||||
|
||||
it 'skips invalid reference and returns conversation from valid reference' do
|
||||
strategy = described_class.new(mail)
|
||||
expect(strategy.find).to eq(conversation)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user