Files
chatwoot/spec/models/conversation_spec.rb
Phuong Nguyen fcb91ab88a fix: Auto resolution flaky spec (#11964)
The test was failing because Current.contact was not being cleared when
testing system auto-resolution. Added Current.contact = nil to ensure
the system auto-resolution message is triggered instead of contact
resolution.

🤖 Generated with [Claude Code](https://claude.ai/code)

# Pull Request Template

## Description

Please include a summary of the change and issue(s) fixed. Also, mention
relevant motivation, context, and any dependencies that this change
requires.
Fixes # (issue)

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.


## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] 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
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2025-09-25 19:36:38 +05:30

953 lines
38 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
require Rails.root.join 'spec/models/concerns/assignment_handler_shared.rb'
require Rails.root.join 'spec/models/concerns/auto_assignment_handler_shared.rb'
RSpec.describe Conversation do
after do
Current.user = nil
Current.account = nil
end
describe 'associations' do
it { is_expected.to belong_to(:account) }
it { is_expected.to belong_to(:inbox) }
it { is_expected.to belong_to(:contact) }
it { is_expected.to belong_to(:contact_inbox) }
it { is_expected.to belong_to(:assignee).optional }
it { is_expected.to belong_to(:team).optional }
it { is_expected.to belong_to(:campaign).optional }
end
describe 'concerns' do
it_behaves_like 'assignment_handler'
it_behaves_like 'auto_assignment_handler'
end
describe '.before_create' do
let(:conversation) { build(:conversation, display_id: nil) }
before do
conversation.save!
conversation.reload
end
it 'runs before_create callbacks' do
expect(conversation.display_id).to eq(1)
end
it 'sets waiting since' do
expect(conversation.waiting_since).not_to be_nil
end
it 'creates a UUID for every conversation automatically' do
uuid_pattern = /[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}$/i
expect(conversation.uuid).to match(uuid_pattern)
end
end
describe '.after_create' do
let(:account) { create(:account) }
let(:agent) { create(:user, email: 'agent1@example.com', account: account) }
let(:inbox) { create(:inbox, account: account) }
let(:conversation) do
create(
:conversation,
account: account,
contact: create(:contact, account: account),
inbox: inbox,
assignee: nil
)
end
before do
allow(Rails.configuration.dispatcher).to receive(:dispatch)
end
it 'runs after_create callbacks' do
# send_events
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
.with(described_class::CONVERSATION_CREATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: false,
changed_attributes: nil, performed_by: nil)
end
end
describe '.validate jsonb attributes' do
let(:account) { create(:account) }
let(:agent) { create(:user, email: 'agent1@example.com', account: account) }
let(:inbox) { create(:inbox, account: account) }
let(:conversation) do
create(
:conversation,
account: account,
contact: create(:contact, account: account),
inbox: inbox,
assignee: nil
)
end
it 'validate length of additional_attributes value' do
conversation.additional_attributes = { company_name: 'some_company' * 200, contact_number: 19_999_999_999 }
conversation.valid?
error_messages = conversation.errors.messages
expect(error_messages[:additional_attributes][0]).to eq('company_name length should be < 1500')
expect(error_messages[:additional_attributes][1]).to eq('contact_number value should be < 9999999999')
end
it 'validate length of custom_attributes value' do
conversation.custom_attributes = { company_name: 'some_company' * 200, contact_number: 19_999_999_999 }
conversation.valid?
error_messages = conversation.errors.messages
expect(error_messages[:custom_attributes][0]).to eq('company_name length should be < 1500')
expect(error_messages[:custom_attributes][1]).to eq('contact_number value should be < 9999999999')
end
end
describe '.after_update' do
let!(:account) { create(:account) }
let!(:old_assignee) do
create(:user, email: 'agent1@example.com', account: account, role: :agent)
end
let(:new_assignee) do
create(:user, email: 'agent2@example.com', account: account, role: :agent)
end
let!(:conversation) do
create(:conversation, status: 'open', account: account, assignee: old_assignee)
end
let(:assignment_mailer) { instance_double(AssignmentMailer, deliver: true) }
let(:label) { create(:label, account: account) }
before do
create(:inbox_member, user: old_assignee, inbox: conversation.inbox)
create(:inbox_member, user: new_assignee, inbox: conversation.inbox)
allow(Rails.configuration.dispatcher).to receive(:dispatch)
Current.user = old_assignee
end
it 'sends conversation updated event if labels are updated' do
conversation.update(label_list: [label.title])
changed_attributes = conversation.previous_changes
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
.with(
described_class::CONVERSATION_UPDATED,
kind_of(Time),
conversation: conversation,
notifiable_assignee_change: false,
changed_attributes: changed_attributes,
performed_by: nil
)
end
it 'runs after_update callbacks' do
conversation.update(
status: :resolved,
contact_last_seen_at: Time.zone.now,
assignee: new_assignee
)
status_change = conversation.status_change
changed_attributes = conversation.previous_changes
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
.with(described_class::CONVERSATION_RESOLVED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true,
changed_attributes: status_change, performed_by: nil)
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
.with(described_class::CONVERSATION_READ, kind_of(Time), conversation: conversation, notifiable_assignee_change: true,
changed_attributes: nil, performed_by: nil)
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
.with(described_class::ASSIGNEE_CHANGED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true,
changed_attributes: changed_attributes, performed_by: nil)
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
.with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true,
changed_attributes: changed_attributes, performed_by: nil)
end
it 'will not run conversation_updated event for empty updates' do
conversation.save!
expect(Rails.configuration.dispatcher).not_to have_received(:dispatch)
.with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
end
it 'will not run conversation_updated event for non whitelisted keys' do
conversation.update(updated_at: DateTime.now.utc)
expect(Rails.configuration.dispatcher).not_to have_received(:dispatch)
.with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
end
it 'will run conversation_updated event for conversation_language in additional_attributes' do
conversation.additional_attributes[:conversation_language] = 'es'
conversation.save!
changed_attributes = conversation.previous_changes
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
.with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: false,
changed_attributes: changed_attributes, performed_by: nil)
end
it 'will not run conversation_updated event for bowser_language in additional_attributes' do
conversation.additional_attributes[:browser_language] = 'es'
conversation.save!
expect(Rails.configuration.dispatcher).not_to have_received(:dispatch)
.with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
end
it 'creates conversation activities' do
conversation.update(
status: :resolved,
contact_last_seen_at: Time.zone.now,
assignee: new_assignee,
label_list: [label.title]
)
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: "#{old_assignee.name} added #{label.title}" }))
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 marked resolved by #{old_assignee.name}" }))
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: "Assigned to #{new_assignee.name} by #{old_assignee.name}" }))
end
it 'adds a message for system auto resolution if marked resolved by system' do
account.update(auto_resolve_after: 40 * 24 * 60)
conversation2 = create(:conversation, status: 'open', account: account, assignee: old_assignee)
Current.reset
message_data = if account.auto_resolve_after >= 1440 && account.auto_resolve_after % 1440 == 0
{ key: 'auto_resolved_days', count: account.auto_resolve_after / 1440 }
elsif account.auto_resolve_after >= 60 && account.auto_resolve_after % 60 == 0
{ key: 'auto_resolved_hours', count: account.auto_resolve_after / 60 }
else
{ key: 'auto_resolved_minutes', count: account.auto_resolve_after }
end
system_resolved_message = "Conversation was marked resolved by system due to #{message_data[:count]} days of inactivity"
expect { conversation2.update(status: :resolved) }
.to have_enqueued_job(Conversations::ActivityMessageJob)
.with(conversation2, { account_id: conversation2.account_id, inbox_id: conversation2.inbox_id, message_type: :activity,
content: system_resolved_message })
end
end
describe '#update_labels' do
let(:account) { create(:account) }
let(:conversation) { create(:conversation, account: account) }
let(:agent) do
create(:user, email: 'agent@example.com', account: account, role: :agent)
end
let(:first_label) { create(:label, account: account) }
let(:second_label) { create(:label, account: account) }
let(:third_label) { create(:label, account: account) }
let(:fourth_label) { create(:label, account: account) }
before do
conversation
Current.user = agent
first_label
second_label
third_label
fourth_label
end
it 'adds one label to conversation' do
labels = [first_label].map(&:title)
expect { conversation.update_labels(labels) }
.to have_enqueued_job(Conversations::ActivityMessageJob)
.with(conversation, { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity,
content: "#{agent.name} added #{labels.join(', ')}" })
expect(conversation.label_list).to match_array(labels)
end
it 'adds and removes previously added labels' do
labels = [first_label, fourth_label].map(&:title)
expect { conversation.update_labels(labels) }
.to have_enqueued_job(Conversations::ActivityMessageJob)
.with(conversation, { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity,
content: "#{agent.name} added #{labels.join(', ')}" })
expect(conversation.label_list).to match_array(labels)
updated_labels = [second_label, third_label].map(&:title)
expect(conversation.update_labels(updated_labels)).to be(true)
expect(conversation.label_list).to match_array(updated_labels)
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: "#{agent.name} added #{updated_labels.join(', ')}" }))
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: "#{agent.name} removed #{labels.join(', ')}" }))
end
end
describe '#toggle_status' do
it 'toggles conversation status to resolved when open' do
conversation = create(:conversation, status: 'open')
expect(conversation.toggle_status).to be(true)
expect(conversation.reload.status).to eq('resolved')
end
it 'toggles conversation status to open when resolved' do
conversation = create(:conversation, status: 'resolved')
expect(conversation.toggle_status).to be(true)
expect(conversation.reload.status).to eq('open')
end
it 'toggles conversation status to open when pending' do
conversation = create(:conversation, status: 'pending')
expect(conversation.toggle_status).to be(true)
expect(conversation.reload.status).to eq('open')
end
it 'toggles conversation status to open when snoozed' do
conversation = create(:conversation, status: 'snoozed')
expect(conversation.toggle_status).to be(true)
expect(conversation.reload.status).to eq('open')
end
end
describe '#toggle_priority' do
it 'defaults priority to nil when created' do
conversation = create(:conversation, status: 'open')
expect(conversation.priority).to be_nil
end
it 'toggles the priority to nil if nothing is passed' do
conversation = create(:conversation, status: 'open', priority: 'high')
expect(conversation.toggle_priority).to be(true)
expect(conversation.reload.priority).to be_nil
end
it 'sets the priority to low' do
conversation = create(:conversation, status: 'open')
expect(conversation.toggle_priority('low')).to be(true)
expect(conversation.reload.priority).to eq('low')
end
it 'sets the priority to medium' do
conversation = create(:conversation, status: 'open')
expect(conversation.toggle_priority('medium')).to be(true)
expect(conversation.reload.priority).to eq('medium')
end
it 'sets the priority to high' do
conversation = create(:conversation, status: 'open')
expect(conversation.toggle_priority('high')).to be(true)
expect(conversation.reload.priority).to eq('high')
end
it 'sets the priority to urgent' do
conversation = create(:conversation, status: 'open')
expect(conversation.toggle_priority('urgent')).to be(true)
expect(conversation.reload.priority).to eq('urgent')
end
end
describe '#ensure_snooze_until_reset' do
it 'resets the snoozed_until when status is toggled' do
conversation = create(:conversation, status: 'snoozed', snoozed_until: 2.days.from_now)
expect(conversation.snoozed_until).not_to be_nil
expect(conversation.toggle_status).to be(true)
expect(conversation.reload.snoozed_until).to be_nil
end
end
describe '#mute!' do
subject(:mute!) { conversation.mute! }
let(:user) do
create(:user, email: 'agent2@example.com', account: create(:account), role: :agent)
end
let(:conversation) { create(:conversation) }
before { Current.user = user }
it 'marks conversation as resolved' do
mute!
expect(conversation.reload.resolved?).to be(true)
end
it 'blocks the contact' do
mute!
expect(conversation.reload.contact.blocked?).to be(true)
end
it 'creates mute message' do
mute!
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: "#{user.name} has muted the conversation" }))
end
end
describe '#unmute!' do
subject(:unmute!) { conversation.unmute! }
let(:user) do
create(:user, email: 'agent2@example.com', account: create(:account), role: :agent)
end
let(:conversation) { create(:conversation).tap(&:mute!) }
before { Current.user = user }
it 'does not change conversation status' do
expect { unmute! }.not_to(change { conversation.reload.status })
end
it 'unblocks the contact' do
unmute!
expect(conversation.reload.contact.blocked?).to be(false)
end
it 'creates unmute message' do
unmute!
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: "#{user.name} has unmuted the conversation" }))
end
end
describe '#muted?' do
subject(:muted?) { conversation.muted? }
let(:conversation) { create(:conversation) }
it 'return true if conversation is muted' do
conversation.mute!
expect(muted?).to be(true)
end
it 'returns false if conversation is not muted' do
expect(muted?).to be(false)
end
end
describe 'unread_messages' do
subject(:unread_messages) { conversation.unread_messages }
let(:conversation) { create(:conversation, agent_last_seen_at: 1.hour.ago) }
let(:message_params) do
{
conversation: conversation,
account: conversation.account,
inbox: conversation.inbox,
sender: conversation.assignee
}
end
let!(:message) do
create(:message, created_at: 1.minute.ago, **message_params)
end
before do
create(:message, created_at: 1.month.ago, **message_params)
end
it 'returns unread messages' do
expect(unread_messages).to include(message)
end
end
describe 'recent_messages' do
subject(:recent_messages) { conversation.recent_messages }
let(:conversation) { create(:conversation, agent_last_seen_at: 1.hour.ago) }
let(:message_params) do
{
conversation: conversation,
account: conversation.account,
inbox: conversation.inbox,
sender: conversation.assignee
}
end
let!(:messages) do
create_list(:message, 10, **message_params) do |message, i|
message.created_at = i.minute.ago
end
end
it 'returns upto 5 recent messages' do
expect(recent_messages.length).to be < 6
expect(recent_messages).to eq messages.last(5)
end
end
describe 'unread_incoming_messages' do
subject(:unread_incoming_messages) { conversation.unread_incoming_messages }
let(:conversation) { create(:conversation, agent_last_seen_at: 1.hour.ago) }
let(:message_params) do
{
conversation: conversation,
account: conversation.account,
inbox: conversation.inbox,
sender: conversation.assignee,
created_at: 1.minute.ago
}
end
let!(:message) do
create(:message, message_type: :incoming, **message_params)
end
before do
create(:message, message_type: :outgoing, **message_params)
end
it 'returns unread incoming messages' do
expect(unread_incoming_messages).to contain_exactly(message)
end
it 'returns unread incoming messages even if the agent has not seen the conversation' do
conversation.update!(agent_last_seen_at: nil)
expect(unread_incoming_messages).to contain_exactly(message)
end
end
describe '#push_event_data' do
subject(:push_event_data) { conversation.push_event_data }
let(:conversation) { create(:conversation) }
let(:expected_data) do
{
additional_attributes: {},
meta: {
sender: conversation.contact.push_event_data,
assignee: conversation.assignee,
team: conversation.team,
hmac_verified: conversation.contact_inbox.hmac_verified
},
id: conversation.display_id,
messages: [],
labels: [],
last_activity_at: conversation.last_activity_at.to_i,
inbox_id: conversation.inbox_id,
status: conversation.status,
contact_inbox: conversation.contact_inbox,
timestamp: conversation.last_activity_at.to_i,
can_reply: true,
channel: 'Channel::WebWidget',
snoozed_until: conversation.snoozed_until,
custom_attributes: conversation.custom_attributes,
first_reply_created_at: nil,
contact_last_seen_at: conversation.contact_last_seen_at.to_i,
agent_last_seen_at: conversation.agent_last_seen_at.to_i,
created_at: conversation.created_at.to_i,
updated_at: conversation.updated_at.to_f,
waiting_since: conversation.waiting_since.to_i,
priority: nil,
unread_count: 0
}
end
it 'returns push event payload' do
expect(push_event_data).to eq(expected_data)
end
end
describe 'when conversation is created by blocked contact' do
let(:account) { create(:account) }
let(:blocked_contact) { create(:contact, account: account, blocked: true) }
let(:inbox) { create(:inbox, account: account) }
it 'creates conversation in resolved state' do
conversation = create(:conversation, account: account, contact: blocked_contact, inbox: inbox)
expect(conversation.status).to eq('resolved')
end
end
describe '#botinbox: when conversation created inside inbox with agent bot' do
let!(:bot_inbox) { create(:agent_bot_inbox) }
let(:conversation) { create(:conversation, inbox: bot_inbox.inbox) }
it 'returns conversation status as pending' do
expect(conversation.status).to eq('pending')
end
it 'returns conversation as open if campaign is present' do
conversation = create(:conversation, inbox: bot_inbox.inbox, campaign: create(:campaign))
expect(conversation.status).to eq('open')
end
end
describe '#botintegration: when conversation created in inbox with dialogflow integration' do
let(:inbox) { create(:inbox) }
let(:hook) { create(:integrations_hook, :dialogflow, inbox: inbox) }
let(:conversation) { create(:conversation, inbox: hook.inbox) }
it 'returns conversation status as pending' do
expect(conversation.status).to eq('pending')
end
end
describe '#delete conversation' do
include ActiveJob::TestHelper
let!(:conversation) { create(:conversation) }
let!(:notification) { create(:notification, notification_type: 'conversation_creation', primary_actor: conversation) }
it 'delete associated notifications if conversation is deleted' do
perform_enqueued_jobs do
conversation.destroy!
end
expect { notification.reload }.to raise_error ActiveRecord::RecordNotFound
end
end
describe 'validate invalid referer url' do
let(:conversation) { create(:conversation, additional_attributes: { referer: 'javascript' }) }
it 'returns nil' do
expect(conversation['additional_attributes']['referer']).to be_nil
end
end
describe 'validate valid referer url' do
let(:conversation) { create(:conversation, additional_attributes: { referer: 'https://www.chatwoot.com/' }) }
it 'returns nil' do
expect(conversation['additional_attributes']['referer']).to eq('https://www.chatwoot.com/')
end
end
describe 'custom sort option' do
include ActiveJob::TestHelper
let!(:conversation_7) { create(:conversation, created_at: DateTime.now - 6.days, last_activity_at: DateTime.now - 13.days) }
let!(:conversation_6) { create(:conversation, created_at: DateTime.now - 7.days, last_activity_at: DateTime.now - 10.days) }
let!(:conversation_5) { create(:conversation, created_at: DateTime.now - 8.days, last_activity_at: DateTime.now - 12.days, priority: :urgent) }
let!(:conversation_4) { create(:conversation, created_at: DateTime.now - 9.days, last_activity_at: DateTime.now - 11.days, priority: :urgent) }
let!(:conversation_3) { create(:conversation, created_at: DateTime.now - 5.days, last_activity_at: DateTime.now - 9.days, priority: :low) }
let!(:conversation_2) { create(:conversation, created_at: DateTime.now - 3.days, last_activity_at: DateTime.now - 6.days, priority: :high) }
let!(:conversation_1) { create(:conversation, created_at: DateTime.now - 4.days, last_activity_at: DateTime.now - 8.days, priority: :medium) }
describe 'sort_on_created_at' do
let(:created_desc_order) do
[
conversation_2.id, conversation_1.id, conversation_3.id, conversation_7.id, conversation_6.id,
conversation_5.id, conversation_4.id
]
end
it 'returns the list in ascending order by default' do
records = described_class.sort_on_created_at
expect(records.map(&:id)).to eq created_desc_order.reverse
end
it 'returns the list in descending order if desc is passed as sort direction' do
records = described_class.sort_on_created_at(:desc)
expect(records.map(&:id)).to eq created_desc_order
end
end
describe 'sort_on_last_activity_at' do
let(:last_activity_asc_order) do
[
conversation_7.id, conversation_5.id, conversation_4.id, conversation_6.id, conversation_3.id,
conversation_1.id, conversation_2.id
]
end
it 'returns the list in descending order by default' do
records = described_class.sort_on_last_activity_at
expect(records.map(&:id)).to eq last_activity_asc_order.reverse
end
it 'returns the list in asc order if asc is passed as sort direction' do
records = described_class.sort_on_last_activity_at(:asc)
expect(records.map(&:id)).to eq last_activity_asc_order
end
end
context 'when last_activity_at updated by some actions' do
before do
create(:message, conversation_id: conversation_1.id, message_type: :incoming, created_at: DateTime.now - 8.days)
create(:message, conversation_id: conversation_2.id, message_type: :incoming, created_at: DateTime.now - 6.days)
create(:message, conversation_id: conversation_3.id, message_type: :incoming, created_at: DateTime.now - 2.days)
end
it 'sort conversations with latest resolved conversation at first' do
records = described_class.sort_on_last_activity_at
expect(records.first.id).to eq(conversation_3.id)
conversation_1.toggle_status
perform_enqueued_jobs do
Conversations::ActivityMessageJob.perform_later(
conversation_1,
account_id: conversation_1.account_id,
inbox_id: conversation_1.inbox_id,
message_type: :activity,
content: 'Conversation was marked resolved by system due to days of inactivity'
)
end
records = described_class.sort_on_last_activity_at
expect(records.first.id).to eq(conversation_1.id)
end
it 'Sort conversations with latest message' do
create(:message, conversation_id: conversation_3.id, message_type: :incoming, created_at: DateTime.now)
records = described_class.sort_on_last_activity_at
expect(records.first.id).to eq(conversation_3.id)
end
end
describe 'sort_on_priority' do
it 'return list with the following order urgent > high > medium > low > nil by default' do
# ensure they are not pre-sorted
records = described_class.sort_on_created_at
expect(records.pluck(:priority)).not_to eq(['urgent', 'urgent', 'high', 'medium', 'low', nil, nil])
records = described_class.sort_on_priority
expect(records.pluck(:priority)).to eq(['urgent', 'urgent', 'high', 'medium', 'low', nil, nil])
expect(records.pluck(:id)).to eq(
[
conversation_4.id, conversation_5.id, conversation_2.id, conversation_1.id, conversation_3.id,
conversation_6.id, conversation_7.id
]
)
end
it 'return list with the following order low > medium > high > urgent > nil by default' do
# ensure they are not pre-sorted
records = described_class.sort_on_created_at
expect(records.pluck(:priority)).not_to eq(['urgent', 'urgent', 'high', 'medium', 'low', nil, nil])
records = described_class.sort_on_priority(:asc)
expect(records.pluck(:priority)).to eq(['low', 'medium', 'high', 'urgent', 'urgent', nil, nil])
expect(records.pluck(:id)).to eq(
[
conversation_3.id, conversation_1.id, conversation_2.id, conversation_4.id, conversation_5.id,
conversation_6.id, conversation_7.id
]
)
end
it 'sorts conversation with last_activity for the same priority' do
records = described_class.where(priority: 'urgent').sort_on_priority
# ensure that the conversation 4 last_activity_at is more recent than conversation 5
expect(conversation_4.last_activity_at > conversation_5.last_activity_at).to be(true)
expect(records.pluck(:priority, :id)).to eq([['urgent', conversation_4.id], ['urgent', conversation_5.id]])
records = described_class.where(priority: nil).sort_on_priority
# ensure that the conversation 6 last_activity_at is more recent than conversation 7
expect(conversation_6.last_activity_at > conversation_7.last_activity_at).to be(true)
expect(records.pluck(:priority, :id)).to eq([[nil, conversation_6.id], [nil, conversation_7.id]])
end
end
describe 'sort_on_waiting_since' do
it 'returns the list in ascending order by default' do
records = described_class.sort_on_waiting_since
expect(records.map(&:id)).to eq [
conversation_4.id, conversation_5.id, conversation_6.id, conversation_7.id, conversation_3.id, conversation_1.id,
conversation_2.id
]
end
it 'returns the list in desc order if asc is passed as sort direction' do
records = described_class.sort_on_waiting_since(:desc)
expect(records.map(&:id)).to eq [
conversation_2.id, conversation_1.id, conversation_3.id, conversation_7.id, conversation_6.id, conversation_5.id,
conversation_4.id
]
end
end
end
describe 'cached_label_list_array' do
let(:conversation) { create(:conversation) }
it 'returns the correct list of labels' do
conversation.update(label_list: %w[customer-support enterprise paid-customer])
expect(conversation.cached_label_list_array).to eq %w[customer-support enterprise paid-customer]
end
end
describe '#last_activity_at' do
let(:conversation) { create(:conversation) }
let(:message_params) do
{
conversation: conversation,
account: conversation.account,
inbox: conversation.inbox,
sender: conversation.assignee
}
end
context 'when a new conversation is created' do
it 'sets last_activity_at to the created_at time (within DB precision)' do
expect(conversation.last_activity_at).to be_within(1.second).of(conversation.created_at)
end
end
context 'when a new message is added' do
it 'updates the last_activity_at to the new message\'s created_at time' do
message = create(:message, created_at: 1.hour.from_now, **message_params)
conversation.reload
expect(conversation.last_activity_at).to be_within(1.second).of(message.created_at)
end
end
context 'when multiple messages are added' do
it 'sets last_activity_at to the most recent message\'s created_at time' do
create(:message, created_at: 2.hours.ago, **message_params)
latest_message = create(:message, created_at: 1.hour.from_now, **message_params)
conversation.reload
expect(conversation.last_activity_at).to be_within(1.second).of(latest_message.created_at)
end
end
end
describe '#can_reply?' do
let(:conversation) { create(:conversation) }
let(:message_window_service) { instance_double(Conversations::MessageWindowService) }
before do
allow(Conversations::MessageWindowService).to receive(:new).with(conversation).and_return(message_window_service)
end
it 'delegates to MessageWindowService' do
allow(message_window_service).to receive(:can_reply?).and_return(true)
expect(conversation.can_reply?).to be true
expect(message_window_service).to have_received(:can_reply?)
end
it 'returns false when MessageWindowService returns false' do
allow(message_window_service).to receive(:can_reply?).and_return(false)
expect(conversation.can_reply?).to be false
expect(message_window_service).to have_received(:can_reply?)
end
end
describe 'reply time calculation flows' do
include ActiveJob::TestHelper
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:contact) { create(:contact, account: account) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:conversation) { create(:conversation, account: account, inbox: inbox, contact: contact, assignee: agent, waiting_since: nil) }
let(:conversation_start_time) { 5.hours.ago }
before do
create(:inbox_member, user: agent, inbox: inbox)
# rubocop:disable Rails/SkipsModelValidations
conversation.update_column(:waiting_since, nil)
conversation.update_column(:created_at, conversation_start_time)
# rubocop:enable Rails/SkipsModelValidations
conversation.messages.destroy_all
conversation.reporting_events.destroy_all
conversation.reload
end
def create_customer_message(conversation, created_at: Time.current)
message = nil
perform_enqueued_jobs do
message = create(:message,
message_type: 'incoming',
account: conversation.account,
inbox: conversation.inbox,
conversation: conversation,
sender: conversation.contact,
created_at: created_at)
end
message
end
def create_agent_message(conversation, created_at: Time.current)
message = nil
perform_enqueued_jobs do
message = create(:message,
message_type: 'outgoing',
account: conversation.account,
inbox: conversation.inbox,
conversation: conversation,
sender: conversation.assignee,
created_at: created_at)
end
message
end
it 'correctly tracks waiting_since and creates first response time events' do
create_customer_message(conversation, created_at: conversation_start_time)
conversation.reload
expect(conversation.waiting_since).to be_within(1.second).of(conversation_start_time)
# Agent replies - this should create first response event
agent_reply1_time = 4.hours.ago
create_agent_message(conversation, created_at: agent_reply1_time)
first_response_events = account.reporting_events.where(name: 'first_response', conversation_id: conversation.id)
expect(first_response_events.count).to eq(1)
expect(first_response_events.first.value).to be_within(1.second).of(1.hour)
# the first response should also clear the waiting_since
conversation.reload
expect(conversation.waiting_since).to be_nil
end
it 'does not reset waiting_since if customer sends another message' do
create_customer_message(conversation, created_at: conversation_start_time)
conversation.reload
expect(conversation.waiting_since).to be_within(1.second).of(conversation_start_time)
create_customer_message(conversation, created_at: 3.hours.ago)
conversation.reload
expect(conversation.waiting_since).to be_within(1.second).of(conversation_start_time)
end
it 'records the correct reply_time for subsequent messages' do
create_customer_message(conversation, created_at: conversation_start_time)
create_agent_message(conversation, created_at: 4.hours.ago)
create_customer_message(conversation, created_at: 3.hours.ago)
create_agent_message(conversation, created_at: 2.hours.ago)
reply_events = account.reporting_events.where(name: 'reply_time', conversation_id: conversation.id)
expect(reply_events.count).to eq(1)
expect(reply_events.first.value).to be_within(1.second).of(1.hour)
conversation.reload
expect(conversation.waiting_since).to be_nil
end
it 'records zero reply time if an agent sends a message after resolution' do
create_customer_message(conversation, created_at: conversation_start_time)
create_agent_message(conversation, created_at: 4.hours.ago)
create_customer_message(conversation, created_at: 3.hours.ago)
conversation.toggle_status
expect(conversation.status).to eq('resolved')
conversation.toggle_status
expect(conversation.status).to eq('open')
conversation.reload
expect(conversation.waiting_since).to be_nil
create_agent_message(conversation, created_at: 1.hour.ago)
# update_waiting_since will ensure that no events were created since the waiting_since was nil
# if the event is created it should log zero value, we have handled that in the reporting_event_listener
reply_events = account.reporting_events.where(name: 'reply_time', conversation_id: conversation.id)
expect(reply_events.count).to eq(0)
end
end
end