Initial commit: Add logistics and order_detail message types
Some checks failed
Lock Threads / action (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Run Linux nightly installer / nightly (push) Has been cancelled

- Add Logistics component with progress tracking
- Add OrderDetail component for order information
- Support data-driven steps and actions
- Add blue color scale to widget SCSS
- Fix node overflow and progress bar rendering issues
- Add English translations for dashboard components

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Liang XJ
2026-01-26 11:16:56 +08:00
commit 092fb2e083
7646 changed files with 975643 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
require 'rails_helper'
RSpec.describe 'Contact Merge Action API', type: :request do
let(:account) { create(:account) }
let!(:base_contact) { create(:contact, account: account) }
let!(:mergee_contact) { create(:contact, account: account) }
describe 'POST /api/v1/accounts/{account.id}/actions/contact_merge' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/actions/contact_merge"
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(:merge_action) { double }
before do
allow(ContactMergeAction).to receive(:new).and_return(merge_action)
allow(merge_action).to receive(:perform)
end
it 'merges two contacts by calling contact merge action' do
post "/api/v1/accounts/#{account.id}/actions/contact_merge",
params: { base_contact_id: base_contact.id, mergee_contact_id: mergee_contact.id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['id']).to eq(base_contact.id)
expected_params = { account: account, base_contact: base_contact, mergee_contact: mergee_contact }
expect(ContactMergeAction).to have_received(:new).with(expected_params)
expect(merge_action).to have_received(:perform)
end
end
end
end

View File

@@ -0,0 +1,316 @@
require 'rails_helper'
RSpec.describe 'Agent Bot API', type: :request do
let!(:account) { create(:account) }
let!(:agent_bot) { create(:agent_bot, account: account) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
describe 'GET /api/v1/accounts/{account.id}/agent_bots' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/agent_bots"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'returns all the agent_bots in account along with global agent bots' do
global_bot = create(:agent_bot)
get "/api/v1/accounts/#{account.id}/agent_bots",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(agent_bot.name)
expect(response.body).to include(global_bot.name)
expect(response.body).to include(agent_bot.access_token.token)
expect(response.body).not_to include(global_bot.access_token.token)
end
it 'properly differentiates between system bots and account bots' do
global_bot = create(:agent_bot)
get "/api/v1/accounts/#{account.id}/agent_bots",
headers: agent.create_new_auth_token,
as: :json
response_data = response.parsed_body
# Find the global bot in the response
global_bot_response = response_data.find { |bot| bot['id'] == global_bot.id }
# Find the account bot in the response
account_bot_response = response_data.find { |bot| bot['id'] == agent_bot.id }
# Verify system_bot attribute and outgoing_url for global bot
expect(global_bot_response['system_bot']).to be(true)
expect(global_bot_response).not_to include('outgoing_url')
# Verify account bot has system_bot attribute false and includes outgoing_url
expect(account_bot_response['system_bot']).to be(false)
expect(account_bot_response).to include('outgoing_url')
# Verify both bots have thumbnail field
expect(global_bot_response).to include('thumbnail')
expect(account_bot_response).to include('thumbnail')
end
end
end
describe 'GET /api/v1/accounts/{account.id}/agent_bots/:id' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'shows the agent bot' do
get "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(agent_bot.name)
expect(response.body).to include(agent_bot.access_token.token)
end
it 'will show a global agent bot' do
global_bot = create(:agent_bot)
get "/api/v1/accounts/#{account.id}/agent_bots/#{global_bot.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(global_bot.name)
expect(response.body).not_to include(global_bot.access_token.token)
# Test for system_bot attribute and webhook URL not being exposed
expect(response.parsed_body['system_bot']).to be(true)
expect(response.parsed_body).not_to include('outgoing_url')
end
end
end
describe 'POST /api/v1/accounts/{account.id}/agent_bots' do
let(:valid_params) { { name: 'test' } }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
expect { post "/api/v1/accounts/#{account.id}/agent_bots", params: valid_params }.not_to change(Label, :count)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'creates the agent bot when administrator' do
expect do
post "/api/v1/accounts/#{account.id}/agent_bots", headers: admin.create_new_auth_token,
params: valid_params
end.to change(AgentBot, :count).by(1)
expect(response).to have_http_status(:success)
end
it 'would not create the agent bot when agent' do
expect do
post "/api/v1/accounts/#{account.id}/agent_bots", headers: agent.create_new_auth_token,
params: valid_params
end.not_to change(AgentBot, :count)
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/agent_bots/:id' do
let(:valid_params) { { name: 'test_updated' } }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
patch "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}",
params: valid_params
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'updates the agent bot' do
patch "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}",
headers: admin.create_new_auth_token,
params: valid_params,
as: :json
expect(response).to have_http_status(:success)
expect(agent_bot.reload.name).to eq('test_updated')
expect(response.body).to include(agent_bot.access_token.token)
end
it 'would not update the agent bot when agent' do
patch "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}",
headers: agent.create_new_auth_token,
params: valid_params,
as: :json
expect(response).to have_http_status(:unauthorized)
expect(agent_bot.reload.name).not_to eq('test_updated')
end
it 'would not update a global agent bot' do
global_bot = create(:agent_bot)
patch "/api/v1/accounts/#{account.id}/agent_bots/#{global_bot.id}",
headers: admin.create_new_auth_token,
params: valid_params,
as: :json
expect(response).to have_http_status(:not_found)
expect(agent_bot.reload.name).not_to eq('test_updated')
expect(response.body).not_to include(global_bot.access_token.token)
end
it 'updates avatar and includes thumbnail in response' do
# no avatar before upload
expect(agent_bot.avatar.attached?).to be(false)
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
patch "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}",
headers: admin.create_new_auth_token,
params: valid_params.merge(avatar: file)
expect(response).to have_http_status(:success)
agent_bot.reload
expect(agent_bot.avatar.attached?).to be(true)
# Verify thumbnail is included in the response
expect(response.parsed_body).to include('thumbnail')
end
it 'updated avatar with avatar_url' do
patch "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}",
headers: admin.create_new_auth_token,
params: valid_params.merge(avatar_url: 'http://example.com/avatar.png'),
as: :json
expect(response).to have_http_status(:success)
expect(Avatar::AvatarFromUrlJob).to have_been_enqueued.with(agent_bot, 'http://example.com/avatar.png')
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/agent_bots/:id' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'deletes an agent bot when administrator' do
delete "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(account.agent_bots.size).to eq(0)
end
it 'would not delete the agent bot when agent' do
delete "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
expect(account.agent_bots.size).not_to eq(0)
end
it 'would not delete a global agent bot' do
global_bot = create(:agent_bot)
delete "/api/v1/accounts/#{account.id}/agent_bots/#{global_bot.id}",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:not_found)
expect(account.agent_bots.size).not_to eq(0)
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/agent_bots/:id/avatar' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
before do
agent_bot.avatar.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
end
it 'delete agent_bot avatar' do
delete "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}/avatar",
headers: admin.create_new_auth_token,
as: :json
expect { agent_bot.avatar.attachment.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect(response).to have_http_status(:success)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/agent_bots/:id/reset_access_token' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}/reset_access_token"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'regenerates the access token when administrator' do
old_token = agent_bot.access_token.token
post "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}/reset_access_token",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
agent_bot.reload
expect(agent_bot.access_token.token).not_to eq(old_token)
json_response = response.parsed_body
expect(json_response['access_token']).to eq(agent_bot.access_token.token)
end
it 'would not reset the access token when agent' do
old_token = agent_bot.access_token.token
post "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}/reset_access_token",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
agent_bot.reload
expect(agent_bot.access_token.token).to eq(old_token)
end
it 'would not reset access token for a global agent bot' do
global_bot = create(:agent_bot)
old_token = global_bot.access_token.token
post "/api/v1/accounts/#{account.id}/agent_bots/#{global_bot.id}/reset_access_token",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:not_found)
global_bot.reload
expect(global_bot.access_token.token).to eq(old_token)
end
end
end
end

View File

@@ -0,0 +1,213 @@
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) }
let!(:agent) { create(:user, account: account, email: 'exists@example.com', role: :agent) }
describe 'GET /api/v1/accounts/{account.id}/agents' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/agents"
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) }
it 'returns all agents of account' do
get "/api/v1/accounts/#{account.id}/agents",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body.size).to eq(account.users.count)
end
it 'returns custom fields on agents if present' do
agent.update(custom_attributes: { test: 'test' })
get "/api/v1/accounts/#{account.id}/agents",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data.first['custom_attributes']['test']).to eq('test')
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/agents/:id' do
let(:other_agent) { create(:user, account: account, role: :agent) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/agents/#{other_agent.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'returns unauthorized for agents' do
delete "/api/v1/accounts/#{account.id}/agents/#{other_agent.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'deletes the agent and user object if associated with only one account' do
expect(account.users).to include(other_agent)
perform_enqueued_jobs(only: DeleteObjectJob) do
delete "/api/v1/accounts/#{account.id}/agents/#{other_agent.id}",
headers: admin.create_new_auth_token,
as: :json
end
expect(response).to have_http_status(:success)
expect(account.reload.users).not_to include(other_agent)
end
it 'deletes only the agent object when user is associated with multiple accounts' do
other_account = create(:account)
create(:account_user, account_id: other_account.id, user_id: other_agent.id)
perform_enqueued_jobs(only: DeleteObjectJob) do
delete "/api/v1/accounts/#{account.id}/agents/#{other_agent.id}",
headers: admin.create_new_auth_token,
as: :json
end
expect(response).to have_http_status(:success)
expect(account.reload.users).not_to include(other_agent)
expect(other_agent.account_users.count).to eq(1) # Should only be associated with other_account now
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 unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/agents/#{other_agent.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
params = { name: 'TestUser' }
it 'returns unauthorized for agents' do
put "/api/v1/accounts/#{account.id}/agents/#{other_agent.id}",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'modifies an agent name' do
put "/api/v1/accounts/#{account.id}/agents/#{other_agent.id}",
params: params,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(other_agent.reload.name).to eq(params[:name])
end
it 'modifies an agents account user attributes' do
put "/api/v1/accounts/#{account.id}/agents/#{other_agent.id}",
params: { role: 'administrator', availability: 'busy', auto_offline: false },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data['role']).to eq('administrator')
expect(response_data['availability_status']).to eq('busy')
expect(response_data['auto_offline']).to be(false)
expect(other_agent.account_users.first.role).to eq('administrator')
end
end
end
describe 'POST /api/v1/accounts/{account.id}/agents' do
let(:other_agent) { create(:user, account: account, role: :agent) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/agents"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
params = { name: 'NewUser', email: Faker::Internet.email, role: :agent }
it 'returns unauthorized for agents' do
post "/api/v1/accounts/#{account.id}/agents",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'creates a new agent' do
post "/api/v1/accounts/#{account.id}/agents",
params: params,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['email']).to eq(params[:email])
expect(account.users.last.name).to eq('NewUser')
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 it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/agents/bulk_create", params: bulk_create_params
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated as admin' do
it 'creates multiple agents successfully' do
expect do
post "/api/v1/accounts/#{account.id}/agents/bulk_create", params: bulk_create_params, headers: admin.create_new_auth_token
end.to change(User, :count).by(3)
expect(response).to have_http_status(:ok)
end
it 'ignores errors if account_user already exists' do
params = { emails: ['exists@example.com', 'test1@example.com', 'test2@example.com'] }
expect do
post "/api/v1/accounts/#{account.id}/agents/bulk_create", params: params,
headers: admin.create_new_auth_token
end.to change(User, :count).by(2)
expect(response).to have_http_status(:ok)
end
end
end
end

View File

@@ -0,0 +1,296 @@
require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::Articles', type: :request do
let(:account) { create(:account) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:admin) { create(:user, account: account, role: :administrator) }
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: agent.id) }
describe 'POST /api/v1/accounts/{account.id}/portals/{portal.slug}/articles' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles", params: {}
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'creates article' do
article_params = {
article: {
category_id: category.id,
description: 'test description',
title: 'MyTitle',
slug: 'my-title',
content: 'This is my content.',
status: :published,
author_id: agent.id,
position: 3
}
}
post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles",
params: article_params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload']['title']).to eql('MyTitle')
expect(json_response['payload']['status']).to eql('published')
expect(json_response['payload']['position']).to be(3)
end
it 'creates article even if category is not provided' do
article_params = {
article: {
category_id: nil,
description: 'test description',
title: 'MyTitle',
slug: 'my-title',
content: 'This is my content.',
status: :published,
author_id: agent.id,
position: 3
}
}
post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles",
params: article_params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload']['title']).to eql('MyTitle')
expect(json_response['payload']['status']).to eql('published')
expect(json_response['payload']['position']).to be(3)
end
it 'creates article as draft when status is not provided' do
article_params = {
article: {
category_id: category.id,
description: 'test description',
title: 'DraftTitle',
slug: 'draft-title',
content: 'This is my draft content.',
author_id: agent.id
}
}
post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles",
params: article_params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload']['title']).to eql('DraftTitle')
expect(json_response['payload']['status']).to eql('draft')
end
it 'associate to the root article' do
root_article = create(:article, category: category, slug: 'root-article', portal: portal, account_id: account.id, author_id: agent.id,
associated_article_id: nil)
parent_article = create(:article, category: category, slug: 'parent-article', portal: portal, account_id: account.id, author_id: agent.id,
associated_article_id: root_article.id)
article_params = {
article: {
category_id: category.id,
description: 'test description',
title: 'MyTitle',
slug: 'MyTitle',
content: 'This is my content.',
status: :published,
author_id: agent.id,
associated_article_id: parent_article.id
}
}
post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles",
params: article_params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload']['title']).to eql('MyTitle')
category = Article.find(json_response['payload']['id'])
expect(category.associated_article_id).to eql(root_article.id)
end
it 'associate to the current parent article' do
parent_article = create(:article, category: category, portal: portal, account_id: account.id, author_id: agent.id, associated_article_id: nil)
article_params = {
article: {
category_id: category.id,
description: 'test description',
title: 'MyTitle',
slug: 'MyTitle',
content: 'This is my content.',
status: :published,
author_id: agent.id,
associated_article_id: parent_article.id
}
}
post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles",
params: article_params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload']['title']).to eql('MyTitle')
category = Article.find(json_response['payload']['id'])
expect(category.associated_article_id).to eql(parent_article.id)
end
end
end
describe 'PUT /api/v1/accounts/{account.id}/portals/{portal.slug}/articles/{article.id}' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles/#{article.id}", params: {}
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'updates article' do
article_params = {
article: {
title: 'MyTitle2',
status: 'published',
description: 'test_description',
position: 5
}
}
expect(article.title).not_to eql(article_params[:article][:title])
put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles/#{article.id}",
params: article_params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload']['title']).to eql(article_params[:article][:title])
expect(json_response['payload']['status']).to eql(article_params[:article][:status])
expect(json_response['payload']['position']).to eql(article_params[:article][:position])
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/portals/{portal.slug}/articles/{article.id}' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles/#{article.id}", params: {}
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'deletes category' do
delete "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles/#{article.id}",
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
deleted_article = Article.find_by(id: article.id)
expect(deleted_article).to be_nil
end
end
end
describe 'GET /api/v1/accounts/{account.id}/portals/{portal.slug}/articles' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'get all articles' do
article2 = create(:article, account_id: account.id, portal: portal, category: category, author_id: agent.id)
expect(article2.id).not_to be_nil
get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles",
headers: admin.create_new_auth_token,
params: {}
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload'].count).to be 2
end
it 'get all articles with uncategorized articles' do
article2 = create(:article, account_id: account.id, portal: portal, category: nil, locale: 'en', author_id: agent.id)
expect(article2.id).not_to be_nil
get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles",
headers: admin.create_new_auth_token,
params: {}
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload'].count).to be 2
expect(json_response['payload'][0]['id']).to eq article2.id
expect(json_response['payload'][0]['category']['id']).to be_nil
end
it 'get all articles with searched params' do
article2 = create(:article, account_id: account.id, portal: portal, category: category, author_id: agent.id)
expect(article2.id).not_to be_nil
get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles",
headers: admin.create_new_auth_token,
params: { category_slug: category.slug }
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload'].count).to be 2
end
it 'get all articles with searched text query' do
article2 = create(:article,
account_id: account.id,
portal: portal,
category: category,
author_id: agent.id,
content: 'this is some test and funny content')
expect(article2.id).not_to be_nil
get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles",
headers: admin.create_new_auth_token,
params: { query: 'funny' }
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload'].count).to be 1
expect(json_response['meta']['all_articles_count']).to be 2
expect(json_response['meta']['articles_count']).to be 1
expect(json_response['meta']['mine_articles_count']).to be 0
end
end
describe 'GET /api/v1/accounts/{account.id}/portals/{portal.slug}/articles/{article.id}' do
it 'get article' do
article2 = create(:article, account_id: account.id, portal: portal, category: category, author_id: agent.id)
expect(article2.id).not_to be_nil
get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles/#{article2.id}",
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload']['title']).to eq(article2.title)
expect(json_response['payload']['id']).to eq(article2.id)
end
it 'get associated articles' do
root_article = create(:article, category: category, portal: portal, account_id: account.id, author_id: agent.id, associated_article_id: nil)
child_article_1 = create(:article, slug: 'child-1', category: category, portal: portal, account_id: account.id, author_id: agent.id,
associated_article_id: root_article.id)
child_article_2 = create(:article, slug: 'child-2', category: category, portal: portal, account_id: account.id, author_id: agent.id,
associated_article_id: root_article.id)
get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles/#{root_article.id}",
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload']['associated_articles'].length).to eq(2)
associated_articles_ids = json_response['payload']['associated_articles'].pluck('id')
expect(associated_articles_ids).to contain_exactly(child_article_1.id, child_article_2.id)
expect(json_response['payload']['id']).to eq(root_article.id)
end
end
end
end

View File

@@ -0,0 +1,67 @@
require 'rails_helper'
RSpec.describe 'Assignable Agents API', type: :request do
let(:account) { create(:account) }
let(:agent1) { create(:user, account: account, role: :agent) }
let!(:agent2) { create(:user, account: account, role: :agent) }
let!(:admin) { create(:user, account: account, role: :administrator) }
describe 'GET /api/v1/accounts/{account.id}/assignable_agents' do
let(:inbox1) { create(:inbox, account: account) }
let(:inbox2) { create(:inbox, account: account) }
before do
create(:inbox_member, user: agent1, inbox: inbox1)
create(:inbox_member, user: agent1, inbox: inbox2)
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/assignable_agents"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when the user is not part of an inbox' do
context 'when the user is an admininstrator' do
it 'returns all assignable inbox members along with administrators' do
get "/api/v1/accounts/#{account.id}/assignable_agents",
params: { inbox_ids: [inbox1.id, inbox2.id] },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)[:payload]
expect(response_data.size).to eq(2)
expect(response_data.pluck(:role)).to include('agent', 'administrator')
end
end
context 'when the user is an agent' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/assignable_agents",
params: { inbox_ids: [inbox1.id, inbox2.id] },
headers: agent2.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
context 'when the user is part of the inbox' do
it 'returns all assignable inbox members along with administrators' do
get "/api/v1/accounts/#{account.id}/assignable_agents",
params: { inbox_ids: [inbox1.id, inbox2.id] },
headers: agent1.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)[:payload]
expect(response_data.size).to eq(2)
expect(response_data.pluck(:role)).to include('agent', 'administrator')
end
end
end
end

View File

@@ -0,0 +1,63 @@
require 'rails_helper'
RSpec.describe 'Assignment Policy Inboxes API', type: :request do
let(:account) { create(:account) }
let(:assignment_policy) { create(:assignment_policy, account: account) }
describe 'GET /api/v1/accounts/{account_id}/assignment_policies/{assignment_policy_id}/inboxes' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}/inboxes"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated admin' do
let(:admin) { create(:user, account: account, role: :administrator) }
context 'when assignment policy has associated inboxes' do
before do
inbox1 = create(:inbox, account: account)
inbox2 = create(:inbox, account: account)
create(:inbox_assignment_policy, inbox: inbox1, assignment_policy: assignment_policy)
create(:inbox_assignment_policy, inbox: inbox2, assignment_policy: assignment_policy)
end
it 'returns all inboxes associated with the assignment policy' do
get "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}/inboxes",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['inboxes']).to be_an(Array)
expect(json_response['inboxes'].length).to eq(2)
end
end
context 'when assignment policy has no associated inboxes' do
it 'returns empty array' do
get "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}/inboxes",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['inboxes']).to eq([])
end
end
end
context 'when it is an agent' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}/inboxes",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
end

View File

@@ -0,0 +1,326 @@
require 'rails_helper'
RSpec.describe 'Assignment Policies API', type: :request do
let(:account) { create(:account) }
describe 'GET /api/v1/accounts/{account.id}/assignment_policies' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/assignment_policies"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated admin' do
let(:admin) { create(:user, account: account, role: :administrator) }
before do
create_list(:assignment_policy, 3, account: account)
end
it 'returns all assignment policies for the account' do
get "/api/v1/accounts/#{account.id}/assignment_policies",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response.length).to eq(3)
expect(json_response.first.keys).to include('id', 'name', 'description')
end
end
context 'when it is an agent' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/assignment_policies",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/assignment_policies/:id' do
let(:assignment_policy) { create(:assignment_policy, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated admin' do
let(:admin) { create(:user, account: account, role: :administrator) }
it 'returns the assignment policy' do
get "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['id']).to eq(assignment_policy.id)
expect(json_response['name']).to eq(assignment_policy.name)
end
it 'returns not found for non-existent policy' do
get "/api/v1/accounts/#{account.id}/assignment_policies/999999",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:not_found)
end
end
context 'when it is an agent' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/assignment_policies' do
let(:valid_params) do
{
assignment_policy: {
name: 'New Assignment Policy',
description: 'Policy for new team',
conversation_priority: 'longest_waiting',
fair_distribution_limit: 15,
enabled: true
}
}
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/assignment_policies", params: valid_params
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated admin' do
let(:admin) { create(:user, account: account, role: :administrator) }
it 'creates a new assignment policy' do
expect do
post "/api/v1/accounts/#{account.id}/assignment_policies",
headers: admin.create_new_auth_token,
params: valid_params,
as: :json
end.to change(AssignmentPolicy, :count).by(1)
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['name']).to eq('New Assignment Policy')
expect(json_response['conversation_priority']).to eq('longest_waiting')
end
it 'creates policy with minimal required params' do
minimal_params = { assignment_policy: { name: 'Minimal Policy' } }
expect do
post "/api/v1/accounts/#{account.id}/assignment_policies",
headers: admin.create_new_auth_token,
params: minimal_params,
as: :json
end.to change(AssignmentPolicy, :count).by(1)
expect(response).to have_http_status(:success)
end
it 'prevents duplicate policy names within account' do
create(:assignment_policy, account: account, name: 'Duplicate Policy')
duplicate_params = { assignment_policy: { name: 'Duplicate Policy' } }
expect do
post "/api/v1/accounts/#{account.id}/assignment_policies",
headers: admin.create_new_auth_token,
params: duplicate_params,
as: :json
end.not_to change(AssignmentPolicy, :count)
expect(response).to have_http_status(:unprocessable_entity)
end
it 'validates required fields' do
invalid_params = { assignment_policy: { name: '' } }
post "/api/v1/accounts/#{account.id}/assignment_policies",
headers: admin.create_new_auth_token,
params: invalid_params,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
context 'when it is an agent' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/assignment_policies",
headers: agent.create_new_auth_token,
params: valid_params,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'PUT /api/v1/accounts/{account.id}/assignment_policies/:id' do
let(:assignment_policy) { create(:assignment_policy, account: account, name: 'Original Policy') }
let(:update_params) do
{
assignment_policy: {
name: 'Updated Policy',
description: 'Updated description',
fair_distribution_limit: 20
}
}
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}",
params: update_params
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated admin' do
let(:admin) { create(:user, account: account, role: :administrator) }
it 'updates the assignment policy' do
put "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}",
headers: admin.create_new_auth_token,
params: update_params,
as: :json
expect(response).to have_http_status(:success)
assignment_policy.reload
expect(assignment_policy.name).to eq('Updated Policy')
expect(assignment_policy.fair_distribution_limit).to eq(20)
end
it 'allows partial updates' do
partial_params = { assignment_policy: { enabled: false } }
put "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}",
headers: admin.create_new_auth_token,
params: partial_params,
as: :json
expect(response).to have_http_status(:success)
expect(assignment_policy.reload.enabled).to be(false)
expect(assignment_policy.name).to eq('Original Policy') # unchanged
end
it 'prevents duplicate names during update' do
create(:assignment_policy, account: account, name: 'Existing Policy')
duplicate_params = { assignment_policy: { name: 'Existing Policy' } }
put "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}",
headers: admin.create_new_auth_token,
params: duplicate_params,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
it 'returns not found for non-existent policy' do
put "/api/v1/accounts/#{account.id}/assignment_policies/999999",
headers: admin.create_new_auth_token,
params: update_params,
as: :json
expect(response).to have_http_status(:not_found)
end
end
context 'when it is an agent' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}",
headers: agent.create_new_auth_token,
params: update_params,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/assignment_policies/:id' do
let(:assignment_policy) { create(:assignment_policy, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated admin' do
let(:admin) { create(:user, account: account, role: :administrator) }
it 'deletes the assignment policy' do
assignment_policy # create it first
expect do
delete "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}",
headers: admin.create_new_auth_token,
as: :json
end.to change(AssignmentPolicy, :count).by(-1)
expect(response).to have_http_status(:ok)
end
it 'cascades deletion to associated inbox assignment policies' do
inbox = create(:inbox, account: account)
create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy)
expect do
delete "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.id}",
headers: admin.create_new_auth_token,
as: :json
end.to change(InboxAssignmentPolicy, :count).by(-1)
expect(response).to have_http_status(:ok)
end
it 'returns not found for non-existent policy' do
delete "/api/v1/accounts/#{account.id}/assignment_policies/999999",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:not_found)
end
end
context 'when it is an agent' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/assignment_policies/#{assignment_policy.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,448 @@
require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::AutomationRulesController', type: :request do
let(:account) { create(:account) }
let(:administrator) { create(:user, account: account, role: :administrator) }
let!(:inbox) { create(:inbox, account: account, enable_auto_assignment: false) }
let!(:contact) { create(:contact, account: account) }
let(:contact_inbox) { create(:contact_inbox, inbox_id: inbox.id, contact_id: contact.id) }
describe 'GET /api/v1/accounts/{account.id}/automation_rules' do
context 'when it is an authenticated user' do
it 'returns all records' do
automation_rule = create(:automation_rule, account: account, name: 'Test Automation Rule')
get "/api/v1/accounts/#{account.id}/automation_rules",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body, symbolize_names: true)
expect(body[:payload].first[:id]).to eq(automation_rule.id)
end
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/automation_rules"
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/automation_rules' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/automation_rules"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:params) do
{
'name': 'Notify Conversation Created and mark priority query',
'description': 'Notify all administrator about conversation created and mark priority query',
'event_name': 'conversation_created',
'conditions': [
{
'attribute_key': 'browser_language',
'filter_operator': 'equal_to',
'values': ['en'],
'query_operator': 'AND'
},
{
'attribute_key': 'country_code',
'filter_operator': 'equal_to',
'values': %w[USA UK],
'query_operator': nil
}
],
'actions': [
{
'action_name': :send_message,
'action_params': ['Welcome to the chatwoot platform.']
},
{
'action_name': :assign_team,
'action_params': [1]
},
{
'action_name': :add_label,
'action_params': %w[support priority_customer]
}
]
}
end
it 'processes invalid query operator' do
expect(account.automation_rules.count).to eq(0)
params[:conditions] << {
'attribute_key': 'browser_language',
'filter_operator': 'equal_to',
'values': ['en'],
'query_operator': 'invalid'
}
post "/api/v1/accounts/#{account.id}/automation_rules",
headers: administrator.create_new_auth_token,
params: params
expect(response).to have_http_status(:unprocessable_entity)
expect(account.automation_rules.count).to eq(0)
end
it 'throws an error for unknown attributes in condtions' do
expect(account.automation_rules.count).to eq(0)
params[:conditions] << {
'attribute_key': 'unknown_attribute',
'filter_operator': 'equal_to',
'values': ['en'],
'query_operator': 'AND'
}
post "/api/v1/accounts/#{account.id}/automation_rules",
headers: administrator.create_new_auth_token,
params: params
expect(response).to have_http_status(:unprocessable_entity)
expect(account.automation_rules.count).to eq(0)
end
it 'Saves for automation_rules for account with country_code and browser_language conditions' do
expect(account.automation_rules.count).to eq(0)
post "/api/v1/accounts/#{account.id}/automation_rules",
headers: administrator.create_new_auth_token,
params: params
expect(response).to have_http_status(:success)
expect(account.automation_rules.count).to eq(1)
end
it 'Saves for automation_rules for account with status conditions' do
params[:conditions] = [
{
attribute_key: 'status',
filter_operator: 'equal_to',
values: ['resolved'],
query_operator: nil
}
]
expect(account.automation_rules.count).to eq(0)
post "/api/v1/accounts/#{account.id}/automation_rules",
headers: administrator.create_new_auth_token,
params: params
expect(response).to have_http_status(:success)
expect(account.automation_rules.count).to eq(1)
end
it 'Saves file in the automation actions to send an attachments' do
blob = ActiveStorage::Blob.create_and_upload!(
io: Rails.root.join('spec/assets/avatar.png').open,
filename: 'avatar.png',
content_type: 'image/png'
)
expect(account.automation_rules.count).to eq(0)
params[:actions] = [
{
'action_name': :send_message,
'action_params': ['Welcome to the chatwoot platform.']
},
{
'action_name': :send_attachment,
'action_params': [blob.signed_id]
}
]
post "/api/v1/accounts/#{account.id}/automation_rules",
headers: administrator.create_new_auth_token,
params: params
automation_rule = account.automation_rules.first
expect(automation_rule.files.presence).to be_truthy
expect(automation_rule.files.count).to eq(1)
end
it 'Saves files in the automation actions to send multiple attachments' do
blob_1 = ActiveStorage::Blob.create_and_upload!(
io: Rails.root.join('spec/assets/avatar.png').open,
filename: 'avatar.png',
content_type: 'image/png'
)
blob_2 = ActiveStorage::Blob.create_and_upload!(
io: Rails.root.join('spec/assets/sample.png').open,
filename: 'sample.png',
content_type: 'image/png'
)
params[:actions] = [
{
'action_name': :send_attachment,
'action_params': [blob_1.signed_id]
},
{
'action_name': :send_attachment,
'action_params': [blob_2.signed_id]
}
]
post "/api/v1/accounts/#{account.id}/automation_rules",
headers: administrator.create_new_auth_token,
params: params
automation_rule = account.automation_rules.first
expect(automation_rule.files.count).to eq(2)
end
it 'returns error for invalid attachment blob_id' do
params[:actions] = [
{
'action_name': :send_attachment,
'action_params': ['invalid_blob_id']
}
]
post "/api/v1/accounts/#{account.id}/automation_rules",
headers: administrator.create_new_auth_token,
params: params
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq(I18n.t('errors.attachments.invalid'))
end
it 'stores the original blob_id in action_params after create' do
blob = ActiveStorage::Blob.create_and_upload!(
io: Rails.root.join('spec/assets/avatar.png').open,
filename: 'avatar.png',
content_type: 'image/png'
)
params[:actions] = [
{
'action_name': :send_attachment,
'action_params': [blob.signed_id]
}
]
post "/api/v1/accounts/#{account.id}/automation_rules",
headers: administrator.create_new_auth_token,
params: params
automation_rule = account.automation_rules.first
attachment_action = automation_rule.actions.find { |a| a['action_name'] == 'send_attachment' }
expect(attachment_action['action_params'].first).to be_a(Integer)
expect(attachment_action['action_params'].first).to eq(automation_rule.files.first.blob_id)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/automation_rules/{automation_rule.id}' do
let!(:automation_rule) { create(:automation_rule, account: account, name: 'Test Automation Rule') }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/automation_rules/#{automation_rule.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'returns for automation_rule for account' do
expect(account.automation_rules.count).to eq(1)
get "/api/v1/accounts/#{account.id}/automation_rules/#{automation_rule.id}",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body, symbolize_names: true)
expect(body[:payload]).to be_present
expect(body[:payload][:id]).to eq(automation_rule.id)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/automation_rules/{automation_rule.id}/clone' do
let!(:automation_rule) { create(:automation_rule, account: account, name: 'Test Automation Rule') }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/automation_rules/#{automation_rule.id}/clone"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'returns for cloned automation_rule for account' do
expect(account.automation_rules.count).to eq(1)
post "/api/v1/accounts/#{account.id}/automation_rules/#{automation_rule.id}/clone",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body, symbolize_names: true)
expect(body[:payload]).to be_present
expect(body[:payload][:id]).not_to eq(automation_rule.id)
expect(account.automation_rules.count).to eq(2)
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/automation_rules/{automation_rule.id}' do
let!(:automation_rule) { create(:automation_rule, account: account, name: 'Test Automation Rule') }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
patch "/api/v1/accounts/#{account.id}/automation_rules/#{automation_rule.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:update_params) do
{
'description': 'Update description',
'name': 'Update name',
'conditions': [
{
'attribute_key': 'browser_language',
'filter_operator': 'equal_to',
'values': ['en'],
'query_operator': 'AND'
}
],
'actions': [
{
'action_name': :add_label,
'action_params': %w[support priority_customer]
}
]
}
end
it 'returns for cloned automation_rule for account' do
expect(account.automation_rules.count).to eq(1)
expect(account.automation_rules.first.actions.size).to eq(4)
patch "/api/v1/accounts/#{account.id}/automation_rules/#{automation_rule.id}",
headers: administrator.create_new_auth_token,
params: update_params
expect(response).to have_http_status(:success)
body = JSON.parse(response.body, symbolize_names: true)
expect(body[:payload][:name]).to eq('Update name')
expect(body[:payload][:description]).to eq('Update description')
expect(body[:payload][:conditions].size).to eq(1)
expect(body[:payload][:actions].size).to eq(1)
end
it 'returns for updated active flag for automation_rule' do
expect(automation_rule.active).to be(true)
params = { active: false }
patch "/api/v1/accounts/#{account.id}/automation_rules/#{automation_rule.id}",
headers: administrator.create_new_auth_token,
params: params
expect(response).to have_http_status(:success)
body = JSON.parse(response.body, symbolize_names: true)
expect(body[:payload][:active]).to be(false)
expect(automation_rule.reload.active).to be(false)
end
it 'allows update with existing blob_id' do
blob = ActiveStorage::Blob.create_and_upload!(
io: Rails.root.join('spec/assets/avatar.png').open,
filename: 'avatar.png',
content_type: 'image/png'
)
automation_rule.update!(actions: [{ 'action_name' => 'send_attachment', 'action_params' => [blob.id] }])
automation_rule.files.attach(blob)
update_params[:actions] = [
{
'action_name': :send_attachment,
'action_params': [blob.id]
}
]
patch "/api/v1/accounts/#{account.id}/automation_rules/#{automation_rule.id}",
headers: administrator.create_new_auth_token,
params: update_params
expect(response).to have_http_status(:success)
end
it 'returns error for invalid blob_id on update' do
update_params[:actions] = [
{
'action_name': :send_attachment,
'action_params': [999_999]
}
]
patch "/api/v1/accounts/#{account.id}/automation_rules/#{automation_rule.id}",
headers: administrator.create_new_auth_token,
params: update_params
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq(I18n.t('errors.attachments.invalid'))
end
it 'allows adding new attachment on update with signed blob_id' do
blob = ActiveStorage::Blob.create_and_upload!(
io: Rails.root.join('spec/assets/avatar.png').open,
filename: 'avatar.png',
content_type: 'image/png'
)
update_params[:actions] = [
{
'action_name': :send_attachment,
'action_params': [blob.signed_id]
}
]
patch "/api/v1/accounts/#{account.id}/automation_rules/#{automation_rule.id}",
headers: administrator.create_new_auth_token,
params: update_params
expect(response).to have_http_status(:success)
expect(automation_rule.reload.files.count).to eq(1)
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/automation_rules/{automation_rule.id}' do
let!(:automation_rule) { create(:automation_rule, account: account, name: 'Test Automation Rule') }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/automation_rules/#{automation_rule.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'delete the automation_rule for account' do
expect(account.automation_rules.count).to eq(1)
delete "/api/v1/accounts/#{account.id}/automation_rules/#{automation_rule.id}",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
expect(account.automation_rules.count).to eq(0)
end
end
end
end

