Restructure omni services and add Chatwoot research snapshot

This commit is contained in:
Ruslan Bakiev
2026-02-21 11:11:27 +07:00
parent edea7a0034
commit b73babbbf6
7732 changed files with 978203 additions and 32 deletions

View File

@@ -0,0 +1,80 @@
require 'rails_helper'
RSpec.describe 'Agent Capacity Policy Inbox Limits API', type: :request do
let(:account) { create(:account) }
let!(:agent_capacity_policy) { create(:agent_capacity_policy, account: account) }
let!(:inbox) { create(:inbox, account: account) }
let(:administrator) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
describe 'POST /api/v1/accounts/{account.id}/agent_capacity_policies/{policy_id}/inbox_limits' do
context 'when not admin' do
it 'requires admin role' do
post "/api/v1/accounts/#{account.id}/agent_capacity_policies/#{agent_capacity_policy.id}/inbox_limits",
params: { inbox_id: inbox.id, conversation_limit: 10 },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when admin' do
it 'creates an inbox limit' do
post "/api/v1/accounts/#{account.id}/agent_capacity_policies/#{agent_capacity_policy.id}/inbox_limits",
params: { inbox_id: inbox.id, conversation_limit: 10 },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['conversation_limit']).to eq(10)
expect(json_response['inbox_id']).to eq(inbox.id)
end
it 'prevents duplicate inbox assignments' do
create(:inbox_capacity_limit, agent_capacity_policy: agent_capacity_policy, inbox: inbox)
post "/api/v1/accounts/#{account.id}/agent_capacity_policies/#{agent_capacity_policy.id}/inbox_limits",
params: { inbox_id: inbox.id, conversation_limit: 10 },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq(I18n.t('agent_capacity_policy.inbox_already_assigned'))
end
end
end
describe 'PUT /api/v1/accounts/{account.id}/agent_capacity_policies/{policy_id}/inbox_limits/{id}' do
let!(:inbox_limit) { create(:inbox_capacity_limit, agent_capacity_policy: agent_capacity_policy, inbox: inbox, conversation_limit: 5) }
context 'when admin' do
it 'updates the inbox limit' do
put "/api/v1/accounts/#{account.id}/agent_capacity_policies/#{agent_capacity_policy.id}/inbox_limits/#{inbox_limit.id}",
params: { conversation_limit: 15 },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['conversation_limit']).to eq(15)
expect(inbox_limit.reload.conversation_limit).to eq(15)
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/agent_capacity_policies/{policy_id}/inbox_limits/{id}' do
let!(:inbox_limit) { create(:inbox_capacity_limit, agent_capacity_policy: agent_capacity_policy, inbox: inbox) }
context 'when admin' do
it 'removes the inbox limit' do
delete "/api/v1/accounts/#{account.id}/agent_capacity_policies/#{agent_capacity_policy.id}/inbox_limits/#{inbox_limit.id}",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:no_content)
expect(agent_capacity_policy.inbox_capacity_limits.find_by(id: inbox_limit.id)).to be_nil
end
end
end
end

View File

@@ -0,0 +1,86 @@
require 'rails_helper'
RSpec.describe 'Agent Capacity Policy Users API', type: :request do
let(:account) { create(:account) }
let!(:agent_capacity_policy) { create(:agent_capacity_policy, account: account) }
let!(:user) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
describe 'GET /api/v1/accounts/{account.id}/agent_capacity_policies/{policy_id}/users' do
context 'when admin' do
it 'returns assigned users' do
user.account_users.first.update!(agent_capacity_policy: agent_capacity_policy)
get "/api/v1/accounts/#{account.id}/agent_capacity_policies/#{agent_capacity_policy.id}/users",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body.first['id']).to eq(user.id)
end
it 'returns each user only once without duplicates' do
# Assign multiple users to the same policy
user.account_users.first.update!(agent_capacity_policy: agent_capacity_policy)
agent.account_users.first.update!(agent_capacity_policy: agent_capacity_policy)
get "/api/v1/accounts/#{account.id}/agent_capacity_policies/#{agent_capacity_policy.id}/users",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
# Check that we have exactly 2 users
expect(response.parsed_body.length).to eq(2)
# Check that each user appears only once
user_ids = response.parsed_body.map { |u| u['id'] }
expect(user_ids).to contain_exactly(user.id, agent.id)
expect(user_ids.uniq).to eq(user_ids) # No duplicates
end
end
end
describe 'POST /api/v1/accounts/{account.id}/agent_capacity_policies/{policy_id}/users' do
context 'when not admin' do
it 'requires admin role' do
post "/api/v1/accounts/#{account.id}/agent_capacity_policies/#{agent_capacity_policy.id}/users",
params: { user_id: user.id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when admin' do
it 'assigns user to the policy' do
post "/api/v1/accounts/#{account.id}/agent_capacity_policies/#{agent_capacity_policy.id}/users",
params: { user_id: user.id },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(user.account_users.first.reload.agent_capacity_policy).to eq(agent_capacity_policy)
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/agent_capacity_policies/{policy_id}/users/{id}' do
context 'when admin' do
before do
user.account_users.first.update!(agent_capacity_policy: agent_capacity_policy)
end
it 'removes user from the policy' do
delete "/api/v1/accounts/#{account.id}/agent_capacity_policies/#{agent_capacity_policy.id}/users/#{user.id}",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(user.account_users.first.reload.agent_capacity_policy).to be_nil
end
end
end
end

View File

@@ -0,0 +1,231 @@
require 'rails_helper'
RSpec.describe 'Agent Capacity Policies API', type: :request do
let(:account) { create(:account) }
let!(:agent_capacity_policy) { create(:agent_capacity_policy, account: account) }
describe 'GET /api/v1/accounts/{account.id}/agent_capacity_policies' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/agent_capacity_policies"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated agent' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'returns unauthorized for agent' do
get "/api/v1/accounts/#{account.id}/agent_capacity_policies",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an administrator' do
let(:administrator) { create(:user, account: account, role: :administrator) }
it 'returns all agent capacity policies' do
get "/api/v1/accounts/#{account.id}/agent_capacity_policies",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body.first['id']).to eq(agent_capacity_policy.id)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/agent_capacity_policies/{id}' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/agent_capacity_policies/#{agent_capacity_policy.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated agent' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'returns unauthorized for agent' do
get "/api/v1/accounts/#{account.id}/agent_capacity_policies/#{agent_capacity_policy.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an administrator' do
let(:administrator) { create(:user, account: account, role: :administrator) }
it 'returns the agent capacity policy' do
get "/api/v1/accounts/#{account.id}/agent_capacity_policies/#{agent_capacity_policy.id}",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['id']).to eq(agent_capacity_policy.id)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/agent_capacity_policies' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/agent_capacity_policies"
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
params = { agent_capacity_policy: { name: 'Test Policy' } }
post "/api/v1/accounts/#{account.id}/agent_capacity_policies",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'creates a new agent capacity policy when administrator' do
params = {
agent_capacity_policy: {
name: 'Test Policy',
description: 'Test Description',
exclusion_rules: {
excluded_labels: %w[urgent spam],
exclude_older_than_hours: 24
}
}
}
post "/api/v1/accounts/#{account.id}/agent_capacity_policies",
params: params,
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['name']).to eq('Test Policy')
expect(response.parsed_body['description']).to eq('Test Description')
expect(response.parsed_body['exclusion_rules']).to eq({
'excluded_labels' => %w[urgent spam],
'exclude_older_than_hours' => 24
})
end
it 'returns validation errors for invalid data' do
params = { agent_capacity_policy: { name: '' } }
post "/api/v1/accounts/#{account.id}/agent_capacity_policies",
params: params,
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
describe 'PUT /api/v1/accounts/{account.id}/agent_capacity_policies/{id}' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/agent_capacity_policies/#{agent_capacity_policy.id}"
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
params = { agent_capacity_policy: { name: 'Updated Policy' } }
put "/api/v1/accounts/#{account.id}/agent_capacity_policies/#{agent_capacity_policy.id}",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'updates the agent capacity policy when administrator' do
params = { agent_capacity_policy: { name: 'Updated Policy' } }
put "/api/v1/accounts/#{account.id}/agent_capacity_policies/#{agent_capacity_policy.id}",
params: params,
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['name']).to eq('Updated Policy')
end
it 'updates exclusion rules when administrator' do
params = {
agent_capacity_policy: {
exclusion_rules: {
excluded_labels: %w[vip priority],
exclude_older_than_hours: 48
}
}
}
put "/api/v1/accounts/#{account.id}/agent_capacity_policies/#{agent_capacity_policy.id}",
params: params,
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['exclusion_rules']).to eq({
'excluded_labels' => %w[vip priority],
'exclude_older_than_hours' => 48
})
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/agent_capacity_policies/{id}' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/agent_capacity_policies/#{agent_capacity_policy.id}"
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
delete "/api/v1/accounts/#{account.id}/agent_capacity_policies/#{agent_capacity_policy.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'deletes the agent capacity policy when administrator' do
delete "/api/v1/accounts/#{account.id}/agent_capacity_policies/#{agent_capacity_policy.id}",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect { agent_capacity_policy.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
end

View File

@@ -0,0 +1,55 @@
require 'rails_helper'
RSpec.describe 'Agents API', type: :request do
include ActiveJob::TestHelper
let(:account) { create(:account) }
let!(:admin) { create(:user, custom_attributes: { test: 'test' }, account: account, role: :administrator) }
describe 'POST /api/v1/accounts/{account.id}/agents' do
context 'when the account has reached its agent limit' do
params = { name: 'NewUser', email: Faker::Internet.email, role: :agent }
before do
account.update(limits: { agents: 4 })
create_list(:user, 4, account: account, role: :agent)
end
it 'prevents adding a new agent and returns a payment required status' do
post "/api/v1/accounts/#{account.id}/agents", params: params, headers: admin.create_new_auth_token, as: :json
expect(response).to have_http_status(:payment_required)
expect(response.body).to include('Account limit exceeded. Please purchase more licenses')
end
end
end
describe 'POST /api/v1/accounts/{account.id}/agents/bulk_create' do
let(:emails) { ['test1@example.com', 'test2@example.com', 'test3@example.com'] }
let(:bulk_create_params) { { emails: emails } }
context 'when exceeding agent limit' do
it 'prevents creating agents and returns a payment required status' do
# Set the limit to be less than the number of emails
account.update(limits: { agents: 2 })
expect do
post "/api/v1/accounts/#{account.id}/agents/bulk_create", params: bulk_create_params, headers: admin.create_new_auth_token
end.not_to change(User, :count)
expect(response).to have_http_status(:payment_required)
expect(response.body).to include('Account limit exceeded. Please purchase more licenses')
end
end
context 'when onboarding step is present in account custom attributes' do
it 'removes onboarding step from account custom attributes' do
account.update(custom_attributes: { onboarding_step: 'completed' })
post "/api/v1/accounts/#{account.id}/agents/bulk_create", params: bulk_create_params, headers: admin.create_new_auth_token
expect(account.reload.custom_attributes).not_to include('onboarding_step')
end
end
end
end

View File

@@ -0,0 +1,217 @@
require 'rails_helper'
RSpec.describe 'Applied SLAs API', type: :request do
let(:account) { create(:account) }
let(:administrator) { create(:user, account: account, role: :administrator) }
let(:agent1) { create(:user, account: account, role: :agent) }
let(:agent2) { create(:user, account: account, role: :agent) }
let(:conversation1) { create(:conversation, account: account, assignee: agent1) }
let(:conversation2) { create(:conversation, account: account, assignee: agent2) }
let(:conversation3) { create(:conversation, account: account, assignee: agent2) }
let(:sla_policy1) { create(:sla_policy, account: account) }
let(:sla_policy2) { create(:sla_policy, account: account) }
before do
AppliedSla.destroy_all
end
describe 'GET /api/v1/accounts/{account.id}/applied_slas/metrics' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/applied_slas/metrics"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'returns the sla metrics' do
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, sla_status: 'missed')
get "/api/v1/accounts/#{account.id}/applied_slas/metrics",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body).to include('total_applied_slas' => 1)
expect(body).to include('number_of_sla_misses' => 1)
expect(body).to include('hit_rate' => '0.0%')
end
it 'filters sla metrics based on a date range' do
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago)
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago)
get "/api/v1/accounts/#{account.id}/applied_slas/metrics",
params: { since: 5.days.ago.to_time.to_i.to_s, until: Time.zone.today.to_time.to_i.to_s },
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body).to include('total_applied_slas' => 1)
expect(body).to include('number_of_sla_misses' => 0)
expect(body).to include('hit_rate' => '100%')
end
it 'filters sla metrics based on a date range and agent ids' do
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago)
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation3, created_at: 3.days.ago)
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago, sla_status: 'missed')
get "/api/v1/accounts/#{account.id}/applied_slas/metrics",
params: { agent_ids: [agent2.id] },
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body).to include('total_applied_slas' => 3)
expect(body).to include('number_of_sla_misses' => 1)
expect(body).to include('hit_rate' => '66.67%')
end
it 'filters sla metrics based on sla policy ids' do
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1)
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, sla_status: 'missed')
create(:applied_sla, sla_policy: sla_policy2, conversation: conversation2, sla_status: 'missed')
get "/api/v1/accounts/#{account.id}/applied_slas/metrics",
params: { sla_policy_id: sla_policy1.id },
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body).to include('total_applied_slas' => 2)
expect(body).to include('number_of_sla_misses' => 1)
expect(body).to include('hit_rate' => '50.0%')
end
it 'filters sla metrics based on labels' do
conversation2.update_labels('label1')
conversation3.update_labels('label1')
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago)
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago, sla_status: 'missed')
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation3, created_at: 3.days.ago)
get "/api/v1/accounts/#{account.id}/applied_slas/metrics",
params: { label_list: 'label1' },
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body).to include('total_applied_slas' => 2)
expect(body).to include('number_of_sla_misses' => 1)
expect(body).to include('hit_rate' => '50.0%')
end
end
end
describe 'GET /api/v1/accounts/{account.id}/applied_slas/download' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/applied_slas/download"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'returns a CSV file with breached conversations' do
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, sla_status: 'missed')
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, sla_status: 'missed')
conversation1.update(status: 'open')
conversation2.update(status: 'resolved')
get "/api/v1/accounts/#{account.id}/applied_slas/download",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
expect(response.headers['Content-Type']).to eq('text/csv')
expect(response.headers['Content-Disposition']).to include('attachment; filename=breached_conversation.csv')
csv_data = CSV.parse(response.body)
csv_data.reject! { |row| row.all?(&:nil?) }
expect(csv_data.size).to eq(3)
expect(csv_data[1][0].to_i).to eq(conversation1.display_id)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/applied_slas' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/applied_slas"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'returns the applied slas' do
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1)
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, sla_status: 'missed')
get "/api/v1/accounts/#{account.id}/applied_slas",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['payload'].size).to eq(1)
expect(body['payload'].first).to include('applied_sla')
expect(body['payload'].first['conversation']['id']).to eq(conversation2.display_id)
expect(body['meta']).to include('count' => 1)
end
it 'filters applied slas based on a date range' do
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago, sla_status: 'missed')
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago, sla_status: 'missed')
get "/api/v1/accounts/#{account.id}/applied_slas",
params: { since: 5.days.ago.to_time.to_i.to_s, until: Time.zone.today.to_time.to_i.to_s },
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['payload'].size).to eq(1)
end
it 'filters applied slas based on a date range and agent ids' do
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago)
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation3, created_at: 3.days.ago, sla_status: 'missed')
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago, sla_status: 'active_with_misses')
get "/api/v1/accounts/#{account.id}/applied_slas",
params: { agent_ids: [agent2.id] },
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['payload'].size).to eq(2)
end
it 'filters applied slas based on sla policy ids' do
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, sla_status: 'missed')
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2)
create(:applied_sla, sla_policy: sla_policy2, conversation: conversation2, sla_status: 'active_with_misses')
get "/api/v1/accounts/#{account.id}/applied_slas",
params: { sla_policy_id: sla_policy1.id },
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['payload'].size).to eq(1)
end
it 'filters applied slas based on labels' do
conversation2.update_labels('label1')
conversation3.update_labels('label1')
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago, sla_status: 'active_with_misses')
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago, sla_status: 'missed')
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation3, created_at: 3.days.ago, sla_status: 'missed')
get "/api/v1/accounts/#{account.id}/applied_slas",
params: { label_list: 'label1' },
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['payload'].size).to eq(2)
end
end
end
end

View File

@@ -0,0 +1,61 @@
require 'rails_helper'
RSpec.describe 'Enterprise Audit API', type: :request do
let!(:account) { create(:account) }
let!(:admin) { create(:user, account: account, role: :administrator) }
let!(:inbox) { create(:inbox, account: account) }
describe 'GET /api/v1/accounts/{account.id}/audit_logs' do
context 'when it is an un-authenticated user' do
it 'does not fetch audit logs associated with the account' do
get "/api/v1/accounts/#{account.id}/audit_logs",
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated normal user' do
let(:user) { create(:user, account: account) }
it 'fetches audit logs associated with the account' do
get "/api/v1/accounts/#{account.id}/audit_logs",
headers: user.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
# check for response in parse
context 'when it is an authenticated admin user' do
it 'returns empty array if feature is not enabled' do
get "/api/v1/accounts/#{account.id}/audit_logs",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response['audit_logs']).to eql([])
end
it 'fetches audit logs associated with the account' do
account.enable_features(:audit_logs)
account.save!
get "/api/v1/accounts/#{account.id}/audit_logs",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response['audit_logs'][1]['auditable_type']).to eql('Inbox')
expect(json_response['audit_logs'][1]['action']).to eql('create')
expect(json_response['audit_logs'][1]['audited_changes']['name']).to eql(inbox.name)
expect(json_response['audit_logs'][1]['associated_id']).to eql(account.id)
expect(json_response['current_page']).to be(1)
# contains audit log for account user as well
# contains audit logs for account update(enable audit logs)
expect(json_response['total_entries']).to be(3)
end
end
end
end

View File

