Compare commits

...
Author SHA1 Message Date
Shivam Mishra 4f3da3f814 Revert "feat: hide google option"
This reverts commit 05c18de547.
2024-06-05 14:04:05 +05:30
Muhsin KelothandGitHub 675bb3b744 Merge branch 'develop' into feature/cw-3369 2024-06-05 11:47:02 +05:30
Shivam Mishra 4f7d1a67cf refactor: move filter fn as separate isValidPath 2024-06-05 09:09:52 +05:30
Shivam Mishra 1336b0c750 refactor: separate sanitizePaths fn 2024-06-03 23:07:19 +05:30
Shivam Mishra 84445e0d1e test: google callbacks controller 2024-06-03 22:59:37 +05:30
Shivam Mishra d9cd8b8261 test: Google::AuthorizationsController 2024-06-03 22:54:34 +05:30
Shivam MishraandGitHub 59766ebc95 Merge branch 'develop' into feature/cw-3369 2024-06-03 22:51:24 +05:30
Shivam Mishra d1d958601d test: update cache_key 2024-06-03 22:49:02 +05:30
Shivam Mishra 46beab047d fix: route name 2024-06-03 22:45:21 +05:30
Shivam Mishra b832fdc329 test: update cache key 2024-06-03 22:43:41 +05:30
Shivam Mishra 5c957054fe feat: enable inbox configuration 2024-06-03 12:55:03 +05:30
Shivam MishraandGitHub df26e940da Merge branch 'develop' into feature/cw-3369 2024-06-03 12:51:34 +05:30
Shivam Mishra 05c18de547 feat: hide google option 2024-06-03 12:23:39 +05:30
Shivam Mishra e54d426577 refactor: explicit token URL 2024-06-03 12:19:35 +05:30
Shivam Mishra e6185536a8 fix: not getting refresh_token in callback response 2024-06-03 12:19:18 +05:30
Shivam Mishra 9d4ce3964b feat: use correct token 2024-06-02 21:32:43 +05:30
Shivam Mishra 5792542ee2 feat: use browser validations 2024-06-02 21:28:12 +05:30
Shivam Mishra 68d20a854e feat: use composition API 2024-06-02 21:25:19 +05:30
Shivam Mishra 23e1d614bf fix: remove trim 2024-06-02 19:51:50 +05:30
Shivam Mishra a8ac78cd8e fix: class 2024-06-02 19:51:22 +05:30
Shivam Mishra 808a27c793 feat: enable controller 2024-06-02 19:50:46 +05:30
Shivam Mishra f57d0f9da9 fix: include concern 2024-06-02 19:50:38 +05:30
Shivam Mishra db13956f81 fix: concern name 2024-06-02 19:48:42 +05:30
Shivam Mishra 47074e8a82 feat: add google client 2024-06-02 19:43:35 +05:30
Shivam Mishra b8c237ca58 feat: add scope to cache key 2024-06-02 19:41:40 +05:30
Shivam Mishra b6f73221b9 feat: add authorization controller for google inbox 2024-06-02 19:37:31 +05:30
Shivam Mishra d974be86ee feat: add google concern 2024-06-02 19:35:41 +05:30
Shivam Mishra 4582794ad4 feat: bring back Microsoft Controller
I made a booboo, Microsoft concern was actually used in Microsoft::AuthorizationsController, which needed the OAuth client, let's bring it back
2024-06-02 19:34:33 +05:30
Shivam Mishra f906b9a775 feat: add imap 2024-06-02 19:27:46 +05:30
Shivam Mishra df373bec62 refactor: move update_channel 2024-06-02 19:27:26 +05:30
Shivam Mishra d7b564ea9f feat: use a generic name for inbox settings and agents page 2024-06-02 19:15:27 +05:30
Shivam Mishra 8bdb1a2ce0 refactor: move more functions to base controller 2024-06-02 19:03:43 +05:30
Shivam Mishra a8e1fba1bd fix: google callbacks 2024-05-31 18:51:16 +05:30
Shivam Mishra ea86184675 fix: tests 2024-05-31 18:49:22 +05:30
Shivam Mishra 6ec5bbc2c7 feat: add provider_name 2024-05-31 18:35:02 +05:30
Shivam Mishra 32f25e3bd7 feat: kickstart google callbacks 2024-05-30 19:10:13 +05:30
Shivam Mishra d51e3ad5bf refactor: separate OauthCallbackController 2024-05-30 19:06:44 +05:30
Shivam Mishra 2dd2cfeb87 feat: enable google 2024-05-30 13:47:09 +05:30
Shivam Mishra bd7e1fba3f feat: add joinURL 2024-05-30 13:42:41 +05:30
Shivam Mishra bece33792e feat: add gmail/google as an option 2024-05-30 13:17:10 +05:30
Shivam Mishra ffb687d12f fix: channel selector layout 2024-05-30 13:16:51 +05:30
Shivam Mishra 980f4da669 feat: allow google as a provider 2024-05-30 13:09:04 +05:30
23 changed files with 529 additions and 98 deletions
@@ -0,0 +1,32 @@
class Api::V1::Accounts::Google::AuthorizationsController < Api::V1::Accounts::BaseController
include GoogleConcern
before_action :check_authorization
def create
email = params[:authorization][:email]
redirect_url = google_client.auth_code.authorize_url(
{
redirect_uri: "#{base_url}/google/callback",
scope: 'email profile https://mail.google.com/',
response_type: 'code',
prompt: 'consent', # the oauth flow does not return a refresh token, this is supposed to fix it
access_type: 'offline', # the default is 'online'
client_id: GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_ID', nil)
}
)
if redirect_url
cache_key = "google::#{email.downcase}"
::Redis::Alfred.setex(cache_key, Current.account.id, 5.minutes)
render json: { success: true, url: redirect_url }
else
render json: { success: false }, status: :unprocessable_entity
end
end
private
def check_authorization
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
end
end
@@ -12,8 +12,8 @@ class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts
}
)
if redirect_url
email = email.downcase
::Redis::Alfred.setex(email, Current.account.id, 5.minutes)
cache_key = "microsoft::#{email.downcase}"
::Redis::Alfred.setex(cache_key, Current.account.id, 5.minutes)
render json: { success: true, url: redirect_url }
else
render json: { success: false }, status: :unprocessable_entity
@@ -0,0 +1,20 @@
module GoogleConcern
extend ActiveSupport::Concern
def google_client
app_id = GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_ID', nil)
app_secret = GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_SECRET', nil)
::OAuth2::Client.new(app_id, app_secret, {
site: 'https://oauth2.googleapis.com',
authorize_url: 'https://accounts.google.com/o/oauth2/auth',
token_url: 'https://accounts.google.com/o/oauth2/token'
})
end
private
def base_url
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
end
end
@@ -15,10 +15,6 @@ module MicrosoftConcern
private
def parsed_body
@parsed_body ||= Rack::Utils.parse_nested_query(@response.raw_response.body)
end
def base_url
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
end
@@ -0,0 +1,18 @@
class Google::CallbacksController < OauthCallbackController
include GoogleConcern
private
def provider_name
'google'
end
def imap_address
'imap.gmail.com'
end
def oauth_client
# from GoogleConcern
google_client
end
end
@@ -1,90 +1,17 @@
class Microsoft::CallbacksController < ApplicationController
class Microsoft::CallbacksController < OauthCallbackController
include MicrosoftConcern
def show
@response = microsoft_client.auth_code.get_token(
oauth_code,
redirect_uri: "#{base_url}/microsoft/callback"
)
inbox, already_exists = find_or_create_inbox
::Redis::Alfred.delete(users_data['email'].downcase)
if already_exists
redirect_to app_microsoft_inbox_settings_url(account_id: account.id, inbox_id: inbox.id)
else
redirect_to app_microsoft_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
end
rescue StandardError => e
ChatwootExceptionTracker.new(e).capture_exception
redirect_to '/'
end
private
def oauth_code
params[:code]
def oauth_client
microsoft_client
end
def users_data
decoded_token = JWT.decode parsed_body[:id_token], nil, false
decoded_token[0]
def provider_name
'microsoft'
end
def parsed_body
@parsed_body ||= @response.response.parsed
end
def account_id
::Redis::Alfred.get(users_data['email'].downcase)
end
def account
@account ||= Account.find(account_id)
end
def find_or_create_inbox
channel_email = Channel::Email.find_by(email: users_data['email'], account: account)
# we need this value to know where to redirect on sucessful processing of the callback
channel_exists = channel_email.present?
channel_email ||= create_microsoft_channel_with_inbox
update_microsoft_channel(channel_email)
# reauthorize channel, this code path only triggers when microsoft auth is successful
# reauthorized will also update cache keys for the associated inbox
channel_email.reauthorized!
[channel_email.inbox, channel_exists]
end
# Fallback name, for when name field is missing from users_data
def fallback_name
users_data['email'].split('@').first.parameterize.titleize
end
def create_microsoft_channel_with_inbox
ActiveRecord::Base.transaction do
channel_email = Channel::Email.create!(email: users_data['email'], account: account)
account.inboxes.create!(
account: account,
channel: channel_email,
name: users_data['name'] || fallback_name
)
channel_email
end
end
def update_microsoft_channel(channel_email)
channel_email.update!({
imap_login: users_data['email'], imap_address: 'outlook.office365.com',
imap_port: '993', imap_enabled: true,
provider: 'microsoft',
provider_config: {
access_token: parsed_body['access_token'],
refresh_token: parsed_body['refresh_token'],
expires_on: (Time.current.utc + 1.hour).to_s
}
})
def imap_address
'outlook.office365.com'
end
end
@@ -0,0 +1,108 @@
class OauthCallbackController < ApplicationController
def show
@response = oauth_client.auth_code.get_token(
oauth_code,
redirect_uri: "#{base_url}/#{provider_name}/callback"
)
handle_response
::Redis::Alfred.delete(cache_key)
rescue StandardError => e
ChatwootExceptionTracker.new(e).capture_exception
redirect_to '/'
end
private
def handle_response
inbox, already_exists = find_or_create_inbox
if already_exists
redirect_to app_email_inbox_settings_url(account_id: account.id, inbox_id: inbox.id)
else
redirect_to app_email_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
end
end
def find_or_create_inbox
channel_email = Channel::Email.find_by(email: users_data['email'], account: account)
# we need this value to know where to redirect on sucessful processing of the callback
channel_exists = channel_email.present?
channel_email ||= create_channel_with_inbox
update_channel(channel_email)
# reauthorize channel, this code path only triggers when microsoft auth is successful
# reauthorized will also update cache keys for the associated inbox
channel_email.reauthorized!
[channel_email.inbox, channel_exists]
end
def update_channel(channel_email)
channel_email.update!({
imap_login: users_data['email'], imap_address: imap_address,
imap_port: '993', imap_enabled: true,
provider: provider_name,
provider_config: {
access_token: parsed_body['access_token'],
refresh_token: parsed_body['refresh_token'],
expires_on: (Time.current.utc + 1.hour).to_s
}
})
end
def provider_name
raise NotImplementedError
end
def oauth_client
raise NotImplementedError
end
def cache_key
"#{provider_name}::#{users_data['email'].downcase}"
end
def create_channel_with_inbox
ActiveRecord::Base.transaction do
channel_email = Channel::Email.create!(email: users_data['email'], account: account)
account.inboxes.create!(
account: account,
channel: channel_email,
name: users_data['name'] || fallback_name
)
channel_email
end
end
def users_data
decoded_token = JWT.decode parsed_body[:id_token], nil, false
decoded_token[0]
end
def account_id
::Redis::Alfred.get(cache_key)
end
def account
@account ||= Account.find(account_id)
end
# Fallback name, for when name field is missing from users_data
def fallback_name
users_data['email'].split('@').first.parameterize.titleize
end
def oauth_code
params[:code]
end
def base_url
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
end
def parsed_body
@parsed_body ||= @response.response.parsed
end
end
@@ -0,0 +1,14 @@
/* global axios */
import ApiClient from '../ApiClient';
class MicrosoftClient extends ApiClient {
constructor() {
super('google', { accountScoped: true });
}
generateAuthorization(payload) {
return axios.post(`${this.url}/authorization`, payload);
}
}
export default new MicrosoftClient();
@@ -1,6 +1,6 @@
<template>
<button
class="bg-white dark:bg-slate-900 cursor-pointer flex flex-col transition-all duration-200 ease-in -m-px py-4 px-0 items-center border border-solid border-slate-25 dark:border-slate-800 hover:border-woot-500 dark:hover:border-woot-500 hover:shadow-md hover:z-50 disabled:opacity-60"
class="bg-white dark:bg-slate-900 cursor-pointer flex flex-col justify-end transition-all duration-200 ease-in -m-px py-4 px-0 items-center border border-solid border-slate-25 dark:border-slate-800 hover:border-woot-500 dark:hover:border-woot-500 hover:shadow-md hover:z-50 disabled:opacity-60"
@click="$emit('click')"
>
<img :src="src" :alt="title" class="w-1/2 my-4 mx-auto" />
@@ -369,6 +369,14 @@
"SIGN_IN": "Sign in with Microsoft",
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
},
"GOOGLE": {
"TITLE": "Google Email",
"DESCRIPTION": "Click on the Sign in with Google button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
"SIGN_IN": "Sign in with Google",
"EMAIL_PLACEHOLDER": "Enter email address",
"HELP": "To add your Google account as a channel, you need to authenticate your Google account by clicking on 'Sign in with Google' ",
"ERROR_MESSAGE": "There was an error connecting to Google, please try again"
}
},
"DETAILS": {
@@ -736,6 +744,7 @@
},
"EMAIL_PROVIDERS": {
"MICROSOFT": "Microsoft",
"GOOGLE": "Google",
"OTHER_PROVIDERS": "Other Providers"
}
}
@@ -541,7 +541,8 @@ export default {
this.isALineChannel ||
this.isAPIInbox ||
(this.isAnEmailChannel && !this.inbox.provider) ||
(this.isAnEmailChannel && this.inbox.provider === 'microsoft') ||
this.isAMicrosoftInbox ||
this.isAGoogleInbox ||
this.isAWhatsAppChannel ||
this.isAWebWidgetInbox
) {
@@ -20,11 +20,13 @@
</div>
</div>
<microsoft v-else-if="provider === 'microsoft'" />
<google v-else-if="provider === 'google'" />
<forward-to-option v-else-if="provider === 'other_provider'" />
</template>
<script>
import ForwardToOption from './emailChannels/ForwardToOption.vue';
import Microsoft from './emailChannels/Microsoft.vue';
import Google from './emailChannels/Google.vue';
import { mapGetters } from 'vuex';
import ChannelSelector from 'dashboard/components/ChannelSelector.vue';
import PageHeader from '../../SettingsSubPageHeader.vue';
@@ -34,6 +36,7 @@ export default {
ChannelSelector,
ForwardToOption,
Microsoft,
Google,
PageHeader,
},
data() {
@@ -52,6 +55,12 @@ export default {
key: 'microsoft',
src: '/assets/images/dashboard/channels/microsoft.png',
},
{
title: this.$t('INBOX_MGMT.EMAIL_PROVIDERS.GOOGLE'),
isEnabled: !!window.chatwootConfig.googleOAuthClientId,
key: 'google',
src: '/assets/images/dashboard/channels/google.png',
},
{
title: this.$t('INBOX_MGMT.EMAIL_PROVIDERS.OTHER_PROVIDERS'),
isEnabled: true,
@@ -0,0 +1,55 @@
<script setup>
import { ref } from 'vue';
import { useAlert } from 'dashboard/composables';
import { useI18n } from 'dashboard/composables/useI18n';
import googleClient from 'dashboard/api/channel/googleClient';
import SettingsSubPageHeader from '../../../SettingsSubPageHeader.vue';
const { t } = useI18n();
const isRequestingAuthorization = ref(false);
const email = ref('');
async function requestAuthorization() {
try {
isRequestingAuthorization.value = true;
const response = await googleClient.generateAuthorization({
email: email.value,
});
const {
data: { url },
} = response;
window.location.href = url;
} catch (error) {
useAlert(t('INBOX_MGMT.ADD.GOOGLE.ERROR_MESSAGE'));
} finally {
isRequestingAuthorization.value = false;
}
}
</script>
<template>
<div
class="border border-slate-25 dark:border-slate-800/60 bg-white dark:bg-slate-900 h-full p-6 w-full max-w-full md:w-3/4 md:max-w-[75%] flex-shrink-0 flex-grow-0"
>
<settings-sub-page-header
:header-title="$t('INBOX_MGMT.ADD.GOOGLE.TITLE')"
:header-content="$t('INBOX_MGMT.ADD.GOOGLE.DESCRIPTION')"
/>
<form class="mt-6" @submit.prevent="requestAuthorization">
<woot-input
v-model="email"
type="email"
:placeholder="$t('INBOX_MGMT.ADD.GOOGLE.EMAIL_PLACEHOLDER')"
/>
<woot-submit-button
icon="brand-twitter"
:button-text="$t('INBOX_MGMT.ADD.GOOGLE.SIGN_IN')"
type="submit"
:loading="isRequestingAuthorization"
/>
</form>
</div>
</template>
+32
View File
@@ -0,0 +1,32 @@
const isValidPath = path => {
if (!path) return false;
if (path === null && path === '') return false;
// if path is just a sequence of slashes
if (/^\/+$/.test(path)) return false;
return true;
};
/**
* Join multiple paths together with a base URL
* NOTE: This function is not designed to handle query strings or fragments
*
* @param {string} baseUrl - The base URL to join the paths to
* @param {...string} paths - The paths to join
* @returns {string} - The full URL
*/
export function joinUrl(baseUrl, ...paths) {
// remove empty undefined and null path items
// also handle if the path is just a slash or just multiple slashes only
const sanitizedPaths = paths.filter(isValidPath);
const fullUrl = sanitizedPaths.reduce(
(acc, path) => {
const sanitizedPath = path.replace(/^\/+|\/+$/g, ''); // Remove leading and trailing slashes from each path segment
return `${acc}/${sanitizedPath}`; // Concatenate with a single slash
},
baseUrl.replace(/\/+$/, '')
);
return fullUrl;
}
@@ -0,0 +1,59 @@
import { joinUrl } from '../joinUrl';
describe('joinUrl', () => {
it('should correctly join base URL and multiple paths', () => {
expect(joinUrl('http://example.com', 'path1', 'path2', 'path3')).toBe(
'http://example.com/path1/path2/path3'
);
});
it('should handle trailing slashes on the base URL', () => {
expect(joinUrl('http://example.com/', 'path1', 'path2')).toBe(
'http://example.com/path1/path2'
);
});
it('should handle leading and trailing slashes on path segments', () => {
expect(joinUrl('http://example.com', '/path1/', '/path2/')).toBe(
'http://example.com/path1/path2'
);
});
it('should handle a mix of slashes in base URL and paths', () => {
expect(joinUrl('http://example.com/', '/path1/', '/path2/')).toBe(
'http://example.com/path1/path2'
);
});
it('should return the base URL if no paths are provided', () => {
expect(joinUrl('http://example.com')).toBe('http://example.com');
});
it('should handle empty path segments', () => {
expect(joinUrl('http://example.com', '', 'path1', '', 'path2')).toBe(
'http://example.com/path1/path2'
);
});
it('should handle paths that are just slashes', () => {
expect(joinUrl('http://example.com', '/', '//', '/path1/', '/')).toBe(
'http://example.com/path1'
);
});
it('should handle null and undefined paths', () => {
expect(
joinUrl('http://example.com', null, 'path1', undefined, 'path2')
).toBe('http://example.com/path1/path2');
});
it('should handle paths with multiple consecutive slashes', () => {
expect(joinUrl('http://example.com', '///path1///', 'path2///')).toBe(
'http://example.com/path1/path2'
);
});
it('should handle only slashes in base URL and paths', () => {
expect(joinUrl('http://example.com/', '/', '/')).toBe('http://example.com');
});
});
@@ -47,6 +47,9 @@ export default {
isAMicrosoftInbox() {
return this.isAnEmailChannel && this.inbox.provider === 'microsoft';
},
isAGoogleInbox() {
return this.isAnEmailChannel && this.inbox.provider === 'google';
},
isAPIInbox() {
return this.channelType === INBOX_TYPES.API;
},
+4
View File
@@ -57,6 +57,10 @@ class Channel::Email < ApplicationRecord
provider == 'microsoft'
end
def google?
provider == 'google'
end
private
def ensure_forward_to_email
+7 -2
View File
@@ -19,8 +19,8 @@ Rails.application.routes.draw do
get '/app/accounts/:account_id/settings/inboxes/new/twitter', to: 'dashboard#index', as: 'app_new_twitter_inbox'
get '/app/accounts/:account_id/settings/inboxes/new/microsoft', to: 'dashboard#index', as: 'app_new_microsoft_inbox'
get '/app/accounts/:account_id/settings/inboxes/new/:inbox_id/agents', to: 'dashboard#index', as: 'app_twitter_inbox_agents'
get '/app/accounts/:account_id/settings/inboxes/new/:inbox_id/agents', to: 'dashboard#index', as: 'app_microsoft_inbox_agents'
get '/app/accounts/:account_id/settings/inboxes/:inbox_id', to: 'dashboard#index', as: 'app_microsoft_inbox_settings'
get '/app/accounts/:account_id/settings/inboxes/new/:inbox_id/agents', to: 'dashboard#index', as: 'app_email_inbox_agents'
get '/app/accounts/:account_id/settings/inboxes/:inbox_id', to: 'dashboard#index', as: 'app_email_inbox_settings'
resource :widget, only: [:show]
namespace :survey do
@@ -209,6 +209,10 @@ Rails.application.routes.draw do
resource :authorization, only: [:create]
end
namespace :google do
resource :authorization, only: [:create]
end
resources :webhooks, only: [:index, :create, :update, :destroy]
namespace :integrations do
resources :apps, only: [:index, :show]
@@ -445,6 +449,7 @@ Rails.application.routes.draw do
end
get 'microsoft/callback', to: 'microsoft/callbacks#show'
get 'google/callback', to: 'google/callbacks#show'
# ----------------------------------------------------------------------
# Routes for external service verifications
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,51 @@
require 'rails_helper'
RSpec.describe 'Google Authorization API', type: :request do
let(:account) { create(:account) }
describe 'POST /api/v1/accounts/{account.id}/google/authorization' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/google/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}/google/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}/google/authorization",
headers: administrator.create_new_auth_token,
params: { email: administrator.email },
as: :json
expect(response).to have_http_status(:success)
google_service = Class.new { extend GoogleConcern }
response_url = google_service.google_client.auth_code.authorize_url(
{
redirect_uri: "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback",
scope: 'email profile https://mail.google.com/',
response_type: 'code',
prompt: 'consent',
access_type: 'offline',
client_id: GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_ID', nil)
}
)
expect(response.parsed_body['url']).to eq response_url
expect(Redis::Alfred.get("google::#{administrator.email}")).to eq(account.id.to_s)
end
end
end
end
@@ -41,7 +41,7 @@ RSpec.describe 'Microsoft Authorization API', type: :request do
}
)
expect(response.parsed_body['url']).to eq response_url
expect(Redis::Alfred.get(administrator.email)).to eq(account.id.to_s)
expect(Redis::Alfred.get("microsoft::#{administrator.email}")).to eq(account.id.to_s)
end
end
end
@@ -0,0 +1,87 @@
require 'rails_helper'
RSpec.describe 'Google::CallbacksController', type: :request do
let(:account) { create(:account) }
let(:code) { SecureRandom.hex(10) }
let(:email) { Faker::Internet.email }
let(:cache_key) { "google::#{email.downcase}" }
before do
Redis::Alfred.set(cache_key, account.id)
end
describe 'GET /google/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
let(:response_body_success_without_name) do
{ id_token: JWT.encode({ email: email }, 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://accounts.google.com/o/oauth2/token')
.with(body: { 'code' => code, 'grant_type' => 'authorization_code',
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback" })
.to_return(status: 200, body: response_body_success.to_json, headers: { 'Content-Type' => 'application/json' })
get google_callback_url, params: { code: code }
expect(response).to redirect_to app_email_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 'imap.gmail.com'
expect(Redis::Alfred.get(cache_key)).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://accounts.google.com/o/oauth2/token')
.with(body: { 'code' => code, 'grant_type' => 'authorization_code',
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback" })
.to_return(status: 200, body: response_body_success.to_json, headers: { 'Content-Type' => 'application/json' })
get google_callback_url, params: { code: code }
expect(response).to redirect_to app_email_inbox_settings_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 'imap.gmail.com'
expect(Redis::Alfred.get(cache_key)).to be_nil
end
it 'creates inboxes with fallback_name when account name is not present in id_token' do
stub_request(:post, 'https://accounts.google.com/o/oauth2/token')
.with(body: { 'code' => code, 'grant_type' => 'authorization_code',
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback" })
.to_return(status: 200, body: response_body_success_without_name.to_json, headers: { 'Content-Type' => 'application/json' })
get google_callback_url, params: { code: code }
expect(response).to redirect_to app_email_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 email.split('@').first.parameterize.titleize
end
it 'redirects to google app in case of error' do
stub_request(:post, 'https://accounts.google.com/o/oauth2/token')
.with(body: { 'code' => code, 'grant_type' => 'authorization_code',
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback" })
.to_return(status: 401)
get google_callback_url, params: { code: code }
expect(response).to redirect_to '/'
expect(Redis::Alfred.get(cache_key).to_i).to eq account.id
end
end
end
@@ -4,9 +4,10 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do
let(:account) { create(:account) }
let(:code) { SecureRandom.hex(10) }
let(:email) { Faker::Internet.email }
let(:cache_key) { "microsoft::#{email.downcase}" }
before do
Redis::Alfred.set(email, account.id)
Redis::Alfred.set(cache_key, account.id)
end
describe 'GET /microsoft/callback' do
@@ -28,14 +29,14 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do
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(response).to redirect_to app_email_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
expect(Redis::Alfred.get(cache_key)).to be_nil
end
it 'creates updates inbox channel config if inbox exists and authentication is successful' do
@@ -49,12 +50,12 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do
get microsoft_callback_url, params: { code: code }
expect(response).to redirect_to app_microsoft_inbox_settings_url(account_id: account.id, inbox_id: account.inboxes.last.id)
expect(response).to redirect_to app_email_inbox_settings_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
expect(Redis::Alfred.get(cache_key)).to be_nil
end
it 'creates inboxes with fallback_name when account name is not present in id_token' do
@@ -65,7 +66,7 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do
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(response).to redirect_to app_email_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 email.split('@').first.parameterize.titleize
@@ -80,7 +81,7 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do
get microsoft_callback_url, params: { code: code }
expect(response).to redirect_to '/'
expect(Redis::Alfred.get(email).to_i).to eq account.id
expect(Redis::Alfred.get(cache_key).to_i).to eq account.id
end
end
end