Merge feature/cw-7513 into feature/cw-7513-specs
This commit is contained in:
@@ -23,6 +23,52 @@ RSpec.describe 'API Base', type: :request do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when API and webhook access is disabled for the account' do
|
||||
let!(:admin) { create(:user, :administrator, account: account) }
|
||||
let!(:conversation) { create(:conversation, account: account) }
|
||||
|
||||
before do
|
||||
allow(Account).to receive(:find).and_call_original
|
||||
allow(Account).to receive(:find).with(account.id.to_s).and_return(account)
|
||||
allow(account).to receive(:api_and_webhooks_enabled?).and_return(false)
|
||||
end
|
||||
|
||||
it 'returns forbidden for token authenticated requests' do
|
||||
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(:forbidden)
|
||||
expect(response.parsed_body['error']).to eq('API access is not enabled for this account')
|
||||
end
|
||||
|
||||
it 'allows session authenticated requests' do
|
||||
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a self-hosted account has the feature flag disabled' do
|
||||
let!(:admin) { create(:user, :administrator, account: account) }
|
||||
let!(:conversation) { create(:conversation, account: account) }
|
||||
|
||||
before do
|
||||
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(false)
|
||||
account.disable_features!('api_and_webhooks')
|
||||
end
|
||||
|
||||
it 'allows token authenticated requests' do
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an invalid api_access_token' do
|
||||
it 'returns unauthorized' do
|
||||
get '/api/v1/profile',
|
||||
@@ -94,6 +140,21 @@ RSpec.describe 'API Base', type: :request do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when API and webhook access is disabled for the account' do
|
||||
it 'returns forbidden for accessible bot endpoints' do
|
||||
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
|
||||
allow(Account).to receive(:find).and_call_original
|
||||
allow(Account).to receive(:find).with(account.id.to_s).and_return(account)
|
||||
allow(account).to receive(:api_and_webhooks_enabled?).and_return(false)
|
||||
|
||||
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(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the account is suspended' do
|
||||
it 'returns 401 unauthorized' do
|
||||
account.update!(status: :suspended)
|
||||
|
||||
@@ -170,6 +170,29 @@ RSpec.describe 'Api::V1::Accounts::Articles', type: :request do
|
||||
expect(json_response['payload']['status']).to eql(article_params[:article][:status])
|
||||
expect(json_response['payload']['position']).to eql(article_params[:article][:position])
|
||||
end
|
||||
|
||||
it 'stages draft-only fields without bumping updated_at' do
|
||||
expect do
|
||||
put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles/#{article.id}",
|
||||
params: { article: { draft_title: 'Draft title', draft_content: 'Draft body' } },
|
||||
headers: admin.create_new_auth_token
|
||||
end.not_to(change { article.reload.updated_at })
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(article.draft_title).to eq('Draft title')
|
||||
expect(article.draft_content).to eq('Draft body')
|
||||
end
|
||||
|
||||
it 'rejects an over-length draft without persisting it' do
|
||||
expect do
|
||||
put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles/#{article.id}",
|
||||
params: { article: { draft_content: 'a' * 20_001 } },
|
||||
headers: admin.create_new_auth_token
|
||||
end.not_to(change { article.reload.draft_content })
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['message']).to include('too long')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Branded Email Layout 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(:layout) { '<html><body><header>Brand</header>{{ content_for_layout }}</body></html>' }
|
||||
|
||||
describe 'GET /api/v1/accounts/{account.id}/branded_email_layout' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v1/accounts/#{account.id}/branded_email_layout"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an agent' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v1/accounts/#{account.id}/branded_email_layout",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an administrator' do
|
||||
it 'returns the account-scoped branded email layout' do
|
||||
create(:email_template, :layout, account: account, body: layout)
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/branded_email_layout",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['branded_email_layout']).to eq(layout)
|
||||
end
|
||||
|
||||
it 'returns null when no account-scoped layout exists' do
|
||||
get "/api/v1/accounts/#{account.id}/branded_email_layout",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['branded_email_layout']).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PATCH /api/v1/accounts/{account.id}/branded_email_layout' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
patch "/api/v1/accounts/#{account.id}/branded_email_layout",
|
||||
params: { branded_email_layout: layout },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an agent' do
|
||||
it 'returns unauthorized' do
|
||||
patch "/api/v1/accounts/#{account.id}/branded_email_layout",
|
||||
headers: agent.create_new_auth_token,
|
||||
params: { branded_email_layout: layout },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an administrator' do
|
||||
it 'updates account-scoped branded email layout when feature is enabled' do
|
||||
account.enable_features!(:branded_email_templates)
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/branded_email_layout",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { branded_email_layout: layout },
|
||||
as: :json
|
||||
|
||||
template = EmailTemplate.account_branded_layout_template_for(account)
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(template.body).to eq(layout)
|
||||
expect(response.parsed_body['branded_email_layout']).to eq(layout)
|
||||
end
|
||||
|
||||
it 'clears account-scoped branded email layout when blank value is passed' do
|
||||
account.enable_features!(:branded_email_templates)
|
||||
create(:email_template, :layout, account: account, body: layout)
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/branded_email_layout",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { branded_email_layout: '' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(EmailTemplate.account_branded_layout_template_for(account)).to be_nil
|
||||
expect(response.parsed_body['branded_email_layout']).to be_nil
|
||||
end
|
||||
|
||||
it 'clears account-scoped branded email layout when null string is passed' do
|
||||
account.enable_features!(:branded_email_templates)
|
||||
create(:email_template, :layout, account: account, body: layout)
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/branded_email_layout",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { branded_email_layout: 'null' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(EmailTemplate.account_branded_layout_template_for(account)).to be_nil
|
||||
expect(response.parsed_body['branded_email_layout']).to be_nil
|
||||
end
|
||||
|
||||
it 'rejects updates when feature is disabled' do
|
||||
patch "/api/v1/accounts/#{account.id}/branded_email_layout",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { branded_email_layout: layout },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Branded email templates feature is not enabled')
|
||||
expect(EmailTemplate.account_branded_layout_template_for(account)).to be_nil
|
||||
end
|
||||
|
||||
it 'rejects account-scoped branded email layout without content slot' do
|
||||
account.enable_features!(:branded_email_templates)
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/branded_email_layout",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { branded_email_layout: '<html>No slot</html>' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to include('must include {{ content_for_layout }}')
|
||||
end
|
||||
|
||||
it 'rejects account-scoped branded email layout with invalid liquid syntax' do
|
||||
account.enable_features!(:branded_email_templates)
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/branded_email_layout",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { branded_email_layout: '<html>{{ content_for_layout }} {{ broken </html>' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to include('has invalid Liquid syntax')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -7,27 +7,120 @@ RSpec.describe '/api/v1/accounts/:account_id/conversations/:conversation_id/dire
|
||||
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(:blob_params) do
|
||||
{
|
||||
blob: {
|
||||
filename: 'avatar.png',
|
||||
byte_size: '1234',
|
||||
checksum: 'dsjbsdhbfif3874823mnsdbf',
|
||||
content_type: 'image/png'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def create_direct_upload(headers)
|
||||
post api_v1_account_conversation_direct_uploads_path(account_id: account.id, conversation_id: conversation.display_id),
|
||||
params: blob_params,
|
||||
headers: headers,
|
||||
as: :json
|
||||
end
|
||||
|
||||
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
|
||||
context 'when it is an unauthenticated request' do
|
||||
it 'returns unauthorized without any credentials' do
|
||||
create_direct_upload({})
|
||||
|
||||
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(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns unauthorized with an empty api_access_token header' do
|
||||
create_direct_upload({ api_access_token: '' })
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns unauthorized with an invalid api_access_token header' do
|
||||
create_direct_upload({ api_access_token: 'invalid-token' })
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated request with an api access token' do
|
||||
it 'creates the blob for the direct upload' do
|
||||
create_direct_upload({ api_access_token: agent.access_token.token })
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['content_type']).to eq('image/png')
|
||||
expect(response.parsed_body['content_type']).to eq('image/png')
|
||||
end
|
||||
|
||||
it 'returns unauthorized for an agent of another account' do
|
||||
other_agent = create(:user, account: create(:account), role: :agent)
|
||||
|
||||
create_direct_upload({ api_access_token: other_agent.access_token.token })
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns unauthorized for an agent bot token' do
|
||||
agent_bot = create(:agent_bot, account: account)
|
||||
|
||||
create_direct_upload({ api_access_token: agent_bot.access_token.token })
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the account api_and_webhooks feature is disabled' do
|
||||
before do
|
||||
allow(Account).to receive(:find).and_call_original
|
||||
allow(Account).to receive(:find).with(account.id.to_s).and_return(account)
|
||||
allow(account).to receive(:api_and_webhooks_enabled?).and_return(false)
|
||||
end
|
||||
|
||||
it 'returns forbidden for a token-authenticated request' do
|
||||
create_direct_upload({ api_access_token: agent.access_token.token })
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
|
||||
it 'still creates the blob for a session-authenticated request' do
|
||||
create_direct_upload(agent.create_new_auth_token)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['content_type']).to eq('image/png')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated session request' do
|
||||
it 'creates the blob for the direct upload' do
|
||||
create_direct_upload(agent.create_new_auth_token)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['content_type']).to eq('image/png')
|
||||
end
|
||||
|
||||
it 'creates the blob when the serialized api access token is empty' do
|
||||
create_direct_upload(agent.create_new_auth_token.merge('api_access_token' => ''))
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['content_type']).to eq('image/png')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when forgery protection is enabled' do
|
||||
around do |example|
|
||||
original = ActionController::Base.allow_forgery_protection
|
||||
ActionController::Base.allow_forgery_protection = true
|
||||
example.run
|
||||
ActionController::Base.allow_forgery_protection = original
|
||||
end
|
||||
|
||||
it 'creates the blob for a token-authenticated request without a CSRF token' do
|
||||
create_direct_upload({ api_access_token: agent.access_token.token })
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['content_type']).to eq('image/png')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -119,7 +119,13 @@ RSpec.describe 'Conversation Messages API', type: :request do
|
||||
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.' }))
|
||||
content: 'System reopened the conversation due to a new incoming message.',
|
||||
content_attributes: {
|
||||
activity: {
|
||||
type: 'conversation_status_changed',
|
||||
status: 'open'
|
||||
}
|
||||
} }))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -36,6 +36,18 @@ RSpec.describe 'Inboxes API', type: :request do
|
||||
expect(JSON.parse(response.body, symbolize_names: true)[:payload].size).to eq(2)
|
||||
end
|
||||
|
||||
it 'does not include branded email layout in index responses' do
|
||||
email_inbox = create(:inbox, :with_email, account: account)
|
||||
create(:email_template, :layout, account: account, inbox: email_inbox, body: '<html>{{ content_for_layout }} Branded</html>')
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/inboxes",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
inbox_data = JSON.parse(response.body, symbolize_names: true)[:payload].find { |item| item[:id] == email_inbox.id }
|
||||
expect(inbox_data).not_to have_key(:branded_email_layout)
|
||||
end
|
||||
|
||||
it 'returns only assigned inboxes of current_account as agent' do
|
||||
get "/api/v1/accounts/#{account.id}/inboxes",
|
||||
headers: agent.create_new_auth_token,
|
||||
@@ -161,8 +173,10 @@ RSpec.describe 'Inboxes API', type: :request do
|
||||
end
|
||||
|
||||
it 'returns imap details in inbox when admin' do
|
||||
account.enable_features!(:branded_email_templates)
|
||||
email_channel = create(:channel_email, account: account, imap_enabled: true, imap_login: 'test@test.com')
|
||||
email_inbox = create(:inbox, channel: email_channel, account: account)
|
||||
create(:email_template, :layout, account: account, inbox: email_inbox, body: '<html>{{ content_for_layout }} Branded</html>')
|
||||
|
||||
imap_connection = double
|
||||
allow(Mail).to receive(:connection).and_return(imap_connection)
|
||||
@@ -176,6 +190,35 @@ RSpec.describe 'Inboxes API', type: :request do
|
||||
|
||||
expect(data[:imap_enabled]).to be_truthy
|
||||
expect(data[:imap_login]).to eq('test@test.com')
|
||||
expect(data[:branded_email_layout]).to eq('<html>{{ content_for_layout }} Branded</html>')
|
||||
end
|
||||
|
||||
it 'does not return saved branded email layout when feature is disabled' do
|
||||
email_channel = create(:channel_email, account: account)
|
||||
email_inbox = create(:inbox, channel: email_channel, account: account)
|
||||
create(:email_template, :layout, account: account, inbox: email_inbox, body: '<html>{{ content_for_layout }} Branded</html>')
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/inboxes/#{email_inbox.id}",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body).not_to have_key('branded_email_layout')
|
||||
end
|
||||
|
||||
it 'does not return branded email layout for an agent' do
|
||||
email_channel = create(:channel_email, account: account)
|
||||
email_inbox = create(:inbox, channel: email_channel, account: account)
|
||||
create(:inbox_member, user: agent, inbox: email_inbox)
|
||||
create(:email_template, :layout, account: account, inbox: email_inbox, body: '<html>{{ content_for_layout }} Branded</html>')
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/inboxes/#{email_inbox.id}",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = JSON.parse(response.body, symbolize_names: true)
|
||||
expect(data[:branded_email_layout]).to be_nil
|
||||
end
|
||||
|
||||
context 'when it is a Twilio inbox' do
|
||||
@@ -577,6 +620,146 @@ RSpec.describe 'Inboxes API', type: :request do
|
||||
expect(email_channel.reload.email).to eq('emailtest@email.test')
|
||||
end
|
||||
|
||||
it 'updates branded email layout for email inbox when feature is enabled' do
|
||||
account.enable_features!(:branded_email_templates)
|
||||
email_channel = create(:channel_email, account: account)
|
||||
email_inbox = create(:inbox, channel: email_channel, account: account)
|
||||
layout = '<html><body><header>Brand</header>{{ content_for_layout }}</body></html>'
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/inboxes/#{email_inbox.id}",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { branded_email_layout: layout },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(email_inbox.reload.branded_email_layout).to eq(layout)
|
||||
expect(response.parsed_body['branded_email_layout']).to eq(layout)
|
||||
end
|
||||
|
||||
it 'rolls back branded email layout when inbox update fails' do
|
||||
account.enable_features!(:branded_email_templates)
|
||||
email_channel = create(:channel_email, account: account)
|
||||
email_inbox = create(:inbox, channel: email_channel, account: account)
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/inboxes/#{email_inbox.id}",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { name: '', branded_email_layout: '<html>{{ content_for_layout }} Branded</html>' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(email_inbox.reload.branded_email_layout).to be_nil
|
||||
end
|
||||
|
||||
it 'clears branded email layout when blank value is passed' do
|
||||
account.enable_features!(:branded_email_templates)
|
||||
email_channel = create(:channel_email, account: account)
|
||||
email_inbox = create(:inbox, channel: email_channel, account: account)
|
||||
create(:email_template, :layout, account: account, inbox: email_inbox)
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/inboxes/#{email_inbox.id}",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { branded_email_layout: '' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(email_inbox.reload.branded_email_layout).to be_nil
|
||||
end
|
||||
|
||||
it 'clears branded email layout when null string value is passed' do
|
||||
account.enable_features!(:branded_email_templates)
|
||||
email_channel = create(:channel_email, account: account)
|
||||
email_inbox = create(:inbox, channel: email_channel, account: account)
|
||||
create(:email_template, :layout, account: account, inbox: email_inbox)
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/inboxes/#{email_inbox.id}",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { branded_email_layout: 'null' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(email_inbox.reload.branded_email_layout).to be_nil
|
||||
end
|
||||
|
||||
it 'rejects branded email layout when feature is disabled' do
|
||||
email_channel = create(:channel_email, account: account)
|
||||
email_inbox = create(:inbox, channel: email_channel, account: account)
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/inboxes/#{email_inbox.id}",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { branded_email_layout: '<html>{{ content_for_layout }}</html>' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Branded email templates feature is not enabled')
|
||||
expect(email_inbox.reload.branded_email_layout).to be_nil
|
||||
end
|
||||
|
||||
it 'ignores blank branded email layout when feature is disabled' do
|
||||
email_channel = create(:channel_email, account: account)
|
||||
email_inbox = create(:inbox, channel: email_channel, account: account)
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/inboxes/#{email_inbox.id}",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { name: 'Renamed Email Inbox', branded_email_layout: nil },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(email_inbox.reload.name).to eq('Renamed Email Inbox')
|
||||
expect(email_inbox.branded_email_layout).to be_nil
|
||||
end
|
||||
|
||||
it 'rejects branded email layout for non-email inboxes' do
|
||||
account.enable_features!(:branded_email_templates)
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { branded_email_layout: '<html>{{ content_for_layout }}</html>' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Branded email layout is only supported for email inboxes')
|
||||
end
|
||||
|
||||
it 'ignores blank branded email layout for non-email inboxes' do
|
||||
account.enable_features!(:branded_email_templates)
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { name: 'Renamed Inbox', branded_email_layout: '' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(inbox.reload.name).to eq('Renamed Inbox')
|
||||
end
|
||||
|
||||
it 'rejects branded email layout without content slot' do
|
||||
account.enable_features!(:branded_email_templates)
|
||||
email_channel = create(:channel_email, account: account)
|
||||
email_inbox = create(:inbox, channel: email_channel, account: account)
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/inboxes/#{email_inbox.id}",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { branded_email_layout: '<html>No slot</html>' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to include('must include {{ content_for_layout }}')
|
||||
end
|
||||
|
||||
it 'rejects branded email layout with invalid liquid syntax' do
|
||||
account.enable_features!(:branded_email_templates)
|
||||
email_channel = create(:channel_email, account: account)
|
||||
email_inbox = create(:inbox, channel: email_channel, account: account)
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/inboxes/#{email_inbox.id}",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { branded_email_layout: '<html>{{ content_for_layout }} {{ broken </html>' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to include('has invalid Liquid syntax')
|
||||
end
|
||||
|
||||
it 'updates twilio sms inbox when administrator' do
|
||||
twilio_sms_channel = create(:channel_twilio_sms, account: account)
|
||||
twilio_sms_inbox = create(:inbox, channel: twilio_sms_channel, account: account)
|
||||
|
||||
@@ -174,7 +174,8 @@ RSpec.describe 'Api::V1::Accounts::Portals', type: :request do
|
||||
'default_locale' => 'en',
|
||||
'layout' => 'classic',
|
||||
'social_profiles' => {},
|
||||
'locale_translations' => {}
|
||||
'locale_translations' => {},
|
||||
'popular_content' => {}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
@@ -26,6 +26,17 @@ RSpec.describe 'Webhooks API', type: :request do
|
||||
expect(response.parsed_body['payload']['webhooks'].count).to eql account.webhooks.count
|
||||
end
|
||||
end
|
||||
|
||||
context 'when api_and_webhooks feature is disabled' do
|
||||
it 'allows session authenticated admins to manage webhooks' do
|
||||
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
|
||||
account.disable_features!('api_and_webhooks')
|
||||
get "/api/v1/accounts/#{account.id}/webhooks",
|
||||
headers: administrator.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/<account_id>/webhooks' do
|
||||
|
||||
@@ -456,29 +456,18 @@ RSpec.describe 'WhatsApp Authorization API', type: :request do
|
||||
create(:inbox_member, inbox: whatsapp_inbox, user: agent)
|
||||
end
|
||||
|
||||
it 'returns unprocessable_entity error' do
|
||||
it 'returns unauthorized 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)
|
||||
expect(Whatsapp::EmbeddedSignupService).not_to receive(:new)
|
||||
|
||||
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)
|
||||
# Reauthorizing an existing inbox swaps live credentials, so it is restricted to admins.
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -199,6 +199,22 @@ RSpec.describe 'Accounts API', type: :request do
|
||||
expect(response.body).to include(account.locale)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when API and webhook access is disabled for the account' do
|
||||
it 'returns forbidden for API token authentication' do
|
||||
account_scope = double
|
||||
allow(account_scope).to receive(:find).with(account.id.to_s).and_return(account)
|
||||
allow_any_instance_of(User).to receive(:accounts).and_return(account_scope) # rubocop:disable RSpec/AnyInstance
|
||||
allow(account).to receive(:api_and_webhooks_enabled?).and_return(false)
|
||||
|
||||
get "/api/v1/accounts/#{account.id}",
|
||||
headers: { api_access_token: admin.access_token.token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(response.parsed_body['error']).to eq('API access is not enabled for this account')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/accounts/{account.id}/cache_keys' do
|
||||
@@ -225,6 +241,21 @@ RSpec.describe 'Accounts API', type: :request do
|
||||
expect(response.headers['Cache-Control']).to include('private')
|
||||
expect(response.headers['Cache-Control']).to include('stale-while-revalidate=300')
|
||||
end
|
||||
|
||||
context 'when API and webhook access is disabled for the account' do
|
||||
it 'returns forbidden for API token authentication' do
|
||||
account_scope = double
|
||||
allow(account_scope).to receive(:find).with(account.id.to_s).and_return(account)
|
||||
allow_any_instance_of(User).to receive(:accounts).and_return(account_scope) # rubocop:disable RSpec/AnyInstance
|
||||
allow(account).to receive(:api_and_webhooks_enabled?).and_return(false)
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/cache_keys",
|
||||
headers: { api_access_token: admin.access_token.token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PATCH /api/v1/accounts/{account.id}' do
|
||||
@@ -324,6 +355,24 @@ RSpec.describe 'Accounts API', type: :request do
|
||||
expect(json_response['message']).to eq('Name is too long (maximum is 255 characters)')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when API and webhook access is disabled for the account' do
|
||||
it 'returns forbidden without modifying the account for API token authentication' do
|
||||
account_scope = double
|
||||
allow(account_scope).to receive(:find).with(account.id.to_s).and_return(account)
|
||||
allow_any_instance_of(User).to receive(:accounts).and_return(account_scope) # rubocop:disable RSpec/AnyInstance
|
||||
allow(account).to receive(:api_and_webhooks_enabled?).and_return(false)
|
||||
|
||||
expect do
|
||||
patch "/api/v1/accounts/#{account.id}",
|
||||
params: { name: 'Updated through API' },
|
||||
headers: { api_access_token: admin.access_token.token },
|
||||
as: :json
|
||||
end.not_to(change { account.reload.name })
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/update_active_at' do
|
||||
@@ -349,5 +398,22 @@ RSpec.describe 'Accounts API', type: :request do
|
||||
expect(agent.account_users.first.active_at).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when API and webhook access is disabled for the account' do
|
||||
it 'returns forbidden without updating active_at for API token authentication' do
|
||||
account_scope = double
|
||||
allow(account_scope).to receive(:find).with(account.id.to_s).and_return(account)
|
||||
allow_any_instance_of(User).to receive(:accounts).and_return(account_scope) # rubocop:disable RSpec/AnyInstance
|
||||
allow(account).to receive(:api_and_webhooks_enabled?).and_return(false)
|
||||
account_user = agent.account_users.first
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/update_active_at",
|
||||
headers: { api_access_token: agent.access_token.token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(account_user.reload.active_at).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -29,6 +29,49 @@ RSpec.describe 'Profile API', type: :request do
|
||||
expect(json_response['custom_attributes']['test']).to eq('test')
|
||||
expect(json_response['message_signature']).to be_nil
|
||||
end
|
||||
|
||||
it 'returns an empty access token when all accounts have API and webhook access disabled' do
|
||||
account.disable_features!('api_and_webhooks')
|
||||
allow(account).to receive(:api_and_webhooks_enabled?).and_return(false)
|
||||
allow_any_instance_of(User).to receive(:accounts).and_return([account]) # rubocop:disable RSpec/AnyInstance
|
||||
|
||||
get '/api/v1/profile',
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['access_token']).to eq('')
|
||||
expect(json_response['accounts'].first['api_and_webhooks']).to be false
|
||||
end
|
||||
|
||||
it 'returns the access token when any account has API and webhook access enabled' do
|
||||
account.disable_features!('api_and_webhooks')
|
||||
enabled_account = create(:account)
|
||||
enabled_account.enable_features!('api_and_webhooks')
|
||||
create(:account_user, account: enabled_account, user: agent)
|
||||
allow(account).to receive(:api_and_webhooks_enabled?).and_return(false)
|
||||
allow(enabled_account).to receive(:api_and_webhooks_enabled?).and_return(true)
|
||||
allow_any_instance_of(User).to receive(:accounts).and_return([account, enabled_account]) # rubocop:disable RSpec/AnyInstance
|
||||
|
||||
get '/api/v1/profile',
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['access_token']).to eq(agent.access_token.token)
|
||||
expect(json_response['accounts'].find { |item| item['id'] == enabled_account.id }['api_and_webhooks']).to be true
|
||||
end
|
||||
|
||||
it 'returns the access token for self-hosted accounts even when the stored feature flag is disabled' do
|
||||
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(false)
|
||||
account.disable_features!('api_and_webhooks')
|
||||
|
||||
get '/api/v1/profile',
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response.parsed_body['access_token']).to eq(agent.access_token.token)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -338,6 +381,21 @@ RSpec.describe 'Profile API', type: :request do
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['access_token']).to eq(agent.access_token.token)
|
||||
end
|
||||
|
||||
it 'regenerates the stored token but returns an empty token when no account has API and webhook access enabled' do
|
||||
account.disable_features!('api_and_webhooks')
|
||||
allow(account).to receive(:api_and_webhooks_enabled?).and_return(false)
|
||||
allow_any_instance_of(User).to receive(:accounts).and_return([account]) # rubocop:disable RSpec/AnyInstance
|
||||
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)
|
||||
expect(agent.reload.access_token.token).not_to eq(old_token)
|
||||
expect(response.parsed_body['access_token']).to eq('')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -285,7 +285,8 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: :activity,
|
||||
content: "Conversation was resolved by #{contact.name}"
|
||||
content: "Conversation was resolved by #{contact.name}",
|
||||
content_attributes: { activity: { type: 'conversation_status_changed', status: 'resolved' } }
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
@@ -202,7 +202,8 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: :activity,
|
||||
content: "Conversation was resolved by #{contact.name}"
|
||||
content: "Conversation was resolved by #{contact.name}",
|
||||
content_attributes: { activity: { type: 'conversation_status_changed', status: 'resolved' } }
|
||||
}
|
||||
)
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
Reference in New Issue
Block a user