791d90c6b7
At present, the websocket pubsub tokens are present at the contact objects in chatwoot. A better approach would be to have these tokens at the contact_inbox object instead. This helps chatwoot to deliver the websocket events targetted to the specific widget connection, stop contact events from leaking into other chat sessions from the same contact. Fixes #1682 Fixes #1664 Co-authored-by: Pranav Raj S <pranav@chatwoot.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
475 lines
17 KiB
Ruby
475 lines
17 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/round_robin_handler_shared.rb'
|
|
|
|
RSpec.describe Conversation, type: :model do
|
|
describe 'associations' do
|
|
it { is_expected.to belong_to(:account) }
|
|
it { is_expected.to belong_to(:inbox) }
|
|
end
|
|
|
|
describe 'concerns' do
|
|
it_behaves_like 'assignment_handler'
|
|
it_behaves_like 'round_robin_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 '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)
|
|
end
|
|
|
|
it 'queues AutoResolveConversationsJob post creation if auto resolve duration present' do
|
|
account.update(auto_resolve_duration: 30)
|
|
expect do
|
|
create(
|
|
:conversation,
|
|
account: account,
|
|
contact: create(:contact, account: account),
|
|
inbox: inbox,
|
|
assignee: nil
|
|
)
|
|
end.to have_enqueued_job(AutoResolveConversationsJob)
|
|
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) { double(deliver: true) }
|
|
let(:label) { create(:label, account: account) }
|
|
|
|
before do
|
|
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
|
Current.user = old_assignee
|
|
end
|
|
|
|
it 'runs after_update callbacks' do
|
|
conversation.update(
|
|
status: :resolved,
|
|
contact_last_seen_at: Time.now,
|
|
assignee: new_assignee,
|
|
label_list: [label.title]
|
|
)
|
|
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
|
.with(described_class::CONVERSATION_RESOLVED, kind_of(Time), conversation: conversation)
|
|
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
|
.with(described_class::CONVERSATION_READ, kind_of(Time), conversation: conversation)
|
|
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
|
.with(described_class::ASSIGNEE_CHANGED, kind_of(Time), conversation: conversation)
|
|
end
|
|
|
|
it 'creates conversation activities' do
|
|
conversation.update(
|
|
status: :resolved,
|
|
contact_last_seen_at: Time.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_duration: 40)
|
|
conversation2 = create(:conversation, status: 'open', account: account, assignee: old_assignee)
|
|
Current.user = nil
|
|
|
|
system_resolved_message = "Conversation was marked resolved by system due to #{account.auto_resolve_duration} 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
|
|
|
|
it 'does not trigger AutoResolutionJob if conversation reopened and account does not have auto resolve duration' do
|
|
expect { conversation.update(status: :open) }
|
|
.not_to have_enqueued_job(AutoResolveConversationsJob).with(conversation.id)
|
|
end
|
|
|
|
it 'does trigger AutoResolutionJob if conversation reopened and account has auto resolve duration' do
|
|
account.update(auto_resolve_duration: 40)
|
|
conversation.resolved!
|
|
conversation.reload.update(status: :open)
|
|
expect(AutoResolveConversationsJob).to have_been_enqueued.with(conversation.id)
|
|
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 eq(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 eq(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 eq(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 eq(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 eq(true)
|
|
expect(conversation.reload.status).to eq('open')
|
|
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 eq nil
|
|
expect(conversation.toggle_status).to eq(true)
|
|
expect(conversation.reload.snoozed_until).to eq(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 eq(true)
|
|
end
|
|
|
|
it 'marks conversation as muted in redis' do
|
|
mute!
|
|
expect(Redis::Alfred.get(conversation.send(:mute_key))).not_to eq(nil)
|
|
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 'marks conversation as muted in redis' do
|
|
expect { unmute! }
|
|
.to change { Redis::Alfred.get(conversation.send(:mute_key)) }
|
|
.to nil
|
|
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 eq(true)
|
|
end
|
|
|
|
it 'returns false if conversation is not muted' do
|
|
expect(muted?).to eq(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 '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
|
|
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,
|
|
hmac_verified: conversation.contact_inbox.hmac_verified
|
|
},
|
|
id: conversation.display_id,
|
|
messages: [],
|
|
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,
|
|
contact_last_seen_at: conversation.contact_last_seen_at.to_i,
|
|
agent_last_seen_at: conversation.agent_last_seen_at.to_i,
|
|
unread_count: 0
|
|
}
|
|
end
|
|
|
|
it 'returns push event payload' do
|
|
expect(push_event_data).to eq(expected_data)
|
|
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
|
|
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 '#can_reply?' do
|
|
describe 'on channels without 24 hour restriction' do
|
|
let(:conversation) { create(:conversation) }
|
|
|
|
it 'returns true' do
|
|
expect(conversation.can_reply?).to eq true
|
|
end
|
|
end
|
|
|
|
describe 'on channels with 24 hour restriction' do
|
|
before do
|
|
stub_request(:post, /graph.facebook.com/)
|
|
end
|
|
|
|
let!(:facebook_channel) { create(:channel_facebook_page) }
|
|
let!(:facebook_inbox) { create(:inbox, channel: facebook_channel, account: facebook_channel.account) }
|
|
let!(:conversation) { create(:conversation, inbox: facebook_inbox, account: facebook_channel.account) }
|
|
|
|
it 'returns false if there are no incoming messages' do
|
|
expect(conversation.can_reply?).to eq false
|
|
end
|
|
|
|
it 'return false if last incoming message is outside of 24 hour window' do
|
|
create(
|
|
:message,
|
|
account: conversation.account,
|
|
inbox: facebook_inbox,
|
|
conversation: conversation,
|
|
created_at: Time.now - 25.hours
|
|
)
|
|
expect(conversation.can_reply?).to eq false
|
|
end
|
|
|
|
it 'return true if last incoming message is inside 24 hour window' do
|
|
create(
|
|
:message,
|
|
account: conversation.account,
|
|
inbox: facebook_inbox,
|
|
conversation: conversation
|
|
)
|
|
expect(conversation.can_reply?).to eq true
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#delete conversation' do
|
|
let!(:conversation) { create(:conversation) }
|
|
|
|
let!(:notification) { create(:notification, notification_type: 'conversation_creation', primary_actor: conversation) }
|
|
|
|
it 'delete associated notifications if conversation is deleted' do
|
|
conversation.destroy
|
|
expect { notification.reload }.to raise_error ActiveRecord::RecordNotFound
|
|
end
|
|
end
|
|
end
|