View File

@@ -0,0 +1,273 @@
require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::BulkActionsController', type: :request do
include ActiveJob::TestHelper
let(:account) { create(:account) }
let(:agent_1) { create(:user, account: account, role: :agent) }
let(:agent_2) { create(:user, account: account, role: :agent) }
let(:team_1) { create(:team, account: account) }
before do
create(:conversation, account_id: account.id, status: :open, team_id: team_1.id)
create(:conversation, account_id: account.id, status: :open, team_id: team_1.id)
create(:conversation, account_id: account.id, status: :open)
create(:conversation, account_id: account.id, status: :open)
Conversation.all.find_each do |conversation|
create(:inbox_member, inbox: conversation.inbox, user: agent_1)
create(:inbox_member, inbox: conversation.inbox, user: agent_2)
end
end
describe 'POST /api/v1/accounts/{account.id}/bulk_action' do
context 'when it is an unauthenticated user' do
let!(:agent) { create(:user) }
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/bulk_actions",
headers: agent.create_new_auth_token,
params: { type: 'Conversation', fields: { status: 'open' }, ids: [1, 2, 3] }
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) }
it 'Ignores bulk_actions for wrong type' do
post "/api/v1/accounts/#{account.id}/bulk_actions",
headers: agent.create_new_auth_token,
params: { type: 'Test', fields: { status: 'snoozed' }, ids: %w[1 2 3] }
expect(response).to have_http_status(:unprocessable_entity)
end
it 'Bulk update conversation status' do
expect(Conversation.first.status).to eq('open')
expect(Conversation.last.status).to eq('open')
expect(Conversation.first.assignee_id).to be_nil
perform_enqueued_jobs do
post "/api/v1/accounts/#{account.id}/bulk_actions",
headers: agent.create_new_auth_token,
params: { type: 'Conversation', fields: { status: 'snoozed' }, ids: Conversation.first(3).pluck(:display_id) }
expect(response).to have_http_status(:success)
end
expect(Conversation.first.status).to eq('snoozed')
expect(Conversation.last.status).to eq('open')
expect(Conversation.first.assignee_id).to be_nil
end
it 'Bulk update conversation team id to none' do
params = { type: 'Conversation', fields: { team_id: 0 }, ids: Conversation.first(1).pluck(:display_id) }
expect(Conversation.first.team).not_to be_nil
perform_enqueued_jobs do
post "/api/v1/accounts/#{account.id}/bulk_actions",
headers: agent.create_new_auth_token,
params: params
expect(response).to have_http_status(:success)
end
expect(Conversation.first.team).to be_nil
last_activity_message = Conversation.first.messages.activity.last
expect(last_activity_message.content).to eq("Unassigned from #{team_1.name} by #{agent.name}")
end
it 'Bulk update conversation team id to team' do
params = { type: 'Conversation', fields: { team_id: team_1.id }, ids: Conversation.last(2).pluck(:display_id) }
expect(Conversation.last.team_id).to be_nil
perform_enqueued_jobs do
post "/api/v1/accounts/#{account.id}/bulk_actions",
headers: agent.create_new_auth_token,
params: params
expect(response).to have_http_status(:success)
end
expect(Conversation.last.team).to eq(team_1)
last_activity_message = Conversation.last.messages.activity.last
expect(last_activity_message.content).to eq("Assigned to #{team_1.name} by #{agent.name}")
end
it 'Bulk update conversation assignee id' do
params = { type: 'Conversation', fields: { assignee_id: agent_1.id }, ids: Conversation.first(3).pluck(:display_id) }
expect(Conversation.first.status).to eq('open')
expect(Conversation.first.assignee_id).to be_nil
expect(Conversation.second.assignee_id).to be_nil
perform_enqueued_jobs do
post "/api/v1/accounts/#{account.id}/bulk_actions",
headers: agent.create_new_auth_token,
params: params
expect(response).to have_http_status(:success)
end
expect(Conversation.first.assignee_id).to eq(agent_1.id)
expect(Conversation.second.assignee_id).to eq(agent_1.id)
expect(Conversation.first.status).to eq('open')
end
it 'Bulk remove assignee id from conversations' do
Conversation.first.update(assignee_id: agent_1.id)
Conversation.second.update(assignee_id: agent_2.id)
params = { type: 'Conversation', fields: { assignee_id: nil }, ids: Conversation.first(3).pluck(:display_id) }
expect(Conversation.first.status).to eq('open')
expect(Conversation.first.assignee_id).to eq(agent_1.id)
expect(Conversation.second.assignee_id).to eq(agent_2.id)
perform_enqueued_jobs do
post "/api/v1/accounts/#{account.id}/bulk_actions",
headers: agent.create_new_auth_token,
params: params
expect(response).to have_http_status(:success)
end
expect(Conversation.first.assignee_id).to be_nil
expect(Conversation.second.assignee_id).to be_nil
expect(Conversation.first.status).to eq('open')
end
it 'Do not bulk update status to nil' do
Conversation.first.update(assignee_id: agent_1.id)
Conversation.second.update(assignee_id: agent_2.id)
params = { type: 'Conversation', fields: { status: nil }, ids: Conversation.first(3).pluck(:display_id) }
expect(Conversation.first.status).to eq('open')
perform_enqueued_jobs do
post "/api/v1/accounts/#{account.id}/bulk_actions",
headers: agent.create_new_auth_token,
params: params
expect(response).to have_http_status(:success)
end
expect(Conversation.first.status).to eq('open')
end
it 'Bulk update conversation status and assignee id' do
params = { type: 'Conversation', fields: { assignee_id: agent_1.id, status: 'snoozed' }, ids: Conversation.first(3).pluck(:display_id) }
expect(Conversation.first.status).to eq('open')
expect(Conversation.second.assignee_id).to be_nil
perform_enqueued_jobs do
post "/api/v1/accounts/#{account.id}/bulk_actions",
headers: agent.create_new_auth_token,
params: params
expect(response).to have_http_status(:success)
end
expect(Conversation.first.assignee_id).to eq(agent_1.id)
expect(Conversation.second.assignee_id).to eq(agent_1.id)
expect(Conversation.first.status).to eq('snoozed')
expect(Conversation.second.status).to eq('snoozed')
end
it 'Bulk update conversation labels' do
params = { type: 'Conversation', ids: Conversation.first(3).pluck(:display_id), labels: { add: %w[support priority_customer] } }
expect(Conversation.first.labels).to eq([])
expect(Conversation.second.labels).to eq([])
perform_enqueued_jobs do
post "/api/v1/accounts/#{account.id}/bulk_actions",
headers: agent.create_new_auth_token,
params: params
expect(response).to have_http_status(:success)
end
expect(Conversation.first.label_list).to contain_exactly('support', 'priority_customer')
expect(Conversation.second.label_list).to contain_exactly('support', 'priority_customer')
end
end
end
describe 'POST /api/v1/accounts/{account.id}/bulk_actions' do
context 'when it is an authenticated user' do
let!(:agent) { create(:user, account: account, role: :agent) }
it 'Bulk delete conversation labels' do
Conversation.first.add_labels(%w[support priority_customer])
Conversation.second.add_labels(%w[support priority_customer])
Conversation.third.add_labels(%w[support priority_customer])
params = { type: 'Conversation', ids: Conversation.first(3).pluck(:display_id), labels: { remove: %w[support] } }
expect(Conversation.first.label_list).to contain_exactly('support', 'priority_customer')
expect(Conversation.second.label_list).to contain_exactly('support', 'priority_customer')
perform_enqueued_jobs do
post "/api/v1/accounts/#{account.id}/bulk_actions",
headers: agent.create_new_auth_token,
params: params
expect(response).to have_http_status(:success)
end
expect(Conversation.first.label_list).to contain_exactly('priority_customer')
expect(Conversation.second.label_list).to contain_exactly('priority_customer')
end
end
end
describe 'POST /api/v1/accounts/{account.id}/bulk_actions (contacts)' do
context 'when it is an authenticated user' do
let!(:agent) { create(:user, account: account, role: :agent) }
it 'enqueues Contacts::BulkActionJob with permitted params' do
contact_one = create(:contact, account: account)
contact_two = create(:contact, account: account)
expect do
post "/api/v1/accounts/#{account.id}/bulk_actions",
headers: agent.create_new_auth_token,
params: {
type: 'Contact',
ids: [contact_one.id, contact_two.id],
labels: { add: %w[vip support] },
extra: 'ignored'
}
end.to have_enqueued_job(Contacts::BulkActionJob).with(
account.id,
agent.id,
hash_including(
'ids' => [contact_one.id.to_s, contact_two.id.to_s],
'labels' => hash_including('add' => %w[vip support])
)
)
expect(response).to have_http_status(:success)
end
it 'returns unauthorized for delete action when user is not admin' do
contact = create(:contact, account: account)
post "/api/v1/accounts/#{account.id}/bulk_actions",
headers: agent.create_new_auth_token,
params: {
type: 'Contact',
ids: [contact.id],
action_name: 'delete'
}
expect(response).to have_http_status(:unauthorized)
end
end
end
end

View File

@@ -0,0 +1,134 @@
require 'rails_helper'
RSpec.describe 'Callbacks API', type: :request do
before do
stub_request(:any, /graph.facebook.com/)
# Mock new and return instance doubles defined above
allow(Koala::Facebook::OAuth).to receive(:new).and_return(koala_oauth)
allow(Koala::Facebook::API).to receive(:new).and_return(koala_api)
allow(Facebook::Messenger::Subscriptions).to receive(:subscribe).and_return(true)
allow(koala_api).to receive(:get_connections).and_return(
[{ 'id' => facebook_page.page_id, 'access_token' => SecureRandom.hex(10) }]
)
allow(koala_oauth).to receive(:exchange_access_token_info).and_return('access_token' => SecureRandom.hex(10))
end
let(:account) { create(:account) }
let!(:facebook_page) { create(:channel_facebook_page, inbox: inbox, account: account) }
let(:valid_params) { attributes_for(:channel_facebook_page).merge(inbox_name: 'Test Inbox') }
let(:inbox) { create(:inbox, account: account) }
# Doubles
let(:koala_api) { instance_double(Koala::Facebook::API) }
let(:koala_oauth) { instance_double(Koala::Facebook::OAuth) }
describe 'POST /api/v1/accounts/{account.id}/callbacks/register_facebook_page' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/callbacks/register_facebook_page"
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 'registers a new facebook page with no avatar' do
post "/api/v1/accounts/#{account.id}/callbacks/register_facebook_page",
headers: admin.create_new_auth_token,
params: valid_params,
as: :json
expect(response).to have_http_status(:success)
end
it 'registers a new facebook page with avatar' do
buf = OpenURI::Buffer.new
io = buf.io
io.base_uri = URI.parse('https://example.org')
allow_any_instance_of(URI::HTTP).to receive(:open).and_return(io) # rubocop:disable RSpec/AnyInstance
post "/api/v1/accounts/#{account.id}/callbacks/register_facebook_page",
headers: admin.create_new_auth_token,
params: valid_params,
as: :json
expect(response).to have_http_status(:success)
end
it 'registers a new facebook page with avatar on redirect' do
allow_any_instance_of(URI::HTTP).to receive(:open).and_raise(OpenURI::HTTPRedirect.new(nil, nil, URI.parse('https://example.org'))) # rubocop:disable RSpec/AnyInstance
post "/api/v1/accounts/#{account.id}/callbacks/register_facebook_page",
headers: admin.create_new_auth_token,
params: valid_params,
as: :json
expect(response).to have_http_status(:success)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/callbacks/facebook_pages' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/callbacks/facebook_pages"
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 facebook pages of account' do
post "/api/v1/accounts/#{account.id}/callbacks/facebook_pages",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(facebook_page.page_id.to_s)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/callbacks/reauthorize_page' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/callbacks/reauthorize_page"
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 'reauthorizes the page' do
params = { inbox_id: inbox.id }
post "/api/v1/accounts/#{account.id}/callbacks/reauthorize_page",
headers: admin.create_new_auth_token,
params: params,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(inbox.id.to_s)
end
it 'returns unprocessable_entity if no page found' do
allow(koala_api).to receive(:get_connections).and_return([])
params = { inbox_id: inbox.id }
post "/api/v1/accounts/#{account.id}/callbacks/reauthorize_page",
headers: admin.create_new_auth_token,
params: params,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
end

View File

@@ -0,0 +1,230 @@
require 'rails_helper'
RSpec.describe 'Campaigns API', type: :request do
let(:account) { create(:account) }
describe 'GET /api/v1/accounts/{account.id}/campaigns' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/campaigns"
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) }
let(:inbox) { create(:inbox, account: account) }
let!(:campaign) { create(:campaign, account: account, inbox: inbox, trigger_rules: { url: 'https://test.com' }) }
it 'returns unauthorized for agents' do
get "/api/v1/accounts/#{account.id}/campaigns",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'returns all campaigns to administrators' do
get "/api/v1/accounts/#{account.id}/campaigns",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
body = JSON.parse(response.body, symbolize_names: true)
expect(body.first[:id]).to eq(campaign.display_id)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/campaigns/:id' do
let(:campaign) { create(:campaign, account: account, trigger_rules: { url: 'https://test.com' }) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/campaigns/#{campaign.display_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 agents' do
get "/api/v1/accounts/#{account.id}/campaigns/#{campaign.display_id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'shows the campaign for administrators' do
get "/api/v1/accounts/#{account.id}/campaigns/#{campaign.display_id}",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body, symbolize_names: true)[:id]).to eq(campaign.display_id)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/campaigns' do
let(:inbox) { create(:inbox, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/campaigns",
params: { inbox_id: inbox.id, title: 'test', message: 'test message' },
as: :json
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 agents' do
post "/api/v1/accounts/#{account.id}/campaigns",
params: { inbox_id: inbox.id, title: 'test', message: 'test message' },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'creates a new campaign' do
post "/api/v1/accounts/#{account.id}/campaigns",
params: { inbox_id: inbox.id, title: 'test', message: 'test message' },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body, symbolize_names: true)[:title]).to eq('test')
end
it 'creates a new ongoing campaign' do
post "/api/v1/accounts/#{account.id}/campaigns",
params: { inbox_id: inbox.id, title: 'test', message: 'test message', trigger_rules: { url: 'https://test.com' } },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body, symbolize_names: true)[:title]).to eq('test')
end
it 'throws error when invalid url provided for ongoing campaign' do
post "/api/v1/accounts/#{account.id}/campaigns",
params: { inbox_id: inbox.id, title: 'test', message: 'test message', trigger_rules: { url: 'javascript' } },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
it 'creates a new oneoff campaign' do
twilio_sms = create(:channel_twilio_sms, account: account)
twilio_inbox = create(:inbox, channel: twilio_sms, account: account)
label1 = create(:label, account: account)
label2 = create(:label, account: account)
scheduled_at = 2.days.from_now
post "/api/v1/accounts/#{account.id}/campaigns",
params: {
inbox_id: twilio_inbox.id, title: 'test', message: 'test message',
scheduled_at: scheduled_at,
audience: [{ type: 'Label', id: label1.id }, { type: 'Label', id: label2.id }]
},
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data[:campaign_type]).to eq('one_off')
expect(response_data[:scheduled_at].present?).to be true
expect(response_data[:scheduled_at]).to eq(scheduled_at.to_i)
expect(response_data[:audience].pluck(:id)).to include(label1.id, label2.id)
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/campaigns/:id' do
let(:inbox) { create(:inbox, account: account) }
let!(:campaign) { create(:campaign, account: account, trigger_rules: { url: 'https://test.com' }) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
patch "/api/v1/accounts/#{account.id}/campaigns/#{campaign.display_id}",
params: { inbox_id: inbox.id, title: 'test', message: 'test message' },
as: :json
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 agents' do
patch "/api/v1/accounts/#{account.id}/campaigns/#{campaign.display_id}",
params: { inbox_id: inbox.id, title: 'test', message: 'test message' },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'updates the campaign' do
patch "/api/v1/accounts/#{account.id}/campaigns/#{campaign.display_id}",
params: { inbox_id: inbox.id, title: 'test', message: 'test message' },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body, symbolize_names: true)[:title]).to eq('test')
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/campaigns/:id' do
let(:inbox) { create(:inbox, account: account) }
let!(:campaign) { create(:campaign, account: account, trigger_rules: { url: 'https://test.com' }) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/campaigns/#{campaign.display_id}",
as: :json
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 'return unauthorized if agent' do
delete "/api/v1/accounts/#{account.id}/campaigns/#{campaign.display_id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'delete campaign if admin' do
delete "/api/v1/accounts/#{account.id}/campaigns/#{campaign.display_id}",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(Campaign.exists?(campaign.display_id)).to be false
end
end
end
end

View File

@@ -0,0 +1,130 @@
require 'rails_helper'
RSpec.describe 'Canned Responses API', type: :request do
let(:account) { create(:account) }
before do
create(:canned_response, account: account, content: 'Hey {{ contact.name }}, Thanks for reaching out', short_code: 'name-short-code')
end
describe 'GET /api/v1/accounts/{account.id}/canned_responses' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/canned_responses"
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) }
it 'returns all the canned responses' do
get "/api/v1/accounts/#{account.id}/canned_responses",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body).to eq(account.canned_responses.as_json)
end
it 'returns all the canned responses the user searched for' do
cr1 = account.canned_responses.first
create(:canned_response, account: account, content: 'Great! Looking forward', short_code: 'short-code')
cr2 = create(:canned_response, account: account, content: 'Thanks for reaching out', short_code: 'content-with-thanks')
cr3 = create(:canned_response, account: account, content: 'Thanks for reaching out', short_code: 'Thanks')
params = { search: 'thanks' }
get "/api/v1/accounts/#{account.id}/canned_responses",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body).to eq(
[cr3, cr2, cr1].as_json
)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/canned_responses' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/canned_responses"
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) }
it 'creates a new canned response' do
params = { short_code: 'short', content: 'content' }
post "/api/v1/accounts/#{account.id}/canned_responses",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(account.canned_responses.count).to eq(2)
end
end
end
describe 'PUT /api/v1/accounts/{account.id}/canned_responses/:id' do
let(:canned_response) { CannedResponse.last }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/canned_responses/#{canned_response.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) }
it 'updates an existing canned response' do
params = { short_code: 'B' }
put "/api/v1/accounts/#{account.id}/canned_responses/#{canned_response.id}",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(canned_response.reload.short_code).to eq('B')
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/canned_responses/:id' do
let(:canned_response) { CannedResponse.last }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/canned_responses/#{canned_response.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) }
it 'destroys the canned response' do
delete "/api/v1/accounts/#{account.id}/canned_responses/#{canned_response.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(CannedResponse.count).to eq(0)
end
end
end
end

View File

@@ -0,0 +1,153 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::Captain::Preferences', 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/preferences' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/captain/preferences",
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
it 'returns captain config' do
get "/api/v1/accounts/#{account.id}/captain/preferences",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response).to have_key(:providers)
expect(json_response).to have_key(:models)
expect(json_response).to have_key(:features)
end
end
context 'when it is an admin' do
it 'returns captain config' do
get "/api/v1/accounts/#{account.id}/captain/preferences",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response).to have_key(:providers)
expect(json_response).to have_key(:models)
expect(json_response).to have_key(:features)
end
end
end
describe 'PUT /api/v1/accounts/{account.id}/captain/preferences' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/captain/preferences",
params: { captain_models: { editor: 'gpt-4.1-mini' } },
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent' do
it 'returns forbidden' do
put "/api/v1/accounts/#{account.id}/captain/preferences",
headers: agent.create_new_auth_token,
params: { captain_models: { editor: 'gpt-4.1-mini' } },
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an admin' do
it 'updates captain_models' do
put "/api/v1/accounts/#{account.id}/captain/preferences",
headers: admin.create_new_auth_token,
params: { captain_models: { editor: 'gpt-4.1-mini' } },
as: :json
expect(response).to have_http_status(:success)
expect(json_response).to have_key(:providers)
expect(json_response).to have_key(:models)
expect(json_response).to have_key(:features)
expect(account.reload.captain_models['editor']).to eq('gpt-4.1-mini')
end
it 'updates captain_features' do
put "/api/v1/accounts/#{account.id}/captain/preferences",
headers: admin.create_new_auth_token,
params: { captain_features: { editor: true } },
as: :json
expect(response).to have_http_status(:success)
expect(json_response).to have_key(:providers)
expect(json_response).to have_key(:models)
expect(json_response).to have_key(:features)
expect(account.reload.captain_features['editor']).to be true
end
it 'merges with existing captain_models' do
account.update!(captain_models: { 'editor' => 'gpt-4.1-mini', 'assistant' => 'gpt-5.1' })
put "/api/v1/accounts/#{account.id}/captain/preferences",
headers: admin.create_new_auth_token,
params: { captain_models: { editor: 'gpt-4.1' } },
as: :json
expect(response).to have_http_status(:success)
expect(json_response).to have_key(:providers)
expect(json_response).to have_key(:models)
expect(json_response).to have_key(:features)
models = account.reload.captain_models
expect(models['editor']).to eq('gpt-4.1')
expect(models['assistant']).to eq('gpt-5.1') # Preserved
end
it 'merges with existing captain_features' do
account.update!(captain_features: { 'editor' => true, 'assistant' => false })
put "/api/v1/accounts/#{account.id}/captain/preferences",
headers: admin.create_new_auth_token,
params: { captain_features: { editor: false } },
as: :json
expect(response).to have_http_status(:success)
expect(json_response).to have_key(:providers)
expect(json_response).to have_key(:models)
expect(json_response).to have_key(:features)
features = account.reload.captain_features
expect(features['editor']).to be false
expect(features['assistant']).to be false # Preserved
end
it 'updates both models and features in single request' do
put "/api/v1/accounts/#{account.id}/captain/preferences",
headers: admin.create_new_auth_token,
params: {
captain_models: { editor: 'gpt-4.1-mini' },
captain_features: { editor: true }
},
as: :json
expect(response).to have_http_status(:success)
expect(json_response).to have_key(:providers)
expect(json_response).to have_key(:models)
expect(json_response).to have_key(:features)
account.reload
expect(account.captain_models['editor']).to eq('gpt-4.1-mini')
expect(account.captain_features['editor']).to be true
end
end
end
end

View File

@@ -0,0 +1,264 @@
require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::Categories', type: :request do
let(:account) { create(:account) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:admin) { create(:user, account: account, role: :administrator) }
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) }
let!(:category_to_associate) do
create(:category, name: 'associated category', portal: portal, account_id: account.id, slug: 'associated_category_slug', position: 2)
end
let!(:related_category_1) do
create(:category, name: 'related category 1', portal: portal, account_id: account.id, slug: 'category_slug_1', position: 3)
end
let!(:related_category_2) do
create(:category, name: 'related category 2', portal: portal, account_id: account.id, slug: 'category_slug_2', position: 4)
end
describe 'POST /api/v1/accounts/{account.id}/portals/{portal.slug}/categories' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories", params: {}
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let!(:category_params) do
{
category: {
name: 'test_category',
description: 'test_description',
position: 5,
locale: 'es',
slug: 'test_category_1',
parent_category_id: category.id,
associated_category_id: category_to_associate.id,
related_category_ids: [related_category_1.id, related_category_2.id]
}
}
end
let!(:category_params_2) do
{
category: {
name: 'test_category_2',
description: 'test_description_2',
position: 6,
locale: 'es',
slug: 'test_category_2',
parent_category_id: category.id,
associated_category_id: category_to_associate.id,
related_category_ids: [related_category_1.id, related_category_2.id]
}
}
end
it 'creates category' do
post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories",
params: category_params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload']['related_categories'][0]['id']).to eql(related_category_1.id)
expect(json_response['payload']['related_categories'][1]['id']).to eql(related_category_2.id)
expect(json_response['payload']['parent_category']['id']).to eql(category.id)
expect(json_response['payload']['root_category']['id']).to eql(category_to_associate.id)
expect(category.reload.sub_category_ids).to eql([Category.last.id])
expect(category_to_associate.reload.associated_category_ids).to eql([Category.last.id])
end
it 'creates multiple sub_categories under one parent_category' do
post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories",
params: category_params,
headers: admin.create_new_auth_token
post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories",
params: category_params_2,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
expect(category.reload.sub_category_ids).to eql(Category.last(2).pluck(:id))
end
it 'creates multiple associated_categories with one category' do
post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories",
params: category_params,
headers: admin.create_new_auth_token
post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories",
params: category_params_2,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
expect(category_to_associate.reload.associated_category_ids).to eql(Category.last(2).pluck(:id))
end
it 'will throw an error on locale, category_id uniqueness' do
post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories",
params: category_params,
headers: admin.create_new_auth_token
post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories",
params: category_params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:unprocessable_entity)
json_response = response.parsed_body
expect(json_response['message']).to eql('Locale should be unique in the category and portal')
end
it 'will throw an error slug presence' do
category_params = {
category: {
name: 'test_category',
description: 'test_description',
position: 1,
locale: 'es'
}
}
post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories",
params: category_params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:unprocessable_entity)
json_response = response.parsed_body
expect(json_response['message']).to eql("Slug can't be blank")
end
end
end
describe 'PUT /api/v1/accounts/{account.id}/portals/{portal.slug}/categories/{category.id}' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories/#{category.id}", params: {}
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'updates category' do
category_params = {
category: {
name: 'test_category_2',
description: 'test_description',
position: 1,
related_category_ids: [related_category_1.id],
parent_category_id: related_category_2.id
}
}
expect(category.name).not_to eql(category_params[:category][:name])
expect(category.related_categories).to be_empty
expect(category.parent_category).to be_nil
put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories/#{category.id}",
params: category_params,
headers: admin.create_new_auth_token
json_response = response.parsed_body
expect(json_response['payload']['name']).to eql(category_params[:category][:name])
expect(json_response['payload']['related_categories'][0]['id']).to eql(related_category_1.id)
expect(json_response['payload']['parent_category']['id']).to eql(related_category_2.id)
expect(related_category_2.reload.sub_category_ids).to eql([category.id])
end
it 'updates related categories' do
category_params = {
category: {
related_category_ids: [related_category_1.id]
}
}
category.related_categories << related_category_2
category.save!
expect(category.related_category_ids).to eq([related_category_2.id])
put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories/#{category.id}",
params: category_params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload']['name']).to eql(category.name)
expect(json_response['payload']['related_categories'][0]['id']).to eql(related_category_1.id)
expect(category.reload.related_category_ids).to eq([related_category_1.id])
expect(related_category_1.reload.related_category_ids).to be_empty
expect(json_response['payload']['position']).to eql(category.position)
end
# [category_1, category_2] !== [category_2, category_1]
it 'update reverse associations for related categories' do
category.related_categories << related_category_2
category.save!
expect(category.related_category_ids).to eq([related_category_2.id])
category_params = {
category: {
related_category_ids: [category.id]
}
}
put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories/#{related_category_2.id}",
params: category_params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
expect(category.reload.related_category_ids).to eq([related_category_2.id])
expect(related_category_2.reload.related_category_ids).to eq([category.id])
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/portals/{portal.slug}/categories/{category.id}' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories/#{category.id}", params: {}
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'deletes category' do
delete "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories/#{category.id}",
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
deleted_category = Category.find_by(id: category.id)
expect(deleted_category).to be_nil
end
end
end
describe 'GET /api/v1/accounts/{account.id}/portals/{portal.slug}/categories' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'get all categories in portal' do
category_count = Category.all.count
category2 = create(:category, name: 'test_category_2', portal: portal, locale: 'es', slug: 'category_slug_2')
expect(category2.id).not_to be_nil
get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories",
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload'].count).to be(category_count + 1)
end
end
end
end