@@ -0,0 +1,270 @@
require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::Captain::AssistantResponses', type: :request do
let(:account) { create(:account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:document) { create(:captain_document, assistant: assistant, account: account) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:another_assistant) { create(:captain_assistant, account: account) }
let(:another_document) { create(:captain_document, account: account, assistant: assistant) }
def json_response
JSON.parse(response.body, symbolize_names: true)
end
describe 'GET /api/v1/accounts/:account_id/captain/assistant_responses' do
context 'when no filters are applied' do
before do
create_list(:captain_assistant_response, 30,
account: account,
assistant: assistant,
documentable: document)
end
it 'returns first page of responses with default pagination' do
get "/api/v1/accounts/#{account.id}/captain/assistant_responses",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(json_response[:payload].length).to eq(25)
end
it 'returns second page of responses' do
get "/api/v1/accounts/#{account.id}/captain/assistant_responses",
params: { page: 2 },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(json_response[:payload].length).to eq(5)
expect(json_response[:meta]).to eq({ page: 2, total_count: 30 })
end
end
context 'when filtering by assistant_id' do
before do
create_list(:captain_assistant_response, 3,
account: account,
assistant: assistant,
documentable: document)
create_list(:captain_assistant_response, 2,
account: account,
assistant: another_assistant,
documentable: document)
end
it 'returns only responses for the specified assistant' do
get "/api/v1/accounts/#{account.id}/captain/assistant_responses",
params: { assistant_id: assistant.id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(json_response[:payload].length).to eq(3)
expect(json_response[:payload][0][:assistant][:id]).to eq(assistant.id)
end
end
context 'when filtering by document_id' do
before do
create_list(:captain_assistant_response, 3,
account: account,
assistant: assistant,
documentable: document)
create_list(:captain_assistant_response, 2,
account: account,
assistant: assistant,
documentable: another_document)
end
it 'returns only responses for the specified document' do
get "/api/v1/accounts/#{account.id}/captain/assistant_responses",
params: { document_id: document.id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(json_response[:payload].length).to eq(3)
expect(json_response[:payload][0][:documentable][:id]).to eq(document.id)
end
end
context 'when searching' do
before do
create(:captain_assistant_response,
account: account,
assistant: assistant,
question: 'How to reset password?',
answer: 'Click forgot password')
create(:captain_assistant_response,
account: account,
assistant: assistant,
question: 'How to change email?',
answer: 'Go to settings')
end
it 'finds responses by question text' do
get "/api/v1/accounts/#{account.id}/captain/assistant_responses",
params: { search: 'password' },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(json_response[:payload].length).to eq(1)
expect(json_response[:payload][0][:question]).to include('password')
end
it 'finds responses by answer text' do
get "/api/v1/accounts/#{account.id}/captain/assistant_responses",
params: { search: 'settings' },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(json_response[:payload].length).to eq(1)
expect(json_response[:payload][0][:answer]).to include('settings')
end
it 'returns empty when no matches' do
get "/api/v1/accounts/#{account.id}/captain/assistant_responses",
params: { search: 'nonexistent' },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(json_response[:payload].length).to eq(0)
end
end
end
describe 'GET /api/v1/accounts/:account_id/captain/assistant_responses/:id' do
let!(:response_record) { create(:captain_assistant_response, assistant: assistant, account: account) }
it 'returns the requested response if the user is agent or admin' do
get "/api/v1/accounts/#{account.id}/captain/assistant_responses/#{response_record.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(json_response[:id]).to eq(response_record.id)
expect(json_response[:question]).to eq(response_record.question)
expect(json_response[:answer]).to eq(response_record.answer)
end
end
describe 'POST /api/v1/accounts/:account_id/captain/assistant_responses' do
let(:valid_params) do
{
assistant_response: {
question: 'Test question?',
answer: 'Test answer',
assistant_id: assistant.id
}
}
end
it 'creates a new response if the user is an admin' do
expect do
post "/api/v1/accounts/#{account.id}/captain/assistant_responses",
params: valid_params,
headers: admin.create_new_auth_token,
as: :json
end.to change(Captain::AssistantResponse, :count).by(1)
expect(response).to have_http_status(:success)
expect(json_response[:question]).to eq('Test question?')
expect(json_response[:answer]).to eq('Test answer')
end
context 'with invalid params' do
let(:invalid_params) do
{
assistant_response: {
question: 'Test',
answer: 'Test'
}
}
end
it 'returns unprocessable entity status' do
post "/api/v1/accounts/#{account.id}/captain/assistant_responses",
params: invalid_params,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
describe 'PATCH /api/v1/accounts/:account_id/captain/assistant_responses/:id' do
let!(:response_record) { create(:captain_assistant_response, assistant: assistant) }
let(:update_params) do
{
assistant_response: {
question: 'Updated question?',
answer: 'Updated answer'
}
}
end
it 'updates the response if the user is an admin' do
patch "/api/v1/accounts/#{account.id}/captain/assistant_responses/#{response_record.id}",
params: update_params,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(json_response[:question]).to eq('Updated question?')
expect(json_response[:answer]).to eq('Updated answer')
end
context 'with invalid params' do
let(:invalid_params) do
{
assistant_response: {
question: '',
answer: ''
}
}
end
it 'returns unprocessable entity status' do
patch "/api/v1/accounts/#{account.id}/captain/assistant_responses/#{response_record.id}",
params: invalid_params,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
describe 'DELETE /api/v1/accounts/:account_id/captain/assistant_responses/:id' do
let!(:response_record) { create(:captain_assistant_response, assistant: assistant) }
it 'deletes the response' do
expect do
delete "/api/v1/accounts/#{account.id}/captain/assistant_responses/#{response_record.id}",
headers: admin.create_new_auth_token,
as: :json
end.to change(Captain::AssistantResponse, :count).by(-1)
expect(response).to have_http_status(:no_content)
end
context 'with invalid id' do
it 'returns not found' do
delete "/api/v1/accounts/#{account.id}/captain/assistant_responses/0",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:not_found)
end
end
end
end

View File

@@ -0,0 +1,317 @@
require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::Captain::Assistants', type: :request do
let(:account) { create(:account) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
def json_response
JSON.parse(response.body, symbolize_names: true)
end
describe 'GET /api/v1/accounts/{account.id}/captain/assistants' do
context 'when it is an un-authenticated user' do
it 'does not fetch assistants' do
get "/api/v1/accounts/#{account.id}/captain/assistants",
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
it 'fetches assistants for the account' do
create_list(:captain_assistant, 3, account: account)
get "/api/v1/accounts/#{account.id}/captain/assistants",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response[:payload].length).to eq(3)
expect(json_response[:meta]).to eq(
{ total_count: 3, page: 1 }
)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/captain/assistants/{id}' do
let(:assistant) { create(:captain_assistant, account: account) }
context 'when it is an un-authenticated user' do
it 'does not fetch the assistant' do
get "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}",
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
it 'fetches the assistant' do
get "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response[:id]).to eq(assistant.id)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/captain/assistants' do
let(:valid_attributes) do
{
assistant: {
name: 'New Assistant',
description: 'Assistant Description',
response_guidelines: ['Be helpful', 'Be concise'],
guardrails: ['No harmful content', 'Stay on topic'],
config: {
product_name: 'Chatwoot',
feature_faq: true,
feature_memory: false,
feature_citation: true
}
}
}
end
context 'when it is an un-authenticated user' do
it 'does not create an assistant' do
post "/api/v1/accounts/#{account.id}/captain/assistants",
params: valid_attributes,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
it 'does not create an assistant' do
post "/api/v1/accounts/#{account.id}/captain/assistants",
params: valid_attributes,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an admin' do
it 'creates a new assistant' do
expect do
post "/api/v1/accounts/#{account.id}/captain/assistants",
params: valid_attributes,
headers: admin.create_new_auth_token,
as: :json
end.to change(Captain::Assistant, :count).by(1)
expect(json_response[:name]).to eq('New Assistant')
expect(json_response[:response_guidelines]).to eq(['Be helpful', 'Be concise'])
expect(json_response[:guardrails]).to eq(['No harmful content', 'Stay on topic'])
expect(json_response[:config][:product_name]).to eq('Chatwoot')
expect(json_response[:config][:feature_citation]).to be(true)
expect(response).to have_http_status(:success)
end
it 'creates an assistant with feature_citation disabled' do
attributes_with_disabled_citation = valid_attributes.deep_dup
attributes_with_disabled_citation[:assistant][:config][:feature_citation] = false
expect do
post "/api/v1/accounts/#{account.id}/captain/assistants",
params: attributes_with_disabled_citation,
headers: admin.create_new_auth_token,
as: :json
end.to change(Captain::Assistant, :count).by(1)
expect(json_response[:config][:feature_citation]).to be(false)
expect(response).to have_http_status(:success)
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/captain/assistants/{id}' do
let(:assistant) { create(:captain_assistant, account: account) }
let(:update_attributes) do
{
assistant: {
name: 'Updated Assistant',
response_guidelines: ['Updated guideline'],
guardrails: ['Updated guardrail'],
config: {
feature_citation: false
}
}
}
end
context 'when it is an un-authenticated user' do
it 'does not update the assistant' do
patch "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}",
params: update_attributes,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
it 'does not update the assistant' do
patch "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}",
params: update_attributes,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an admin' do
it 'updates the assistant' do
patch "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}",
params: update_attributes,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response[:name]).to eq('Updated Assistant')
expect(json_response[:response_guidelines]).to eq(['Updated guideline'])
expect(json_response[:guardrails]).to eq(['Updated guardrail'])
end
it 'updates only response_guidelines when only that is provided' do
assistant.update!(response_guidelines: ['Original guideline'], guardrails: ['Original guardrail'])
original_name = assistant.name
patch "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}",
params: { assistant: { response_guidelines: ['New guideline only'] } },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response[:name]).to eq(original_name)
expect(json_response[:response_guidelines]).to eq(['New guideline only'])
expect(json_response[:guardrails]).to eq(['Original guardrail'])
end
it 'updates only guardrails when only that is provided' do
assistant.update!(response_guidelines: ['Original guideline'], guardrails: ['Original guardrail'])
original_name = assistant.name
patch "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}",
params: { assistant: { guardrails: ['New guardrail only'] } },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response[:name]).to eq(original_name)
expect(json_response[:response_guidelines]).to eq(['Original guideline'])
expect(json_response[:guardrails]).to eq(['New guardrail only'])
end
it 'updates feature_citation config' do
assistant.update!(config: { 'feature_citation' => true })
patch "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}",
params: { assistant: { config: { feature_citation: false } } },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response[:config][:feature_citation]).to be(false)
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/captain/assistants/{id}' do
let!(:assistant) { create(:captain_assistant, account: account) }
context 'when it is an un-authenticated user' do
it 'does not delete the assistant' do
delete "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}",
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
it 'delete the assistant' do
delete "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an admin' do
it 'deletes the assistant' do
expect do
delete "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}",
headers: admin.create_new_auth_token,
as: :json
end.to change(Captain::Assistant, :count).by(-1)
expect(response).to have_http_status(:no_content)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/captain/assistants/{id}/playground' do
let(:assistant) { create(:captain_assistant, account: account) }
let(:valid_params) do
{
message_content: 'Hello assistant',
message_history: [
{ role: 'user', content: 'Previous message' },
{ role: 'assistant', content: 'Previous response' }
]
}
end
context 'when it is an un-authenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/playground",
params: valid_params,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
it 'generates a response' do
chat_service = instance_double(Captain::Llm::AssistantChatService)
allow(Captain::Llm::AssistantChatService).to receive(:new).with(assistant: assistant).and_return(chat_service)
allow(chat_service).to receive(:generate_response).and_return({ content: 'Assistant response' })
post "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/playground",
params: valid_params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(chat_service).to have_received(:generate_response).with(
additional_message: valid_params[:message_content],
message_history: valid_params[:message_history]
)
expect(json_response[:content]).to eq('Assistant response')
end
end
context 'when message_history is not provided' do
it 'uses empty array as default' do
params_without_history = { message_content: 'Hello assistant' }
chat_service = instance_double(Captain::Llm::AssistantChatService)
allow(Captain::Llm::AssistantChatService).to receive(:new).with(assistant: assistant).and_return(chat_service)
allow(chat_service).to receive(:generate_response).and_return({ content: 'Assistant response' })
post "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/playground",
params: params_without_history,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(chat_service).to have_received(:generate_response).with(
additional_message: params_without_history[:message_content],
message_history: []
)
end
end
end
end

View File

