Merge feature/cw-7513 into feature/cw-7513-specs

This commit is contained in:
Tanmay Deep Sharma
2026-07-20 11:46:56 +05:30
1175 changed files with 52371 additions and 1946 deletions
@@ -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)