View File

@@ -0,0 +1,156 @@
require 'rails_helper'
RSpec.describe '/api/v1/accounts/{account.id}/channels/twilio_channel', type: :request do
let(:account) { create(:account) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:twilio_client) { instance_double(Twilio::REST::Client) }
let(:message_double) { double }
let(:twilio_webhook_setup_service) { instance_double(Twilio::WebhookSetupService) }
before do
allow(Twilio::REST::Client).to receive(:new).and_return(twilio_client)
allow(Twilio::WebhookSetupService).to receive(:new).and_return(twilio_webhook_setup_service)
allow(twilio_webhook_setup_service).to receive(:perform)
end
describe 'POST /api/v1/accounts/{account.id}/channels/twilio_channel' do
let(:params) do
{
twilio_channel: {
account_sid: 'sid',
auth_token: 'token',
phone_number: '',
messaging_service_sid: 'MGec8130512b5dd462cfe03095ec1342ed',
name: 'SMS Channel',
medium: 'sms'
}
}
end
context 'when unauthenticated user' do
it 'returns unauthorized' do
post api_v1_account_channels_twilio_channel_path(account), params: params
expect(response).to have_http_status(:unauthorized)
end
end
context 'when user is logged in' do
context 'with user as administrator' do
it 'creates inbox and returns inbox object' do
allow(twilio_client).to receive(:messages).and_return(message_double)
allow(message_double).to receive(:list).and_return([])
post api_v1_account_channels_twilio_channel_path(account),
params: params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['name']).to eq('SMS Channel')
expect(json_response['messaging_service_sid']).to eq('MGec8130512b5dd462cfe03095ec1342ed')
end
it 'creates inbox with blank phone number and returns inbox object' do
params = {
twilio_channel: {
account_sid: 'sid-1',
auth_token: 'token-1',
phone_number: '',
messaging_service_sid: 'MGec8130512b5dd462cfe03095ec1111ed',
name: 'SMS Channel',
medium: 'whatsapp'
}
}
allow(twilio_client).to receive(:messages).and_return(message_double)
allow(message_double).to receive(:list).and_return([])
post api_v1_account_channels_twilio_channel_path(account),
params: params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['messaging_service_sid']).to eq('MGec8130512b5dd462cfe03095ec1111ed')
end
context 'with a phone number' do # rubocop:disable RSpec/NestedGroups
let(:params) do
{
twilio_channel: {
account_sid: 'sid',
auth_token: 'token',
phone_number: '+1234567890',
messaging_service_sid: '',
name: 'SMS Channel',
medium: 'sms'
}
}
end
it 'creates inbox with empty messaging service sid and returns inbox object' do
allow(twilio_client).to receive(:messages).and_return(message_double)
allow(message_double).to receive(:list).and_return([])
post api_v1_account_channels_twilio_channel_path(account),
params: params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['name']).to eq('SMS Channel')
expect(json_response['phone_number']).to eq('+1234567890')
end
it 'creates one more inbox with empty messaging service sid' do
params = {
twilio_channel: {
account_sid: 'sid-1',
auth_token: 'token-1',
phone_number: '+1224466880',
messaging_service_sid: '',
name: 'SMS Channel',
medium: 'whatsapp'
}
}
allow(twilio_client).to receive(:messages).and_return(message_double)
allow(message_double).to receive(:list).and_return([])
post api_v1_account_channels_twilio_channel_path(account),
params: params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['phone_number']).to eq('whatsapp:+1224466880')
end
end
it 'return error if Twilio tokens are incorrect' do
allow(twilio_client).to receive(:messages).and_return(message_double)
allow(message_double).to receive(:list).and_raise(Twilio::REST::TwilioError)
post api_v1_account_channels_twilio_channel_path(account),
params: params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:unprocessable_entity)
end
end
context 'with user as agent' do
it 'returns unauthorized' do
post api_v1_account_channels_twilio_channel_path(account),
params: params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
end
end
end

View File

@@ -0,0 +1,71 @@
require 'rails_helper'
RSpec.describe 'Contact Inboxes API', type: :request do
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:contact) { create(:contact, account: account) }
let!(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: inbox) }
describe 'POST /api/v1/accounts/{account.id}/contact_inboxes/filter' do
let(:admin) { create(:user, account: account, role: :administrator) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/contact_inboxes/filter"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated admin user' do
it 'returns not found if the params are invalid' do
post "/api/v1/accounts/#{account.id}/contact_inboxes/filter",
headers: admin.create_new_auth_token,
params: { inbox_id: inbox.id, source_id: 'random_source_id' },
as: :json
expect(response).to have_http_status(:not_found)
end
it 'returns the contact if the params are valid' do
post "/api/v1/accounts/#{account.id}/contact_inboxes/filter",
headers: admin.create_new_auth_token,
params: { inbox_id: inbox.id, source_id: contact_inbox.source_id },
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body['id']).to eq(contact.id)
expect(response_body['contact_inboxes'].first['source_id']).to eq(contact_inbox.source_id)
end
end
context 'when it is an authenticated agent user' do
let(:agent_with_inbox_access) { create(:user, account: account, role: :agent) }
let(:agent_without_inbox_access) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, user: agent_with_inbox_access, inbox: inbox)
end
it 'returns unauthorized if agent does not have inbox access' do
post "/api/v1/accounts/#{account.id}/contact_inboxes/filter",
headers: agent_without_inbox_access.create_new_auth_token,
params: { inbox_id: inbox.id, source_id: contact_inbox.source_id },
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'returns success if agent have inbox access' do
post "/api/v1/accounts/#{account.id}/contact_inboxes/filter",
headers: agent_with_inbox_access.create_new_auth_token,
params: { inbox_id: inbox.id, source_id: contact_inbox.source_id },
as: :json
expect(response).to have_http_status(:success)
end
end
end
end

View File

@@ -0,0 +1,76 @@
require 'rails_helper'
RSpec.describe '/api/v1/accounts/{account.id}/contacts/:id/contact_inboxes', type: :request do
let(:account) { create(:account) }
let(:contact) { create(:contact, account: account, email: 'f.o.o.b.a.r@gmail.com') }
let(:channel_twilio_sms) { create(:channel_twilio_sms, account: account) }
let(:channel_email) { create(:channel_email, account: account) }
let(:channel_api) { create(:channel_api, account: account) }
let(:agent) { create(:user, account: account) }
describe 'GET /api/v1/accounts/{account.id}/contacts/:id/contact_inboxes' do
context 'when unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/contact_inboxes"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated user with access to inbox' do
it 'creates a contact inbox' do
create(:inbox_member, inbox: channel_api.inbox, user: agent)
expect do
post "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/contact_inboxes",
params: { inbox_id: channel_api.inbox.id },
headers: agent.create_new_auth_token,
as: :json
end.to change(ContactInbox, :count).by(1)
expect(response).to have_http_status(:success)
contact_inbox = contact.reload.contact_inboxes.find_by(inbox_id: channel_api.inbox.id)
expect(contact_inbox).to be_present
expect(contact_inbox.hmac_verified).to be(false)
end
it 'creates a valid email contact inbox' do
create(:inbox_member, inbox: channel_email.inbox, user: agent)
expect do
post "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/contact_inboxes",
params: { inbox_id: channel_email.inbox.id },
headers: agent.create_new_auth_token,
as: :json
end.to change(ContactInbox, :count).by(1)
expect(response).to have_http_status(:success)
expect(contact.reload.contact_inboxes.map(&:inbox_id)).to include(channel_email.inbox.id)
end
it 'creates an hmac verified contact inbox' do
create(:inbox_member, inbox: channel_api.inbox, user: agent)
expect do
post "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/contact_inboxes",
params: { inbox_id: channel_api.inbox.id, hmac_verified: true },
headers: agent.create_new_auth_token,
as: :json
end.to change(ContactInbox, :count).by(1)
expect(response).to have_http_status(:success)
contact_inbox = contact.reload.contact_inboxes.find_by(inbox_id: channel_api.inbox.id)
expect(contact_inbox).to be_present
expect(contact_inbox.hmac_verified).to be(true)
end
it 'throws error for invalid source id' do
create(:inbox_member, inbox: channel_twilio_sms.inbox, user: agent)
expect do
post "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/contact_inboxes",
params: { inbox_id: channel_twilio_sms.inbox.id },
headers: agent.create_new_auth_token,
as: :json
end.not_to change(ContactInbox, :count)
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
end

View File

@@ -0,0 +1,65 @@
require 'rails_helper'
RSpec.describe '/api/v1/accounts/{account.id}/contacts/:id/conversations', type: :request do
let(:account) { create(:account) }
let(:contact) { create(:contact, account: account) }
let(:inbox_1) { create(:inbox, account: account) }
let(:inbox_2) { create(:inbox, account: account) }
let(:contact_inbox_1) { create(:contact_inbox, contact: contact, inbox: inbox_1) }
let(:contact_inbox_2) { create(:contact_inbox, contact: contact, inbox: inbox_2) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:unknown) { create(:user, account: account, role: nil) }
before do
create(:inbox_member, user: agent, inbox: inbox_1)
2.times.each do
create(:conversation, account: account, inbox: inbox_1, contact: contact, contact_inbox: contact_inbox_1)
create(:conversation, account: account, inbox: inbox_2, contact: contact, contact_inbox: contact_inbox_2)
end
end
describe 'GET /api/v1/accounts/{account.id}/contacts/:id/conversations' do
context 'when unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/conversations"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when user is logged in' do
context 'with user as administrator' do
it 'returns conversations from all inboxes' do
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/conversations", headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload'].length).to eq 4
end
end
context 'with user as agent' do
it 'returns conversations from the inboxes which agent has access to' do
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/conversations", headers: agent.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload'].length).to eq 2
end
end
context 'with user as unknown role' do
it 'returns conversations from no inboxes' do
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/conversations", headers: unknown.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload'].length).to eq 0
end
end
end
end
end

View File

@@ -0,0 +1,67 @@
require 'rails_helper'
RSpec.describe 'Contact Label API', type: :request do
let(:account) { create(:account) }
describe 'GET /api/v1/accounts/{account.id}/contacts/<id>/labels' do
let(:contact) { create(:contact, account: account) }
before do
contact.update_labels('label1, label2')
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get api_v1_account_contact_labels_url(account_id: account.id, contact_id: contact.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) }
it 'returns all the labels for the contact' do
get api_v1_account_contact_labels_url(account_id: account.id, contact_id: contact.id),
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include('label1')
expect(response.body).to include('label2')
end
end
end
describe 'POST /api/v1/accounts/{account.id}/contacts/<id>/labels' do
let(:contact) { create(:contact, account: account) }
before do
contact.update_labels('label1, label2')
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post api_v1_account_contact_labels_url(account_id: account.id, contact_id: contact.id),
params: { labels: %w[label3 label4] },
as: :json
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) }
it 'creates labels for the contact' do
post api_v1_account_contact_labels_url(account_id: account.id, contact_id: contact.id),
params: { labels: %w[label3 label4] },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include('label3')
expect(response.body).to include('label4')
end
end
end
end

View File

@@ -0,0 +1,121 @@
require 'rails_helper'
RSpec.describe 'Notes API', type: :request do
let!(:account) { create(:account) }
let!(:contact) { create(:contact, account: account) }
let!(:note) { create(:note, contact: contact) }
let!(:agent) { create(:user, account: account, role: :agent) }
describe 'GET /api/v1/accounts/{account.id}/contacts/{contact.id}/notes' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/notes"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'returns all notes to agents' do
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/notes",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
body = JSON.parse(response.body, symbolize_names: true)
expect(body.first[:content]).to eq(note.content)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/contacts/{contact.id}/notes/{note.id}' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/notes/#{note.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'shows the note for agents' do
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/notes/#{note.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body, symbolize_names: true)[:id]).to eq(note.id)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/contacts/{contact.id}/notes' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/notes",
params: { content: 'test message' },
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'creates a new note' do
post "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/notes",
params: { content: 'test note' },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body, symbolize_names: true)[:content]).to eq('test note')
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/contacts/{contact.id}/notes/:id' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
patch "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/notes/#{note.id}",
params: { content: 'test message' },
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'updates the note' do
patch "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/notes/#{note.id}",
params: { content: 'test message' },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body, symbolize_names: true)[:content]).to eq('test message')
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/notes/:id' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/notes/#{note.id}",
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'delete note if agent' do
delete "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/notes/#{note.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(Note.exists?(note.id)).to be false
end
end
end
end

View File

@@ -0,0 +1,783 @@
require 'rails_helper'
RSpec.describe 'Contacts API', type: :request do
let(:account) { create(:account) }
let(:email_filter) do
{
attribute_key: 'email',
filter_operator: 'contains',
values: 'looped',
query_operator: 'and',
attribute_model: 'standard',
custom_attribute_type: ''
}
end
describe 'GET /api/v1/accounts/{account.id}/contacts' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/contacts"
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!(:contact) { create(:contact, :with_email, account: account, additional_attributes: { company_name: 'Company 1', country_code: 'IN' }) }
let!(:contact_1) do
create(:contact, :with_email, account: account, additional_attributes: { company_name: 'Test Company 1', country_code: 'CA' })
end
let(:contact_2) do
create(:contact, :with_email, account: account, additional_attributes: { company_name: 'Marvel Company', country_code: 'AL' })
end
let(:contact_3) do
create(:contact, :with_email, account: account, additional_attributes: { company_name: nil, country_code: nil })
end
let!(:contact_4) do
create(:contact, :with_email, account: account, additional_attributes: { company_name: nil, country_code: nil })
end
let!(:contact_inbox) { create(:contact_inbox, contact: contact) }
it 'returns all resolved contacts along with contact inboxes' do
get "/api/v1/accounts/#{account.id}/contacts",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
contact_emails = response_body['payload'].pluck('email')
contact_inboxes_source_ids = response_body['payload'].flat_map { |c| c['contact_inboxes'].pluck('source_id') }
expect(contact_emails).to include(contact.email)
expect(contact_inboxes_source_ids).to include(contact_inbox.source_id)
end
it 'returns all contacts without contact inboxes' do
get "/api/v1/accounts/#{account.id}/contacts?include_contact_inboxes=false",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
contact_emails = response_body['payload'].pluck('email')
contact_inboxes = response_body['payload'].pluck('contact_inboxes').flatten.compact
expect(contact_emails).to include(contact.email)
expect(contact_inboxes).to eq([])
end
it 'returns limited information on inboxes' do
get "/api/v1/accounts/#{account.id}/contacts?include_contact_inboxes=true",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
contact_emails = response_body['payload'].pluck('email')
contact_inboxes = response_body['payload'].pluck('contact_inboxes').flatten.compact
expect(contact_emails).to include(contact.email)
first_inbox = contact_inboxes[0]['inbox']
expect(first_inbox).to be_a(Hash)
expect(first_inbox).to include('id', 'channel_id', 'channel_type', 'name', 'avatar_url', 'provider')
expect(first_inbox).not_to include('imap_login',
'imap_password',
'imap_address',
'imap_port',
'imap_enabled',
'imap_enable_ssl')
expect(first_inbox).not_to include('smtp_login',
'smtp_password',
'smtp_address',
'smtp_port',
'smtp_enabled',
'smtp_domain')
expect(first_inbox).not_to include('hmac_token', 'provider_config')
end
it 'returns all contacts with company name desc order' do
get "/api/v1/accounts/#{account.id}/contacts?include_contact_inboxes=false&sort=-company",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body['payload'].last['id']).to eq(contact_4.id)
expect(response_body['payload'].last['email']).to eq(contact_4.email)
end
it 'returns all contacts with company name asc order with null values at last' do
get "/api/v1/accounts/#{account.id}/contacts?include_contact_inboxes=false&sort=-company",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body['payload'].first['email']).to eq(contact_1.email)
expect(response_body['payload'].first['id']).to eq(contact_1.id)
expect(response_body['payload'].last['email']).to eq(contact_4.email)
end
it 'returns all contacts with country name desc order with null values at last' do
contact_from_albania = create(:contact, :with_email, account: account, additional_attributes: { country_code: 'AL', country: 'Albania' })
get "/api/v1/accounts/#{account.id}/contacts?include_contact_inboxes=false&sort=country",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body['payload'].first['email']).to eq(contact_from_albania.email)
expect(response_body['payload'].first['id']).to eq(contact_from_albania.id)
expect(response_body['payload'].last['email']).to eq(contact_4.email)
end
it 'returns last seen at' do
create(:conversation, contact: contact, account: account, inbox: contact_inbox.inbox, contact_last_seen_at: Time.now.utc)
get "/api/v1/accounts/#{account.id}/contacts",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body['payload'].first['last_seen_at']).present?
end
it 'filters resolved contacts based on label filter' do
contact_with_label1, contact_with_label2 = FactoryBot.create_list(:contact, 2, :with_email, account: account)
contact_with_label1.update_labels(['label1'])
contact_with_label2.update_labels(['label2'])
get "/api/v1/accounts/#{account.id}/contacts",
params: { labels: %w[label1 label2] },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body['meta']['count']).to eq(2)
expect(response_body['payload'].pluck('email')).to include(contact_with_label1.email, contact_with_label2.email)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/contacts/import' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/contacts/import"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user with out permission' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/contacts/import",
headers: agent.create_new_auth_token,
as: :json
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 'creates a data import' do
file = fixture_file_upload(Rails.root.join('spec/assets/contacts.csv'), 'text/csv')
post "/api/v1/accounts/#{account.id}/contacts/import",
headers: admin.create_new_auth_token,
params: { import_file: file }
expect(response).to have_http_status(:success)
expect(account.data_imports.count).to eq(1)
expect(account.data_imports.first.import_file.attached?).to be(true)
end
end
context 'when file is empty' do
let(:admin) { create(:user, account: account, role: :administrator) }
it 'returns Unprocessable Entity' do
post "/api/v1/accounts/#{account.id}/contacts/import",
headers: admin.create_new_auth_token
json_response = response.parsed_body
expect(response).to have_http_status(:unprocessable_entity)
expect(json_response['error']).to eq('File is blank')
end
end
end
describe 'POST /api/v1/accounts/{account.id}/contacts/export' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/contacts/export"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user with out permission' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/contacts/export",
headers: agent.create_new_auth_token,
as: :json
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 'enqueues a contact export job' do
expect(Account::ContactsExportJob).to receive(:perform_later).with(account.id, admin.id, nil, { :payload => nil, :label => nil }).once
post "/api/v1/accounts/#{account.id}/contacts/export",
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
end
it 'enqueues a contact export job with sent_columns' do
expect(Account::ContactsExportJob).to receive(:perform_later).with(account.id, admin.id, %w[phone_number email],
{ :payload => nil, :label => nil }).once
post "/api/v1/accounts/#{account.id}/contacts/export",
headers: admin.create_new_auth_token,
params: { column_names: %w[phone_number email] }
expect(response).to have_http_status(:success)
end
it 'enqueues a contact export job with payload' do
expect(Account::ContactsExportJob).to receive(:perform_later).with(account.id, admin.id, nil,
{
:payload => [ActionController::Parameters.new(email_filter).permit!],
:label => nil
}).once
post "/api/v1/accounts/#{account.id}/contacts/export",
headers: admin.create_new_auth_token,
params: { payload: [email_filter] }
expect(response).to have_http_status(:success)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/contacts/active' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/contacts/active"
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!(:contact) { create(:contact, account: account) }
it 'returns no contacts if no are online' do
get "/api/v1/accounts/#{account.id}/contacts/active",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).not_to include(contact.name)
end
it 'returns all contacts who are online' do
allow(OnlineStatusTracker).to receive(:get_available_contact_ids).and_return([contact.id])
get "/api/v1/accounts/#{account.id}/contacts/active",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(contact.name)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/contacts/search' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/contacts/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) }
let!(:contact1) { create(:contact, :with_email, account: account) }
let!(:contact2) { create(:contact, :with_email, name: 'testcontact', account: account, email: 'test@test.com') }
it 'returns all resolved contacts with contact inboxes' do
get "/api/v1/accounts/#{account.id}/contacts/search",
params: { q: contact2.email },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(contact2.email)
expect(response.body).not_to include(contact1.email)
end
it 'matches the contact ignoring the case in email' do
get "/api/v1/accounts/#{account.id}/contacts/search",
params: { q: 'Test@Test.com' },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(contact2.email)
expect(response.body).not_to include(contact1.email)
end
it 'matches the contact ignoring the case in name' do
get "/api/v1/accounts/#{account.id}/contacts/search",
params: { q: 'TestContact' },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(contact2.email)
expect(response.body).not_to include(contact1.email)
end
it 'searches contacts using company name' do
contact2.update(additional_attributes: { company_name: 'acme.inc' })
get "/api/v1/accounts/#{account.id}/contacts/search",
params: { q: 'acme.inc' },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(contact2.email)
expect(response.body).not_to include(contact1.email)
end
it 'matches the resolved contact respecting the identifier character casing' do
contact_normal = create(:contact, name: 'testcontact', account: account, identifier: 'testidentifer')
contact_special = create(:contact, name: 'testcontact', account: account, identifier: 'TestIdentifier')
get "/api/v1/accounts/#{account.id}/contacts/search",
params: { q: 'TestIdentifier' },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(contact_special.identifier)
expect(response.body).not_to include(contact_normal.identifier)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/contacts/filter' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/contacts/filter"
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!(:contact1) { create(:contact, :with_email, account: account, additional_attributes: { country_code: 'US' }) }
let!(:contact2) do
create(:contact, :with_email, name: 'testcontact', account: account, email: 'test@test.com', additional_attributes: { country_code: 'US' })
end
it 'returns all contacts when query is empty' do
post "/api/v1/accounts/#{account.id}/contacts/filter",
params: { payload: [
attribute_key: 'country_code',
filter_operator: 'equal_to',
values: ['US']
] },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(contact2.email)
expect(response.body).to include(contact1.email)
end
it 'returns error the query operator is invalid' do
post "/api/v1/accounts/#{account.id}/contacts/filter",
params: { payload: [
attribute_key: 'country_code',
filter_operator: 'eq',
values: ['US']
] },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).to include('Invalid operator. The allowed operators for country_code are [equal_to,not_equal_to]')
end
it 'returns error the query value is invalid' do
post "/api/v1/accounts/#{account.id}/contacts/filter",
params: { payload: [
attribute_key: 'country_code',
filter_operator: 'equal_to',
values: []
] },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).to include('Invalid value. The values provided for country_code are invalid"')
end
end
end
describe 'GET /api/v1/accounts/{account.id}/contacts/:id' do
let!(:contact) { create(:contact, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/contacts/#{contact.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) }
it 'shows the contact' do
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(contact.name)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/contacts/:id/contactable_inboxes' do
let!(:contact) { create(:contact, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/contactable_inboxes"
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!(:twilio_sms) { create(:channel_twilio_sms, account: account) }
let!(:twilio_sms_inbox) { create(:inbox, channel: twilio_sms, account: account) }
let!(:twilio_whatsapp) { create(:channel_twilio_sms, medium: :whatsapp, account: account) }
let!(:twilio_whatsapp_inbox) { create(:inbox, channel: twilio_whatsapp, account: account) }
it 'shows the contactable inboxes which the user has access to' do
create(:inbox_member, user: agent, inbox: twilio_whatsapp_inbox)
inbox_service = double
allow(Contacts::ContactableInboxesService).to receive(:new).and_return(inbox_service)
allow(inbox_service).to receive(:get).and_return([
{ source_id: '1123', inbox: twilio_sms_inbox },
{ source_id: '1123', inbox: twilio_whatsapp_inbox }
])
expect(inbox_service).to receive(:get)
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/contactable_inboxes",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
# only the inboxes which agent has access to are shown
expect(response.parsed_body['payload'].pluck('inbox').pluck('id')).to eq([twilio_whatsapp_inbox.id])
end
end
end
describe 'POST /api/v1/accounts/{account.id}/contacts' do
let(:custom_attributes) { { test: 'test', test1: 'test1' } }
let(:valid_params) { { name: 'test', custom_attributes: custom_attributes } }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
expect { post "/api/v1/accounts/#{account.id}/contacts", params: valid_params }.not_to change(Contact, :count)
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(:inbox) { create(:inbox, account: account) }
it 'creates the contact' do
expect do
post "/api/v1/accounts/#{account.id}/contacts", headers: admin.create_new_auth_token,
params: valid_params
end.to change(Contact, :count).by(1)
expect(response).to have_http_status(:success)
# custom attributes are updated
json_response = response.parsed_body
expect(json_response['payload']['contact']['custom_attributes']).to eq({ 'test' => 'test', 'test1' => 'test1' })
end
it 'does not create the contact' do
valid_params[:name] = 'test' * 999
post "/api/v1/accounts/#{account.id}/contacts", headers: admin.create_new_auth_token,
params: valid_params
expect(response).to have_http_status(:unprocessable_entity)
json_response = response.parsed_body
expect(json_response['message']).to eq('Name is too long (maximum is 255 characters)')
end
it 'creates the contact inbox when inbox id is passed' do
expect do
post "/api/v1/accounts/#{account.id}/contacts", headers: admin.create_new_auth_token,
params: valid_params.merge({ inbox_id: inbox.id })
end.to change(ContactInbox, :count).by(1)
expect(response).to have_http_status(:success)
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/contacts/:id' do
let(:custom_attributes) { { test: 'test', test1: 'test1' } }
let(:additional_attributes) { { attr1: 'attr1', attr2: 'attr2' } }
let!(:contact) { create(:contact, account: account, custom_attributes: custom_attributes, additional_attributes: additional_attributes) }
let(:valid_params) do
{ name: 'Test Blub', custom_attributes: { test: 'new test', test2: 'test2' }, additional_attributes: { attr2: 'new attr2', attr3: 'attr3' } }
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
params: valid_params
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 'updates the contact' do
patch "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
headers: admin.create_new_auth_token,
params: valid_params,
as: :json
expect(response).to have_http_status(:success)
expect(contact.reload.name).to eq('Test Blub')
# custom attributes are merged properly without overwriting existing ones
expect(contact.custom_attributes).to eq({ 'test' => 'new test', 'test1' => 'test1', 'test2' => 'test2' })
expect(contact.additional_attributes).to eq({ 'attr1' => 'attr1', 'attr2' => 'new attr2', 'attr3' => 'attr3' })
end
it 'prevents the update of contact of another account' do
other_account = create(:account)
other_contact = create(:contact, account: other_account)
patch "/api/v1/accounts/#{account.id}/contacts/#{other_contact.id}",
headers: admin.create_new_auth_token,
params: valid_params,
as: :json
expect(response).to have_http_status(:not_found)
end
it 'prevents updating with an existing email' do
other_contact = create(:contact, account: account, email: 'test1@example.com')
patch "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
headers: admin.create_new_auth_token,
params: valid_params.merge({ email: other_contact.email }),
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['attributes']).to include('email')
end
it 'prevents updating with an existing phone number' do
other_contact = create(:contact, account: account, phone_number: '+12000000')
patch "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
headers: admin.create_new_auth_token,
params: valid_params.merge({ phone_number: other_contact.phone_number }),
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['attributes']).to include('phone_number')
end
it 'updates avatar' do
# no avatar before upload
expect(contact.avatar.attached?).to be(false)
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
patch "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
params: valid_params.merge(avatar: file),
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
contact.reload
expect(contact.avatar.attached?).to be(true)
end
it 'updated avatar with avatar_url' do
patch "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
params: valid_params.merge(avatar_url: 'http://example.com/avatar.png'),
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
expect(Avatar::AvatarFromUrlJob).to have_been_enqueued.with(contact, 'http://example.com/avatar.png')
end
it 'allows blocking of contact' do
patch "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
params: { blocked: true },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(contact.reload.blocked).to be(true)
end
it 'allows unblocking of contact' do
contact.update(blocked: true)
patch "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
params: { blocked: false },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(contact.reload.blocked).to be(false)
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/contacts/:id', :contact_delete do
let(:inbox) { create(:inbox, account: account) }
let(:contact) { create(:contact, account: account) }
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: inbox) }
let(:conversation) { create(:conversation, account: account, inbox: inbox, contact: contact, contact_inbox: contact_inbox) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/contacts/#{contact.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(:agent) { create(:user, account: account, role: :agent) }
it 'deletes the contact for administrator user' do
allow(OnlineStatusTracker).to receive(:get_presence).and_return(false)
delete "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
headers: admin.create_new_auth_token
expect(contact.conversations).to be_empty
expect(contact.inboxes).to be_empty
expect(contact.contact_inboxes).to be_empty
expect(contact.csat_survey_responses).to be_empty
expect { contact.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect(response).to have_http_status(:success)
end
it 'does not delete the contact if online' do
allow(OnlineStatusTracker).to receive(:get_presence).and_return(true)
delete "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
headers: admin.create_new_auth_token
expect(response).to have_http_status(:unprocessable_entity)
end
it 'returns unauthorized for agent user' do
delete "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/contacts/:id/destroy_custom_attributes' do
let(:custom_attributes) { { test: 'test', test1: 'test1' } }
let!(:contact) { create(:contact, account: account, custom_attributes: custom_attributes) }
let(:valid_params) { { custom_attributes: ['test'] } }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/destroy_custom_attributes",
params: valid_params
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 'delete the custom attribute' do
post "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/destroy_custom_attributes",
headers: admin.create_new_auth_token,
params: valid_params,
as: :json
expect(response).to have_http_status(:success)
expect(contact.reload.custom_attributes).to eq({ 'test1' => 'test1' })
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/contacts/:id/avatar' do
let(:contact) { create(:contact, account: account) }
let(:agent) { create(:user, account: account, role: :agent) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/avatar"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
before do
create(:contact, account: account)
contact.avatar.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
end
it 'delete contact avatar' do
delete "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/avatar",
headers: agent.create_new_auth_token,
as: :json
expect { contact.avatar.attachment.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect(response).to have_http_status(:success)
end
end
end
end

View File

@@ -0,0 +1,184 @@
require 'rails_helper'
RSpec.describe 'Conversation Assignment API', type: :request do
let(:account) { create(:account) }
describe 'POST /api/v1/accounts/{account.id}/conversations/<id>/assignments' do
let(:conversation) { create(:conversation, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post api_v1_account_conversation_assignments_url(account_id: account.id, conversation_id: conversation.display_id)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated bot with out access to the inbox' do
let(:agent_bot) { create(:agent_bot) }
let(:agent) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: conversation.inbox, user: agent)
end
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/assignments",
headers: { api_access_token: agent_bot.access_token.token },
params: {
assignee_id: agent.id
},
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user with access to the inbox' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:agent_bot) { create(:agent_bot, account: account) }
let(:team) { create(:team, account: account) }
before do
create(:inbox_member, inbox: conversation.inbox, user: agent)
end
it 'assigns a user to the conversation' do
params = { assignee_id: agent.id }
post api_v1_account_conversation_assignments_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.assignee).to eq(agent)
end
it 'assigns an agent bot to the conversation' do
params = { assignee_id: agent_bot.id, assignee_type: 'AgentBot' }
expect(Conversations::AssignmentService).to receive(:new)
.with(hash_including(conversation: conversation, assignee_id: agent_bot.id, assignee_type: 'AgentBot'))
.and_call_original
post api_v1_account_conversation_assignments_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['name']).to eq(agent_bot.name)
conversation.reload
expect(conversation.assignee_agent_bot).to eq(agent_bot)
expect(conversation.assignee).to be_nil
end
it 'assigns a team to the conversation' do
team_member = create(:user, account: account, role: :agent, auto_offline: false)
create(:inbox_member, inbox: conversation.inbox, user: team_member)
create(:team_member, team: team, user: team_member)
params = { team_id: team.id }
post api_v1_account_conversation_assignments_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.team).to eq(team)
# assignee will be from team
expect(conversation.reload.assignee).to eq(team_member)
end
end
context 'when it is an authenticated bot with access to the inbox' do
let(:agent_bot) { create(:agent_bot, account: account) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:team) { create(:team, account: account) }
before do
create(:agent_bot_inbox, inbox: conversation.inbox, agent_bot: agent_bot)
end
it 'assignment of an agent in the conversation by bot agent' do
create(:inbox_member, user: agent, inbox: conversation.inbox)
conversation.update!(assignee_id: nil)
expect(conversation.reload.assignee).to be_nil
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/assignments",
headers: { api_access_token: agent_bot.access_token.token },
params: {
assignee_id: agent.id
},
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.assignee).to eq(agent)
end
it 'assignment of an team in the conversation by bot agent' do
create(:inbox_member, user: agent, inbox: conversation.inbox)
conversation.update!(team_id: nil)
expect(conversation.reload.team).to be_nil
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/assignments",
headers: { api_access_token: agent_bot.access_token.token },
params: {
team_id: team.id
},
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.team).to eq(team)
end
end
context 'when conversation already has an assignee' do
let(:agent) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: conversation.inbox, user: agent)
conversation.update!(assignee: agent)
end
it 'unassigns the assignee from the conversation' do
params = { assignee_id: nil }
post api_v1_account_conversation_assignments_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.assignee).to be_nil
expect(Conversations::ActivityMessageJob)
.to(have_been_enqueued.at_least(:once)
.with(conversation, { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity,
content: "Conversation unassigned by #{agent.name}" }))
end
end
context 'when conversation already has a team' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:team) { create(:team, account: account) }
before do
conversation.update!(team: team)
create(:inbox_member, inbox: conversation.inbox, user: agent)
end
it 'unassigns the team from the conversation' do
params = { team_id: 0 }
post api_v1_account_conversation_assignments_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.team).to be_nil
end
end
end
end