@@ -0,0 +1,143 @@
require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::Captain::BulkActions', type: :request do
let(:account) { create(:account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
let!(:pending_responses) do
create_list(
:captain_assistant_response,
2,
assistant: assistant,
account: account,
status: 'pending'
)
end
def json_response
JSON.parse(response.body, symbolize_names: true)
end
describe 'POST /api/v1/accounts/:account_id/captain/bulk_actions' do
context 'when approving responses' do
let(:valid_params) do
{
type: 'AssistantResponse',
ids: pending_responses.map(&:id),
fields: { status: 'approve' }
}
end
it 'approves the responses and returns the updated records' do
post "/api/v1/accounts/#{account.id}/captain/bulk_actions",
params: valid_params,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(json_response).to be_an(Array)
expect(json_response.length).to eq(2)
# Verify responses were approved
pending_responses.each do |response|
expect(response.reload.status).to eq('approved')
end
end
end
context 'when deleting responses' do
let(:delete_params) do
{
type: 'AssistantResponse',
ids: pending_responses.map(&:id),
fields: { status: 'delete' }
}
end
it 'deletes the responses and returns an empty array' do
expect do
post "/api/v1/accounts/#{account.id}/captain/bulk_actions",
params: delete_params,
headers: admin.create_new_auth_token,
as: :json
end.to change(Captain::AssistantResponse, :count).by(-2)
expect(response).to have_http_status(:ok)
expect(json_response).to eq([])
# Verify responses were deleted
pending_responses.each do |response|
expect { response.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
context 'with invalid type' do
let(:invalid_params) do
{
type: 'InvalidType',
ids: pending_responses.map(&:id),
fields: { status: 'approve' }
}
end
it 'returns unprocessable entity status' do
post "/api/v1/accounts/#{account.id}/captain/bulk_actions",
params: invalid_params,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(json_response[:success]).to be(false)
# Verify no changes were made
pending_responses.each do |response|
expect(response.reload.status).to eq('pending')
end
end
end
context 'with missing parameters' do
let(:missing_params) do
{
type: 'AssistantResponse',
fields: { status: 'approve' }
}
end
it 'returns unprocessable entity status' do
post "/api/v1/accounts/#{account.id}/captain/bulk_actions",
params: missing_params,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(json_response[:success]).to be(false)
# Verify no changes were made
pending_responses.each do |response|
expect(response.reload.status).to eq('pending')
end
end
end
context 'with unauthorized user' do
let(:unauthorized_user) { create(:user, account: create(:account)) }
it 'returns unauthorized status' do
post "/api/v1/accounts/#{account.id}/captain/bulk_actions",
params: { type: 'AssistantResponse', ids: [1], fields: { status: 'approve' } },
headers: unauthorized_user.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
# Verify no changes were made
pending_responses.each do |response|
expect(response.reload.status).to eq('pending')
end
end
end
end
end

View File

@@ -0,0 +1,78 @@
require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::Captain::CopilotMessagesController', type: :request do
let(:account) { create(:account) }
let(:user) { create(:user, account: account, role: :administrator) }
let(:copilot_thread) { create(:captain_copilot_thread, account: account, user: user) }
let!(:copilot_message) { create(:captain_copilot_message, copilot_thread: copilot_thread, account: account) }
describe 'GET /api/v1/accounts/{account.id}/captain/copilot_threads/{thread.id}/copilot_messages' do
context 'when it is an authenticated user' do
it 'returns all messages' do
get "/api/v1/accounts/#{account.id}/captain/copilot_threads/#{copilot_thread.id}/copilot_messages",
headers: user.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload'].length).to eq(1)
expect(json_response['payload'][0]['id']).to eq(copilot_message.id)
end
end
context 'when thread id is invalid' do
it 'returns not found error' do
get "/api/v1/accounts/#{account.id}/captain/copilot_threads/999999999/copilot_messages",
headers: user.create_new_auth_token,
as: :json
expect(response).to have_http_status(:not_found)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/captain/copilot_threads/{thread.id}/copilot_messages' do
context 'when it is an authenticated user' do
it 'creates a new message' do
message_content = { 'content' => 'This is a test message' }
expect do
post "/api/v1/accounts/#{account.id}/captain/copilot_threads/#{copilot_thread.id}/copilot_messages",
params: { message: message_content },
headers: user.create_new_auth_token,
as: :json
end.to change(CopilotMessage, :count).by(1)
expect(response).to have_http_status(:success)
expect(CopilotMessage.last.message).to eq({ 'content' => message_content })
expect(CopilotMessage.last.message_type).to eq('user')
expect(CopilotMessage.last.copilot_thread_id).to eq(copilot_thread.id)
end
end
context 'when thread does not exist' do
it 'returns not found error' do
post "/api/v1/accounts/#{account.id}/captain/copilot_threads/999999999/copilot_messages",
params: { message: { text: 'Test message' } },
headers: user.create_new_auth_token,
as: :json
expect(response).to have_http_status(:not_found)
end
end
context 'when thread belongs to another user' do
let(:another_user) { create(:user, account: account) }
let(:another_thread) { create(:captain_copilot_thread, account: account, user: another_user) }
it 'returns not found error' do
post "/api/v1/accounts/#{account.id}/captain/copilot_threads/#{another_thread.id}/copilot_messages",
params: { message: { text: 'Test message' } },
headers: user.create_new_auth_token,
as: :json
expect(response).to have_http_status(:not_found)
end
end
end
end

View File

@@ -0,0 +1,140 @@
require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::Captain::CopilotThreads', type: :request do
let(:account) { create(:account) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:conversation) { create(:conversation, account: account) }
def json_response
JSON.parse(response.body, symbolize_names: true)
end
describe 'GET /api/v1/accounts/{account.id}/captain/copilot_threads' do
context 'when it is an un-authenticated user' do
it 'does not fetch copilot threads' do
get "/api/v1/accounts/#{account.id}/captain/copilot_threads",
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'fetches copilot threads for the current user' do
# Create threads for the current agent
create_list(:captain_copilot_thread, 3, account: account, user: agent)
# Create threads for another user (should not be included)
create_list(:captain_copilot_thread, 2, account: account, user: admin)
get "/api/v1/accounts/#{account.id}/captain/copilot_threads",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response[:payload].length).to eq(3)
expect(json_response[:payload].map { |thread| thread[:user][:id] }.uniq).to eq([agent.id])
end
it 'returns threads in descending order of creation' do
threads = create_list(:captain_copilot_thread, 3, account: account, user: agent)
get "/api/v1/accounts/#{account.id}/captain/copilot_threads",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response[:payload].pluck(:id)).to eq(threads.reverse.pluck(:id))
end
end
end
describe 'POST /api/v1/accounts/{account.id}/captain/copilot_threads' do
let(:assistant) { create(:captain_assistant, account: account) }
let(:valid_params) { { message: 'Hello, how can you help me?', assistant_id: assistant.id, conversation_id: conversation.display_id } }
context 'when it is an un-authenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/captain/copilot_threads",
params: valid_params,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
context 'with invalid params' do
it 'returns error when message is blank' do
post "/api/v1/accounts/#{account.id}/captain/copilot_threads",
params: { message: '', assistant_id: assistant.id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(json_response[:error]).to eq('Message is required')
end
it 'returns error when assistant_id is invalid' do
post "/api/v1/accounts/#{account.id}/captain/copilot_threads",
params: { message: 'Hello', assistant_id: 0 },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:not_found)
end
end
context 'with valid params' do
it 'returns error when usage limit is exceeded' do
account.limits = { captain_responses: 2 }
account.custom_attributes = { captain_responses_usage: 2 }
account.save!
post "/api/v1/accounts/#{account.id}/captain/copilot_threads",
params: valid_params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(CopilotMessage.last.message['content']).to eq(
'You are out of Copilot credits. You can buy more credits from the billing section.'
)
end
it 'creates a new copilot thread with initial message' do
account.limits = { captain_responses: 2 }
account.custom_attributes = { captain_responses_usage: 0 }
account.save!
expect do
post "/api/v1/accounts/#{account.id}/captain/copilot_threads",
params: valid_params,
headers: agent.create_new_auth_token,
as: :json
end.to change(CopilotThread, :count).by(1)
.and change(CopilotMessage, :count).by(1)
expect(response).to have_http_status(:success)
thread = CopilotThread.last
expect(thread.title).to eq(valid_params[:message])
expect(thread.user_id).to eq(agent.id)
expect(thread.assistant_id).to eq(assistant.id)
message = thread.copilot_messages.last
expect(message.message).to eq({ 'content' => valid_params[:message] })
expect(Captain::Copilot::ResponseJob).to have_been_enqueued.with(
assistant: assistant,
conversation_id: valid_params[:conversation_id],
user_id: agent.id,
copilot_thread_id: thread.id,
message: valid_params[:message]
)
end
end
end
end
end

View File

@@ -0,0 +1,281 @@
require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::Captain::CustomTools', type: :request do
let(:account) { create(:account) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
def json_response
JSON.parse(response.body, symbolize_names: true)
end
describe 'GET /api/v1/accounts/{account.id}/captain/custom_tools' do
context 'when it is an un-authenticated user' do
it 'returns unauthorized status' do
get "/api/v1/accounts/#{account.id}/captain/custom_tools"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
it 'returns success status' do
create_list(:captain_custom_tool, 3, account: account)
get "/api/v1/accounts/#{account.id}/captain/custom_tools",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response[:payload].length).to eq(3)
end
end
context 'when it is an admin' do
it 'returns success status and custom tools' do
create_list(:captain_custom_tool, 5, account: account)
get "/api/v1/accounts/#{account.id}/captain/custom_tools",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response[:payload].length).to eq(5)
end
it 'returns only enabled custom tools' do
create(:captain_custom_tool, account: account, enabled: true)
create(:captain_custom_tool, account: account, enabled: false)
get "/api/v1/accounts/#{account.id}/captain/custom_tools",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response[:payload].length).to eq(1)
expect(json_response[:payload].first[:enabled]).to be(true)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/captain/custom_tools/{id}' do
let(:custom_tool) { create(:captain_custom_tool, account: account) }
context 'when it is an un-authenticated user' do
it 'returns unauthorized status' do
get "/api/v1/accounts/#{account.id}/captain/custom_tools/#{custom_tool.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
it 'returns success status and custom tool' do
get "/api/v1/accounts/#{account.id}/captain/custom_tools/#{custom_tool.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response[:id]).to eq(custom_tool.id)
expect(json_response[:title]).to eq(custom_tool.title)
end
end
context 'when custom tool does not exist' do
it 'returns not found status' do
get "/api/v1/accounts/#{account.id}/captain/custom_tools/999999",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:not_found)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/captain/custom_tools' do
let(:valid_attributes) do
{
custom_tool: {
title: 'Fetch Order Status',
description: 'Fetches order status from external API',
endpoint_url: 'https://api.example.com/orders/{{ order_id }}',
http_method: 'GET',
enabled: true,
param_schema: [
{ name: 'order_id', type: 'string', description: 'The order ID', required: true }
]
}
}
end
context 'when it is an un-authenticated user' do
it 'returns unauthorized status' do
post "/api/v1/accounts/#{account.id}/captain/custom_tools",
params: valid_attributes
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
it 'returns unauthorized status' do
post "/api/v1/accounts/#{account.id}/captain/custom_tools",
params: valid_attributes,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an admin' do
it 'creates a new custom tool and returns success status' do
post "/api/v1/accounts/#{account.id}/captain/custom_tools",
params: valid_attributes,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response[:title]).to eq('Fetch Order Status')
expect(json_response[:description]).to eq('Fetches order status from external API')
expect(json_response[:enabled]).to be(true)
expect(json_response[:slug]).to eq('custom_fetch_order_status')
expect(json_response[:param_schema]).to eq([
{ name: 'order_id', type: 'string', description: 'The order ID', required: true }
])
end
context 'with invalid parameters' do
let(:invalid_attributes) do
{
custom_tool: {
title: '',
endpoint_url: ''
}
}
end
it 'returns unprocessable entity status' do
post "/api/v1/accounts/#{account.id}/captain/custom_tools",
params: invalid_attributes,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
context 'with invalid endpoint URL' do
let(:invalid_url_attributes) do
{
custom_tool: {
title: 'Test Tool',
endpoint_url: 'http://localhost/api',
http_method: 'GET'
}
}
end
it 'returns unprocessable entity status' do
post "/api/v1/accounts/#{account.id}/captain/custom_tools",
params: invalid_url_attributes,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/captain/custom_tools/{id}' do
let(:custom_tool) { create(:captain_custom_tool, account: account) }
let(:update_attributes) do
{
custom_tool: {
title: 'Updated Tool Title',
enabled: false
}
}
end
context 'when it is an un-authenticated user' do
it 'returns unauthorized status' do
patch "/api/v1/accounts/#{account.id}/captain/custom_tools/#{custom_tool.id}",
params: update_attributes
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
it 'returns unauthorized status' do
patch "/api/v1/accounts/#{account.id}/captain/custom_tools/#{custom_tool.id}",
params: update_attributes,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an admin' do
it 'updates the custom tool and returns success status' do
patch "/api/v1/accounts/#{account.id}/captain/custom_tools/#{custom_tool.id}",
params: update_attributes,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response[:title]).to eq('Updated Tool Title')
expect(json_response[:enabled]).to be(false)
end
context 'with invalid parameters' do
let(:invalid_attributes) do
{
custom_tool: {
title: ''
}
}
end
it 'returns unprocessable entity status' do
patch "/api/v1/accounts/#{account.id}/captain/custom_tools/#{custom_tool.id}",
params: invalid_attributes,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/captain/custom_tools/{id}' do
let!(:custom_tool) { create(:captain_custom_tool, account: account) }
context 'when it is an un-authenticated user' do
it 'returns unauthorized status' do
delete "/api/v1/accounts/#{account.id}/captain/custom_tools/#{custom_tool.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
it 'returns unauthorized status' do
delete "/api/v1/accounts/#{account.id}/captain/custom_tools/#{custom_tool.id}",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an admin' do
it 'deletes the custom tool and returns no content status' do
expect do
delete "/api/v1/accounts/#{account.id}/captain/custom_tools/#{custom_tool.id}",
headers: admin.create_new_auth_token
end.to change(Captain::CustomTool, :count).by(-1)
expect(response).to have_http_status(:no_content)
end
context 'when custom tool does not exist' do
it 'returns not found status' do
delete "/api/v1/accounts/#{account.id}/captain/custom_tools/999999",
headers: admin.create_new_auth_token
expect(response).to have_http_status(:not_found)
end
end
end
end
end

View File

@@ -0,0 +1,291 @@
require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::Captain::Documents', type: :request do
let(:account) { create(:account, custom_attributes: { plan_name: 'startups' }) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:assistant2) { create(:captain_assistant, account: account) }
let(:document) { create(:captain_document, assistant: assistant, account: account) }
let(:captain_limits) do
{
:startups => { :documents => 1, :responses => 100 }
}.with_indifferent_access
end
def json_response
JSON.parse(response.body, symbolize_names: true)
end
describe 'GET /api/v1/accounts/:account_id/captain/documents' do
context 'when it is an un-authenticated user' do
before do
get "/api/v1/accounts/#{account.id}/captain/documents"
end
it 'returns unauthorized status' do
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
context 'when no filters are applied' do
before do
create_list(:captain_document, 30, assistant: assistant, account: account)
end
it 'returns the first page of documents' do
get "/api/v1/accounts/#{account.id}/captain/documents", headers: agent.create_new_auth_token, as: :json
expect(response).to have_http_status(:ok)
expect(json_response[:payload].length).to eq(25)
expect(json_response[:meta]).to eq({ page: 1, total_count: 30 })
end
it 'returns the second page of documents' do
get "/api/v1/accounts/#{account.id}/captain/documents",
params: { page: 2 },
headers: agent.create_new_auth_token, as: :json
expect(response).to have_http_status(:ok)
expect(json_response[:payload].length).to eq(5)
expect(json_response[:meta]).to eq({ page: 2, total_count: 30 })
end
end
context 'when filtering by assistant_id' do
before do
create_list(:captain_document, 3, assistant: assistant, account: account)
create_list(:captain_document, 2, assistant: assistant2, account: account)
end
it 'returns only documents for the specified assistant' do
get "/api/v1/accounts/#{account.id}/captain/documents",
params: { assistant_id: assistant.id },
headers: agent.create_new_auth_token, as: :json
expect(response).to have_http_status(:ok)
expect(json_response[:payload].length).to eq(3)
expect(json_response[:payload][0][:assistant][:id]).to eq(assistant.id)
end
it 'returns empty array when assistant has no documents' do
new_assistant = create(:captain_assistant, account: account)
get "/api/v1/accounts/#{account.id}/captain/documents",
params: { assistant_id: new_assistant.id },
headers: agent.create_new_auth_token, as: :json
expect(response).to have_http_status(:ok)
expect(json_response[:payload]).to be_empty
end
end
context 'when documents belong to different accounts' do
let(:other_account) { create(:account) }
before do
create_list(:captain_document, 3, assistant: assistant, account: account)
create_list(:captain_document, 2, account: other_account)
end
it 'only returns documents for the current account' do
get "/api/v1/accounts/#{account.id}/captain/documents",
headers: agent.create_new_auth_token, as: :json
expect(response).to have_http_status(:ok)
expect(json_response[:payload].length).to eq(3)
document_account_ids = json_response[:payload].pluck(:account_id).uniq
expect(document_account_ids).to eq([account.id])
end
end
context 'with pagination and assistant filter combined' do
before do
create_list(:captain_document, 30, assistant: assistant, account: account)
create_list(:captain_document, 10, assistant: assistant2, account: account)
end
it 'returns paginated results for specific assistant' do
get "/api/v1/accounts/#{account.id}/captain/documents",
params: { assistant_id: assistant.id, page: 2 },
headers: agent.create_new_auth_token, as: :json
expect(response).to have_http_status(:ok)
expect(json_response[:payload].length).to eq(5)
expect(json_response[:payload][0][:assistant][:id]).to eq(assistant.id)
expect(json_response[:meta]).to eq({ page: 2, total_count: 30 })
end
end
end
end
describe 'GET /api/v1/accounts/:account_id/captain/documents/:id' do
context 'when it is an un-authenticated user' do
before do
get "/api/v1/accounts/#{account.id}/captain/documents/#{document.id}"
end
it 'returns unauthorized status' do
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
before do
get "/api/v1/accounts/#{account.id}/captain/documents/#{document.id}",
headers: agent.create_new_auth_token, as: :json
end
it 'returns success status' do
expect(response).to have_http_status(:success)
end
it 'returns the requested document' do
expect(json_response[:id]).to eq(document.id)
expect(json_response[:name]).to eq(document.name)
expect(json_response[:external_link]).to eq(document.external_link)
end
end
end
describe 'POST /api/v1/accounts/:account_id/captain/documents' do
let(:valid_attributes) do
{
document: {
name: 'Test Document',
external_link: 'https://example.com/doc',
assistant_id: assistant.id
}
}
end
let(:invalid_attributes) do
{
document: {
name: 'Test Document',
external_link: 'https://example.com/doc'
}
}
end
context 'when it is an un-authenticated user' do
before do
post "/api/v1/accounts/#{account.id}/captain/documents",
params: valid_attributes, as: :json
end
it 'returns unauthorized status' do
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/captain/documents",
params: valid_attributes,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an admin' do
context 'with valid parameters' do
it 'creates a new document' do
expect do
post "/api/v1/accounts/#{account.id}/captain/documents",
params: valid_attributes,
headers: admin.create_new_auth_token
end.to change(Captain::Document, :count).by(1)
end
it 'returns success status and the created document' do
post "/api/v1/accounts/#{account.id}/captain/documents",
params: valid_attributes,
headers: admin.create_new_auth_token, as: :json
expect(response).to have_http_status(:success)
expect(json_response[:name]).to eq('Test Document')
expect(json_response[:external_link]).to eq('https://example.com/doc')
end
end
context 'with invalid parameters' do
before do
post "/api/v1/accounts/#{account.id}/captain/documents",
params: invalid_attributes,
headers: admin.create_new_auth_token
end
it 'returns unprocessable entity status' do
expect(response).to have_http_status(:unprocessable_entity)
end
end
context 'with limits exceeded' do
before do
create_list(:captain_document, 5, assistant: assistant, account: account)
create(:installation_config, name: 'CAPTAIN_CLOUD_PLAN_LIMITS', value: captain_limits.to_json)
post "/api/v1/accounts/#{account.id}/captain/documents",
params: valid_attributes,
headers: admin.create_new_auth_token
end
it 'returns an error' do
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
end
describe 'DELETE /api/v1/accounts/:account_id/captain/documents/:id' do
context 'when it is an un-authenticated user' do
before do
delete "/api/v1/accounts/#{account.id}/captain/documents/#{document.id}"
end
it 'returns unauthorized status' do
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
let!(:document_to_delete) { create(:captain_document, assistant: assistant) }
it 'deletes the document' do
delete "/api/v1/accounts/#{account.id}/captain/documents/#{document_to_delete.id}",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an admin' do
context 'when document exists' do
let!(:document_to_delete) { create(:captain_document, assistant: assistant) }
it 'deletes the document' do
expect do
delete "/api/v1/accounts/#{account.id}/captain/documents/#{document_to_delete.id}",
headers: admin.create_new_auth_token
end.to change(Captain::Document, :count).by(-1)
end
it 'returns no content status' do
delete "/api/v1/accounts/#{account.id}/captain/documents/#{document_to_delete.id}",
headers: admin.create_new_auth_token
expect(response).to have_http_status(:no_content)
end
end
context 'when document does not exist' do
before do
delete "/api/v1/accounts/#{account.id}/captain/documents/invalid_id",
headers: admin.create_new_auth_token
end
it 'returns not found status' do
expect(response).to have_http_status(:not_found)
end
end
end
end
end

View File

@@ -0,0 +1,119 @@
require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::Captain::Inboxes', type: :request do
let(:account) { create(:account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:inbox) { create(:inbox, account: account) }
let(:inbox2) { create(:inbox, account: account) }
let!(:captain_inbox) { create(:captain_inbox, captain_assistant: assistant, inbox: inbox) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
def json_response
JSON.parse(response.body, symbolize_names: true)
end
describe 'GET /api/v1/accounts/:account_id/captain/assistants/:assistant_id/inboxes' do
context 'when user is authorized' do
it 'returns a list of inboxes for the assistant' do
get "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/inboxes",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:ok)
expect(json_response[:payload].first[:id]).to eq(captain_inbox.inbox.id)
end
end
context 'when user is unauthorized' do
it 'returns unauthorized status' do
get "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/inboxes"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when assistant does not exist' do
it 'returns not found status' do
get "/api/v1/accounts/#{account.id}/captain/assistants/999999/inboxes",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:not_found)
end
end
end
describe 'POST /api/v1/accounts/:account/captain/assistants/:assistant_id/inboxes' do
let(:valid_params) do
{
inbox: {
inbox_id: inbox2.id
}
}
end
context 'when user is authorized' do
it 'creates a new captain inbox' do
expect do
post "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/inboxes",
params: valid_params,
headers: admin.create_new_auth_token
end.to change(CaptainInbox, :count).by(1)
expect(response).to have_http_status(:success)
expect(json_response[:id]).to eq(inbox2.id)
end
context 'when inbox does not exist' do
it 'returns not found status' do
post "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/inboxes",
params: { inbox: { inbox_id: 999_999 } },
headers: admin.create_new_auth_token
expect(response).to have_http_status(:not_found)
end
end
context 'when params are invalid' do
it 'returns unprocessable entity status' do
post "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/inboxes",
params: {},
headers: admin.create_new_auth_token
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
context 'when user is agent' do
it 'returns unauthorized status' do
post "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/inboxes",
params: valid_params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'DELETE /api/v1/accounts/captain/assistants/:assistant_id/inboxes/:inbox_id' do
context 'when user is authorized' do
it 'deletes the captain inbox' do
expect do
delete "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/inboxes/#{inbox.id}",
headers: admin.create_new_auth_token
end.to change(CaptainInbox, :count).by(-1)
expect(response).to have_http_status(:no_content)
end
context 'when captain inbox does not exist' do
it 'returns not found status' do
delete "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/inboxes/999999",
headers: admin.create_new_auth_token
expect(response).to have_http_status(:not_found)
end
end
end
end
end

View File

