Files
chatwoot/spec/enterprise/controllers/api/v1/accounts/conversations_controller_spec.rb
7bf76057c2 fix: SLA handling for blocked contacts (#14861)
# Pull Request Template

## Description

Blocked contacts are now excluded from SLA assignment, processing,
reports, and conversation SLA UI while they remain blocked. Existing SLA
records are preserved, and SLA behavior resumes if the contact is
unblocked.

Fixes
https://linear.app/chatwoot/issue/CW-7435/sla-should-not-trigger-for-blocked-contacts

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

- `bundle exec rspec spec/enterprise/models/conversation_spec.rb
spec/enterprise/models/applied_sla_spec.rb
spec/enterprise/services/enterprise/action_service_spec.rb
spec/enterprise/services/sla/evaluate_applied_sla_service_spec.rb
spec/enterprise/jobs/sla/process_account_applied_slas_job_spec.rb
spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb
spec/enterprise/controllers/api/v1/accounts/conversations_controller_spec.rb
spec/enterprise/controllers/enterprise/api/v1/accounts/conversations_controller_spec.rb
spec/enterprise/presenters/conversations/event_data_presenter_spec.rb` —
78 examples, 0 failures
- `bundle exec rubocop
enterprise/app/controllers/api/v1/accounts/applied_slas_controller.rb
enterprise/app/jobs/sla/process_account_applied_slas_job.rb
enterprise/app/models/applied_sla.rb
enterprise/app/models/enterprise/concerns/conversation.rb
enterprise/app/presenters/enterprise/conversations/event_data_presenter.rb
enterprise/app/services/enterprise/action_service.rb
enterprise/app/services/sla/evaluate_applied_sla_service.rb
lib/tasks/apply_sla.rake
spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb
spec/enterprise/controllers/api/v1/accounts/conversations_controller_spec.rb
spec/enterprise/controllers/enterprise/api/v1/accounts/conversations_controller_spec.rb
spec/enterprise/jobs/sla/process_account_applied_slas_job_spec.rb
spec/enterprise/models/applied_sla_spec.rb
spec/enterprise/models/conversation_spec.rb
spec/enterprise/presenters/conversations/event_data_presenter_spec.rb
spec/enterprise/services/enterprise/action_service_spec.rb
spec/enterprise/services/sla/evaluate_applied_sla_service_spec.rb` — no
offenses
- `pnpm exec vitest --no-watch --no-cache --no-coverage
app/javascript/dashboard/components/widgets/conversation/specs/ConversationCard.spec.js`
— 2 tests passed
- `pnpm exec eslint
app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreviewWithMeta.vue
app/javascript/dashboard/components-next/Conversation/ConversationCard/ConversationCardExpanded.vue
app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue
app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue
app/javascript/dashboard/components/widgets/conversation/specs/ConversationCard.spec.js`
— passed with existing raw-text warnings in `ConversationHeader.vue`
- `git diff --cached --check` — clean

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-07-02 14:16:29 +05:30

263 lines
11 KiB
Ruby

require 'rails_helper'
RSpec.describe 'Conversations API', type: :request do
let(:account) { create(:account) }
let(:administrator) { create(:user, account: account, role: :administrator) }
describe 'GET /api/v1/accounts/{account.id}/conversations/:id' do
it 'returns SLA data for the conversation if the feature is enabled' do
account.enable_features!('sla')
conversation = create(:conversation, account: account)
applied_sla = create(:applied_sla, conversation: conversation)
sla_event = create(:sla_event, conversation: conversation, applied_sla: applied_sla)
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}", headers: administrator.create_new_auth_token
expect(response).to have_http_status(:ok)
expect(response.parsed_body['applied_sla']['id']).to eq(applied_sla.id)
expect(response.parsed_body['sla_events'].first['id']).to eq(sla_event.id)
end
it 'returns cleared SLA data when the contact is blocked' do
account.enable_features!('sla')
conversation = create(:conversation, account: account)
applied_sla = create(:applied_sla, conversation: conversation)
create(:sla_event, conversation: conversation, applied_sla: applied_sla)
conversation.contact.update!(blocked: true)
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}", headers: administrator.create_new_auth_token
expect(response).to have_http_status(:ok)
expect(response.parsed_body['sla_policy_id']).to be_nil
expect(response.parsed_body['applied_sla']).to be_nil
expect(response.parsed_body['sla_events']).to eq([])
end
it 'does not return SLA data for the conversation if the feature is disabled' do
account.disable_features!('sla')
conversation = create(:conversation, account: account)
create(:applied_sla, conversation: conversation)
create(:sla_event, conversation: conversation)
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}", headers: administrator.create_new_auth_token
expect(response).to have_http_status(:ok)
expect(response.parsed_body.keys).not_to include('applied_sla')
expect(response.parsed_body.keys).not_to include('sla_events')
end
context 'when agent has team access' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:team) { create(:team, account: account) }
let(:conversation) { create(:conversation, account: account, team: team) }
before do
create(:team_member, team: team, user: agent)
end
it 'allows accessing the conversation via team membership' do
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}", headers: agent.create_new_auth_token
expect(response).to have_http_status(:ok)
expect(response.parsed_body['id']).to eq(conversation.display_id)
end
end
context 'when agent has a custom role' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:conversation) { create(:conversation, account: account) }
before do
create(:inbox_member, user: agent, inbox: conversation.inbox)
end
it 'returns unauthorized for unassigned conversation without permission' do
custom_role = create(:custom_role, account: account, permissions: ['conversation_participating_manage'])
account.account_users.find_by(user_id: agent.id).update!(custom_role: custom_role)
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}", headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
it 'returns the conversation when permission allows managing unassigned conversations, including when assigned to agent' do
custom_role = create(:custom_role, account: account, permissions: ['conversation_unassigned_manage'])
account_user = account.account_users.find_by(user_id: agent.id)
account_user.update!(custom_role: custom_role)
conversation.update!(assignee: agent)
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}", headers: agent.create_new_auth_token
expect(response).to have_http_status(:ok)
expect(response.parsed_body['id']).to eq(conversation.display_id)
end
it 'returns the conversation when permission allows managing assigned conversations' do
custom_role = create(:custom_role, account: account, permissions: ['conversation_participating_manage'])
account_user = account.account_users.find_by(user_id: agent.id)
account_user.update!(custom_role: custom_role)
conversation.update!(assignee: agent)
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}", headers: agent.create_new_auth_token
expect(response).to have_http_status(:ok)
expect(response.parsed_body['id']).to eq(conversation.display_id)
end
it 'returns the conversation when permission allows managing participating conversations' do
custom_role = create(:custom_role, account: account, permissions: ['conversation_participating_manage'])
account_user = account.account_users.find_by(user_id: agent.id)
account_user.update!(custom_role: custom_role)
create(:conversation_participant, conversation: conversation, account: account, user: agent)
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}", headers: agent.create_new_auth_token
expect(response).to have_http_status(:ok)
expect(response.parsed_body['id']).to eq(conversation.display_id)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/conversations/:id/reporting_events' do
let(:conversation) { create(:conversation, account: account) }
let(:inbox) { conversation.inbox }
let(:agent) { administrator }
before do
# Create reporting events for this conversation
@event1 = create(:reporting_event,
account: account,
conversation: conversation,
inbox: inbox,
user: agent,
name: 'first_response',
value: 120,
created_at: 3.hours.ago)
@event2 = create(:reporting_event,
account: account,
conversation: conversation,
inbox: inbox,
user: agent,
name: 'reply_time',
value: 45,
created_at: 2.hours.ago)
@event3 = create(:reporting_event,
account: account,
conversation: conversation,
inbox: inbox,
user: agent,
name: 'resolution',
value: 300,
created_at: 1.hour.ago)
# Create an event for a different conversation (should not be included)
other_conversation = create(:conversation, account: account)
create(:reporting_event,
account: account,
conversation: other_conversation,
inbox: other_conversation.inbox,
user: agent,
name: 'other_conversation_event',
value: 60)
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/reporting_events",
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user with conversation access' do
it 'returns all reporting events for the conversation' do
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/reporting_events",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
# Should return array directly (no pagination)
expect(json_response).to be_an(Array)
expect(json_response.size).to eq(3)
# Check they are sorted by created_at asc (oldest first)
expect(json_response.first['name']).to eq('first_response')
expect(json_response.last['name']).to eq('resolution')
# Verify it doesn't include events from other conversations
event_names = json_response.map { |e| e['name'] }
expect(event_names).not_to include('other_conversation_event')
end
it 'returns empty array when conversation has no reporting events' do
conversation_without_events = create(:conversation, account: account)
get "/api/v1/accounts/#{account.id}/conversations/#{conversation_without_events.display_id}/reporting_events",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response).to be_an(Array)
expect(json_response).to be_empty
end
end
context 'when agent has limited access' do
let(:limited_agent) { create(:user, account: account, role: :agent) }
it 'returns unauthorized for unassigned conversation without permission' do
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/reporting_events",
headers: limited_agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'returns reporting events when agent is assigned to the conversation' do
conversation.update!(assignee: limited_agent)
# Also create inbox member for the agent
create(:inbox_member, user: limited_agent, inbox: conversation.inbox)
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/reporting_events",
headers: limited_agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response).to be_an(Array)
expect(json_response.size).to eq(3)
end
end
context 'when agent has team access' do
let(:team_agent) { create(:user, account: account, role: :agent) }
let(:team) { create(:team, account: account) }
before do
create(:team_member, team: team, user: team_agent)
conversation.update!(team: team)
end
it 'allows accessing conversation reporting events via team membership' do
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/reporting_events",
headers: team_agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response).to be_an(Array)
expect(json_response.size).to eq(3)
end
end
end
end