feat: add notion integration app

This commit is contained in:
Shivam Mishra
2025-06-18 19:25:56 +05:30
parent f6dbbf0d90
commit f4b6ad6c90
9 changed files with 292 additions and 0 deletions
+4
View File
@@ -53,6 +53,10 @@ class Integrations::Hook < ApplicationRecord
app_id == 'dialogflow'
end
def notion?
app_id == 'notion'
end
def disable
update(status: 'disabled')
end
+7
View File
@@ -63,6 +63,13 @@ linear:
action: https://linear.app/oauth/authorize
hook_type: account
allow_multiple_hooks: false
notion:
id: notion
logo: notion.png
i18n_key: notion
action: https://api.notion.com/v1/oauth/authorize
hook_type: account
allow_multiple_hooks: false
slack:
id: slack
logo: slack.png
+4
View File
@@ -255,6 +255,10 @@ en:
name: 'Linear'
short_description: 'Create and link Linear issues directly from conversations.'
description: 'Create issues in Linear directly from your conversation window. Alternatively, link existing Linear issues for a more streamlined and efficient issue tracking process.'
notion:
name: 'Notion'
short_description: 'Integrate databases, documents and pages directly with Captain.'
description: 'Connect your Notion workspace to allow captain to generate responses from your databases, documents and pages.'
shopify:
name: 'Shopify'
short_description: 'Access order details and customer data from your Shopify store.'
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@@ -0,0 +1,53 @@
require 'rails_helper'
RSpec.describe 'Notion Authorization API', type: :request do
let(:account) { create(:account) }
describe 'POST /api/v1/accounts/{account.id}/notion/authorization' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/notion/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 unauthorized for agent' do
post "/api/v1/accounts/#{account.id}/notion/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}/notion/authorization",
headers: administrator.create_new_auth_token,
params: { email: administrator.email },
as: :json
expect(response).to have_http_status(:success)
# Validate URL components
url = response.parsed_body['url']
uri = URI.parse(url)
params = CGI.parse(uri.query)
expect(url).to start_with('https://api.notion.com/v1/oauth/authorize')
expect(params['response_type']).to eq(['code'])
expect(params['owner']).to eq(['user'])
expect(params['redirect_uri']).to eq(["#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/notion/callback"])
# Validate state parameter exists and can be decoded back to the account
expect(params['state']).to be_present
decoded_account = GlobalID::Locator.locate_signed(params['state'].first, for: 'default')
expect(decoded_account).to eq(account)
end
end
end
end
@@ -0,0 +1,62 @@
require 'rails_helper'
RSpec.describe NotionConcern, type: :concern do
let(:controller_class) do
Class.new do
include NotionConcern
end
end
let(:controller) { controller_class.new }
describe '#notion_client' do
let(:client_id) { 'test_notion_client_id' }
let(:client_secret) { 'test_notion_client_secret' }
before do
allow(GlobalConfigService).to receive(:load).with('NOTION_CLIENT_ID', nil).and_return(client_id)
allow(GlobalConfigService).to receive(:load).with('NOTION_CLIENT_SECRET', nil).and_return(client_secret)
end
it 'creates OAuth2 client with correct configuration' do
expect(OAuth2::Client).to receive(:new).with(
client_id,
client_secret,
{
site: 'https://api.notion.com',
authorize_url: 'https://api.notion.com/v1/oauth/authorize',
token_url: 'https://api.notion.com/v1/oauth/token',
auth_scheme: :basic_auth
}
)
controller.notion_client
end
it 'loads client credentials from GlobalConfigService' do
expect(GlobalConfigService).to receive(:load).with('NOTION_CLIENT_ID', nil)
expect(GlobalConfigService).to receive(:load).with('NOTION_CLIENT_SECRET', nil)
controller.notion_client
end
it 'returns OAuth2::Client instance' do
client = controller.notion_client
expect(client).to be_an_instance_of(OAuth2::Client)
end
it 'configures client with Notion-specific endpoints' do
client = controller.notion_client
expect(client.site).to eq('https://api.notion.com')
expect(client.options[:authorize_url]).to eq('https://api.notion.com/v1/oauth/authorize')
expect(client.options[:token_url]).to eq('https://api.notion.com/v1/oauth/token')
expect(client.options[:auth_scheme]).to eq(:basic_auth)
end
end
describe '#scope' do
it 'returns read scope for Notion API' do
expect(controller.send(:scope)).to eq('read')
end
end
end
@@ -0,0 +1,122 @@
require 'rails_helper'
RSpec.describe Notion::CallbacksController, type: :controller do
let(:account) { create(:account) }
let(:state) { account.to_sgid.to_s }
let(:oauth_code) { 'test_oauth_code' }
let(:mock_oauth_client) { instance_double(OAuth2::Client) }
let(:mock_auth_code) { instance_double(OAuth2::Strategy::AuthCode) }
let(:mock_token_response) { instance_double(OAuth2::AccessToken) }
let(:mock_response) { instance_double(Faraday::Response) }
let(:notion_response_body) do
{
'access_token' => 'notion_access_token_123',
'token_type' => 'bearer',
'workspace_name' => 'Test Workspace',
'workspace_id' => 'workspace_123',
'workspace_icon' => 'https://notion.so/icon.png',
'bot_id' => 'bot_123',
'owner' => {
'type' => 'user',
'user' => {
'id' => 'user_123',
'name' => 'Test User'
}
}
}
end
before do
allow(controller).to receive(:notion_client).and_return(mock_oauth_client)
allow(mock_oauth_client).to receive(:auth_code).and_return(mock_auth_code)
allow(mock_auth_code).to receive(:get_token).and_return(mock_token_response)
allow(mock_token_response).to receive(:response).and_return(mock_response)
allow(mock_response).to receive(:parsed).and_return(notion_response_body)
end
describe 'GET #show' do
context 'when OAuth callback is successful' do
it 'creates a new integration hook' do
expect do
get :show, params: { code: oauth_code, state: state }
end.to change(Integrations::Hook, :count).by(1)
end
it 'sets correct hook attributes' do
get :show, params: { code: oauth_code, state: state }
hook = Integrations::Hook.last
expect(hook.account).to eq(account)
expect(hook.app_id).to eq('notion')
expect(hook.access_token).to eq('notion_access_token_123')
expect(hook.status).to eq('enabled')
end
it 'stores notion workspace data in settings' do
get :show, params: { code: oauth_code, state: state }
hook = Integrations::Hook.last
expect(hook.settings['token_type']).to eq('bearer')
expect(hook.settings['workspace_name']).to eq('Test Workspace')
expect(hook.settings['workspace_id']).to eq('workspace_123')
expect(hook.settings['workspace_icon']).to eq('https://notion.so/icon.png')
expect(hook.settings['bot_id']).to eq('bot_123')
expect(hook.settings['owner']).to eq(notion_response_body['owner'])
end
it 'redirects to integration settings page' do
get :show, params: { code: oauth_code, state: state }
expect(response).to redirect_to("#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/app/accounts/#{account.id}/settings/integrations/notion")
end
it 'calls the OAuth client with correct parameters' do
expect(mock_auth_code).to receive(:get_token).with(
oauth_code,
redirect_uri: "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/notion/callback"
)
get :show, params: { code: oauth_code, state: state }
end
end
context 'when hook save fails during handle_response' do
it 'raises error and uses parent exception handling' do
mock_hook = instance_double(Integrations::Hook)
allow(account.hooks).to receive(:new).and_return(mock_hook)
allow(mock_hook).to receive(:save!).and_raise(StandardError, 'Save failed')
# Parent class handles exceptions with ChatwootExceptionTracker and redirects to '/'
expect(ChatwootExceptionTracker).to receive(:new).and_call_original
get :show, params: { code: oauth_code, state: state }
expect(response).to redirect_to('/')
end
end
end
describe 'provider-specific methods' do
describe '#provider_name' do
it 'returns notion' do
expect(controller.send(:provider_name)).to eq('notion')
end
end
describe '#oauth_client' do
it 'returns notion_client' do
expect(controller.send(:oauth_client)).to eq(mock_oauth_client)
end
end
describe '#notion_redirect_uri' do
it 'returns correct redirect URI' do
allow(controller).to receive(:account).and_return(account)
expected_uri = "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/app/accounts/#{account.id}/settings/integrations/notion"
expect(controller.send(:notion_redirect_uri)).to eq(expected_uri)
end
end
end
end
+40
View File
@@ -78,6 +78,46 @@ RSpec.describe Integrations::Hook do
end
end
describe 'app type methods' do
let(:account) { create(:account) }
describe '#slack?' do
it 'returns true for slack integration' do
hook = create(:integrations_hook, account: account, app_id: 'slack')
expect(hook.slack?).to be true
end
it 'returns false for non-slack integrations' do
hook = create(:integrations_hook, account: account, app_id: 'notion')
expect(hook.slack?).to be false
end
end
describe '#dialogflow?' do
it 'returns true for dialogflow integration' do
hook = create(:integrations_hook, account: account, app_id: 'dialogflow')
expect(hook.dialogflow?).to be true
end
it 'returns false for non-dialogflow integrations' do
hook = create(:integrations_hook, account: account, app_id: 'notion')
expect(hook.dialogflow?).to be false
end
end
describe '#notion?' do
it 'returns true for notion integration' do
hook = create(:integrations_hook, account: account, app_id: 'notion')
expect(hook.notion?).to be true
end
it 'returns false for non-notion integrations' do
hook = create(:integrations_hook, account: account, app_id: 'slack')
expect(hook.notion?).to be false
end
end
end
describe '#crm_integration?' do
let(:account) { create(:account) }