@@ -0,0 +1,258 @@
require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::Captain::Scenarios', type: :request do
let(:account) { create(:account) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:assistant) { create(:captain_assistant, account: account) }
def json_response
JSON.parse(response.body, symbolize_names: true)
end
describe 'GET /api/v1/accounts/{account.id}/captain/assistants/{assistant.id}/scenarios' do
context 'when it is an un-authenticated user' do
it 'returns unauthorized status' do
get "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/scenarios"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
it 'returns success status' do
create_list(:captain_scenario, 3, assistant: assistant, account: account)
get "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/scenarios",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response[:payload].length).to eq(3)
end
end
context 'when it is an admin' do
it 'returns success status and scenarios' do
create_list(:captain_scenario, 5, assistant: assistant, account: account)
get "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/scenarios",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response[:payload].length).to eq(5)
end
it 'returns only enabled scenarios' do
create(:captain_scenario, assistant: assistant, account: account, enabled: true)
create(:captain_scenario, assistant: assistant, account: account, enabled: false)
get "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/scenarios",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response[:payload].length).to eq(1)
expect(json_response[:payload].first[:enabled]).to be(true)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/captain/assistants/{assistant.id}/scenarios/{id}' do
let(:scenario) { create(:captain_scenario, assistant: assistant, account: account) }
context 'when it is an un-authenticated user' do
it 'returns unauthorized status' do
get "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/scenarios/#{scenario.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
it 'returns success status and scenario' do
get "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/scenarios/#{scenario.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response[:id]).to eq(scenario.id)
expect(json_response[:title]).to eq(scenario.title)
end
end
context 'when scenario does not exist' do
it 'returns not found status' do
get "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/scenarios/999999",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:not_found)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/captain/assistants/{assistant.id}/scenarios' do
let(:valid_attributes) do
{
scenario: {
title: 'Test Scenario',
description: 'Test description',
instruction: 'Test instruction',
enabled: true,
tools: %w[tool1 tool2]
}
}
end
context 'when it is an un-authenticated user' do
it 'returns unauthorized status' do
post "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/scenarios",
params: valid_attributes
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
it 'returns unauthorized status' do
post "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/scenarios",
params: valid_attributes,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an admin' do
it 'creates a new scenario and returns success status' do
expect do
post "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/scenarios",
params: valid_attributes,
headers: admin.create_new_auth_token,
as: :json
end.to change(Captain::Scenario, :count).by(1)
expect(response).to have_http_status(:success)
expect(json_response[:title]).to eq('Test Scenario')
expect(json_response[:description]).to eq('Test description')
expect(json_response[:enabled]).to be(true)
expect(json_response[:assistant_id]).to eq(assistant.id)
end
context 'with invalid parameters' do
let(:invalid_attributes) do
{
scenario: {
title: '',
description: '',
instruction: ''
}
}
end
it 'returns unprocessable entity status' do
post "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/scenarios",
params: invalid_attributes,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/captain/assistants/{assistant.id}/scenarios/{id}' do
let(:scenario) { create(:captain_scenario, assistant: assistant, account: account) }
let(:update_attributes) do
{
scenario: {
title: 'Updated Scenario Title',
enabled: false
}
}
end
context 'when it is an un-authenticated user' do
it 'returns unauthorized status' do
patch "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/scenarios/#{scenario.id}",
params: update_attributes
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
it 'returns unauthorized status' do
patch "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/scenarios/#{scenario.id}",
params: update_attributes,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an admin' do
it 'updates the scenario and returns success status' do
patch "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/scenarios/#{scenario.id}",
params: update_attributes,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response[:title]).to eq('Updated Scenario Title')
expect(json_response[:enabled]).to be(false)
end
context 'with invalid parameters' do
let(:invalid_attributes) do
{
scenario: {
title: ''
}
}
end
it 'returns unprocessable entity status' do
patch "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/scenarios/#{scenario.id}",
params: invalid_attributes,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/captain/assistants/{assistant.id}/scenarios/{id}' do
let!(:scenario) { create(:captain_scenario, assistant: assistant, account: account) }
context 'when it is an un-authenticated user' do
it 'returns unauthorized status' do
delete "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/scenarios/#{scenario.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
it 'returns unauthorized status' do
delete "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/scenarios/#{scenario.id}",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an admin' do
it 'deletes the scenario and returns no content status' do
expect do
delete "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/scenarios/#{scenario.id}",
headers: admin.create_new_auth_token
end.to change(Captain::Scenario, :count).by(-1)
expect(response).to have_http_status(:no_content)
end
context 'when scenario does not exist' do
it 'returns not found status' do
delete "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/scenarios/999999",
headers: admin.create_new_auth_token
expect(response).to have_http_status(:not_found)
end
end
end
end
end

View File

@@ -0,0 +1,344 @@
require 'rails_helper'
RSpec.describe 'Companies API', type: :request do
let(:account) { create(:account) }
describe 'GET /api/v1/accounts/{account.id}/companies' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/companies"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:admin) { create(:user, account: account, role: :administrator) }
let!(:company1) { create(:company, name: 'Company 1', account: account) }
let!(:company2) { create(:company, account: account) }
it 'returns all companies' do
get "/api/v1/accounts/#{account.id}/companies",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body['payload'].size).to eq(2)
expect(response_body['payload'].map { |c| c['name'] }).to contain_exactly(company1.name, company2.name)
end
it 'returns companies with pagination' do
create_list(:company, 30, account: account)
get "/api/v1/accounts/#{account.id}/companies",
params: { page: 1 },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body['payload'].size).to eq(25)
expect(response_body['meta']['total_count']).to eq(32)
expect(response_body['meta']['page']).to eq(1)
end
it 'returns second page of companies' do
create_list(:company, 30, account: account)
get "/api/v1/accounts/#{account.id}/companies",
params: { page: 2 },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body['payload'].size).to eq(7)
expect(response_body['meta']['total_count']).to eq(32)
expect(response_body['meta']['page']).to eq(2)
end
it 'returns companies with contacts_count' do
company_with_contacts = create(:company, name: 'Company With Contacts', account: account)
create_list(:contact, 5, company: company_with_contacts, account: account)
get "/api/v1/accounts/#{account.id}/companies",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
company_data = response_body['payload'].find { |c| c['id'] == company_with_contacts.id }
expect(company_data['contacts_count']).to eq(5)
end
it 'does not return companies from other accounts' do
other_account = create(:account)
create(:company, name: 'Other Account Company', account: other_account)
create(:company, name: 'My Company', account: account)
get "/api/v1/accounts/#{account.id}/companies",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body['payload'].size).to eq(3)
expect(response_body['payload'].map { |c| c['name'] }).not_to include('Other Account Company')
end
it 'sorts companies by contacts_count in ascending order' do
company_with_5 = create(:company, name: 'Company with 5', account: account)
company_with_2 = create(:company, name: 'Company with 2', account: account)
company_with_10 = create(:company, name: 'Company with 10', account: account)
create_list(:contact, 5, company: company_with_5, account: account)
create_list(:contact, 2, company: company_with_2, account: account)
create_list(:contact, 10, company: company_with_10, account: account)
get "/api/v1/accounts/#{account.id}/companies",
params: { sort: 'contacts_count' },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
company_ids = response_body['payload'].map { |c| c['id'] }
expect(company_ids.index(company_with_2.id)).to be < company_ids.index(company_with_5.id)
expect(company_ids.index(company_with_5.id)).to be < company_ids.index(company_with_10.id)
end
it 'sorts companies by contacts_count in descending order' do
company_with_5 = create(:company, name: 'Company with 5', account: account)
company_with_2 = create(:company, name: 'Company with 2', account: account)
company_with_10 = create(:company, name: 'Company with 10', account: account)
create_list(:contact, 5, company: company_with_5, account: account)
create_list(:contact, 2, company: company_with_2, account: account)
create_list(:contact, 10, company: company_with_10, account: account)
get "/api/v1/accounts/#{account.id}/companies",
params: { sort: '-contacts_count' },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
company_ids = response_body['payload'].map { |c| c['id'] }
expect(company_ids.index(company_with_10.id)).to be < company_ids.index(company_with_5.id)
expect(company_ids.index(company_with_5.id)).to be < company_ids.index(company_with_2.id)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/companies/search' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/companies/search"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:admin) { create(:user, account: account, role: :administrator) }
it 'returns error when q parameter is missing' do
get "/api/v1/accounts/#{account.id}/companies/search",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq('Specify search string with parameter q')
end
it 'searches companies by name' do
create(:company, name: 'Acme Corp', domain: 'acme.com', account: account)
create(:company, name: 'Tech Solutions', domain: 'tech.com', account: account)
create(:company, name: 'Global Inc', domain: 'global.com', account: account)
get "/api/v1/accounts/#{account.id}/companies/search",
params: { q: 'tech' },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body['payload'].size).to eq(1)
expect(response_body['payload'].first['name']).to eq('Tech Solutions')
end
it 'searches companies by domain' do
create(:company, name: 'Acme Corp', domain: 'acme.com', account: account)
create(:company, name: 'Tech Solutions', domain: 'tech.com', account: account)
create(:company, name: 'Global Inc', domain: 'global.com', account: account)
get "/api/v1/accounts/#{account.id}/companies/search",
params: { q: 'acme.com' },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body['payload'].size).to eq(1)
expect(response_body['payload'].first['domain']).to eq('acme.com')
end
it 'search is case insensitive' do
create(:company, name: 'Acme Corp', domain: 'acme.com', account: account)
get "/api/v1/accounts/#{account.id}/companies/search",
params: { q: 'ACME' },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body['payload'].size).to eq(1)
end
it 'returns empty array when no companies match search' do
create(:company, name: 'Acme Corp', domain: 'acme.com', account: account)
get "/api/v1/accounts/#{account.id}/companies/search",
params: { q: 'nonexistent' },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body['payload'].size).to eq(0)
expect(response_body['meta']['total_count']).to eq(0)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/companies/{id}' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
company = create(:company, account: account)
get "/api/v1/accounts/#{account.id}/companies/#{company.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:admin) { create(:user, account: account, role: :administrator) }
let(:company) { create(:company, account: account) }
it 'returns the company' do
get "/api/v1/accounts/#{account.id}/companies/#{company.id}",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body['payload']['name']).to eq(company.name)
expect(response_body['payload']['id']).to eq(company.id)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/companies' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/companies"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:admin) { create(:user, account: account, role: :administrator) }
let(:valid_params) do
{
company: {
name: 'New Company',
domain: 'newcompany.com',
description: 'A new company'
}
}
end
it 'creates a new company' do
expect do
post "/api/v1/accounts/#{account.id}/companies",
params: valid_params,
headers: admin.create_new_auth_token,
as: :json
end.to change(Company, :count).by(1)
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body['payload']['name']).to eq('New Company')
expect(response_body['payload']['domain']).to eq('newcompany.com')
end
it 'returns error for invalid params' do
invalid_params = { company: { name: '' } }
post "/api/v1/accounts/#{account.id}/companies",
params: invalid_params,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/companies/{id}' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
company = create(:company, account: account)
patch "/api/v1/accounts/#{account.id}/companies/#{company.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:admin) { create(:user, account: account, role: :administrator) }
let(:company) { create(:company, account: account) }
let(:update_params) do
{
company: {
name: 'Updated Company Name',
domain: 'updated.com'
}
}
end
it 'updates the company' do
patch "/api/v1/accounts/#{account.id}/companies/#{company.id}",
params: update_params,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body['payload']['name']).to eq('Updated Company Name')
expect(response_body['payload']['domain']).to eq('updated.com')
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/companies/{id}' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
company = create(:company, account: account)
delete "/api/v1/accounts/#{account.id}/companies/#{company.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated administrator' do
let(:admin) { create(:user, account: account, role: :administrator) }
let(:company) { create(:company, account: account) }
it 'deletes the company' do
company
expect do
delete "/api/v1/accounts/#{account.id}/companies/#{company.id}",
headers: admin.create_new_auth_token,
as: :json
end.to change(Company, :count).by(-1)
expect(response).to have_http_status(:ok)
end
end
context 'when it is a regular agent' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:company) { create(:company, account: account) }
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/companies/#{company.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
end

View File

@@ -0,0 +1,142 @@
require 'rails_helper'
RSpec.describe Api::V1::Accounts::ConferenceController, type: :request do
let(:account) { create(:account) }
let(:voice_channel) { create(:channel_voice, account: account) }
let(:voice_inbox) { voice_channel.inbox }
let(:conversation) { create(:conversation, account: account, inbox: voice_inbox, identifier: nil) }
let(:admin) { create(:user, :administrator, account: account) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:webhook_service) { instance_double(Twilio::VoiceWebhookSetupService, perform: true) }
let(:voice_grant) { instance_double(Twilio::JWT::AccessToken::VoiceGrant) }
let(:conference_service) do
instance_double(
Voice::Provider::Twilio::ConferenceService,
ensure_conference_sid: 'CF123',
mark_agent_joined: true,
end_conference: true
)
end
before do
allow(Twilio::VoiceWebhookSetupService).to receive(:new).and_return(webhook_service)
allow(Twilio::JWT::AccessToken::VoiceGrant).to receive(:new).and_return(voice_grant)
allow(voice_grant).to receive(:outgoing_application_sid=)
allow(voice_grant).to receive(:outgoing_application_params=)
allow(voice_grant).to receive(:incoming_allow=)
allow(Voice::Provider::Twilio::ConferenceService).to receive(:new).and_return(conference_service)
end
describe 'GET /conference/token' do
context 'when unauthenticated' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/inboxes/#{voice_inbox.id}/conference/token"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated agent with inbox access' do
before { create(:inbox_member, inbox: voice_inbox, user: agent) }
it 'returns token payload' do
fake_token = instance_double(Twilio::JWT::AccessToken, to_jwt: 'jwt-token', add_grant: nil)
allow(Twilio::JWT::AccessToken).to receive(:new).and_return(fake_token)
get "/api/v1/accounts/#{account.id}/inboxes/#{voice_inbox.id}/conference/token",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:ok)
body = response.parsed_body
expect(body['token']).to eq('jwt-token')
expect(body['account_id']).to eq(account.id)
expect(body['inbox_id']).to eq(voice_inbox.id)
end
end
end
describe 'POST /conference' do
context 'when unauthenticated' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/inboxes/#{voice_inbox.id}/conference"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated agent with inbox access' do
before { create(:inbox_member, inbox: voice_inbox, user: agent) }
it 'creates conference and sets identifier' do
post "/api/v1/accounts/#{account.id}/inboxes/#{voice_inbox.id}/conference",
headers: agent.create_new_auth_token,
params: { conversation_id: conversation.display_id, call_sid: 'CALL123' }
expect(response).to have_http_status(:ok)
body = response.parsed_body
expect(body['conference_sid']).to be_present
conversation.reload
expect(conversation.identifier).to eq('CALL123')
expect(conference_service).to have_received(:ensure_conference_sid)
expect(conference_service).to have_received(:mark_agent_joined)
end
it 'does not allow accessing conversations from inboxes without access' do
other_inbox = create(:inbox, account: account)
other_conversation = create(:conversation, account: account, inbox: other_inbox, identifier: nil)
post "/api/v1/accounts/#{account.id}/inboxes/#{voice_inbox.id}/conference",
headers: agent.create_new_auth_token,
params: { conversation_id: other_conversation.display_id, call_sid: 'CALL123' }
expect(response).to have_http_status(:not_found)
other_conversation.reload
expect(other_conversation.identifier).to be_nil
end
it 'returns conflict when call_sid missing' do
post "/api/v1/accounts/#{account.id}/inboxes/#{voice_inbox.id}/conference",
headers: agent.create_new_auth_token,
params: { conversation_id: conversation.display_id }
expect(response).to have_http_status(:unprocessable_content)
end
end
end
describe 'DELETE /conference' do
context 'when unauthenticated' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/inboxes/#{voice_inbox.id}/conference"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated agent with inbox access' do
before { create(:inbox_member, inbox: voice_inbox, user: agent) }
it 'ends conference and returns success' do
delete "/api/v1/accounts/#{account.id}/inboxes/#{voice_inbox.id}/conference",
headers: agent.create_new_auth_token,
params: { conversation_id: conversation.display_id }
expect(response).to have_http_status(:ok)
expect(response.parsed_body['id']).to eq(conversation.display_id)
expect(conference_service).to have_received(:end_conference)
end
it 'does not allow ending conferences for conversations from inboxes without access' do
other_inbox = create(:inbox, account: account)
other_conversation = create(:conversation, account: account, inbox: other_inbox, identifier: nil)
delete "/api/v1/accounts/#{account.id}/inboxes/#{voice_inbox.id}/conference",
headers: agent.create_new_auth_token,
params: { conversation_id: other_conversation.display_id }
expect(response).to have_http_status(:not_found)
end
end
end
end

View File