View File

@@ -0,0 +1,34 @@
require 'rails_helper'
RSpec.describe '/api/v1/accounts/:account_id/conversations/:conversation_id/direct_uploads', type: :request do
let(:account) { create(:account) }
let(:web_widget) { create(:channel_widget, account: account) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:contact) { create(:contact, account: account, email: nil) }
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: web_widget.inbox) }
let(:conversation) { create(:conversation, contact: contact, account: account, inbox: web_widget.inbox, contact_inbox: contact_inbox) }
describe 'POST /api/v1/accounts/:account_id/conversations/:conversation_id/direct_uploads' do
context 'when post request is made' do
it 'creates attachment message in conversation' do
contact
post api_v1_account_conversation_direct_uploads_path(account_id: account.id, conversation_id: conversation.display_id),
params: {
blob: {
filename: 'avatar.png',
byte_size: '1234',
checksum: 'dsjbsdhbfif3874823mnsdbf',
content_type: 'image/png'
}
},
headers: { api_access_token: agent.access_token.token },
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['content_type']).to eq('image/png')
end
end
end
end

View File

@@ -0,0 +1,62 @@
require 'rails_helper'
RSpec.describe 'Conversation Draft Messages API', type: :request do
let(:account) { create(:account) }
describe 'POST /api/v1/accounts/{account.id}/conversations/<id>/draft_messages' do
let(:conversation) { create(:conversation, account: account) }
let(:cache_key) { format(Redis::Alfred::CONVERSATION_DRAFT_MESSAGE, id: conversation.id) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get api_v1_account_conversation_draft_messages_url(account_id: account.id, conversation_id: conversation.display_id)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user with access to the inbox' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:message) { Faker::Lorem.paragraph }
before do
create(:inbox_member, inbox: conversation.inbox, user: agent)
end
it 'saves the draft message for the conversation' do
params = { draft_message: { message: message } }
patch api_v1_account_conversation_draft_messages_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(Redis::Alfred.get(cache_key)).to eq(params[:draft_message][:message])
end
it 'gets the draft message for the conversation' do
Redis::Alfred.set(cache_key, message)
get api_v1_account_conversation_draft_messages_url(account_id: account.id, conversation_id: conversation.display_id),
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(message)
end
it 'removes the draft messages for the conversation' do
Redis::Alfred.set(cache_key, message)
expect(Redis::Alfred.get(cache_key)).to eq(message)
delete api_v1_account_conversation_draft_messages_url(account_id: account.id, conversation_id: conversation.display_id),
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(Redis::Alfred.get(cache_key)).to be_nil
end
end
end
end

View File

@@ -0,0 +1,76 @@
require 'rails_helper'
RSpec.describe 'Conversation Label API', type: :request do
let(:account) { create(:account) }
describe 'GET /api/v1/accounts/{account.id}/conversations/<id>/labels' do
let(:conversation) { create(:conversation, account: account) }
before do
conversation.update_labels('label1, label2')
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get api_v1_account_conversation_labels_url(account_id: account.id, conversation_id: conversation.display_id)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user with access to the conversation' do
let(:agent) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: conversation.inbox, user: agent)
end
it 'returns all the labels for the conversation' do
get api_v1_account_conversation_labels_url(account_id: account.id, conversation_id: conversation.display_id),
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include('label1')
expect(response.body).to include('label2')
end
end
end
describe 'POST /api/v1/accounts/{account.id}/conversations/<id>/labels' do
let(:conversation) { create(:conversation, account: account) }
before do
conversation.update_labels('label1, label2')
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post api_v1_account_conversation_labels_url(account_id: account.id, conversation_id: conversation.display_id),
params: { labels: %w[label3 label4] },
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user with access to the conversation' do
let(:agent) { create(:user, account: account, role: :agent) }
before do
conversation.update_labels('label1, label2')
create(:inbox_member, inbox: conversation.inbox, user: agent)
end
it 'creates labels for the conversation' do
post api_v1_account_conversation_labels_url(account_id: account.id, conversation_id: conversation.display_id),
params: { labels: %w[label3 label4] },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include('label3')
expect(response.body).to include('label4')
end
end
end
end

View File

@@ -0,0 +1,359 @@
require 'rails_helper'
RSpec.describe 'Conversation Messages API', type: :request do
let!(:account) { create(:account) }
describe 'POST /api/v1/accounts/{account.id}/conversations/<id>/messages' do
let!(:inbox) { create(:inbox, account: account) }
let!(:conversation) { create(:conversation, inbox: inbox, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user with access to conversation' do
let(:agent) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: conversation.inbox, user: agent)
end
it 'creates a new outgoing message' do
params = { content: 'test-message', private: true }
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(conversation.messages.count).to eq(1)
expect(conversation.messages.first.content).to eq(params[:content])
end
it 'does not create the message' do
params = { content: "#{'h' * 150 * 1000}a", private: true }
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
json_response = response.parsed_body
expect(json_response['error']).to eq('Validation failed: Content is too long (maximum is 150000 characters)')
end
it 'creates an outgoing text message with a specific bot sender' do
agent_bot = create(:agent_bot)
time_stamp = Time.now.utc.to_s
params = { content: 'test-message', external_created_at: time_stamp, sender_type: 'AgentBot', sender_id: agent_bot.id }
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data['content_attributes']['external_created_at']).to eq time_stamp
expect(conversation.messages.count).to eq(1)
expect(conversation.messages.last.sender_id).to eq(agent_bot.id)
expect(conversation.messages.last.content_type).to eq('text')
end
it 'creates a new outgoing message with attachment' do
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
params = { content: 'test-message', attachments: [file] }
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:success)
expect(conversation.messages.last.attachments.first.file.present?).to be(true)
expect(conversation.messages.last.attachments.first.file_type).to eq('image')
end
context 'when api inbox' do
let(:api_channel) { create(:channel_api, account: account) }
let(:api_inbox) { create(:inbox, channel: api_channel, account: account) }
let(:conversation) { create(:conversation, inbox: api_inbox, account: account) }
it 'reopens the conversation with new incoming message' do
create(:message, conversation: conversation, account: account)
conversation.resolved!
params = { content: 'test-message', private: false, message_type: 'incoming' }
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.status).to eq('open')
expect(Conversations::ActivityMessageJob)
.to(have_been_enqueued.at_least(:once)
.with(conversation, { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity,
content: 'System reopened the conversation due to a new incoming message.' }))
end
end
end
context 'when it is an authenticated agent bot' do
let!(:agent_bot) { create(:agent_bot) }
it 'creates a new outgoing message' do
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
params = { content: 'test-message' }
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: { api_access_token: agent_bot.access_token.token },
as: :json
expect(response).to have_http_status(:success)
expect(conversation.messages.count).to eq(1)
expect(conversation.messages.first.content).to eq(params[:content])
end
it 'creates a new outgoing input select message' do
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
select_item1 = build(:bot_message_select)
select_item2 = build(:bot_message_select)
params = { content_type: 'input_select', content_attributes: { items: [select_item1, select_item2] } }
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: { api_access_token: agent_bot.access_token.token },
as: :json
expect(response).to have_http_status(:success)
expect(conversation.messages.count).to eq(1)
expect(conversation.messages.first.content_type).to eq(params[:content_type])
expect(conversation.messages.first.content).to be_nil
end
it 'creates a new outgoing cards message' do
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
card = build(:bot_message_card)
params = { content_type: 'cards', content_attributes: { items: [card] } }
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: { api_access_token: agent_bot.access_token.token },
as: :json
expect(response).to have_http_status(:success)
expect(conversation.messages.count).to eq(1)
expect(conversation.messages.first.content_type).to eq(params[:content_type])
end
end
end
describe 'GET /api/v1/accounts/{account.id}/conversations/:id/messages' do
let(:conversation) { create(:conversation, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/messages"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user with access to conversation' do
let(:agent) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: conversation.inbox, user: agent)
end
it 'shows the conversation' do
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/messages",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body, symbolize_names: true)[:meta][:contact][:id]).to eq(conversation.contact_id)
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/conversations/:conversation_id/messages/:id' do
let(:message) { create(:message, account: account, content_attributes: { bcc_emails: ['hello@chatwoot.com'] }) }
let(:conversation) { message.conversation }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/messages/#{message.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user with access to conversation' do
let(:agent) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: conversation.inbox, user: agent)
end
it 'deletes the message' do
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/messages/#{message.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(message.reload.content).to eq 'This message was deleted'
expect(message.reload.deleted).to be true
expect(message.reload.content_attributes['bcc_emails']).to be_nil
end
it 'deletes interactive messages' do
interactive_message = create(
:message, message_type: :outgoing, content: 'test', content_type: 'input_select',
content_attributes: { 'items' => [{ 'title' => 'test', 'value' => 'test' }] },
conversation: conversation
)
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/messages/#{interactive_message.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(interactive_message.reload.deleted).to be true
end
end
context 'when the message id is invalid' do
let(:agent) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: conversation.inbox, user: agent)
end
it 'returns not found error' do
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/messages/99999",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:not_found)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/conversations/:conversation_id/messages/:id/retry' do
let(:message) { create(:message, account: account, status: :failed, content_attributes: { external_error: 'error' }) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/conversations/#{message.conversation.display_id}/messages/#{message.id}/retry"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user with access to conversation' do
let(:agent) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: message.conversation.inbox, user: agent)
end
it 'retries the message' do
post "/api/v1/accounts/#{account.id}/conversations/#{message.conversation.display_id}/messages/#{message.id}/retry",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(message.reload.status).to eq('sent')
expect(message.reload.content_attributes['external_error']).to be_nil
end
end
context 'when the message id is invalid' do
let(:agent) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: message.conversation.inbox, user: agent)
end
it 'returns not found error' do
post "/api/v1/accounts/#{account.id}/conversations/#{message.conversation.display_id}/messages/99999/retry",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/conversations/:conversation_id/messages/:id' do
let(:api_channel) { create(:channel_api, account: account) }
let(:api_inbox) { create(:inbox, channel: api_channel, account: account) }
let(:agent) { create(:user, account: account, role: :agent) }
let!(:conversation) { create(:conversation, inbox: api_inbox, account: account) }
let!(:message) { create(:message, conversation: conversation, account: account, status: :sent) }
context 'when unauthenticated' do
it 'returns unauthorized' do
patch api_v1_account_conversation_message_url(account_id: account.id, conversation_id: conversation.display_id, id: message.id)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated agent' do
context 'when agent has non-API inbox' do
let(:inbox) { create(:inbox, account: account) }
let(:agent) { create(:user, account: account, role: :agent) }
let!(:conversation) { create(:conversation, inbox: inbox, account: account) }
before { create(:inbox_member, inbox: inbox, user: agent) }
it 'returns forbidden' do
patch api_v1_account_conversation_message_url(
account_id: account.id,
conversation_id: conversation.display_id,
id: message.id
), params: { status: 'failed', external_error: 'err' }, headers: agent.create_new_auth_token, as: :json
expect(response).to have_http_status(:forbidden)
end
end
context 'when agent has API inbox' do
before { create(:inbox_member, inbox: api_inbox, user: agent) }
it 'uses StatusUpdateService to perform status update' do
service = instance_double(Messages::StatusUpdateService)
expect(Messages::StatusUpdateService).to receive(:new)
.with(message, 'failed', 'err123')
.and_return(service)
expect(service).to receive(:perform)
patch api_v1_account_conversation_message_url(
account_id: account.id,
conversation_id: conversation.display_id,
id: message.id
), params: { status: 'failed', external_error: 'err123' }, headers: agent.create_new_auth_token, as: :json
end
it 'updates status to failed with external_error' do
patch api_v1_account_conversation_message_url(
account_id: account.id,
conversation_id: conversation.display_id,
id: message.id
), params: { status: 'failed', external_error: 'err123' }, headers: agent.create_new_auth_token, as: :json
expect(response).to have_http_status(:success)
expect(message.reload.status).to eq('failed')
expect(message.reload.external_error).to eq('err123')
end
end
end
end
end

View File

@@ -0,0 +1,142 @@
require 'rails_helper'
RSpec.describe 'Conversation Participants API', type: :request do
let(:account) { create(:account) }
let(:conversation) { create(:conversation, account: account) }
let(:agent) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: conversation.inbox, user: agent)
end
describe 'GET /api/v1/accounts/{account.id}/conversations/<id>/paricipants' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get api_v1_account_conversation_participants_url(account_id: account.id, conversation_id: conversation.display_id)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user with access to the conversation' do
let(:participant1) { create(:user, account: account, role: :agent) }
let(:participant2) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: conversation.inbox, user: participant1)
create(:inbox_member, inbox: conversation.inbox, user: participant2)
end
it 'returns all the partipants for the conversation' do
create(:conversation_participant, conversation: conversation, user: participant1)
create(:conversation_participant, conversation: conversation, user: participant2)
get api_v1_account_conversation_participants_url(account_id: account.id, conversation_id: conversation.display_id),
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(participant1.email)
expect(response.body).to include(participant2.email)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/conversations/<id>/participants' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post api_v1_account_conversation_participants_url(account_id: account.id, conversation_id: conversation.display_id)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:participant) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: conversation.inbox, user: participant)
end
it 'creates a new participants when its authorized agent' do
params = { user_ids: [participant.id] }
expect(conversation.conversation_participants.count).to eq(0)
post api_v1_account_conversation_participants_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(participant.email)
expect(conversation.conversation_participants.count).to eq(1)
end
end
end
describe 'PUT /api/v1/accounts/{account.id}/conversations/<id>/participants' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put api_v1_account_conversation_participants_url(account_id: account.id, conversation_id: conversation.display_id)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:participant) { create(:user, account: account, role: :agent) }
let(:participant_to_be_added) { create(:user, account: account, role: :agent) }
let(:participant_to_be_removed) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: conversation.inbox, user: participant)
create(:inbox_member, inbox: conversation.inbox, user: participant_to_be_added)
create(:inbox_member, inbox: conversation.inbox, user: participant_to_be_removed)
end
it 'updates participants when its authorized agent' do
params = { user_ids: [participant.id, participant_to_be_added.id] }
create(:conversation_participant, conversation: conversation, user: participant)
create(:conversation_participant, conversation: conversation, user: participant_to_be_removed)
expect(conversation.conversation_participants.count).to eq(2)
put api_v1_account_conversation_participants_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(participant.email)
expect(response.body).to include(participant_to_be_added.email)
expect(conversation.conversation_participants.count).to eq(2)
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/conversations/<id>/participants' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete api_v1_account_conversation_participants_url(account_id: account.id, conversation_id: conversation.display_id)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:participant) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: conversation.inbox, user: participant)
end
it 'deletes participants when its authorized agent' do
params = { user_ids: [participant.id] }
create(:conversation_participant, conversation: conversation, user: participant)
expect(conversation.conversation_participants.count).to eq(1)
delete api_v1_account_conversation_participants_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(conversation.conversation_participants.count).to eq(0)
end
end
end
end

View File

@@ -0,0 +1,988 @@
require 'rails_helper'
RSpec.describe 'Conversations API', type: :request do
let(:account) { create(:account) }
describe 'GET /api/v1/accounts/{account.id}/conversations' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/conversations"
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(:conversation) { create(:conversation, account: account) }
before do
create(:inbox_member, user: agent, inbox: conversation.inbox)
end
it 'returns all conversations with messages' do
message = create(:message, conversation: conversation, account: account)
get "/api/v1/accounts/#{account.id}/conversations",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
body = JSON.parse(response.body, symbolize_names: true)
expect(body[:data][:meta][:all_count]).to eq(1)
expect(body[:data][:meta].keys).to include(:all_count, :mine_count, :assigned_count, :unassigned_count)
expect(body[:data][:payload].first[:uuid]).to eq(conversation.uuid)
expect(body[:data][:payload].first[:messages].first[:id]).to eq(message.id)
end
it 'returns conversations with empty messages array for conversations with out messages' do
get "/api/v1/accounts/#{account.id}/conversations",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
body = JSON.parse(response.body, symbolize_names: true)
expect(body[:data][:meta][:all_count]).to eq(1)
expect(body[:data][:payload].first[:messages]).to eq([])
end
it 'returns unattended conversations' do
attended_conversation = create(:conversation, account: account, first_reply_created_at: Time.now.utc)
# to ensure that waiting since value is populated
create(:message, message_type: :outgoing, conversation: attended_conversation, account: account)
unattended_conversation_no_first_reply = create(:conversation, account: account, first_reply_created_at: nil)
unattended_conversation_waiting_since = create(:conversation, account: account, first_reply_created_at: Time.now.utc)
agent_1 = create(:user, account: account, role: :agent)
create(:inbox_member, user: agent_1, inbox: attended_conversation.inbox)
create(:inbox_member, user: agent_1, inbox: unattended_conversation_no_first_reply.inbox)
create(:inbox_member, user: agent_1, inbox: unattended_conversation_waiting_since.inbox)
get "/api/v1/accounts/#{account.id}/conversations",
headers: agent_1.create_new_auth_token,
params: { conversation_type: 'unattended' },
as: :json
expect(response).to have_http_status(:success)
body = JSON.parse(response.body, symbolize_names: true)
expect(body[:data][:meta][:all_count]).to eq(2)
expect(body[:data][:payload].count).to eq(2)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/conversations/meta' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/conversations/meta"
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) }
before do
conversation = create(:conversation, account: account)
create(:inbox_member, user: agent, inbox: conversation.inbox)
end
it 'returns all conversations counts' do
get "/api/v1/accounts/#{account.id}/conversations/meta",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
body = JSON.parse(response.body, symbolize_names: true)
expect(body[:meta].keys).to include(:all_count, :mine_count, :assigned_count, :unassigned_count)
expect(body[:meta][:all_count]).to eq(1)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/conversations/search' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/conversations/search", params: { q: 'test' }
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) }
before do
conversation = create(:conversation, account: account)
create(:message, conversation: conversation, account: account, content: 'test1')
create(:message, conversation: conversation, account: account, content: 'test2')
create(:inbox_member, user: agent, inbox: conversation.inbox)
end
it 'returns all conversations with messages containing the search query' do
get "/api/v1/accounts/#{account.id}/conversations/search",
headers: agent.create_new_auth_token,
params: { q: 'test1' },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data[:meta][:all_count]).to eq(1)
expect(response_data[:payload].first[:messages].first[:content]).to eq 'test1'
end
end
end
describe 'GET /api/v1/accounts/{account.id}/conversations/filter' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/conversations/filter", params: { q: 'test' }
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) }
before do
conversation = create(:conversation, account: account)
create(:message, conversation: conversation, account: account, content: 'test1')
create(:message, conversation: conversation, account: account, content: 'test2')
create(:inbox_member, user: agent, inbox: conversation.inbox)
end
it 'returns all conversations matching the query' do
post "/api/v1/accounts/#{account.id}/conversations/filter",
headers: agent.create_new_auth_token,
params: {
payload: [{
attribute_key: 'status',
filter_operator: 'equal_to',
values: ['open']
}]
},
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data.count).to eq(2)
end
it 'returns error if the filters contain invalid attributes' do
post "/api/v1/accounts/#{account.id}/conversations/filter",
headers: agent.create_new_auth_token,
params: {
payload: [{
attribute_key: 'phone_number',
filter_operator: 'equal_to',
values: ['open']
}]
},
as: :json
expect(response).to have_http_status(:unprocessable_entity)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data[:error]).to include('Invalid attribute key - [phone_number]')
end
it 'returns error if the filters contain invalid operator' do
post "/api/v1/accounts/#{account.id}/conversations/filter",
headers: agent.create_new_auth_token,
params: {
payload: [{
attribute_key: 'status',
filter_operator: 'eq',
values: ['open']
}]
},
as: :json
expect(response).to have_http_status(:unprocessable_entity)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data[:error]).to eq('Invalid operator. The allowed operators for status are [equal_to,not_equal_to].')
end
end
end
describe 'GET /api/v1/accounts/{account.id}/conversations/:id' do
let(:conversation) { create(:conversation, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_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 'does not shows the conversation if you do not have access to it' do
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'shows the conversation if you are an administrator' do
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body, symbolize_names: true)[:id]).to eq(conversation.display_id)
end
it 'shows the conversation if you are an agent with access to inbox' do
create(:inbox_member, user: agent, inbox: conversation.inbox)
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body, symbolize_names: true)[:id]).to eq(conversation.display_id)
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/conversations/:id' do
let(:conversation) { create(:conversation, account: account) }
let(:params) { { priority: 'high' } }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
patch "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}",
params: params
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 'does not update the conversation if you do not have access to it' 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(:unauthorized)
end
it 'updates the conversation if you are an administrator' do
patch "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}",
params: params,
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body, symbolize_names: true)[:priority]).to eq('high')
end
it 'updates the conversation if you are an agent with access to inbox' do
create(:inbox_member, user: agent, inbox: conversation.inbox)
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)[:priority]).to eq('high')
end
end
end
describe 'POST /api/v1/accounts/{account.id}/conversations' do
let(:contact) { create(:contact, account: account) }
let(:inbox) { create(:inbox, account: account) }
let!(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: inbox) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/conversations",
params: { source_id: contact_inbox.source_id },
as: :json
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, auto_offline: false) }
let(:team) { create(:team, account: account) }
it 'will not create a new conversation if agent does not have access to inbox' do
allow(Rails.configuration.dispatcher).to receive(:dispatch)
additional_attributes = { test: 'test' }
post "/api/v1/accounts/#{account.id}/conversations",
headers: agent.create_new_auth_token,
params: { source_id: contact_inbox.source_id, additional_attributes: additional_attributes },
as: :json
expect(response).to have_http_status(:unauthorized)
end
context 'when it is an authenticated user who has access to the inbox' do
before do
create(:inbox_member, user: agent, inbox: inbox)
end
it 'creates a new conversation' do
allow(Rails.configuration.dispatcher).to receive(:dispatch)
additional_attributes = { test: 'test' }
post "/api/v1/accounts/#{account.id}/conversations",
headers: agent.create_new_auth_token,
params: { source_id: contact_inbox.source_id, additional_attributes: additional_attributes },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data[:additional_attributes]).to eq(additional_attributes)
end
it 'does not create a new conversation if source_id is not unique' do
new_contact = create(:contact, account: account)
post "/api/v1/accounts/#{account.id}/conversations",
headers: agent.create_new_auth_token,
params: { source_id: contact_inbox.source_id, inbox_id: inbox.id, contact_id: new_contact.id },
as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
it 'creates a conversation in specificed status' do
allow(Rails.configuration.dispatcher).to receive(:dispatch)
post "/api/v1/accounts/#{account.id}/conversations",
headers: agent.create_new_auth_token,
params: { source_id: contact_inbox.source_id, status: 'pending' },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data[:status]).to eq('pending')
end
it 'creates a new conversation with message when message is passed' do
allow(Rails.configuration.dispatcher).to receive(:dispatch)
post "/api/v1/accounts/#{account.id}/conversations",
headers: agent.create_new_auth_token,
params: { source_id: contact_inbox.source_id, message: { content: 'hi' } },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data[:additional_attributes]).to eq({})
expect(account.conversations.find_by(display_id: response_data[:id]).messages.outgoing.first.content).to eq 'hi'
end
it 'calls contact inbox builder if contact_id and inbox_id is present' do
builder = double
allow(Rails.configuration.dispatcher).to receive(:dispatch)
allow(ContactInboxBuilder).to receive(:new).with(contact: contact, inbox: inbox, source_id: nil, hmac_verified: false).and_return(builder)
allow(builder).to receive(:perform)
expect(builder).to receive(:perform)
post "/api/v1/accounts/#{account.id}/conversations",
headers: agent.create_new_auth_token,
params: { contact_id: contact.id, inbox_id: inbox.id, hmac_verified: 'false' },
as: :json
end
it 'creates a new conversation with assignee and team' do
allow(Rails.configuration.dispatcher).to receive(:dispatch)
post "/api/v1/accounts/#{account.id}/conversations",
headers: agent.create_new_auth_token,
params: { source_id: contact_inbox.source_id, contact_id: contact.id, inbox_id: inbox.id, assignee_id: agent.id, team_id: team.id },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data[:meta][:assignee][:name]).to eq(agent.name)
expect(response_data[:meta][:team][:name]).to eq(team.name)
end
end
end
end
describe 'POST /api/v1/accounts/{account.id}/conversations/:id/toggle_status' do
let(:conversation) { create(:conversation, account: account) }
let(:inbox) { create(:inbox, account: account) }
let(:pending_conversation) { create(:conversation, inbox: inbox, account: account, status: 'pending') }
let(:agent_bot) { create(:agent_bot, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_status"
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) }
before do
create(:inbox_member, user: agent, inbox: conversation.inbox)
end
it 'toggles the conversation status if status is empty' do
expect(conversation.status).to eq('open')
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_status",
headers: agent.create_new_auth_token,
params: { status: '' },
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.status).to eq('resolved')
end
it 'toggles the conversation status to open from pending' do
conversation.update!(status: 'pending')
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_status",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.status).to eq('open')
end
it 'self assign if agent changes the conversation status to open' do
conversation.update!(status: 'pending')
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_status",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.status).to eq('open')
expect(conversation.reload.assignee_id).to eq(agent.id)
end
it 'disbale self assign if admin changes the conversation status to open' do
conversation.update!(status: 'pending')
conversation.update!(assignee_id: nil)
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_status",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.status).to eq('open')
expect(conversation.reload.assignee_id).not_to eq(administrator.id)
end
it 'toggles the conversation status to specific status when parameter is passed' do
expect(conversation.status).to eq('open')
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_status",
headers: agent.create_new_auth_token,
params: { status: 'pending' },
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.status).to eq('pending')
end
it 'toggles the conversation status to snoozed when parameter is passed' do
expect(conversation.status).to eq('open')
snoozed_until = (DateTime.now.utc + 2.days).to_i
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_status",
headers: agent.create_new_auth_token,
params: { status: 'snoozed', snoozed_until: snoozed_until },
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.status).to eq('snoozed')
expect(conversation.reload.snoozed_until.to_i).to eq(snoozed_until)
end
end
context 'when it is an authenticated bot' do
# this test will basically ensure that the status actually changes
# regardless of the value to be done
it 'returns authorized for arbritrary status' do
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
conversation.update!(status: 'open')
expect(conversation.reload.status).to eq('open')
snoozed_until = (DateTime.now.utc + 2.days).to_i
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_status",
headers: { api_access_token: agent_bot.access_token.token },
params: { status: 'snoozed', snoozed_until: snoozed_until },
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.status).to eq('snoozed')
end
it 'triggers handoff event when moving from pending to open' do
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
allow(Rails.configuration.dispatcher).to receive(:dispatch)
post "/api/v1/accounts/#{account.id}/conversations/#{pending_conversation.display_id}/toggle_status",
headers: { api_access_token: agent_bot.access_token.token },
params: { status: 'open' },
as: :json
expect(response).to have_http_status(:success)
expect(pending_conversation.reload.status).to eq('open')
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
.with(Events::Types::CONVERSATION_BOT_HANDOFF, kind_of(Time), conversation: pending_conversation, notifiable_assignee_change: false,
changed_attributes: anything, performed_by: anything)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/conversations/:id/toggle_priority' do
let(:inbox) { create(:inbox, account: account) }
let(:conversation) { create(:conversation, account: account) }
let(:pending_conversation) { create(:conversation, inbox: inbox, account: account, status: 'pending') }
let(:agent) { create(:user, account: account, role: :agent) }
let(:agent_bot) { create(:agent_bot, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_priority"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:administrator) { create(:user, account: account, role: :administrator) }
before do
create(:inbox_member, user: agent, inbox: conversation.inbox)
end
it 'toggles the conversation priority to nil if no value is passed' do
expect(conversation.priority).to be_nil
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_priority",
headers: agent.create_new_auth_token,
params: { priority: 'low' },
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.priority).to eq('low')
end
it 'toggles the conversation priority' do
conversation.priority = 'low'
conversation.save!
expect(conversation.reload.priority).to eq('low')
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_priority",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.priority).to be_nil
end
end
context 'when it is an authenticated bot' do
it 'toggle the priority of the bot agent conversation' do
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
conversation.update!(priority: 'low')
expect(conversation.reload.priority).to eq('low')
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_priority",
headers: { api_access_token: agent_bot.access_token.token },
params: { priority: 'high' },
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.priority).to eq('high')
end
end
end
describe 'POST /api/v1/accounts/{account.id}/conversations/:id/toggle_typing_status' do
let(:conversation) { create(:conversation, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_typing_status"
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) }
before do
create(:inbox_member, user: agent, inbox: conversation.inbox)
end
it 'toggles the conversation status' do
allow(Rails.configuration.dispatcher).to receive(:dispatch)
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_typing_status",
headers: agent.create_new_auth_token,
params: { typing_status: 'on', is_private: false },
as: :json
expect(response).to have_http_status(:success)
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
.with(Conversation::CONVERSATION_TYPING_ON, kind_of(Time), { conversation: conversation, user: agent, is_private: false })
end
it 'toggles the conversation status for private notes' do
allow(Rails.configuration.dispatcher).to receive(:dispatch)
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_typing_status",
headers: agent.create_new_auth_token,
params: { typing_status: 'on', is_private: true },
as: :json
expect(response).to have_http_status(:success)
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
.with(Conversation::CONVERSATION_TYPING_ON, kind_of(Time), { conversation: conversation, user: agent, is_private: true })
end
end
end
describe 'POST /api/v1/accounts/{account.id}/conversations/:id/update_last_seen' do
let(:conversation) { create(:conversation, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/update_last_seen"
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) }
before do
create(:inbox_member, user: agent, inbox: conversation.inbox)
end
it 'updates last seen' do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/update_last_seen",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.agent_last_seen_at).not_to be_nil
end
it 'updates assignee last seen' do
conversation.update!(assignee_id: agent.id)
expect(conversation.reload.assignee_last_seen_at).to be_nil
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/update_last_seen",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.assignee_last_seen_at).not_to be_nil
end
end
end
describe 'POST /api/v1/accounts/{account.id}/conversations/:id/unread' do
let(:conversation) { create(:conversation, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/unread"
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) }
before do
create(:inbox_member, user: agent, inbox: conversation.inbox)
create(:message, conversation: conversation, account: account, inbox: conversation.inbox, content: 'Hello', message_type: 'incoming')
end
it 'updates last seen' do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/unread",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
last_seen_at = conversation.messages.incoming.last.created_at - 1.second
expect(conversation.reload.agent_last_seen_at).to eq(last_seen_at)
expect(conversation.reload.assignee_last_seen_at).to eq(last_seen_at)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/conversations/:id/mute' do
let(:conversation) { create(:conversation, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/mute"
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) }
before do
create(:inbox_member, user: agent, inbox: conversation.inbox)
end
it 'mutes conversation' do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/mute",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.resolved?).to be(true)
expect(conversation.reload.muted?).to be(true)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/conversations/:id/unmute' do
let(:conversation) { create(:conversation, account: account).tap(&:mute!) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/unmute"
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) }
before do
create(:inbox_member, user: agent, inbox: conversation.inbox)
end
it 'unmutes conversation' do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/unmute",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.muted?).to be(false)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/conversations/:id/transcript' do
let(:conversation) { create(:conversation, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/transcript"
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(:params) { { email: 'test@test.com' } }
before do
create(:inbox_member, user: agent, inbox: conversation.inbox)
end
it 'mutes conversation' do
mailer = double
allow(ConversationReplyMailer).to receive(:with).and_return(mailer)
allow(mailer).to receive(:conversation_transcript)
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/transcript",
headers: agent.create_new_auth_token,
params: params,
as: :json
expect(response).to have_http_status(:success)
expect(mailer).to have_received(:conversation_transcript).with(conversation, 'test@test.com')
end
it 'renders error when parameter missing' do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/transcript",
headers: agent.create_new_auth_token,
params: {},
as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/conversations/:id/custom_attributes' do
let(:conversation) { create(:conversation, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/custom_attributes"
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(:custom_attributes) { { user_id: 1001, created_date: '23/12/2012', subscription_id: 12 } }
let(:valid_params) { { custom_attributes: custom_attributes } }
before do
create(:inbox_member, user: agent, inbox: conversation.inbox)
end
it 'updates custom attributes' do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/custom_attributes",
headers: agent.create_new_auth_token,
params: valid_params,
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.custom_attributes).not_to be_nil
expect(conversation.reload.custom_attributes.count).to eq 3
end
end
context 'when it is a bot' do
let(:agent_bot) { create(:agent_bot, account: account) }
let(:custom_attributes) { { bot_id: 1001, flow_name: 'support_flow', step: 'greeting' } }
let(:valid_params) { { custom_attributes: custom_attributes } }
before do
create(:agent_bot_inbox, agent_bot: agent_bot, inbox: conversation.inbox)
end
it 'updates custom attributes' do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/custom_attributes",
headers: { api_access_token: agent_bot.access_token.token },
params: valid_params,
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.custom_attributes).not_to be_nil
expect(conversation.reload.custom_attributes.count).to eq 3
end
end
end
describe 'GET /api/v1/accounts/{account.id}/conversations/:id/attachments' do
let(:conversation) { create(:conversation, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/attachments"
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) }
before do
create(:message, :with_attachment, conversation: conversation, account: account, inbox: conversation.inbox, message_type: 'incoming')
end
it 'does not return the attachments if you do not have access to it' do
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/attachments",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'return the attachments if you are an administrator' do
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/attachments",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body['payload'].first['file_type']).to eq('image')
expect(response_body['payload'].first['sender']['id']).to eq(conversation.messages.last.sender.id)
end
it 'return the attachments if you are an agent with access to inbox' do
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/attachments",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body['payload'].length).to eq(1)
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/conversations/:id' do
let(:conversation) { create(:conversation, account: account) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated agent' do
before do
create(:inbox_member, user: agent, inbox: conversation.inbox)
end
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
response_body = response.parsed_body
expect(response_body['error']).to eq('You are not authorized to do this action')
end
end
context 'when it is an authenticated administrator' do
before do
create(:inbox_member, user: administrator, inbox: conversation.inbox)
end
it 'successfully deletes the conversation' do
expect do
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}",
headers: administrator.create_new_auth_token,
as: :json
end.to have_enqueued_job(DeleteObjectJob).with(conversation, administrator, anything)
expect(response).to have_http_status(:ok)
end
it 'can delete conversations from inboxes without direct access' do
other_inbox = create(:inbox, account: account)
other_conversation = create(:conversation, account: account, inbox: other_inbox)
expect do
delete "/api/v1/accounts/#{account.id}/conversations/#{other_conversation.display_id}",
headers: administrator.create_new_auth_token,
as: :json
end.to have_enqueued_job(DeleteObjectJob).with(other_conversation, administrator, anything)
expect(response).to have_http_status(:ok)
end
end
end
end

View File

@@ -0,0 +1,188 @@
require 'rails_helper'
RSpec.describe 'CSAT Survey Responses API', type: :request do
let(:account) { create(:account) }
let!(:csat_survey_response) { create(:csat_survey_response, account: account) }
let(:administrator) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
describe 'GET /api/v1/accounts/{account.id}/csat_survey_responses' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/csat_survey_responses"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'returns unauthorized for agents' do
get "/api/v1/accounts/#{account.id}/csat_survey_responses",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'returns all the csat survey responses for administrators' do
get "/api/v1/accounts/#{account.id}/csat_survey_responses",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body.first['feedback_message']).to eq(csat_survey_response.feedback_message)
end
it 'filters csat responses based on a date range' do
csat_10_days_ago = create(:csat_survey_response, account: account, created_at: 10.days.ago)
csat_3_days_ago = create(:csat_survey_response, account: account, created_at: 3.days.ago)
get "/api/v1/accounts/#{account.id}/csat_survey_responses",
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,
as: :json
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data.pluck('id')).to include(csat_3_days_ago.id)
expect(response_data.pluck('id')).not_to include(csat_10_days_ago.id)
end
it 'filters csat responses based on a date range and agent ids' do
csat1_assigned_agent = create(:user, account: account, role: :agent)
csat2_assigned_agent = create(:user, account: account, role: :agent)
create(:csat_survey_response, account: account, created_at: 10.days.ago, assigned_agent: csat1_assigned_agent)
create(:csat_survey_response, account: account, created_at: 3.days.ago, assigned_agent: csat2_assigned_agent)
create(:csat_survey_response, account: account, created_at: 5.days.ago)
get "/api/v1/accounts/#{account.id}/csat_survey_responses",
params: { since: 11.days.ago.to_time.to_i.to_s, until: Time.zone.today.to_time.to_i.to_s,
user_ids: [csat1_assigned_agent.id, csat2_assigned_agent.id] },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data.size).to eq 2
end
it 'returns csat responses even if the agent is deleted from account' do
deleted_agent_csat = create(:csat_survey_response, account: account, assigned_agent: agent)
deleted_agent_csat.assigned_agent.account_users.destroy_all
get "/api/v1/accounts/#{account.id}/csat_survey_responses",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/csat_survey_responses/metrics' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/csat_survey_responses/metrics"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'returns unauthorized for agents' do
get "/api/v1/accounts/#{account.id}/csat_survey_responses/metrics",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'returns csat metrics for administrators' do
get "/api/v1/accounts/#{account.id}/csat_survey_responses/metrics",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data['total_count']).to eq 1
expect(response_data['total_sent_messages_count']).to eq 0
expect(response_data['ratings_count']).to eq({ '1' => 1 })
end
it 'filters csat metrics based on a date range' do
# clearing any existing csat responses
CsatSurveyResponse.destroy_all
create(:csat_survey_response, account: account, created_at: 10.days.ago)
create(:csat_survey_response, account: account, created_at: 3.days.ago)
get "/api/v1/accounts/#{account.id}/csat_survey_responses/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,
as: :json
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data['total_count']).to eq 1
expect(response_data['total_sent_messages_count']).to eq 0
expect(response_data['ratings_count']).to eq({ '1' => 1 })
end
it 'filters csat metrics based on a date range and agent ids' do
csat1_assigned_agent = create(:user, account: account, role: :agent)
csat2_assigned_agent = create(:user, account: account, role: :agent)
create(:csat_survey_response, account: account, created_at: 10.days.ago, assigned_agent: csat1_assigned_agent)
create(:csat_survey_response, account: account, created_at: 3.days.ago, assigned_agent: csat2_assigned_agent)
create(:csat_survey_response, account: account, created_at: 5.days.ago)
get "/api/v1/accounts/#{account.id}/csat_survey_responses/metrics",
params: { since: 11.days.ago.to_time.to_i.to_s, until: Time.zone.today.to_time.to_i.to_s,
user_ids: [csat1_assigned_agent.id, csat2_assigned_agent.id] },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data['total_count']).to eq 2
expect(response_data['total_sent_messages_count']).to eq 0
expect(response_data['ratings_count']).to eq({ '1' => 2 })
end
end
end
describe 'GET /api/v1/accounts/{account.id}/csat_survey_responses/download' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/csat_survey_responses/download"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:params) { { since: 5.days.ago.to_time.to_i.to_s, until: Time.zone.tomorrow.to_time.to_i.to_s } }
it 'returns unauthorized for agents' do
get "/api/v1/accounts/#{account.id}/csat_survey_responses/download",
params: params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
it 'returns summary' do
get "/api/v1/accounts/#{account.id}/csat_survey_responses/download",
params: params,
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
content = CSV.parse(response.body)
# Check rating from CSAT Row
expect(content[1][1]).to eq '1'
expect(content.length).to eq 3
end
end
end
end

