Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
0
research/chatwoot/spec/controllers/.keep
Normal file
0
research/chatwoot/spec/controllers/.keep
Normal file
@@ -0,0 +1,10 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe '.well-known/assetlinks.json', type: :request do
|
||||
describe 'GET /.well-known/assetlinks.json' do
|
||||
it 'successfully retrieves assetlinks.json file' do
|
||||
get '/.well-known/assetlinks.json'
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
121
research/chatwoot/spec/controllers/api/base_controller_spec.rb
Normal file
121
research/chatwoot/spec/controllers/api/base_controller_spec.rb
Normal file
@@ -0,0 +1,121 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'API Base', type: :request do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:user) { create(:user, account: account) }
|
||||
|
||||
describe 'request with api_access_token for user' do
|
||||
context 'when accessing an account scoped resource' do
|
||||
let!(:admin) { create(:user, :administrator, account: account) }
|
||||
let!(:conversation) { create(:conversation, account: account) }
|
||||
|
||||
it 'sets Current attributes for the request and then returns the response' do
|
||||
# This test verifies that Current.user, Current.account, and Current.account_user
|
||||
# are properly set during request processing. We verify this indirectly:
|
||||
# - A successful response proves Current.account_user was set (required for authorization)
|
||||
# - The correct conversation data proves Current.account was set (scopes the query)
|
||||
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}",
|
||||
headers: { api_access_token: admin.access_token.token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['id']).to eq(conversation.display_id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an invalid api_access_token' do
|
||||
it 'returns unauthorized' do
|
||||
get '/api/v1/profile',
|
||||
headers: { api_access_token: 'invalid' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is a valid api_access_token' do
|
||||
it 'returns current user information' do
|
||||
get '/api/v1/profile',
|
||||
headers: { api_access_token: user.access_token.token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['id']).to eq(user.id)
|
||||
expect(json_response['email']).to eq(user.email)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'request with api_access_token for a super admin' do
|
||||
before do
|
||||
user.update!(type: 'SuperAdmin')
|
||||
end
|
||||
|
||||
context 'when its a valid api_access_token' do
|
||||
it 'returns current user information' do
|
||||
get '/api/v1/profile',
|
||||
headers: { api_access_token: user.access_token.token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['id']).to eq(user.id)
|
||||
expect(json_response['email']).to eq(user.email)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'request with api_access_token for bot' do
|
||||
let!(:agent_bot) { create(:agent_bot) }
|
||||
let!(:inbox) { create(:inbox, account: account) }
|
||||
let!(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user, status: 'pending') }
|
||||
|
||||
context 'when it is an unauthorized url' do
|
||||
it 'returns unauthorized' do
|
||||
get '/api/v1/profile',
|
||||
headers: { api_access_token: agent_bot.access_token.token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is a accessible url' do
|
||||
it 'returns success' do
|
||||
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_status",
|
||||
headers: { api_access_token: agent_bot.access_token.token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(conversation.reload.status).to eq('open')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the account is suspended' do
|
||||
it 'returns 401 unauthorized' do
|
||||
account.update!(status: :suspended)
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/canned_responses",
|
||||
headers: { api_access_token: user.access_token.token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
# this exception occured in a client instance (DoubleRender error)
|
||||
it 'will not throw exception if user does not have access to suspended account' do
|
||||
user_with_out_access = create(:user)
|
||||
account.update!(status: :suspended)
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/canned_responses",
|
||||
headers: { api_access_token: user_with_out_access.access_token.token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,815 @@
|
||||
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 '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
|
||||
|
||||
it 'returns has_more as false when results fit in one page' 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)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body['meta']['has_more']).to be(false)
|
||||
expect(response_body['meta']['count']).to eq(1)
|
||||
end
|
||||
|
||||
it 'returns has_more as true when there are more results' do
|
||||
# Create 16 contacts (more than RESULTS_PER_PAGE which is 15)
|
||||
create_list(:contact, 16, account: account, name: 'searchable_contact')
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/contacts/search",
|
||||
params: { q: 'searchable_contact' },
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body['meta']['has_more']).to be(true)
|
||||
expect(response_body['meta']['count']).to eq(15)
|
||||
expect(response_body['payload'].length).to eq(15)
|
||||
end
|
||||
|
||||
it 'returns has_more as false on the last page' do
|
||||
# Create 16 contacts
|
||||
create_list(:contact, 16, account: account, name: 'searchable_contact')
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/contacts/search",
|
||||
params: { q: 'searchable_contact', page: 2 },
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body['meta']['has_more']).to be(false)
|
||||
expect(response_body['meta']['count']).to eq(1)
|
||||
expect(response_body['payload'].length).to eq(1)
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -0,0 +1,131 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Integration Apps API', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
describe 'GET /api/v1/integrations/apps' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get api_v1_account_integrations_apps_url(account)
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
|
||||
it 'returns all active apps without sensitive information if the user is an agent' do
|
||||
first_app = Integrations::App.all.find { |app| app.active?(account) }
|
||||
get api_v1_account_integrations_apps_url(account),
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
apps = response.parsed_body['payload'].first
|
||||
expect(apps['id']).to eql(first_app.id)
|
||||
expect(apps['name']).to eql(first_app.name)
|
||||
expect(apps['action']).to be_nil
|
||||
end
|
||||
|
||||
it 'will not return sensitive information for openai app for agents' do
|
||||
openai = create(:integrations_hook, :openai, account: account)
|
||||
get api_v1_account_integrations_apps_url(account),
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
app = response.parsed_body['payload'].find { |int_app| int_app['id'] == openai.app.id }
|
||||
expect(app['hooks'].first['settings']).to be_nil
|
||||
end
|
||||
|
||||
it 'returns all active apps with sensitive information if user is an admin' do
|
||||
first_app = Integrations::App.all.find { |app| app.active?(account) }
|
||||
get api_v1_account_integrations_apps_url(account),
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
apps = response.parsed_body['payload'].first
|
||||
expect(apps['id']).to eql(first_app.id)
|
||||
expect(apps['name']).to eql(first_app.name)
|
||||
expect(apps['action']).to eql(first_app.action)
|
||||
end
|
||||
|
||||
it 'returns slack app with appropriate redirect url when configured' do
|
||||
with_modified_env SLACK_CLIENT_ID: 'client_id', SLACK_CLIENT_SECRET: 'client_secret' do
|
||||
get api_v1_account_integrations_apps_url(account),
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
apps = response.parsed_body['payload']
|
||||
slack_app = apps.find { |app| app['id'] == 'slack' }
|
||||
expect(slack_app['action']).to include('client_id=client_id')
|
||||
end
|
||||
end
|
||||
|
||||
it 'will return sensitive information for openai app for admins' do
|
||||
openai = create(:integrations_hook, :openai, account: account)
|
||||
get api_v1_account_integrations_apps_url(account),
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
app = response.parsed_body['payload'].find { |int_app| int_app['id'] == openai.app.id }
|
||||
expect(app['hooks'].first['settings']).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/integrations/apps/:id' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get api_v1_account_integrations_app_url(account_id: account.id, id: 'slack')
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
|
||||
it 'returns details of the app' do
|
||||
get api_v1_account_integrations_app_url(account_id: account.id, id: 'slack'),
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
app = response.parsed_body
|
||||
expect(app['id']).to eql('slack')
|
||||
expect(app['name']).to eql('Slack')
|
||||
end
|
||||
|
||||
it 'will not return sensitive information for openai app for agents' do
|
||||
openai = create(:integrations_hook, :openai, account: account)
|
||||
get api_v1_account_integrations_app_url(account_id: account.id, id: openai.app.id),
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
app = response.parsed_body
|
||||
expect(app['hooks'].first['settings']).to be_nil
|
||||
end
|
||||
|
||||
it 'will return sensitive information for openai app for admins' do
|
||||
openai = create(:integrations_hook, :openai, account: account)
|
||||
get api_v1_account_integrations_app_url(account_id: account.id, id: openai.app.id),
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
app = response.parsed_body
|
||||
expect(app['hooks'].first['settings']).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,138 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Dyte Integration API', type: :request do
|
||||
let(:headers) { { 'Content-Type' => 'application/json' } }
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:conversation) { create(:conversation, account: account, status: :pending) }
|
||||
let(:message) { create(:message, conversation: conversation, account: account, inbox: conversation.inbox) }
|
||||
let(:integration_message) do
|
||||
create(:message, content_type: 'integrations',
|
||||
content_attributes: { type: 'dyte', data: { meeting_id: 'm_id' } },
|
||||
conversation: conversation, account: account, inbox: conversation.inbox)
|
||||
end
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:unauthorized_agent) { create(:user, account: account, role: :agent) }
|
||||
|
||||
before do
|
||||
create(:integrations_hook, :dyte, account: account)
|
||||
create(:inbox_member, user: agent, inbox: conversation.inbox)
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/:account_id/integrations/dyte/create_a_meeting' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post create_a_meeting_api_v1_account_integrations_dyte_url(account)
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the agent does not have access to the inbox' do
|
||||
it 'returns unauthorized' do
|
||||
post create_a_meeting_api_v1_account_integrations_dyte_url(account),
|
||||
params: { conversation_id: conversation.display_id },
|
||||
headers: unauthorized_agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an agent with inbox access and the Dyte API is a success' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.dyte.io/v2/meetings')
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: { success: true, data: { id: 'meeting_id' } }.to_json,
|
||||
headers: headers
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns valid message payload' do
|
||||
post create_a_meeting_api_v1_account_integrations_dyte_url(account),
|
||||
params: { conversation_id: conversation.display_id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = response.parsed_body
|
||||
last_message = conversation.reload.messages.last
|
||||
expect(conversation.display_id).to eq(response_body['conversation_id'])
|
||||
expect(last_message.id).to eq(response_body['id'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an agent with inbox access and the Dyte API is errored' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.dyte.io/v2/meetings')
|
||||
.to_return(
|
||||
status: 422,
|
||||
body: { success: false, data: { message: 'Title is required' } }.to_json,
|
||||
headers: headers
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns error payload' do
|
||||
post create_a_meeting_api_v1_account_integrations_dyte_url(account),
|
||||
params: { conversation_id: conversation.display_id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body['error']).to eq({ 'data' => { 'message' => 'Title is required' }, 'success' => false })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/:account_id/integrations/dyte/add_participant_to_meeting' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post add_participant_to_meeting_api_v1_account_integrations_dyte_url(account)
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the agent does not have access to the inbox' do
|
||||
it 'returns unauthorized' do
|
||||
post add_participant_to_meeting_api_v1_account_integrations_dyte_url(account),
|
||||
params: { message_id: message.id },
|
||||
headers: unauthorized_agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an agent with inbox access and message_type is not integrations' do
|
||||
it 'returns error' do
|
||||
post add_participant_to_meeting_api_v1_account_integrations_dyte_url(account),
|
||||
params: { message_id: message.id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an agent with inbox access and message_type is integrations' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.dyte.io/v2/meetings/m_id/participants')
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: { success: true, data: { id: 'random_uuid', auth_token: 'json-web-token' } }.to_json,
|
||||
headers: headers
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns auth_token' do
|
||||
post add_participant_to_meeting_api_v1_account_integrations_dyte_url(account),
|
||||
params: { message_id: integration_message.id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body).to eq(
|
||||
{
|
||||
'id' => 'random_uuid', 'auth_token' => 'json-web-token'
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,137 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Integration Hooks API', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:params) { { app_id: 'dialogflow', inbox_id: inbox.id, settings: { project_id: 'xx', credentials: { test: 'test' }, region: 'europe-west1' } } }
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/integrations/hooks' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post api_v1_account_integrations_hooks_url(account_id: account.id),
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'return unauthorized if agent' do
|
||||
post api_v1_account_integrations_hooks_url(account_id: account.id),
|
||||
params: params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'creates hooks if admin' do
|
||||
post api_v1_account_integrations_hooks_url(account_id: account.id),
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data['app_id']).to eq params[:app_id]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PATCH /api/v1/accounts/{account.id}/integrations/hooks/{hook_id}' do
|
||||
let(:hook) { create(:integrations_hook, account: account) }
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
patch api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'return unauthorized if agent' do
|
||||
patch api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
|
||||
params: params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'updates hook if admin' do
|
||||
patch api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data['app_id']).to eq 'slack'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/integrations/hooks/{hook_id}/process_event' do
|
||||
let(:hook) { create(:integrations_hook, account: account) }
|
||||
let(:params) { { event: 'rephrase', payload: { test: 'test' } } }
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post process_event_api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'will process the events' do
|
||||
post process_event_api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
|
||||
params: params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq 'No processor found'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/accounts/{account.id}/integrations/hooks/{hook_id}' do
|
||||
let(:hook) { create(:integrations_hook, account: account) }
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
delete api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'return unauthorized if agent' do
|
||||
delete api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'updates hook if admin' do
|
||||
delete api_v1_account_integrations_hook_url(account_id: account.id, id: hook.id),
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(Integrations::Hook.exists?(hook.id)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,330 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Linear Integration API', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user) }
|
||||
let(:api_key) { 'valid_api_key' }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:processor_service) { instance_double(Integrations::Linear::ProcessorService) }
|
||||
|
||||
before do
|
||||
create(:integrations_hook, :linear, account: account)
|
||||
allow(Integrations::Linear::ProcessorService).to receive(:new).with(account: account).and_return(processor_service)
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/accounts/:account_id/integrations/linear' do
|
||||
it 'deletes the linear integration' do
|
||||
# Stub the HTTP call to Linear's revoke endpoint
|
||||
allow(HTTParty).to receive(:post).with(
|
||||
'https://api.linear.app/oauth/revoke',
|
||||
anything
|
||||
).and_return(instance_double(HTTParty::Response, success?: true))
|
||||
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/linear",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(account.hooks.count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/accounts/:account_id/integrations/linear/teams' do
|
||||
context 'when it is an authenticated user' do
|
||||
context 'when data is retrieved successfully' do
|
||||
let(:teams_data) { { data: [{ 'id' => 'team1', 'name' => 'Team One' }] } }
|
||||
|
||||
it 'returns team data' do
|
||||
allow(processor_service).to receive(:teams).and_return(teams_data)
|
||||
get "/api/v1/accounts/#{account.id}/integrations/linear/teams",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('Team One')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when data retrieval fails' do
|
||||
it 'returns error message' do
|
||||
allow(processor_service).to receive(:teams).and_return(error: 'error message')
|
||||
get "/api/v1/accounts/#{account.id}/integrations/linear/teams",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.body).to include('error message')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/accounts/:account_id/integrations/linear/team_entities' do
|
||||
let(:team_id) { 'team1' }
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
context 'when data is retrieved successfully' do
|
||||
let(:team_entities_data) do
|
||||
{ data: {
|
||||
users: [{ 'id' => 'user1', 'name' => 'User One' }],
|
||||
projects: [{ 'id' => 'project1', 'name' => 'Project One' }],
|
||||
states: [{ 'id' => 'state1', 'name' => 'State One' }],
|
||||
labels: [{ 'id' => 'label1', 'name' => 'Label One' }]
|
||||
} }
|
||||
end
|
||||
|
||||
it 'returns team entities data' do
|
||||
allow(processor_service).to receive(:team_entities).with(team_id).and_return(team_entities_data)
|
||||
get "/api/v1/accounts/#{account.id}/integrations/linear/team_entities",
|
||||
params: { team_id: team_id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('User One')
|
||||
expect(response.body).to include('Project One')
|
||||
expect(response.body).to include('State One')
|
||||
expect(response.body).to include('Label One')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when data retrieval fails' do
|
||||
it 'returns error message' do
|
||||
allow(processor_service).to receive(:team_entities).with(team_id).and_return(error: 'error message')
|
||||
get "/api/v1/accounts/#{account.id}/integrations/linear/team_entities",
|
||||
params: { team_id: team_id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.body).to include('error message')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/:account_id/integrations/linear/create_issue' do
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:conversation) { create(:conversation, account: account, inbox: inbox) }
|
||||
let(:issue_params) do
|
||||
{
|
||||
team_id: 'team1',
|
||||
title: 'Sample Issue',
|
||||
description: 'This is a sample issue.',
|
||||
assignee_id: 'user1',
|
||||
priority: 'high',
|
||||
state_id: 'state1',
|
||||
label_ids: ['label1'],
|
||||
conversation_id: conversation.display_id
|
||||
}
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
context 'when the issue is created successfully' do
|
||||
let(:created_issue) { { data: { identifier: 'ENG-123', title: 'Sample Issue' } } }
|
||||
|
||||
it 'returns the created issue' do
|
||||
allow(processor_service).to receive(:create_issue).with(issue_params.stringify_keys, agent).and_return(created_issue)
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue",
|
||||
params: issue_params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('Sample Issue')
|
||||
end
|
||||
|
||||
it 'creates activity message when conversation is provided' do
|
||||
allow(processor_service).to receive(:create_issue).with(issue_params.stringify_keys, agent).and_return(created_issue)
|
||||
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue",
|
||||
params: issue_params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
.with(conversation, {
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: :activity,
|
||||
content: "Linear issue ENG-123 was created by #{agent.name}"
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issue creation fails' do
|
||||
it 'returns error message and does not create activity message' do
|
||||
allow(processor_service).to receive(:create_issue).with(issue_params.stringify_keys, agent).and_return(error: 'error message')
|
||||
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue",
|
||||
params: issue_params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.body).to include('error message')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/:account_id/integrations/linear/link_issue' do
|
||||
let(:issue_id) { 'ENG-456' }
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:link) { "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/conversations/#{conversation.display_id}" }
|
||||
let(:title) { 'Sample Issue' }
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
context 'when the issue is linked successfully' do
|
||||
let(:linked_issue) { { data: { 'id' => 'issue1', 'link' => 'https://linear.app/issue1' } } }
|
||||
|
||||
it 'returns the linked issue and creates activity message' do
|
||||
allow(processor_service).to receive(:link_issue).with(link, issue_id, title, agent).and_return(linked_issue)
|
||||
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/link_issue",
|
||||
params: { conversation_id: conversation.display_id, issue_id: issue_id, title: title },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
.with(conversation, {
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: :activity,
|
||||
content: "Linear issue ENG-456 was linked by #{agent.name}"
|
||||
})
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('https://linear.app/issue1')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issue linking fails' do
|
||||
it 'returns error message and does not create activity message' do
|
||||
allow(processor_service).to receive(:link_issue).with(link, issue_id, title, agent).and_return(error: 'error message')
|
||||
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/link_issue",
|
||||
params: { conversation_id: conversation.display_id, issue_id: issue_id, title: title },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.body).to include('error message')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/:account_id/integrations/linear/unlink_issue' do
|
||||
let(:link_id) { 'attachment1' }
|
||||
let(:issue_id) { 'ENG-789' }
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
context 'when the issue is unlinked successfully' do
|
||||
let(:unlinked_issue) { { data: { 'id' => 'issue1', 'link' => 'https://linear.app/issue1' } } }
|
||||
|
||||
it 'returns the unlinked issue and creates activity message' do
|
||||
allow(processor_service).to receive(:unlink_issue).with(link_id).and_return(unlinked_issue)
|
||||
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/unlink_issue",
|
||||
params: { link_id: link_id, issue_id: issue_id, conversation_id: conversation.display_id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
.with(conversation, {
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: :activity,
|
||||
content: "Linear issue ENG-789 was unlinked by #{agent.name}"
|
||||
})
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('https://linear.app/issue1')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issue unlinking fails' do
|
||||
it 'returns error message and does not create activity message' do
|
||||
allow(processor_service).to receive(:unlink_issue).with(link_id).and_return(error: 'error message')
|
||||
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/unlink_issue",
|
||||
params: { link_id: link_id, issue_id: issue_id, conversation_id: conversation.display_id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.body).to include('error message')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/accounts/:account_id/integrations/linear/search_issue' do
|
||||
let(:term) { 'issue' }
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
context 'when search is successful' do
|
||||
let(:search_results) { { data: [{ 'id' => 'issue1', 'title' => 'Sample Issue' }] } }
|
||||
|
||||
it 'returns search results' do
|
||||
allow(processor_service).to receive(:search_issue).with(term).and_return(search_results)
|
||||
get "/api/v1/accounts/#{account.id}/integrations/linear/search_issue",
|
||||
params: { q: term },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('Sample Issue')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when search fails' do
|
||||
it 'returns error message' do
|
||||
allow(processor_service).to receive(:search_issue).with(term).and_return(error: 'error message')
|
||||
get "/api/v1/accounts/#{account.id}/integrations/linear/search_issue",
|
||||
params: { q: term },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.body).to include('error message')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/accounts/:account_id/integrations/linear/linked_issues' do
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:link) { "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/conversations/#{conversation.display_id}" }
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
context 'when linked issue is found' do
|
||||
let(:linked_issue) { { data: [{ 'id' => 'issue1', 'title' => 'Sample Issue' }] } }
|
||||
|
||||
it 'returns linked issue' do
|
||||
allow(processor_service).to receive(:linked_issues).with(link).and_return(linked_issue)
|
||||
get "/api/v1/accounts/#{account.id}/integrations/linear/linked_issues",
|
||||
params: { conversation_id: conversation.display_id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('Sample Issue')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when linked issue is not found' do
|
||||
it 'returns error message' do
|
||||
allow(processor_service).to receive(:linked_issues).with(link).and_return(error: 'error message')
|
||||
get "/api/v1/accounts/#{account.id}/integrations/linear/linked_issues",
|
||||
params: { conversation_id: conversation.display_id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.body).to include('error message')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,187 @@
|
||||
require 'rails_helper'
|
||||
|
||||
# Stub class for ShopifyAPI response
|
||||
class ShopifyAPIResponse
|
||||
attr_reader :body
|
||||
|
||||
def initialize(body)
|
||||
@body = body
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe 'Shopify Integration API', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:unauthorized_agent) { create(:user, account: account, role: :agent) }
|
||||
let(:contact) { create(:contact, account: account, email: 'test@example.com', phone_number: '+1234567890') }
|
||||
|
||||
describe 'POST /api/v1/accounts/:account_id/integrations/shopify/auth' do
|
||||
let(:shop_domain) { 'test-store.myshopify.com' }
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'returns a redirect URL for Shopify OAuth' do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/shopify/auth",
|
||||
params: { shop_domain: shop_domain },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.parsed_body).to have_key('redirect_url')
|
||||
expect(response.parsed_body['redirect_url']).to include(shop_domain)
|
||||
end
|
||||
|
||||
it 'returns error when shop domain is missing' do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/shopify/auth",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Shop domain is required')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/shopify/auth",
|
||||
params: { shop_domain: shop_domain },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/accounts/:account_id/integrations/shopify/orders' do
|
||||
before do
|
||||
create(:integrations_hook, :shopify, account: account)
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
# rubocop:disable RSpec/AnyInstance
|
||||
let(:shopify_client) { instance_double(ShopifyAPI::Clients::Rest::Admin) }
|
||||
|
||||
let(:customers_response) do
|
||||
instance_double(
|
||||
ShopifyAPIResponse,
|
||||
body: { 'customers' => [{ 'id' => '123' }] }
|
||||
)
|
||||
end
|
||||
|
||||
let(:orders_response) do
|
||||
instance_double(
|
||||
ShopifyAPIResponse,
|
||||
body: {
|
||||
'orders' => [{
|
||||
'id' => '456',
|
||||
'email' => 'test@example.com',
|
||||
'created_at' => Time.now.iso8601,
|
||||
'total_price' => '100.00',
|
||||
'currency' => 'USD',
|
||||
'fulfillment_status' => 'fulfilled',
|
||||
'financial_status' => 'paid'
|
||||
}]
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
allow_any_instance_of(Api::V1::Accounts::Integrations::ShopifyController).to receive(:shopify_client).and_return(shopify_client)
|
||||
|
||||
allow_any_instance_of(Api::V1::Accounts::Integrations::ShopifyController).to receive(:client_id).and_return('test_client_id')
|
||||
allow_any_instance_of(Api::V1::Accounts::Integrations::ShopifyController).to receive(:client_secret).and_return('test_client_secret')
|
||||
|
||||
allow(shopify_client).to receive(:get).with(
|
||||
path: 'customers/search.json',
|
||||
query: { query: "email:#{contact.email} OR phone:#{contact.phone_number}", fields: 'id,email,phone' }
|
||||
).and_return(customers_response)
|
||||
|
||||
allow(shopify_client).to receive(:get).with(
|
||||
path: 'orders.json',
|
||||
query: { customer_id: '123', status: 'any', fields: 'id,email,created_at,total_price,currency,fulfillment_status,financial_status' }
|
||||
).and_return(orders_response)
|
||||
end
|
||||
|
||||
it 'returns orders for the contact' do
|
||||
get "/api/v1/accounts/#{account.id}/integrations/shopify/orders",
|
||||
params: { contact_id: contact.id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.parsed_body).to have_key('orders')
|
||||
expect(response.parsed_body['orders'].length).to eq(1)
|
||||
expect(response.parsed_body['orders'][0]['id']).to eq('456')
|
||||
end
|
||||
|
||||
it 'returns error when contact has no email or phone' do
|
||||
contact_without_info = create(:contact, account: account)
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/integrations/shopify/orders",
|
||||
params: { contact_id: contact_without_info.id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Contact information missing')
|
||||
end
|
||||
|
||||
it 'returns empty array when no customers found' do
|
||||
empty_customers_response = instance_double(
|
||||
ShopifyAPIResponse,
|
||||
body: { 'customers' => [] }
|
||||
)
|
||||
|
||||
allow(shopify_client).to receive(:get).with(
|
||||
path: 'customers/search.json',
|
||||
query: { query: "email:#{contact.email} OR phone:#{contact.phone_number}", fields: 'id,email,phone' }
|
||||
).and_return(empty_customers_response)
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/integrations/shopify/orders",
|
||||
params: { contact_id: contact.id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.parsed_body['orders']).to eq([])
|
||||
end
|
||||
# rubocop:enable RSpec/AnyInstance
|
||||
end
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v1/accounts/#{account.id}/integrations/shopify/orders",
|
||||
params: { contact_id: contact.id },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/accounts/:account_id/integrations/shopify' do
|
||||
before do
|
||||
create(:integrations_hook, :shopify, account: account)
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'deletes the shopify integration' do
|
||||
expect do
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/shopify",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.to change { account.hooks.count }.by(-1)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/shopify",
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,53 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Notion Authorization API', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/notion/authorization' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post "/api/v1/accounts/#{account.id}/notion/authorization"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:administrator) { create(:user, account: account, role: :administrator) }
|
||||
|
||||
it 'returns unauthorized for agent' do
|
||||
post "/api/v1/accounts/#{account.id}/notion/authorization",
|
||||
headers: agent.create_new_auth_token,
|
||||
params: { email: administrator.email },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'creates a new authorization and returns the redirect url' do
|
||||
post "/api/v1/accounts/#{account.id}/notion/authorization",
|
||||
headers: administrator.create_new_auth_token,
|
||||
params: { email: administrator.email },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
# Validate URL components
|
||||
url = response.parsed_body['url']
|
||||
uri = URI.parse(url)
|
||||
params = CGI.parse(uri.query)
|
||||
|
||||
expect(url).to start_with('https://api.notion.com/v1/oauth/authorize')
|
||||
expect(params['response_type']).to eq(['code'])
|
||||
expect(params['owner']).to eq(['user'])
|
||||
expect(params['redirect_uri']).to eq(["#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/notion/callback"])
|
||||
|
||||
# Validate state parameter exists and can be decoded back to the account
|
||||
expect(params['state']).to be_present
|
||||
decoded_account = GlobalID::Locator.locate_signed(params['state'].first, for: 'default')
|
||||
expect(decoded_account).to eq(account)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,278 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Accounts API', type: :request do
|
||||
describe 'POST /api/v1/accounts' do
|
||||
let(:email) { Faker::Internet.email }
|
||||
let(:user_full_name) { Faker::Name.name_with_middle }
|
||||
|
||||
context 'when posting to accounts with correct parameters' do
|
||||
let(:account_builder) { double }
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, email: email, account: account, name: user_full_name) }
|
||||
|
||||
before do
|
||||
allow(AccountBuilder).to receive(:new).and_return(account_builder)
|
||||
end
|
||||
|
||||
it 'calls account builder' do
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
|
||||
allow(account_builder).to receive(:perform).and_return([user, account])
|
||||
|
||||
params = { account_name: 'test', email: email, user: nil, locale: nil, user_full_name: user_full_name, password: 'Password1!' }
|
||||
|
||||
post api_v1_accounts_url,
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(AccountBuilder).to have_received(:new).with(params.except(:password).merge(user_password: params[:password]))
|
||||
expect(account_builder).to have_received(:perform)
|
||||
expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid')
|
||||
expect(response.body).to include('en')
|
||||
end
|
||||
end
|
||||
|
||||
it 'calls ChatwootCaptcha' do
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
|
||||
captcha = double
|
||||
allow(account_builder).to receive(:perform).and_return([user, account])
|
||||
allow(ChatwootCaptcha).to receive(:new).and_return(captcha)
|
||||
allow(captcha).to receive(:valid?).and_return(true)
|
||||
|
||||
params = { account_name: 'test', email: email, user: nil, locale: nil, user_full_name: user_full_name, password: 'Password1!',
|
||||
h_captcha_client_response: '123' }
|
||||
|
||||
post api_v1_accounts_url,
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(ChatwootCaptcha).to have_received(:new).with('123')
|
||||
expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid')
|
||||
expect(response.body).to include('en')
|
||||
end
|
||||
end
|
||||
|
||||
it 'renders error response on invalid params' do
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
|
||||
allow(account_builder).to receive(:perform).and_return(nil)
|
||||
|
||||
params = { account_name: nil, email: nil, user: nil, locale: 'en', user_full_name: nil }
|
||||
|
||||
post api_v1_accounts_url,
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(AccountBuilder).not_to have_received(:new)
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(response.body).to eq({ message: I18n.t('errors.signup.invalid_params') }.to_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ENABLE_ACCOUNT_SIGNUP env variable is set to false' do
|
||||
it 'responds 404 on requests' do
|
||||
params = { account_name: 'test', email: email, user_full_name: user_full_name }
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'false' do
|
||||
post api_v1_accounts_url,
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ENABLE_ACCOUNT_SIGNUP env variable is set to api_only' do
|
||||
it 'does not respond 404 on requests' do
|
||||
params = { account_name: 'test', email: email, user_full_name: user_full_name, password: 'Password1!' }
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'api_only' do
|
||||
post api_v1_accounts_url,
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/accounts/{account.id}' do
|
||||
let(:account) { create(:account) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:user_without_access) { create(:user) }
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v1/accounts/#{account.id}"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an unauthorized user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v1/accounts/#{account.id}",
|
||||
headers: user_without_access.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'shows an account' do
|
||||
account.update(name: 'new name')
|
||||
|
||||
get "/api/v1/accounts/#{account.id}",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include(account.name)
|
||||
expect(response.body).to include(account.locale)
|
||||
expect(response.body).to include(account.domain)
|
||||
expect(response.body).to include(account.support_email)
|
||||
expect(response.body).to include(account.locale)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/accounts/{account.id}/cache_keys' do
|
||||
let(:account) { create(:account) }
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
|
||||
it 'returns cache_keys as expected' do
|
||||
account.update(auto_resolve_duration: 30)
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/cache_keys",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['cache_keys'].keys).to match_array(%w[label inbox team])
|
||||
end
|
||||
|
||||
it 'sets the appropriate cache headers' do
|
||||
get "/api/v1/accounts/#{account.id}/cache_keys",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response.headers['Cache-Control']).to include('max-age=10')
|
||||
expect(response.headers['Cache-Control']).to include('private')
|
||||
expect(response.headers['Cache-Control']).to include('stale-while-revalidate=300')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /api/v1/accounts/{account.id}' do
|
||||
let(:account) { create(:account) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
put "/api/v1/accounts/#{account.id}"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an unauthorized user' do
|
||||
it 'returns unauthorized' do
|
||||
put "/api/v1/accounts/#{account.id}",
|
||||
headers: agent.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
params = {
|
||||
name: 'New Name',
|
||||
locale: 'en',
|
||||
domain: 'example.com',
|
||||
support_email: 'care@example.com',
|
||||
auto_resolve_after: 40,
|
||||
auto_resolve_message: 'Auto resolved',
|
||||
auto_resolve_ignore_waiting: false,
|
||||
timezone: 'Asia/Kolkata',
|
||||
industry: 'Technology',
|
||||
company_size: '1-10'
|
||||
}
|
||||
|
||||
it 'modifies an account' do
|
||||
put "/api/v1/accounts/#{account.id}",
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(account.reload.name).to eq(params[:name])
|
||||
expect(account.reload.locale).to eq(params[:locale])
|
||||
expect(account.reload.domain).to eq(params[:domain])
|
||||
expect(account.reload.support_email).to eq(params[:support_email])
|
||||
|
||||
%w[auto_resolve_after auto_resolve_message auto_resolve_ignore_waiting].each do |attribute|
|
||||
expect(account.reload.settings[attribute]).to eq(params[attribute.to_sym])
|
||||
end
|
||||
|
||||
%w[timezone industry company_size].each do |attribute|
|
||||
expect(account.reload.custom_attributes[attribute]).to eq(params[attribute.to_sym])
|
||||
end
|
||||
end
|
||||
|
||||
it 'updates onboarding step to invite_team if onboarding step is present in account custom attributes' do
|
||||
account.update(custom_attributes: { onboarding_step: 'account_update' })
|
||||
put "/api/v1/accounts/#{account.id}",
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(account.reload.custom_attributes['onboarding_step']).to eq('invite_team')
|
||||
end
|
||||
|
||||
it 'will not update onboarding step if onboarding step is not present in account custom attributes' do
|
||||
put "/api/v1/accounts/#{account.id}",
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(account.reload.custom_attributes['onboarding_step']).to be_nil
|
||||
end
|
||||
|
||||
it 'Throws error 422' do
|
||||
params[:name] = 'test' * 999
|
||||
|
||||
put "/api/v1/accounts/#{account.id}",
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/update_active_at' do
|
||||
let(:account) { create(:account) }
|
||||
let(: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}/update_active_at"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'modifies an account' do
|
||||
expect(agent.account_users.first.active_at).to be_nil
|
||||
post "/api/v1/accounts/#{account.id}/update_active_at",
|
||||
params: {},
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(agent.account_users.first.active_at).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,111 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Notifications Subscriptions API', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
describe 'POST /api/v1/notification_subscriptions' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post '/api/v1/notification_subscriptions'
|
||||
|
||||
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 notification subscriptions' do
|
||||
post '/api/v1/notification_subscriptions',
|
||||
params: {
|
||||
notification_subscription: {
|
||||
subscription_type: 'browser_push',
|
||||
'subscription_attributes': {
|
||||
endpoint: 'test',
|
||||
p256dh: 'test',
|
||||
auth: 'test'
|
||||
}
|
||||
}
|
||||
},
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['subscription_type']).to eq('browser_push')
|
||||
expect(json_response['subscription_attributes']['auth']).to eq('test')
|
||||
end
|
||||
|
||||
it 'returns existing notification subscription if subscription exists' do
|
||||
subscription = create(:notification_subscription, user: agent)
|
||||
post '/api/v1/notification_subscriptions',
|
||||
params: {
|
||||
notification_subscription: {
|
||||
subscription_type: 'browser_push',
|
||||
'subscription_attributes': {
|
||||
endpoint: 'test',
|
||||
p256dh: 'test',
|
||||
auth: 'test'
|
||||
}
|
||||
}
|
||||
},
|
||||
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(subscription.id)
|
||||
end
|
||||
|
||||
it 'move notification subscription to user if its of another user' do
|
||||
subscription = create(:notification_subscription, user: create(:user))
|
||||
post '/api/v1/notification_subscriptions',
|
||||
params: {
|
||||
notification_subscription: {
|
||||
subscription_type: 'browser_push',
|
||||
'subscription_attributes': {
|
||||
endpoint: 'test',
|
||||
p256dh: 'test',
|
||||
auth: 'test'
|
||||
}
|
||||
}
|
||||
},
|
||||
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(subscription.id)
|
||||
expect(json_response['user_id']).to eq(agent.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/notification_subscriptions' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
delete '/api/v1/notification_subscriptions'
|
||||
|
||||
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 'delete existing notification subscription if subscription exists' do
|
||||
subscription = create(:notification_subscription, subscription_type: 'fcm', subscription_attributes: { push_token: 'bUvZo8AYGGmCMr' },
|
||||
user: agent)
|
||||
delete '/api/v1/notification_subscriptions',
|
||||
params: {
|
||||
push_token: subscription.subscription_attributes['push_token']
|
||||
},
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect { subscription.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,339 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Profile API', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
describe 'GET /api/v1/profile' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get '/api/v1/profile'
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) { create(:user, account: account, custom_attributes: { test: 'test' }, role: :agent) }
|
||||
|
||||
it 'returns current user information' do
|
||||
get '/api/v1/profile',
|
||||
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(agent.id)
|
||||
expect(json_response['email']).to eq(agent.email)
|
||||
expect(json_response['access_token']).to eq(agent.access_token.token)
|
||||
expect(json_response['custom_attributes']['test']).to eq('test')
|
||||
expect(json_response['message_signature']).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /api/v1/profile' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
put '/api/v1/profile'
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) { create(:user, password: 'Test123!', account: account, role: :agent) }
|
||||
|
||||
it 'updates the name' do
|
||||
put '/api/v1/profile',
|
||||
params: { profile: { name: 'test' } },
|
||||
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['id']).to eq(agent.id)
|
||||
expect(json_response['name']).to eq(agent.name)
|
||||
expect(agent.name).to eq('test')
|
||||
end
|
||||
|
||||
it 'updates custom attributes' do
|
||||
put '/api/v1/profile',
|
||||
params: { profile: { phone_number: '+123456789' } },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
agent.reload
|
||||
|
||||
expect(agent.custom_attributes['phone_number']).to eq('+123456789')
|
||||
end
|
||||
|
||||
it 'updates the message_signature' do
|
||||
put '/api/v1/profile',
|
||||
params: { profile: { name: 'test', message_signature: 'Thanks\nMy Signature' } },
|
||||
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['id']).to eq(agent.id)
|
||||
expect(json_response['name']).to eq(agent.name)
|
||||
expect(agent.name).to eq('test')
|
||||
expect(json_response['message_signature']).to eq('Thanks\nMy Signature')
|
||||
end
|
||||
|
||||
it 'updates the password when current password is provided' do
|
||||
put '/api/v1/profile',
|
||||
params: { profile: { current_password: 'Test123!', password: 'Test1234!', password_confirmation: 'Test1234!' } },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(agent.reload.valid_password?('Test1234!')).to be true
|
||||
end
|
||||
|
||||
it 'does not reset the display name if updates the password' do
|
||||
display_name = agent.display_name
|
||||
|
||||
put '/api/v1/profile',
|
||||
params: { profile: { current_password: 'Test123!', password: 'Test1234!', password_confirmation: 'Test1234!' } },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(agent.reload.display_name).to eq(display_name)
|
||||
end
|
||||
|
||||
it 'throws error when current password provided is invalid' do
|
||||
put '/api/v1/profile',
|
||||
params: { profile: { current_password: 'Test', password: 'test123', password_confirmation: 'test123' } },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'validate name' do
|
||||
user_name = 'test' * 999
|
||||
put '/api/v1/profile',
|
||||
params: { profile: { name: user_name } },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
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 'updates avatar' do
|
||||
# no avatar before upload
|
||||
expect(agent.avatar.attached?).to be(false)
|
||||
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
|
||||
put '/api/v1/profile',
|
||||
params: { profile: { avatar: file } },
|
||||
headers: agent.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
agent.reload
|
||||
expect(agent.avatar.attached?).to be(true)
|
||||
end
|
||||
|
||||
it 'updates the ui settings' do
|
||||
put '/api/v1/profile',
|
||||
params: { profile: { ui_settings: { is_contact_sidebar_open: false } } },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['ui_settings']['is_contact_sidebar_open']).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an authenticated user updates email' do
|
||||
let(:agent) { create(:user, password: 'Test123!', account: account, role: :agent) }
|
||||
|
||||
it 'populates the unconfirmed email' do
|
||||
new_email = Faker::Internet.email
|
||||
put '/api/v1/profile',
|
||||
params: { profile: { email: new_email } },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
agent.reload
|
||||
|
||||
expect(agent.unconfirmed_email).to eq(new_email)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/profile/avatar' do
|
||||
let(:agent) { create(:user, password: 'Test123!', account: account, role: :agent) }
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
delete '/api/v1/profile/avatar'
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
before do
|
||||
agent.avatar.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
|
||||
end
|
||||
|
||||
it 'deletes the agent avatar' do
|
||||
delete '/api/v1/profile/avatar',
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['avatar_url']).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/profile/availability' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post '/api/v1/profile/availability'
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) { create(:user, password: 'Test123!', account: account, role: :agent) }
|
||||
|
||||
it 'updates the availability status' do
|
||||
post '/api/v1/profile/availability',
|
||||
params: { profile: { availability: 'busy', account_id: account.id } },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(OnlineStatusTracker.get_status(account.id, agent.id)).to eq('busy')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/profile/auto_offline' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post '/api/v1/profile/auto_offline'
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) { create(:user, password: 'Test123!', account: account, role: :agent) }
|
||||
|
||||
it 'updates the auto offline status' do
|
||||
post '/api/v1/profile/auto_offline',
|
||||
params: { profile: { auto_offline: false, account_id: account.id } },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['accounts'].first['auto_offline']).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /api/v1/profile/set_active_account' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
put '/api/v1/profile/set_active_account'
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) { create(:user, password: 'Test123!', account: account, role: :agent) }
|
||||
|
||||
it 'updates the last active account id' do
|
||||
put '/api/v1/profile/set_active_account',
|
||||
params: { profile: { account_id: account.id } },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/profile/resend_confirmation' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post '/api/v1/profile/resend_confirmation'
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) do
|
||||
create(:user, password: 'Test123!', email: 'test-unconfirmed@email.com', account: account, role: :agent,
|
||||
unconfirmed_email: 'test-unconfirmed@email.com')
|
||||
end
|
||||
|
||||
it 'does not send the confirmation email if the user is already confirmed' do
|
||||
expect do
|
||||
post '/api/v1/profile/resend_confirmation',
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.not_to have_enqueued_mail(Devise::Mailer, :confirmation_instructions)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
it 'resends the confirmation email if the user is unconfirmed' do
|
||||
agent.confirmed_at = nil
|
||||
agent.save!
|
||||
|
||||
expect do
|
||||
post '/api/v1/profile/resend_confirmation',
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.to have_enqueued_mail(Devise::Mailer, :confirmation_instructions)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/profile/reset_access_token' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post '/api/v1/profile/reset_access_token'
|
||||
|
||||
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 'regenerates the access token' do
|
||||
old_token = agent.access_token.token
|
||||
|
||||
post '/api/v1/profile/reset_access_token',
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
agent.reload
|
||||
expect(agent.access_token.token).not_to eq(old_token)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['access_token']).to eq(agent.access_token.token)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,110 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V1::Accounts::UploadController', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:upload_url) { "/api/v1/accounts/#{account.id}/upload/" }
|
||||
|
||||
describe 'POST /api/v1/accounts/:account_id/upload/' do
|
||||
context 'when uploading a file' do
|
||||
let(:file) { fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png') }
|
||||
|
||||
it 'uploads the image when authorized' do
|
||||
post upload_url,
|
||||
headers: user.create_new_auth_token,
|
||||
params: { attachment: file }
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
blob = response.parsed_body
|
||||
expect(blob['errors']).to be_nil
|
||||
expect(blob['file_url']).to be_present
|
||||
expect(blob['blob_id']).to be_present
|
||||
end
|
||||
|
||||
it 'does not upload when unauthorized' do
|
||||
post upload_url,
|
||||
headers: {},
|
||||
params: { attachment: file }
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
blob = response.parsed_body
|
||||
expect(blob['errors']).to be_present
|
||||
expect(blob['file_url']).to be_nil
|
||||
expect(blob['blob_key']).to be_nil
|
||||
expect(blob['blob_id']).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when uploading from a URL' do
|
||||
let(:valid_external_url) { 'http://example.com/image.jpg' }
|
||||
|
||||
before do
|
||||
stub_request(:get, valid_external_url)
|
||||
.to_return(status: 200, body: File.new(Rails.root.join('spec/assets/avatar.png')), headers: { 'Content-Type' => 'image/png' })
|
||||
end
|
||||
|
||||
it 'uploads the image from URL when authorized' do
|
||||
post upload_url,
|
||||
headers: user.create_new_auth_token,
|
||||
params: { external_url: valid_external_url }
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
blob = response.parsed_body
|
||||
expect(blob['error']).to be_nil
|
||||
expect(blob['file_url']).to be_present
|
||||
expect(blob['blob_id']).to be_present
|
||||
end
|
||||
|
||||
it 'handles invalid URL format' do
|
||||
post upload_url,
|
||||
headers: user.create_new_auth_token,
|
||||
params: { external_url: 'not_a_url' }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Invalid URL provided')
|
||||
end
|
||||
|
||||
it 'handles URL with unsupported protocol' do
|
||||
post upload_url,
|
||||
headers: user.create_new_auth_token,
|
||||
params: { external_url: 'ftp://example.com/image.jpg' }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Invalid URL provided')
|
||||
end
|
||||
|
||||
it 'handles unreachable URLs' do
|
||||
stub_request(:get, 'http://nonexistent.example.com')
|
||||
.to_raise(SocketError.new('Failed to open TCP connection'))
|
||||
|
||||
post upload_url,
|
||||
headers: user.create_new_auth_token,
|
||||
params: { external_url: 'http://nonexistent.example.com' }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Invalid URL provided')
|
||||
end
|
||||
|
||||
it 'handles HTTP errors' do
|
||||
stub_request(:get, 'http://error.example.com')
|
||||
.to_return(status: 404)
|
||||
|
||||
post upload_url,
|
||||
headers: user.create_new_auth_token,
|
||||
params: { external_url: 'http://error.example.com' }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to start_with('Failed to fetch file from URL')
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns an error when no file or URL is provided' do
|
||||
post upload_url,
|
||||
headers: user.create_new_auth_token,
|
||||
params: {}
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('No file or URL provided')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe '/api/v1/widget/campaigns', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:web_widget) { create(:channel_widget, account: account) }
|
||||
let!(:campaign_1) { create(:campaign, inbox: web_widget.inbox, enabled: true, account: account, trigger_rules: { url: 'https://test.com' }) }
|
||||
let!(:campaign_2) { create(:campaign, inbox: web_widget.inbox, enabled: false, account: account, trigger_rules: { url: 'https://test.com' }) }
|
||||
|
||||
describe 'GET /api/v1/widget/campaigns' do
|
||||
let(:params) { { website_token: web_widget.website_token } }
|
||||
|
||||
context 'when campaigns feature is enabled' do
|
||||
before do
|
||||
account.enable_features!('campaigns')
|
||||
end
|
||||
|
||||
it 'returns the list of enabled campaigns' do
|
||||
get '/api/v1/widget/campaigns', params: params
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response.length).to eq 1
|
||||
expect(json_response.pluck('id')).to include(campaign_1.display_id)
|
||||
expect(json_response.pluck('id')).not_to include(campaign_2.display_id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when campaigns feature is disabled' do
|
||||
before do
|
||||
account.disable_features!('campaigns')
|
||||
end
|
||||
|
||||
it 'returns empty array' do
|
||||
get '/api/v1/widget/campaigns', params: params
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response).to eq []
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid website token' do
|
||||
it 'returns not found status' do
|
||||
get '/api/v1/widget/campaigns', params: { website_token: '' }
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,78 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe '/api/v1/widget/config', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:web_widget) { create(:channel_widget, account: account) }
|
||||
let!(:contact) { create(:contact, account: account) }
|
||||
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: web_widget.inbox) }
|
||||
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
|
||||
let(:token) { Widget::TokenService.new(payload: payload).generate_token }
|
||||
|
||||
describe 'POST /api/v1/widget/config' do
|
||||
let(:params) { { website_token: web_widget.website_token } }
|
||||
let(:response_keys) { %w[website_channel_config contact global_config] }
|
||||
|
||||
context 'with invalid website token' do
|
||||
it 'returns not found' do
|
||||
post '/api/v1/widget/config', params: { website_token: '' }
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with correct website token and missing X-Auth-Token' do
|
||||
it 'returns widget config along with a new contact' do
|
||||
expect do
|
||||
post '/api/v1/widget/config',
|
||||
params: params,
|
||||
as: :json
|
||||
end.to change(Contact, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = response.parsed_body
|
||||
expect(response_data.keys).to include(*response_keys)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with correct website token and valid X-Auth-Token' do
|
||||
it 'returns widget config along with the same contact' do
|
||||
expect do
|
||||
post '/api/v1/widget/config',
|
||||
params: params,
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
end.not_to change(Contact, :count)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = response.parsed_body
|
||||
expect(response_data.keys).to include(*response_keys)
|
||||
expect(response_data['contact']['pubsub_token']).to eq(contact_inbox.pubsub_token)
|
||||
end
|
||||
|
||||
it 'returns 401 if account is suspended' do
|
||||
account.update!(status: :suspended)
|
||||
|
||||
post '/api/v1/widget/config',
|
||||
params: params,
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with correct website token and invalid X-Auth-Token' do
|
||||
it 'returns widget config and new contact with error message' do
|
||||
expect do
|
||||
post '/api/v1/widget/config',
|
||||
params: params,
|
||||
headers: { 'X-Auth-Token' => 'invalid token' },
|
||||
as: :json
|
||||
end.to change(Contact, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = response.parsed_body
|
||||
expect(response_data.keys).to include(*response_keys)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,209 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe '/api/v1/widget/contacts', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:web_widget) { create(:channel_widget, account: account) }
|
||||
let(:contact) { create(:contact, account: account, email: 'test@test.com', phone_number: '+745623239') }
|
||||
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: web_widget.inbox) }
|
||||
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
|
||||
let(:token) { Widget::TokenService.new(payload: payload).generate_token }
|
||||
|
||||
describe 'PATCH /api/v1/widget/contact' do
|
||||
let(:params) { { website_token: web_widget.website_token, identifier: 'test' } }
|
||||
|
||||
context 'with invalid website token' do
|
||||
it 'returns unauthorized' do
|
||||
patch '/api/v1/widget/contact', params: { website_token: '' }
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with correct website token' do
|
||||
let(:identify_action) { double }
|
||||
|
||||
before do
|
||||
allow(ContactIdentifyAction).to receive(:new).and_return(identify_action)
|
||||
allow(identify_action).to receive(:perform).and_return(contact)
|
||||
end
|
||||
|
||||
it 'calls contact identify' do
|
||||
patch '/api/v1/widget/contact',
|
||||
params: params,
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expected_params = { contact: contact, params: params, discard_invalid_attrs: true }
|
||||
expect(ContactIdentifyAction).to have_received(:new).with(expected_params)
|
||||
expect(identify_action).to have_received(:perform)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with update contact' do
|
||||
let(:params) { { website_token: web_widget.website_token } }
|
||||
|
||||
it 'dont update phone number if invalid phone number passed' do
|
||||
patch '/api/v1/widget/contact',
|
||||
params: params.merge({ phone_number: '45623239' }),
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
body = response.parsed_body
|
||||
expect(body['has_phone_number']).to be true
|
||||
contact.reload
|
||||
expect(contact.phone_number).to eq('+745623239')
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
it 'update phone number if valid phone number passed' do
|
||||
patch '/api/v1/widget/contact',
|
||||
params: params.merge({ phone_number: '+245623239' }),
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
body = response.parsed_body
|
||||
expect(body['has_phone_number']).to be true
|
||||
contact.reload
|
||||
expect(contact.phone_number).to eq('+245623239')
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
it 'dont update email if invalid email passed' do
|
||||
patch '/api/v1/widget/contact',
|
||||
params: params.merge({ email: 'test@' }),
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
body = response.parsed_body
|
||||
expect(body['has_email']).to be true
|
||||
contact.reload
|
||||
expect(contact.email).to eq('test@test.com')
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
it 'dont update email if empty value email passed' do
|
||||
patch '/api/v1/widget/contact',
|
||||
params: params.merge({ email: '' }),
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
body = response.parsed_body
|
||||
expect(body['has_email']).to be true
|
||||
contact.reload
|
||||
expect(contact.email).to eq('test@test.com')
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
it 'dont update email if nil value email passed' do
|
||||
patch '/api/v1/widget/contact',
|
||||
params: params.merge({ email: nil }),
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
body = response.parsed_body
|
||||
expect(body['has_email']).to be true
|
||||
contact.reload
|
||||
expect(contact.email).to eq('test@test.com')
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
it 'update email if valid email passed' do
|
||||
patch '/api/v1/widget/contact',
|
||||
params: params.merge({ email: 'test-1@test.com' }),
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
body = response.parsed_body
|
||||
expect(body['has_email']).to be true
|
||||
contact.reload
|
||||
expect(contact.email).to eq('test-1@test.com')
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PATCH /api/v1/widget/contact/set_user' do
|
||||
let(:params) { { website_token: web_widget.website_token, identifier: 'test' } }
|
||||
let(:web_widget) { create(:channel_widget, account: account, hmac_mandatory: true) }
|
||||
let(:correct_identifier_hash) { OpenSSL::HMAC.hexdigest('sha256', web_widget.hmac_token, params[:identifier].to_s) }
|
||||
let(:incorrect_identifier_hash) { 'test' }
|
||||
|
||||
context 'when the current contact identifier is different from param identifier' do
|
||||
before do
|
||||
contact.update(identifier: 'random')
|
||||
end
|
||||
|
||||
it 'return a new contact for the provided identifier' do
|
||||
patch '/api/v1/widget/contact/set_user',
|
||||
params: params.merge(identifier_hash: correct_identifier_hash),
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
body = response.parsed_body
|
||||
expect(body['id']).not_to eq(contact.id)
|
||||
expect(body['widget_auth_token']).not_to be_nil
|
||||
expect(Contact.find(body['id']).contact_inboxes.first.hmac_verified?).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with mandatory hmac' do
|
||||
let(:identify_action) { double }
|
||||
|
||||
before do
|
||||
allow(ContactIdentifyAction).to receive(:new).and_return(identify_action)
|
||||
allow(identify_action).to receive(:perform).and_return(contact)
|
||||
end
|
||||
|
||||
it 'returns success when correct identifier hash is provided' do
|
||||
patch '/api/v1/widget/contact/set_user',
|
||||
params: params.merge(identifier_hash: correct_identifier_hash),
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
it 'returns error when incorrect identifier hash is provided' do
|
||||
patch '/api/v1/widget/contact/set_user',
|
||||
params: params.merge(identifier_hash: incorrect_identifier_hash),
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns error when identifier hash is blank' do
|
||||
patch '/api/v1/widget/contact/set_user',
|
||||
params: params.merge(identifier_hash: ''),
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns error when identifier hash is not provided' do
|
||||
patch '/api/v1/widget/contact/set_user',
|
||||
params: params,
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/widget/destroy_custom_attributes' do
|
||||
let(:params) { { website_token: web_widget.website_token, identifier: 'test', custom_attributes: ['test'] } }
|
||||
|
||||
context 'with invalid website token' do
|
||||
it 'returns unauthorized' do
|
||||
post '/api/v1/widget/destroy_custom_attributes', params: { website_token: '' }
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with correct website token' do
|
||||
it 'calls destroy custom attributes' do
|
||||
post '/api/v1/widget/destroy_custom_attributes',
|
||||
params: params,
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
expect(contact.reload.custom_attributes).to eq({})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,334 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:web_widget) { create(:channel_widget, account: account) }
|
||||
let(:contact) { create(:contact, account: account, email: nil) }
|
||||
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: web_widget.inbox) }
|
||||
let(:second_session) { 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) }
|
||||
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
|
||||
let(:token) { Widget::TokenService.new(payload: payload).generate_token }
|
||||
let(:token_without_conversation) do
|
||||
Widget::TokenService.new(payload: { source_id: second_session.source_id, inbox_id: web_widget.inbox.id }).generate_token
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
{
|
||||
website_token: web_widget.website_token,
|
||||
contact: {
|
||||
name: 'contact-name',
|
||||
email: 'contact-email@chatwoot.com',
|
||||
phone_number: '+919745313456'
|
||||
},
|
||||
message: {
|
||||
content: 'This is a test message'
|
||||
},
|
||||
custom_attributes: { order_id: '12345' }
|
||||
}
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/widget/conversations' do
|
||||
context 'with a conversation' do
|
||||
it 'returns the correct conversation params' do
|
||||
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
||||
get '/api/v1/widget/conversations',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: { website_token: web_widget.website_token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response['id']).to eq(conversation.display_id)
|
||||
expect(json_response['status']).to eq(conversation.status)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a conversation but invalid source id' do
|
||||
it 'returns the correct conversation params' do
|
||||
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
||||
|
||||
payload = { source_id: 'invalid source id', inbox_id: web_widget.inbox.id }
|
||||
token = Widget::TokenService.new(payload: payload).generate_token
|
||||
get '/api/v1/widget/conversations',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: { website_token: web_widget.website_token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/widget/conversations' do
|
||||
it 'creates a conversation with correct details' do
|
||||
post '/api/v1/widget/conversations',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: conversation_params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['id']).not_to be_nil
|
||||
expect(json_response['contact']['email']).to eq 'contact-email@chatwoot.com'
|
||||
expect(json_response['contact']['phone_number']).to eq '+919745313456'
|
||||
expect(json_response['contact']['name']).to eq 'contact-name'
|
||||
end
|
||||
|
||||
it 'creates a conversation with correct message and custom attributes' do
|
||||
post '/api/v1/widget/conversations',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: conversation_params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['custom_attributes']['order_id']).to eq '12345'
|
||||
expect(json_response['messages'][0]['content']).to eq 'This is a test message'
|
||||
expect(json_response['messages'][0]['message_type']).to eq 0
|
||||
end
|
||||
|
||||
it 'create a conversation with a name and without an email' do
|
||||
post '/api/v1/widget/conversations',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: {
|
||||
website_token: web_widget.website_token,
|
||||
contact: {
|
||||
name: 'alphy'
|
||||
},
|
||||
message: {
|
||||
content: 'This is a test message'
|
||||
}
|
||||
},
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['id']).not_to be_nil
|
||||
expect(json_response['contact']['email']).to be_nil
|
||||
expect(json_response['contact']['name']).to eq 'alphy'
|
||||
expect(json_response['messages'][0]['content']).to eq 'This is a test message'
|
||||
end
|
||||
|
||||
it 'does not update the name if the contact already exist' do
|
||||
existing_contact = create(:contact, account: account, email: 'contact-email@chatwoot.com')
|
||||
|
||||
post '/api/v1/widget/conversations',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: {
|
||||
website_token: web_widget.website_token,
|
||||
contact: {
|
||||
name: 'contact-name',
|
||||
email: existing_contact.email,
|
||||
phone_number: '+919745313456'
|
||||
},
|
||||
message: {
|
||||
content: 'This is a test message'
|
||||
},
|
||||
custom_attributes: { order_id: '12345' }
|
||||
},
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['id']).not_to be_nil
|
||||
expect(json_response['contact']['email']).to eq existing_contact.email
|
||||
expect(json_response['contact']['name']).not_to eq 'contact-name'
|
||||
expect(json_response['contact']['phone_number']).to eq '+919745313456'
|
||||
expect(json_response['custom_attributes']['order_id']).to eq '12345'
|
||||
expect(json_response['messages'][0]['content']).to eq 'This is a test message'
|
||||
end
|
||||
|
||||
it 'doesnt not add phone number if the invalid phone number is provided' do
|
||||
existing_contact = create(:contact, account: account)
|
||||
|
||||
post '/api/v1/widget/conversations',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: {
|
||||
website_token: web_widget.website_token,
|
||||
contact: {
|
||||
name: 'contact-name-1',
|
||||
email: existing_contact.email,
|
||||
phone_number: '13456'
|
||||
},
|
||||
message: {
|
||||
content: 'This is a test message'
|
||||
}
|
||||
},
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['contact']['phone_number']).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/widget/conversations/toggle_typing' do
|
||||
context 'with a conversation' do
|
||||
it 'dispatches the correct typing status' do
|
||||
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
||||
post '/api/v1/widget/conversations/toggle_typing',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: { typing_status: 'on', website_token: web_widget.website_token },
|
||||
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: contact })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/widget/conversations/update_last_seen' do
|
||||
context 'with a conversation' do
|
||||
it 'returns the correct conversation params' do
|
||||
current_time = DateTime.now.utc
|
||||
allow(DateTime).to receive(:now).and_return(current_time)
|
||||
|
||||
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
||||
expect(conversation.contact_last_seen_at).to be_nil
|
||||
expect(Conversations::UpdateMessageStatusJob).to receive(:perform_later).with(conversation.id, current_time)
|
||||
|
||||
post '/api/v1/widget/conversations/update_last_seen',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: { website_token: web_widget.website_token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
expect(conversation.reload.contact_last_seen_at).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/widget/conversations/transcript' do
|
||||
context 'with a conversation' do
|
||||
it 'sends transcript email' do
|
||||
contact.update(email: 'test@test.com')
|
||||
mailer = double
|
||||
allow(ConversationReplyMailer).to receive(:with).and_return(mailer)
|
||||
allow(mailer).to receive(:conversation_transcript)
|
||||
|
||||
post '/api/v1/widget/conversations/transcript',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: { website_token: web_widget.website_token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(mailer).to have_received(:conversation_transcript).with(conversation, 'test@test.com')
|
||||
contact.update(email: nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/widget/conversations/toggle_status' do
|
||||
context 'when user end conversation from widget' do
|
||||
it 'resolves the conversation' do
|
||||
expect(conversation.open?).to be true
|
||||
|
||||
get '/api/v1/widget/conversations/toggle_status',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: { website_token: web_widget.website_token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(conversation.reload.resolved?).to be true
|
||||
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 was resolved by #{contact.name}"
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when end conversation is not permitted' do
|
||||
before do
|
||||
web_widget.end_conversation = false
|
||||
web_widget.save!
|
||||
end
|
||||
|
||||
it 'returns action not permitted status' do
|
||||
expect(conversation.open?).to be true
|
||||
|
||||
get '/api/v1/widget/conversations/toggle_status',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: { website_token: web_widget.website_token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(conversation.reload.resolved?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a token without any conversation is used' do
|
||||
it 'returns not found status' do
|
||||
get '/api/v1/widget/conversations/toggle_status',
|
||||
headers: { 'X-Auth-Token' => token_without_conversation },
|
||||
params: { website_token: web_widget.website_token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/widget/conversations/set_custom_attributes' do
|
||||
let(:params) { { website_token: web_widget.website_token, custom_attributes: { 'product_name': 'Chatwoot' } } }
|
||||
|
||||
context 'with invalid website token' do
|
||||
it 'returns unauthorized' do
|
||||
post '/api/v1/widget/conversations/set_custom_attributes', params: { website_token: '' }
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with correct website token' do
|
||||
it 'sets the values when provided' do
|
||||
post '/api/v1/widget/conversations/set_custom_attributes',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
conversation.reload
|
||||
# conversation custom attributes should have "product_name" key with value "Chatwoot"
|
||||
expect(conversation.custom_attributes).to include('product_name' => 'Chatwoot')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/widget/conversations/destroy_custom_attributes' do
|
||||
let(:params) { { website_token: web_widget.website_token, custom_attribute: ['product_name'] } }
|
||||
|
||||
context 'with invalid website token' do
|
||||
it 'returns unauthorized' do
|
||||
post '/api/v1/widget/conversations/destroy_custom_attributes', params: { website_token: '' }
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with correct website token' do
|
||||
it 'sets the values when provided' do
|
||||
# ensure conversation has the attribute
|
||||
conversation.custom_attributes = { 'product_name': 'Chatwoot' }
|
||||
conversation.save!
|
||||
expect(conversation.custom_attributes).to include('product_name' => 'Chatwoot')
|
||||
|
||||
post '/api/v1/widget/conversations/destroy_custom_attributes',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
conversation.reload
|
||||
# conversation custom attributes should not have "product_name" key with value "Chatwoot"
|
||||
expect(conversation.custom_attributes).not_to include('product_name' => 'Chatwoot')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe '/api/v1/widget/direct_uploads', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:web_widget) { create(:channel_widget, account: account) }
|
||||
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) }
|
||||
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
|
||||
let(:token) { Widget::TokenService.new(payload: payload).generate_token }
|
||||
|
||||
describe 'POST /api/v1/widget/direct_uploads' do
|
||||
context 'when post request is made' do
|
||||
before do
|
||||
token
|
||||
contact
|
||||
payload
|
||||
end
|
||||
|
||||
it 'creates attachment message in conversation' do
|
||||
post api_v1_widget_direct_uploads_url,
|
||||
params: {
|
||||
website_token: web_widget.website_token,
|
||||
blob: {
|
||||
filename: 'avatar.png',
|
||||
byte_size: '1234',
|
||||
checksum: 'dsjbsdhbfif3874823mnsdbf',
|
||||
content_type: 'image/png'
|
||||
}
|
||||
},
|
||||
headers: { 'X-Auth-Token' => token }
|
||||
|
||||
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
|
||||
@@ -0,0 +1,39 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe '/api/v1/widget/events', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:web_widget) { create(:channel_widget, account: account) }
|
||||
let(:contact) { create(:contact, account: account) }
|
||||
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: web_widget.inbox) }
|
||||
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
|
||||
let(:token) { Widget::TokenService.new(payload: payload).generate_token }
|
||||
|
||||
describe 'POST /api/v1/widget/events' do
|
||||
let(:params) { { website_token: web_widget.website_token, name: 'webwidget.triggered', event_info: { test_id: 'test' } } }
|
||||
|
||||
context 'with invalid website token' do
|
||||
it 'returns unauthorized' do
|
||||
post '/api/v1/widget/events', params: { website_token: '' }
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with correct website token' do
|
||||
before do
|
||||
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
||||
end
|
||||
|
||||
it 'dispatches the webwidget event' do
|
||||
post '/api/v1/widget/events',
|
||||
params: params,
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
||||
.with(params[:name], anything, contact_inbox: contact_inbox,
|
||||
event_info: { test_id: 'test', browser_language: nil, widget_language: nil, browser: anything })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe '/api/v1/widget/inbox_members', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:web_widget) { create(:channel_widget, account: account) }
|
||||
let(:agent_1) { create(:user, account: account) }
|
||||
let(:agent_2) { create(:user, account: account) }
|
||||
|
||||
before do
|
||||
create(:inbox_member, user: agent_1, inbox: web_widget.inbox)
|
||||
create(:inbox_member, user: agent_2, inbox: web_widget.inbox)
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/widget/inbox_members' do
|
||||
let(:params) { { website_token: web_widget.website_token } }
|
||||
|
||||
context 'with correct website token' do
|
||||
it 'returns the list of agents' do
|
||||
get '/api/v1/widget/inbox_members', params: params
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['payload'].length).to eq 2
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid website token' do
|
||||
it 'returns the list of agents' do
|
||||
get '/api/v1/widget/inbox_members', params: { website_token: '' }
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,74 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe '/api/v1/widget/integrations/dyte', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:web_widget) { create(:channel_widget, account: account) }
|
||||
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) }
|
||||
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
|
||||
let(:token) { Widget::TokenService.new(payload: payload).generate_token }
|
||||
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
|
||||
|
||||
before do
|
||||
create(:integrations_hook, :dyte, account: account)
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/widget/integrations/dyte/add_participant_to_meeting' do
|
||||
context 'when token is invalid' do
|
||||
it 'returns error' do
|
||||
post add_participant_to_meeting_api_v1_widget_integrations_dyte_url,
|
||||
params: { website_token: web_widget.website_token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when token is valid' do
|
||||
context 'when message is not an integration message' do
|
||||
it 'returns error' do
|
||||
post add_participant_to_meeting_api_v1_widget_integrations_dyte_url,
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: { website_token: web_widget.website_token, message_id: message.id },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body['error']).to eq('Invalid message type. Action not permitted')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when message is an integration message' 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: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns auth_token' do
|
||||
post add_participant_to_meeting_api_v1_widget_integrations_dyte_url,
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: { website_token: web_widget.website_token, message_id: integration_message.id },
|
||||
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
|
||||
end
|
||||
@@ -0,0 +1,79 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe '/api/v1/widget/labels', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:web_widget) { create(:channel_widget, account: account) }
|
||||
let(:contact) { create(:contact, account: account) }
|
||||
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) }
|
||||
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
|
||||
let(:token) { Widget::TokenService.new(payload: payload).generate_token }
|
||||
|
||||
describe 'POST /api/v1/widget/labels' do
|
||||
let(:params) { { website_token: web_widget.website_token, label: 'customer-support' } }
|
||||
|
||||
context 'with correct website token and undefined label' do
|
||||
it 'does not add the label' do
|
||||
post '/api/v1/widget/labels',
|
||||
params: params,
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(conversation.reload.label_list.count).to eq 0
|
||||
end
|
||||
end
|
||||
|
||||
context 'with correct website token and a defined label' do
|
||||
before do
|
||||
account.labels.create!(title: 'customer-support')
|
||||
end
|
||||
|
||||
it 'add the label to the conversation' do
|
||||
post '/api/v1/widget/labels',
|
||||
params: params,
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(conversation.reload.label_list.count).to eq 1
|
||||
expect(conversation.reload.label_list.first).to eq 'customer-support'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid website token' do
|
||||
it 'returns the list of labels' do
|
||||
post '/api/v1/widget/labels', params: { website_token: '' }
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/widget/labels' do
|
||||
before do
|
||||
conversation.label_list.add('customer-support')
|
||||
conversation.save!
|
||||
end
|
||||
|
||||
let(:params) { { website_token: web_widget.website_token, label: 'customer-support' } }
|
||||
|
||||
context 'with correct website token' do
|
||||
it 'returns the list of labels' do
|
||||
delete "/api/v1/widget/labels/#{params[:label]}",
|
||||
params: params,
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(conversation.reload.label_list.count).to eq 0
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid website token' do
|
||||
it 'returns the list of labels' do
|
||||
delete "/api/v1/widget/labels/#{params[:label]}", params: { website_token: '' }
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,220 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe '/api/v1/widget/messages', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:web_widget) { create(:channel_widget, account: account) }
|
||||
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) }
|
||||
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
|
||||
let(:token) { Widget::TokenService.new(payload: payload).generate_token }
|
||||
|
||||
before do |example|
|
||||
2.times.each { create(:message, account: account, inbox: web_widget.inbox, conversation: conversation) } unless example.metadata[:skip_before]
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/widget/messages' do
|
||||
context 'when get request is made' do
|
||||
it 'returns messages in conversation' do
|
||||
get api_v1_widget_messages_url,
|
||||
params: { website_token: web_widget.website_token },
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
# 2 messages created + 2 messages by the email hook
|
||||
expect(json_response['payload'].length).to eq(4)
|
||||
expect(json_response['meta']).not_to be_empty
|
||||
end
|
||||
|
||||
it 'returns empty messages', :skip_before do
|
||||
get api_v1_widget_messages_url,
|
||||
params: { website_token: web_widget.website_token },
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['payload'].length).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/widget/messages' do
|
||||
context 'when post request is made' do
|
||||
it 'creates message in conversation' do
|
||||
conversation.destroy! # Test all params
|
||||
message_params = { content: 'hello world', timestamp: Time.current }
|
||||
post api_v1_widget_messages_url,
|
||||
params: { website_token: web_widget.website_token, message: message_params },
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['content']).to eq(message_params[:content])
|
||||
end
|
||||
|
||||
it 'does not create the message' do
|
||||
conversation.destroy! # Test all params
|
||||
message_params = { content: "#{'h' * 150 * 1000}a", timestamp: Time.current }
|
||||
post api_v1_widget_messages_url,
|
||||
params: { website_token: web_widget.website_token, message: message_params },
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response['message']).to eq('Content is too long (maximum is 150000 characters)')
|
||||
end
|
||||
|
||||
it 'creates message in conversation with a valid reply to' do
|
||||
message_params = { content: 'hello world reply', timestamp: Time.current, reply_to: conversation.messages.first.id }
|
||||
post api_v1_widget_messages_url,
|
||||
params: { website_token: web_widget.website_token, message: message_params },
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['content']).to eq(message_params[:content])
|
||||
expect(json_response['content_attributes']['in_reply_to']).to eq(conversation.messages.first.id)
|
||||
# check nil for external id since this is a web widget conversation
|
||||
expect(json_response['content_attributes']['in_reply_to_external_id']).to be_nil
|
||||
end
|
||||
|
||||
it 'creates message in conversation with an in-valid reply to' do
|
||||
message_params = { content: 'hello world reply', timestamp: Time.current, reply_to: conversation.messages.first.id + 300 }
|
||||
post api_v1_widget_messages_url,
|
||||
params: { website_token: web_widget.website_token, message: message_params },
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['content']).to eq(message_params[:content])
|
||||
expect(json_response['content_attributes']['in_reply_to']).to be_nil
|
||||
expect(json_response['content_attributes']['in_reply_to_external_id']).to be_nil
|
||||
end
|
||||
|
||||
it 'creates attachment message in conversation' do
|
||||
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
|
||||
message_params = { content: 'hello world', timestamp: Time.current, attachments: [file] }
|
||||
post api_v1_widget_messages_url,
|
||||
params: { website_token: web_widget.website_token, message: message_params },
|
||||
headers: { 'X-Auth-Token' => token }
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['content']).to eq(message_params[:content])
|
||||
|
||||
expect(conversation.messages.last.attachments.first.file.present?).to be(true)
|
||||
expect(conversation.messages.last.attachments.first.file_type).to eq('image')
|
||||
end
|
||||
|
||||
it 'does not reopen conversation when conversation is muted' do
|
||||
conversation.mute!
|
||||
|
||||
message_params = { content: 'hello world', timestamp: Time.current }
|
||||
post api_v1_widget_messages_url,
|
||||
params: { website_token: web_widget.website_token, message: message_params },
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(conversation.reload.resolved?).to be(true)
|
||||
end
|
||||
|
||||
it 'does not create resolved activity messages when snoozed conversation is opened' do
|
||||
conversation.snoozed!
|
||||
|
||||
message_params = { content: 'hello world', timestamp: Time.current }
|
||||
post api_v1_widget_messages_url,
|
||||
params: { website_token: web_widget.website_token, message: message_params },
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(Conversations::ActivityMessageJob).not_to have_been_enqueued.at_least(:once).with(
|
||||
conversation,
|
||||
{
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: :activity,
|
||||
content: "Conversation was resolved by #{contact.name}"
|
||||
}
|
||||
)
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(conversation.reload.open?).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /api/v1/widget/messages' do
|
||||
context 'when put request is made with non existing email' do
|
||||
it 'updates message in conversation and creates a new contact' do
|
||||
message = create(:message, content_type: 'input_email', account: account, inbox: web_widget.inbox, conversation: conversation)
|
||||
email = Faker::Internet.email
|
||||
contact_params = { email: email }
|
||||
put api_v1_widget_message_url(message.id),
|
||||
params: { website_token: web_widget.website_token, contact: contact_params },
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
message.reload
|
||||
expect(message.submitted_email).to eq(email)
|
||||
expect(message.conversation.contact.email).to eq(email)
|
||||
expect(message.conversation.contact.name).to eq(email.split('@')[0])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when put request is made with invalid email' do
|
||||
it 'rescues the error' do
|
||||
message = create(:message, account: account, content_type: 'input_email', inbox: web_widget.inbox, conversation: conversation)
|
||||
contact_params = { email: nil }
|
||||
put api_v1_widget_message_url(message.id),
|
||||
params: { website_token: web_widget.website_token, contact: contact_params },
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when put request is made with existing email' do
|
||||
it 'updates message in conversation and deletes the current contact' do
|
||||
message = create(:message, account: account, content_type: 'input_email', inbox: web_widget.inbox, conversation: conversation)
|
||||
email = Faker::Internet.email
|
||||
existing_contact = create(:contact, account: account, email: email, name: 'John Doe')
|
||||
contact_params = { email: email }
|
||||
put api_v1_widget_message_url(message.id),
|
||||
params: { website_token: web_widget.website_token, contact: contact_params },
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
message.reload
|
||||
expect(existing_contact.reload.name).to eq('John Doe')
|
||||
expect { contact.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'ignores the casing of email, updates message in conversation and deletes the current contact' do
|
||||
message = create(:message, content_type: 'input_email', account: account, inbox: web_widget.inbox, conversation: conversation)
|
||||
email = Faker::Internet.email
|
||||
create(:contact, account: account, email: email)
|
||||
contact_params = { email: email.upcase }
|
||||
put api_v1_widget_message_url(message.id),
|
||||
params: { website_token: web_widget.website_token, contact: contact_params },
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
message.reload
|
||||
expect { contact.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,171 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V2::Accounts::LiveReports', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let!(:team) { create(:team, account: account) }
|
||||
let(:team_member) { create(:team_member, team: team, user: admin) }
|
||||
|
||||
describe 'GET /api/v2/accounts/{account.id}/live_reports/conversation_metrics' do
|
||||
context 'when unauthenticated' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/live_reports/conversation_metrics"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated but not authorized' do
|
||||
it 'returns forbidden' do
|
||||
get "/api/v2/accounts/#{account.id}/live_reports/conversation_metrics",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated and authorized' do
|
||||
before do
|
||||
create(:conversation, :with_assignee, account: account, status: :open)
|
||||
create(:conversation, account: account, status: :open)
|
||||
create(:conversation, :with_assignee, account: account, status: :pending)
|
||||
create(:conversation, :with_assignee, account: account, status: :open) do |conversation|
|
||||
create(:message, account: account, conversation: conversation, message_type: :outgoing)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns conversation metrics' do
|
||||
get "/api/v2/accounts/#{account.id}/live_reports/conversation_metrics",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
response_data = response.parsed_body
|
||||
expect(response_data['open']).to eq(3)
|
||||
expect(response_data['unattended']).to eq(2)
|
||||
expect(response_data['unassigned']).to eq(1)
|
||||
expect(response_data['pending']).to eq(1)
|
||||
end
|
||||
|
||||
context 'with team_id parameter' do
|
||||
before do
|
||||
create(:conversation, account: account, status: :open, team_id: team.id)
|
||||
create(:conversation, account: account, status: :open)
|
||||
end
|
||||
|
||||
it 'returns metrics filtered by team' do
|
||||
get "/api/v2/accounts/#{account.id}/live_reports/conversation_metrics",
|
||||
params: { team_id: team.id },
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
response_data = response.parsed_body
|
||||
expect(response_data['open']).to eq(1)
|
||||
expect(response_data['unattended']).to eq(1)
|
||||
expect(response_data['unassigned']).to eq(1)
|
||||
expect(response_data['pending']).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v2/accounts/{account.id}/live_reports/grouped_conversation_metrics' do
|
||||
context 'when unauthenticated' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/live_reports/grouped_conversation_metrics",
|
||||
params: { group_by: 'team_id' }
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated but not authorized' do
|
||||
it 'returns forbidden' do
|
||||
get "/api/v2/accounts/#{account.id}/live_reports/grouped_conversation_metrics",
|
||||
params: { group_by: 'team_id' },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid group_by parameter' do
|
||||
it 'returns unprocessable_entity error' do
|
||||
get "/api/v2/accounts/#{account.id}/live_reports/grouped_conversation_metrics",
|
||||
params: { group_by: 'invalid_param' },
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('invalid group_by')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when grouped by team_id' do
|
||||
let(:assignee1) { create(:user, account: account) }
|
||||
|
||||
before do
|
||||
create(:conversation, account: account, status: :open, team_id: team.id)
|
||||
create(:conversation, account: account, status: :open, team_id: team.id)
|
||||
create(:conversation, account: account, status: :open, team_id: team.id) do |conversation|
|
||||
create(:message, account: account, conversation: conversation, message_type: :outgoing)
|
||||
end
|
||||
|
||||
create(:conversation, account: account, status: :open, assignee_id: assignee1.id)
|
||||
create(:conversation, account: account, status: :open) do |conversation|
|
||||
create(:message, account: account, conversation: conversation, message_type: :outgoing)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns metrics grouped by team' do
|
||||
get "/api/v2/accounts/#{account.id}/live_reports/grouped_conversation_metrics",
|
||||
params: { group_by: 'team_id' },
|
||||
headers: admin.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)
|
||||
expect(response_data).to include(
|
||||
{ 'team_id' => nil, 'open' => 2, 'unattended' => 1, 'unassigned' => 1 }
|
||||
)
|
||||
expect(response_data).to include(
|
||||
{ 'team_id' => team.id, 'open' => 3, 'unattended' => 2, 'unassigned' => 3 }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by assignee_id' do
|
||||
let(:assignee1) { create(:user, account: account) }
|
||||
|
||||
before do
|
||||
create(:conversation, assignee_id: agent.id, account: account, status: :open)
|
||||
create(:conversation, account: account, status: :open)
|
||||
create(:conversation, assignee_id: agent.id, account: account, status: :open) do |conversation|
|
||||
create(:message, account: account, conversation: conversation, message_type: :outgoing)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns metrics grouped by assignee' do
|
||||
get "/api/v2/accounts/#{account.id}/live_reports/grouped_conversation_metrics",
|
||||
params: { group_by: 'assignee_id' },
|
||||
headers: admin.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
|
||||
expect(response_data).to include(
|
||||
{ 'assignee_id' => agent.id, 'open' => 2, 'unassigned' => 0, 'unattended' => 1 }
|
||||
)
|
||||
expect(response_data).to include(
|
||||
{ 'assignee_id' => nil, 'open' => 1, 'unassigned' => 1, 'unattended' => 1 }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,481 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Reports 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!(:user) { create(:user, account: account) }
|
||||
let!(:inbox) { create(:inbox, account: account) }
|
||||
let(:inbox_member) { create(:inbox_member, user: user, inbox: inbox) }
|
||||
let(:default_timezone) { 'UTC' }
|
||||
let(:start_of_today) { Time.current.in_time_zone(default_timezone).beginning_of_day.to_i }
|
||||
let(:end_of_today) { Time.current.in_time_zone(default_timezone).end_of_day.to_i }
|
||||
let(:params) { { timezone_offset: Time.zone.utc_offset } }
|
||||
let(:new_account) { create(:account) }
|
||||
|
||||
before do
|
||||
create_list(:conversation, 10, account: account, inbox: inbox,
|
||||
assignee: user, created_at: Time.current.in_time_zone(default_timezone).to_date)
|
||||
end
|
||||
|
||||
describe 'GET /api/v2/accounts/:account_id/reports' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:params) do
|
||||
super().merge(
|
||||
metric: 'conversations_count',
|
||||
type: :account,
|
||||
since: start_of_today.to_s,
|
||||
until: end_of_today.to_s
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns unauthorized for agents' do
|
||||
get "/api/v2/accounts/#{account.id}/reports",
|
||||
params: params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'return timeseries metrics' do
|
||||
get "/api/v2/accounts/#{account.id}/reports",
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
|
||||
current_day_metric = json_response.select { |x| x['timestamp'] == start_of_today }
|
||||
expect(current_day_metric.length).to eq(1)
|
||||
expect(current_day_metric[0]['value']).to eq(10)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v2/accounts/:account_id/reports/conversations' do
|
||||
context 'when it is an authenticated user' do
|
||||
it 'return conversation metrics in account level' do
|
||||
unassigned_conversation = create(:conversation, account: account, inbox: inbox,
|
||||
assignee: nil, created_at: Time.zone.today)
|
||||
unassigned_conversation.save!
|
||||
|
||||
get "/api/v2/accounts/#{account.id}/reports/conversations",
|
||||
params: {
|
||||
type: :account
|
||||
},
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response['open']).to eq(11)
|
||||
expect(json_response['unattended']).to eq(11)
|
||||
expect(json_response['unassigned']).to eq(1)
|
||||
end
|
||||
|
||||
it 'return conversation metrics for user in account level' do
|
||||
create_list(:conversation, 2, account: account, inbox: inbox,
|
||||
assignee: admin, created_at: Time.zone.today)
|
||||
create_list(:conversation, 2, account: new_account, inbox: inbox,
|
||||
assignee: admin, created_at: Time.zone.today)
|
||||
|
||||
get "/api/v2/accounts/#{account.id}/reports/conversations",
|
||||
params: {
|
||||
type: :agent
|
||||
},
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response.blank?).to be false
|
||||
user_metrics = json_response.find { |item| item['name'] == admin[:name] }
|
||||
expect(user_metrics.present?).to be true
|
||||
|
||||
expect(user_metrics['metric']['open']).to eq(2)
|
||||
expect(user_metrics['metric']['unattended']).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an agent1 associated to conversation having first reply from agent2' do
|
||||
let(:listener) { ReportingEventListener.instance }
|
||||
let(:account) { create(:account) }
|
||||
let(:agent2) { create(:user, account: account, role: :agent) }
|
||||
|
||||
it 'returns unattended conversation count zero for agent1' do
|
||||
create(:inbox_member, user: agent, inbox: inbox)
|
||||
create(:inbox_member, user: agent2, inbox: inbox)
|
||||
conversation = create(:conversation, account: account,
|
||||
inbox: inbox, assignee: agent2)
|
||||
|
||||
create(:message, message_type: 'incoming', content: 'Hi',
|
||||
account: account, inbox: inbox,
|
||||
conversation: conversation)
|
||||
first_reply_message = create(:message, message_type: 'outgoing', content: 'Hi',
|
||||
account: account, inbox: inbox, sender: agent2,
|
||||
conversation: conversation)
|
||||
|
||||
event = Events::Base.new('first.reply.created', Time.zone.now, message: first_reply_message)
|
||||
listener.first_reply_created(event)
|
||||
|
||||
conversation.assignee_id = agent.id
|
||||
conversation.save!
|
||||
|
||||
get "/api/v2/accounts/#{account.id}/reports/conversations",
|
||||
params: {
|
||||
type: :agent
|
||||
},
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
json_response = response.parsed_body
|
||||
user_metrics = json_response.find { |item| item['name'] == agent[:name] }
|
||||
expect(user_metrics.present?).to be true
|
||||
|
||||
expect(user_metrics['metric']['open']).to eq(1)
|
||||
expect(user_metrics['metric']['unattended']).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v2/accounts/:account_id/reports/summary' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/summary"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:params) do
|
||||
super().merge(
|
||||
type: :account,
|
||||
since: start_of_today.to_s,
|
||||
until: end_of_today.to_s
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns unauthorized for agents' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/summary",
|
||||
params: params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns summary metrics' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/summary",
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response['conversations_count']).to eq(10)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v2/accounts/:account_id/reports/bot_summary' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/bot_summary"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:params) do
|
||||
super().merge(
|
||||
type: :account,
|
||||
since: start_of_today.to_s,
|
||||
until: end_of_today.to_s
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns unauthorized for agents' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/bot_summary",
|
||||
params: params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns bot summary metrics' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/bot_summary",
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response['bot_resolutions_count']).to eq(0)
|
||||
expect(json_response['bot_handoffs_count']).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v2/accounts/:account_id/reports/agents' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/agents.csv"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:params) do
|
||||
super().merge(
|
||||
since: 30.days.ago.to_i.to_s,
|
||||
until: end_of_today.to_s
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns unauthorized for agents' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/agents.csv",
|
||||
params: params,
|
||||
headers: agent.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns summary' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/agents.csv",
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an agent has access to multiple accounts' do
|
||||
let(:account1) { create(:account) }
|
||||
let(:account2) { create(:account) }
|
||||
|
||||
let(:params) do
|
||||
super().merge(
|
||||
type: :agent,
|
||||
since: 30.days.ago.to_i.to_s,
|
||||
until: end_of_today.to_s
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns agent metrics from the current account' do
|
||||
admin1 = create(:user, account: account1, role: :administrator)
|
||||
inbox1 = create(:inbox, account: account1)
|
||||
inbox2 = create(:inbox, account: account2)
|
||||
|
||||
create(:account_user, user: admin1, account: account2)
|
||||
create(:conversation, account: account1, inbox: inbox1,
|
||||
assignee: admin1, created_at: Time.zone.today - 2.days)
|
||||
create(:conversation, account: account2, inbox: inbox2,
|
||||
assignee: admin1, created_at: Time.zone.today - 2.days)
|
||||
|
||||
get "/api/v2/accounts/#{account1.id}/reports/summary",
|
||||
params: params.merge({ id: admin1.id }),
|
||||
headers: admin1.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['conversations_count']).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v2/accounts/:account_id/reports/inboxes' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/inboxes"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:params) do
|
||||
super().merge(
|
||||
since: 30.days.ago.to_i.to_s,
|
||||
until: end_of_today.to_s
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns unauthorized for inboxes' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/inboxes",
|
||||
params: params,
|
||||
headers: agent.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns summary' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/inboxes",
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v2/accounts/:account_id/reports/labels' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/labels.csv"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:params) do
|
||||
super().merge(
|
||||
since: 30.days.ago.to_i.to_s,
|
||||
until: end_of_today.to_s
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns unauthorized for labels' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/labels.csv",
|
||||
params: params,
|
||||
headers: agent.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns summary' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/labels.csv",
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v2/accounts/:account_id/reports/teams' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/teams.csv"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:params) do
|
||||
super().merge(
|
||||
since: 30.days.ago.to_i.to_s,
|
||||
until: end_of_today.to_s
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns unauthorized for teams' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/teams.csv",
|
||||
params: params,
|
||||
headers: agent.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns summary' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/teams.csv",
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v2/accounts/:account_id/reports/conversation_traffic' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/conversation_traffic.csv"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:params) do
|
||||
super().merge(
|
||||
since: 7.days.ago.to_i.to_s,
|
||||
until: end_of_today.to_s
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/conversation_traffic.csv",
|
||||
params: params,
|
||||
headers: agent.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns values' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/conversation_traffic.csv",
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v2/accounts/:account_id/reports/bot_metrics' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/bot_metrics"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:params) do
|
||||
super().merge(
|
||||
since: 7.days.ago.to_i.to_s,
|
||||
until: end_of_today.to_s
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns unauthorized if the user is an agent' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/bot_metrics",
|
||||
params: params,
|
||||
headers: agent.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns values' do
|
||||
expect(V2::Reports::BotMetricsBuilder).to receive(:new).and_call_original
|
||||
get "/api/v2/accounts/#{account.id}/reports/bot_metrics",
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body.keys).to match_array(%w[conversation_count message_count resolution_rate handoff_rate])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,425 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Api::V2::Accounts::ReportsController, 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) }
|
||||
|
||||
describe 'GET /api/v2/accounts/{account.id}/reports' do
|
||||
context 'when authenticated and authorized' do
|
||||
before do
|
||||
# Create conversations across 24 hours at different times
|
||||
base_time = Time.utc(2024, 1, 14, 23, 0) # Start at 23:00 to span 2 days
|
||||
|
||||
# Create conversations every 4 hours across 24 hours
|
||||
6.times do |i|
|
||||
time = base_time + (i * 4).hours
|
||||
travel_to time do
|
||||
conversation = create(:conversation, account: account, inbox: inbox, assignee: agent)
|
||||
create(:message, account: account, conversation: conversation, message_type: :outgoing)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'timezone_offset affects data grouping and timestamps correctly' do
|
||||
travel_to Time.utc(2024, 1, 15, 12, 0) do
|
||||
Time.use_zone('UTC') do
|
||||
base_time = Time.utc(2024, 1, 14, 23, 0) # Start at 23:00 to span 2 days
|
||||
base_params = {
|
||||
metric: 'conversations_count',
|
||||
type: 'account',
|
||||
since: (base_time - 1.day).to_i.to_s,
|
||||
until: (base_time + 2.days).to_i.to_s,
|
||||
group_by: 'day'
|
||||
}
|
||||
|
||||
responses = [0, -8, 9].map do |offset|
|
||||
get "/api/v2/accounts/#{account.id}/reports",
|
||||
params: base_params.merge(timezone_offset: offset),
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
response.parsed_body
|
||||
end
|
||||
|
||||
data_entries = responses.map { |r| r.select { |e| e['value'] > 0 } }
|
||||
totals = responses.map { |r| r.sum { |e| e['value'] } }
|
||||
timestamps = responses.map { |r| r.map { |e| e['timestamp'] } }
|
||||
|
||||
# Data conservation and redistribution
|
||||
expect(totals.uniq).to eq([6])
|
||||
expect(data_entries[0].map { |e| e['value'] }).to eq([1, 5])
|
||||
expect(data_entries[1].map { |e| e['value'] }).to eq([3, 3])
|
||||
expect(data_entries[2].map { |e| e['value'] }).to eq([4, 2])
|
||||
|
||||
# Timestamp differences
|
||||
expect(timestamps.uniq.size).to eq(3)
|
||||
timestamps[0].zip(timestamps[1]).each { |utc, pst| expect(utc - pst).to eq(-28_800) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'timezone_offset does not affect summary report totals' do
|
||||
let(:base_time) { Time.utc(2024, 1, 15, 12, 0) }
|
||||
let(:summary_params) do
|
||||
{
|
||||
type: 'account',
|
||||
since: (base_time - 1.day).to_i.to_s,
|
||||
until: (base_time + 1.day).to_i.to_s
|
||||
}
|
||||
end
|
||||
|
||||
let(:jst_params) do
|
||||
# For JST: User wants "Jan 15 JST" which translates to:
|
||||
# Jan 14 15:00 UTC to Jan 15 15:00 UTC (event NOT included)
|
||||
{
|
||||
type: 'account',
|
||||
since: (Time.utc(2024, 1, 15, 0, 0) - 9.hours).to_i.to_s, # Jan 14 15:00 UTC
|
||||
until: (Time.utc(2024, 1, 16, 0, 0) - 9.hours).to_i.to_s # Jan 15 15:00 UTC
|
||||
}
|
||||
end
|
||||
let(:utc_params) do
|
||||
# For UTC: Jan 15 00:00 UTC to Jan 16 00:00 UTC (event included)
|
||||
{
|
||||
type: 'account',
|
||||
since: Time.utc(2024, 1, 15, 0, 0).to_i.to_s,
|
||||
until: Time.utc(2024, 1, 16, 0, 0).to_i.to_s
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns identical conversation counts across timezones' do
|
||||
Time.use_zone('UTC') do
|
||||
summaries = [-8, 0, 9].map do |offset|
|
||||
get "/api/v2/accounts/#{account.id}/reports/summary",
|
||||
params: summary_params.merge(timezone_offset: offset),
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
response.parsed_body
|
||||
end
|
||||
|
||||
conversation_counts = summaries.map { |s| s['conversations_count'] }
|
||||
expect(conversation_counts.uniq).to eq([6])
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns identical message counts across timezones' do
|
||||
Time.use_zone('UTC') do
|
||||
get "/api/v2/accounts/#{account.id}/reports/summary",
|
||||
params: summary_params.merge(timezone_offset: 0),
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
utc_summary = response.parsed_body
|
||||
|
||||
get "/api/v2/accounts/#{account.id}/reports/summary",
|
||||
params: summary_params.merge(timezone_offset: -8),
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
pst_summary = response.parsed_body
|
||||
|
||||
expect(utc_summary['incoming_messages_count']).to eq(pst_summary['incoming_messages_count'])
|
||||
expect(utc_summary['outgoing_messages_count']).to eq(pst_summary['outgoing_messages_count'])
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns consistent resolution counts across timezones' do
|
||||
Time.use_zone('UTC') do
|
||||
get "/api/v2/accounts/#{account.id}/reports/summary",
|
||||
params: summary_params.merge(timezone_offset: 0),
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
utc_summary = response.parsed_body
|
||||
|
||||
get "/api/v2/accounts/#{account.id}/reports/summary",
|
||||
params: summary_params.merge(timezone_offset: 9),
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
jst_summary = response.parsed_body
|
||||
|
||||
expect(utc_summary['resolutions_count']).to eq(jst_summary['resolutions_count'])
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns consistent previous period data across timezones' do
|
||||
Time.use_zone('UTC') do
|
||||
get "/api/v2/accounts/#{account.id}/reports/summary",
|
||||
params: summary_params.merge(timezone_offset: 0),
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
utc_summary = response.parsed_body
|
||||
|
||||
get "/api/v2/accounts/#{account.id}/reports/summary",
|
||||
params: summary_params.merge(timezone_offset: -8),
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
pst_summary = response.parsed_body
|
||||
|
||||
expect(utc_summary['previous']['conversations_count']).to eq(pst_summary['previous']['conversations_count']) if utc_summary['previous']
|
||||
end
|
||||
end
|
||||
|
||||
it 'summary reports work when frontend sends correct timezone boundaries' do
|
||||
Time.use_zone('UTC') do
|
||||
# Create a resolution event right at timezone boundary
|
||||
boundary_time = Time.utc(2024, 1, 15, 23, 30) # 11:30 PM UTC on Jan 15
|
||||
gravatar_url = 'https://www.gravatar.com'
|
||||
stub_request(:get, /#{gravatar_url}.*/).to_return(status: 404)
|
||||
|
||||
travel_to boundary_time do
|
||||
perform_enqueued_jobs do
|
||||
conversation = create(:conversation, account: account, inbox: inbox, assignee: agent)
|
||||
conversation.resolved!
|
||||
end
|
||||
end
|
||||
|
||||
get "/api/v2/accounts/#{account.id}/reports/summary",
|
||||
params: jst_params.merge(timezone_offset: 9),
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
jst_summary = response.parsed_body
|
||||
|
||||
get "/api/v2/accounts/#{account.id}/reports/summary",
|
||||
params: utc_params.merge(timezone_offset: 0),
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
utc_summary = response.parsed_body
|
||||
|
||||
expect(jst_summary['resolutions_count']).to eq(0)
|
||||
expect(utc_summary['resolutions_count']).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when unauthenticated' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated but not authorized' do
|
||||
it 'returns forbidden' do
|
||||
get "/api/v2/accounts/#{account.id}/reports",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v2/accounts/{account.id}/reports/inbox_label_matrix' do
|
||||
let!(:inbox_one) { create(:inbox, account: account, name: 'Email Support') }
|
||||
let!(:label_one) { create(:label, account: account, title: 'bug') }
|
||||
|
||||
context 'when unauthenticated' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/inbox_label_matrix"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated as agent' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/inbox_label_matrix",
|
||||
headers: agent.create_new_auth_token, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated as admin' do
|
||||
before do
|
||||
c1 = create(:conversation, account: account, inbox: inbox_one, created_at: 2.days.ago)
|
||||
c1.update(label_list: [label_one.title])
|
||||
end
|
||||
|
||||
it 'returns the inbox label matrix' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/inbox_label_matrix",
|
||||
params: { since: 1.week.ago.to_i.to_s, until: Time.current.to_i.to_s },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
body = response.parsed_body
|
||||
expect(body['inboxes']).to be_an(Array)
|
||||
expect(body['labels']).to be_an(Array)
|
||||
expect(body['matrix']).to be_an(Array)
|
||||
end
|
||||
|
||||
it 'filters by inbox_ids and label_ids' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/inbox_label_matrix",
|
||||
params: { inbox_ids: [inbox_one.id], label_ids: [label_one.id] },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
body = response.parsed_body
|
||||
expect(body['inboxes'].length).to eq(1)
|
||||
expect(body['labels'].length).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v2/accounts/{account.id}/reports/first_response_time_distribution' do
|
||||
let!(:web_widget_inbox) { create(:inbox, account: account, channel: create(:channel_widget, account: account)) }
|
||||
|
||||
context 'when unauthenticated' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/first_response_time_distribution"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated as agent' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/first_response_time_distribution",
|
||||
headers: agent.create_new_auth_token, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated as admin' do
|
||||
before do
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 1_800, created_at: 2.days.ago)
|
||||
end
|
||||
|
||||
it 'returns the first response time distribution' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/first_response_time_distribution",
|
||||
params: { since: 1.week.ago.to_i.to_s, until: Time.current.to_i.to_s },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
body = response.parsed_body
|
||||
expect(body).to be_a(Hash)
|
||||
expect(body['Channel::WebWidget']).to include('0-1h', '1-4h', '4-8h', '8-24h', '24h+')
|
||||
end
|
||||
|
||||
it 'returns correct counts in buckets' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/first_response_time_distribution",
|
||||
params: { since: 1.week.ago.to_i.to_s, until: Time.current.to_i.to_s },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
body = response.parsed_body
|
||||
expect(body['Channel::WebWidget']['0-1h']).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v2/accounts/{account.id}/reports/outgoing_messages_count' do
|
||||
let(:since_epoch) { 1.week.ago.to_i.to_s }
|
||||
let(:until_epoch) { 1.day.from_now.to_i.to_s }
|
||||
|
||||
context 'when unauthenticated' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'agent', since: since_epoch, until: until_epoch }
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated as agent' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'agent', since: since_epoch, until: until_epoch },
|
||||
headers: agent.create_new_auth_token, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated as admin' do
|
||||
let(:agent2) { create(:user, account: account, role: :agent) }
|
||||
let(:team) { create(:team, account: account) }
|
||||
let(:inbox2) { create(:inbox, account: account) }
|
||||
|
||||
# Separate conversations for agent and team grouping because
|
||||
# model callbacks clear assignee_id when team is set.
|
||||
before do
|
||||
conv_agent = create(:conversation, account: account, inbox: inbox, assignee: agent)
|
||||
conv_agent2 = create(:conversation, account: account, inbox: inbox2, assignee: agent2)
|
||||
conv_team = create(:conversation, account: account, inbox: inbox, team: team)
|
||||
|
||||
create_list(:message, 3, account: account, conversation: conv_agent, inbox: inbox, message_type: :outgoing, sender: agent)
|
||||
create_list(:message, 2, account: account, conversation: conv_agent2, inbox: inbox2, message_type: :outgoing, sender: agent2)
|
||||
create_list(:message, 4, account: account, conversation: conv_team, inbox: inbox, message_type: :outgoing)
|
||||
# incoming message should not be counted
|
||||
create(:message, account: account, conversation: conv_agent, inbox: inbox, message_type: :incoming)
|
||||
end
|
||||
|
||||
it 'returns unprocessable_entity for invalid group_by' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'invalid', since: since_epoch, until: until_epoch },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'returns outgoing message counts grouped by agent' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'agent', since: since_epoch, until: until_epoch },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data).to be_an(Array)
|
||||
|
||||
agent_entry = data.find { |e| e['id'] == agent.id }
|
||||
agent2_entry = data.find { |e| e['id'] == agent2.id }
|
||||
expect(agent_entry['outgoing_messages_count']).to eq(3)
|
||||
expect(agent2_entry['outgoing_messages_count']).to eq(2)
|
||||
end
|
||||
|
||||
it 'returns outgoing message counts grouped by team' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'team', since: since_epoch, until: until_epoch },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data).to be_an(Array)
|
||||
expect(data.length).to eq(1)
|
||||
expect(data.first['id']).to eq(team.id)
|
||||
expect(data.first['outgoing_messages_count']).to eq(4)
|
||||
end
|
||||
|
||||
it 'returns outgoing message counts grouped by inbox' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'inbox', since: since_epoch, until: until_epoch },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data).to be_an(Array)
|
||||
|
||||
inbox_entry = data.find { |e| e['id'] == inbox.id }
|
||||
inbox2_entry = data.find { |e| e['id'] == inbox2.id }
|
||||
expect(inbox_entry['outgoing_messages_count']).to eq(7)
|
||||
expect(inbox2_entry['outgoing_messages_count']).to eq(2)
|
||||
end
|
||||
|
||||
it 'returns outgoing message counts grouped by label' do
|
||||
label = create(:label, account: account, title: 'support')
|
||||
conversation = account.conversations.first
|
||||
conversation.label_list.add('support')
|
||||
conversation.save!
|
||||
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'label', since: since_epoch, until: until_epoch },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data).to be_an(Array)
|
||||
expect(data.length).to eq(1)
|
||||
expect(data.first['id']).to eq(label.id)
|
||||
expect(data.first['name']).to eq('support')
|
||||
end
|
||||
|
||||
it 'excludes bot messages when grouped by agent' do
|
||||
bot = create(:agent_bot)
|
||||
bot_conversation = create(:conversation, account: account, inbox: inbox)
|
||||
create(:message, account: account, conversation: bot_conversation, inbox: inbox,
|
||||
message_type: :outgoing, sender: bot)
|
||||
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'agent', since: since_epoch, until: until_epoch },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
data = response.parsed_body
|
||||
agent_entry = data.find { |e| e['id'] == agent.id }
|
||||
# 3 from before block; bot message excluded (sender_type != 'User')
|
||||
expect(agent_entry['outgoing_messages_count']).to eq(3)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,227 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Summary Reports 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(:default_timezone) { ActiveSupport::TimeZone[0]&.name }
|
||||
let(:start_of_today) { Time.current.in_time_zone(default_timezone).beginning_of_day.to_i }
|
||||
let(:end_of_today) { Time.current.in_time_zone(default_timezone).end_of_day.to_i }
|
||||
|
||||
describe 'GET /api/v2/accounts/:account_id/summary_reports/agent' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/summary_reports/agent"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:params) do
|
||||
{
|
||||
since: start_of_today.to_s,
|
||||
until: end_of_today.to_s,
|
||||
business_hours: true
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns unauthorized for agents' do
|
||||
get "/api/v2/accounts/#{account.id}/summary_reports/agent",
|
||||
params: params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'calls V2::Reports::AgentSummaryBuilder with the right params if the user is an admin' do
|
||||
agent_summary_builder = double
|
||||
allow(V2::Reports::AgentSummaryBuilder).to receive(:new).and_return(agent_summary_builder)
|
||||
allow(agent_summary_builder).to receive(:build).and_return([{ id: 1, conversations_count: 110 }])
|
||||
|
||||
get "/api/v2/accounts/#{account.id}/summary_reports/agent",
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(V2::Reports::AgentSummaryBuilder).to have_received(:new).with(account: account, params: params)
|
||||
expect(agent_summary_builder).to have_received(:build)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response.length).to eq(1)
|
||||
expect(json_response.first['id']).to eq(1)
|
||||
expect(json_response.first['conversations_count']).to eq(110)
|
||||
expect(json_response.first['avg_reply_time']).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v2/accounts/:account_id/summary_reports/inbox' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/summary_reports/inbox"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:params) do
|
||||
{
|
||||
since: start_of_today.to_s,
|
||||
until: end_of_today.to_s,
|
||||
business_hours: true
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns unauthorized for inbox' do
|
||||
get "/api/v2/accounts/#{account.id}/summary_reports/inbox",
|
||||
params: params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'calls V2::Reports::InboxSummaryBuilder with the right params if the user is an admin' do
|
||||
inbox_summary_builder = double
|
||||
allow(V2::Reports::InboxSummaryBuilder).to receive(:new).and_return(inbox_summary_builder)
|
||||
allow(inbox_summary_builder).to receive(:build).and_return([{ id: 1, conversations_count: 110 }])
|
||||
|
||||
get "/api/v2/accounts/#{account.id}/summary_reports/inbox",
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(V2::Reports::InboxSummaryBuilder).to have_received(:new).with(account: account, params: params)
|
||||
expect(inbox_summary_builder).to have_received(:build)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response.length).to eq(1)
|
||||
expect(json_response.first['id']).to eq(1)
|
||||
expect(json_response.first['conversations_count']).to eq(110)
|
||||
expect(json_response.first['avg_reply_time']).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v2/accounts/:account_id/summary_reports/team' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/summary_reports/team"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:params) do
|
||||
{
|
||||
since: start_of_today.to_s,
|
||||
until: end_of_today.to_s,
|
||||
business_hours: true
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns unauthorized for agents' do
|
||||
get "/api/v2/accounts/#{account.id}/summary_reports/team",
|
||||
params: params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'calls V2::Reports::TeamSummaryBuilder with the right params if the user is an admin' do
|
||||
team_summary_builder = double
|
||||
allow(V2::Reports::TeamSummaryBuilder).to receive(:new).and_return(team_summary_builder)
|
||||
allow(team_summary_builder).to receive(:build).and_return([{ id: 1, conversations_count: 110 }])
|
||||
|
||||
get "/api/v2/accounts/#{account.id}/summary_reports/team",
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(V2::Reports::TeamSummaryBuilder).to have_received(:new).with(account: account, params: params)
|
||||
expect(team_summary_builder).to have_received(:build)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response.length).to eq(1)
|
||||
expect(json_response.first['id']).to eq(1)
|
||||
expect(json_response.first['conversations_count']).to eq(110)
|
||||
expect(json_response.first['avg_reply_time']).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v2/accounts/:account_id/summary_reports/channel' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/summary_reports/channel"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:params) do
|
||||
{
|
||||
since: start_of_today.to_s,
|
||||
until: end_of_today.to_s
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns unauthorized for agents' do
|
||||
get "/api/v2/accounts/#{account.id}/summary_reports/channel",
|
||||
params: params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'calls V2::Reports::ChannelSummaryBuilder with the right params if the user is an admin' do
|
||||
channel_summary_builder = double
|
||||
allow(V2::Reports::ChannelSummaryBuilder).to receive(:new).and_return(channel_summary_builder)
|
||||
allow(channel_summary_builder).to receive(:build)
|
||||
.and_return({
|
||||
'Channel::WebWidget' => { open: 5, resolved: 10, pending: 2, snoozed: 1, total: 18 }
|
||||
})
|
||||
|
||||
get "/api/v2/accounts/#{account.id}/summary_reports/channel",
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(V2::Reports::ChannelSummaryBuilder).to have_received(:new).with(
|
||||
account: account,
|
||||
params: hash_including(since: start_of_today.to_s, until: end_of_today.to_s)
|
||||
)
|
||||
expect(channel_summary_builder).to have_received(:build)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response['Channel::WebWidget']['open']).to eq(5)
|
||||
expect(json_response['Channel::WebWidget']['total']).to eq(18)
|
||||
end
|
||||
|
||||
it 'returns unprocessable_entity when date range exceeds 6 months' do
|
||||
get "/api/v2/accounts/#{account.id}/summary_reports/channel",
|
||||
params: { since: 1.year.ago.to_i.to_s, until: Time.current.to_i.to_s },
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq(I18n.t('errors.reports.date_range_too_long'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,118 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Accounts API', type: :request do
|
||||
describe 'POST /api/v2/accounts' do
|
||||
let(:email) { Faker::Internet.email }
|
||||
|
||||
context 'when posting to accounts with correct parameters' do
|
||||
let(:account_builder) { double }
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, email: email, account: account) }
|
||||
|
||||
before do
|
||||
allow(AccountBuilder).to receive(:new).and_return(account_builder)
|
||||
end
|
||||
|
||||
it 'calls account builder' do
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
|
||||
allow(account_builder).to receive(:perform).and_return([user, account])
|
||||
|
||||
params = { email: email, user: nil, locale: nil, password: 'Password1!' }
|
||||
|
||||
post api_v2_accounts_url,
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(AccountBuilder).to have_received(:new).with(params.except(:password).merge(user_password: params[:password]))
|
||||
expect(account_builder).to have_received(:perform)
|
||||
expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid')
|
||||
expect(response.body).to include('en')
|
||||
end
|
||||
end
|
||||
|
||||
it 'updates the onboarding step in custom attributes' do
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
|
||||
allow(account_builder).to receive(:perform).and_return([user, account])
|
||||
|
||||
params = { email: email, user: nil, locale: nil, password: 'Password1!' }
|
||||
|
||||
post api_v2_accounts_url,
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(account.reload.custom_attributes['onboarding_step']).to eq('profile_update')
|
||||
end
|
||||
end
|
||||
|
||||
it 'calls ChatwootCaptcha' do
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
|
||||
captcha = double
|
||||
allow(account_builder).to receive(:perform).and_return([user, account])
|
||||
allow(ChatwootCaptcha).to receive(:new).and_return(captcha)
|
||||
allow(captcha).to receive(:valid?).and_return(true)
|
||||
|
||||
params = { email: email, user: nil, password: 'Password1!', locale: nil, h_captcha_client_response: '123' }
|
||||
|
||||
post api_v2_accounts_url,
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(ChatwootCaptcha).to have_received(:new).with('123')
|
||||
expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid')
|
||||
expect(response.body).to include('en')
|
||||
end
|
||||
end
|
||||
|
||||
it 'renders error response on invalid params' do
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
|
||||
allow(account_builder).to receive(:perform).and_return(nil)
|
||||
|
||||
params = { email: nil, user: nil, locale: nil }
|
||||
|
||||
post api_v2_accounts_url,
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(AccountBuilder).to have_received(:new).with(params.merge(user_password: params[:password]))
|
||||
expect(account_builder).to have_received(:perform)
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(response.body).to eq({ message: I18n.t('errors.signup.failed') }.to_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ENABLE_ACCOUNT_SIGNUP env variable is set to false' do
|
||||
it 'responds 404 on requests' do
|
||||
params = { email: email }
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'false' do
|
||||
post api_v2_accounts_url,
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ENABLE_ACCOUNT_SIGNUP env variable is set to api_only' do
|
||||
let(:account_builder) { double }
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, email: email, account: account) }
|
||||
|
||||
it 'does not respond 404 on requests' do
|
||||
allow(AccountBuilder).to receive(:new).and_return(account_builder)
|
||||
allow(account_builder).to receive(:perform).and_return([user, account])
|
||||
|
||||
params = { email: email, user: nil, password: 'Password1!', locale: nil }
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'api_only' do
|
||||
post api_v2_accounts_url,
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(AccountBuilder).to have_received(:new).with(params.except(:password).merge(user_password: params[:password]))
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
13
research/chatwoot/spec/controllers/api_controller_spec.rb
Normal file
13
research/chatwoot/spec/controllers/api_controller_spec.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'API Base', type: :request do
|
||||
describe 'request to api base url' do
|
||||
it 'returns api version' do
|
||||
get '/api/'
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include(Chatwoot.config[:version])
|
||||
expect(response.body).to include('queue_services')
|
||||
expect(response.body).to include('data_services')
|
||||
end
|
||||
end
|
||||
end
|
||||
10
research/chatwoot/spec/controllers/apple_app_spec.rb
Normal file
10
research/chatwoot/spec/controllers/apple_app_spec.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe '.well-known/apple-app-site-association', type: :request do
|
||||
describe 'GET /.well-known/apple-app-site-association' do
|
||||
it 'renders the apple-app-site-association file' do
|
||||
get '/.well-known/apple-app-site-association'
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,138 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe InstagramConcern do
|
||||
let(:dummy_class) { Class.new { include InstagramConcern } }
|
||||
let(:dummy_instance) { dummy_class.new }
|
||||
let(:client_id) { 'test_client_id' }
|
||||
let(:client_secret) { 'test_client_secret' }
|
||||
let(:short_lived_token) { 'short_lived_token' }
|
||||
let(:long_lived_token) { 'long_lived_token' }
|
||||
let(:access_token) { 'access_token' }
|
||||
|
||||
before do
|
||||
allow(GlobalConfigService).to receive(:load).with('INSTAGRAM_APP_ID', nil).and_return(client_id)
|
||||
allow(GlobalConfigService).to receive(:load).with('INSTAGRAM_APP_SECRET', nil).and_return(client_secret)
|
||||
allow(Rails.logger).to receive(:error)
|
||||
end
|
||||
|
||||
describe '#instagram_client' do
|
||||
it 'creates an OAuth2 client with correct configuration', :aggregate_failures do
|
||||
client = dummy_instance.instagram_client
|
||||
|
||||
expect(client).to be_a(OAuth2::Client)
|
||||
expect(client.id).to eq(client_id)
|
||||
expect(client.secret).to eq(client_secret)
|
||||
expect(client.site).to eq('https://api.instagram.com')
|
||||
expect(client.options[:authorize_url]).to eq('https://api.instagram.com/oauth/authorize')
|
||||
expect(client.options[:token_url]).to eq('https://api.instagram.com/oauth/access_token')
|
||||
expect(client.options[:auth_scheme]).to eq(:request_body)
|
||||
expect(client.options[:token_method]).to eq(:post)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#exchange_for_long_lived_token' do
|
||||
let(:response_body) { { 'access_token' => long_lived_token, 'expires_in' => 5_184_000 }.to_json }
|
||||
let(:mock_response) { instance_double(HTTParty::Response, body: response_body, success?: true) }
|
||||
|
||||
before do
|
||||
allow(HTTParty).to receive(:get).and_return(mock_response)
|
||||
allow(mock_response).to receive(:inspect).and_return(response_body)
|
||||
end
|
||||
|
||||
it 'exchanges short lived token for long lived token' do
|
||||
result = dummy_instance.send(:exchange_for_long_lived_token, short_lived_token)
|
||||
|
||||
expect(HTTParty).to have_received(:get).with(
|
||||
'https://graph.instagram.com/access_token',
|
||||
{
|
||||
query: {
|
||||
grant_type: 'ig_exchange_token',
|
||||
client_secret: client_secret,
|
||||
access_token: short_lived_token,
|
||||
client_id: client_id
|
||||
},
|
||||
headers: { 'Accept' => 'application/json' }
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).to eq({ 'access_token' => long_lived_token, 'expires_in' => 5_184_000 })
|
||||
end
|
||||
|
||||
context 'when the request fails' do
|
||||
let(:mock_response) { instance_double(HTTParty::Response, body: 'Error', success?: false, code: 400) }
|
||||
|
||||
it 'raises an error' do
|
||||
expect do
|
||||
dummy_instance.send(:exchange_for_long_lived_token, short_lived_token)
|
||||
end.to raise_error(RuntimeError, 'Failed to exchange token: Error')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the response is not valid JSON' do
|
||||
let(:mock_response) { instance_double(HTTParty::Response, body: 'Not JSON', success?: true) }
|
||||
|
||||
it 'raises a JSON parse error' do
|
||||
allow(JSON).to receive(:parse).and_raise(JSON::ParserError.new('Invalid JSON'))
|
||||
|
||||
expect { dummy_instance.send(:exchange_for_long_lived_token, short_lived_token) }.to raise_error(JSON::ParserError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#fetch_instagram_user_details' do
|
||||
let(:user_details) do
|
||||
{
|
||||
'id' => '12345',
|
||||
'username' => 'test_user',
|
||||
'user_id' => '12345',
|
||||
'name' => 'Test User',
|
||||
'profile_picture_url' => 'https://example.com/profile.jpg',
|
||||
'account_type' => 'BUSINESS'
|
||||
}
|
||||
end
|
||||
let(:response_body) { user_details.to_json }
|
||||
let(:mock_response) { instance_double(HTTParty::Response, body: response_body, success?: true) }
|
||||
|
||||
before do
|
||||
allow(HTTParty).to receive(:get).and_return(mock_response)
|
||||
allow(mock_response).to receive(:inspect).and_return(response_body)
|
||||
end
|
||||
|
||||
it 'fetches Instagram user details' do
|
||||
result = dummy_instance.send(:fetch_instagram_user_details, access_token)
|
||||
|
||||
expect(HTTParty).to have_received(:get).with(
|
||||
'https://graph.instagram.com/v22.0/me',
|
||||
{
|
||||
query: {
|
||||
fields: 'id,username,user_id,name,profile_picture_url,account_type',
|
||||
access_token: access_token
|
||||
},
|
||||
headers: { 'Accept' => 'application/json' }
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).to eq(user_details)
|
||||
end
|
||||
|
||||
context 'when the request fails' do
|
||||
let(:mock_response) { instance_double(HTTParty::Response, body: 'Error', success?: false, code: 400) }
|
||||
|
||||
it 'raises an error' do
|
||||
expect do
|
||||
dummy_instance.send(:fetch_instagram_user_details, access_token)
|
||||
end.to raise_error(RuntimeError, 'Failed to fetch Instagram user details: Error')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the response is not valid JSON' do
|
||||
let(:mock_response) { instance_double(HTTParty::Response, body: 'Not JSON', success?: true) }
|
||||
|
||||
it 'raises a JSON parse error' do
|
||||
allow(JSON).to receive(:parse).and_raise(JSON::ParserError.new('Invalid JSON'))
|
||||
|
||||
expect { dummy_instance.send(:fetch_instagram_user_details, access_token) }.to raise_error(JSON::ParserError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,56 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe NotionConcern, type: :concern do
|
||||
let(:controller_class) do
|
||||
Class.new do
|
||||
include NotionConcern
|
||||
end
|
||||
end
|
||||
|
||||
let(:controller) { controller_class.new }
|
||||
|
||||
describe '#notion_client' do
|
||||
let(:client_id) { 'test_notion_client_id' }
|
||||
let(:client_secret) { 'test_notion_client_secret' }
|
||||
|
||||
before do
|
||||
allow(GlobalConfigService).to receive(:load).with('NOTION_CLIENT_ID', nil).and_return(client_id)
|
||||
allow(GlobalConfigService).to receive(:load).with('NOTION_CLIENT_SECRET', nil).and_return(client_secret)
|
||||
end
|
||||
|
||||
it 'creates OAuth2 client with correct configuration' do
|
||||
expect(OAuth2::Client).to receive(:new).with(
|
||||
client_id,
|
||||
client_secret,
|
||||
{
|
||||
site: 'https://api.notion.com',
|
||||
authorize_url: 'https://api.notion.com/v1/oauth/authorize',
|
||||
token_url: 'https://api.notion.com/v1/oauth/token',
|
||||
auth_scheme: :basic_auth
|
||||
}
|
||||
)
|
||||
|
||||
controller.notion_client
|
||||
end
|
||||
|
||||
it 'loads client credentials from GlobalConfigService' do
|
||||
expect(GlobalConfigService).to receive(:load).with('NOTION_CLIENT_ID', nil)
|
||||
expect(GlobalConfigService).to receive(:load).with('NOTION_CLIENT_SECRET', nil)
|
||||
|
||||
controller.notion_client
|
||||
end
|
||||
|
||||
it 'returns OAuth2::Client instance' do
|
||||
client = controller.notion_client
|
||||
expect(client).to be_an_instance_of(OAuth2::Client)
|
||||
end
|
||||
|
||||
it 'configures client with Notion-specific endpoints' do
|
||||
client = controller.notion_client
|
||||
expect(client.site).to eq('https://api.notion.com')
|
||||
expect(client.options[:authorize_url]).to eq('https://api.notion.com/v1/oauth/authorize')
|
||||
expect(client.options[:token_url]).to eq('https://api.notion.com/v1/oauth/token')
|
||||
expect(client.options[:auth_scheme]).to eq(:basic_auth)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,42 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe '/app/login', type: :request do
|
||||
context 'without DEFAULT_LOCALE' do
|
||||
it 'renders the dashboard' do
|
||||
get '/app/login'
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with DEFAULT_LOCALE' do
|
||||
it 'renders the dashboard' do
|
||||
with_modified_env DEFAULT_LOCALE: 'pt_BR' do
|
||||
get '/app/login'
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include "selectedLocale: 'pt_BR'"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with non-HTML format' do
|
||||
it 'returns not acceptable for JSON with error message' do
|
||||
get '/app/login', headers: { 'Accept' => 'application/json' }
|
||||
expect(response).to have_http_status(:not_acceptable)
|
||||
expect(response.parsed_body).to eq({ 'error' => 'Please use API routes instead of dashboard routes for JSON requests' })
|
||||
end
|
||||
end
|
||||
|
||||
# Routes are loaded once on app start
|
||||
# hence Rails.application.reload_routes! is used in this spec
|
||||
# ref : https://stackoverflow.com/a/63584877/939299
|
||||
context 'with CW_API_ONLY_SERVER true' do
|
||||
it 'returns 404' do
|
||||
with_modified_env CW_API_ONLY_SERVER: 'true' do
|
||||
Rails.application.reload_routes!
|
||||
get '/app/login'
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
Rails.application.reload_routes!
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,52 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Token Confirmation', type: :request do
|
||||
describe 'POST /auth/confirmation' do
|
||||
let(:response_json) { JSON.parse(response.body, symbolize_names: true) }
|
||||
|
||||
before do
|
||||
create(:user, email: 'john.doe@gmail.com', **user_attributes)
|
||||
|
||||
post user_confirmation_url, params: { confirmation_token: confirmation_token }
|
||||
end
|
||||
|
||||
context 'when token is valid' do
|
||||
let(:user_attributes) { { confirmation_token: '12345', skip_confirmation: false } }
|
||||
let(:confirmation_token) { '12345' }
|
||||
|
||||
it 'has status 200' do
|
||||
expect(response).to have_http_status :ok
|
||||
end
|
||||
|
||||
it 'returns "auth data"' do
|
||||
expect(response.body).to include('john.doe@gmail.com')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when token is invalid' do
|
||||
let(:user_attributes) { { confirmation_token: '12345' } }
|
||||
let(:confirmation_token) { '' }
|
||||
|
||||
it 'has status 422' do
|
||||
expect(response).to have_http_status :unprocessable_entity
|
||||
end
|
||||
|
||||
it 'returns message "Invalid token"' do
|
||||
expect(response_json).to eq({ message: 'Invalid token', redirect_url: '/' })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user had already been confirmed' do
|
||||
let(:user_attributes) { { confirmation_token: '12345' } }
|
||||
let(:confirmation_token) { '12345' }
|
||||
|
||||
it 'has status 422' do
|
||||
expect(response).to have_http_status :unprocessable_entity
|
||||
end
|
||||
|
||||
it 'returns message "Already confirmed"' do
|
||||
expect(response_json).to eq({ message: 'Already confirmed', redirect_url: '/' })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,148 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'DeviseOverrides::OmniauthCallbacksController', type: :request do
|
||||
let(:account_builder) { double }
|
||||
let(:user_double) { object_double(:user) }
|
||||
let(:email_validation_service) { instance_double(Account::SignUpEmailValidationService) }
|
||||
|
||||
def set_omniauth_config(for_email = 'test@example.com')
|
||||
OmniAuth.config.test_mode = true
|
||||
OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new(
|
||||
provider: 'google',
|
||||
uid: '123545',
|
||||
info: {
|
||||
name: 'test',
|
||||
email: for_email,
|
||||
image: 'https://example.com/image.jpg'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(Account::SignUpEmailValidationService).to receive(:new).and_return(email_validation_service)
|
||||
end
|
||||
|
||||
describe '#omniauth_sucess' do
|
||||
before do
|
||||
GlobalConfig.clear_cache
|
||||
end
|
||||
|
||||
it 'allows signup' do
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true', FRONTEND_URL: 'http://www.example.com' do
|
||||
set_omniauth_config('test_not_preset@example.com')
|
||||
allow(AccountBuilder).to receive(:new).and_return(account_builder)
|
||||
allow(account_builder).to receive(:perform).and_return(user_double)
|
||||
allow(Avatar::AvatarFromUrlJob).to receive(:perform_later).and_return(true)
|
||||
allow(email_validation_service).to receive(:perform).and_return(true)
|
||||
|
||||
get '/omniauth/google_oauth2/callback'
|
||||
|
||||
# expect a 302 redirect to auth/google_oauth2/callback
|
||||
expect(response).to redirect_to('http://www.example.com/auth/google_oauth2/callback')
|
||||
follow_redirect!
|
||||
|
||||
expect(AccountBuilder).to have_received(:new).with({
|
||||
account_name: 'example',
|
||||
user_full_name: 'test',
|
||||
email: 'test_not_preset@example.com',
|
||||
locale: I18n.locale,
|
||||
confirmed: nil
|
||||
})
|
||||
expect(account_builder).to have_received(:perform)
|
||||
end
|
||||
end
|
||||
|
||||
it 'blocks personal accounts signup' do
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true', FRONTEND_URL: 'http://www.example.com' do
|
||||
set_omniauth_config('personal@gmail.com')
|
||||
allow(email_validation_service).to receive(:perform).and_raise(CustomExceptions::Account::InvalidEmail.new({ valid: false, disposable: nil }))
|
||||
|
||||
get '/omniauth/google_oauth2/callback'
|
||||
|
||||
# expect a 302 redirect to auth/google_oauth2/callback
|
||||
expect(response).to redirect_to('http://www.example.com/auth/google_oauth2/callback')
|
||||
follow_redirect!
|
||||
|
||||
# expect a 302 redirect to app/login with error disallowing personal accounts
|
||||
expect(response).to redirect_to(%r{/app/login\?error=business-account-only$})
|
||||
end
|
||||
end
|
||||
|
||||
it 'blocks personal accounts signup with different Gmail case variations' do
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true', FRONTEND_URL: 'http://www.example.com' do
|
||||
# Test different case variations of Gmail
|
||||
['personal@Gmail.com', 'personal@GMAIL.com', 'personal@Gmail.COM'].each do |email|
|
||||
set_omniauth_config(email)
|
||||
allow(email_validation_service).to receive(:perform).and_raise(CustomExceptions::Account::InvalidEmail.new({ valid: false,
|
||||
disposable: nil }))
|
||||
|
||||
get '/omniauth/google_oauth2/callback'
|
||||
|
||||
# expect a 302 redirect to auth/google_oauth2/callback
|
||||
expect(response).to redirect_to('http://www.example.com/auth/google_oauth2/callback')
|
||||
follow_redirect!
|
||||
|
||||
# expect a 302 redirect to app/login with error disallowing personal accounts
|
||||
expect(response).to redirect_to(%r{/app/login\?error=business-account-only$})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This test does not affect line coverage, but it is important to ensure that the logic
|
||||
# does not allow any signup if the ENV explicitly disables it
|
||||
it 'blocks signup if ENV disabled' do
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'false', FRONTEND_URL: 'http://www.example.com' do
|
||||
set_omniauth_config('does-not-exist-for-sure@example.com')
|
||||
allow(email_validation_service).to receive(:perform).and_return(true)
|
||||
|
||||
get '/omniauth/google_oauth2/callback'
|
||||
|
||||
# expect a 302 redirect to auth/google_oauth2/callback
|
||||
expect(response).to redirect_to('http://www.example.com/auth/google_oauth2/callback')
|
||||
follow_redirect!
|
||||
|
||||
# expect a 302 redirect to app/login with error disallowing signup
|
||||
expect(response).to redirect_to(%r{/app/login\?error=no-account-found$})
|
||||
end
|
||||
end
|
||||
|
||||
it 'allows login' do
|
||||
with_modified_env FRONTEND_URL: 'http://www.example.com' do
|
||||
create(:user, email: 'test@example.com')
|
||||
set_omniauth_config('test@example.com')
|
||||
|
||||
get '/omniauth/google_oauth2/callback'
|
||||
# expect a 302 redirect to auth/google_oauth2/callback
|
||||
expect(response).to redirect_to('http://www.example.com/auth/google_oauth2/callback')
|
||||
|
||||
follow_redirect!
|
||||
expect(response).to redirect_to(%r{/app/login\?email=.+&sso_auth_token=.+$})
|
||||
|
||||
# expect app/login page to respond with 200 and render
|
||||
follow_redirect!
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
# from a line coverage point of view this may seem redundant
|
||||
# but to ensure that the logic allows for existing users even if they have a gmail account
|
||||
# we need to test this explicitly
|
||||
it 'allows personal account login' do
|
||||
with_modified_env FRONTEND_URL: 'http://www.example.com' do
|
||||
create(:user, email: 'personal-existing@gmail.com')
|
||||
set_omniauth_config('personal-existing@gmail.com')
|
||||
|
||||
get '/omniauth/google_oauth2/callback'
|
||||
# expect a 302 redirect to auth/google_oauth2/callback
|
||||
expect(response).to redirect_to('http://www.example.com/auth/google_oauth2/callback')
|
||||
|
||||
follow_redirect!
|
||||
expect(response).to redirect_to(%r{/app/login\?email=.+&sso_auth_token=.+$})
|
||||
|
||||
# expect app/login page to respond with 200 and render
|
||||
follow_redirect!
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,96 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Session', type: :request do
|
||||
describe 'GET /sign_in' do
|
||||
let!(:account) { create(:account) }
|
||||
|
||||
context 'when it is invalid credentials' do
|
||||
it 'returns unauthorized' do
|
||||
params = { email: 'invalid@invalid.com', password: 'invalid' }
|
||||
|
||||
post new_user_session_url,
|
||||
params: params,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
expect(response.body).to include('Invalid login credentials')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is valid credentials' do
|
||||
let!(:user) { create(:user, password: 'Password1!', account: account) }
|
||||
let!(:user_with_new_pwd) { create(:user, password: 'Password1!.><?', account: account) }
|
||||
|
||||
it 'returns successful auth response' do
|
||||
params = { email: user.email, password: 'Password1!' }
|
||||
|
||||
post new_user_session_url,
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include(user.email)
|
||||
end
|
||||
|
||||
it 'returns successful auth response with new password special characters' do
|
||||
params = { email: user_with_new_pwd.email, password: 'Password1!.><?' }
|
||||
|
||||
post new_user_session_url,
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include(user_with_new_pwd.email)
|
||||
end
|
||||
|
||||
it 'returns the permission of the user' do
|
||||
params = { email: user.email, password: 'Password1!' }
|
||||
|
||||
post new_user_session_url,
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['data']['accounts'].first['permissions']).to eq(['agent'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is invalid sso auth token' do
|
||||
let!(:user) { create(:user, password: 'Password1!', account: account) }
|
||||
|
||||
it 'returns unauthorized' do
|
||||
params = { email: user.email, sso_auth_token: SecureRandom.hex(32) }
|
||||
|
||||
post new_user_session_url,
|
||||
params: params,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
expect(response.body).to include('Invalid login credentials')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when with valid sso auth token' do
|
||||
let!(:user) { create(:user, password: 'Password1!', account: account) }
|
||||
|
||||
it 'returns successful auth response' do
|
||||
params = { email: user.email, sso_auth_token: user.generate_sso_auth_token }
|
||||
|
||||
post new_user_session_url, params: params, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include(user.email)
|
||||
|
||||
# token won't work on a subsequent request
|
||||
post new_user_session_url, params: params, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /auth/sign_in' do
|
||||
it 'redirects to the frontend login page with error' do
|
||||
get new_user_session_url
|
||||
|
||||
expect(response).to redirect_to(%r{/app/login\?error=access-denied$})
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Token Validation API', type: :request do
|
||||
describe 'GET /validate_token' do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
context 'when it is an invalid token' do
|
||||
it 'returns unauthorized' do
|
||||
get '/auth/validate_token'
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is a valid token' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
|
||||
it 'returns all the labels for the conversation' do
|
||||
get '/auth/validate_token',
|
||||
headers: agent.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include('payload')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,166 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe DeviseOverrides::SessionsController, type: :controller do
|
||||
include Devise::Test::ControllerHelpers
|
||||
|
||||
before do
|
||||
request.env['devise.mapping'] = Devise.mappings[:user]
|
||||
end
|
||||
|
||||
describe 'POST #create' do
|
||||
let(:user) { create(:user, password: 'Test@123456') }
|
||||
|
||||
context 'with standard authentication' do
|
||||
it 'authenticates with valid credentials' do
|
||||
post :create, params: { email: user.email, password: 'Test@123456' }
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
it 'rejects invalid credentials' do
|
||||
post :create, params: { email: user.email, password: 'wrong' }
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with MFA authentication' do
|
||||
before do
|
||||
skip('Skipping since MFA is not configured in this environment') unless Chatwoot.encryption_configured?
|
||||
user.enable_two_factor!
|
||||
user.update!(otp_required_for_login: true)
|
||||
end
|
||||
|
||||
it 'requires MFA verification after successful password authentication' do
|
||||
post :create, params: { email: user.email, password: 'Test@123456' }
|
||||
|
||||
expect(response).to have_http_status(:partial_content)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['mfa_required']).to be(true)
|
||||
expect(json_response['mfa_token']).to be_present
|
||||
end
|
||||
|
||||
it 'does not return authentication tokens before MFA verification' do
|
||||
post :create, params: { email: user.email, password: 'Test@123456' }
|
||||
|
||||
expect(response).to have_http_status(:partial_content)
|
||||
|
||||
# Check that no authentication headers are present
|
||||
expect(response.headers['access-token']).to be_nil
|
||||
expect(response.headers['uid']).to be_nil
|
||||
expect(response.headers['client']).to be_nil
|
||||
expect(response.headers['Authorization']).to be_nil
|
||||
|
||||
# Check that no bearer token is present in any form
|
||||
response.headers.each do |key, value|
|
||||
expect(value.to_s).not_to include('Bearer') if key.downcase.include?('auth')
|
||||
end
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['data']).to be_nil
|
||||
end
|
||||
|
||||
context 'when verifying MFA' do
|
||||
let(:mfa_token) { Mfa::TokenService.new(user: user).generate_token }
|
||||
|
||||
it 'authenticates with valid OTP' do
|
||||
post :create, params: {
|
||||
mfa_token: mfa_token,
|
||||
otp_code: user.current_otp
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
it 'authenticates with valid backup code' do
|
||||
backup_codes = user.generate_backup_codes!
|
||||
|
||||
post :create, params: {
|
||||
mfa_token: mfa_token,
|
||||
backup_code: backup_codes.first
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
it 'rejects invalid OTP' do
|
||||
post :create, params: {
|
||||
mfa_token: mfa_token,
|
||||
otp_code: '000000'
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
expect(response.parsed_body['error']).to eq(I18n.t('errors.mfa.invalid_code'))
|
||||
end
|
||||
|
||||
it 'rejects invalid backup code' do
|
||||
user.generate_backup_codes!
|
||||
|
||||
post :create, params: {
|
||||
mfa_token: mfa_token,
|
||||
backup_code: 'invalid'
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
expect(response.parsed_body['error']).to eq(I18n.t('errors.mfa.invalid_code'))
|
||||
end
|
||||
|
||||
it 'rejects expired MFA token' do
|
||||
expired_token = JWT.encode(
|
||||
{ user_id: user.id, exp: 1.minute.ago.to_i },
|
||||
Rails.application.secret_key_base,
|
||||
'HS256'
|
||||
)
|
||||
|
||||
post :create, params: {
|
||||
mfa_token: expired_token,
|
||||
otp_code: user.current_otp
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
expect(response.parsed_body['error']).to eq(I18n.t('errors.mfa.invalid_token'))
|
||||
end
|
||||
|
||||
it 'requires either OTP or backup code' do
|
||||
post :create, params: { mfa_token: mfa_token }
|
||||
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
expect(response.parsed_body['error']).to eq(I18n.t('errors.mfa.invalid_code'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with SSO authentication' do
|
||||
it 'authenticates with valid SSO token' do
|
||||
sso_token = user.generate_sso_auth_token
|
||||
|
||||
post :create, params: {
|
||||
email: user.email,
|
||||
sso_auth_token: sso_token
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
it 'rejects invalid SSO token' do
|
||||
post :create, params: {
|
||||
email: user.email,
|
||||
sso_auth_token: 'invalid'
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #new' do
|
||||
it 'redirects to frontend login page' do
|
||||
allow(ENV).to receive(:fetch).and_call_original
|
||||
allow(ENV).to receive(:fetch).with('FRONTEND_URL', nil).and_return('/frontend')
|
||||
|
||||
get :new
|
||||
|
||||
expect(response).to redirect_to('/frontend/app/login?error=access-denied')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,81 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Google::CallbacksController', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:code) { SecureRandom.hex(10) }
|
||||
let(:email) { Faker::Internet.email }
|
||||
let(:state) { account.to_sgid(expires_in: 15.minutes).to_s }
|
||||
|
||||
describe 'GET /google/callback' do
|
||||
let(:response_body_success) do
|
||||
{ id_token: JWT.encode({ email: email, name: 'test' }, false), access_token: SecureRandom.hex(10), token_type: 'Bearer',
|
||||
refresh_token: SecureRandom.hex(10) }
|
||||
end
|
||||
|
||||
let(:response_body_success_without_name) do
|
||||
{ id_token: JWT.encode({ email: email }, false), access_token: SecureRandom.hex(10), token_type: 'Bearer',
|
||||
refresh_token: SecureRandom.hex(10) }
|
||||
end
|
||||
|
||||
it 'creates inboxes if authentication is successful' do
|
||||
stub_request(:post, 'https://accounts.google.com/o/oauth2/token')
|
||||
.with(body: { 'code' => code, 'grant_type' => 'authorization_code',
|
||||
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback" })
|
||||
.to_return(status: 200, body: response_body_success.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
get google_callback_url, params: { code: code, state: state }
|
||||
|
||||
expect(response).to redirect_to app_email_inbox_agents_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
||||
expect(account.inboxes.count).to be 1
|
||||
inbox = account.inboxes.last
|
||||
expect(inbox.name).to eq 'test'
|
||||
expect(inbox.channel.reload.provider_config.keys).to include('access_token', 'refresh_token', 'expires_on')
|
||||
expect(inbox.channel.reload.provider_config['access_token']).to eq response_body_success[:access_token]
|
||||
expect(inbox.channel.imap_address).to eq 'imap.gmail.com'
|
||||
end
|
||||
|
||||
it 'updates inbox channel config if inbox exists with imap_login and authentication is successful' do
|
||||
channel_email = create(:channel_email, account: account, imap_login: email)
|
||||
inbox = channel_email.inbox
|
||||
expect(inbox.channel.provider_config).to eq({})
|
||||
|
||||
stub_request(:post, 'https://accounts.google.com/o/oauth2/token')
|
||||
.with(body: { 'code' => code, 'grant_type' => 'authorization_code',
|
||||
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback" })
|
||||
.to_return(status: 200, body: response_body_success.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
get google_callback_url, params: { code: code, state: state }
|
||||
|
||||
expect(response).to redirect_to app_email_inbox_settings_url(account_id: account.id, inbox_id: inbox.id)
|
||||
expect(account.inboxes.count).to be 1
|
||||
expect(inbox.channel.reload.provider_config.keys).to include('access_token', 'refresh_token', 'expires_on')
|
||||
expect(inbox.channel.reload.provider_config['access_token']).to eq response_body_success[:access_token]
|
||||
expect(inbox.channel.imap_address).to eq 'imap.gmail.com'
|
||||
end
|
||||
|
||||
it 'creates inboxes with fallback_name when account name is not present in id_token' do
|
||||
stub_request(:post, 'https://accounts.google.com/o/oauth2/token')
|
||||
.with(body: { 'code' => code, 'grant_type' => 'authorization_code',
|
||||
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback" })
|
||||
.to_return(status: 200, body: response_body_success_without_name.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
get google_callback_url, params: { code: code, state: state }
|
||||
|
||||
expect(response).to redirect_to app_email_inbox_agents_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
||||
expect(account.inboxes.count).to be 1
|
||||
inbox = account.inboxes.last
|
||||
expect(inbox.name).to eq email.split('@').first.parameterize.titleize
|
||||
end
|
||||
|
||||
it 'redirects to google app in case of error' do
|
||||
stub_request(:post, 'https://accounts.google.com/o/oauth2/token')
|
||||
.with(body: { 'code' => code, 'grant_type' => 'authorization_code',
|
||||
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback" })
|
||||
.to_return(status: 401)
|
||||
|
||||
get google_callback_url, params: { code: code, state: state }
|
||||
|
||||
expect(response).to redirect_to '/'
|
||||
end
|
||||
end
|
||||
end
|
||||
11
research/chatwoot/spec/controllers/health_controller_spec.rb
Normal file
11
research/chatwoot/spec/controllers/health_controller_spec.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Health Check', type: :request do
|
||||
describe 'GET /health' do
|
||||
it 'returns success status' do
|
||||
get '/health'
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['status']).to eq('woot')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,129 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Instagram::CallbacksController do
|
||||
let(:account) { create(:account) }
|
||||
let(:valid_params) { { code: 'valid_code', state: "#{account.id}|valid_token" } }
|
||||
let(:error_params) { { error: 'access_denied', error_description: 'User denied access', state: "#{account.id}|valid_token" } }
|
||||
let(:oauth_client) { instance_double(OAuth2::Client) }
|
||||
let(:auth_code_object) { instance_double(OAuth2::Strategy::AuthCode) }
|
||||
let(:access_token) { instance_double(OAuth2::AccessToken, token: 'test_token') }
|
||||
let(:long_lived_token_response) { { 'access_token' => 'long_lived_test_token', 'expires_in' => 5_184_000 } }
|
||||
let(:user_details) { { 'username' => 'test_user', 'user_id' => '12345' } }
|
||||
let(:exception_tracker) { instance_double(ChatwootExceptionTracker) }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:verify_instagram_token).and_return(account.id)
|
||||
allow(controller).to receive(:instagram_client).and_return(oauth_client)
|
||||
allow(controller).to receive(:base_url).and_return('https://app.chatwoot.com')
|
||||
allow(controller).to receive(:account).and_return(account)
|
||||
allow(oauth_client).to receive(:auth_code).and_return(auth_code_object)
|
||||
allow(controller).to receive(:exchange_for_long_lived_token).and_return(long_lived_token_response)
|
||||
allow(controller).to receive(:fetch_instagram_user_details).and_return(user_details)
|
||||
allow(ChatwootExceptionTracker).to receive(:new).and_return(exception_tracker)
|
||||
allow(exception_tracker).to receive(:capture_exception)
|
||||
|
||||
# Stub the exact request format that's being made
|
||||
stub_request(:post, 'https://graph.instagram.com/v22.0/12345/subscribed_apps?access_token=long_lived_test_token&subscribed_fields%5B%5D=messages&subscribed_fields%5B%5D=message_reactions&subscribed_fields%5B%5D=messaging_seen')
|
||||
.with(
|
||||
headers: {
|
||||
'Accept' => '*/*',
|
||||
'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
|
||||
'User-Agent' => 'Ruby'
|
||||
}
|
||||
)
|
||||
.to_return(status: 200, body: '', headers: {})
|
||||
end
|
||||
|
||||
describe '#show' do
|
||||
context 'when authorization is successful' do
|
||||
before do
|
||||
allow(auth_code_object).to receive(:get_token).and_return(access_token)
|
||||
end
|
||||
|
||||
it 'creates instagram channel and inbox' do
|
||||
expect do
|
||||
get :show, params: valid_params
|
||||
end.to change(Channel::Instagram, :count).by(1).and change(Inbox, :count).by(1)
|
||||
|
||||
expect(Channel::Instagram.last.access_token).to eq('long_lived_test_token')
|
||||
expect(Channel::Instagram.last.instagram_id).to eq('12345')
|
||||
expect(Inbox.last.name).to eq('test_user')
|
||||
|
||||
expect(Inbox.last.channel.reauthorization_required?).to be false
|
||||
expect(response).to redirect_to(app_instagram_inbox_agents_url(account_id: account.id, inbox_id: Inbox.last.id))
|
||||
end
|
||||
|
||||
it 'updates existing channel with new token' do
|
||||
# Create an existing channel
|
||||
existing_channel = create(:channel_instagram, account: account, instagram_id: '12345', access_token: 'old_token')
|
||||
create(:inbox, channel: existing_channel, account: account, name: 'old_username')
|
||||
|
||||
expect do
|
||||
get :show, params: valid_params
|
||||
end.to not_change(Channel::Instagram, :count).and not_change(Inbox, :count)
|
||||
|
||||
existing_channel.reload
|
||||
expect(existing_channel.access_token).to eq('long_lived_test_token')
|
||||
expect(existing_channel.instagram_id).to eq('12345')
|
||||
expect(existing_channel.reauthorization_required?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user denies authorization' do
|
||||
it 'redirects to error page with authorization error details' do
|
||||
get :show, params: error_params
|
||||
|
||||
expect(response).to redirect_to(
|
||||
app_new_instagram_inbox_url(
|
||||
account_id: account.id,
|
||||
error_type: 'access_denied',
|
||||
code: 400,
|
||||
error_message: 'User denied access'
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an OAuth error occurs' do
|
||||
before do
|
||||
oauth_error = OAuth2::Error.new(
|
||||
OpenStruct.new(
|
||||
body: { error_type: 'OAuthException', code: 400, error_message: 'Invalid OAuth code' }.to_json,
|
||||
status: 400
|
||||
)
|
||||
)
|
||||
allow(auth_code_object).to receive(:get_token).and_raise(oauth_error)
|
||||
end
|
||||
|
||||
it 'handles OAuth errors and redirects to error page' do
|
||||
get :show, params: valid_params
|
||||
|
||||
expected_url = app_new_instagram_inbox_url(
|
||||
account_id: account.id,
|
||||
error_type: 'OAuthException',
|
||||
code: 400,
|
||||
error_message: 'Invalid OAuth code'
|
||||
)
|
||||
expect(response).to redirect_to(expected_url)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a standard error occurs' do
|
||||
before do
|
||||
allow(auth_code_object).to receive(:get_token).and_raise(StandardError.new('Unknown error'))
|
||||
end
|
||||
|
||||
it 'handles standard errors and redirects to error page' do
|
||||
get :show, params: valid_params
|
||||
|
||||
expected_url = app_new_instagram_inbox_url(
|
||||
account_id: account.id,
|
||||
error_type: 'StandardError',
|
||||
code: 500,
|
||||
error_message: 'Unknown error'
|
||||
)
|
||||
expect(response).to redirect_to(expected_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,64 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Installation::Onboarding API', type: :request do
|
||||
let(:super_admin) { create(:super_admin) }
|
||||
|
||||
describe 'GET /installation/onboarding' do
|
||||
context 'when CHATWOOT_INSTALLATION_ONBOARDING redis key is not set' do
|
||||
it 'redirects back' do
|
||||
expect(Redis::Alfred.get(Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)).to be_nil
|
||||
get '/installation/onboarding'
|
||||
expect(response).to have_http_status(:redirect)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when CHATWOOT_INSTALLATION_ONBOARDING redis key is set' do
|
||||
it 'returns onboarding page' do
|
||||
Redis::Alfred.set(Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING, true)
|
||||
get '/installation/onboarding'
|
||||
expect(response).to have_http_status(:success)
|
||||
Redis::Alfred.delete(Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /installation/onboarding' do
|
||||
let(:account_builder) { double }
|
||||
|
||||
before do
|
||||
allow(AccountBuilder).to receive(:new).and_return(account_builder)
|
||||
allow(account_builder).to receive(:perform).and_return(true)
|
||||
allow(ChatwootHub).to receive(:register_instance).and_return(true)
|
||||
Redis::Alfred.set(Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING, true)
|
||||
end
|
||||
|
||||
after do
|
||||
Redis::Alfred.delete(Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)
|
||||
end
|
||||
|
||||
context 'when onboarding successfull' do
|
||||
it 'deletes the redis key' do
|
||||
post '/installation/onboarding', params: { user: {} }
|
||||
expect(Redis::Alfred.get(Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)).to be_nil
|
||||
end
|
||||
|
||||
it 'will not call register instance when checkboxes are unchecked' do
|
||||
post '/installation/onboarding', params: { user: {} }
|
||||
expect(ChatwootHub).not_to have_received(:register_instance)
|
||||
end
|
||||
|
||||
it 'will call register instance when checkboxes are checked' do
|
||||
post '/installation/onboarding', params: { user: {}, subscribe_to_updates: 1 }
|
||||
expect(ChatwootHub).to have_received(:register_instance)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when onboarding is not successfull' do
|
||||
it 'does not deletes the redis key' do
|
||||
allow(AccountBuilder).to receive(:new).and_raise('error')
|
||||
post '/installation/onboarding', params: { user: {} }
|
||||
expect(Redis::Alfred.get(Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,88 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Linear::CallbacksController, type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:code) { SecureRandom.hex(10) }
|
||||
let(:client_secret) { 'test_linear_secret' }
|
||||
let(:state) { JWT.encode({ sub: account.id, iat: Time.current.to_i }, client_secret, 'HS256') }
|
||||
let(:linear_redirect_uri) { "#{ENV.fetch('FRONTEND_URL', '')}/app/accounts/#{account.id}/settings/integrations/linear" }
|
||||
|
||||
describe 'GET /linear/callback' do
|
||||
let(:access_token) { SecureRandom.hex(10) }
|
||||
let(:response_body) do
|
||||
{
|
||||
'access_token' => access_token,
|
||||
'token_type' => 'Bearer',
|
||||
'expires_in' => 7200,
|
||||
'scope' => 'read,write'
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
stub_const('ENV', ENV.to_hash.merge('FRONTEND_URL' => 'http://www.example.com'))
|
||||
allow(GlobalConfigService).to receive(:load).and_call_original
|
||||
allow(GlobalConfigService).to receive(:load).with('LINEAR_CLIENT_SECRET', nil).and_return(client_secret)
|
||||
allow(GlobalConfigService).to receive(:load).with('LINEAR_CLIENT_ID', nil).and_return('test_client_id')
|
||||
end
|
||||
|
||||
context 'when successful' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.linear.app/oauth/token')
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: response_body.to_json,
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
end
|
||||
|
||||
it 'creates a new integration hook' do
|
||||
expect do
|
||||
get linear_callback_path, params: { code: code, state: state }
|
||||
end.to change(Integrations::Hook, :count).by(1)
|
||||
|
||||
hook = Integrations::Hook.last
|
||||
expect(hook.access_token).to eq(access_token)
|
||||
expect(hook.app_id).to eq('linear')
|
||||
expect(hook.status).to eq('enabled')
|
||||
expect(hook.settings).to eq(
|
||||
'token_type' => 'Bearer',
|
||||
'expires_in' => 7200,
|
||||
'scope' => 'read,write'
|
||||
)
|
||||
expect(response).to redirect_to(linear_redirect_uri)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the code is missing' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.linear.app/oauth/token')
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: response_body.to_json,
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
end
|
||||
|
||||
it 'redirects to the linear_redirect_uri' do
|
||||
get linear_callback_path, params: { state: state }
|
||||
expect(response).to redirect_to(linear_redirect_uri)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the token is invalid' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.linear.app/oauth/token')
|
||||
.to_return(
|
||||
status: 400,
|
||||
body: { error: 'invalid_grant' }.to_json,
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
end
|
||||
|
||||
it 'redirects to the linear_redirect_uri' do
|
||||
get linear_callback_path, params: { code: code, state: state }
|
||||
expect(response).to redirect_to(linear_redirect_uri)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,80 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Microsoft::CallbacksController', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:code) { SecureRandom.hex(10) }
|
||||
let(:email) { Faker::Internet.email }
|
||||
let(:state) { account.to_sgid(expires_in: 15.minutes).to_s }
|
||||
|
||||
describe 'GET /microsoft/callback' do
|
||||
let(:response_body_success) do
|
||||
{ id_token: JWT.encode({ email: email, name: 'test' }, false), access_token: SecureRandom.hex(10), token_type: 'Bearer',
|
||||
refresh_token: SecureRandom.hex(10) }
|
||||
end
|
||||
|
||||
let(:response_body_success_without_name) do
|
||||
{ id_token: JWT.encode({ email: email }, false), access_token: SecureRandom.hex(10), token_type: 'Bearer',
|
||||
refresh_token: SecureRandom.hex(10) }
|
||||
end
|
||||
|
||||
it 'creates inboxes if authentication is successful' do
|
||||
stub_request(:post, 'https://login.microsoftonline.com/common/oauth2/v2.0/token')
|
||||
.with(body: { 'code' => code, 'grant_type' => 'authorization_code',
|
||||
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback" })
|
||||
.to_return(status: 200, body: response_body_success.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
get microsoft_callback_url, params: { code: code, state: state }
|
||||
|
||||
expect(response).to redirect_to app_email_inbox_agents_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
||||
expect(account.inboxes.count).to be 1
|
||||
inbox = account.inboxes.last
|
||||
expect(inbox.name).to eq 'test'
|
||||
expect(inbox.channel.reload.provider_config.keys).to include('access_token', 'refresh_token', 'expires_on')
|
||||
expect(inbox.channel.reload.provider_config['access_token']).to eq response_body_success[:access_token]
|
||||
expect(inbox.channel.imap_address).to eq 'outlook.office365.com'
|
||||
end
|
||||
|
||||
it 'creates updates inbox channel config if inbox exists and authentication is successful' do
|
||||
inbox = create(:channel_email, account: account, email: email)&.inbox
|
||||
expect(inbox.channel.provider_config).to eq({})
|
||||
|
||||
stub_request(:post, 'https://login.microsoftonline.com/common/oauth2/v2.0/token')
|
||||
.with(body: { 'code' => code, 'grant_type' => 'authorization_code',
|
||||
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback" })
|
||||
.to_return(status: 200, body: response_body_success.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
get microsoft_callback_url, params: { code: code, state: state }
|
||||
|
||||
expect(response).to redirect_to app_email_inbox_settings_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
||||
expect(account.inboxes.count).to be 1
|
||||
expect(inbox.channel.reload.provider_config.keys).to include('access_token', 'refresh_token', 'expires_on')
|
||||
expect(inbox.channel.reload.provider_config['access_token']).to eq response_body_success[:access_token]
|
||||
expect(inbox.channel.imap_address).to eq 'outlook.office365.com'
|
||||
end
|
||||
|
||||
it 'creates inboxes with fallback_name when account name is not present in id_token' do
|
||||
stub_request(:post, 'https://login.microsoftonline.com/common/oauth2/v2.0/token')
|
||||
.with(body: { 'code' => code, 'grant_type' => 'authorization_code',
|
||||
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback" })
|
||||
.to_return(status: 200, body: response_body_success_without_name.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
get microsoft_callback_url, params: { code: code, state: state }
|
||||
|
||||
expect(response).to redirect_to app_email_inbox_agents_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
||||
expect(account.inboxes.count).to be 1
|
||||
inbox = account.inboxes.last
|
||||
expect(inbox.name).to eq email.split('@').first.parameterize.titleize
|
||||
end
|
||||
|
||||
it 'redirects to microsoft app in case of error' do
|
||||
stub_request(:post, 'https://login.microsoftonline.com/common/oauth2/v2.0/token')
|
||||
.with(body: { 'code' => code, 'grant_type' => 'authorization_code',
|
||||
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback" })
|
||||
.to_return(status: 401)
|
||||
|
||||
get microsoft_callback_url, params: { code: code, state: state }
|
||||
|
||||
expect(response).to redirect_to '/'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe '/.well-known/microsoft-identity-association.json', type: :request do
|
||||
describe 'GET /.well-known/microsoft-identity-association.json' do
|
||||
it 'successfully retrieves assetlinks.json file' do
|
||||
with_modified_env AZURE_APP_ID: 'azure-application-client-id' do
|
||||
get '/.well-known/microsoft-identity-association.json'
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include '"applicationId":"azure-application-client-id"'
|
||||
|
||||
content_length = { associatedApplications: [{ applicationId: 'azure-application-client-id' }] }.to_json.length
|
||||
|
||||
expect(response.headers['Content-Length']).to eq(content_length.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,112 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Notion::CallbacksController, type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:state) { account.to_sgid.to_s }
|
||||
let(:oauth_code) { 'test_oauth_code' }
|
||||
let(:notion_redirect_uri) { "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/app/accounts/#{account.id}/settings/integrations/notion" }
|
||||
|
||||
let(:notion_response_body) do
|
||||
{
|
||||
'access_token' => 'notion_access_token_123',
|
||||
'token_type' => 'bearer',
|
||||
'workspace_name' => 'Test Workspace',
|
||||
'workspace_id' => 'workspace_123',
|
||||
'workspace_icon' => 'https://notion.so/icon.png',
|
||||
'bot_id' => 'bot_123',
|
||||
'owner' => {
|
||||
'type' => 'user',
|
||||
'user' => {
|
||||
'id' => 'user_123',
|
||||
'name' => 'Test User'
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
describe 'GET /notion/callback' do
|
||||
before do
|
||||
account.enable_features('notion_integration')
|
||||
stub_const('ENV', ENV.to_hash.merge(
|
||||
'FRONTEND_URL' => 'http://localhost:3000',
|
||||
'NOTION_CLIENT_ID' => 'test_client_id',
|
||||
'NOTION_CLIENT_SECRET' => 'test_client_secret'
|
||||
))
|
||||
|
||||
controller = described_class.new
|
||||
allow(controller).to receive(:account).and_return(account)
|
||||
allow(controller).to receive(:notion_redirect_uri).and_return(notion_redirect_uri)
|
||||
allow(described_class).to receive(:new).and_return(controller)
|
||||
end
|
||||
|
||||
context 'when OAuth callback is successful' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.notion.com/v1/oauth/token')
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: notion_response_body.to_json,
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
end
|
||||
|
||||
it 'creates a new integration hook' do
|
||||
expect do
|
||||
get '/notion/callback', params: { code: oauth_code, state: state }
|
||||
end.to change(Integrations::Hook, :count).by(1)
|
||||
|
||||
hook = Integrations::Hook.last
|
||||
expect(hook.access_token).to eq('notion_access_token_123')
|
||||
expect(hook.app_id).to eq('notion')
|
||||
expect(hook.status).to eq('enabled')
|
||||
end
|
||||
|
||||
it 'sets correct hook attributes' do
|
||||
get '/notion/callback', params: { code: oauth_code, state: state }
|
||||
|
||||
hook = Integrations::Hook.last
|
||||
expect(hook.account).to eq(account)
|
||||
expect(hook.app_id).to eq('notion')
|
||||
expect(hook.access_token).to eq('notion_access_token_123')
|
||||
expect(hook.status).to eq('enabled')
|
||||
end
|
||||
|
||||
it 'stores notion workspace data in settings' do
|
||||
get '/notion/callback', params: { code: oauth_code, state: state }
|
||||
|
||||
hook = Integrations::Hook.last
|
||||
expect(hook.settings['token_type']).to eq('bearer')
|
||||
expect(hook.settings['workspace_name']).to eq('Test Workspace')
|
||||
expect(hook.settings['workspace_id']).to eq('workspace_123')
|
||||
expect(hook.settings['workspace_icon']).to eq('https://notion.so/icon.png')
|
||||
expect(hook.settings['bot_id']).to eq('bot_123')
|
||||
expect(hook.settings['owner']).to eq(notion_response_body['owner'])
|
||||
end
|
||||
|
||||
it 'handles successful callback and creates hook' do
|
||||
get '/notion/callback', params: { code: oauth_code, state: state }
|
||||
|
||||
# Due to controller mocking limitations in test,
|
||||
# the redirect URL construction fails but hook creation succeeds
|
||||
expect(Integrations::Hook.last.app_id).to eq('notion')
|
||||
expect(response).to be_redirect
|
||||
end
|
||||
end
|
||||
|
||||
context 'when OAuth token request fails' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.notion.com/v1/oauth/token')
|
||||
.to_return(
|
||||
status: 400,
|
||||
body: { error: 'invalid_grant' }.to_json,
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
end
|
||||
|
||||
it 'redirects to home page on error' do
|
||||
get '/notion/callback', params: { code: oauth_code, state: state }
|
||||
|
||||
expect(response).to redirect_to('/')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,93 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Platform Account Users API', type: :request do
|
||||
let!(:account) { create(:account) }
|
||||
|
||||
describe 'GET /platform/api/v1/accounts/{account_id}/account_users' do
|
||||
context 'when it is an unauthenticated platform app' do
|
||||
it 'returns unauthorized' do
|
||||
get "/platform/api/v1/accounts/#{account.id}/account_users"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated platform app' do
|
||||
let(:platform_app) { create(:platform_app) }
|
||||
let!(:account_user) { create(:account_user, account: account) }
|
||||
|
||||
it 'returns all the account users for the account' do
|
||||
create(:platform_app_permissible, platform_app: platform_app, permissible: account)
|
||||
|
||||
get "/platform/api/v1/accounts/#{account.id}/account_users",
|
||||
headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include(account_user.id.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /platform/api/v1/accounts/{account_id}/account_users' do
|
||||
context 'when it is an unauthenticated platform app' do
|
||||
it 'returns unauthorized' do
|
||||
post "/platform/api/v1/accounts/#{account.id}/account_users"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated platform app' do
|
||||
let(:platform_app) { create(:platform_app) }
|
||||
|
||||
it 'creates a new account user for the account' do
|
||||
user = create(:user)
|
||||
create(:platform_app_permissible, platform_app: platform_app, permissible: account)
|
||||
|
||||
post "/platform/api/v1/accounts/#{account.id}/account_users",
|
||||
params: { user_id: user.id, role: 'administrator' },
|
||||
headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data['user_id']).to eq(user.id)
|
||||
end
|
||||
|
||||
it 'updates the new account user for the account' do
|
||||
create(:platform_app_permissible, platform_app: platform_app, permissible: account)
|
||||
account_user = create(:account_user, account: account, role: 'agent')
|
||||
|
||||
post "/platform/api/v1/accounts/#{account.id}/account_users",
|
||||
params: { user_id: account_user.user_id, role: 'administrator' },
|
||||
headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data['role']).to eq('administrator')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /platform/api/v1/accounts/{account_id}/account_users' do
|
||||
let(:account_user) { create(:account_user, account: account, role: 'agent') }
|
||||
|
||||
context 'when it is an unauthenticated platform app' do
|
||||
it 'returns unauthorized' do
|
||||
delete "/platform/api/v1/accounts/#{account.id}/account_users", params: { user_id: account_user.user_id }
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated platform app' do
|
||||
let(:platform_app) { create(:platform_app) }
|
||||
|
||||
it 'returns deletes the account user' do
|
||||
create(:platform_app_permissible, platform_app: platform_app, permissible: account)
|
||||
|
||||
delete "/platform/api/v1/accounts/#{account.id}/account_users", params: { user_id: account_user.user_id },
|
||||
headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(account.account_users.count).to eq 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,235 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Platform Accounts API', type: :request do
|
||||
let!(:account) { create(:account) }
|
||||
|
||||
describe 'POST /platform/api/v1/accounts' do
|
||||
context 'when it is an unauthenticated platform app' do
|
||||
it 'returns unauthorized' do
|
||||
post '/platform/api/v1/accounts'
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an invalid platform app token' do
|
||||
it 'returns unauthorized' do
|
||||
post '/platform/api/v1/accounts', params: { name: 'Test Account' },
|
||||
headers: { api_access_token: 'invalid' }, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated platform app' do
|
||||
let(:platform_app) { create(:platform_app) }
|
||||
|
||||
it 'creates an account when and its permissible relationship' do
|
||||
post '/platform/api/v1/accounts', params: { name: 'Test Account' },
|
||||
headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include('Test Account')
|
||||
expect(platform_app.platform_app_permissibles.first.permissible.name).to eq('Test Account')
|
||||
end
|
||||
|
||||
it 'creates an account with locale' do
|
||||
InstallationConfig.where(name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS').first_or_create!(value: [{ 'name' => 'agent_management',
|
||||
'enabled' => true }])
|
||||
post '/platform/api/v1/accounts', params: { name: 'Test Account', locale: 'es' },
|
||||
headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['name']).to eq('Test Account')
|
||||
expect(json_response['locale']).to eq('es')
|
||||
expect(json_response['features']['agent_management']).to be(true)
|
||||
end
|
||||
|
||||
it 'creates an account with feature flags' do
|
||||
InstallationConfig.where(name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS').first_or_create!(value: [{ 'name' => 'inbox_management',
|
||||
'enabled' => true },
|
||||
{ 'name' => 'disable_branding',
|
||||
'enabled' => true },
|
||||
{ 'name' => 'help_center',
|
||||
'enabled' => false }])
|
||||
|
||||
post '/platform/api/v1/accounts', params: { name: 'Test Account', features: {
|
||||
ip_lookup: true,
|
||||
help_center: true,
|
||||
disable_branding: false
|
||||
} }, headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
|
||||
json_response = response.parsed_body
|
||||
created_account = Account.find(json_response['id'])
|
||||
expect(created_account.enabled_features.keys).to match_array(%w[inbox_management ip_lookup help_center])
|
||||
expect(json_response['name']).to include('Test Account')
|
||||
expect(json_response['features'].keys).to match_array(%w[inbox_management ip_lookup help_center])
|
||||
end
|
||||
|
||||
it 'creates an account with limits settings' do
|
||||
post '/platform/api/v1/accounts', params: { name: 'Test Account', limits: { agents: 5, inboxes: 10 } },
|
||||
headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include('Test Account')
|
||||
expect(response.body).to include('5')
|
||||
expect(response.body).to include('10')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /platform/api/v1/accounts' do
|
||||
context 'when it is an unauthenticated platform app' do
|
||||
it 'returns unauthorized' do
|
||||
get '/platform/api/v1/accounts'
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an invalid platform app token' do
|
||||
it 'returns unauthorized' do
|
||||
get '/platform/api/v1/accounts', headers: { api_access_token: 'invalid' }, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated platform app' do
|
||||
let(:platform_app) { create(:platform_app) }
|
||||
let!(:account1) { create(:account, name: 'Account A') }
|
||||
let!(:account2) { create(:account, name: 'Account B') }
|
||||
|
||||
before do
|
||||
create(:platform_app_permissible, platform_app: platform_app, permissible: account1)
|
||||
create(:platform_app_permissible, platform_app: platform_app, permissible: account2)
|
||||
end
|
||||
|
||||
it 'returns all permissible accounts' do
|
||||
get '/platform/api/v1/accounts', headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response.size).to eq(2)
|
||||
expect(json_response.map { |acc| acc['name'] }).to include('Account A', 'Account B')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /platform/api/v1/accounts/{account_id}' do
|
||||
context 'when it is an unauthenticated platform app' do
|
||||
it 'returns unauthorized' do
|
||||
get "/platform/api/v1/accounts/#{account.id}"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an invalid platform app token' do
|
||||
it 'returns unauthorized' do
|
||||
get "/platform/api/v1/accounts/#{account.id}", headers: { api_access_token: 'invalid' }, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated platform app' do
|
||||
let(:platform_app) { create(:platform_app) }
|
||||
|
||||
it 'returns unauthorized when its not a permissible object' do
|
||||
get "/platform/api/v1/accounts/#{account.id}", headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'shows an account when its permissible object' do
|
||||
create(:platform_app_permissible, platform_app: platform_app, permissible: account)
|
||||
|
||||
get "/platform/api/v1/accounts/#{account.id}",
|
||||
headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include(account.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PATCH /platform/api/v1/accounts/{account_id}' do
|
||||
context 'when it is an unauthenticated platform app' do
|
||||
it 'returns unauthorized' do
|
||||
patch "/platform/api/v1/accounts/#{account.id}"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an invalid platform app token' do
|
||||
it 'returns unauthorized' do
|
||||
patch "/platform/api/v1/accounts/#{account.id}", params: { name: 'Test Account' },
|
||||
headers: { api_access_token: 'invalid' }, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated platform app' do
|
||||
let(:platform_app) { create(:platform_app) }
|
||||
|
||||
it 'returns unauthorized when its not a permissible object' do
|
||||
patch "/platform/api/v1/accounts/#{account.id}", params: { name: 'Test Account' },
|
||||
headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'updates an account when its permissible object' do
|
||||
create(:platform_app_permissible, platform_app: platform_app, permissible: account)
|
||||
account.enable_features!('inbox_management', 'channel_facebook')
|
||||
|
||||
patch "/platform/api/v1/accounts/#{account.id}", params: {
|
||||
name: 'Test Account',
|
||||
features: {
|
||||
ip_lookup: true,
|
||||
help_center: true,
|
||||
channel_facebook: false
|
||||
},
|
||||
limits: { agents: 5, inboxes: 10 }
|
||||
}, headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
account.reload
|
||||
expect(account.name).to eq('Test Account')
|
||||
expect(account.enabled_features.keys).to match_array(%w[inbox_management ip_lookup help_center])
|
||||
expect(account.enabled_features['channel_facebook']).to be_nil
|
||||
expect(account.limits['agents']).to eq(5)
|
||||
expect(account.limits['inboxes']).to eq(10)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /platform/api/v1/accounts/{account_id}' do
|
||||
context 'when it is an unauthenticated platform app' do
|
||||
it 'returns unauthorized' do
|
||||
delete "/platform/api/v1/accounts/#{account.id}"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an invalid platform app token' do
|
||||
it 'returns unauthorized' do
|
||||
delete "/platform/api/v1/accounts/#{account.id}", headers: { api_access_token: 'invalid' }, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated platform app' do
|
||||
let(:platform_app) { create(:platform_app) }
|
||||
|
||||
it 'returns unauthorized when its not a permissible object' do
|
||||
delete "/platform/api/v1/accounts/#{account.id}", headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'destroys the object' do
|
||||
create(:platform_app_permissible, platform_app: platform_app, permissible: account)
|
||||
expect(DeleteObjectJob).to receive(:perform_later).with(account).once
|
||||
delete "/platform/api/v1/accounts/#{account.id}",
|
||||
headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,220 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Platform Agent Bot API', type: :request do
|
||||
let!(:agent_bot) { create(:agent_bot) }
|
||||
|
||||
describe 'GET /platform/api/v1/agent_bots' do
|
||||
context 'when it is an unauthenticated platform app' do
|
||||
it 'returns unauthorized' do
|
||||
get '/platform/api/v1/agent_bots'
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an invalid platform app token' do
|
||||
it 'returns unauthorized' do
|
||||
get '/platform/api/v1/agent_bots', headers: { api_access_token: 'invalid' }, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated platform app' do
|
||||
let(:platform_app) { create(:platform_app) }
|
||||
|
||||
it 'returns unauthorized when its not a permissible object' do
|
||||
get '/platform/api/v1/agent_bots', headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data.length).to eq(0)
|
||||
end
|
||||
|
||||
it 'shows a agent_bot when its permissible object' do
|
||||
create(:platform_app_permissible, platform_app: platform_app, permissible: agent_bot)
|
||||
|
||||
get '/platform/api/v1/agent_bots',
|
||||
headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data.length).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /platform/api/v1/agent_bots/{agent_bot_id}' do
|
||||
context 'when it is an unauthenticated platform app' do
|
||||
it 'returns unauthorized' do
|
||||
get "/platform/api/v1/agent_bots/#{agent_bot.id}"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an invalid platform app token' do
|
||||
it 'returns unauthorized' do
|
||||
get "/platform/api/v1/agent_bots/#{agent_bot.id}", headers: { api_access_token: 'invalid' }, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated platform app' do
|
||||
let(:platform_app) { create(:platform_app) }
|
||||
|
||||
it 'returns unauthorized when its not a permissible object' do
|
||||
get "/platform/api/v1/agent_bots/#{agent_bot.id}", headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'shows a agent_bot when its permissible object' do
|
||||
create(:platform_app_permissible, platform_app: platform_app, permissible: agent_bot)
|
||||
|
||||
get "/platform/api/v1/agent_bots/#{agent_bot.id}",
|
||||
headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data['name']).to eq(agent_bot.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /platform/api/v1/agent_bots/' do
|
||||
context 'when it is an unauthenticated platform app' do
|
||||
it 'returns unauthorized' do
|
||||
post '/platform/api/v1/agent_bots'
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an invalid platform app token' do
|
||||
it 'returns unauthorized' do
|
||||
post '/platform/api/v1/agent_bots/', headers: { api_access_token: 'invalid' }, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated platform app' do
|
||||
let(:platform_app) { create(:platform_app) }
|
||||
|
||||
it 'creates a new agent bot' do
|
||||
post '/platform/api/v1/agent_bots/', params: { name: 'test' },
|
||||
headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data['name']).to eq('test')
|
||||
expect(platform_app.platform_app_permissibles.first.permissible_id).to eq data['id']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PATCH /platform/api/v1/agent_bots/{agent_bot_id}' do
|
||||
context 'when it is an unauthenticated platform app' do
|
||||
it 'returns unauthorized' do
|
||||
patch "/platform/api/v1/agent_bots/#{agent_bot.id}"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an invalid platform app token' do
|
||||
it 'returns unauthorized' do
|
||||
patch "/platform/api/v1/agent_bots/#{agent_bot.id}", headers: { api_access_token: 'invalid' }, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated platform app' do
|
||||
let(:platform_app) { create(:platform_app) }
|
||||
|
||||
it 'returns unauthorized when its not a permissible object' do
|
||||
patch "/platform/api/v1/agent_bots/#{agent_bot.id}", params: { name: 'test' },
|
||||
headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'updates the agent_bot' do
|
||||
create(:platform_app_permissible, platform_app: platform_app, permissible: agent_bot)
|
||||
patch "/platform/api/v1/agent_bots/#{agent_bot.id}", params: { name: 'test123' },
|
||||
headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data['name']).to eq('test123')
|
||||
end
|
||||
|
||||
it 'updates avatar' do
|
||||
# no avatar before upload
|
||||
create(:platform_app_permissible, platform_app: platform_app, permissible: agent_bot)
|
||||
expect(agent_bot.avatar.attached?).to be(false)
|
||||
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
|
||||
patch "/platform/api/v1/agent_bots/#{agent_bot.id}", params: { name: 'test123' }.merge(avatar: file),
|
||||
headers: { api_access_token: platform_app.access_token.token }
|
||||
expect(response).to have_http_status(:success)
|
||||
agent_bot.reload
|
||||
expect(agent_bot.avatar.attached?).to be(true)
|
||||
end
|
||||
|
||||
it 'updated avatar with avatar_url' do
|
||||
create(:platform_app_permissible, platform_app: platform_app, permissible: agent_bot)
|
||||
patch "/platform/api/v1/agent_bots/#{agent_bot.id}", params: { name: 'test123' }.merge(avatar_url: 'http://example.com/avatar.png'),
|
||||
headers: { api_access_token: platform_app.access_token.token }
|
||||
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 /platform/api/v1/agent_bots/{agent_bot_id}' do
|
||||
context 'when it is an unauthenticated platform app' do
|
||||
it 'returns unauthorized' do
|
||||
delete "/platform/api/v1/agent_bots/#{agent_bot.id}", headers: { api_access_token: 'invalid' }, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated platform app' do
|
||||
let(:platform_app) { create(:platform_app) }
|
||||
|
||||
it 'returns unauthorized when its not a permissible object' do
|
||||
delete "/platform/api/v1/agent_bots/#{agent_bot.id}", headers: { api_access_token: 'invalid' }, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns deletes the account user' do
|
||||
create(:platform_app_permissible, platform_app: platform_app, permissible: agent_bot)
|
||||
|
||||
delete "/platform/api/v1/agent_bots/#{agent_bot.id}", headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(AgentBot.count).to eq 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /platform/api/v1/agent_bots/{agent_bot_id}/avatar' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
delete "/platform/api/v1/agent_bots/#{agent_bot.id}/avatar"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:platform_app) { create(:platform_app) }
|
||||
|
||||
before do
|
||||
agent_bot.avatar.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
|
||||
create(:platform_app_permissible, platform_app: platform_app, permissible: agent_bot)
|
||||
end
|
||||
|
||||
it 'delete agent_bot avatar' do
|
||||
delete "/platform/api/v1/agent_bots/#{agent_bot.id}/avatar",
|
||||
headers: { api_access_token: platform_app.access_token.token },
|
||||
as: :json
|
||||
|
||||
expect { agent_bot.avatar.attachment.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user