@@ -0,0 +1,247 @@
require 'rails_helper'
RSpec.describe 'Conversations API', type: :request do
let(:account) { create(:account) }
let(:administrator) { create(:user, account: account, role: :administrator) }
describe 'GET /api/v1/accounts/{account.id}/conversations/:id' do
it 'returns SLA data for the conversation if the feature is enabled' do
account.enable_features!('sla')
conversation = create(:conversation, account: account)
applied_sla = create(:applied_sla, conversation: conversation)
sla_event = create(:sla_event, conversation: conversation, applied_sla: applied_sla)
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}", headers: administrator.create_new_auth_token
expect(response).to have_http_status(:ok)
expect(response.parsed_body['applied_sla']['id']).to eq(applied_sla.id)
expect(response.parsed_body['sla_events'].first['id']).to eq(sla_event.id)
end
it 'does not return SLA data for the conversation if the feature is disabled' do
account.disable_features!('sla')
conversation = create(:conversation, account: account)
create(:applied_sla, conversation: conversation)
create(:sla_event, conversation: conversation)
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}", headers: administrator.create_new_auth_token
expect(response).to have_http_status(:ok)
expect(response.parsed_body.keys).not_to include('applied_sla')
expect(response.parsed_body.keys).not_to include('sla_events')
end
context 'when agent has team access' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:team) { create(:team, account: account) }
let(:conversation) { create(:conversation, account: account, team: team) }
before do
create(:team_member, team: team, user: agent)
end
it 'allows accessing the conversation via team membership' do
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}", headers: agent.create_new_auth_token
expect(response).to have_http_status(:ok)
expect(response.parsed_body['id']).to eq(conversation.display_id)
end
end
context 'when agent has a custom role' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:conversation) { create(:conversation, account: account) }
before do
create(:inbox_member, user: agent, inbox: conversation.inbox)
end
it 'returns unauthorized for unassigned conversation without permission' do
custom_role = create(:custom_role, account: account, permissions: ['conversation_participating_manage'])
account.account_users.find_by(user_id: agent.id).update!(custom_role: custom_role)
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}", headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
it 'returns the conversation when permission allows managing unassigned conversations, including when assigned to agent' do
custom_role = create(:custom_role, account: account, permissions: ['conversation_unassigned_manage'])
account_user = account.account_users.find_by(user_id: agent.id)
account_user.update!(custom_role: custom_role)
conversation.update!(assignee: agent)
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}", headers: agent.create_new_auth_token
expect(response).to have_http_status(:ok)
expect(response.parsed_body['id']).to eq(conversation.display_id)
end
it 'returns the conversation when permission allows managing assigned conversations' do
custom_role = create(:custom_role, account: account, permissions: ['conversation_participating_manage'])
account_user = account.account_users.find_by(user_id: agent.id)
account_user.update!(custom_role: custom_role)
conversation.update!(assignee: agent)
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}", headers: agent.create_new_auth_token
expect(response).to have_http_status(:ok)
expect(response.parsed_body['id']).to eq(conversation.display_id)
end
it 'returns the conversation when permission allows managing participating conversations' do
custom_role = create(:custom_role, account: account, permissions: ['conversation_participating_manage'])
account_user = account.account_users.find_by(user_id: agent.id)
account_user.update!(custom_role: custom_role)
create(:conversation_participant, conversation: conversation, account: account, user: agent)
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}", headers: agent.create_new_auth_token
expect(response).to have_http_status(:ok)
expect(response.parsed_body['id']).to eq(conversation.display_id)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/conversations/:id/reporting_events' do
let(:conversation) { create(:conversation, account: account) }
let(:inbox) { conversation.inbox }
let(:agent) { administrator }
before do
# Create reporting events for this conversation
@event1 = create(:reporting_event,
account: account,
conversation: conversation,
inbox: inbox,
user: agent,
name: 'first_response',
value: 120,
created_at: 3.hours.ago)
@event2 = create(:reporting_event,
account: account,
conversation: conversation,
inbox: inbox,
user: agent,
name: 'reply_time',
value: 45,
created_at: 2.hours.ago)
@event3 = create(:reporting_event,
account: account,
conversation: conversation,
inbox: inbox,
user: agent,
name: 'resolution',
value: 300,
created_at: 1.hour.ago)
# Create an event for a different conversation (should not be included)
other_conversation = create(:conversation, account: account)
create(:reporting_event,
account: account,
conversation: other_conversation,
inbox: other_conversation.inbox,
user: agent,
name: 'other_conversation_event',
value: 60)
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/reporting_events",
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user with conversation access' do
it 'returns all reporting events for the conversation' do
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/reporting_events",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
# Should return array directly (no pagination)
expect(json_response).to be_an(Array)
expect(json_response.size).to eq(3)
# Check they are sorted by created_at asc (oldest first)
expect(json_response.first['name']).to eq('first_response')
expect(json_response.last['name']).to eq('resolution')
# Verify it doesn't include events from other conversations
event_names = json_response.map { |e| e['name'] }
expect(event_names).not_to include('other_conversation_event')
end
it 'returns empty array when conversation has no reporting events' do
conversation_without_events = create(:conversation, account: account)
get "/api/v1/accounts/#{account.id}/conversations/#{conversation_without_events.display_id}/reporting_events",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response).to be_an(Array)
expect(json_response).to be_empty
end
end
context 'when agent has limited access' do
let(:limited_agent) { create(:user, account: account, role: :agent) }
it 'returns unauthorized for unassigned conversation without permission' do
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/reporting_events",
headers: limited_agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'returns reporting events when agent is assigned to the conversation' do
conversation.update!(assignee: limited_agent)
# Also create inbox member for the agent
create(:inbox_member, user: limited_agent, inbox: conversation.inbox)
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/reporting_events",
headers: limited_agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response).to be_an(Array)
expect(json_response.size).to eq(3)
end
end
context 'when agent has team access' do
let(:team_agent) { create(:user, account: account, role: :agent) }
let(:team) { create(:team, account: account) }
before do
create(:team_member, team: team, user: team_agent)
conversation.update!(team: team)
end
it 'allows accessing conversation reporting events via team membership' do
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/reporting_events",
headers: team_agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response).to be_an(Array)
expect(json_response.size).to eq(3)
end
end
end
end

View File

@@ -0,0 +1,175 @@
require 'rails_helper'
RSpec.describe 'Custom Roles API', type: :request do
let!(:account) { create(:account) }
let!(:administrator) { create(:user, account: account, role: :administrator) }
let!(:agent) { create(:user, account: account, role: :agent) }
let!(:custom_role) { create(:custom_role, account: account, name: 'Manager') }
describe 'GET #index' do
context 'when it is an authenticated administrator' do
it 'returns all custom roles in the account' do
get "/api/v1/accounts/#{account.id}/custom_roles",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body[0]).to include('name' => custom_role.name)
end
end
context 'when the user is an agent and is authenticated' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/custom_roles",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/custom_roles"
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'GET #show' do
context 'when it is an authenticated administrator' do
it 'returns the custom role details' do
get "/api/v1/accounts/#{account.id}/custom_roles/#{custom_role.id}",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body).to include('name' => custom_role.name)
end
end
context 'when the user is an agent and is authenticated' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/custom_roles/#{custom_role.id}",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/custom_roles/#{custom_role.id}"
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'POST #create' do
let(:valid_params) do
{ custom_role: { name: 'Support', description: 'Support role',
permissions: CustomRole::PERMISSIONS.sample(SecureRandom.random_number(1..4)) } }
end
context 'when it is an authenticated administrator' do
it 'creates the custom role' do
expect do
post "/api/v1/accounts/#{account.id}/custom_roles",
params: valid_params,
headers: administrator.create_new_auth_token
end.to change(CustomRole, :count).by(1)
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body).to include('name' => 'Support')
end
end
context 'when the user is an agent and is authenticated' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/custom_roles",
params: valid_params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/custom_roles",
params: valid_params
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'PUT #update' do
let(:update_params) { { custom_role: { name: 'Updated Role' } } }
context 'when it is an authenticated administrator' do
it 'updates the custom role' do
put "/api/v1/accounts/#{account.id}/custom_roles/#{custom_role.id}",
params: update_params,
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body).to include('name' => 'Updated Role')
end
end
context 'when the user is an agent and is authenticated' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/custom_roles/#{custom_role.id}",
params: update_params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/custom_roles/#{custom_role.id}",
params: update_params
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'DELETE #destroy' do
context 'when it is an authenticated administrator' do
it 'deletes the custom role' do
delete "/api/v1/accounts/#{account.id}/custom_roles/#{custom_role.id}",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
expect(CustomRole.count).to eq(0)
end
end
context 'when the user is an agent and is authenticated' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/custom_roles/#{custom_role.id}",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/custom_roles/#{custom_role.id}"
expect(response).to have_http_status(:unauthorized)
end
end
end
end

View File

@@ -0,0 +1,217 @@
require 'rails_helper'
RSpec.describe 'Enterprise Reporting Events 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!(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: agent) }
describe 'GET /api/v1/accounts/{account.id}/reporting_events' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/reporting_events",
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated normal agent user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/reporting_events",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated admin user' do
before do
create(:reporting_event,
account: account,
conversation: conversation,
inbox: inbox,
user: agent,
name: 'first_response',
value: 120,
created_at: 3.days.ago)
create(:reporting_event,
account: account,
conversation: conversation,
inbox: inbox,
user: agent,
name: 'resolution',
value: 300,
created_at: 2.days.ago)
create(:reporting_event,
account: account,
conversation: conversation,
inbox: inbox,
user: agent,
name: 'reply_time',
value: 45,
created_at: 1.day.ago)
end
it 'fetches reporting events with pagination' do
get "/api/v1/accounts/#{account.id}/reporting_events",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
# Check structure and pagination
expect(json_response).to have_key('payload')
expect(json_response).to have_key('meta')
expect(json_response['meta']['count']).to eq(3)
# Check events are sorted by created_at desc (newest first)
events = json_response['payload']
expect(events.size).to eq(3)
expect(events.first['name']).to eq('reply_time')
expect(events.last['name']).to eq('first_response')
end
it 'filters reporting events by date range using since and until' do
get "/api/v1/accounts/#{account.id}/reporting_events",
params: { since: 2.5.days.ago.to_time.to_i.to_s, until: 1.5.days.ago.to_time.to_i.to_s },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['meta']['count']).to eq(1)
expect(json_response['payload'].first['name']).to eq('resolution')
end
it 'filters reporting events by inbox_id' do
other_inbox = create(:inbox, account: account)
other_conversation = create(:conversation, account: account, inbox: other_inbox)
create(:reporting_event,
account: account,
conversation: other_conversation,
inbox: other_inbox,
user: agent,
name: 'other_inbox_event')
get "/api/v1/accounts/#{account.id}/reporting_events",
params: { inbox_id: inbox.id },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['meta']['count']).to eq(3)
expect(json_response['payload'].map { |e| e['name'] }).not_to include('other_inbox_event')
end
it 'filters reporting events by user_id (agent)' do
other_agent = create(:user, account: account, role: :agent)
create(:reporting_event,
account: account,
conversation: conversation,
inbox: inbox,
user: other_agent,
name: 'other_agent_event')
get "/api/v1/accounts/#{account.id}/reporting_events",
params: { user_id: agent.id },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['meta']['count']).to eq(3)
expect(json_response['payload'].map { |e| e['name'] }).not_to include('other_agent_event')
end
it 'filters reporting events by name' do
get "/api/v1/accounts/#{account.id}/reporting_events",
params: { name: 'first_response' },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['meta']['count']).to eq(1)
expect(json_response['payload'].first['name']).to eq('first_response')
end
it 'supports combining multiple filters' do
# Create more test data
other_conversation = create(:conversation, account: account, inbox: inbox, assignee: agent)
create(:reporting_event,
account: account,
conversation: other_conversation,
inbox: inbox,
user: agent,
name: 'first_response',
value: 90,
created_at: 2.days.ago)
get "/api/v1/accounts/#{account.id}/reporting_events",
params: {
inbox_id: inbox.id,
user_id: agent.id,
name: 'first_response',
since: 4.days.ago.to_time.to_i.to_s,
until: Time.zone.now.to_time.to_i.to_s
},
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['meta']['count']).to eq(2)
expect(json_response['payload'].map { |e| e['name'] }).to all(eq('first_response'))
end
context 'with pagination' do
before do
# Create more events to test pagination
30.times do |i|
create(:reporting_event,
account: account,
conversation: conversation,
inbox: inbox,
user: agent,
name: "event_#{i}",
created_at: i.hours.ago)
end
end
it 'returns 25 events per page by default' do
get "/api/v1/accounts/#{account.id}/reporting_events",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload'].size).to eq(25)
expect(json_response['meta']['count']).to eq(33) # 30 + 3 original events
expect(json_response['meta']['current_page']).to eq(1)
end
it 'supports page navigation' do
get "/api/v1/accounts/#{account.id}/reporting_events",
params: { page: 2 },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload'].size).to eq(8) # Remaining events
expect(json_response['meta']['current_page']).to eq(2)
end
end
end
end
end

View File

@@ -0,0 +1,265 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::SamlSettings', type: :request do
let(:account) { create(:account) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
before do
account.enable_features('saml')
account.save!
end
def json_response
JSON.parse(response.body, symbolize_names: true)
end
describe 'GET /api/v1/accounts/{account.id}/saml_settings' do
context 'when unauthenticated' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/saml_settings"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated as administrator' do
context 'when SAML settings exist' do
let(:saml_settings) do
create(:account_saml_settings,
account: account,
sso_url: 'https://idp.example.com/saml/sso',
role_mappings: { 'Admins' => { 'role' => 1 } })
end
before do
saml_settings # Ensure the record exists
end
it 'returns the SAML settings' do
get "/api/v1/accounts/#{account.id}/saml_settings",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response[:sso_url]).to eq('https://idp.example.com/saml/sso')
expect(json_response[:role_mappings]).to eq({ Admins: { role: 1 } })
end
end
context 'when SAML settings do not exist' do
it 'returns default SAML settings' do
get "/api/v1/accounts/#{account.id}/saml_settings",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response[:role_mappings]).to eq({})
end
end
end
context 'when authenticated as agent' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/saml_settings",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when SAML feature is not enabled' do
before do
account.disable_features('saml')
account.save!
end
it 'returns forbidden with feature not enabled message' do
get "/api/v1/accounts/#{account.id}/saml_settings",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:forbidden)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/saml_settings' do
let(:valid_params) do
key = OpenSSL::PKey::RSA.new(2048)
cert = OpenSSL::X509::Certificate.new
cert.version = 2
cert.serial = 1
cert.subject = OpenSSL::X509::Name.parse('/C=US/ST=Test/L=Test/O=Test/CN=test.example.com')
cert.issuer = cert.subject
cert.public_key = key.public_key
cert.not_before = Time.zone.now
cert.not_after = cert.not_before + (365 * 24 * 60 * 60)
cert.sign(key, OpenSSL::Digest.new('SHA256'))
{
saml_settings: {
sso_url: 'https://idp.example.com/saml/sso',
certificate: cert.to_pem,
idp_entity_id: 'https://idp.example.com/saml/metadata',
role_mappings: { 'Admins' => { 'role' => 1 }, 'Users' => { 'role' => 0 } }
}
}
end
context 'when unauthenticated' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/saml_settings", params: valid_params
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated as administrator' do
context 'with valid parameters' do
it 'creates SAML settings' do
expect do
post "/api/v1/accounts/#{account.id}/saml_settings",
params: valid_params,
headers: administrator.create_new_auth_token,
as: :json
end.to change(AccountSamlSettings, :count).by(1)
expect(response).to have_http_status(:success)
saml_settings = AccountSamlSettings.find_by(account: account)
expect(saml_settings.sso_url).to eq('https://idp.example.com/saml/sso')
expect(saml_settings.role_mappings).to eq({ 'Admins' => { 'role' => 1 }, 'Users' => { 'role' => 0 } })
end
end
context 'with invalid parameters' do
let(:invalid_params) do
valid_params.tap do |params|
params[:saml_settings][:sso_url] = nil
end
end
it 'returns unprocessable entity' do
post "/api/v1/accounts/#{account.id}/saml_settings",
params: invalid_params,
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(AccountSamlSettings.count).to eq(0)
end
end
end
context 'when authenticated as agent' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/saml_settings",
params: valid_params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
expect(AccountSamlSettings.count).to eq(0)
end
end
end
describe 'PUT /api/v1/accounts/{account.id}/saml_settings' do
let(:saml_settings) do
create(:account_saml_settings,
account: account,
sso_url: 'https://old.example.com/saml')
end
let(:update_params) do
key = OpenSSL::PKey::RSA.new(2048)
cert = OpenSSL::X509::Certificate.new
cert.version = 2
cert.serial = 3
cert.subject = OpenSSL::X509::Name.parse('/C=US/ST=Test/L=Test/O=Test/CN=update.example.com')
cert.issuer = cert.subject
cert.public_key = key.public_key
cert.not_before = Time.zone.now
cert.not_after = cert.not_before + (365 * 24 * 60 * 60)
cert.sign(key, OpenSSL::Digest.new('SHA256'))
{
saml_settings: {
sso_url: 'https://new.example.com/saml/sso',
certificate: cert.to_pem,
role_mappings: { 'NewGroup' => { 'custom_role_id' => 5 } }
}
}
end
before do
saml_settings # Ensure the record exists
end
context 'when unauthenticated' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/saml_settings", params: update_params
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated as administrator' do
it 'updates SAML settings' do
put "/api/v1/accounts/#{account.id}/saml_settings",
params: update_params,
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
saml_settings.reload
expect(saml_settings.sso_url).to eq('https://new.example.com/saml/sso')
expect(saml_settings.role_mappings).to eq({ 'NewGroup' => { 'custom_role_id' => 5 } })
end
end
context 'when authenticated as agent' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/saml_settings",
params: update_params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/saml_settings' do
let(:saml_settings) { create(:account_saml_settings, account: account) }
before do
saml_settings # Ensure the record exists
end
context 'when unauthenticated' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/saml_settings"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated as administrator' do
it 'destroys SAML settings' do
expect do
delete "/api/v1/accounts/#{account.id}/saml_settings",
headers: administrator.create_new_auth_token
end.to change(AccountSamlSettings, :count).by(-1)
expect(response).to have_http_status(:no_content)
end
end
context 'when authenticated as agent' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/saml_settings",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
expect(AccountSamlSettings.count).to eq(1)
end
end
end
end

View File

@@ -0,0 +1,192 @@
require 'rails_helper'
RSpec.describe 'Enterprise SLA API', type: :request do
let(:account) { create(:account) }
let(:administrator) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
before do
create(:sla_policy, account: account, name: 'SLA 1')
end
describe 'GET #index' do
context 'when it is an authenticated user' do
it 'returns all slas in the account' do
get "/api/v1/accounts/#{account.id}/sla_policies",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['payload'][0]).to include('name' => 'SLA 1')
end
end
context 'when the user is an agent' do
it 'returns slas in the account' do
get "/api/v1/accounts/#{account.id}/sla_policies",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['payload'][0]).to include('name' => 'SLA 1')
end
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/sla_policies"
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'GET #show' do
let(:sla_policy) { create(:sla_policy, account: account) }
context 'when it is an authenticated user' do
it 'shows the sla' do
get "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['payload']).to include('name' => sla_policy.name)
end
end
context 'when the user is an agent' do
it 'shows the sla details' do
get "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['payload']).to include('name' => sla_policy.name)
end
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/sla_policies"
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'POST #create' do
let(:valid_params) do
{ sla_policy: { name: 'SLA 2',
description: 'SLA for premium customers',
first_response_time_threshold: 1000,
next_response_time_threshold: 2000,
resolution_time_threshold: 3000,
only_during_business_hours: false } }
end
context 'when it is an authenticated user' do
it 'creates the sla_policy' do
expect do
post "/api/v1/accounts/#{account.id}/sla_policies", params: valid_params,
headers: administrator.create_new_auth_token
end.to change(SlaPolicy, :count).by(1)
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['payload']).to include('name' => 'SLA 2')
end
end
context 'when the user is an agent' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/sla_policies",
params: valid_params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/sla_policies"
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'PUT #update' do
let(:sla_policy) { create(:sla_policy, account: account) }
context 'when it is an authenticated user' do
it 'updates the sla_policy' do
put "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}",
params: { sla_policy: { name: 'SLA 2' } },
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['payload']).to include('name' => 'SLA 2')
end
end
context 'when the user is an agent' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}",
params: { sla_policy: { name: 'SLA 2' } },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}"
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'DELETE #destroy' do
let(:sla_policy) { create(:sla_policy, account: account) }
context 'when it is an authenticated user' do
it 'queues the sla_policy for deletion' do
expect(DeleteObjectJob).to receive(:perform_later).with(sla_policy, administrator, kind_of(String))
delete "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
end
end
context 'when the user is an agent' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}"
expect(response).to have_http_status(:unauthorized)
end
end
end
end

View File