View File

@@ -0,0 +1,166 @@
require 'rails_helper'
RSpec.describe 'Custom Attribute Definitions API', type: :request do
let(:account) { create(:account) }
let(:user) { create(:user, account: account) }
describe 'GET /api/v1/accounts/{account.id}/custom_attribute_definitions' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/custom_attribute_definitions"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let!(:custom_attribute_definition) { create(:custom_attribute_definition, account: account) }
it 'returns all customer attribute definitions related to the account' do
create(:custom_attribute_definition, attribute_model: 'contact_attribute', account: account)
get "/api/v1/accounts/#{account.id}/custom_attribute_definitions",
headers: user.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body.count).to eq(2)
expect(response_body.first['attribute_key']).to eq(custom_attribute_definition.attribute_key)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/custom_attribute_definitions/:id' do
let!(:custom_attribute_definition) { create(:custom_attribute_definition, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/custom_attribute_definitions/#{custom_attribute_definition.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'shows the custom attribute definition' do
get "/api/v1/accounts/#{account.id}/custom_attribute_definitions/#{custom_attribute_definition.id}",
headers: user.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(custom_attribute_definition.attribute_key)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/custom_attribute_definitions' do
let(:payload) do
{
custom_attribute_definition: {
attribute_display_name: 'Developer ID',
attribute_key: 'developer_id',
attribute_model: 'contact_attribute',
attribute_display_type: 'text',
default_value: ''
}
}
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
expect do
post "/api/v1/accounts/#{account.id}/custom_attribute_definitions",
params: payload
end.not_to change(CustomAttributeDefinition, :count)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'creates the filter' do
expect do
post "/api/v1/accounts/#{account.id}/custom_attribute_definitions", headers: user.create_new_auth_token,
params: payload
end.to change(CustomAttributeDefinition, :count).by(1)
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['attribute_key']).to eq 'developer_id'
end
context 'when creating with a conflicting attribute_key' do
let(:standard_key) { CustomAttributeDefinition::STANDARD_ATTRIBUTES[:conversation].first }
let(:conflicting_payload) do
{
custom_attribute_definition: {
attribute_display_name: 'Conflicting Key',
attribute_key: standard_key,
attribute_model: 'conversation_attribute',
attribute_display_type: 'text'
}
}
end
it 'returns error for conflicting key' do
post "/api/v1/accounts/#{account.id}/custom_attribute_definitions",
headers: user.create_new_auth_token,
params: conflicting_payload
expect(response).to have_http_status(:unprocessable_entity)
json_response = response.parsed_body
expect(json_response['message']).to include('The provided key is not allowed as it might conflict with default attributes.')
end
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/custom_attribute_definitions/:id' do
let(:payload) { { custom_attribute_definition: { attribute_display_name: 'Developer ID', attribute_key: 'developer_id' } } }
let!(:custom_attribute_definition) { create(:custom_attribute_definition, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/custom_attribute_definitions/#{custom_attribute_definition.id}",
params: payload
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'updates the custom attribute definition' do
patch "/api/v1/accounts/#{account.id}/custom_attribute_definitions/#{custom_attribute_definition.id}",
headers: user.create_new_auth_token,
params: payload,
as: :json
expect(response).to have_http_status(:success)
expect(custom_attribute_definition.reload.attribute_display_name).to eq('Developer ID')
expect(custom_attribute_definition.reload.attribute_key).to eq('developer_id')
expect(custom_attribute_definition.reload.attribute_model).to eq('conversation_attribute')
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/custom_attribute_definitions/:id' do
let!(:custom_attribute_definition) { create(:custom_attribute_definition, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/custom_attribute_definitions/#{custom_attribute_definition.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated admin user' do
it 'deletes custom attribute' do
delete "/api/v1/accounts/#{account.id}/custom_attribute_definitions/#{custom_attribute_definition.id}",
headers: user.create_new_auth_token,
as: :json
expect(response).to have_http_status(:no_content)
expect(account.custom_attribute_definitions.count).to be 0
end
end
end
end

View File

@@ -0,0 +1,181 @@
require 'rails_helper'
RSpec.describe 'Custom Filters API', type: :request do
let(:account) { create(:account) }
let(:user) { create(:user, account: account, role: :agent) }
let!(:custom_filter) { create(:custom_filter, user: user, account: account) }
before do
create(:conversation, account: account, assignee: user, status: 'open')
create(:conversation, account: account, assignee: user, status: 'resolved')
custom_filter.query = { payload: [
{
values: ['open'],
attribute_key: 'status',
query_operator: nil,
attribute_model: 'standard',
filter_operator: 'equal_to',
custom_attribute_type: ''
}
] }
custom_filter.save
end
describe 'GET /api/v1/accounts/{account.id}/custom_filters' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/custom_filters"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'returns all custom_filter related to the user' do
get "/api/v1/accounts/#{account.id}/custom_filters",
headers: user.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body.first['name']).to eq(custom_filter.name)
expect(response_body.first['query']).to eq(custom_filter.query)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/custom_filters/:id' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/custom_filters/#{custom_filter.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'shows the custom filter' do
get "/api/v1/accounts/#{account.id}/custom_filters/#{custom_filter.id}",
headers: user.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(custom_filter.name)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/custom_filters' do
let(:payload) do
{ custom_filter: {
name: 'vip-customers', filter_type: 'conversation',
query: { payload: [{
values: ['open'], attribute_key: 'status', attribute_model: 'standard', filter_operator: 'equal_to'
}] }
} }
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
expect { post "/api/v1/accounts/#{account.id}/custom_filters", params: payload }.not_to change(CustomFilter, :count)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'creates the filter' do
post "/api/v1/accounts/#{account.id}/custom_filters", headers: user.create_new_auth_token,
params: payload
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['name']).to eq 'vip-customers'
end
it 'gives the error for 1001st record' do
CustomFilter.delete_all
Limits::MAX_CUSTOM_FILTERS_PER_USER.times do
create(:custom_filter, user: user, account: account)
end
expect do
post "/api/v1/accounts/#{account.id}/custom_filters", headers: user.create_new_auth_token,
params: payload
end.not_to change(CustomFilter, :count)
expect(response).to have_http_status(:unprocessable_entity)
json_response = response.parsed_body
expect(json_response['message']).to include(
'Account Limit reached. The maximum number of allowed custom filters for a user per account is 1000.'
)
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/custom_filters/:id' do
let(:payload) do
{ custom_filter: {
name: 'vip-customers', filter_type: 'conversation',
query: { payload: [{
values: ['resolved'], attribute_key: 'status', attribute_model: 'standard', filter_operator: 'equal_to'
}] }
} }
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/custom_filters/#{custom_filter.id}",
params: payload
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'updates the custom filter' do
patch "/api/v1/accounts/#{account.id}/custom_filters/#{custom_filter.id}",
headers: user.create_new_auth_token,
params: payload,
as: :json
expect(response).to have_http_status(:success)
expect(custom_filter.reload.name).to eq('vip-customers')
expect(custom_filter.reload.filter_type).to eq('conversation')
expect(custom_filter.reload.query['payload'][0]['values']).to eq(['resolved'])
end
it 'prevents the update of custom filter of another user/account' do
other_account = create(:account)
other_user = create(:user, account: other_account)
other_custom_filter = create(:custom_filter, user: other_user, account: other_account)
patch "/api/v1/accounts/#{account.id}/custom_filters/#{other_custom_filter.id}",
headers: user.create_new_auth_token,
params: payload,
as: :json
expect(response).to have_http_status(:not_found)
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/custom_filters/:id' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/custom_filters/#{custom_filter.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated admin user' do
it 'deletes custom filter if it is attached to the current user and account' do
delete "/api/v1/accounts/#{account.id}/custom_filters/#{custom_filter.id}",
headers: user.create_new_auth_token,
as: :json
expect(response).to have_http_status(:no_content)
expect(user.custom_filters.count).to be 0
end
end
end
end

View File

@@ -0,0 +1,186 @@
require 'rails_helper'
RSpec.describe 'DashboardAppsController', type: :request do
let(:account) { create(:account) }
describe 'GET /api/v1/accounts/{account.id}/dashboard_apps' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/dashboard_apps"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:user) { create(:user, account: account) }
let!(:dashboard_app) { create(:dashboard_app, user: user, account: account) }
it 'returns all dashboard_apps in the account' do
get "/api/v1/accounts/#{account.id}/dashboard_apps",
headers: user.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body.first['title']).to eq(dashboard_app.title)
expect(response_body.first['content']).to eq(dashboard_app.content)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/dashboard_apps/:id' do
let(:user) { create(:user, account: account) }
let!(:dashboard_app) { create(:dashboard_app, user: user, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'shows the dashboard app' do
get "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}",
headers: user.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(dashboard_app.title)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/dashboard_apps' do
let(:payload) { { dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'frame', url: 'https://link.com' }] } } }
let(:no_ssl_payload) { { dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'frame', url: 'http://link.com' }] } } }
let(:invalid_type_payload) { { dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'dda', url: 'https://link.com' }] } } }
let(:invalid_url_payload) { { dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'frame', url: 'com' }] } } }
let(:non_http_url_payload) do
{ dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'frame', url: 'ftp://wontwork.chatwoot.com/hello-world' }] } }
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
expect { post "/api/v1/accounts/#{account.id}/dashboard_apps", params: payload }.not_to change(CustomFilter, :count)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:user) { create(:user, account: account) }
it 'creates the dashboard app' do
expect do
post "/api/v1/accounts/#{account.id}/dashboard_apps", headers: user.create_new_auth_token,
params: payload
end.to change(DashboardApp, :count).by(1)
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['title']).to eq 'CRM Dashboard'
expect(json_response['content'][0]['link']).to eq payload[:dashboard_app][:content][0][:link]
expect(json_response['content'][0]['type']).to eq payload[:dashboard_app][:content][0][:type]
end
it 'creates the dashboard app even if the URL does not have SSL' do
expect do
post "/api/v1/accounts/#{account.id}/dashboard_apps", headers: user.create_new_auth_token,
params: no_ssl_payload
end.to change(DashboardApp, :count).by(1)
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['title']).to eq 'CRM Dashboard'
expect(json_response['content'][0]['link']).to eq payload[:dashboard_app][:content][0][:link]
expect(json_response['content'][0]['type']).to eq payload[:dashboard_app][:content][0][:type]
end
it 'does not create the dashboard app if invalid URL' do
expect do
post "/api/v1/accounts/#{account.id}/dashboard_apps", headers: user.create_new_auth_token,
params: invalid_url_payload
end.not_to change(DashboardApp, :count)
expect(response).to have_http_status(:unprocessable_entity)
json_response = response.parsed_body
expect(json_response['message']).to eq 'Content : Invalid data'
end
it 'does not create the dashboard app if non HTTP URL' do
expect do
post "/api/v1/accounts/#{account.id}/dashboard_apps", headers: user.create_new_auth_token,
params: non_http_url_payload
end.not_to change(DashboardApp, :count)
expect(response).to have_http_status(:unprocessable_entity)
json_response = response.parsed_body
expect(json_response['message']).to eq 'Content : Invalid data'
end
it 'does not create the dashboard app if invalid type' do
expect do
post "/api/v1/accounts/#{account.id}/dashboard_apps", headers: user.create_new_auth_token,
params: invalid_type_payload
end.not_to change(DashboardApp, :count)
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/dashboard_apps/:id' do
let(:payload) { { dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'frame', url: 'https://link.com' }] } } }
let(:user) { create(:user, account: account) }
let!(:dashboard_app) { create(:dashboard_app, user: user, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}",
params: payload
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'updates the dashboard app' do
patch "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}",
headers: user.create_new_auth_token,
params: payload,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(dashboard_app.reload.title).to eq('CRM Dashboard')
expect(json_response['content'][0]['link']).to eq payload[:dashboard_app][:content][0][:link]
expect(json_response['content'][0]['type']).to eq payload[:dashboard_app][:content][0][:type]
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/dashboard_apps/:id' do
let(:user) { create(:user, account: account) }
let!(:dashboard_app) { create(:dashboard_app, user: user, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated admin user' do
it 'deletes dashboard app' do
delete "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}",
headers: user.create_new_auth_token,
as: :json
expect(response).to have_http_status(:no_content)
expect(user.dashboard_apps.count).to be 0
end
end
end
end

View File

@@ -0,0 +1,52 @@
require 'rails_helper'
RSpec.describe 'Google Authorization API', type: :request do
let(:account) { create(:account) }
describe 'POST /api/v1/accounts/{account.id}/google/authorization' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/google/authorization"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
it 'returns unathorized for agent' do
post "/api/v1/accounts/#{account.id}/google/authorization",
headers: agent.create_new_auth_token,
params: { email: administrator.email },
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'creates a new authorization and returns the redirect url' do
post "/api/v1/accounts/#{account.id}/google/authorization",
headers: administrator.create_new_auth_token,
params: { email: administrator.email },
as: :json
expect(response).to have_http_status(:success)
# Validate URL components
url = response.parsed_body['url']
uri = URI.parse(url)
params = CGI.parse(uri.query)
expect(url).to start_with('https://accounts.google.com/o/oauth2/auth')
expect(params['scope']).to eq(['email profile https://mail.google.com/'])
expect(params['redirect_uri']).to eq(["#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback"])
# Validate state parameter exists and can be decoded back to the account
expect(params['state']).to be_present
decoded_account = GlobalID::Locator.locate_signed(params['state'].first, for: 'default')
expect(decoded_account).to eq(account)
end
end
end
end

View File

@@ -0,0 +1,383 @@
require 'rails_helper'
RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request do
let(:account) { create(:account) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:whatsapp_channel) do
create(:channel_whatsapp, account: account, provider: 'whatsapp_cloud', sync_templates: false, validate_provider_config: false)
end
let(:whatsapp_inbox) { create(:inbox, channel: whatsapp_channel, account: account) }
let(:web_widget_inbox) { create(:inbox, account: account) }
let(:mock_service) { instance_double(Whatsapp::CsatTemplateService) }
before do
create(:inbox_member, user: agent, inbox: whatsapp_inbox)
allow(Whatsapp::CsatTemplateService).to receive(:new).and_return(mock_service)
end
describe 'GET /api/v1/accounts/{account.id}/inboxes/{inbox.id}/csat_template' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is not a WhatsApp channel' do
it 'returns bad request' do
get "/api/v1/accounts/#{account.id}/inboxes/#{web_widget_inbox.id}/csat_template",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:bad_request)
expect(response.parsed_body['error']).to eq('CSAT template operations only available for WhatsApp and Twilio WhatsApp channels')
end
end
context 'when it is a WhatsApp channel' do
it 'returns template not found when no configuration exists' do
get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['template_exists']).to be false
end
it 'returns template status when template exists on WhatsApp' do
template_config = {
'template' => {
'name' => 'custom_survey_template',
'template_id' => '123456789',
'language' => 'en'
}
}
whatsapp_inbox.update!(csat_config: template_config)
allow(mock_service).to receive(:get_template_status)
.with('custom_survey_template')
.and_return({
success: true,
template: { id: '123456789', status: 'APPROVED' }
})
get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data['template_exists']).to be true
expect(response_data['template_name']).to eq('custom_survey_template')
expect(response_data['status']).to eq('APPROVED')
expect(response_data['template_id']).to eq('123456789')
end
it 'returns template not found when template does not exist on WhatsApp' do
template_config = { 'template' => { 'name' => 'custom_survey_template' } }
whatsapp_inbox.update!(csat_config: template_config)
allow(mock_service).to receive(:get_template_status)
.with('custom_survey_template')
.and_return({ success: false, error: 'Template not found' })
get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data['template_exists']).to be false
expect(response_data['error']).to eq('Template not found')
end
it 'handles service errors gracefully' do
template_config = { 'template' => { 'name' => 'custom_survey_template' } }
whatsapp_inbox.update!(csat_config: template_config)
allow(mock_service).to receive(:get_template_status)
.and_raise(StandardError, 'API connection failed')
get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:internal_server_error)
expect(response.parsed_body['error']).to eq('API connection failed')
end
it 'returns unauthorized when agent is not assigned to inbox' do
other_agent = create(:user, account: account, role: :agent)
get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: other_agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'allows access when agent is assigned to inbox' do
whatsapp_inbox.update!(csat_config: { 'template' => { 'name' => 'test' } })
allow(mock_service).to receive(:get_template_status)
.and_return({ success: true, template: { id: '123', status: 'APPROVED' } })
get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/inboxes/{inbox.id}/csat_template' do
let(:valid_template_params) do
{
template: {
message: 'How would you rate your experience?',
button_text: 'Rate Us',
language: 'en'
}
}
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
params: valid_template_params,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is not a WhatsApp channel' do
it 'returns bad request' do
post "/api/v1/accounts/#{account.id}/inboxes/#{web_widget_inbox.id}/csat_template",
headers: admin.create_new_auth_token,
params: valid_template_params,
as: :json
expect(response).to have_http_status(:bad_request)
expect(response.parsed_body['error']).to eq('CSAT template operations only available for WhatsApp and Twilio WhatsApp channels')
end
end
context 'when it is a WhatsApp channel' do
it 'returns error when message is missing' do
invalid_params = {
template: {
button_text: 'Rate Us',
language: 'en'
}
}
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: admin.create_new_auth_token,
params: invalid_params,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq('Message is required')
end
it 'returns error when template parameters are completely missing' do
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: admin.create_new_auth_token,
params: {},
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq('Template parameters are required')
end
it 'creates template successfully' do
allow(mock_service).to receive(:get_template_status).and_return({ success: false })
allow(mock_service).to receive(:create_template).and_return({
success: true,
template_name: "customer_satisfaction_survey_#{whatsapp_inbox.id}",
template_id: '987654321'
})
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: admin.create_new_auth_token,
params: valid_template_params,
as: :json
expect(response).to have_http_status(:created)
response_data = response.parsed_body
expect(response_data['template']['name']).to eq("customer_satisfaction_survey_#{whatsapp_inbox.id}")
expect(response_data['template']['template_id']).to eq('987654321')
expect(response_data['template']['status']).to eq('PENDING')
expect(response_data['template']['language']).to eq('en')
end
it 'uses default values for optional parameters' do
minimal_params = {
template: {
message: 'How would you rate your experience?'
}
}
allow(mock_service).to receive(:get_template_status).and_return({ success: false })
expect(mock_service).to receive(:create_template) do |config|
expect(config[:button_text]).to eq('Please rate us')
expect(config[:language]).to eq('en')
expect(config[:template_name]).to eq("customer_satisfaction_survey_#{whatsapp_inbox.id}")
{ success: true, template_name: "customer_satisfaction_survey_#{whatsapp_inbox.id}", template_id: '123' }
end
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: admin.create_new_auth_token,
params: minimal_params,
as: :json
expect(response).to have_http_status(:created)
end
it 'handles WhatsApp API errors with user-friendly messages' do
whatsapp_error_response = {
'error' => {
'code' => 100,
'error_subcode' => 2_388_092,
'message' => 'Invalid parameter',
'error_user_title' => 'Template Creation Failed',
'error_user_msg' => 'The template message contains invalid content. Please review your message and try again.'
}
}
allow(mock_service).to receive(:get_template_status).and_return({ success: false })
allow(mock_service).to receive(:create_template).and_return({
success: false,
error: 'Template creation failed',
response_body: whatsapp_error_response.to_json
})
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: admin.create_new_auth_token,
params: valid_template_params,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
response_data = response.parsed_body
expect(response_data['error']).to eq('The template message contains invalid content. Please review your message and try again.')
expect(response_data['details']).to include({
'code' => 100,
'subcode' => 2_388_092,
'title' => 'Template Creation Failed'
})
end
it 'handles generic API errors' do
allow(mock_service).to receive(:get_template_status).and_return({ success: false })
allow(mock_service).to receive(:create_template).and_return({
success: false,
error: 'Network timeout',
response_body: nil
})
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: admin.create_new_auth_token,
params: valid_template_params,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq('Network timeout')
end
it 'handles unexpected service errors' do
allow(mock_service).to receive(:get_template_status).and_return({ success: false })
allow(mock_service).to receive(:create_template)
.and_raise(StandardError, 'Unexpected error')
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: admin.create_new_auth_token,
params: valid_template_params,
as: :json
expect(response).to have_http_status(:internal_server_error)
expect(response.parsed_body['error']).to eq('Template creation failed')
end
it 'deletes existing template before creating new one' do
whatsapp_inbox.update!(csat_config: {
'template' => {
'name' => 'existing_template',
'template_id' => '111111111'
}
})
allow(mock_service).to receive(:get_template_status)
.with('existing_template')
.and_return({ success: true, template: { id: '111111111' } })
expect(mock_service).to receive(:delete_template)
.with('existing_template')
.and_return({ success: true })
expect(mock_service).to receive(:create_template)
.and_return({
success: true,
template_name: "customer_satisfaction_survey_#{whatsapp_inbox.id}",
template_id: '222222222'
})
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: admin.create_new_auth_token,
params: valid_template_params,
as: :json
expect(response).to have_http_status(:created)
end
it 'continues with creation even if deletion fails' do
whatsapp_inbox.update!(csat_config: {
'template' => { 'name' => 'existing_template' }
})
allow(mock_service).to receive(:get_template_status).and_return({ success: true })
allow(mock_service).to receive(:delete_template)
.and_return({ success: false, response_body: 'Delete failed' })
allow(mock_service).to receive(:create_template).and_return({
success: true,
template_name: "customer_satisfaction_survey_#{whatsapp_inbox.id}",
template_id: '333333333'
})
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: admin.create_new_auth_token,
params: valid_template_params,
as: :json
expect(response).to have_http_status(:created)
end
it 'returns unauthorized when agent is not assigned to inbox' do
other_agent = create(:user, account: account, role: :agent)
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: other_agent.create_new_auth_token,
params: valid_template_params,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'allows access when agent is assigned to inbox' do
allow(mock_service).to receive(:get_template_status).and_return({ success: false })
allow(mock_service).to receive(:create_template).and_return({
success: true,
template_name: 'customer_satisfaction_survey',
template_id: '444444444'
})
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: agent.create_new_auth_token,
params: valid_template_params,
as: :json
expect(response).to have_http_status(:created)
end
end
end
end

