Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user