Feat: Support for Microsoft Oauth in Email Channel (#6227)
- Adds the backend APIs required for Microsoft Email Channels Co-authored-by: Pranav Raj S <pranav@chatwoot.com> Co-authored-by: Sojan <sojan@pepalo.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Microsoft Authorization API', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/microsoft/authorization' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post "/api/v1/accounts/#{account.id}/microsoft/authorization"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:administrator) { create(:user, account: account, role: :administrator) }
|
||||
|
||||
it 'returns unathorized for agent' do
|
||||
post "/api/v1/accounts/#{account.id}/microsoft/authorization",
|
||||
headers: agent.create_new_auth_token,
|
||||
params: { email: administrator.email },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'creates a new authorization and returns the redirect url' do
|
||||
post "/api/v1/accounts/#{account.id}/microsoft/authorization",
|
||||
headers: administrator.create_new_auth_token,
|
||||
params: { email: administrator.email },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
microsoft_service = Class.new { extend MicrosoftConcern }
|
||||
response_url = microsoft_service.microsoft_client.auth_code.authorize_url(
|
||||
{
|
||||
redirect_uri: "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback",
|
||||
scope: 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send openid',
|
||||
prompt: 'consent'
|
||||
}
|
||||
)
|
||||
expect(JSON.parse(response.body)['url']).to eq response_url
|
||||
expect(::Redis::Alfred.get(administrator.email)).to eq(account.id.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
67
spec/controllers/microsoft/callbacks_controller_spec.rb
Normal file
67
spec/controllers/microsoft/callbacks_controller_spec.rb
Normal file
@@ -0,0 +1,67 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Microsoft::CallbacksController', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:code) { SecureRandom.hex(10) }
|
||||
let(:email) { Faker::Internet.email }
|
||||
|
||||
before do
|
||||
Redis::Alfred.set(email, account.id)
|
||||
end
|
||||
|
||||
describe 'GET /microsoft/callback' do
|
||||
let(:response_body_success) do
|
||||
{ id_token: JWT.encode({ email: email, name: 'test' }, false), access_token: SecureRandom.hex(10), token_type: 'Bearer',
|
||||
refresh_token: SecureRandom.hex(10) }
|
||||
end
|
||||
|
||||
it 'creates inboxes if authentication is successful' do
|
||||
stub_request(:post, 'https://login.microsoftonline.com/common/oauth2/v2.0/token')
|
||||
.with(body: { 'code' => code, 'grant_type' => 'authorization_code',
|
||||
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback" })
|
||||
.to_return(status: 200, body: response_body_success.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
get microsoft_callback_url, params: { code: code }
|
||||
|
||||
expect(response).to redirect_to app_microsoft_inbox_agents_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
||||
expect(account.inboxes.count).to be 1
|
||||
inbox = account.inboxes.last
|
||||
expect(inbox.name).to eq 'test'
|
||||
expect(inbox.channel.reload.provider_config.keys).to include('access_token', 'refresh_token', 'expires_on')
|
||||
expect(inbox.channel.reload.provider_config['access_token']).to eq response_body_success[:access_token]
|
||||
expect(inbox.channel.imap_address).to eq 'outlook.office365.com'
|
||||
expect(Redis::Alfred.get(email)).to be_nil
|
||||
end
|
||||
|
||||
it 'creates updates inbox channel config if inbox exists and authentication is successful' do
|
||||
inbox = create(:channel_email, account: account, email: email)&.inbox
|
||||
expect(inbox.channel.provider_config).to eq({})
|
||||
|
||||
stub_request(:post, 'https://login.microsoftonline.com/common/oauth2/v2.0/token')
|
||||
.with(body: { 'code' => code, 'grant_type' => 'authorization_code',
|
||||
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback" })
|
||||
.to_return(status: 200, body: response_body_success.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
get microsoft_callback_url, params: { code: code }
|
||||
|
||||
expect(response).to redirect_to app_microsoft_inbox_agents_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
||||
expect(account.inboxes.count).to be 1
|
||||
expect(inbox.channel.reload.provider_config.keys).to include('access_token', 'refresh_token', 'expires_on')
|
||||
expect(inbox.channel.reload.provider_config['access_token']).to eq response_body_success[:access_token]
|
||||
expect(inbox.channel.imap_address).to eq 'outlook.office365.com'
|
||||
expect(Redis::Alfred.get(email)).to be_nil
|
||||
end
|
||||
|
||||
it 'redirects to microsoft app in case of error' do
|
||||
stub_request(:post, 'https://login.microsoftonline.com/common/oauth2/v2.0/token')
|
||||
.with(body: { 'code' => code, 'grant_type' => 'authorization_code',
|
||||
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback" })
|
||||
.to_return(status: 401)
|
||||
|
||||
get microsoft_callback_url, params: { code: code }
|
||||
|
||||
expect(response).to redirect_to '/'
|
||||
expect(Redis::Alfred.get(email).to_i).to eq account.id
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,12 +1,21 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Inboxes::FetchImapEmailsJob, type: :job do
|
||||
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', imap_inbox_synced_at: Time.now.utc, account: account)
|
||||
end
|
||||
let(:microsoft_imap_email_channel) do
|
||||
create(:channel_email, provider: 'microsoft', imap_enabled: true, imap_address: 'outlook.office365.com',
|
||||
imap_port: 993, imap_login: 'imap@outlook.com', imap_password: 'password', account: account,
|
||||
provider_config: { access_token: 'access_token' })
|
||||
end
|
||||
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!') }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { described_class.perform_later }.to have_enqueued_job(described_class)
|
||||
@@ -31,6 +40,35 @@ RSpec.describe Inboxes::FetchImapEmailsJob, type: :job do
|
||||
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
|
||||
|
||||
@@ -170,6 +170,22 @@ RSpec.describe ConversationReplyMailer, type: :mailer do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when smtp enabled for microsoft email channel' do
|
||||
let(:ms_smtp_email_channel) do
|
||||
create(:channel_email, imap_login: 'smtp@outlook.com',
|
||||
imap_enabled: true, account: account, provider: 'microsoft', provider_config: { access_token: 'access_token' })
|
||||
end
|
||||
let(:conversation) { create(:conversation, assignee: agent, inbox: ms_smtp_email_channel.inbox, account: account).reload }
|
||||
let(:message) { create(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') }
|
||||
|
||||
it 'use smtp mail server' do
|
||||
mail = described_class.email_reply(message)
|
||||
expect(mail.delivery_method.settings.empty?).to be false
|
||||
expect(mail.delivery_method.settings[:address]).to eq 'smtp.office365.com'
|
||||
expect(mail.delivery_method.settings[:port]).to eq 587
|
||||
end
|
||||
end
|
||||
|
||||
context 'when smtp disabled for email channel', :test do
|
||||
let(:conversation) { create(:conversation, assignee: agent, inbox: email_channel.inbox, account: account).reload }
|
||||
let(:message) { create(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') }
|
||||
|
||||
@@ -24,4 +24,15 @@ RSpec.describe Channel::Email do
|
||||
it 'has a valid name' do
|
||||
expect(channel.name).to eq('Email')
|
||||
end
|
||||
|
||||
context 'when microsoft?' do
|
||||
it 'returns false' do
|
||||
expect(channel.microsoft?).to be(false)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
channel.provider = 'microsoft'
|
||||
expect(channel.microsoft?).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
53
spec/services/microsoft/refresh_oauth_token_service_spec.rb
Normal file
53
spec/services/microsoft/refresh_oauth_token_service_spec.rb
Normal file
@@ -0,0 +1,53 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Microsoft::RefreshOauthTokenService do
|
||||
let(:access_token) { SecureRandom.hex }
|
||||
let(:refresh_token) { SecureRandom.hex }
|
||||
let(:expires_on) { Time.zone.now + 3600 }
|
||||
|
||||
let!(:microsoft_email_channel) do
|
||||
create(:channel_email, provider_config: { access_token: access_token, refresh_token: refresh_token, expires_on: expires_on })
|
||||
end
|
||||
let(:new_tokens) { { access_token: access_token, refresh_token: refresh_token, expires_at: expires_on.to_i, token_type: 'bearer' } }
|
||||
|
||||
describe '#access_token' do
|
||||
context 'when token is not expired' do
|
||||
it 'returns the existing access token' do
|
||||
expect(described_class.new(channel: microsoft_email_channel).access_token).to eq(access_token)
|
||||
expect(microsoft_email_channel.reload.provider_config['refresh_token']).to eq(refresh_token)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when token is expired' do
|
||||
let(:expires_on) { 1.minute.from_now }
|
||||
|
||||
before do
|
||||
stub_request(:post, 'https://login.microsoftonline.com/common/oauth2/v2.0/token').with(
|
||||
body: { 'grant_type' => 'refresh_token', 'refresh_token' => refresh_token }
|
||||
).to_return(status: 200, body: new_tokens.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
end
|
||||
|
||||
it 'fetches new access token and refresh tokens' do
|
||||
microsoft_email_channel.provider_config['expires_on'] = Time.zone.now - 3600
|
||||
microsoft_email_channel.save!
|
||||
|
||||
expect(described_class.new(channel: microsoft_email_channel).access_token).not_to eq(access_token)
|
||||
expect(microsoft_email_channel.reload.provider_config['access_token']).to eq(new_tokens[:access_token])
|
||||
expect(microsoft_email_channel.reload.provider_config['refresh_token']).to eq(new_tokens[:refresh_token])
|
||||
expect(microsoft_email_channel.reload.provider_config['expires_on']).to eq(Time.at(new_tokens[:expires_at]).utc.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when refresh token is not present in provider config and access token is expired' do
|
||||
it 'throws an error' do
|
||||
microsoft_email_channel.update(provider_config: {
|
||||
access_token: access_token,
|
||||
expires_on: expires_on - 3600
|
||||
})
|
||||
expect do
|
||||
described_class.new(channel: microsoft_email_channel).access_token
|
||||
end.to raise_error(RuntimeError, 'A refresh_token is not available')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user