Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Integration Apps API', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
describe 'GET /api/v1/integrations/apps' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get api_v1_account_integrations_apps_url(account)
|
||||
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(:admin) { create(:user, account: account, role: :administrator) }
|
||||
|
||||
it 'returns all active apps without sensitive information if the user is an agent' do
|
||||
first_app = Integrations::App.all.find { |app| app.active?(account) }
|
||||
get api_v1_account_integrations_apps_url(account),
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
apps = response.parsed_body['payload'].first
|
||||
expect(apps['id']).to eql(first_app.id)
|
||||
expect(apps['name']).to eql(first_app.name)
|
||||
expect(apps['action']).to be_nil
|
||||
end
|
||||
|
||||
it 'will not return sensitive information for openai app for agents' do
|
||||
openai = create(:integrations_hook, :openai, account: account)
|
||||
get api_v1_account_integrations_apps_url(account),
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
app = response.parsed_body['payload'].find { |int_app| int_app['id'] == openai.app.id }
|
||||
expect(app['hooks'].first['settings']).to be_nil
|
||||
end
|
||||
|
||||
it 'returns all active apps with sensitive information if user is an admin' do
|
||||
first_app = Integrations::App.all.find { |app| app.active?(account) }
|
||||
get api_v1_account_integrations_apps_url(account),
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
apps = response.parsed_body['payload'].first
|
||||
expect(apps['id']).to eql(first_app.id)
|
||||
expect(apps['name']).to eql(first_app.name)
|
||||
expect(apps['action']).to eql(first_app.action)
|
||||
end
|
||||
|
||||
it 'returns slack app with appropriate redirect url when configured' do
|
||||
with_modified_env SLACK_CLIENT_ID: 'client_id', SLACK_CLIENT_SECRET: 'client_secret' do
|
||||
get api_v1_account_integrations_apps_url(account),
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
apps = response.parsed_body['payload']
|
||||
slack_app = apps.find { |app| app['id'] == 'slack' }
|
||||
expect(slack_app['action']).to include('client_id=client_id')
|
||||
end
|
||||
end
|
||||
|
||||
it 'will return sensitive information for openai app for admins' do
|
||||
openai = create(:integrations_hook, :openai, account: account)
|
||||
get api_v1_account_integrations_apps_url(account),
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
app = response.parsed_body['payload'].find { |int_app| int_app['id'] == openai.app.id }
|
||||
expect(app['hooks'].first['settings']).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/integrations/apps/:id' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get api_v1_account_integrations_app_url(account_id: account.id, id: 'slack')
|
||||
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(:admin) { create(:user, account: account, role: :administrator) }
|
||||
|
||||
it 'returns details of the app' do
|
||||
get api_v1_account_integrations_app_url(account_id: account.id, id: 'slack'),
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
app = response.parsed_body
|
||||
expect(app['id']).to eql('slack')
|
||||
expect(app['name']).to eql('Slack')
|
||||
end
|
||||
|
||||
it 'will not return sensitive information for openai app for agents' do
|
||||
openai = create(:integrations_hook, :openai, account: account)
|
||||
get api_v1_account_integrations_app_url(account_id: account.id, id: openai.app.id),
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
app = response.parsed_body
|
||||
expect(app['hooks'].first['settings']).to be_nil
|
||||
end
|
||||
|
||||
it 'will return sensitive information for openai app for admins' do
|
||||
openai = create(:integrations_hook, :openai, account: account)
|
||||
get api_v1_account_integrations_app_url(account_id: account.id, id: openai.app.id),
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
app = response.parsed_body
|
||||
expect(app['hooks'].first['settings']).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,138 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Dyte Integration API', type: :request do
|
||||
let(:headers) { { 'Content-Type' => 'application/json' } }
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:conversation) { create(:conversation, account: account, status: :pending) }
|
||||
let(:message) { create(:message, conversation: conversation, account: account, inbox: conversation.inbox) }
|
||||
let(:integration_message) do
|
||||
create(:message, content_type: 'integrations',
|
||||
content_attributes: { type: 'dyte', data: { meeting_id: 'm_id' } },
|
||||
conversation: conversation, account: account, inbox: conversation.inbox)
|
||||
end
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:unauthorized_agent) { create(:user, account: account, role: :agent) }
|
||||
|
||||
before do
|
||||
create(:integrations_hook, :dyte, account: account)
|
||||
create(:inbox_member, user: agent, inbox: conversation.inbox)
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/:account_id/integrations/dyte/create_a_meeting' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post create_a_meeting_api_v1_account_integrations_dyte_url(account)
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the agent does not have access to the inbox' do
|
||||
it 'returns unauthorized' do
|
||||
post create_a_meeting_api_v1_account_integrations_dyte_url(account),
|
||||
params: { conversation_id: conversation.display_id },
|
||||
headers: unauthorized_agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an agent with inbox access and the Dyte API is a success' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.dyte.io/v2/meetings')
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: { success: true, data: { id: 'meeting_id' } }.to_json,
|
||||
headers: headers
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns valid message payload' do
|
||||
post create_a_meeting_api_v1_account_integrations_dyte_url(account),
|
||||
params: { conversation_id: conversation.display_id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = response.parsed_body
|
||||
last_message = conversation.reload.messages.last
|
||||
expect(conversation.display_id).to eq(response_body['conversation_id'])
|
||||
expect(last_message.id).to eq(response_body['id'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an agent with inbox access and the Dyte API is errored' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.dyte.io/v2/meetings')
|
||||
.to_return(
|
||||
status: 422,
|
||||
body: { success: false, data: { message: 'Title is required' } }.to_json,
|
||||
headers: headers
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns error payload' do
|
||||
post create_a_meeting_api_v1_account_integrations_dyte_url(account),
|
||||
params: { conversation_id: conversation.display_id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body['error']).to eq({ 'data' => { 'message' => 'Title is required' }, 'success' => false })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/:account_id/integrations/dyte/add_participant_to_meeting' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post add_participant_to_meeting_api_v1_account_integrations_dyte_url(account)
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the agent does not have access to the inbox' do
|
||||
it 'returns unauthorized' do
|
||||
post add_participant_to_meeting_api_v1_account_integrations_dyte_url(account),
|
||||
params: { message_id: message.id },
|
||||
headers: unauthorized_agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an agent with inbox access and message_type is not integrations' do
|
||||
it 'returns error' do
|
||||
post add_participant_to_meeting_api_v1_account_integrations_dyte_url(account),
|
||||
params: { message_id: message.id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an agent with inbox access and message_type is integrations' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.dyte.io/v2/meetings/m_id/participants')
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: { success: true, data: { id: 'random_uuid', auth_token: 'json-web-token' } }.to_json,
|
||||
headers: headers
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns auth_token' do
|
||||
post add_participant_to_meeting_api_v1_account_integrations_dyte_url(account),
|
||||
params: { message_id: integration_message.id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body).to eq(
|
||||
{
|
||||
'id' => 'random_uuid', 'auth_token' => 'json-web-token'
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,137 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Integration Hooks API', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:params) { { app_id: 'dialogflow', inbox_id: inbox.id, settings: { project_id: 'xx', credentials: { test: 'test' }, region: 'europe-west1' } } }
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/integrations/hooks' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post api_v1_account_integrations_hooks_url(account_id: account.id),
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'return unauthorized if agent' do
|
||||
post api_v1_account_integrations_hooks_url(account_id: account.id),
|
||||
params: params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'creates hooks if admin' do
|
||||
post api_v1_account_integrations_hooks_url(account_id: account.id),
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data['app_id']).to eq params[:app_id]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PATCH /api/v1/accounts/{account.id}/integrations/hooks/{hook_id}' do
|
||||
let(:hook) { create(:integrations_hook, account: account) }
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
patch api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'return unauthorized if agent' do
|
||||
patch api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
|
||||
params: params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'updates hook if admin' do
|
||||
patch api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data['app_id']).to eq 'slack'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/integrations/hooks/{hook_id}/process_event' do
|
||||
let(:hook) { create(:integrations_hook, account: account) }
|
||||
let(:params) { { event: 'rephrase', payload: { test: 'test' } } }
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post process_event_api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'will process the events' do
|
||||
post process_event_api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
|
||||
params: params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq 'No processor found'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/accounts/{account.id}/integrations/hooks/{hook_id}' do
|
||||
let(:hook) { create(:integrations_hook, account: account) }
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
delete api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'return unauthorized if agent' do
|
||||
delete api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'updates hook if admin' do
|
||||
delete api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(Integrations::Hook.exists?(hook.id)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,330 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Linear Integration API', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user) }
|
||||
let(:api_key) { 'valid_api_key' }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:processor_service) { instance_double(Integrations::Linear::ProcessorService) }
|
||||
|
||||
before do
|
||||
create(:integrations_hook, :linear, account: account)
|
||||
allow(Integrations::Linear::ProcessorService).to receive(:new).with(account: account).and_return(processor_service)
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/accounts/:account_id/integrations/linear' do
|
||||
it 'deletes the linear integration' do
|
||||
# Stub the HTTP call to Linear's revoke endpoint
|
||||
allow(HTTParty).to receive(:post).with(
|
||||
'https://api.linear.app/oauth/revoke',
|
||||
anything
|
||||
).and_return(instance_double(HTTParty::Response, success?: true))
|
||||
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/linear",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(account.hooks.count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/accounts/:account_id/integrations/linear/teams' do
|
||||
context 'when it is an authenticated user' do
|
||||
context 'when data is retrieved successfully' do
|
||||
let(:teams_data) { { data: [{ 'id' => 'team1', 'name' => 'Team One' }] } }
|
||||
|
||||
it 'returns team data' do
|
||||
allow(processor_service).to receive(:teams).and_return(teams_data)
|
||||
get "/api/v1/accounts/#{account.id}/integrations/linear/teams",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('Team One')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when data retrieval fails' do
|
||||
it 'returns error message' do
|
||||
allow(processor_service).to receive(:teams).and_return(error: 'error message')
|
||||
get "/api/v1/accounts/#{account.id}/integrations/linear/teams",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.body).to include('error message')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/accounts/:account_id/integrations/linear/team_entities' do
|
||||
let(:team_id) { 'team1' }
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
context 'when data is retrieved successfully' do
|
||||
let(:team_entities_data) do
|
||||
{ data: {
|
||||
users: [{ 'id' => 'user1', 'name' => 'User One' }],
|
||||
projects: [{ 'id' => 'project1', 'name' => 'Project One' }],
|
||||
states: [{ 'id' => 'state1', 'name' => 'State One' }],
|
||||
labels: [{ 'id' => 'label1', 'name' => 'Label One' }]
|
||||
} }
|
||||
end
|
||||
|
||||
it 'returns team entities data' do
|
||||
allow(processor_service).to receive(:team_entities).with(team_id).and_return(team_entities_data)
|
||||
get "/api/v1/accounts/#{account.id}/integrations/linear/team_entities",
|
||||
params: { team_id: team_id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('User One')
|
||||
expect(response.body).to include('Project One')
|
||||
expect(response.body).to include('State One')
|
||||
expect(response.body).to include('Label One')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when data retrieval fails' do
|
||||
it 'returns error message' do
|
||||
allow(processor_service).to receive(:team_entities).with(team_id).and_return(error: 'error message')
|
||||
get "/api/v1/accounts/#{account.id}/integrations/linear/team_entities",
|
||||
params: { team_id: team_id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.body).to include('error message')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/:account_id/integrations/linear/create_issue' do
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:conversation) { create(:conversation, account: account, inbox: inbox) }
|
||||
let(:issue_params) do
|
||||
{
|
||||
team_id: 'team1',
|
||||
title: 'Sample Issue',
|
||||
description: 'This is a sample issue.',
|
||||
assignee_id: 'user1',
|
||||
priority: 'high',
|
||||
state_id: 'state1',
|
||||
label_ids: ['label1'],
|
||||
conversation_id: conversation.display_id
|
||||
}
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
context 'when the issue is created successfully' do
|
||||
let(:created_issue) { { data: { identifier: 'ENG-123', title: 'Sample Issue' } } }
|
||||
|
||||
it 'returns the created issue' do
|
||||
allow(processor_service).to receive(:create_issue).with(issue_params.stringify_keys, agent).and_return(created_issue)
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue",
|
||||
params: issue_params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('Sample Issue')
|
||||
end
|
||||
|
||||
it 'creates activity message when conversation is provided' do
|
||||
allow(processor_service).to receive(:create_issue).with(issue_params.stringify_keys, agent).and_return(created_issue)
|
||||
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue",
|
||||
params: issue_params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
.with(conversation, {
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: :activity,
|
||||
content: "Linear issue ENG-123 was created by #{agent.name}"
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issue creation fails' do
|
||||
it 'returns error message and does not create activity message' do
|
||||
allow(processor_service).to receive(:create_issue).with(issue_params.stringify_keys, agent).and_return(error: 'error message')
|
||||
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue",
|
||||
params: issue_params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.body).to include('error message')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/:account_id/integrations/linear/link_issue' do
|
||||
let(:issue_id) { 'ENG-456' }
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:link) { "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/conversations/#{conversation.display_id}" }
|
||||
let(:title) { 'Sample Issue' }
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
context 'when the issue is linked successfully' do
|
||||
let(:linked_issue) { { data: { 'id' => 'issue1', 'link' => 'https://linear.app/issue1' } } }
|
||||
|
||||
it 'returns the linked issue and creates activity message' do
|
||||
allow(processor_service).to receive(:link_issue).with(link, issue_id, title, agent).and_return(linked_issue)
|
||||
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/link_issue",
|
||||
params: { conversation_id: conversation.display_id, issue_id: issue_id, title: title },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
.with(conversation, {
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: :activity,
|
||||
content: "Linear issue ENG-456 was linked by #{agent.name}"
|
||||
})
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('https://linear.app/issue1')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issue linking fails' do
|
||||
it 'returns error message and does not create activity message' do
|
||||
allow(processor_service).to receive(:link_issue).with(link, issue_id, title, agent).and_return(error: 'error message')
|
||||
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/link_issue",
|
||||
params: { conversation_id: conversation.display_id, issue_id: issue_id, title: title },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.body).to include('error message')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/:account_id/integrations/linear/unlink_issue' do
|
||||
let(:link_id) { 'attachment1' }
|
||||
let(:issue_id) { 'ENG-789' }
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
context 'when the issue is unlinked successfully' do
|
||||
let(:unlinked_issue) { { data: { 'id' => 'issue1', 'link' => 'https://linear.app/issue1' } } }
|
||||
|
||||
it 'returns the unlinked issue and creates activity message' do
|
||||
allow(processor_service).to receive(:unlink_issue).with(link_id).and_return(unlinked_issue)
|
||||
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/unlink_issue",
|
||||
params: { link_id: link_id, issue_id: issue_id, conversation_id: conversation.display_id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
.with(conversation, {
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: :activity,
|
||||
content: "Linear issue ENG-789 was unlinked by #{agent.name}"
|
||||
})
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('https://linear.app/issue1')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issue unlinking fails' do
|
||||
it 'returns error message and does not create activity message' do
|
||||
allow(processor_service).to receive(:unlink_issue).with(link_id).and_return(error: 'error message')
|
||||
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/unlink_issue",
|
||||
params: { link_id: link_id, issue_id: issue_id, conversation_id: conversation.display_id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.body).to include('error message')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/accounts/:account_id/integrations/linear/search_issue' do
|
||||
let(:term) { 'issue' }
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
context 'when search is successful' do
|
||||
let(:search_results) { { data: [{ 'id' => 'issue1', 'title' => 'Sample Issue' }] } }
|
||||
|
||||
it 'returns search results' do
|
||||
allow(processor_service).to receive(:search_issue).with(term).and_return(search_results)
|
||||
get "/api/v1/accounts/#{account.id}/integrations/linear/search_issue",
|
||||
params: { q: term },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('Sample Issue')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when search fails' do
|
||||
it 'returns error message' do
|
||||
allow(processor_service).to receive(:search_issue).with(term).and_return(error: 'error message')
|
||||
get "/api/v1/accounts/#{account.id}/integrations/linear/search_issue",
|
||||
params: { q: term },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.body).to include('error message')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/accounts/:account_id/integrations/linear/linked_issues' do
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:link) { "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/conversations/#{conversation.display_id}" }
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
context 'when linked issue is found' do
|
||||
let(:linked_issue) { { data: [{ 'id' => 'issue1', 'title' => 'Sample Issue' }] } }
|
||||
|
||||
it 'returns linked issue' do
|
||||
allow(processor_service).to receive(:linked_issues).with(link).and_return(linked_issue)
|
||||
get "/api/v1/accounts/#{account.id}/integrations/linear/linked_issues",
|
||||
params: { conversation_id: conversation.display_id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('Sample Issue')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when linked issue is not found' do
|
||||
it 'returns error message' do
|
||||
allow(processor_service).to receive(:linked_issues).with(link).and_return(error: 'error message')
|
||||
get "/api/v1/accounts/#{account.id}/integrations/linear/linked_issues",
|
||||
params: { conversation_id: conversation.display_id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.body).to include('error message')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,187 @@
|
||||
require 'rails_helper'
|
||||
|
||||
# Stub class for ShopifyAPI response
|
||||
class ShopifyAPIResponse
|
||||
attr_reader :body
|
||||
|
||||
def initialize(body)
|
||||
@body = body
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe 'Shopify Integration API', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:unauthorized_agent) { create(:user, account: account, role: :agent) }
|
||||
let(:contact) { create(:contact, account: account, email: 'test@example.com', phone_number: '+1234567890') }
|
||||
|
||||
describe 'POST /api/v1/accounts/:account_id/integrations/shopify/auth' do
|
||||
let(:shop_domain) { 'test-store.myshopify.com' }
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'returns a redirect URL for Shopify OAuth' do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/shopify/auth",
|
||||
params: { shop_domain: shop_domain },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.parsed_body).to have_key('redirect_url')
|
||||
expect(response.parsed_body['redirect_url']).to include(shop_domain)
|
||||
end
|
||||
|
||||
it 'returns error when shop domain is missing' do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/shopify/auth",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Shop domain is required')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/shopify/auth",
|
||||
params: { shop_domain: shop_domain },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/accounts/:account_id/integrations/shopify/orders' do
|
||||
before do
|
||||
create(:integrations_hook, :shopify, account: account)
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
# rubocop:disable RSpec/AnyInstance
|
||||
let(:shopify_client) { instance_double(ShopifyAPI::Clients::Rest::Admin) }
|
||||
|
||||
let(:customers_response) do
|
||||
instance_double(
|
||||
ShopifyAPIResponse,
|
||||
body: { 'customers' => [{ 'id' => '123' }] }
|
||||
)
|
||||
end
|
||||
|
||||
let(:orders_response) do
|
||||
instance_double(
|
||||
ShopifyAPIResponse,
|
||||
body: {
|
||||
'orders' => [{
|
||||
'id' => '456',
|
||||
'email' => 'test@example.com',
|
||||
'created_at' => Time.now.iso8601,
|
||||
'total_price' => '100.00',
|
||||
'currency' => 'USD',
|
||||
'fulfillment_status' => 'fulfilled',
|
||||
'financial_status' => 'paid'
|
||||
}]
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
allow_any_instance_of(Api::V1::Accounts::Integrations::ShopifyController).to receive(:shopify_client).and_return(shopify_client)
|
||||
|
||||
allow_any_instance_of(Api::V1::Accounts::Integrations::ShopifyController).to receive(:client_id).and_return('test_client_id')
|
||||
allow_any_instance_of(Api::V1::Accounts::Integrations::ShopifyController).to receive(:client_secret).and_return('test_client_secret')
|
||||
|
||||
allow(shopify_client).to receive(:get).with(
|
||||
path: 'customers/search.json',
|
||||
query: { query: "email:#{contact.email} OR phone:#{contact.phone_number}", fields: 'id,email,phone' }
|
||||
).and_return(customers_response)
|
||||
|
||||
allow(shopify_client).to receive(:get).with(
|
||||
path: 'orders.json',
|
||||
query: { customer_id: '123', status: 'any', fields: 'id,email,created_at,total_price,currency,fulfillment_status,financial_status' }
|
||||
).and_return(orders_response)
|
||||
end
|
||||
|
||||
it 'returns orders for the contact' do
|
||||
get "/api/v1/accounts/#{account.id}/integrations/shopify/orders",
|
||||
params: { contact_id: contact.id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.parsed_body).to have_key('orders')
|
||||
expect(response.parsed_body['orders'].length).to eq(1)
|
||||
expect(response.parsed_body['orders'][0]['id']).to eq('456')
|
||||
end
|
||||
|
||||
it 'returns error when contact has no email or phone' do
|
||||
contact_without_info = create(:contact, account: account)
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/integrations/shopify/orders",
|
||||
params: { contact_id: contact_without_info.id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Contact information missing')
|
||||
end
|
||||
|
||||
it 'returns empty array when no customers found' do
|
||||
empty_customers_response = instance_double(
|
||||
ShopifyAPIResponse,
|
||||
body: { 'customers' => [] }
|
||||
)
|
||||
|
||||
allow(shopify_client).to receive(:get).with(
|
||||
path: 'customers/search.json',
|
||||
query: { query: "email:#{contact.email} OR phone:#{contact.phone_number}", fields: 'id,email,phone' }
|
||||
).and_return(empty_customers_response)
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/integrations/shopify/orders",
|
||||
params: { contact_id: contact.id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.parsed_body['orders']).to eq([])
|
||||
end
|
||||
# rubocop:enable RSpec/AnyInstance
|
||||
end
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v1/accounts/#{account.id}/integrations/shopify/orders",
|
||||
params: { contact_id: contact.id },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/accounts/:account_id/integrations/shopify' do
|
||||
before do
|
||||
create(:integrations_hook, :shopify, account: account)
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'deletes the shopify integration' do
|
||||
expect do
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/shopify",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.to change { account.hooks.count }.by(-1)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/shopify",
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user