View File

@@ -0,0 +1,285 @@
require 'rails_helper'
RSpec.describe 'Inbox Member API', type: :request do
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
describe 'GET /api/v1/accounts/{account.id}/inbox_members/:id' do
let(:inbox_member) { create(:inbox_member, inbox: inbox) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/inbox_members/#{inbox.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user with out access to inbox' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'returns inbox member' do
get "/api/v1/accounts/#{account.id}/inbox_members/#{inbox.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user with access to inbox' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'returns inbox member' do
create(:inbox_member, user: agent, inbox: inbox)
get "/api/v1/accounts/#{account.id}/inbox_members/#{inbox.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['payload'].pluck('id')).to eq(inbox.inbox_members.pluck(:user_id))
end
end
end
describe 'POST /api/v1/accounts/{account.id}/inbox_members' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/inbox_members"
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) }
before do
create(:inbox_member, user: agent, inbox: inbox)
end
it 'returns unauthorized' do
params = { inbox_id: inbox.id, user_ids: [agent.id] }
post "/api/v1/accounts/#{account.id}/inbox_members",
headers: agent.create_new_auth_token,
params: params,
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) }
let(:old_agent) { create(:user, account: account, role: :agent) }
let(:agent_to_add) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, user: old_agent, inbox: inbox)
end
it 'add inbox members' do
params = { inbox_id: inbox.id, user_ids: [old_agent.id, agent_to_add.id] }
post "/api/v1/accounts/#{account.id}/inbox_members",
headers: administrator.create_new_auth_token,
params: params,
as: :json
expect(response).to have_http_status(:success)
expect(inbox.inbox_members&.count).to eq(2)
expect(inbox.inbox_members&.second&.user).to eq(agent_to_add)
end
it 'renders not found when inbox not found' do
params = { inbox_id: nil, user_ids: [agent_to_add.id] }
post "/api/v1/accounts/#{account.id}/inbox_members",
headers: administrator.create_new_auth_token,
params: params,
as: :json
expect(response).to have_http_status(:not_found)
end
it 'renders error on invalid params' do
params = { inbox_id: inbox.id, user_ids: ['invalid'] }
post "/api/v1/accounts/#{account.id}/inbox_members",
headers: administrator.create_new_auth_token,
params: params,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).to include('User must exist')
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/inbox_members' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
patch "/api/v1/accounts/#{account.id}/inbox_members"
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) }
before do
create(:inbox_member, user: agent, inbox: inbox)
end
it 'returns unauthorized' do
params = { inbox_id: inbox.id, user_ids: [agent.id] }
patch "/api/v1/accounts/#{account.id}/inbox_members",
headers: agent.create_new_auth_token,
params: params,
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) }
let(:old_agent) { create(:user, account: account, role: :agent) }
let(:agent_to_add) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, user: old_agent, inbox: inbox)
end
it 'modifies inbox members' do
params = { inbox_id: inbox.id, user_ids: [agent_to_add.id] }
patch "/api/v1/accounts/#{account.id}/inbox_members",
headers: administrator.create_new_auth_token,
params: params,
as: :json
expect(response).to have_http_status(:success)
expect(inbox.inbox_members&.count).to eq(1)
expect(inbox.inbox_members&.first&.user).to eq(agent_to_add)
end
it 'renders not found when inbox not found' do
params = { inbox_id: nil, user_ids: [agent_to_add.id] }
patch "/api/v1/accounts/#{account.id}/inbox_members",
headers: administrator.create_new_auth_token,
params: params,
as: :json
expect(response).to have_http_status(:not_found)
end
it 'renders error on invalid params' do
params = { inbox_id: inbox.id, user_ids: ['invalid'] }
patch "/api/v1/accounts/#{account.id}/inbox_members",
headers: administrator.create_new_auth_token,
params: params,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).to include('User must exist')
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/inbox_members' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/inbox_members"
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) }
before do
create(:inbox_member, user: agent, inbox: inbox)
end
it 'returns unauthorized' do
params = { inbox_id: inbox.id, user_ids: [agent.id] }
delete "/api/v1/accounts/#{account.id}/inbox_members",
headers: agent.create_new_auth_token,
params: params,
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) }
let(:old_agent) { create(:user, account: account, role: :agent) }
let(:agent_to_delete) { create(:user, account: account, role: :agent) }
let(:non_member_agent) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, user: old_agent, inbox: inbox)
create(:inbox_member, user: agent_to_delete, inbox: inbox)
end
it 'deletes inbox members' do
params = { inbox_id: inbox.id, user_ids: [agent_to_delete.id] }
delete "/api/v1/accounts/#{account.id}/inbox_members",
headers: administrator.create_new_auth_token,
params: params,
as: :json
expect(response).to have_http_status(:success)
expect(inbox.inbox_members&.count).to eq(1)
end
it 'renders not found when inbox not found' do
params = { inbox_id: nil, user_ids: [agent_to_delete.id] }
delete "/api/v1/accounts/#{account.id}/inbox_members",
headers: administrator.create_new_auth_token,
params: params,
as: :json
expect(response).to have_http_status(:not_found)
end
it 'ignores invalid params' do
params = { inbox_id: inbox.id, user_ids: ['invalid'] }
original_count = inbox.inbox_members&.count
delete "/api/v1/accounts/#{account.id}/inbox_members",
headers: administrator.create_new_auth_token,
params: params,
as: :json
expect(response).to have_http_status(:success)
expect(inbox.inbox_members&.count).to eq(original_count)
end
it 'ignores non member params' do
params = { inbox_id: inbox.id, user_ids: [non_member_agent.id] }
original_count = inbox.inbox_members&.count
delete "/api/v1/accounts/#{account.id}/inbox_members",
headers: administrator.create_new_auth_token,
params: params,
as: :json
expect(response).to have_http_status(:success)
expect(inbox.inbox_members&.count).to eq(original_count)
end
end
end
end

View File

@@ -0,0 +1,195 @@
require 'rails_helper'
RSpec.describe 'Inbox Assignment Policies API', type: :request do
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:assignment_policy) { create(:assignment_policy, account: account) }
describe 'GET /api/v1/accounts/{account_id}/inboxes/{inbox_id}/assignment_policy' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated admin' do
let(:admin) { create(:user, account: account, role: :administrator) }
context 'when inbox has an assignment policy' do
before do
create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy)
end
it 'returns the assignment policy for the inbox' do
get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['id']).to eq(assignment_policy.id)
expect(json_response['name']).to eq(assignment_policy.name)
end
end
context 'when inbox has no assignment policy' do
it 'returns not found' do
get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:not_found)
end
end
end
context 'when it is an agent' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'POST /api/v1/accounts/{account_id}/inboxes/{inbox_id}/assignment_policy' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy",
params: { assignment_policy_id: assignment_policy.id }
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated admin' do
let(:admin) { create(:user, account: account, role: :administrator) }
it 'assigns a policy to the inbox' do
expect do
post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy",
params: { assignment_policy_id: assignment_policy.id },
headers: admin.create_new_auth_token,
as: :json
end.to change(InboxAssignmentPolicy, :count).by(1)
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['id']).to eq(assignment_policy.id)
end
it 'replaces existing assignment policy for inbox' do
other_policy = create(:assignment_policy, account: account)
create(:inbox_assignment_policy, inbox: inbox, assignment_policy: other_policy)
expect do
post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy",
params: { assignment_policy_id: assignment_policy.id },
headers: admin.create_new_auth_token,
as: :json
end.not_to change(InboxAssignmentPolicy, :count)
expect(response).to have_http_status(:success)
expect(inbox.reload.inbox_assignment_policy.assignment_policy).to eq(assignment_policy)
end
it 'returns not found for invalid assignment policy' do
post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy",
params: { assignment_policy_id: 999_999 },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:not_found)
end
it 'returns not found for invalid inbox' do
post "/api/v1/accounts/#{account.id}/inboxes/999999/assignment_policy",
params: { assignment_policy_id: assignment_policy.id },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:not_found)
end
end
context 'when it is an agent' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy",
params: { assignment_policy_id: assignment_policy.id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'DELETE /api/v1/accounts/{account_id}/inboxes/{inbox_id}/assignment_policy' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated admin' do
let(:admin) { create(:user, account: account, role: :administrator) }
context 'when inbox has an assignment policy' do
before do
create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy)
end
it 'removes the assignment policy from inbox' do
expect do
delete "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy",
headers: admin.create_new_auth_token,
as: :json
end.to change(InboxAssignmentPolicy, :count).by(-1)
expect(response).to have_http_status(:success)
expect(inbox.reload.inbox_assignment_policy).to be_nil
end
end
context 'when inbox has no assignment policy' do
it 'returns error' do
expect do
delete "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy",
headers: admin.create_new_auth_token,
as: :json
end.not_to change(InboxAssignmentPolicy, :count)
expect(response).to have_http_status(:not_found)
end
end
it 'returns not found for invalid inbox' do
delete "/api/v1/accounts/#{account.id}/inboxes/999999/assignment_policy",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:not_found)
end
end
context 'when it is an agent' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignment_policy",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,54 @@
require 'rails_helper'
RSpec.describe 'Instagram Authorization API', type: :request do
let(:account) { create(:account) }
describe 'POST /api/v1/accounts/{account.id}/instagram/authorization' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/instagram/authorization"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
it 'returns unauthorized for agent' do
post "/api/v1/accounts/#{account.id}/instagram/authorization",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'creates a new authorization and returns the redirect url' do
post "/api/v1/accounts/#{account.id}/instagram/authorization",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['success']).to be true
instagram_service = Class.new do
extend InstagramConcern
extend Instagram::IntegrationHelper
end
frontend_url = ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
response_url = instagram_service.instagram_client.auth_code.authorize_url(
{
redirect_uri: "#{frontend_url}/instagram/callback",
scope: Instagram::IntegrationHelper::REQUIRED_SCOPES.join(','),
enable_fb_login: '0',
force_authentication: '1',
response_type: 'code',
state: instagram_service.generate_instagram_token(account.id)
}
)
expect(response.parsed_body['url']).to eq response_url
end
end
end
end

View File

@@ -0,0 +1,131 @@
require 'rails_helper'
RSpec.describe 'Integration Apps API', type: :request do
let(:account) { create(:account) }
describe 'GET /api/v1/integrations/apps' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get api_v1_account_integrations_apps_url(account)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:admin) { create(:user, account: account, role: :administrator) }
it 'returns all active apps without sensitive information if the user is an agent' do
first_app = Integrations::App.all.find { |app| app.active?(account) }
get api_v1_account_integrations_apps_url(account),
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
apps = response.parsed_body['payload'].first
expect(apps['id']).to eql(first_app.id)
expect(apps['name']).to eql(first_app.name)
expect(apps['action']).to be_nil
end
it 'will not return sensitive information for openai app for agents' do
openai = create(:integrations_hook, :openai, account: account)
get api_v1_account_integrations_apps_url(account),
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
app = response.parsed_body['payload'].find { |int_app| int_app['id'] == openai.app.id }
expect(app['hooks'].first['settings']).to be_nil
end
it 'returns all active apps with sensitive information if user is an admin' do
first_app = Integrations::App.all.find { |app| app.active?(account) }
get api_v1_account_integrations_apps_url(account),
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
apps = response.parsed_body['payload'].first
expect(apps['id']).to eql(first_app.id)
expect(apps['name']).to eql(first_app.name)
expect(apps['action']).to eql(first_app.action)
end
it 'returns slack app with appropriate redirect url when configured' do
with_modified_env SLACK_CLIENT_ID: 'client_id', SLACK_CLIENT_SECRET: 'client_secret' do
get api_v1_account_integrations_apps_url(account),
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
apps = response.parsed_body['payload']
slack_app = apps.find { |app| app['id'] == 'slack' }
expect(slack_app['action']).to include('client_id=client_id')
end
end
it 'will return sensitive information for openai app for admins' do
openai = create(:integrations_hook, :openai, account: account)
get api_v1_account_integrations_apps_url(account),
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
app = response.parsed_body['payload'].find { |int_app| int_app['id'] == openai.app.id }
expect(app['hooks'].first['settings']).not_to be_nil
end
end
end
describe 'GET /api/v1/integrations/apps/:id' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get api_v1_account_integrations_app_url(account_id: account.id, id: 'slack')
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:admin) { create(:user, account: account, role: :administrator) }
it 'returns details of the app' do
get api_v1_account_integrations_app_url(account_id: account.id, id: 'slack'),
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
app = response.parsed_body
expect(app['id']).to eql('slack')
expect(app['name']).to eql('Slack')
end
it 'will not return sensitive information for openai app for agents' do
openai = create(:integrations_hook, :openai, account: account)
get api_v1_account_integrations_app_url(account_id: account.id, id: openai.app.id),
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
app = response.parsed_body
expect(app['hooks'].first['settings']).to be_nil
end
it 'will return sensitive information for openai app for admins' do
openai = create(:integrations_hook, :openai, account: account)
get api_v1_account_integrations_app_url(account_id: account.id, id: openai.app.id),
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
app = response.parsed_body
expect(app['hooks'].first['settings']).not_to be_nil
end
end
end
end

View File

@@ -0,0 +1,138 @@
require 'rails_helper'
RSpec.describe 'Dyte Integration API', type: :request do
let(:headers) { { 'Content-Type' => 'application/json' } }
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:conversation) { create(:conversation, account: account, status: :pending) }
let(:message) { create(:message, conversation: conversation, account: account, inbox: conversation.inbox) }
let(:integration_message) do
create(:message, content_type: 'integrations',
content_attributes: { type: 'dyte', data: { meeting_id: 'm_id' } },
conversation: conversation, account: account, inbox: conversation.inbox)
end
let(:agent) { create(:user, account: account, role: :agent) }
let(:unauthorized_agent) { create(:user, account: account, role: :agent) }
before do
create(:integrations_hook, :dyte, account: account)
create(:inbox_member, user: agent, inbox: conversation.inbox)
end
describe 'POST /api/v1/accounts/:account_id/integrations/dyte/create_a_meeting' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post create_a_meeting_api_v1_account_integrations_dyte_url(account)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when the agent does not have access to the inbox' do
it 'returns unauthorized' do
post create_a_meeting_api_v1_account_integrations_dyte_url(account),
params: { conversation_id: conversation.display_id },
headers: unauthorized_agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent with inbox access and the Dyte API is a success' do
before do
stub_request(:post, 'https://api.dyte.io/v2/meetings')
.to_return(
status: 200,
body: { success: true, data: { id: 'meeting_id' } }.to_json,
headers: headers
)
end
it 'returns valid message payload' do
post create_a_meeting_api_v1_account_integrations_dyte_url(account),
params: { conversation_id: conversation.display_id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
last_message = conversation.reload.messages.last
expect(conversation.display_id).to eq(response_body['conversation_id'])
expect(last_message.id).to eq(response_body['id'])
end
end
context 'when it is an agent with inbox access and the Dyte API is errored' do
before do
stub_request(:post, 'https://api.dyte.io/v2/meetings')
.to_return(
status: 422,
body: { success: false, data: { message: 'Title is required' } }.to_json,
headers: headers
)
end
it 'returns error payload' do
post create_a_meeting_api_v1_account_integrations_dyte_url(account),
params: { conversation_id: conversation.display_id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
response_body = response.parsed_body
expect(response_body['error']).to eq({ 'data' => { 'message' => 'Title is required' }, 'success' => false })
end
end
end
describe 'POST /api/v1/accounts/:account_id/integrations/dyte/add_participant_to_meeting' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post add_participant_to_meeting_api_v1_account_integrations_dyte_url(account)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when the agent does not have access to the inbox' do
it 'returns unauthorized' do
post add_participant_to_meeting_api_v1_account_integrations_dyte_url(account),
params: { message_id: message.id },
headers: unauthorized_agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent with inbox access and message_type is not integrations' do
it 'returns error' do
post add_participant_to_meeting_api_v1_account_integrations_dyte_url(account),
params: { message_id: message.id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
context 'when it is an agent with inbox access and message_type is integrations' do
before do
stub_request(:post, 'https://api.dyte.io/v2/meetings/m_id/participants')
.to_return(
status: 200,
body: { success: true, data: { id: 'random_uuid', auth_token: 'json-web-token' } }.to_json,
headers: headers
)
end
it 'returns auth_token' do
post add_participant_to_meeting_api_v1_account_integrations_dyte_url(account),
params: { message_id: integration_message.id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body).to eq(
{
'id' => 'random_uuid', 'auth_token' => 'json-web-token'
}
)
end
end
end
end

View File

@@ -0,0 +1,137 @@
require 'rails_helper'
RSpec.describe 'Integration Hooks API', type: :request do
let(:account) { create(:account) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:inbox) { create(:inbox, account: account) }
let(:params) { { app_id: 'dialogflow', inbox_id: inbox.id, settings: { project_id: 'xx', credentials: { test: 'test' }, region: 'europe-west1' } } }
describe 'POST /api/v1/accounts/{account.id}/integrations/hooks' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post api_v1_account_integrations_hooks_url(account_id: account.id),
params: params,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'return unauthorized if agent' do
post api_v1_account_integrations_hooks_url(account_id: account.id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'creates hooks if admin' do
post api_v1_account_integrations_hooks_url(account_id: account.id),
params: params,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data['app_id']).to eq params[:app_id]
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/integrations/hooks/{hook_id}' do
let(:hook) { create(:integrations_hook, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
patch api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
params: params,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'return unauthorized if agent' do
patch api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'updates hook if admin' do
patch api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
params: params,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data['app_id']).to eq 'slack'
end
end
end
describe 'POST /api/v1/accounts/{account.id}/integrations/hooks/{hook_id}/process_event' do
let(:hook) { create(:integrations_hook, account: account) }
let(:params) { { event: 'rephrase', payload: { test: 'test' } } }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post process_event_api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
params: params,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'will process the events' do
post process_event_api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq 'No processor found'
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/integrations/hooks/{hook_id}' do
let(:hook) { create(:integrations_hook, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'return unauthorized if agent' do
delete api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'updates hook if admin' do
delete api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(Integrations::Hook.exists?(hook.id)).to be false
end
end
end
end

View File

@@ -0,0 +1,330 @@
require 'rails_helper'
RSpec.describe 'Linear Integration API', type: :request do
let(:account) { create(:account) }
let(:user) { create(:user) }
let(:api_key) { 'valid_api_key' }
let(:agent) { create(:user, account: account, role: :agent) }
let(:processor_service) { instance_double(Integrations::Linear::ProcessorService) }
before do
create(:integrations_hook, :linear, account: account)
allow(Integrations::Linear::ProcessorService).to receive(:new).with(account: account).and_return(processor_service)
end
describe 'DELETE /api/v1/accounts/:account_id/integrations/linear' do
it 'deletes the linear integration' do
# Stub the HTTP call to Linear's revoke endpoint
allow(HTTParty).to receive(:post).with(
'https://api.linear.app/oauth/revoke',
anything
).and_return(instance_double(HTTParty::Response, success?: true))
delete "/api/v1/accounts/#{account.id}/integrations/linear",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(account.hooks.count).to eq(0)
end
end
describe 'GET /api/v1/accounts/:account_id/integrations/linear/teams' do
context 'when it is an authenticated user' do
context 'when data is retrieved successfully' do
let(:teams_data) { { data: [{ 'id' => 'team1', 'name' => 'Team One' }] } }
it 'returns team data' do
allow(processor_service).to receive(:teams).and_return(teams_data)
get "/api/v1/accounts/#{account.id}/integrations/linear/teams",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(response.body).to include('Team One')
end
end
context 'when data retrieval fails' do
it 'returns error message' do
allow(processor_service).to receive(:teams).and_return(error: 'error message')
get "/api/v1/accounts/#{account.id}/integrations/linear/teams",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).to include('error message')
end
end
end
end
describe 'GET /api/v1/accounts/:account_id/integrations/linear/team_entities' do
let(:team_id) { 'team1' }
context 'when it is an authenticated user' do
context 'when data is retrieved successfully' do
let(:team_entities_data) do
{ data: {
users: [{ 'id' => 'user1', 'name' => 'User One' }],
projects: [{ 'id' => 'project1', 'name' => 'Project One' }],
states: [{ 'id' => 'state1', 'name' => 'State One' }],
labels: [{ 'id' => 'label1', 'name' => 'Label One' }]
} }
end
it 'returns team entities data' do
allow(processor_service).to receive(:team_entities).with(team_id).and_return(team_entities_data)
get "/api/v1/accounts/#{account.id}/integrations/linear/team_entities",
params: { team_id: team_id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(response.body).to include('User One')
expect(response.body).to include('Project One')
expect(response.body).to include('State One')
expect(response.body).to include('Label One')
end
end
context 'when data retrieval fails' do
it 'returns error message' do
allow(processor_service).to receive(:team_entities).with(team_id).and_return(error: 'error message')
get "/api/v1/accounts/#{account.id}/integrations/linear/team_entities",
params: { team_id: team_id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).to include('error message')
end
end
end
end
describe 'POST /api/v1/accounts/:account_id/integrations/linear/create_issue' do
let(:inbox) { create(:inbox, account: account) }
let(:conversation) { create(:conversation, account: account, inbox: inbox) }
let(:issue_params) do
{
team_id: 'team1',
title: 'Sample Issue',
description: 'This is a sample issue.',
assignee_id: 'user1',
priority: 'high',
state_id: 'state1',
label_ids: ['label1'],
conversation_id: conversation.display_id
}
end
context 'when it is an authenticated user' do
context 'when the issue is created successfully' do
let(:created_issue) { { data: { identifier: 'ENG-123', title: 'Sample Issue' } } }
it 'returns the created issue' do
allow(processor_service).to receive(:create_issue).with(issue_params.stringify_keys, agent).and_return(created_issue)
post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue",
params: issue_params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(response.body).to include('Sample Issue')
end
it 'creates activity message when conversation is provided' do
allow(processor_service).to receive(:create_issue).with(issue_params.stringify_keys, agent).and_return(created_issue)
expect do
post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue",
params: issue_params,
headers: agent.create_new_auth_token,
as: :json
end.to have_enqueued_job(Conversations::ActivityMessageJob)
.with(conversation, {
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: :activity,
content: "Linear issue ENG-123 was created by #{agent.name}"
})
end
end
context 'when issue creation fails' do
it 'returns error message and does not create activity message' do
allow(processor_service).to receive(:create_issue).with(issue_params.stringify_keys, agent).and_return(error: 'error message')
expect do
post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue",
params: issue_params,
headers: agent.create_new_auth_token,
as: :json
end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).to include('error message')
end
end
end
end
describe 'POST /api/v1/accounts/:account_id/integrations/linear/link_issue' do
let(:issue_id) { 'ENG-456' }
let(:conversation) { create(:conversation, account: account) }
let(:link) { "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/conversations/#{conversation.display_id}" }
let(:title) { 'Sample Issue' }
context 'when it is an authenticated user' do
context 'when the issue is linked successfully' do
let(:linked_issue) { { data: { 'id' => 'issue1', 'link' => 'https://linear.app/issue1' } } }
it 'returns the linked issue and creates activity message' do
allow(processor_service).to receive(:link_issue).with(link, issue_id, title, agent).and_return(linked_issue)
expect do
post "/api/v1/accounts/#{account.id}/integrations/linear/link_issue",
params: { conversation_id: conversation.display_id, issue_id: issue_id, title: title },
headers: agent.create_new_auth_token,
as: :json
end.to have_enqueued_job(Conversations::ActivityMessageJob)
.with(conversation, {
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: :activity,
content: "Linear issue ENG-456 was linked by #{agent.name}"
})
expect(response).to have_http_status(:ok)
expect(response.body).to include('https://linear.app/issue1')
end
end
context 'when issue linking fails' do
it 'returns error message and does not create activity message' do
allow(processor_service).to receive(:link_issue).with(link, issue_id, title, agent).and_return(error: 'error message')
expect do
post "/api/v1/accounts/#{account.id}/integrations/linear/link_issue",
params: { conversation_id: conversation.display_id, issue_id: issue_id, title: title },
headers: agent.create_new_auth_token,
as: :json
end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).to include('error message')
end
end
end
end
describe 'POST /api/v1/accounts/:account_id/integrations/linear/unlink_issue' do
let(:link_id) { 'attachment1' }
let(:issue_id) { 'ENG-789' }
let(:conversation) { create(:conversation, account: account) }
context 'when it is an authenticated user' do
context 'when the issue is unlinked successfully' do
let(:unlinked_issue) { { data: { 'id' => 'issue1', 'link' => 'https://linear.app/issue1' } } }
it 'returns the unlinked issue and creates activity message' do
allow(processor_service).to receive(:unlink_issue).with(link_id).and_return(unlinked_issue)
expect do
post "/api/v1/accounts/#{account.id}/integrations/linear/unlink_issue",
params: { link_id: link_id, issue_id: issue_id, conversation_id: conversation.display_id },
headers: agent.create_new_auth_token,
as: :json
end.to have_enqueued_job(Conversations::ActivityMessageJob)
.with(conversation, {
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: :activity,
content: "Linear issue ENG-789 was unlinked by #{agent.name}"
})
expect(response).to have_http_status(:ok)
expect(response.body).to include('https://linear.app/issue1')
end
end
context 'when issue unlinking fails' do
it 'returns error message and does not create activity message' do
allow(processor_service).to receive(:unlink_issue).with(link_id).and_return(error: 'error message')
expect do
post "/api/v1/accounts/#{account.id}/integrations/linear/unlink_issue",
params: { link_id: link_id, issue_id: issue_id, conversation_id: conversation.display_id },
headers: agent.create_new_auth_token,
as: :json
end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).to include('error message')
end
end
end
end
describe 'GET /api/v1/accounts/:account_id/integrations/linear/search_issue' do
let(:term) { 'issue' }
context 'when it is an authenticated user' do
context 'when search is successful' do
let(:search_results) { { data: [{ 'id' => 'issue1', 'title' => 'Sample Issue' }] } }
it 'returns search results' do
allow(processor_service).to receive(:search_issue).with(term).and_return(search_results)
get "/api/v1/accounts/#{account.id}/integrations/linear/search_issue",
params: { q: term },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(response.body).to include('Sample Issue')
end
end
context 'when search fails' do
it 'returns error message' do
allow(processor_service).to receive(:search_issue).with(term).and_return(error: 'error message')
get "/api/v1/accounts/#{account.id}/integrations/linear/search_issue",
params: { q: term },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).to include('error message')
end
end
end
end
describe 'GET /api/v1/accounts/:account_id/integrations/linear/linked_issues' do
let(:conversation) { create(:conversation, account: account) }
let(:link) { "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/conversations/#{conversation.display_id}" }
context 'when it is an authenticated user' do
context 'when linked issue is found' do
let(:linked_issue) { { data: [{ 'id' => 'issue1', 'title' => 'Sample Issue' }] } }
it 'returns linked issue' do
allow(processor_service).to receive(:linked_issues).with(link).and_return(linked_issue)
get "/api/v1/accounts/#{account.id}/integrations/linear/linked_issues",
params: { conversation_id: conversation.display_id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(response.body).to include('Sample Issue')
end
end
context 'when linked issue is not found' do
it 'returns error message' do
allow(processor_service).to receive(:linked_issues).with(link).and_return(error: 'error message')
get "/api/v1/accounts/#{account.id}/integrations/linear/linked_issues",
params: { conversation_id: conversation.display_id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).to include('error message')
end
end
end
end
end

View File

@@ -0,0 +1,187 @@
require 'rails_helper'
# Stub class for ShopifyAPI response
class ShopifyAPIResponse
attr_reader :body
def initialize(body)
@body = body
end
end
RSpec.describe 'Shopify Integration API', type: :request do
let(:account) { create(:account) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:unauthorized_agent) { create(:user, account: account, role: :agent) }
let(:contact) { create(:contact, account: account, email: 'test@example.com', phone_number: '+1234567890') }
describe 'POST /api/v1/accounts/:account_id/integrations/shopify/auth' do
let(:shop_domain) { 'test-store.myshopify.com' }
context 'when it is an authenticated user' do
it 'returns a redirect URL for Shopify OAuth' do
post "/api/v1/accounts/#{account.id}/integrations/shopify/auth",
params: { shop_domain: shop_domain },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(response.parsed_body).to have_key('redirect_url')
expect(response.parsed_body['redirect_url']).to include(shop_domain)
end
it 'returns error when shop domain is missing' do
post "/api/v1/accounts/#{account.id}/integrations/shopify/auth",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq('Shop domain is required')
end
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/integrations/shopify/auth",
params: { shop_domain: shop_domain },
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'GET /api/v1/accounts/:account_id/integrations/shopify/orders' do
before do
create(:integrations_hook, :shopify, account: account)
end
context 'when it is an authenticated user' do
# rubocop:disable RSpec/AnyInstance
let(:shopify_client) { instance_double(ShopifyAPI::Clients::Rest::Admin) }
let(:customers_response) do
instance_double(
ShopifyAPIResponse,
body: { 'customers' => [{ 'id' => '123' }] }
)
end
let(:orders_response) do
instance_double(
ShopifyAPIResponse,
body: {
'orders' => [{
'id' => '456',
'email' => 'test@example.com',
'created_at' => Time.now.iso8601,
'total_price' => '100.00',
'currency' => 'USD',
'fulfillment_status' => 'fulfilled',
'financial_status' => 'paid'
}]
}
)
end
before do
allow_any_instance_of(Api::V1::Accounts::Integrations::ShopifyController).to receive(:shopify_client).and_return(shopify_client)
allow_any_instance_of(Api::V1::Accounts::Integrations::ShopifyController).to receive(:client_id).and_return('test_client_id')
allow_any_instance_of(Api::V1::Accounts::Integrations::ShopifyController).to receive(:client_secret).and_return('test_client_secret')
allow(shopify_client).to receive(:get).with(
path: 'customers/search.json',
query: { query: "email:#{contact.email} OR phone:#{contact.phone_number}", fields: 'id,email,phone' }
).and_return(customers_response)
allow(shopify_client).to receive(:get).with(
path: 'orders.json',
query: { customer_id: '123', status: 'any', fields: 'id,email,created_at,total_price,currency,fulfillment_status,financial_status' }
).and_return(orders_response)
end
it 'returns orders for the contact' do
get "/api/v1/accounts/#{account.id}/integrations/shopify/orders",
params: { contact_id: contact.id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(response.parsed_body).to have_key('orders')
expect(response.parsed_body['orders'].length).to eq(1)
expect(response.parsed_body['orders'][0]['id']).to eq('456')
end
it 'returns error when contact has no email or phone' do
contact_without_info = create(:contact, account: account)
get "/api/v1/accounts/#{account.id}/integrations/shopify/orders",
params: { contact_id: contact_without_info.id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq('Contact information missing')
end
it 'returns empty array when no customers found' do
empty_customers_response = instance_double(
ShopifyAPIResponse,
body: { 'customers' => [] }
)
allow(shopify_client).to receive(:get).with(
path: 'customers/search.json',
query: { query: "email:#{contact.email} OR phone:#{contact.phone_number}", fields: 'id,email,phone' }
).and_return(empty_customers_response)
get "/api/v1/accounts/#{account.id}/integrations/shopify/orders",
params: { contact_id: contact.id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(response.parsed_body['orders']).to eq([])
end
# rubocop:enable RSpec/AnyInstance
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/integrations/shopify/orders",
params: { contact_id: contact.id },
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'DELETE /api/v1/accounts/:account_id/integrations/shopify' do
before do
create(:integrations_hook, :shopify, account: account)
end
context 'when it is an authenticated user' do
it 'deletes the shopify integration' do
expect do
delete "/api/v1/accounts/#{account.id}/integrations/shopify",
headers: agent.create_new_auth_token,
as: :json
end.to change { account.hooks.count }.by(-1)
expect(response).to have_http_status(:ok)
end
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/integrations/shopify",
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
end

View File

@@ -0,0 +1,104 @@
require 'rails_helper'
RSpec.describe 'Label API', type: :request do
let!(:account) { create(:account) }
let!(:label) { create(:label, account: account) }
describe 'GET /api/v1/accounts/{account.id}/labels' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/labels"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :administrator) }
it 'returns all the labels in account' do
get "/api/v1/accounts/#{account.id}/labels",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(label.title)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/labels/:id' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/labels/#{label.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) }
it 'shows the contact' do
get "/api/v1/accounts/#{account.id}/labels/#{label.id}",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(label.title)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/labels' do
let(:valid_params) { { label: { title: 'test' } } }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
expect { post "/api/v1/accounts/#{account.id}/labels", params: valid_params }.not_to change(Label, :count)
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 'creates the contact' do
expect do
post "/api/v1/accounts/#{account.id}/labels", headers: admin.create_new_auth_token,
params: valid_params
end.to change(Label, :count).by(1)
expect(response).to have_http_status(:success)
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/labels/:id' do
let(:valid_params) { { title: 'Test_2' } }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/labels/#{label.id}",
params: valid_params
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 'updates the label' do
patch "/api/v1/accounts/#{account.id}/labels/#{label.id}",
headers: admin.create_new_auth_token,
params: valid_params,
as: :json
expect(response).to have_http_status(:success)
expect(label.reload.title).to eq('test_2')
end
end
end
end

View File

@@ -0,0 +1,548 @@
require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::MacrosController', type: :request do
include ActiveJob::TestHelper
let(:account) { create(:account) }
let(:administrator) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:agent_1) { create(:user, account: account, role: :agent) }
before do
create(:macro, account: account, created_by: administrator, updated_by: administrator, visibility: :global)
create(:macro, account: account, created_by: administrator, updated_by: administrator, visibility: :global)
create(:macro, account: account, created_by: administrator, updated_by: administrator, visibility: :personal)
create(:macro, account: account, created_by: agent, updated_by: agent, visibility: :personal)
create(:macro, account: account, created_by: agent, updated_by: agent, visibility: :personal)
create(:macro, account: account, created_by: agent_1, updated_by: agent_1, visibility: :personal)
end
describe 'GET /api/v1/accounts/{account.id}/macros' do
context 'when it is an authenticated administrator' do
it 'returns all records in the account' do
get "/api/v1/accounts/#{account.id}/macros",
headers: administrator.create_new_auth_token
visible_macros = account.macros.global.or(account.macros.personal.where(created_by_id: administrator.id)).order(:id)
expect(response).to have_http_status(:success)
body = response.parsed_body
expect(body['payload'].length).to eq(visible_macros.count)
expect(body['payload'].first['id']).to eq(visible_macros.first.id)
expect(body['payload'].last['id']).to eq(visible_macros.last.id)
end
end
context 'when it is an authenticated agent' do
it 'returns all records in account and created_by the agent' do
get "/api/v1/accounts/#{account.id}/macros",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:success)
body = response.parsed_body
visible_macros = account.macros.global.or(account.macros.personal.where(created_by_id: agent.id)).order(:id)
expect(body['payload'].length).to eq(visible_macros.count)
expect(body['payload'].first['id']).to eq(visible_macros.first.id)
expect(body['payload'].last['id']).to eq(visible_macros.last.id)
end
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/macros"
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/macros' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/macros"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:params) do
{
'name': 'Add label, send message and close the chat, remove label',
'actions': [
{
'action_name': :add_label,
'action_params': %w[support priority_customer]
},
{
'action_name': :remove_assigned_team
},
{
'action_name': :send_message,
'action_params': ['Welcome to the chatwoot platform.']
},
{
'action_name': :resolve_conversation
},
{
'action_name': :remove_label,
'action_params': %w[support]
}
],
visibility: 'global',
created_by_id: administrator.id
}.with_indifferent_access
end
it 'creates the macro' do
post "/api/v1/accounts/#{account.id}/macros",
params: params,
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload']['name']).to eql(params['name'])
expect(json_response['payload']['visibility']).to eql(params['visibility'])
expect(json_response['payload']['created_by']['id']).to eql(administrator.id)
end
it 'sets visibility default to personal for agent' do
post "/api/v1/accounts/#{account.id}/macros",
params: params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload']['name']).to eql(params['name'])
expect(json_response['payload']['visibility']).to eql('personal')
expect(json_response['payload']['created_by']['id']).to eql(agent.id)
end
it 'Saves file in the macros actions to send an attachments' do
blob = ActiveStorage::Blob.create_and_upload!(
io: Rails.root.join('spec/assets/avatar.png').open,
filename: 'avatar.png',
content_type: 'image/png'
)
params[:actions] = [
{
'action_name': :send_message,
'action_params': ['Welcome to the chatwoot platform.']
},
{
'action_name': :send_attachment,
'action_params': [blob.signed_id]
}
]
post "/api/v1/accounts/#{account.id}/macros",
headers: administrator.create_new_auth_token,
params: params
macro = account.macros.last
expect(macro.files.presence).to be_truthy
expect(macro.files.count).to eq(1)
end
it 'returns error for invalid attachment blob_id' do
params[:actions] = [
{
'action_name': :send_attachment,
'action_params': ['invalid_blob_id']
}
]
post "/api/v1/accounts/#{account.id}/macros",
headers: administrator.create_new_auth_token,
params: params
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq(I18n.t('errors.attachments.invalid'))
end
it 'stores the original blob_id in action_params after create' do
blob = ActiveStorage::Blob.create_and_upload!(
io: Rails.root.join('spec/assets/avatar.png').open,
filename: 'avatar.png',
content_type: 'image/png'
)
params[:actions] = [
{
'action_name': :send_attachment,
'action_params': [blob.signed_id]
}
]
post "/api/v1/accounts/#{account.id}/macros",
headers: administrator.create_new_auth_token,
params: params
macro = account.macros.last
attachment_action = macro.actions.find { |a| a['action_name'] == 'send_attachment' }
expect(attachment_action['action_params'].first).to be_a(Integer)
expect(attachment_action['action_params'].first).to eq(macro.files.first.blob_id)
end
end
end
describe 'PUT /api/v1/accounts/{account.id}/macros/{macro.id}' do
let!(:macro) { create(:macro, account: account, created_by: administrator, updated_by: administrator) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/macros/#{macro.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:params) do
{
'name': 'Add label, send message and close the chat'
}
end
it 'Updates the macro' do
put "/api/v1/accounts/#{account.id}/macros/#{macro.id}",
params: params,
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['name']).to eql(params['name'])
end
it 'Unauthorize to update the macro' do
macro = create(:macro, account: account, created_by: agent, updated_by: agent)
put "/api/v1/accounts/#{account.id}/macros/#{macro.id}",
params: params,
headers: agent_1.create_new_auth_token
json_response = response.parsed_body
expect(response).to have_http_status(:unauthorized)
expect(json_response['error']).to eq('You are not authorized to do this action')
end
it 'allows update with existing blob_id' do
blob = ActiveStorage::Blob.create_and_upload!(
io: Rails.root.join('spec/assets/avatar.png').open,
filename: 'avatar.png',
content_type: 'image/png'
)
macro.update!(actions: [{ 'action_name' => 'send_attachment', 'action_params' => [blob.id] }])
macro.files.attach(blob)
put "/api/v1/accounts/#{account.id}/macros/#{macro.id}",
params: { actions: [{ 'action_name': :send_attachment, 'action_params': [blob.id] }] },
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
end
it 'returns error for invalid blob_id on update' do
put "/api/v1/accounts/#{account.id}/macros/#{macro.id}",
params: { actions: [{ 'action_name': :send_attachment, 'action_params': [999_999] }] },
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq(I18n.t('errors.attachments.invalid'))
end
it 'allows adding new attachment on update with signed blob_id' do
blob = ActiveStorage::Blob.create_and_upload!(
io: Rails.root.join('spec/assets/avatar.png').open,
filename: 'avatar.png',
content_type: 'image/png'
)
put "/api/v1/accounts/#{account.id}/macros/#{macro.id}",
params: { actions: [{ 'action_name': :send_attachment, 'action_params': [blob.signed_id] }] },
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
expect(macro.reload.files.count).to eq(1)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/macros/{macro.id}' do
let!(:macro) { create(:macro, account: account, created_by: administrator, updated_by: administrator) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/macros/#{macro.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'fetch the macro' do
get "/api/v1/accounts/#{account.id}/macros/#{macro.id}",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload']['name']).to eql(macro.name)
expect(json_response['payload']['created_by']['id']).to eql(administrator.id)
end
it 'return not_found status when macros not available' do
get "/api/v1/accounts/#{account.id}/macros/15",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:not_found)
end
it 'Unauthorize to fetch other agents private macro' do
macro = create(:macro, account: account, created_by: agent, updated_by: agent, visibility: :personal)
get "/api/v1/accounts/#{account.id}/macros/#{macro.id}",
headers: agent_1.create_new_auth_token
json_response = response.parsed_body
expect(response).to have_http_status(:unauthorized)
expect(json_response['error']).to eq('You are not authorized to do this action')
end
it 'authorize to fetch other agents public macro' do
macro = create(:macro, account: account, created_by: agent, updated_by: agent, visibility: :global)
get "/api/v1/accounts/#{account.id}/macros/#{macro.id}",
headers: agent_1.create_new_auth_token
expect(response).to have_http_status(:success)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/macros/{macro.id}/execute' do
let!(:macro) { create(:macro, account: account, created_by: administrator, updated_by: administrator) }
let(:inbox) { create(:inbox, account: account) }
let(:contact) { create(:contact, account: account, identifier: '123') }
let(:conversation) { create(:conversation, inbox: inbox, account: account, status: :open) }
let(:team) { create(:team, account: account) }
let(:user_1) { create(:user, role: 0) }
before do
create(:team_member, user: user_1, team: team)
create(:account_user, user: user_1, account: account)
create(:inbox_member, user: user_1, inbox: inbox)
macro.update!(actions:
[
{ 'action_name' => 'assign_team', 'action_params' => [team.id] },
{ 'action_name' => 'add_label', 'action_params' => %w[support priority_customer] },
{ 'action_name' => 'snooze_conversation' },
{ 'action_name' => 'assign_agent', 'action_params' => [user_1.id] },
{ 'action_name' => 'send_message', 'action_params' => ['Send this message.'] },
{ 'action_name' => 'add_private_note', :action_params => ['We are sending greeting message to customer.'] }
])
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/macros/#{macro.id}/execute"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
context 'when execute the macro' do
it 'send the message with sender' do
expect(conversation.messages).to be_empty
perform_enqueued_jobs do
post "/api/v1/accounts/#{account.id}/macros/#{macro.id}/execute",
params: { conversation_ids: [conversation.display_id] },
headers: administrator.create_new_auth_token
end
expect(conversation.messages.chat.last.content).to eq('Send this message.')
expect(conversation.messages.chat.last.sender).to eq(administrator)
end
it 'Assign the agent when he is inbox member' do
expect(conversation.assignee).to be_nil
perform_enqueued_jobs do
post "/api/v1/accounts/#{account.id}/macros/#{macro.id}/execute",
params: { conversation_ids: [conversation.display_id] },
headers: administrator.create_new_auth_token
end
expect(conversation.messages.activity.last.content).to eq("Assigned to #{user_1.name} by #{administrator.name}")
end
it 'Assign the agent when he is not inbox member' do
InboxMember.last.destroy
expect(conversation.assignee).to be_nil
perform_enqueued_jobs do
post "/api/v1/accounts/#{account.id}/macros/#{macro.id}/execute",
params: { conversation_ids: [conversation.display_id] },
headers: administrator.create_new_auth_token
end
expect(conversation.messages.activity.last.content).not_to eq("Assigned to #{user_1.name} by #{administrator.name}")
end
it 'Assign the labels' do
expect(conversation.labels).to be_empty
perform_enqueued_jobs do
post "/api/v1/accounts/#{account.id}/macros/#{macro.id}/execute",
params: { conversation_ids: [conversation.display_id] },
headers: administrator.create_new_auth_token
end
expect(conversation.reload.label_list).to match_array(%w[support priority_customer])
end
it 'Update the status' do
expect(conversation.reload.status).to eql('open')
perform_enqueued_jobs do
post "/api/v1/accounts/#{account.id}/macros/#{macro.id}/execute",
params: { conversation_ids: [conversation.display_id] },
headers: administrator.create_new_auth_token
end
expect(conversation.reload.status).to eql('snoozed')
end
it 'Remove selected label' do
macro.update!(actions: [{ 'action_name' => 'remove_label', 'action_params' => ['support'] }])
conversation.add_labels(%w[support priority_customer])
expect(conversation.label_list).to match_array(%w[support priority_customer])
perform_enqueued_jobs do
post "/api/v1/accounts/#{account.id}/macros/#{macro.id}/execute",
params: { conversation_ids: [conversation.display_id] },
headers: administrator.create_new_auth_token
end
expect(conversation.reload.label_list).to match_array(%w[priority_customer])
end
it 'Adds the private note' do
expect(conversation.messages).to be_empty
perform_enqueued_jobs do
post "/api/v1/accounts/#{account.id}/macros/#{macro.id}/execute",
params: { conversation_ids: [conversation.display_id] },
headers: administrator.create_new_auth_token
end
expect(conversation.messages.last.content).to eq('We are sending greeting message to customer.')
expect(conversation.messages.last.sender).to eq(administrator)
expect(conversation.messages.last.private).to be_truthy
end
it 'Assign the team if team_ids are present' do
expect(conversation.team).to be_nil
perform_enqueued_jobs do
post "/api/v1/accounts/#{account.id}/macros/#{macro.id}/execute",
params: { conversation_ids: [conversation.display_id] },
headers: administrator.create_new_auth_token
end
expect(conversation.reload.team_id).to eq(team.id)
end
it 'Unassign the team' do
macro.update!(actions: [
{ 'action_name' => 'remove_assigned_team' }
])
conversation.update!(team_id: team.id)
expect(conversation.reload.team).not_to be_nil
perform_enqueued_jobs do
post "/api/v1/accounts/#{account.id}/macros/#{macro.id}/execute",
params: { conversation_ids: [conversation.display_id] },
headers: administrator.create_new_auth_token
end
expect(conversation.reload.team_id).to be_nil
end
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/macros/{macro.id}' do
let!(:macro) { create(:macro, account: account, created_by: administrator, updated_by: administrator) }
context 'when it is an authenticated user' do
it 'Deletes the macro' do
delete "/api/v1/accounts/#{account.id}/macros/#{macro.id}",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
end
it 'deletes the orphan public record with admin credentials' do
macro = create(:macro, account: account, created_by: agent, updated_by: agent, visibility: :global)
expect(macro.created_by).to eq(agent)
agent.destroy!
expect(macro.reload.created_by).to be_nil
delete "/api/v1/accounts/#{account.id}/macros/#{macro.id}",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
end
it 'can not delete orphan public record with agent credentials' do
macro = create(:macro, account: account, created_by: agent, updated_by: agent, visibility: :global)
expect(macro.created_by).to eq(agent)
agent.destroy!
expect(macro.reload.created_by).to be_nil
delete "/api/v1/accounts/#{account.id}/macros/#{macro.id}",
headers: agent_1.create_new_auth_token
json_response = response.parsed_body
expect(response).to have_http_status(:unauthorized)
expect(json_response['error']).to eq('You are not authorized to do this action')
end
it 'Unauthorize to delete the macro' do
macro = create(:macro, account: account, created_by: agent, updated_by: agent)
delete "/api/v1/accounts/#{account.id}/macros/#{macro.id}",
headers: agent_1.create_new_auth_token
json_response = response.parsed_body
expect(response).to have_http_status(:unauthorized)
expect(json_response['error']).to eq('You are not authorized to do this action')
end
end
end
end

View File

@@ -0,0 +1,54 @@
require 'rails_helper'
RSpec.describe 'Microsoft Authorization API', type: :request do
let(:account) { create(:account) }
describe 'POST /api/v1/accounts/{account.id}/microsoft/authorization' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/microsoft/authorization"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
it 'returns unathorized for agent' do
post "/api/v1/accounts/#{account.id}/microsoft/authorization",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'creates a new authorization and returns the redirect url' do
post "/api/v1/accounts/#{account.id}/microsoft/authorization",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
# Validate URL components
url = response.parsed_body['url']
uri = URI.parse(url)
params = CGI.parse(uri.query)
expect(url).to start_with('https://login.microsoftonline.com/common/oauth2/v2.0/authorize')
expected_scope = [
'offline_access https://outlook.office.com/IMAP.AccessAsUser.All ' \
'https://outlook.office.com/SMTP.Send openid profile email'
]
expect(params['scope']).to eq(expected_scope)
expect(params['redirect_uri']).to eq(["#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback"])
# Validate state parameter exists and can be decoded back to the account
expect(params['state']).to be_present
decoded_account = GlobalID::Locator.locate_signed(params['state'].first, for: 'default')
expect(decoded_account).to eq(account)
end
end
end
end

View File

@@ -0,0 +1,58 @@
require 'rails_helper'
RSpec.describe 'Notification Settings API', type: :request do
let(:account) { create(:account) }
describe 'GET /api/v1/accounts/{account.id}/notification_settings' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/notification_settings"
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) }
it 'returns current user notification settings' do
get "/api/v1/accounts/#{account.id}/notification_settings",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['user_id']).to eq(agent.id)
expect(json_response['account_id']).to eq(account.id)
end
end
end
describe 'PUT /api/v1/accounts/{account.id}/notification_settings' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/notification_settings"
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) }
it 'updates the email related notification flags' do
put "/api/v1/accounts/#{account.id}/notification_settings",
params: { notification_settings: { selected_email_flags: ['email_conversation_assignment'] } },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
agent.reload
expect(json_response['user_id']).to eq(agent.id)
expect(json_response['account_id']).to eq(account.id)
expect(json_response['selected_email_flags']).to eq(['email_conversation_assignment'])
end
end
end
end