@@ -0,0 +1,137 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Api::V1::Auth', type: :request do
let(:account) { create(:account) }
let(:user) { create(:user, email: 'user@example.com') }
before do
account.enable_features('saml')
account.save!
allow(ENV).to receive(:fetch).and_call_original
allow(ENV).to receive(:fetch).with('FRONTEND_URL', nil).and_return('http://www.example.com')
end
describe 'POST /api/v1/auth/saml_login' do
context 'when email is blank' do
it 'returns bad request' do
post '/api/v1/auth/saml_login', params: { email: '' }
expect(response).to have_http_status(:bad_request)
end
end
context 'when email is nil' do
it 'returns bad request' do
post '/api/v1/auth/saml_login', params: {}
expect(response).to have_http_status(:bad_request)
end
end
context 'when user does not exist' do
it 'redirects to SSO login page with error' do
post '/api/v1/auth/saml_login', params: { email: 'nonexistent@example.com' }
expect(response.location).to eq('http://www.example.com/app/login/sso?error=saml-authentication-failed')
end
it 'redirects to mobile deep link with error when target is mobile' do
post '/api/v1/auth/saml_login', params: { email: 'nonexistent@example.com', target: 'mobile' }
expect(response.location).to eq('chatwootapp://auth/saml?error=saml-authentication-failed')
end
end
context 'when user exists but has no SAML enabled accounts' do
before do
create(:account_user, user: user, account: account)
end
it 'redirects to SSO login page with error' do
post '/api/v1/auth/saml_login', params: { email: user.email }
expect(response.location).to eq('http://www.example.com/app/login/sso?error=saml-authentication-failed')
end
it 'redirects to mobile deep link with error when target is mobile' do
post '/api/v1/auth/saml_login', params: { email: user.email, target: 'mobile' }
expect(response.location).to eq('chatwootapp://auth/saml?error=saml-authentication-failed')
end
end
context 'when user has account without SAML feature enabled' do
let(:saml_settings) { create(:account_saml_settings, account: account) }
before do
saml_settings
create(:account_user, user: user, account: account)
account.disable_features('saml')
account.save!
end
it 'redirects to SSO login page with error' do
post '/api/v1/auth/saml_login', params: { email: user.email }
expect(response.location).to eq('http://www.example.com/app/login/sso?error=saml-authentication-failed')
end
it 'redirects to mobile deep link with error when target is mobile' do
post '/api/v1/auth/saml_login', params: { email: user.email, target: 'mobile' }
expect(response.location).to eq('chatwootapp://auth/saml?error=saml-authentication-failed')
end
end
context 'when user has valid SAML configuration' do
let(:saml_settings) do
create(:account_saml_settings, account: account)
end
before do
saml_settings
create(:account_user, user: user, account: account)
end
it 'redirects to SAML initiation URL' do
post '/api/v1/auth/saml_login', params: { email: user.email }
expect(response.location).to include("/auth/saml?account_id=#{account.id}")
end
it 'redirects to SAML initiation URL with mobile relay state' do
post '/api/v1/auth/saml_login', params: { email: user.email, target: 'mobile' }
expect(response.location).to include("/auth/saml?account_id=#{account.id}&RelayState=mobile")
end
end
context 'when user has multiple accounts with SAML' do
let(:account2) { create(:account) }
let(:saml_settings1) do
create(:account_saml_settings, account: account)
end
let(:saml_settings2) do
create(:account_saml_settings, account: account2)
end
before do
account2.enable_features('saml')
account2.save!
saml_settings1
saml_settings2
create(:account_user, user: user, account: account)
create(:account_user, user: user, account: account2)
end
it 'redirects to the first SAML enabled account' do
post '/api/v1/auth/saml_login', params: { email: user.email }
returned_account_id = response.location.match(/account_id=(\d+)/)[1].to_i
expect([account.id, account2.id]).to include(returned_account_id)
end
end
end
end

View File

@@ -0,0 +1,28 @@
require 'rails_helper'
RSpec.describe 'Profile API', type: :request do
describe 'GET /api/v1/profile' do
let(:account) { create(:account) }
let!(:custom_role_account) { create(:account, name: 'Custom Role Account') }
let!(:custom_role) { create(:custom_role, name: 'Custom Role', account: custom_role_account) }
let!(:agent) { create(:user, account: account, custom_attributes: { test: 'test' }, role: :agent) }
before do
create(:account_user, account: custom_role_account, user: agent, custom_role: custom_role)
end
context 'when it is an authenticated user' do
it 'returns user custom role information' do
get '/api/v1/profile',
headers: agent.create_new_auth_token,
as: :json
parsed_response = response.parsed_body
# map accounts object and make sure custom role id and name are present
role_account = parsed_response['accounts'].find { |account| account['id'] == custom_role_account.id }
expect(role_account['custom_role']['id']).to eq(custom_role.id)
expect(role_account['custom_role']['name']).to eq(custom_role.name)
end
end
end
end

View File

@@ -0,0 +1,39 @@
require 'rails_helper'
RSpec.describe 'Enterprise Agents API', type: :request do
let(:account) { create(:account) }
let(:admin) { create(:user, account: account, role: :administrator) }
let!(:custom_role) { create(:custom_role, account: account) }
describe 'POST /api/v1/accounts/{account.id}/agents' do
let(:params) { { email: 'test@example.com', name: 'Test User', role: 'agent', custom_role_id: custom_role.id } }
context 'when it is an authenticated administrator' do
it 'creates an agent with the specified custom role' do
post "/api/v1/accounts/#{account.id}/agents", headers: admin.create_new_auth_token, params: params, as: :json
expect(response).to have_http_status(:success)
agent = account.agents.last
expect(agent.account_users.first.custom_role_id).to eq(custom_role.id)
expect(JSON.parse(response.body)['custom_role_id']).to eq(custom_role.id)
end
end
end
describe 'PUT /api/v1/accounts/{account.id}/agents/:id' do
let(:other_agent) { create(:user, account: account, role: :agent) }
context 'when it is an authenticated administrator' do
it 'modified the custom role of the agent' do
put "/api/v1/accounts/#{account.id}/agents/#{other_agent.id}",
headers: admin.create_new_auth_token,
params: { custom_role_id: custom_role.id },
as: :json
expect(response).to have_http_status(:success)
expect(other_agent.account_users.first.reload.custom_role_id).to eq(custom_role.id)
expect(JSON.parse(response.body)['custom_role_id']).to eq(custom_role.id)
end
end
end
end

View File

@@ -0,0 +1,103 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Enterprise Articles API', type: :request do
let(:account) { create(:account) }
let(:admin) { create(:user, :administrator, account: account) }
let(:agent) { create(:user, account: account, role: :agent) }
let!(:portal) { create(:portal, name: 'test_portal', account_id: account.id) }
let!(:category) { create(:category, name: 'category', portal: portal, account_id: account.id, locale: 'en', slug: 'category_slug') }
let!(:article) { create(:article, category: category, portal: portal, account_id: account.id, author_id: admin.id) }
# Create a custom role with knowledge_base_manage permission
let!(:custom_role) { create(:custom_role, account: account, permissions: ['knowledge_base_manage']) }
# Create user without account
let!(:agent_with_role) { create(:user) }
# Then create account_user association with custom_role
let(:agent_with_role_account_user) do
create(:account_user, user: agent_with_role, account: account, role: :agent, custom_role: custom_role)
end
# Ensure the account_user with custom role is created before tests run
before do
agent_with_role_account_user
end
describe 'GET /api/v1/accounts/:account_id/portals/:portal_slug/articles/:id' do
context 'when it is an authenticated user' do
it 'returns success for agents with knowledge_base_manage permission' do
get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles/#{article.id}",
headers: agent_with_role.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
end
end
end
describe 'POST /api/v1/accounts/:account_id/portals/:portal_slug/articles' do
let(:article_params) do
{
article: {
category_id: category.id,
title: 'New Article',
slug: 'new-article',
content: 'This is a new article',
author_id: agent_with_role.id,
status: 'draft'
}
}
end
context 'when it is an authenticated user' do
it 'returns success for agents with knowledge_base_manage permission' do
post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles",
params: article_params,
headers: agent_with_role.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload']['title']).to eq('New Article')
end
end
end
describe 'PUT /api/v1/accounts/:account_id/portals/:portal_slug/articles/:id' do
let(:article_params) do
{
article: {
title: 'Updated Article',
content: 'This is an updated article'
}
}
end
context 'when it is an authenticated user' do
it 'returns success for agents with knowledge_base_manage permission' do
put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles/#{article.id}",
params: article_params,
headers: agent_with_role.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload']['title']).to eq('Updated Article')
end
end
end
describe 'DELETE /api/v1/accounts/:account_id/portals/:portal_slug/articles/:id' do
context 'when it is an authenticated user' do
it 'returns success for agents with knowledge_base_manage permission' do
delete "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles/#{article.id}",
headers: agent_with_role.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(Article.find_by(id: article.id)).to be_nil
end
end
end
end

View File

@@ -0,0 +1,111 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Enterprise Categories 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!(:portal) { create(:portal, name: 'test_portal', account_id: account.id, config: { allowed_locales: %w[en es] }) }
let!(:category) { create(:category, name: 'category', portal: portal, account_id: account.id, slug: 'category_slug', position: 1) }
# Create a custom role with knowledge_base_manage permission
let!(:custom_role) { create(:custom_role, account: account, permissions: ['knowledge_base_manage']) }
let!(:agent_with_role) { create(:user) }
let(:agent_with_role_account_user) do
create(:account_user, user: agent_with_role, account: account, role: :agent, custom_role: custom_role)
end
# Ensure the account_user with custom role is created before tests run
before do
agent_with_role_account_user
end
describe 'GET /api/v1/accounts/:account_id/portals/:portal_slug/categories' do
context 'when it is an authenticated user' do
it 'returns success for agents with knowledge_base_manage permission' do
get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories",
headers: agent_with_role.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
end
end
end
describe 'GET /api/v1/accounts/:account_id/portals/:portal_slug/categories/:id' do
context 'when it is an authenticated user' do
it 'returns success for agents with knowledge_base_manage permission' do
get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories/#{category.id}",
headers: agent_with_role.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload']['name']).to eq('category')
end
end
end
describe 'POST /api/v1/accounts/:account_id/portals/:portal_slug/categories' do
let(:category_params) do
{
category: {
name: 'New Category',
slug: 'new-category',
locale: 'en',
description: 'This is a new category'
}
}
end
context 'when it is an authenticated user' do
it 'returns success for agents with knowledge_base_manage permission' do
post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories",
params: category_params,
headers: agent_with_role.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload']['name']).to eq('New Category')
end
end
end
describe 'PUT /api/v1/accounts/:account_id/portals/:portal_slug/categories/:id' do
let(:category_params) do
{
category: {
name: 'Updated Category',
description: 'This is an updated category'
}
}
end
context 'when it is an authenticated user' do
it 'returns success for agents with knowledge_base_manage permission' do
put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories/#{category.id}",
params: category_params,
headers: agent_with_role.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload']['name']).to eq('Updated Category')
end
end
end
describe 'DELETE /api/v1/accounts/:account_id/portals/:portal_slug/categories/:id' do
context 'when it is an authenticated user' do
it 'returns success for agents with knowledge_base_manage permission' do
delete "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories/#{category.id}",
headers: agent_with_role.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
end
end
end
end

View File

@@ -0,0 +1,123 @@
require 'rails_helper'
RSpec.describe '/api/v1/accounts/{account.id}/contacts/:id/conversations enterprise', type: :request do
let(:account) { create(:account) }
let(:contact) { create(:contact, account: account) }
let(:inbox) { create(:inbox, account: account) }
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: inbox) }
describe 'GET /api/v1/accounts/{account.id}/contacts/:id/conversations with custom role permissions' do
context 'with user having custom role' do
let(:agent_with_custom_role) { create(:user, account: account, role: :agent) }
let(:custom_role) { create(:custom_role, account: account) }
before do
create(:inbox_member, user: agent_with_custom_role, inbox: inbox)
end
context 'with conversation_participating_manage permission' do
let(:assigned_conversation) do
create(:conversation, account: account, inbox: inbox, contact: contact,
contact_inbox: contact_inbox, assignee: agent_with_custom_role)
end
before do
# Create a conversation assigned to this agent
assigned_conversation
# Create another conversation that shouldn't be visible
create(:conversation, account: account, inbox: inbox, contact: contact,
contact_inbox: contact_inbox, assignee: create(:user, account: account, role: :agent))
# Set up permissions
custom_role.update!(permissions: %w[conversation_participating_manage])
# Associate the custom role with the agent
account_user = AccountUser.find_by(user: agent_with_custom_role, account: account)
account_user.update!(role: :agent, custom_role: custom_role)
end
it 'returns only conversations assigned to the agent' do
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/conversations",
headers: agent_with_custom_role.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
# Should only return the conversation assigned to this agent
expect(json_response['payload'].length).to eq 1
expect(json_response['payload'][0]['id']).to eq assigned_conversation.display_id
end
end
context 'with conversation_unassigned_manage permission' do
let(:unassigned_conversation) do
create(:conversation, account: account, inbox: inbox, contact: contact,
contact_inbox: contact_inbox, assignee: nil)
end
let(:assigned_conversation) do
create(:conversation, account: account, inbox: inbox, contact: contact,
contact_inbox: contact_inbox, assignee: agent_with_custom_role)
end
before do
# Create the conversations
unassigned_conversation
assigned_conversation
create(:conversation, account: account, inbox: inbox, contact: contact,
contact_inbox: contact_inbox, assignee: create(:user, account: account, role: :agent))
# Set up permissions
custom_role.update!(permissions: %w[conversation_unassigned_manage])
# Associate the custom role with the agent
account_user = AccountUser.find_by(user: agent_with_custom_role, account: account)
account_user.update!(role: :agent, custom_role: custom_role)
end
it 'returns unassigned conversations AND conversations assigned to the agent' do
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/conversations",
headers: agent_with_custom_role.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
# Should return both unassigned and assigned to this agent conversations
expect(json_response['payload'].length).to eq 2
conversation_ids = json_response['payload'].pluck('id')
expect(conversation_ids).to include(unassigned_conversation.display_id)
expect(conversation_ids).to include(assigned_conversation.display_id)
end
end
context 'with conversation_manage permission' do
before do
# Create multiple conversations
3.times do
create(:conversation, account: account, inbox: inbox, contact: contact,
contact_inbox: contact_inbox)
end
# Set up permissions
custom_role.update!(permissions: %w[conversation_manage])
# Associate the custom role with the agent
account_user = AccountUser.find_by(user: agent_with_custom_role, account: account)
account_user.update!(role: :agent, custom_role: custom_role)
end
it 'returns all conversations' do
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/conversations",
headers: agent_with_custom_role.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
# Should return all conversations in this inbox
expect(json_response['payload'].length).to eq 3
end
end
end
end
end

View File

@@ -0,0 +1,41 @@
require 'rails_helper'
RSpec.describe 'Enterprise Conversations API', type: :request do
let(:account) { create(:account) }
let(:admin) { create(:user, account: account, role: :administrator) }
describe 'PATCH /api/v1/accounts/{account.id}/conversations/:id' do
let(:conversation) { create(:conversation, account: account) }
let(:sla_policy) { create(:sla_policy, account: account) }
let(:params) { { sla_policy_id: sla_policy.id } }
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, user: agent, inbox: conversation.inbox)
end
it 'updates the conversation if you are an agent with access to inbox' do
patch "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body, symbolize_names: true)[:sla_policy_id]).to eq(sla_policy.id)
end
it 'throws error if conversation already has a different sla' do
conversation.update(sla_policy: create(:sla_policy, account: account))
patch "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(JSON.parse(response.body, symbolize_names: true)[:message]).to eq('Sla policy conversation already has a different sla')
end
end
end
end

View File

@@ -0,0 +1,85 @@
require 'rails_helper'
RSpec.describe 'Enterprise CSAT Survey Responses API', type: :request do
let(:account) { create(:account) }
let(:administrator) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
let!(:csat_survey_response) { create(:csat_survey_response, account: account) }
describe 'PATCH /api/v1/accounts/{account.id}/csat_survey_responses/:id' do
let(:update_params) { { csat_review_notes: 'Customer was very satisfied with the resolution' } }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
patch "/api/v1/accounts/#{account.id}/csat_survey_responses/#{csat_survey_response.id}",
params: update_params,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated agent without permissions' do
it 'returns unauthorized' do
patch "/api/v1/accounts/#{account.id}/csat_survey_responses/#{csat_survey_response.id}",
headers: agent.create_new_auth_token,
params: update_params,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated administrator' do
it 'updates the csat survey response review notes' do
freeze_time do
patch "/api/v1/accounts/#{account.id}/csat_survey_responses/#{csat_survey_response.id}",
headers: administrator.create_new_auth_token,
params: update_params,
as: :json
expect(response).to have_http_status(:success)
csat_survey_response.reload
expect(csat_survey_response.csat_review_notes).to eq('Customer was very satisfied with the resolution')
expect(csat_survey_response.review_notes_updated_by).to eq(administrator)
expect(csat_survey_response.review_notes_updated_at).to eq(Time.current)
end
end
end
context 'when it is an agent with report_manage permission' do
let(:custom_role) { create(:custom_role, account: account, permissions: ['report_manage']) }
let(:agent_with_role) { create(:user) }
before do
create(:account_user, user: agent_with_role, account: account, role: :agent, custom_role: custom_role)
end
it 'updates the csat survey response review notes' do
freeze_time do
patch "/api/v1/accounts/#{account.id}/csat_survey_responses/#{csat_survey_response.id}",
headers: agent_with_role.create_new_auth_token,
params: update_params,
as: :json
expect(response).to have_http_status(:success)
csat_survey_response.reload
expect(csat_survey_response.csat_review_notes).to eq('Customer was very satisfied with the resolution')
expect(csat_survey_response.review_notes_updated_by).to eq(agent_with_role)
expect(csat_survey_response.review_notes_updated_at).to eq(Time.current)
end
end
end
context 'when csat survey response does not exist' do
it 'returns not found' do
patch "/api/v1/accounts/#{account.id}/csat_survey_responses/0",
headers: administrator.create_new_auth_token,
params: update_params,
as: :json
expect(response).to have_http_status(:not_found)
end
end
end
end

View File

