fix(refactor): Cleanup the specs and the logic for FetchIMAP job (#8766)

This commit is contained in:
Pranav Raj S
2024-02-10 14:03:50 -08:00
committed by GitHub
parent 6a630bc489
commit eb379e1849
9 changed files with 384 additions and 273 deletions

View File

@@ -25,5 +25,14 @@ FactoryBot.define do
end
provider { 'microsoft' }
end
trait :imap_email do
imap_enabled { true }
imap_address { 'imap.gmail.com' }
imap_port { 993 }
imap_login { 'email@example.com' }
imap_password { 'random-password' }
imap_enable_ssl { true }
end
end
end

View File

@@ -1,161 +1,106 @@
require 'rails_helper'
RSpec.describe Inboxes::FetchImapEmailsJob do
include ActiveJob::TestHelper
include ActionMailbox::TestHelper
let(:account) { create(:account) }
let(:imap_email_channel) do
create(:channel_email, imap_enabled: true, imap_address: 'imap.gmail.com', imap_port: 993, imap_login: 'imap@gmail.com',
imap_password: 'password', account: account)
end
let(:imap_email_channel) { create(:channel_email, :imap_email, account: account) }
let(:channel_with_imap_disabled) { create(:channel_email, :imap_email, imap_enabled: false, account: account) }
let(:microsoft_imap_email_channel) { create(:channel_email, :microsoft_email) }
let(:ms_email_inbox) { create(:inbox, channel: microsoft_imap_email_channel, account: account) }
let!(:conversation) { create(:conversation, inbox: imap_email_channel.inbox, account: account) }
let(:inbound_mail) { create_inbound_email_from_mail(from: 'testemail@gmail.com', to: 'imap@outlook.com', subject: 'Hello!') }
let(:inbound_mail_with_attachments) { create_inbound_email_from_fixture('multiple_attachments.eml') }
it 'enqueues the job' do
expect { described_class.perform_later }.to have_enqueued_job(described_class)
.on_queue('scheduled_jobs')
end
context 'when imap fetch new emails' do
it 'process the email' do
email = Mail.new do
to 'test@outlook.com'
from 'test@gmail.com'
subject :test.to_s
body 'hello'
end
imap_fetch_mail = Net::IMAP::FetchData.new
imap_fetch_mail.attr = { seqno: 1, RFC822: email }.with_indifferent_access
imap = double
allow(Net::IMAP).to receive(:new).and_return(imap)
allow(imap).to receive(:authenticate)
allow(imap).to receive(:select)
allow(imap).to receive(:search).and_return([1])
allow(imap).to receive(:fetch).and_return([imap_fetch_mail])
read_mail = Mail::Message.new(date: DateTime.now, from: 'testemail@gmail.com', to: 'imap@outlook.com', subject: 'Hello!')
allow(Mail).to receive(:read_from_string).and_return(inbound_mail.mail)
imap_mailbox = double
allow(Imap::ImapMailbox).to receive(:new).and_return(imap_mailbox)
expect(imap_mailbox).to receive(:process).with(read_mail, imap_email_channel).once
described_class.perform_now(imap_email_channel)
describe '#perform' do
it 'enqueues the job' do
expect do
described_class.perform_later
end.to have_enqueued_job(described_class).on_queue('scheduled_jobs')
end
it 'process the email with no date' do
fixture_path = Rails.root.join('spec/fixtures/files/mail_with_no_date.eml')
eml_content = File.read(fixture_path)
inbound_mail_with_no_date = create_inbound_email_from_fixture('mail_with_no_date.eml')
email_header = Net::IMAP::FetchData.new(1, 'BODY[HEADER]' => eml_content)
imap_fetch_mail = Net::IMAP::FetchData.new(1, 'RFC822' => eml_content)
imap = double
allow(Net::IMAP).to receive(:new).and_return(imap)
allow(imap).to receive(:authenticate)
allow(imap).to receive(:select)
allow(imap).to receive(:search).and_return([1])
allow(imap).to receive(:fetch).with([1], 'BODY.PEEK[HEADER]').and_return([email_header])
allow(imap).to receive(:fetch).with(1, 'RFC822').and_return([imap_fetch_mail])
imap_mailbox = double
allow(Imap::ImapMailbox).to receive(:new).and_return(imap_mailbox)
expect(imap_mailbox).to receive(:process).with(inbound_mail_with_no_date.mail, imap_email_channel).once
described_class.perform_now(imap_email_channel)
context 'when IMAP is disabled' do
it 'does not fetch emails' do
expect(Imap::FetchEmailService).not_to receive(:new)
expect(Imap::MicrosoftFetchEmailService).not_to receive(:new)
described_class.perform_now(channel_with_imap_disabled)
end
end
end
context 'when imap fetch new emails with more than 15 attachments' do
it 'process the email' do
email = Mail.new do
to 'test@outlook.com'
from 'test@gmail.com'
subject :test.to_s
body 'hello'
context 'when IMAP reauthorization is required' do
it 'does not fetch emails' do
10.times do
imap_email_channel.authorization_error!
end
expect(Imap::FetchEmailService).not_to receive(:new)
# Confirm the imap_enabled flag is true to avoid false positives.
expect(imap_email_channel.imap_enabled?).to be true
described_class.perform_now(imap_email_channel)
end
end
context 'when the channel is regular imap' do
it 'calls the imap fetch service' do
fetch_service = double
allow(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel).and_return(fetch_service)
allow(fetch_service).to receive(:perform).and_return([])
described_class.perform_now(imap_email_channel)
expect(fetch_service).to have_received(:perform)
end
end
context 'when the channel is Microsoft' do
it 'calls the Microsoft fetch service' do
fetch_service = double
allow(Imap::MicrosoftFetchEmailService).to receive(:new).with(channel: microsoft_imap_email_channel).and_return(fetch_service)
allow(fetch_service).to receive(:perform).and_return([])
described_class.perform_now(microsoft_imap_email_channel)
expect(fetch_service).to have_received(:perform)
end
end
context 'when IMAP connection errors out' do
it 'mark the connection for authorization required' do
allow(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel).and_raise(Errno::ECONNREFUSED)
allow(Redis::Alfred).to receive(:incr)
expect(Redis::Alfred).to receive(:incr).with("AUTHORIZATION_ERROR_COUNT:channel_email:#{imap_email_channel.id}")
described_class.perform_now(imap_email_channel)
end
end
context 'when the fetch service returns the email objects' do
let(:inbound_mail) { create_inbound_email_from_fixture('welcome.eml').mail }
let(:mailbox) { double }
let(:exception_tracker) { double }
let(:fetch_service) { double }
before do
allow(Imap::ImapMailbox).to receive(:new).and_return(mailbox)
allow(ChatwootExceptionTracker).to receive(:new).and_return(exception_tracker)
allow(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel).and_return(fetch_service)
allow(fetch_service).to receive(:perform).and_return([inbound_mail])
end
imap_fetch_mail = Net::IMAP::FetchData.new
imap_fetch_mail.attr = { seqno: 1, RFC822: email }.with_indifferent_access
it 'calls the mailbox to create emails' do
allow(mailbox).to receive(:process)
imap = double
expect(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel).and_return(fetch_service)
expect(fetch_service).to receive(:perform).and_return([inbound_mail])
expect(mailbox).to receive(:process).with(inbound_mail, imap_email_channel)
allow(Net::IMAP).to receive(:new).and_return(imap)
allow(imap).to receive(:authenticate)
allow(imap).to receive(:select)
allow(imap).to receive(:search).and_return([1])
allow(imap).to receive(:fetch).and_return([imap_fetch_mail])
inbound_mail_with_attachments.mail.date = DateTime.now
allow(Mail).to receive(:read_from_string).and_return(inbound_mail_with_attachments.mail)
imap_mailbox = Imap::ImapMailbox.new
allow(Imap::ImapMailbox).to receive(:new).and_return(imap_mailbox)
described_class.perform_now(imap_email_channel)
expect(Message.last.attachments.count).to eq(15)
end
end
context 'when imap fetch new emails for microsoft mailer' do
it 'fetch and process all emails' do
email = Mail.new do
to 'test@outlook.com'
from 'test@gmail.com'
subject :test.to_s
body 'hello'
end
imap_fetch_mail = Net::IMAP::FetchData.new
imap_fetch_mail.attr = { RFC822: email }.with_indifferent_access
ms_imap = double
allow(Net::IMAP).to receive(:new).and_return(ms_imap)
allow(ms_imap).to receive(:authenticate)
allow(ms_imap).to receive(:select)
allow(ms_imap).to receive(:search).and_return([1])
allow(ms_imap).to receive(:fetch).and_return([imap_fetch_mail])
allow(Mail).to receive(:read_from_string).and_return(inbound_mail)
ms_imap_email_inbox = double
allow(Imap::ImapMailbox).to receive(:new).and_return(ms_imap_email_inbox)
expect(ms_imap_email_inbox).to receive(:process).with(inbound_mail, microsoft_imap_email_channel).once
described_class.perform_now(microsoft_imap_email_channel)
end
end
context 'when imap fetch existing emails' do
it 'does not process the email' do
email = Mail.new do
to 'test@outlook.com'
from 'test@gmail.com'
subject :test.to_s
body 'hello'
message_id '<messageId@example.com>'
described_class.perform_now(imap_email_channel)
end
create(:message, message_type: 'incoming', source_id: email.message_id, account: account, inbox: imap_email_channel.inbox,
conversation: conversation)
it 'logs errors if mailbox returns errors' do
allow(mailbox).to receive(:process).and_raise(StandardError)
allow(Mail).to receive(:find).and_return([email])
imap_mailbox = double
allow(Imap::ImapMailbox).to receive(:new).and_return(imap_mailbox)
expect(imap_mailbox).not_to receive(:process).with(email, imap_email_channel)
expect(exception_tracker).to receive(:capture_exception)
described_class.perform_now(imap_email_channel)
described_class.perform_now(imap_email_channel)
end
end
end
end

View File

@@ -3,14 +3,10 @@ require 'rails_helper'
RSpec.describe Imap::ImapMailbox do
include ActionMailbox::TestHelper
describe 'add mail as a new conversation in the email inbox' do
describe '#process' do
let(:account) { create(:account) }
let(:agent) { create(:user, email: 'agent@example.com', account: account) }
let(:channel) do
create(:channel_email, imap_enabled: true, imap_address: 'imap.gmail.com',
imap_port: 993, imap_login: 'imap@gmail.com', imap_password: 'password',
account: account)
end
let(:channel) { create(:channel_email, :imap_email) }
let(:inbox) { channel.inbox }
let!(:contact) { create(:contact, email: 'email@gmail.com', phone_number: '+919584546666', account: account, identifier: '123') }
let(:conversation) { Conversation.where(inbox_id: channel.inbox).last }
@@ -20,17 +16,33 @@ RSpec.describe Imap::ImapMailbox do
create(:contact_inbox, contact_id: contact.id, inbox_id: channel.inbox.id)
end
context 'when a new email from non existing contact' do
context 'when the email is from a new contact' do
let(:inbound_mail) { create_inbound_email_from_mail(from: 'testemail@gmail.com', to: 'imap@gmail.com', subject: 'Hello!') }
it 'creates the contact and conversation with message' do
class_instance.process(inbound_mail.mail, channel)
expect do
class_instance.process(inbound_mail.mail, channel)
end.to change(Conversation, :count).by(1)
expect(conversation.contact.email).to eq(inbound_mail.mail.from.first)
expect(conversation.additional_attributes['source']).to eq('email')
expect(conversation.messages.empty?).to be false
end
end
context 'when the email has 15 or more attachments' do
let(:inbound_mail) { create_inbound_email_from_fixture('multiple_attachments.eml') }
it 'creates a converstation and a message properly' do
expect do
class_instance.process(inbound_mail.mail, channel)
end.to change(Conversation, :count).by(1)
expect(conversation.contact.email).to eq(inbound_mail.mail.from.first)
expect(conversation.messages.last.attachments.count).to be 15
end
end
context 'when a new email from existing contact' do
let(:inbound_mail) { create_inbound_email_from_mail(from: 'email@gmail.com', to: 'imap@gmail.com', subject: 'Hello!') }

View File

@@ -0,0 +1,65 @@
require 'rails_helper'
RSpec.describe Imap::FetchEmailService do
include ActionMailbox::TestHelper
let(:logger) { instance_double(ActiveSupport::Logger, info: true, error: true) }
let(:account) { create(:account) }
let(:imap_email_channel) { create(:channel_email, :imap_email, account: account) }
let(:imap) { instance_double(Net::IMAP) }
let(:eml_content_with_message_id) { Rails.root.join('spec/fixtures/files/only_text.eml').read }
describe '#perform' do
before do
allow(Rails).to receive(:logger).and_return(logger)
allow(Net::IMAP).to receive(:new).with(
imap_email_channel.imap_address, port: imap_email_channel.imap_port, ssl: true
).and_return(imap)
allow(imap).to receive(:authenticate).with(
'PLAIN', imap_email_channel.imap_login, imap_email_channel.imap_password
)
allow(imap).to receive(:select).with('INBOX')
end
context 'when new emails are available in the mailbox' do
it 'fetches the emails and returns the emails that are not present in the db' do
travel_to '26.10.2020 10:00'.to_datetime do
email_object = create_inbound_email_from_fixture('only_text.eml')
email_header = Net::IMAP::FetchData.new(1, 'BODY[HEADER]' => eml_content_with_message_id)
imap_fetch_mail = Net::IMAP::FetchData.new(1, 'RFC822' => eml_content_with_message_id)
allow(imap).to receive(:search).with(%w[SINCE 25-Oct-2020]).and_return([1])
allow(imap).to receive(:fetch).with([1], 'BODY.PEEK[HEADER]').and_return([email_header])
allow(imap).to receive(:fetch).with(1, 'RFC822').and_return([imap_fetch_mail])
result = described_class.new(channel: imap_email_channel).perform
expect(result.length).to eq 1
expect(result[0].message_id).to eq email_object.message_id
expect(imap).to have_received(:search).with(%w[SINCE 25-Oct-2020])
expect(imap).to have_received(:fetch).with([1], 'BODY.PEEK[HEADER]')
expect(imap).to have_received(:fetch).with(1, 'RFC822')
expect(logger).to have_received(:info).with("[IMAP::FETCH_EMAIL_SERVICE] Fetching mails from #{imap_email_channel.email}, found 1.")
end
end
it 'fetches the emails and returns the mail objects that are not present in the db' do
travel_to '26.10.2020 10:00'.to_datetime do
email_object = create_inbound_email_from_fixture('only_text.eml')
create(:message, source_id: email_object.message_id, account: account, inbox: imap_email_channel.inbox)
email_header = Net::IMAP::FetchData.new(1, 'BODY[HEADER]' => eml_content_with_message_id)
allow(imap).to receive(:search).with(%w[SINCE 25-Oct-2020]).and_return([1])
allow(imap).to receive(:fetch).with([1], 'BODY.PEEK[HEADER]').and_return([email_header])
result = described_class.new(channel: imap_email_channel).perform
expect(result.length).to eq 0
expect(imap).to have_received(:search).with(%w[SINCE 25-Oct-2020])
expect(imap).to have_received(:fetch).with([1], 'BODY.PEEK[HEADER]')
expect(imap).not_to have_received(:fetch).with(1, 'RFC822')
end
end
end
end
end

View File

@@ -0,0 +1,53 @@
require 'rails_helper'
RSpec.describe Imap::MicrosoftFetchEmailService do
include ActionMailbox::TestHelper
let(:logger) { instance_double(ActiveSupport::Logger, info: true, error: true) }
let(:account) { create(:account) }
let(:microsoft_channel) { create(:channel_email, :microsoft_email, account: account) }
let(:imap) { instance_double(Net::IMAP) }
let(:refresh_token_service) { double }
let(:eml_content_with_message_id) { Rails.root.join('spec/fixtures/files/only_text.eml').read }
describe '#perform' do
before do
allow(Rails).to receive(:logger).and_return(logger)
allow(Net::IMAP).to receive(:new).with(
microsoft_channel.imap_address, port: microsoft_channel.imap_port, ssl: true
).and_return(imap)
allow(imap).to receive(:authenticate).with(
'XOAUTH2', microsoft_channel.imap_login, microsoft_channel.provider_config['access_token']
)
allow(imap).to receive(:select).with('INBOX')
allow(Microsoft::RefreshOauthTokenService).to receive(:new).and_return(refresh_token_service)
allow(refresh_token_service).to receive(:access_token).and_return(microsoft_channel.provider_config['access_token'])
end
context 'when new emails are available in the mailbox' do
it 'fetches the emails and returns the emails that are not present in the db' do
travel_to '26.10.2020 10:00'.to_datetime do
email_object = create_inbound_email_from_fixture('only_text.eml')
email_header = Net::IMAP::FetchData.new(1, 'BODY[HEADER]' => eml_content_with_message_id)
imap_fetch_mail = Net::IMAP::FetchData.new(1, 'RFC822' => eml_content_with_message_id)
allow(imap).to receive(:search).with(%w[SINCE 25-Oct-2020]).and_return([1])
allow(imap).to receive(:fetch).with([1], 'BODY.PEEK[HEADER]').and_return([email_header])
allow(imap).to receive(:fetch).with(1, 'RFC822').and_return([imap_fetch_mail])
result = described_class.new(channel: microsoft_channel).perform
expect(refresh_token_service).to have_received(:access_token)
expect(result.length).to eq 1
expect(result[0].message_id).to eq email_object.message_id
expect(imap).to have_received(:search).with(%w[SINCE 25-Oct-2020])
expect(imap).to have_received(:fetch).with([1], 'BODY.PEEK[HEADER]')
expect(imap).to have_received(:fetch).with(1, 'RFC822')
expect(logger).to have_received(:info).with("[IMAP::FETCH_EMAIL_SERVICE] Fetching mails from #{microsoft_channel.email}, found 1.")
end
end
end
end
end