View File

@@ -0,0 +1,251 @@
require 'rails_helper'
RSpec.describe 'Notifications API', type: :request do
let(:account) { create(:account) }
describe 'GET /api/v1/accounts/{account.id}/notifications' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/notifications"
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!(:notification1) { create(:notification, account: account, user: admin) }
let!(:notification2) { create(:notification, account: account, user: admin) }
it 'returns all notifications' do
get "/api/v1/accounts/#{account.id}/notifications",
headers: admin.create_new_auth_token,
as: :json
response_json = response.parsed_body
expect(response).to have_http_status(:success)
expect(response.body).to include(notification1.notification_type)
expect(response_json['data']['meta']['unread_count']).to eq 2
expect(response_json['data']['meta']['count']).to eq 2
# notification appear in descending order
expect(response_json['data']['payload'].first['id']).to eq notification2.id
expect(response_json['data']['payload'].first['primary_actor']).not_to be_nil
end
end
end
describe 'POST /api/v1/accounts/{account.id}/notifications/read_all' do
let(:admin) { create(:user, account: account, role: :administrator) }
let!(:notification1) { create(:notification, account: account, user: admin) }
let!(:notification2) { create(:notification, account: account, user: admin) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/notifications/read_all"
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 'updates all the notifications read at' do
post "/api/v1/accounts/#{account.id}/notifications/read_all",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(notification1.reload.read_at).not_to eq('')
expect(notification2.reload.read_at).not_to eq('')
end
it 'updates only the notifications read at for primary actor when param is passed' do
post "/api/v1/accounts/#{account.id}/notifications/read_all",
headers: admin.create_new_auth_token,
params: {
primary_actor_id: notification1.primary_actor_id,
primary_actor_type: notification1.primary_actor_type
},
as: :json
expect(response).to have_http_status(:success)
expect(notification1.reload.read_at).not_to eq('')
expect(notification2.reload.read_at).to be_nil
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/notifications/:id' do
let(:admin) { create(:user, account: account, role: :administrator) }
let!(:notification) { create(:notification, account: account, user: admin) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/notifications/#{notification.id}",
params: { read_at: true }
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 'updates the notification read at' do
patch "/api/v1/accounts/#{account.id}/notifications/#{notification.id}",
headers: admin.create_new_auth_token,
params: { read_at: true },
as: :json
expect(response).to have_http_status(:success)
expect(notification.reload.read_at).not_to eq('')
end
end
end
describe 'GET /api/v1/accounts/{account.id}/notifications/unread_count' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/notifications/unread_count"
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 notifications unread count' do
2.times.each { create(:notification, account: account, user: admin) }
get "/api/v1/accounts/#{account.id}/notifications/unread_count",
headers: admin.create_new_auth_token,
as: :json
response_json = response.parsed_body
expect(response).to have_http_status(:success)
expect(response_json).to eq 2
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/notifications/:id' do
let(:admin) { create(:user, account: account, role: :administrator) }
let!(:notification) { create(:notification, account: account, user: admin) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/notifications/#{notification.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) }
it 'deletes the notification' do
delete "/api/v1/accounts/#{account.id}/notifications/#{notification.id}",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(Notification.count).to eq(0)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/notifications/:id/snooze' do
let(:admin) { create(:user, account: account, role: :administrator) }
let!(:notification) { create(:notification, account: account, user: admin) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/notifications/#{notification.id}/snooze",
params: { snoozed_until: DateTime.now.utc + 1.day }
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 'updates the notification snoozed until' do
post "/api/v1/accounts/#{account.id}/notifications/#{notification.id}/snooze",
headers: admin.create_new_auth_token,
params: { snoozed_until: DateTime.now.utc + 1.day },
as: :json
expect(response).to have_http_status(:success)
expect(notification.reload.snoozed_until).not_to eq('')
expect(notification.reload.meta['last_snoozed_at']).to be_nil
end
end
end
describe 'POST /api/v1/accounts/{account.id}/notifications/:id/unread' do
let(:admin) { create(:user, account: account, role: :administrator) }
let!(:notification) { create(:notification, account: account, user: admin) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/notifications/#{notification.id}/unread"
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 'updates the notification read at' do
post "/api/v1/accounts/#{account.id}/notifications/#{notification.id}/unread",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(notification.reload.read_at).to be_nil
end
end
end
describe 'POST /api/v1/accounts/{account.id}/notifications/destroy_all' do
let(:admin) { create(:user, account: account, role: :administrator) }
let(:notification1) { create(:notification, account: account, user: admin) }
let(:notification2) { create(:notification, account: account, user: admin) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/notifications/destroy_all"
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 'deletes all the read notifications' do
expect(Notification::DeleteNotificationJob).to receive(:perform_later).with(admin, type: :read)
post "/api/v1/accounts/#{account.id}/notifications/destroy_all",
headers: admin.create_new_auth_token,
params: { type: 'read' },
as: :json
expect(response).to have_http_status(:success)
end
it 'deletes all the notifications' do
expect(Notification::DeleteNotificationJob).to receive(:perform_later).with(admin, type: :all)
post "/api/v1/accounts/#{account.id}/notifications/destroy_all",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
end
end
end
end

View File

@@ -0,0 +1,53 @@
require 'rails_helper'
RSpec.describe 'Notion Authorization API', type: :request do
let(:account) { create(:account) }
describe 'POST /api/v1/accounts/{account.id}/notion/authorization' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/notion/authorization"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
it 'returns unauthorized for agent' do
post "/api/v1/accounts/#{account.id}/notion/authorization",
headers: agent.create_new_auth_token,
params: { email: administrator.email },
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'creates a new authorization and returns the redirect url' do
post "/api/v1/accounts/#{account.id}/notion/authorization",
headers: administrator.create_new_auth_token,
params: { email: administrator.email },
as: :json
expect(response).to have_http_status(:success)
# Validate URL components
url = response.parsed_body['url']
uri = URI.parse(url)
params = CGI.parse(uri.query)
expect(url).to start_with('https://api.notion.com/v1/oauth/authorize')
expect(params['response_type']).to eq(['code'])
expect(params['owner']).to eq(['user'])
expect(params['redirect_uri']).to eq(["#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/notion/callback"])
# Validate state parameter exists and can be decoded back to the account
expect(params['state']).to be_present
decoded_account = GlobalID::Locator.locate_signed(params['state'].first, for: 'default')
expect(decoded_account).to eq(account)
end
end
end
end

View File

@@ -0,0 +1,304 @@
require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::Portals', type: :request do
let(:account) { create(:account) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent_1) { create(:user, account: account, role: :agent) }
let(:agent_2) { create(:user, account: account, role: :agent) }
let!(:portal) { create(:portal, slug: 'portal-1', name: 'test_portal', account_id: account.id) }
describe 'GET /api/v1/accounts/{account.id}/portals' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/portals"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'get all portals' do
portal2 = create(:portal, name: 'test_portal_2', account_id: account.id, slug: 'portal-2')
expect(portal2.id).not_to be_nil
get "/api/v1/accounts/#{account.id}/portals",
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload'].length).to be 2
expect(json_response['payload'][0]['id']).to be portal.id
end
end
end
describe 'GET /api/v1/accounts/{account.id}/portals/{portal.slug}' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/portals"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'get one portals' do
get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}",
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['name']).to eq portal.name
expect(json_response['meta']['all_articles_count']).to eq 0
end
it 'returns portal articles metadata' do
portal.update(config: { allowed_locales: %w[en es], default_locale: 'en' })
en_cat = create(:category, locale: :en, portal_id: portal.id, slug: 'en-cat')
es_cat = create(:category, locale: :es, portal_id: portal.id, slug: 'es-cat')
create(:article, category_id: en_cat.id, portal_id: portal.id, author_id: agent.id)
create(:article, category_id: en_cat.id, portal_id: portal.id, author_id: admin.id)
create(:article, category_id: es_cat.id, portal_id: portal.id, author_id: agent.id)
get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}?locale=en",
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['name']).to eq portal.name
expect(json_response['meta']['all_articles_count']).to eq 2
expect(json_response['meta']['mine_articles_count']).to eq 1
end
end
end
describe 'POST /api/v1/accounts/{account.id}/portals' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/portals",
params: {},
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'creates portal' do
portal_params = {
portal: {
name: 'test_portal',
slug: 'test_kbase',
custom_domain: 'https://support.chatwoot.dev'
}
}
post "/api/v1/accounts/#{account.id}/portals",
params: portal_params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['name']).to eql('test_portal')
expect(json_response['custom_domain']).to eql('support.chatwoot.dev')
end
end
end
describe 'PUT /api/v1/accounts/{account.id}/portals/{portal.slug}' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}", params: {}
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'updates portal' do
portal_params = {
portal: {
name: 'updated_test_portal',
config: { 'allowed_locales' => %w[en es] }
}
}
expect(portal.name).to eql('test_portal')
put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}",
params: portal_params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['name']).to eql(portal_params[:portal][:name])
expect(json_response['config']).to eql({ 'allowed_locales' => [{ 'articles_count' => 0, 'categories_count' => 0, 'code' => 'en' },
{ 'articles_count' => 0, 'categories_count' => 0, 'code' => 'es' }] })
end
it 'archive portal' do
portal_params = {
portal: {
archived: true
}
}
expect(portal.archived).to be_falsy
put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}",
params: portal_params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['archived']).to eql(portal_params[:portal][:archived])
portal.reload
expect(portal.archived).to be_truthy
end
it 'clears associated web widget when inbox selection is blank' do
web_widget_inbox = create(:inbox, account: account)
portal.update!(channel_web_widget: web_widget_inbox.channel)
expect(portal.channel_web_widget_id).to eq(web_widget_inbox.channel.id)
put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}",
params: {
portal: { name: portal.name },
inbox_id: ''
},
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
portal.reload
expect(portal.channel_web_widget_id).to be_nil
expect(response.parsed_body['inbox']).to be_nil
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/portals/{portal.slug}' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/portals/#{portal.slug}", params: {}
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'deletes portal' do
delete "/api/v1/accounts/#{account.id}/portals/#{portal.slug}",
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
deleted_portal = Portal.find_by(id: portal.slug)
expect(deleted_portal).to be_nil
end
end
end
# Portal members endpoint removed
describe 'DELETE /api/v1/accounts/{account.id}/portals/{portal.slug}/logo' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/logo"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
before do
portal.logo.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
end
it 'throw error if agent' do
delete "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/logo",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'delete portal logo if admin' do
delete "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/logo",
headers: admin.create_new_auth_token,
as: :json
expect { portal.logo.attachment.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect(response).to have_http_status(:success)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/portals/{portal.slug}/send_instructions' 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
post "/api/v1/accounts/#{account.id}/portals/#{portal_with_domain.slug}/send_instructions",
params: { email: 'dev@example.com' }
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated agent' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/portals/#{portal_with_domain.slug}/send_instructions",
headers: agent.create_new_auth_token,
params: { email: 'dev@example.com' },
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated admin' do
it 'returns error when email is missing' do
post "/api/v1/accounts/#{account.id}/portals/#{portal_with_domain.slug}/send_instructions",
headers: admin.create_new_auth_token,
params: {},
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq('Email is required')
end
it 'returns error when email is invalid' do
post "/api/v1/accounts/#{account.id}/portals/#{portal_with_domain.slug}/send_instructions",
headers: admin.create_new_auth_token,
params: { email: 'invalid-email' },
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq('Invalid email format')
end
it 'returns error when custom domain is not configured' do
post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/send_instructions",
headers: admin.create_new_auth_token,
params: { email: 'dev@example.com' },
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq('Custom domain is not configured')
end
it 'sends instructions successfully' do
mailer_double = instance_double(ActionMailer::MessageDelivery)
allow(PortalInstructionsMailer).to receive(:send_cname_instructions).and_return(mailer_double)
allow(mailer_double).to receive(:deliver_later)
post "/api/v1/accounts/#{account.id}/portals/#{portal_with_domain.slug}/send_instructions",
headers: admin.create_new_auth_token,
params: { email: 'dev@example.com' },
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['message']).to eq('Instructions sent successfully')
expect(PortalInstructionsMailer).to have_received(:send_cname_instructions)
.with(portal: portal_with_domain, recipient_email: 'dev@example.com')
end
end
end
end

View File