@@ -0,0 +1,66 @@
require 'rails_helper'
RSpec.describe 'Enterprise Inboxes API', type: :request do
let(:account) { create(:account) }
let(:admin) { create(:user, account: account, role: :administrator) }
describe 'POST /api/v1/accounts/{account.id}/inboxes' do
let(:inbox) { create(:inbox, account: account) }
context 'when it is an authenticated user' do
let(:admin) { create(:user, account: account, role: :administrator) }
let(:valid_params) do
{ name: 'test', auto_assignment_config: { max_assignment_limit: 10 }, channel: { type: 'web_widget', website_url: 'test.com' } }
end
it 'creates a webwidget inbox with auto assignment config' do
post "/api/v1/accounts/#{account.id}/inboxes",
headers: admin.create_new_auth_token,
params: valid_params,
as: :json
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body)['auto_assignment_config']['max_assignment_limit']).to eq 10
end
it 'creates a voice inbox when administrator' do
allow(Twilio::VoiceWebhookSetupService).to receive(:new).and_return(instance_double(Twilio::VoiceWebhookSetupService,
perform: "AP#{SecureRandom.hex(16)}"))
post "/api/v1/accounts/#{account.id}/inboxes",
headers: admin.create_new_auth_token,
params: { name: 'Voice Inbox',
channel: { type: 'voice', phone_number: '+15551234567',
provider_config: { account_sid: "AC#{SecureRandom.hex(16)}",
auth_token: SecureRandom.hex(16),
api_key_sid: SecureRandom.hex(8),
api_key_secret: SecureRandom.hex(16),
twiml_app_sid: "AP#{SecureRandom.hex(16)}" } } },
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include('Voice Inbox')
expect(response.body).to include('+15551234567')
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/inboxes/:id' do
let(:inbox) { create(:inbox, account: account, auto_assignment_config: { max_assignment_limit: 5 }) }
context 'when it is an authenticated user' do
let(:admin) { create(:user, account: account, role: :administrator) }
let(:valid_params) { { name: 'new test inbox', auto_assignment_config: { max_assignment_limit: 10 } } }
it 'updates inbox with auto assignment config' do
patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
headers: admin.create_new_auth_token,
params: valid_params,
as: :json
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body)['auto_assignment_config']['max_assignment_limit']).to eq 10
end
end
end
end

View File

@@ -0,0 +1,159 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Enterprise Portal API', type: :request do
let(:account) { create(:account) }
let(:admin) { create(:user, :administrator, account: account) }
let(:agent) { create(:user, account: account, role: :agent) }
let!(:portal) { create(:portal, name: 'test_portal', account_id: account.id) }
# Create a custom role with knowledge_base_manage permission
let!(:custom_role) { create(:custom_role, account: account, permissions: ['knowledge_base_manage']) }
# Create user without account
let!(:agent_with_role) { create(:user) }
# Then create account_user association with custom_role
let(:agent_with_role_account_user) do
create(:account_user, user: agent_with_role, account: account, role: :agent, custom_role: custom_role)
end
# Ensure the account_user with custom role is created before tests run
before do
agent_with_role_account_user
end
describe 'GET /api/v1/accounts/:account_id/portals' do
context 'when it is an authenticated user' do
it 'returns success for agents with knowledge_base_manage permission' do
get "/api/v1/accounts/#{account.id}/portals",
headers: agent_with_role.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
end
end
end
describe 'GET /api/v1/accounts/:account_id/portals/:portal_slug' do
context 'when it is an authenticated user' do
it 'returns success for agents with knowledge_base_manage permission' do
get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}",
headers: agent_with_role.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['name']).to eq('test_portal')
end
end
end
describe 'POST /api/v1/accounts/:account_id/portals' do
let(:portal_params) do
{ portal: {
name: 'test_portal',
slug: 'test_kbase',
custom_domain: 'https://support.chatwoot.dev'
} }
end
context 'when it is an authenticated user' do
it 'restricts portal creation for agents with knowledge_base_manage permission' do
post "/api/v1/accounts/#{account.id}/portals",
params: portal_params,
headers: agent_with_role.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'PUT /api/v1/accounts/:account_id/portals/:portal_slug' do
let(:portal_params) do
{ portal: { name: 'updated_portal' } }
end
context 'when it is an authenticated user' do
it 'returns success for agents with knowledge_base_manage permission' do
put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}",
params: portal_params,
headers: agent_with_role.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['name']).to eq('updated_portal')
end
end
end
describe 'GET /api/v1/accounts/{account.id}/portals/{portal.slug}/ssl_status' do
let(:portal_with_domain) { create(:portal, slug: 'portal-with-domain', account_id: account.id, custom_domain: 'docs.example.com') }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/portals/#{portal_with_domain.slug}/ssl_status"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'returns error when custom domain is not configured' do
get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/ssl_status",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq('Custom domain is not configured')
end
it 'returns SSL status when portal has ssl_settings' do
portal_with_domain.update(ssl_settings: {
'cf_status' => 'active',
'cf_verification_errors' => nil
})
mock_service = instance_double(Cloudflare::CheckCustomHostnameService)
allow(Cloudflare::CheckCustomHostnameService).to receive(:new).and_return(mock_service)
allow(mock_service).to receive(:perform).and_return({ data: [] })
get "/api/v1/accounts/#{account.id}/portals/#{portal_with_domain.slug}/ssl_status",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['status']).to eq('active')
expect(response.parsed_body['verification_errors']).to be_nil
end
it 'returns null values when portal has no ssl_settings' do
mock_service = instance_double(Cloudflare::CheckCustomHostnameService)
allow(Cloudflare::CheckCustomHostnameService).to receive(:new).and_return(mock_service)
allow(mock_service).to receive(:perform).and_return({ data: [] })
get "/api/v1/accounts/#{account.id}/portals/#{portal_with_domain.slug}/ssl_status",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['status']).to be_nil
expect(response.parsed_body['verification_errors']).to be_nil
end
it 'returns error when Cloudflare service returns errors' do
mock_service = instance_double(Cloudflare::CheckCustomHostnameService)
allow(Cloudflare::CheckCustomHostnameService).to receive(:new).and_return(mock_service)
allow(mock_service).to receive(:perform).and_return({ errors: ['API token not found'] })
get "/api/v1/accounts/#{account.id}/portals/#{portal_with_domain.slug}/ssl_status",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq(['API token not found'])
end
end
end
end

View File

@@ -0,0 +1,436 @@
require 'rails_helper'
RSpec.describe 'Enterprise Billing APIs', type: :request do
let(:account) { create(:account) }
let!(:admin) { create(:user, account: account, role: :administrator) }
let!(:agent) { create(:user, account: account, role: :agent) }
describe 'POST /enterprise/api/v1/accounts/{account.id}/subscription' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/enterprise/api/v1/accounts/#{account.id}/subscription", as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
context 'when it is an agent' do
it 'returns unauthorized' do
post "/enterprise/api/v1/accounts/#{account.id}/subscription",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an admin' do
it 'enqueues a job' do
expect do
post "/enterprise/api/v1/accounts/#{account.id}/subscription",
headers: admin.create_new_auth_token,
as: :json
end.to have_enqueued_job(Enterprise::CreateStripeCustomerJob).with(account)
expect(account.reload.custom_attributes).to eq({ 'is_creating_customer': true }.with_indifferent_access)
end
it 'does not enqueue a job if a job is already enqueued' do
account.update!(custom_attributes: { is_creating_customer: true })
expect do
post "/enterprise/api/v1/accounts/#{account.id}/subscription",
headers: admin.create_new_auth_token,
as: :json
end.not_to have_enqueued_job(Enterprise::CreateStripeCustomerJob).with(account)
end
it 'does not enqueues a job if customer id is present' do
account.update!(custom_attributes: { 'stripe_customer_id': 'cus_random_string' })
expect do
post "/enterprise/api/v1/accounts/#{account.id}/subscription",
headers: admin.create_new_auth_token,
as: :json
end.not_to have_enqueued_job(Enterprise::CreateStripeCustomerJob).with(account)
end
end
end
end
describe 'POST /enterprise/api/v1/accounts/{account.id}/checkout' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/enterprise/api/v1/accounts/#{account.id}/checkout", as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
context 'when it is an agent' do
it 'returns unauthorized' do
post "/enterprise/api/v1/accounts/#{account.id}/checkout",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an admin and the stripe customer id is not present' do
it 'returns error' do
post "/enterprise/api/v1/accounts/#{account.id}/checkout",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
json_response = JSON.parse(response.body)
expect(json_response['error']).to eq('Please subscribe to a plan before viewing the billing details')
end
end
context 'when it is an admin and the stripe customer is present' do
it 'calls create session' do
account.update!(custom_attributes: { 'stripe_customer_id': 'cus_random_string' })
create_session_service = double
allow(Enterprise::Billing::CreateSessionService).to receive(:new).and_return(create_session_service)
allow(create_session_service).to receive(:create_session).and_return(create_session_service)
allow(create_session_service).to receive(:url).and_return('https://billing.stripe.com/random_string')
post "/enterprise/api/v1/accounts/#{account.id}/checkout",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response['redirect_url']).to eq('https://billing.stripe.com/random_string')
end
end
end
end
describe 'GET /enterprise/api/v1/accounts/{account.id}/limits' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/enterprise/api/v1/accounts/#{account.id}/limits", as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
before do
InstallationConfig.where(name: 'DEPLOYMENT_ENV').first_or_create(value: 'cloud')
InstallationConfig.where(name: 'CHATWOOT_CLOUD_PLANS').first_or_create(value: [{ 'name': 'Hacker' }])
end
context 'when it is an agent' do
it 'returns unauthorized' do
get "/enterprise/api/v1/accounts/#{account.id}/limits",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response['id']).to eq(account.id)
expect(json_response['limits']).to eq(
{
'conversation' => {
'allowed' => 500,
'consumed' => 0
},
'non_web_inboxes' => {
'allowed' => 0,
'consumed' => 0
},
'agents' => {
'allowed' => 2,
'consumed' => 2
}
}
)
end
end
context 'when it is an admin' do
before do
create(:conversation, account: account)
create(:channel_api, account: account)
InstallationConfig.where(name: 'DEPLOYMENT_ENV').first_or_create(value: 'cloud')
InstallationConfig.where(name: 'CHATWOOT_CLOUD_PLANS').first_or_create(value: [{ 'name': 'Hacker' }])
end
it 'returns the limits if the plan is default' do
account.update!(custom_attributes: { plan_name: 'Hacker' })
get "/enterprise/api/v1/accounts/#{account.id}/limits",
headers: admin.create_new_auth_token,
as: :json
expected_response = {
'id' => account.id,
'limits' => {
'conversation' => {
'allowed' => 500,
'consumed' => 1
},
'non_web_inboxes' => {
'allowed' => 0,
'consumed' => 1
},
'agents' => {
'allowed' => 2,
'consumed' => 2
}
}
}
expect(response).to have_http_status(:ok)
expect(JSON.parse(response.body)).to eq(expected_response)
end
it 'returns nil if the plan is not default' do
account.update!(custom_attributes: { plan_name: 'Startups' })
get "/enterprise/api/v1/accounts/#{account.id}/limits",
headers: admin.create_new_auth_token,
as: :json
expected_response = {
'id' => account.id,
'limits' => {
'agents' => {
'allowed' => account.usage_limits[:agents],
'consumed' => account.users.count
},
'conversation' => {},
'captain' => {
'documents' => { 'consumed' => 0, 'current_available' => ChatwootApp.max_limit, 'total_count' => ChatwootApp.max_limit },
'responses' => { 'consumed' => 0, 'current_available' => ChatwootApp.max_limit, 'total_count' => ChatwootApp.max_limit }
},
'non_web_inboxes' => {}
}
}
expect(response).to have_http_status(:ok)
expect(JSON.parse(response.body)).to eq(expected_response)
end
it 'returns limits if a plan is not configured' do
get "/enterprise/api/v1/accounts/#{account.id}/limits",
headers: admin.create_new_auth_token,
as: :json
expected_response = {
'id' => account.id,
'limits' => {
'conversation' => {
'allowed' => 500,
'consumed' => 1
},
'non_web_inboxes' => {
'allowed' => 0,
'consumed' => 1
},
'agents' => {
'allowed' => 2,
'consumed' => 2
}
}
}
expect(response).to have_http_status(:ok)
expect(JSON.parse(response.body)).to eq(expected_response)
end
end
end
end
describe 'POST /enterprise/api/v1/accounts/{account.id}/topup_checkout' do
let(:stripe_customer_id) { 'cus_test123' }
let(:invoice_settings) { Struct.new(:default_payment_method).new('pm_test123') }
let(:stripe_customer) { Struct.new(:invoice_settings, :default_source).new(invoice_settings, nil) }
let(:stripe_invoice) { Struct.new(:id).new('inv_test123') }
before do
create(:installation_config, name: 'CHATWOOT_CLOUD_PLANS', value: [
{ 'name' => 'Hacker', 'product_id' => ['prod_hacker'], 'price_ids' => ['price_hacker'] },
{ 'name' => 'Business', 'product_id' => ['prod_business'], 'price_ids' => ['price_business'] }
])
end
it 'returns unauthorized for unauthenticated user' do
post "/enterprise/api/v1/accounts/#{account.id}/topup_checkout", as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'returns unauthorized for agent' do
post "/enterprise/api/v1/accounts/#{account.id}/topup_checkout",
headers: agent.create_new_auth_token,
params: { credits: 1000 },
as: :json
expect(response).to have_http_status(:unauthorized)
end
context 'when it is an admin' do
before do
account.update!(
custom_attributes: { plan_name: 'Business', stripe_customer_id: stripe_customer_id },
limits: { 'captain_responses' => 1000 }
)
allow(Stripe::Customer).to receive(:retrieve).with(stripe_customer_id).and_return(stripe_customer)
allow(Stripe::Invoice).to receive(:create).and_return(stripe_invoice)
allow(Stripe::InvoiceItem).to receive(:create)
allow(Stripe::Invoice).to receive(:finalize_invoice)
allow(Stripe::Invoice).to receive(:pay)
allow(Stripe::Billing::CreditGrant).to receive(:create)
end
it 'successfully processes topup and returns correct response' do
post "/enterprise/api/v1/accounts/#{account.id}/topup_checkout",
headers: admin.create_new_auth_token,
params: { credits: 1000 },
as: :json
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response['credits']).to eq(1000)
expect(json_response['amount']).to eq(20.0)
expect(json_response['limits']['captain_responses']).to eq(2000)
end
it 'returns error when credits parameter is missing' do
post "/enterprise/api/v1/accounts/#{account.id}/topup_checkout",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
it 'returns error for invalid credits amount' do
post "/enterprise/api/v1/accounts/#{account.id}/topup_checkout",
headers: admin.create_new_auth_token,
params: { credits: 999 },
as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
describe 'POST /enterprise/api/v1/accounts/{account.id}/toggle_deletion' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/enterprise/api/v1/accounts/#{account.id}/toggle_deletion", as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
context 'when it is an agent' do
it 'returns unauthorized' do
post "/enterprise/api/v1/accounts/#{account.id}/toggle_deletion",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when deployment environment is not cloud' do
before do
# Set deployment environment to something other than cloud
InstallationConfig.where(name: 'DEPLOYMENT_ENV').first_or_create(value: 'self_hosted')
end
it 'returns not found' do
post "/enterprise/api/v1/accounts/#{account.id}/toggle_deletion",
headers: admin.create_new_auth_token,
params: { action_type: 'delete' },
as: :json
expect(response).to have_http_status(:not_found)
expect(JSON.parse(response.body)['error']).to eq('Not found')
end
end
context 'when it is an admin' do
before do
# Create the installation config for cloud environment
InstallationConfig.where(name: 'DEPLOYMENT_ENV').first_or_initialize.update!(value: 'cloud')
end
it 'marks the account for deletion when action is delete' do
cancellation_service = instance_double(Enterprise::Billing::CancelCloudSubscriptionsService, perform: true)
allow(Enterprise::Billing::CancelCloudSubscriptionsService).to receive(:new).with(account: account)
.and_return(cancellation_service)
post "/enterprise/api/v1/accounts/#{account.id}/toggle_deletion",
headers: admin.create_new_auth_token,
params: { action_type: 'delete' },
as: :json
expect(response).to have_http_status(:ok)
expect(account.reload.custom_attributes['marked_for_deletion_at']).to be_present
expect(account.custom_attributes['marked_for_deletion_reason']).to eq('manual_deletion')
expect(Enterprise::Billing::CancelCloudSubscriptionsService).to have_received(:new).with(account: account)
expect(cancellation_service).to have_received(:perform)
end
it 'returns success even if stripe cancellation fails' do
cancellation_service = instance_double(Enterprise::Billing::CancelCloudSubscriptionsService)
allow(Enterprise::Billing::CancelCloudSubscriptionsService).to receive(:new).with(account: account)
.and_return(cancellation_service)
allow(cancellation_service).to receive(:perform).and_raise(Stripe::APIError.new('stripe unavailable'))
post "/enterprise/api/v1/accounts/#{account.id}/toggle_deletion",
headers: admin.create_new_auth_token,
params: { action_type: 'delete' },
as: :json
expect(response).to have_http_status(:ok)
expect(account.reload.custom_attributes['marked_for_deletion_at']).to be_present
expect(account.custom_attributes['marked_for_deletion_reason']).to eq('manual_deletion')
end
it 'unmarks the account for deletion when action is undelete' do
# First mark the account for deletion
account.update!(
custom_attributes: {
'marked_for_deletion_at' => 7.days.from_now.iso8601,
'marked_for_deletion_reason' => 'manual_deletion'
}
)
post "/enterprise/api/v1/accounts/#{account.id}/toggle_deletion",
headers: admin.create_new_auth_token,
params: { action_type: 'undelete' },
as: :json
expect(response).to have_http_status(:ok)
expect(account.reload.custom_attributes['marked_for_deletion_at']).to be_nil
expect(account.custom_attributes['marked_for_deletion_reason']).to be_nil
end
it 'returns error for invalid action' do
post "/enterprise/api/v1/accounts/#{account.id}/toggle_deletion",
headers: admin.create_new_auth_token,
params: { action_type: 'invalid' },
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(JSON.parse(response.body)['error']).to include('Invalid action_type')
end
it 'returns error when action parameter is missing' do
post "/enterprise/api/v1/accounts/#{account.id}/toggle_deletion",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(JSON.parse(response.body)['error']).to include('Invalid action_type')
end
end
end
end
end

View File

@@ -0,0 +1,67 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Enterprise Reports API', type: :request do
let(:account) { create(:account) }
let(:agent) { create(:user, account: account, role: :agent) }
# Create a custom role with report_manage permission
let!(:custom_role) { create(:custom_role, account: account, permissions: ['report_manage']) }
let!(:agent_with_role) { create(:user) }
let(:agent_with_role_account_user) do
create(:account_user, user: agent_with_role, account: account, role: :agent, custom_role: custom_role)
end
let(:default_timezone) { 'UTC' }
let(:start_of_today) { Time.current.in_time_zone(default_timezone).beginning_of_day.to_i }
let(:end_of_today) { Time.current.in_time_zone(default_timezone).end_of_day.to_i }
let(:params) { { timezone_offset: Time.zone.utc_offset } }
before do
agent_with_role_account_user
end
describe 'GET /api/v2/accounts/:account_id/reports' do
context 'when it is an authenticated user' do
let(:params) do
super().merge(
metric: 'conversations_count',
type: :account,
since: start_of_today.to_s,
until: end_of_today.to_s
)
end
it 'returns success for agents with report_manage permission' do
get "/api/v2/accounts/#{account.id}/reports",
params: params,
headers: agent_with_role.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
end
end
end
describe 'GET /api/v2/accounts/:account_id/reports/summary' do
context 'when it is an authenticated user' do
let(:params) do
super().merge(
type: :account,
since: start_of_today.to_s,
until: end_of_today.to_s
)
end
it 'returns success for agents with report_manage permission' do
get "/api/v2/accounts/#{account.id}/reports/summary",
params: params,
headers: agent_with_role.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
end
end
end
end

View File

@@ -0,0 +1,88 @@
require 'rails_helper'
RSpec.describe Enterprise::Api::V2::AccountsController, type: :request do
let(:email) { Faker::Internet.email }
let(:user) { create(:user) }
let(:account) { create(:account) }
let(:clearbit_data) do
{
name: 'John Doe',
company_name: 'Acme Inc',
industry: 'Software',
company_size: '51-200',
timezone: 'America/Los_Angeles',
logo: 'https://logo.clearbit.com/acme.com'
}
end
before do
allow(Enterprise::ClearbitLookupService).to receive(:lookup).and_return(clearbit_data)
end
describe 'POST /api/v1/accounts' do
let(:account_builder) { double }
let(:account) { create(:account) }
let(:user) { create(:user, email: email, account: account) }
before do
allow(AccountBuilder).to receive(:new).and_return(account_builder)
end
it 'fetches data from clearbit and updates user and account info' do
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
allow(account_builder).to receive(:perform).and_return([user, account])
params = { email: email, user: nil, locale: nil, password: 'Password1!' }
post api_v2_accounts_url,
params: params,
as: :json
expect(AccountBuilder).to have_received(:new).with(params.except(:password).merge(user_password: params[:password]))
expect(account_builder).to have_received(:perform)
expect(Enterprise::ClearbitLookupService).to have_received(:lookup).with(email)
custom_attributes = account.custom_attributes
expect(account.name).to eq('Acme Inc')
expect(custom_attributes['industry']).to eq('Software')
expect(custom_attributes['company_size']).to eq('51-200')
expect(custom_attributes['timezone']).to eq('America/Los_Angeles')
end
end
it 'updates the onboarding step in custom attributes' do
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
allow(account_builder).to receive(:perform).and_return([user, account])
params = { email: email, user: nil, locale: nil, password: 'Password1!' }
post api_v2_accounts_url,
params: params,
as: :json
custom_attributes = account.custom_attributes
expect(custom_attributes['onboarding_step']).to eq('profile_update')
end
end
it 'handles errors when fetching data from clearbit' do
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
allow(account_builder).to receive(:perform).and_return([user, account])
allow(Enterprise::ClearbitLookupService).to receive(:lookup).and_raise(StandardError)
params = { email: email, user: nil, locale: nil, password: 'Password1!' }
post api_v2_accounts_url,
params: params,
as: :json
expect(AccountBuilder).to have_received(:new).with(params.except(:password).merge(user_password: params[:password]))
expect(account_builder).to have_received(:perform)
expect(Enterprise::ClearbitLookupService).to have_received(:lookup).with(email)
expect(response).to have_http_status(:success)
end
end
end
end

View File

@@ -0,0 +1,61 @@
require 'rails_helper'
RSpec.describe 'Enterprise SAML OmniAuth Callbacks', type: :request do
let!(:account) { create(:account) }
let(:saml_settings) { create(:account_saml_settings, account: account) }
def set_saml_config(email = 'test@example.com')
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:saml] = OmniAuth::AuthHash.new(
provider: 'saml',
uid: '123545',
info: {
name: 'Test User',
email: email
}
)
end
before do
allow(ChatwootApp).to receive(:enterprise?).and_return(true)
account.enable_features!('saml')
saml_settings
end
describe '#saml callback' do
it 'creates new user and logs them in' do
with_modified_env FRONTEND_URL: 'http://www.example.com' do
set_saml_config('new_user@example.com')
get "/omniauth/saml/callback?account_id=#{account.id}"
# expect a 302 redirect to auth/saml/callback
expect(response).to redirect_to('http://www.example.com/auth/saml/callback')
follow_redirect!
# expect redirect to login with SSO token
expect(response).to redirect_to(%r{/app/login\?email=.+&sso_auth_token=.+$})
# verify user was created
user = User.from_email('new_user@example.com')
expect(user).to be_present
expect(user.provider).to eq('saml')
end
end
it 'logs in existing user' do
with_modified_env FRONTEND_URL: 'http://www.example.com' do
create(:user, email: 'existing@example.com', account: account)
set_saml_config('existing@example.com')
get "/omniauth/saml/callback?account_id=#{account.id}"
# expect a 302 redirect to auth/saml/callback
expect(response).to redirect_to('http://www.example.com/auth/saml/callback')
follow_redirect!
expect(response).to redirect_to(%r{/app/login\?email=.+&sso_auth_token=.+$})
end
end
end
end

View File

@@ -0,0 +1,36 @@
require 'rails_helper'
RSpec.describe 'Enterprise Passwords Controller', type: :request do
let!(:account) { create(:account) }
describe 'POST /auth/password' do
context 'with SAML user email' do
let!(:saml_user) { create(:user, email: 'saml@example.com', provider: 'saml', account: account) }
it 'prevents password reset and returns forbidden with custom error message' do
params = { email: saml_user.email, redirect_url: 'http://test.host' }
post user_password_path, params: params, as: :json
expect(response).to have_http_status(:forbidden)
json_response = JSON.parse(response.body)
expect(json_response['success']).to be(false)
expect(json_response['errors']).to include(I18n.t('messages.reset_password_saml_user'))
end
end
context 'with non-SAML user email' do
let!(:regular_user) { create(:user, email: 'regular@example.com', provider: 'email', account: account) }
it 'allows password reset for non-SAML users' do
params = { email: regular_user.email, redirect_url: 'http://test.host' }
post user_password_path, params: params, as: :json
expect(response).to have_http_status(:ok)
json_response = JSON.parse(response.body)
expect(json_response['message']).to be_present
end
end
end
end

View File

@@ -0,0 +1,96 @@
require 'rails_helper'
RSpec.describe 'Enterprise Audit API', type: :request do
let!(:account) { create(:account) }
let!(:user) { create(:user, password: 'Password1!', account: account) }
describe 'POST /sign_in' do
context 'with SAML user attempting password login' do
let(:saml_settings) { create(:account_saml_settings, account: account) }
let(:saml_user) { create(:user, email: 'saml@example.com', provider: 'saml', account: account) }
before do
saml_settings
saml_user
end
it 'prevents login and returns SAML authentication error' do
params = { email: saml_user.email, password: 'Password1!' }
post new_user_session_url, params: params, as: :json
expect(response).to have_http_status(:unauthorized)
json_response = JSON.parse(response.body)
expect(json_response['success']).to be(false)
expect(json_response['errors']).to include(I18n.t('messages.login_saml_user'))
end
it 'allows login with valid SSO token' do
valid_token = saml_user.generate_sso_auth_token
params = { email: saml_user.email, sso_auth_token: valid_token, password: 'Password1!' }
expect do
post new_user_session_url, params: params, as: :json
end.to change(Enterprise::AuditLog, :count).by(1)
expect(response).to have_http_status(:success)
expect(response.body).to include(saml_user.email)
end
end
context 'with regular user credentials' do
it 'creates a sign_in audit event wwith valid credentials' do
params = { email: user.email, password: 'Password1!' }
expect do
post new_user_session_url,
params: params,
as: :json
end.to change(Enterprise::AuditLog, :count).by(1)
expect(response).to have_http_status(:success)
expect(response.body).to include(user.email)
# Check if the sign_in event is created
user.reload
expect(user.audits.last.action).to eq('sign_in')
expect(user.audits.last.associated_id).to eq(account.id)
expect(user.audits.last.associated_type).to eq('Account')
end
it 'will not create a sign_in audit event with invalid credentials' do
params = { email: user.email, password: 'invalid' }
expect do
post new_user_session_url,
params: params,
as: :json
end.not_to change(Enterprise::AuditLog, :count)
end
end
context 'with blank email' do
it 'skips SAML check and processes normally' do
params = { email: '', password: 'Password1!' }
post new_user_session_url, params: params, as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'DELETE /sign_out' do
context 'when it is an authenticated user' do
it 'signs out the user and creates an audit event' do
expect do
delete '/auth/sign_out', headers: user.create_new_auth_token
end.to change(Enterprise::AuditLog, :count).by(1)
expect(response).to have_http_status(:success)
user.reload
expect(user.audits.last.action).to eq('sign_out')
expect(user.audits.last.associated_id).to eq(account.id)
expect(user.audits.last.associated_type).to eq('Account')
end
end
end
end

View File

@@ -0,0 +1,19 @@
require 'rails_helper'
RSpec.describe 'Public Articles API', type: :request do
let!(:portal) { create(:portal, slug: 'test-portal', config: { allowed_locales: %w[en es] }, custom_domain: 'www.example.com') }
describe 'GET /public/api/v1/portals/:slug/articles' do
before do
portal.account.enable_features!(:help_center_embedding_search)
end
context 'with help_center_embedding_search feature' do
it 'get all articles with searched text query using vector search if enabled' do
allow(Article).to receive(:vector_search)
get "/hc/#{portal.slug}/en/articles.json", params: { query: 'funny' }
expect(Article).to have_received(:vector_search)
end
end
end
end

View File

@@ -0,0 +1,121 @@
require 'rails_helper'
RSpec.describe 'Firecrawl Webhooks', type: :request do
describe 'POST /enterprise/webhooks/firecrawl?assistant_id=:assistant_id&token=:token' do
let!(:api_key) { create(:installation_config, name: 'CAPTAIN_FIRECRAWL_API_KEY', value: 'test_api_key_123') }
let!(:account) { create(:account) }
let!(:assistant) { create(:captain_assistant, account: account) }
let(:payload_data) do
{
markdown: 'hello world',
metadata: { ogUrl: 'https://example.com' }
}
end
# Generate actual token using the helper
let(:valid_token) do
token_base = "#{api_key.value[-4..]}#{assistant.id}#{assistant.account_id}"
Digest::SHA256.hexdigest(token_base)
end
context 'with valid token' do
context 'with crawl.page event type' do
let(:valid_params) do
{
type: 'crawl.page',
data: [payload_data]
}
end
it 'processes the webhook and returns success' do
expect(Captain::Tools::FirecrawlParserJob).to receive(:perform_later)
.with(
assistant_id: assistant.id,
payload: payload_data
)
post(
"/enterprise/webhooks/firecrawl?assistant_id=#{assistant.id}&token=#{valid_token}",
params: valid_params,
as: :json
)
expect(response).to have_http_status(:ok)
expect(response.body).to be_empty
end
end
context 'with crawl.completed event type' do
let(:valid_params) do
{
type: 'crawl.completed'
}
end
it 'returns success without enqueuing job' do
expect(Captain::Tools::FirecrawlParserJob).not_to receive(:perform_later)
post("/enterprise/webhooks/firecrawl?assistant_id=#{assistant.id}&token=#{valid_token}",
params: valid_params,
as: :json)
expect(response).to have_http_status(:ok)
expect(response.body).to be_empty
end
end
end
context 'with invalid token' do
let(:invalid_params) do
{
type: 'crawl.page',
data: [payload_data]
}
end
it 'returns unauthorized status' do
post("/enterprise/webhooks/firecrawl?assistant_id=#{assistant.id}&token=invalid_token",
params: invalid_params,
as: :json)
expect(response).to have_http_status(:unauthorized)
end
end
context 'with invalid assistant_id' do
context 'with non-existent assistant_id' do
it 'returns not found status' do
post("/enterprise/webhooks/firecrawl?assistant_id=invalid_id&token=#{valid_token}",
params: { type: 'crawl.page', data: [payload_data] },
as: :json)
expect(response).to have_http_status(:not_found)
end
end
context 'with nil assistant_id' do
it 'returns not found status' do
post("/enterprise/webhooks/firecrawl?token=#{valid_token}",
params: { type: 'crawl.page', data: [payload_data] },
as: :json)
expect(response).to have_http_status(:not_found)
end
end
end
context 'when CAPTAIN_FIRECRAWL_API_KEY is not configured' do
before do
api_key.destroy
end
it 'returns unauthorized status' do
post("/enterprise/webhooks/firecrawl?assistant_id=#{assistant.id}&token=#{valid_token}",
params: { type: 'crawl.page', data: [payload_data] },
as: :json)
expect(response).to have_http_status(:unauthorized)
end
end
end
end

View File

@@ -0,0 +1,26 @@
require 'rails_helper'
RSpec.describe 'Enterprise::Webhooks::StripeController', type: :request do
describe 'POST /enterprise/webhooks/stripe' do
let(:params) { { content: 'hello' } }
it 'call the Enterprise::Billing::HandleStripeEventService with the params' do
handle_stripe = double
allow(Stripe::Webhook).to receive(:construct_event).and_return(params)
allow(Enterprise::Billing::HandleStripeEventService).to receive(:new).and_return(handle_stripe)
allow(handle_stripe).to receive(:perform)
post '/enterprise/webhooks/stripe', headers: { 'Stripe-Signature': 'test' }, params: params
expect(handle_stripe).to have_received(:perform).with(event: params)
end
it 'returns a bad request if the headers are missing' do
post '/enterprise/webhooks/stripe', params: params
expect(response).to have_http_status(:bad_request)
end
it 'returns a bad request if the headers are invalid' do
post '/enterprise/webhooks/stripe', headers: { 'Stripe-Signature': 'test' }, params: params
expect(response).to have_http_status(:bad_request)
end
end
end

View File

@@ -0,0 +1,146 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Twilio::VoiceController', type: :request do
let(:account) { create(:account) }
let(:channel) { create(:channel_voice, account: account, phone_number: '+15551230003') }
let(:inbox) { channel.inbox }
let(:digits) { channel.phone_number.delete_prefix('+') }
before do
allow(Twilio::VoiceWebhookSetupService).to receive(:new)
.and_return(instance_double(Twilio::VoiceWebhookSetupService, perform: "AP#{SecureRandom.hex(16)}"))
end
describe 'POST /twilio/voice/call/:phone' do
let(:call_sid) { 'CA_test_call_sid_123' }
let(:from_number) { '+15550003333' }
let(:to_number) { channel.phone_number }
it 'invokes Voice::InboundCallBuilder for inbound calls and renders conference TwiML' do
instance_double(Voice::InboundCallBuilder)
conversation = create(:conversation, account: account, inbox: inbox)
expect(Voice::InboundCallBuilder).to receive(:perform!).with(
account: account,
inbox: inbox,
from_number: from_number,
call_sid: call_sid
).and_return(conversation)
post "/twilio/voice/call/#{digits}", params: {
'CallSid' => call_sid,
'From' => from_number,
'To' => to_number,
'Direction' => 'inbound'
}
expect(response).to have_http_status(:ok)
expect(response.body).to include('<Response>')
expect(response.body).to include('<Dial>')
end
it 'syncs an existing outbound conversation when Twilio sends the PSTN leg' do
conversation = create(:conversation, account: account, inbox: inbox, identifier: call_sid)
sync_double = instance_double(Voice::CallSessionSyncService, perform: conversation)
expect(Voice::CallSessionSyncService).to receive(:new).with(
hash_including(
conversation: conversation,
call_sid: call_sid,
message_call_sid: conversation.identifier,
leg: {
from_number: from_number,
to_number: to_number,
direction: 'outbound'
}
)
).and_return(sync_double)
post "/twilio/voice/call/#{digits}", params: {
'CallSid' => call_sid,
'From' => from_number,
'To' => to_number,
'Direction' => 'outbound-api'
}
expect(response).to have_http_status(:ok)
expect(response.body).to include('<Response>')
end
it 'uses the parent call SID when syncing outbound-dial legs' do
parent_sid = 'CA_parent'
child_sid = 'CA_child'
conversation = create(:conversation, account: account, inbox: inbox, identifier: parent_sid)
sync_double = instance_double(Voice::CallSessionSyncService, perform: conversation)
expect(Voice::CallSessionSyncService).to receive(:new).with(
hash_including(
conversation: conversation,
call_sid: child_sid,
message_call_sid: parent_sid,
leg: {
from_number: from_number,
to_number: to_number,
direction: 'outbound'
}
)
).and_return(sync_double)
post "/twilio/voice/call/#{digits}", params: {
'CallSid' => child_sid,
'ParentCallSid' => parent_sid,
'From' => from_number,
'To' => to_number,
'Direction' => 'outbound-dial'
}
expect(response).to have_http_status(:ok)
end
it 'raises not found when inbox is not present' do
expect(Voice::InboundCallBuilder).not_to receive(:perform!)
post '/twilio/voice/call/19998887777', params: {
'CallSid' => call_sid,
'From' => from_number,
'To' => to_number,
'Direction' => 'inbound'
}
expect(response).to have_http_status(:not_found)
end
end
describe 'POST /twilio/voice/status/:phone' do
let(:call_sid) { 'CA_status_sid_456' }
it 'invokes Voice::StatusUpdateService with expected params' do
service_double = instance_double(Voice::StatusUpdateService, perform: nil)
expect(Voice::StatusUpdateService).to receive(:new).with(
hash_including(
account: account,
call_sid: call_sid,
call_status: 'completed',
payload: hash_including('CallSid' => call_sid, 'CallStatus' => 'completed')
)
).and_return(service_double)
expect(service_double).to receive(:perform)
post "/twilio/voice/status/#{digits}", params: {
'CallSid' => call_sid,
'CallStatus' => 'completed'
}
expect(response).to have_http_status(:no_content)
end
it 'raises not found when inbox is not present' do
expect(Voice::StatusUpdateService).not_to receive(:new)
post '/twilio/voice/status/18005550101', params: {
'CallSid' => call_sid,
'CallStatus' => 'busy'
}
expect(response).to have_http_status(:not_found)
end
end
end