@@ -0,0 +1,420 @@
require 'rails_helper'
RSpec.describe 'Search', type: :request do
let(:account) { create(:account) }
let(:agent) { create(:user, account: account, role: :agent) }
before do
contact = create(:contact, email: 'test@example.com', account: account)
conversation = create(:conversation, account: account, contact_id: contact.id)
create(:message, conversation: conversation, account: account, content: 'test1')
create(:message, conversation: conversation, account: account, content: 'test2')
create(:contact_inbox, contact_id: contact.id, inbox_id: conversation.inbox.id)
create(:inbox_member, user: agent, inbox: conversation.inbox)
# Create articles for testing
portal = create(:portal, account: account)
create(:article, title: 'Test Article Guide', content: 'This is a test article content',
account: account, portal: portal, author: agent, status: 'published')
end
describe 'GET /api/v1/accounts/{account.id}/search' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/search", params: { q: 'test' }
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'returns all conversations with messages containing the search query' do
get "/api/v1/accounts/#{account.id}/search",
headers: agent.create_new_auth_token,
params: { q: 'test' },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data[:payload][:messages].first[:content]).to eq 'test2'
expect(response_data[:payload].keys).to contain_exactly(:contacts, :conversations, :messages, :articles)
expect(response_data[:payload][:messages].length).to eq 2
expect(response_data[:payload][:conversations].length).to eq 1
expect(response_data[:payload][:contacts].length).to eq 1
expect(response_data[:payload][:articles].length).to eq 1
end
end
end
describe 'GET /api/v1/accounts/{account.id}/search/contacts' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/search/contacts", params: { q: 'test' }
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'returns all conversations with messages containing the search query' do
get "/api/v1/accounts/#{account.id}/search/contacts",
headers: agent.create_new_auth_token,
params: { q: 'test' },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data[:payload].keys).to contain_exactly(:contacts)
expect(response_data[:payload][:contacts].length).to eq 1
end
it 'returns last_activity_at in contact search results' do
contact = create(:contact, email: 'activity@test.com', account: account, last_activity_at: 3.days.ago)
get "/api/v1/accounts/#{account.id}/search/contacts",
headers: agent.create_new_auth_token,
params: { q: 'activity' },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
contact_result = response_data[:payload][:contacts].first
expect(contact_result[:last_activity_at]).to eq(contact.last_activity_at.to_i)
expect(contact_result).not_to have_key(:created_at)
end
context 'with advanced_search feature enabled', :opensearch do
before do
account.enable_features!('advanced_search')
end
it 'filters contacts by since parameter' do
create(:contact, email: 'old@test.com', account: account, last_activity_at: 10.days.ago)
create(:contact, email: 'recent@test.com', account: account, last_activity_at: 2.days.ago)
get "/api/v1/accounts/#{account.id}/search/contacts",
headers: agent.create_new_auth_token,
params: { q: 'test', since: 5.days.ago.to_i },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
contact_emails = response_data[:payload][:contacts].pluck(:email)
expect(contact_emails).to include('recent@test.com')
expect(contact_emails).not_to include('old@test.com')
end
it 'filters contacts by until parameter' do
create(:contact, email: 'old@test.com', account: account, last_activity_at: 10.days.ago)
create(:contact, email: 'recent@test.com', account: account, last_activity_at: 2.days.ago)
get "/api/v1/accounts/#{account.id}/search/contacts",
headers: agent.create_new_auth_token,
params: { q: 'test', until: 5.days.ago.to_i },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
contact_emails = response_data[:payload][:contacts].pluck(:email)
expect(contact_emails).to include('old@test.com')
expect(contact_emails).not_to include('recent@test.com')
end
it 'filters contacts by both since and until parameters' do
create(:contact, email: 'veryold@test.com', account: account, last_activity_at: 20.days.ago)
create(:contact, email: 'old@test.com', account: account, last_activity_at: 10.days.ago)
create(:contact, email: 'recent@test.com', account: account, last_activity_at: 2.days.ago)
get "/api/v1/accounts/#{account.id}/search/contacts",
headers: agent.create_new_auth_token,
params: { q: 'test', since: 15.days.ago.to_i, until: 5.days.ago.to_i },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
contact_emails = response_data[:payload][:contacts].pluck(:email)
expect(contact_emails).to include('old@test.com')
expect(contact_emails).not_to include('veryold@test.com', 'recent@test.com')
end
end
end
end
describe 'GET /api/v1/accounts/{account.id}/search/conversations' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/search/conversations", params: { q: 'test' }
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'returns all conversations with messages containing the search query' do
get "/api/v1/accounts/#{account.id}/search/conversations",
headers: agent.create_new_auth_token,
params: { q: 'test' },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data[:payload].keys).to contain_exactly(:conversations)
expect(response_data[:payload][:conversations].length).to eq 1
end
context 'with advanced_search feature enabled', :opensearch do
before do
account.enable_features!('advanced_search')
end
it 'filters conversations by since parameter' do
unique_id = SecureRandom.hex(8)
old_contact = create(:contact, email: "old-#{unique_id}@test.com", account: account)
recent_contact = create(:contact, email: "recent-#{unique_id}@test.com", account: account)
old_conversation = create(:conversation, account: account, contact: old_contact)
recent_conversation = create(:conversation, account: account, contact: recent_contact)
create(:message, conversation: old_conversation, account: account, content: 'message 1')
create(:message, conversation: recent_conversation, account: account, content: 'message 2')
create(:inbox_member, user: agent, inbox: old_conversation.inbox)
create(:inbox_member, user: agent, inbox: recent_conversation.inbox)
# Bypass CURRENT_TIMESTAMP default
# rubocop:disable Rails/SkipsModelValidations
Conversation.where(id: old_conversation.id).update_all(last_activity_at: 10.days.ago)
Conversation.where(id: recent_conversation.id).update_all(last_activity_at: 2.days.ago)
# rubocop:enable Rails/SkipsModelValidations
get "/api/v1/accounts/#{account.id}/search/conversations",
headers: agent.create_new_auth_token,
params: { q: unique_id, since: 5.days.ago.to_i },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
conversation_display_ids = response_data[:payload][:conversations].pluck(:id)
expect(conversation_display_ids).to eq([recent_conversation.display_id])
end
it 'filters conversations by until parameter' do
unique_id = SecureRandom.hex(8)
old_contact = create(:contact, email: "old-#{unique_id}@test.com", account: account)
recent_contact = create(:contact, email: "recent-#{unique_id}@test.com", account: account)
old_conversation = create(:conversation, account: account, contact: old_contact)
recent_conversation = create(:conversation, account: account, contact: recent_contact)
create(:message, conversation: old_conversation, account: account, content: 'message 1')
create(:message, conversation: recent_conversation, account: account, content: 'message 2')
create(:inbox_member, user: agent, inbox: old_conversation.inbox)
create(:inbox_member, user: agent, inbox: recent_conversation.inbox)
# Bypass CURRENT_TIMESTAMP default
# rubocop:disable Rails/SkipsModelValidations
Conversation.where(id: old_conversation.id).update_all(last_activity_at: 10.days.ago)
Conversation.where(id: recent_conversation.id).update_all(last_activity_at: 2.days.ago)
# rubocop:enable Rails/SkipsModelValidations
get "/api/v1/accounts/#{account.id}/search/conversations",
headers: agent.create_new_auth_token,
params: { q: unique_id, until: 5.days.ago.to_i },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
conversation_display_ids = response_data[:payload][:conversations].pluck(:id)
expect(conversation_display_ids).to eq([old_conversation.display_id])
end
it 'filters conversations by both since and until parameters' do
unique_id = SecureRandom.hex(8)
very_old_contact = create(:contact, email: "veryold-#{unique_id}@test.com", account: account)
old_contact = create(:contact, email: "old-#{unique_id}@test.com", account: account)
recent_contact = create(:contact, email: "recent-#{unique_id}@test.com", account: account)
very_old_conversation = create(:conversation, account: account, contact: very_old_contact)
old_conversation = create(:conversation, account: account, contact: old_contact)
recent_conversation = create(:conversation, account: account, contact: recent_contact)
create(:message, conversation: very_old_conversation, account: account, content: 'message 1')
create(:message, conversation: old_conversation, account: account, content: 'message 2')
create(:message, conversation: recent_conversation, account: account, content: 'message 3')
create(:inbox_member, user: agent, inbox: very_old_conversation.inbox)
create(:inbox_member, user: agent, inbox: old_conversation.inbox)
create(:inbox_member, user: agent, inbox: recent_conversation.inbox)
# Bypass CURRENT_TIMESTAMP default
# rubocop:disable Rails/SkipsModelValidations
Conversation.where(id: very_old_conversation.id).update_all(last_activity_at: 20.days.ago)
Conversation.where(id: old_conversation.id).update_all(last_activity_at: 10.days.ago)
Conversation.where(id: recent_conversation.id).update_all(last_activity_at: 2.days.ago)
# rubocop:enable Rails/SkipsModelValidations
get "/api/v1/accounts/#{account.id}/search/conversations",
headers: agent.create_new_auth_token,
params: { q: unique_id, since: 15.days.ago.to_i, until: 5.days.ago.to_i },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
conversation_display_ids = response_data[:payload][:conversations].pluck(:id)
expect(conversation_display_ids).to eq([old_conversation.display_id])
end
end
end
end
describe 'GET /api/v1/accounts/{account.id}/search/messages' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/search/messages", params: { q: 'test' }
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'returns all conversations with messages containing the search query' do
get "/api/v1/accounts/#{account.id}/search/messages",
headers: agent.create_new_auth_token,
params: { q: 'test' },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data[:payload].keys).to contain_exactly(:messages)
expect(response_data[:payload][:messages].length).to eq 2
end
end
end
describe 'GET /api/v1/accounts/{account.id}/search/articles' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/search/articles", params: { q: 'test' }
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'returns all articles containing the search query' do
get "/api/v1/accounts/#{account.id}/search/articles",
headers: agent.create_new_auth_token,
params: { q: 'test' },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data[:payload].keys).to contain_exactly(:articles)
expect(response_data[:payload][:articles].length).to eq 1
expect(response_data[:payload][:articles].first[:title]).to eq 'Test Article Guide'
end
it 'returns empty results when no articles match the search query' do
get "/api/v1/accounts/#{account.id}/search/articles",
headers: agent.create_new_auth_token,
params: { q: 'nonexistent' },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data[:payload].keys).to contain_exactly(:articles)
expect(response_data[:payload][:articles].length).to eq 0
end
it 'supports pagination' do
portal = create(:portal, account: account)
16.times do |i|
create(:article, title: "Test Article #{i}", account: account, portal: portal, author: agent, status: 'published')
end
get "/api/v1/accounts/#{account.id}/search/articles",
headers: agent.create_new_auth_token,
params: { q: 'test', page: 1 },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data[:payload][:articles].length).to eq 15 # Default per_page is 15
end
context 'with advanced_search feature enabled', :opensearch do
before do
account.enable_features!('advanced_search')
end
it 'filters articles by since parameter' do
portal = create(:portal, account: account)
old_article = create(:article, title: 'Old Article test', account: account, portal: portal,
author: agent, status: 'published', updated_at: 10.days.ago)
recent_article = create(:article, title: 'Recent Article test', account: account, portal: portal,
author: agent, status: 'published', updated_at: 2.days.ago)
get "/api/v1/accounts/#{account.id}/search/articles",
headers: agent.create_new_auth_token,
params: { q: 'test', since: 5.days.ago.to_i },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
article_ids = response_data[:payload][:articles].pluck(:id)
expect(article_ids).to include(recent_article.id)
expect(article_ids).not_to include(old_article.id)
end
it 'filters articles by until parameter' do
portal = create(:portal, account: account)
old_article = create(:article, title: 'Old Article test', account: account, portal: portal,
author: agent, status: 'published', updated_at: 10.days.ago)
recent_article = create(:article, title: 'Recent Article test', account: account, portal: portal,
author: agent, status: 'published', updated_at: 2.days.ago)
get "/api/v1/accounts/#{account.id}/search/articles",
headers: agent.create_new_auth_token,
params: { q: 'test', until: 5.days.ago.to_i },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
article_ids = response_data[:payload][:articles].pluck(:id)
expect(article_ids).to include(old_article.id)
expect(article_ids).not_to include(recent_article.id)
end
it 'filters articles by both since and until parameters' do
portal = create(:portal, account: account)
very_old_article = create(:article, title: 'Very Old Article test', account: account, portal: portal,
author: agent, status: 'published', updated_at: 20.days.ago)
old_article = create(:article, title: 'Old Article test', account: account, portal: portal,
author: agent, status: 'published', updated_at: 10.days.ago)
recent_article = create(:article, title: 'Recent Article test', account: account, portal: portal,
author: agent, status: 'published', updated_at: 2.days.ago)
get "/api/v1/accounts/#{account.id}/search/articles",
headers: agent.create_new_auth_token,
params: { q: 'test', since: 15.days.ago.to_i, until: 5.days.ago.to_i },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
article_ids = response_data[:payload][:articles].pluck(:id)
expect(article_ids).to include(old_article.id)
expect(article_ids).not_to include(very_old_article.id, recent_article.id)
end
end
end
end
end

View File

@@ -0,0 +1,165 @@
require 'rails_helper'
RSpec.describe 'Team Members API', type: :request do
let(:account) { create(:account) }
let(:account_2) { create(:account) }
let!(:team) { create(:team, account: account) }
describe 'GET /api/v1/accounts/{account.id}/teams/{team_id}/team_members' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members"
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) }
it 'returns all the teams' do
create(:team_member, team: team, user: agent)
get "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body.first['id']).to eq(agent.id)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/teams/{team_id}/team_members' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
it 'returns unathorized for agent' do
params = { user_id: agent.id }
post "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'add a new team members when its administrator' do
user_ids = (1..5).map { create(:user, account: account, role: :agent).id }
params = { user_ids: user_ids }
# have a team member added already
create(:team_member, team: team, user: User.find(user_ids.first))
post "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members",
params: params,
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response.count).to eq(user_ids.count - 1)
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/teams/{team_id}/team_members' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members"
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 'return unauthorized for agent' do
params = { user_id: agent.id }
delete "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'destroys the team members when its administrator' do
user_ids = (1..5).map { create(:user, account: account, role: :agent).id }
user_ids.each { |id| create(:team_member, team: team, user: User.find(id)) }
params = { user_ids: user_ids.first(3) }
delete "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members",
params: params,
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(team.team_members.count).to eq(2)
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/teams/{team_id}/team_members' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
patch "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members"
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(:agent_2) { create(:user, account: account_2, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
it 'return unauthorized for agent' do
params = { user_id: agent.id }
patch "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'updates the team members when its administrator' do
user_ids = (1..5).map { create(:user, account: account, role: :agent).id }
params = { user_ids: user_ids }
patch "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members",
params: params,
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response.count).to eq(user_ids.count)
end
it 'ignores the user ids when its not a valid account user id' do
params = { user_ids: [agent_2.id] }
patch "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members",
params: params,
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
json_response = response.parsed_body
expect(json_response['error']).to eq('Invalid User IDs')
end
end
end
end

View File

@@ -0,0 +1,160 @@
require 'rails_helper'
RSpec.describe 'Teams API', type: :request do
let(:account) { create(:account) }
let!(:team) { create(:team, account: account) }
describe 'GET /api/v1/accounts/{account.id}/teams' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/teams"
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) }
it 'returns all the teams' do
get "/api/v1/accounts/#{account.id}/teams",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body.first['id']).to eq(account.teams.first.id)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/teams/{team_id}' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/teams/#{team.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) }
it 'returns all the teams' do
get "/api/v1/accounts/#{account.id}/teams/#{team.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['id']).to eq(team.id)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/teams' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/teams"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
it 'returns unathorized for agent' do
params = { name: 'Test Team' }
post "/api/v1/accounts/#{account.id}/teams",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'creates a new team when its administrator' do
params = { name: 'test-team' }
post "/api/v1/accounts/#{account.id}/teams",
params: params,
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(Team.count).to eq(2)
end
end
end
describe 'PUT /api/v1/accounts/{account.id}/teams/:id' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/teams/#{team.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 = { name: 'new-team' }
put "/api/v1/accounts/#{account.id}/teams/#{team.id}",
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'updates an existing team when its an administrator' do
params = { name: 'new-team' }
put "/api/v1/accounts/#{account.id}/teams/#{team.id}",
params: params,
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(team.reload.name).to eq('new-team')
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/teams/:id' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/teams/#{team.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 'return unauthorized for agent' do
delete "/api/v1/accounts/#{account.id}/teams/#{team.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'destroys the team when its administrator' do
delete "/api/v1/accounts/#{account.id}/teams/#{team.id}",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(Team.count).to eq(0)
end
end
end
end

View File

@@ -0,0 +1,55 @@
require 'rails_helper'
RSpec.describe 'TikTok Authorization API', type: :request do
let(:account) { create(:account) }
describe 'POST /api/v1/accounts/{account.id}/tiktok/authorization' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/tiktok/authorization"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
before do
InstallationConfig.where(name: %w[TIKTOK_APP_ID TIKTOK_APP_SECRET]).delete_all
GlobalConfig.clear_cache
end
it 'returns unauthorized for agent' do
with_modified_env TIKTOK_APP_ID: 'tiktok-app-id', TIKTOK_APP_SECRET: 'tiktok-app-secret' do
post "/api/v1/accounts/#{account.id}/tiktok/authorization",
headers: agent.create_new_auth_token,
as: :json
end
expect(response).to have_http_status(:unauthorized)
end
it 'creates a new authorization and returns the redirect url' do
with_modified_env TIKTOK_APP_ID: 'tiktok-app-id', TIKTOK_APP_SECRET: 'tiktok-app-secret' do
post "/api/v1/accounts/#{account.id}/tiktok/authorization",
headers: administrator.create_new_auth_token,
as: :json
end
expect(response).to have_http_status(:success)
expect(response.parsed_body['success']).to be true
helper = Class.new do
include Tiktok::IntegrationHelper
end.new
expected_state = helper.generate_tiktok_token(account.id)
expected_url = Tiktok::AuthClient.authorize_url(state: expected_state)
expect(response.parsed_body['url']).to eq(expected_url)
end
end
end
end

View File

@@ -0,0 +1,46 @@
require 'rails_helper'
RSpec.describe 'Twitter Authorization API', type: :request do
let(:account) { create(:account) }
describe 'POST /api/v1/accounts/{account.id}/twitter/authorization' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/twitter/authorization"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
let(:twitter_client) { double }
let(:twitter_response) { double }
let(:raw_response) { double }
it 'returns unathorized for agent' do
post "/api/v1/accounts/#{account.id}/twitter/authorization",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'creates a new authorization and returns the redirect url' do
allow(Twitty::Facade).to receive(:new).and_return(twitter_client)
allow(twitter_client).to receive(:request_oauth_token).and_return(twitter_response)
allow(twitter_response).to receive(:status).and_return('200')
allow(twitter_response).to receive(:raw_response).and_return(raw_response)
allow(raw_response).to receive(:body).and_return('oauth_token=test_token')
post "/api/v1/accounts/#{account.id}/twitter/authorization",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['url']).to include('test_token')
end
end
end
end

View File

@@ -0,0 +1,146 @@
require 'rails_helper'
RSpec.describe 'Webhooks API', type: :request do
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:webhook) { create(:webhook, account: account, inbox: inbox, url: 'https://hello.com', name: 'My Webhook') }
let(:administrator) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
describe 'GET /api/v1/accounts/<account_id>/webhooks' do
context 'when it is an authenticated agent' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/webhooks",
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
it 'gets all webhook' do
get "/api/v1/accounts/#{account.id}/webhooks",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['payload']['webhooks'].count).to eql account.webhooks.count
end
end
end
describe 'POST /api/v1/accounts/<account_id>/webhooks' do
context 'when it is an authenticated agent' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/webhooks",
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
it 'creates webhook' do
post "/api/v1/accounts/#{account.id}/webhooks",
params: { account_id: account.id, inbox_id: inbox.id, url: 'https://hello.com' },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['payload']['webhook']['url']).to eql 'https://hello.com'
end
it 'creates webhook with name' do
post "/api/v1/accounts/#{account.id}/webhooks",
params: { account_id: account.id, inbox_id: inbox.id, url: 'https://hello.com', name: 'My Webhook' },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['payload']['webhook']['name']).to eql 'My Webhook'
end
it 'throws error when invalid url provided' do
post "/api/v1/accounts/#{account.id}/webhooks",
params: { account_id: account.id, inbox_id: inbox.id, url: 'javascript:alert(1)' },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['message']).to eql 'Url is invalid'
end
it 'throws error if subscription events are invalid' do
post "/api/v1/accounts/#{account.id}/webhooks",
params: { url: 'https://hello.com', subscriptions: ['conversation_random_event'] },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['message']).to eql 'Subscriptions Invalid events'
end
it 'throws error if subscription events are empty' do
post "/api/v1/accounts/#{account.id}/webhooks",
params: { url: 'https://hello.com', subscriptions: [] },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['message']).to eql 'Subscriptions Invalid events'
end
it 'use default if subscription events are nil' do
post "/api/v1/accounts/#{account.id}/webhooks",
params: { url: 'https://hello.com', subscriptions: nil },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(
response.parsed_body['payload']['webhook']['subscriptions']
).to eql %w[conversation_status_changed conversation_updated conversation_created contact_created contact_updated
message_created message_updated webwidget_triggered]
end
end
end
describe 'PUT /api/v1/accounts/<account_id>/webhooks/:id' do
context 'when it is an authenticated agent' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/webhooks/#{webhook.id}",
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
it 'updates webhook' do
put "/api/v1/accounts/#{account.id}/webhooks/#{webhook.id}",
params: { url: 'https://hello.com', name: 'Another Webhook' },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['payload']['webhook']['url']).to eql 'https://hello.com'
expect(response.parsed_body['payload']['webhook']['name']).to eql 'Another Webhook'
end
end
end
describe 'DELETE /api/v1/accounts/<account_id>/webhooks/:id' do
context 'when it is an authenticated agent' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/webhooks/#{webhook.id}",
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
it 'deletes webhook' do
delete "/api/v1/accounts/#{account.id}/webhooks/#{webhook.id}",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(account.webhooks.count).to be 0
end
end
end
end

View File

@@ -0,0 +1,495 @@
require 'rails_helper'
RSpec.describe 'WhatsApp Authorization API', type: :request do
let(:account) { create(:account) }
describe 'POST /api/v1/accounts/{account.id}/whatsapp/authorization' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/whatsapp/authorization"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
context 'when authenticated user makes request' do
it 'returns unprocessable entity when code is missing' do
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
business_id: 'test_business_id',
waba_id: 'test_waba_id'
},
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to include('code')
end
it 'returns unprocessable entity when business_id is missing' do
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
code: 'test_code',
waba_id: 'test_waba_id'
},
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to include('business_id')
end
it 'returns unprocessable entity when waba_id is missing' do
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
code: 'test_code',
business_id: 'test_business_id'
},
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to include('waba_id')
end
it 'creates whatsapp channel successfully' do
whatsapp_channel = create(:channel_whatsapp, account: account, validate_provider_config: false, sync_templates: false)
inbox = create(:inbox, account: account, channel: whatsapp_channel)
embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService)
allow(Whatsapp::EmbeddedSignupService).to receive(:new).and_return(embedded_signup_service)
allow(embedded_signup_service).to receive(:perform).and_return(whatsapp_channel)
allow(whatsapp_channel).to receive(:inbox).and_return(inbox)
# Stub webhook setup service to prevent HTTP calls
webhook_service = instance_double(Whatsapp::WebhookSetupService)
allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service)
allow(webhook_service).to receive(:perform)
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id',
phone_number_id: 'test_phone_id'
},
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data['success']).to be true
expect(response_data['id']).to eq(inbox.id)
expect(response_data['name']).to eq(inbox.name)
expect(response_data['channel_type']).to eq('whatsapp')
end
it 'calls the embedded signup service with correct parameters' do
whatsapp_channel = create(:channel_whatsapp, account: account, validate_provider_config: false, sync_templates: false)
inbox = create(:inbox, account: account, channel: whatsapp_channel)
embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService)
expect(Whatsapp::EmbeddedSignupService).to receive(:new).with(
account: account,
params: {
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id',
phone_number_id: 'test_phone_id'
},
inbox_id: nil
).and_return(embedded_signup_service)
allow(embedded_signup_service).to receive(:perform).and_return(whatsapp_channel)
allow(whatsapp_channel).to receive(:inbox).and_return(inbox)
allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(instance_double(Whatsapp::WebhookSetupService, perform: true))
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id',
phone_number_id: 'test_phone_id'
},
headers: agent.create_new_auth_token,
as: :json
end
it 'accepts phone_number_id as optional parameter' do
whatsapp_channel = create(:channel_whatsapp, account: account, validate_provider_config: false, sync_templates: false)
inbox = create(:inbox, account: account, channel: whatsapp_channel)
embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService)
expect(Whatsapp::EmbeddedSignupService).to receive(:new).with(
account: account,
params: {
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
},
inbox_id: nil
).and_return(embedded_signup_service)
allow(embedded_signup_service).to receive(:perform).and_return(whatsapp_channel)
allow(whatsapp_channel).to receive(:inbox).and_return(inbox)
allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(instance_double(Whatsapp::WebhookSetupService, perform: true))
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
},
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
end
it 'returns unprocessable entity when service fails' do
allow(Whatsapp::EmbeddedSignupService).to receive(:new).and_raise(StandardError, 'Service error')
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
},
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
response_data = response.parsed_body
expect(response_data['success']).to be false
expect(response_data['error']).to eq('Service error')
end
it 'logs error when service fails' do
allow(Whatsapp::EmbeddedSignupService).to receive(:new).and_raise(StandardError, 'Service error')
expect(Rails.logger).to receive(:error).with(/\[WHATSAPP AUTHORIZATION\] Embedded signup error: Service error/)
expect(Rails.logger).to receive(:error).with(/authorizations_controller/)
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
},
headers: agent.create_new_auth_token,
as: :json
end
it 'handles token exchange errors' do
allow(Whatsapp::EmbeddedSignupService).to receive(:new)
.and_raise(StandardError, 'Invalid authorization code')
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
code: 'invalid_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
},
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq('Invalid authorization code')
end
it 'handles channel already exists error' do
allow(Whatsapp::EmbeddedSignupService).to receive(:new)
.and_raise(StandardError, 'Channel already exists')
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
},
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq('Channel already exists')
end
end
context 'when user is not authorized for the account' do
let(:other_account) { create(:account) }
it 'returns unauthorized' do
post "/api/v1/accounts/#{other_account.id}/whatsapp/authorization",
params: {
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
},
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when user is an administrator' do
it 'allows channel creation' do
embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService)
whatsapp_channel = create(:channel_whatsapp, account: account, validate_provider_config: false, sync_templates: false)
inbox = create(:inbox, account: account, channel: whatsapp_channel)
allow(Whatsapp::EmbeddedSignupService).to receive(:new).and_return(embedded_signup_service)
allow(embedded_signup_service).to receive(:perform).and_return(whatsapp_channel)
allow(whatsapp_channel).to receive(:inbox).and_return(inbox)
# Stub webhook setup service
webhook_service = instance_double(Whatsapp::WebhookSetupService)
allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service)
allow(webhook_service).to receive(:perform)
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
},
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
end
end
end
end
describe 'POST /api/v1/accounts/{account.id}/whatsapp/authorization with inbox_id (reauthorization)' do
let(:whatsapp_channel) do
channel = build(:channel_whatsapp, account: account, provider: 'whatsapp_cloud',
provider_config: {
'api_key' => 'test_token',
'phone_number_id' => '123456',
'business_account_id' => '654321',
'source' => 'embedded_signup'
})
allow(channel).to receive(:validate_provider_config).and_return(true)
allow(channel).to receive(:sync_templates).and_return(true)
allow(channel).to receive(:setup_webhooks).and_return(true)
channel.save!
# Call authorization_error! twice to reach the threshold
channel.authorization_error!
channel.authorization_error!
channel
end
let(:whatsapp_inbox) { create(:inbox, channel: whatsapp_channel, account: account) }
context 'when user is an administrator' do
let(:administrator) { create(:user, account: account, role: :administrator) }
context 'with valid parameters' do
let(:valid_params) do
{
code: 'auth_code_123',
business_id: 'business_123',
waba_id: 'waba_123',
phone_number_id: 'phone_123'
}
end
it 'reauthorizes the WhatsApp channel successfully' do
allow(whatsapp_channel).to receive(:reauthorization_required?).and_return(true)
embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService)
allow(Whatsapp::EmbeddedSignupService).to receive(:new).with(
account: account,
params: {
code: 'auth_code_123',
business_id: 'business_123',
waba_id: 'waba_123',
phone_number_id: 'phone_123'
},
inbox_id: whatsapp_inbox.id
).and_return(embedded_signup_service)
allow(embedded_signup_service).to receive(:perform).and_return(whatsapp_channel)
allow(whatsapp_channel).to receive(:inbox).and_return(whatsapp_inbox)
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: valid_params.merge(inbox_id: whatsapp_inbox.id),
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['success']).to be true
expect(json_response['id']).to eq(whatsapp_inbox.id)
end
it 'handles reauthorization failure' do
embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService)
allow(Whatsapp::EmbeddedSignupService).to receive(:new).with(
account: account,
params: {
code: 'auth_code_123',
business_id: 'business_123',
waba_id: 'waba_123',
phone_number_id: 'phone_123'
},
inbox_id: whatsapp_inbox.id
).and_return(embedded_signup_service)
allow(embedded_signup_service).to receive(:perform)
.and_raise(StandardError, 'Token exchange failed')
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: valid_params.merge(inbox_id: whatsapp_inbox.id),
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
json_response = response.parsed_body
expect(json_response['success']).to be false
expect(json_response['error']).to eq('Token exchange failed')
end
it 'handles phone number mismatch error' do
embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService)
allow(Whatsapp::EmbeddedSignupService).to receive(:new).with(
account: account,
params: {
code: 'auth_code_123',
business_id: 'business_123',
waba_id: 'waba_123',
phone_number_id: 'phone_123'
},
inbox_id: whatsapp_inbox.id
).and_return(embedded_signup_service)
allow(embedded_signup_service).to receive(:perform)
.and_raise(StandardError, 'Phone number mismatch. The new phone number (+1234567890) does not match ' \
'the existing phone number (+15551234567). Please use the same WhatsApp ' \
'Business Account that was originally connected.')
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: valid_params.merge(inbox_id: whatsapp_inbox.id),
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
json_response = response.parsed_body
expect(json_response['success']).to be false
expect(json_response['error']).to include('Phone number mismatch')
end
end
context 'when inbox does not exist' do
it 'returns not found error' do
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: { inbox_id: 0, code: 'test', business_id: 'test', waba_id: 'test' },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:not_found)
end
end
context 'when reauthorization is not required' do
let(:fresh_channel) do
channel = build(:channel_whatsapp, account: account, provider: 'whatsapp_cloud',
provider_config: {
'api_key' => 'test_token',
'phone_number_id' => '123456',
'business_account_id' => '654321',
'source' => 'embedded_signup'
})
allow(channel).to receive(:validate_provider_config).and_return(true)
allow(channel).to receive(:sync_templates).and_return(true)
allow(channel).to receive(:setup_webhooks).and_return(true)
channel.save!
# Do NOT call authorization_error! - channel is working fine
channel
end
let(:fresh_inbox) { create(:inbox, channel: fresh_channel, account: account) }
it 'returns unprocessable entity error' do
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: { inbox_id: fresh_inbox.id },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
json_response = response.parsed_body
expect(json_response['success']).to be false
end
end
context 'when channel is not WhatsApp' do
let(:facebook_channel) do
stub_request(:post, 'https://graph.facebook.com/v3.2/me/subscribed_apps')
.to_return(status: 200, body: '{}', headers: {})
channel = create(:channel_facebook_page, account: account)
# Call authorization_error! twice to reach the threshold
channel.authorization_error!
channel.authorization_error!
channel
end
let(:facebook_inbox) { create(:inbox, channel: facebook_channel, account: account) }
it 'returns unprocessable entity error' do
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: { inbox_id: facebook_inbox.id },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
json_response = response.parsed_body
expect(json_response['success']).to be false
end
end
end
context 'when user is an agent' do
let(:agent) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: whatsapp_inbox, user: agent)
end
it 'returns unprocessable_entity error' do
allow(whatsapp_channel).to receive(:reauthorization_required?).and_return(true)
# Stub the embedded signup service to prevent HTTP calls
embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService)
allow(Whatsapp::EmbeddedSignupService).to receive(:new).with(
account: account,
params: {
code: 'test',
business_id: 'test',
waba_id: 'test'
},
inbox_id: whatsapp_inbox.id
).and_return(embedded_signup_service)
allow(embedded_signup_service).to receive(:perform).and_return(whatsapp_channel)
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: { inbox_id: whatsapp_inbox.id, code: 'test', business_id: 'test', waba_id: 'test' },
headers: agent.create_new_auth_token,
as: :json
# Agents should get unprocessable_entity since they can find the inbox but channel doesn't need reauth
expect(response).to have_http_status(:unprocessable_entity)
end
end
context 'when user is not authenticated' do
it 'returns unauthorized error' do
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: { inbox_id: whatsapp_inbox.id },
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
end