Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
233
research/chatwoot/spec/finders/conversation_finder_spec.rb
Normal file
233
research/chatwoot/spec/finders/conversation_finder_spec.rb
Normal file
@@ -0,0 +1,233 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ConversationFinder do
|
||||
subject(:conversation_finder) { described_class.new(user_1, params) }
|
||||
|
||||
let!(:account) { create(:account) }
|
||||
let!(:user_1) { create(:user, account: account) }
|
||||
let!(:user_2) { create(:user, account: account) }
|
||||
let!(:admin) { create(:user, account: account, role: :administrator) }
|
||||
let!(:inbox) { create(:inbox, account: account, enable_auto_assignment: false) }
|
||||
let!(:contact_inbox) { create(:contact_inbox, inbox: inbox, source_id: 'testing_source_id') }
|
||||
let!(:restricted_inbox) { create(:inbox, account: account) }
|
||||
|
||||
before do
|
||||
create(:inbox_member, user: user_1, inbox: inbox)
|
||||
create(:inbox_member, user: user_2, inbox: inbox)
|
||||
create(:conversation, account: account, inbox: inbox, assignee: user_1)
|
||||
create(:conversation, account: account, inbox: inbox, assignee: user_1)
|
||||
create(:conversation, account: account, inbox: inbox, assignee: user_1, status: 'resolved')
|
||||
create(:conversation, account: account, inbox: inbox, assignee: user_2, contact_inbox: contact_inbox)
|
||||
# unassigned conversation
|
||||
create(:conversation, account: account, inbox: inbox)
|
||||
Current.account = account
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'with status' do
|
||||
let(:params) { { status: 'open', assignee_type: 'me' } }
|
||||
|
||||
it 'filter conversations by status' do
|
||||
result = conversation_finder.perform
|
||||
expect(result[:conversations].length).to be 2
|
||||
end
|
||||
end
|
||||
|
||||
context 'with inbox' do
|
||||
let!(:restricted_conversation) { create(:conversation, account: account, inbox_id: restricted_inbox.id) }
|
||||
|
||||
it 'returns conversation from any inbox if its admin' do
|
||||
params = { inbox_id: restricted_inbox.id }
|
||||
result = described_class.new(admin, params).perform
|
||||
|
||||
expect(result[:conversations].map(&:id)).to include(restricted_conversation.id)
|
||||
end
|
||||
|
||||
it 'returns conversation from inbox if agent is its member' do
|
||||
params = { inbox_id: restricted_inbox.id }
|
||||
create(:inbox_member, user: user_1, inbox: restricted_inbox)
|
||||
result = described_class.new(user_1, params).perform
|
||||
|
||||
expect(result[:conversations].map(&:id)).to include(restricted_conversation.id)
|
||||
end
|
||||
|
||||
it 'does not return conversations from inboxes where agent is not a member' do
|
||||
params = { inbox_id: restricted_inbox.id }
|
||||
result = described_class.new(user_1, params).perform
|
||||
|
||||
expect(result[:conversations].map(&:id)).not_to include(restricted_conversation.id)
|
||||
end
|
||||
|
||||
it 'returns only the conversations from the inbox if inbox_id filter is passed' do
|
||||
conversation = create(:conversation, account: account, inbox_id: inbox.id)
|
||||
params = { inbox_id: restricted_inbox.id }
|
||||
result = described_class.new(admin, params).perform
|
||||
|
||||
conversation_ids = result[:conversations].map(&:id)
|
||||
expect(conversation_ids).not_to include(conversation.id)
|
||||
expect(conversation_ids).to include(restricted_conversation.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with assignee_type all' do
|
||||
let(:params) { { assignee_type: 'all' } }
|
||||
|
||||
it 'filter conversations by assignee type all' do
|
||||
result = conversation_finder.perform
|
||||
expect(result[:conversations].length).to be 4
|
||||
end
|
||||
end
|
||||
|
||||
context 'with assignee_type unassigned' do
|
||||
let(:params) { { assignee_type: 'unassigned' } }
|
||||
|
||||
it 'filter conversations by assignee type unassigned' do
|
||||
result = conversation_finder.perform
|
||||
expect(result[:conversations].length).to be 1
|
||||
end
|
||||
end
|
||||
|
||||
context 'with status all' do
|
||||
let(:params) { { status: 'all' } }
|
||||
|
||||
it 'returns all conversations' do
|
||||
result = conversation_finder.perform
|
||||
expect(result[:conversations].length).to be 5
|
||||
end
|
||||
end
|
||||
|
||||
context 'with assignee_type assigned' do
|
||||
let(:params) { { assignee_type: 'assigned' } }
|
||||
|
||||
it 'filter conversations by assignee type assigned' do
|
||||
result = conversation_finder.perform
|
||||
expect(result[:conversations].length).to be 3
|
||||
end
|
||||
|
||||
it 'returns the correct meta' do
|
||||
result = conversation_finder.perform
|
||||
expect(result[:count]).to eq({
|
||||
mine_count: 2,
|
||||
assigned_count: 3,
|
||||
unassigned_count: 1,
|
||||
all_count: 4
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'with team' do
|
||||
let(:team) { create(:team, account: account) }
|
||||
let(:params) { { team_id: team.id } }
|
||||
|
||||
it 'filter conversations by team' do
|
||||
create(:conversation, account: account, inbox: inbox, team: team)
|
||||
result = conversation_finder.perform
|
||||
expect(result[:conversations].length).to be 1
|
||||
end
|
||||
end
|
||||
|
||||
context 'with labels' do
|
||||
let(:params) { { labels: ['resolved'] } }
|
||||
|
||||
it 'filter conversations by labels' do
|
||||
conversation = inbox.conversations.first
|
||||
conversation.update_labels('resolved')
|
||||
|
||||
result = conversation_finder.perform
|
||||
expect(result[:conversations].length).to be 1
|
||||
end
|
||||
end
|
||||
|
||||
context 'with source_id' do
|
||||
let(:params) { { source_id: 'testing_source_id' } }
|
||||
|
||||
it 'filter conversations by source id' do
|
||||
result = conversation_finder.perform
|
||||
expect(result[:conversations].length).to be 1
|
||||
end
|
||||
end
|
||||
|
||||
context 'without source' do
|
||||
let(:params) { {} }
|
||||
|
||||
it 'returns conversations with any source' do
|
||||
result = conversation_finder.perform
|
||||
expect(result[:conversations].length).to be 4
|
||||
end
|
||||
end
|
||||
|
||||
context 'with updated_within' do
|
||||
let(:params) { { updated_within: 20, assignee_type: 'unassigned', sort_by: 'created_at_asc' } }
|
||||
|
||||
it 'filters based on params, sort order but returns all conversations without pagination with in time range' do
|
||||
# value of updated_within is in seconds
|
||||
# write spec based on that
|
||||
conversations = create_list(:conversation, 50, account: account,
|
||||
inbox: inbox, assignee: nil,
|
||||
updated_at: Time.now.utc - 30.seconds,
|
||||
created_at: Time.now.utc - 30.seconds)
|
||||
# update updated_at of 27 conversations to be with in 20 seconds
|
||||
conversations[0..27].each do |conversation|
|
||||
conversation.update(updated_at: Time.now.utc - 10.seconds)
|
||||
end
|
||||
result = conversation_finder.perform
|
||||
# pagination is not applied
|
||||
# filters are applied
|
||||
# modified conversations + 1 conversation created during set up
|
||||
expect(result[:conversations].length).to be 29
|
||||
# ensure that the conversations are sorted by created_at
|
||||
expect(result[:conversations].first.created_at).to be < result[:conversations].last.created_at
|
||||
end
|
||||
end
|
||||
|
||||
context 'with pagination' do
|
||||
let(:params) { { status: 'open', assignee_type: 'me', page: 1 } }
|
||||
|
||||
it 'returns paginated conversations' do
|
||||
create_list(:conversation, 50, account: account, inbox: inbox, assignee: user_1)
|
||||
result = conversation_finder.perform
|
||||
expect(result[:conversations].length).to be 25
|
||||
end
|
||||
end
|
||||
|
||||
context 'with perform_meta_only' do
|
||||
let(:params) { { assignee_type: 'assigned' } }
|
||||
|
||||
it 'returns only count without conversations' do
|
||||
result = conversation_finder.perform_meta_only
|
||||
expect(result).to have_key(:count)
|
||||
expect(result).not_to have_key(:conversations)
|
||||
end
|
||||
|
||||
it 'returns the correct counts' do
|
||||
result = conversation_finder.perform_meta_only
|
||||
expect(result[:count]).to eq({
|
||||
mine_count: 2,
|
||||
assigned_count: 3,
|
||||
unassigned_count: 1,
|
||||
all_count: 4
|
||||
})
|
||||
end
|
||||
|
||||
it 'returns same counts as perform' do
|
||||
meta_result = conversation_finder.perform_meta_only
|
||||
full_result = conversation_finder.perform
|
||||
expect(meta_result[:count]).to eq(full_result[:count])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unattended' do
|
||||
let(:params) { { status: 'open', assignee_type: 'me', conversation_type: 'unattended' } }
|
||||
|
||||
it 'returns unattended conversations' do
|
||||
create(:conversation, account: account, first_reply_created_at: Time.now.utc, assignee: user_1) # attended_conversation
|
||||
create(:conversation, account: account, first_reply_created_at: nil, assignee: user_1) # unattended_conversation_no_first_reply
|
||||
create(:conversation, account: account, first_reply_created_at: Time.now.utc,
|
||||
assignee: user_1, waiting_since: Time.now.utc) # unattended_conversation_waiting_since
|
||||
|
||||
result = conversation_finder.perform
|
||||
expect(result[:conversations].length).to be 2
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
143
research/chatwoot/spec/finders/email_channel_finder_spec.rb
Normal file
143
research/chatwoot/spec/finders/email_channel_finder_spec.rb
Normal file
@@ -0,0 +1,143 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe EmailChannelFinder do
|
||||
include ActionMailbox::TestHelper
|
||||
|
||||
let!(:channel_email) { create(:channel_email) }
|
||||
|
||||
describe '#perform' do
|
||||
context 'with cc mail' do
|
||||
let(:reply_cc_mail) { create_inbound_email_from_fixture('reply_cc.eml') }
|
||||
|
||||
it 'return channel with cc email' do
|
||||
channel_email.update(email: 'test@example.com')
|
||||
channel = described_class.new(reply_cc_mail.mail).perform
|
||||
expect(channel).to eq(channel_email)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with to mail' do
|
||||
let(:reply_mail) { create_inbound_email_from_fixture('reply.eml') }
|
||||
|
||||
it 'return channel with to email' do
|
||||
channel_email.update(email: 'test@example.com')
|
||||
reply_mail.mail['to'] = 'test@example.com'
|
||||
channel = described_class.new(reply_mail.mail).perform
|
||||
expect(channel).to eq(channel_email)
|
||||
end
|
||||
|
||||
it 'return channel with to+extension email' do
|
||||
channel_email.update(email: 'test@example.com')
|
||||
reply_mail.mail['to'] = 'test+123@example.com'
|
||||
channel = described_class.new(reply_mail.mail).perform
|
||||
expect(channel).to eq(channel_email)
|
||||
end
|
||||
|
||||
it 'return channel with cc email' do
|
||||
channel_email.update(email: 'test@example.com')
|
||||
reply_mail.mail['to'] = nil
|
||||
reply_mail.mail['cc'] = 'test@example.com'
|
||||
channel = described_class.new(reply_mail.mail).perform
|
||||
expect(channel).to eq(channel_email)
|
||||
end
|
||||
|
||||
it 'return channel with bcc email' do
|
||||
channel_email.update(email: 'test@example.com')
|
||||
reply_mail.mail['to'] = nil
|
||||
reply_mail.mail['bcc'] = 'test@example.com'
|
||||
channel = described_class.new(reply_mail.mail).perform
|
||||
expect(channel).to eq(channel_email)
|
||||
end
|
||||
|
||||
it 'skip bcc email when account is configured to skip BCC processing' do
|
||||
channel_email.update(email: 'test@example.com')
|
||||
reply_mail.mail['to'] = nil
|
||||
reply_mail.mail['bcc'] = 'test@example.com'
|
||||
|
||||
allow(GlobalConfigService).to receive(:load)
|
||||
.with('SKIP_INCOMING_BCC_PROCESSING', '')
|
||||
.and_return(channel_email.account_id.to_s)
|
||||
|
||||
channel = described_class.new(reply_mail.mail).perform
|
||||
expect(channel).to be_nil
|
||||
end
|
||||
|
||||
it 'skip bcc email when account is in multiple account ids config' do
|
||||
channel_email.update(email: 'test@example.com')
|
||||
reply_mail.mail['to'] = nil
|
||||
reply_mail.mail['bcc'] = 'test@example.com'
|
||||
|
||||
# Include this account along with other account IDs
|
||||
other_account_ids = [123, 456, channel_email.account_id, 789]
|
||||
allow(GlobalConfigService).to receive(:load)
|
||||
.with('SKIP_INCOMING_BCC_PROCESSING', '')
|
||||
.and_return(other_account_ids.join(','))
|
||||
|
||||
channel = described_class.new(reply_mail.mail).perform
|
||||
expect(channel).to be_nil
|
||||
end
|
||||
|
||||
it 'process bcc email when account is not in skip config' do
|
||||
channel_email.update(email: 'test@example.com')
|
||||
reply_mail.mail['to'] = nil
|
||||
reply_mail.mail['bcc'] = 'test@example.com'
|
||||
|
||||
# Configure other account IDs but not this one
|
||||
other_account_ids = [123, 456, 789]
|
||||
allow(GlobalConfigService).to receive(:load)
|
||||
.with('SKIP_INCOMING_BCC_PROCESSING', '')
|
||||
.and_return(other_account_ids.join(','))
|
||||
|
||||
channel = described_class.new(reply_mail.mail).perform
|
||||
expect(channel).to eq(channel_email)
|
||||
end
|
||||
|
||||
it 'process bcc email when skip config is empty' do
|
||||
channel_email.update(email: 'test@example.com')
|
||||
reply_mail.mail['to'] = nil
|
||||
reply_mail.mail['bcc'] = 'test@example.com'
|
||||
|
||||
allow(GlobalConfigService).to receive(:load)
|
||||
.with('SKIP_INCOMING_BCC_PROCESSING', '')
|
||||
.and_return('')
|
||||
|
||||
channel = described_class.new(reply_mail.mail).perform
|
||||
expect(channel).to eq(channel_email)
|
||||
end
|
||||
|
||||
it 'process bcc email when skip config is nil' do
|
||||
channel_email.update(email: 'test@example.com')
|
||||
reply_mail.mail['to'] = nil
|
||||
reply_mail.mail['bcc'] = 'test@example.com'
|
||||
|
||||
allow(GlobalConfigService).to receive(:load)
|
||||
.with('SKIP_INCOMING_BCC_PROCESSING', '')
|
||||
.and_return(nil)
|
||||
|
||||
channel = described_class.new(reply_mail.mail).perform
|
||||
expect(channel).to eq(channel_email)
|
||||
end
|
||||
|
||||
it 'return channel with X-Original-To email' do
|
||||
channel_email.update(email: 'test@example.com')
|
||||
reply_mail.mail['to'] = nil
|
||||
reply_mail.mail['X-Original-To'] = 'test@example.com'
|
||||
channel = described_class.new(reply_mail.mail).perform
|
||||
expect(channel).to eq(channel_email)
|
||||
end
|
||||
|
||||
it 'process X-Original-To email even when account is configured to skip BCC processing' do
|
||||
channel_email.update(email: 'test@example.com')
|
||||
reply_mail.mail['to'] = nil
|
||||
reply_mail.mail['X-Original-To'] = 'test@example.com'
|
||||
|
||||
allow(GlobalConfigService).to receive(:load)
|
||||
.with('SKIP_INCOMING_BCC_PROCESSING', '')
|
||||
.and_return(channel_email.account_id.to_s)
|
||||
|
||||
channel = described_class.new(reply_mail.mail).perform
|
||||
expect(channel).to eq(channel_email)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
77
research/chatwoot/spec/finders/message_finder_spec.rb
Normal file
77
research/chatwoot/spec/finders/message_finder_spec.rb
Normal file
@@ -0,0 +1,77 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe MessageFinder do
|
||||
subject(:message_finder) { described_class.new(conversation, params) }
|
||||
|
||||
let!(:account) { create(:account) }
|
||||
let!(:user) { create(:user, account: account) }
|
||||
let!(:inbox) { create(:inbox, account: account) }
|
||||
let!(:contact) { create(:contact, email: nil) }
|
||||
let!(:conversation) do
|
||||
create(:conversation, account: account, inbox: inbox, assignee: user, contact: contact)
|
||||
end
|
||||
|
||||
before do
|
||||
create(:message, account: account, inbox: inbox, conversation: conversation)
|
||||
create(:message, message_type: 'activity', account: account, inbox: inbox, conversation: conversation)
|
||||
create(:message, message_type: 'activity', account: account, inbox: inbox, conversation: conversation)
|
||||
# this outgoing message creates 2 additional messages because of the email hook execution service
|
||||
create(:message, message_type: 'outgoing', account: account, inbox: inbox, conversation: conversation)
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'with filter_internal_messages false' do
|
||||
let(:params) { { filter_internal_messages: false } }
|
||||
|
||||
it 'filter conversations by status' do
|
||||
result = message_finder.perform
|
||||
expect(result.count).to be 6
|
||||
end
|
||||
end
|
||||
|
||||
context 'with filter_internal_messages true' do
|
||||
let(:params) { { filter_internal_messages: true } }
|
||||
|
||||
it 'filter conversations by status' do
|
||||
result = message_finder.perform
|
||||
expect(result.count).to be 4
|
||||
end
|
||||
end
|
||||
|
||||
context 'with before attribute' do
|
||||
let!(:outgoing) { create(:message, message_type: 'outgoing', account: account, inbox: inbox, conversation: conversation) }
|
||||
let(:params) { { before: outgoing.id } }
|
||||
|
||||
it 'filter conversations by status' do
|
||||
result = message_finder.perform
|
||||
expect(result.count).to be 6
|
||||
end
|
||||
end
|
||||
|
||||
context 'with after attribute' do
|
||||
let(:params) { { after: conversation.messages.first.id } }
|
||||
|
||||
it 'filter conversations by status' do
|
||||
result = message_finder.perform
|
||||
expect(result.count).to be 5
|
||||
expect(result.first.id).to be conversation.messages.second.id
|
||||
expect(result.last.message_type).to eq 'outgoing'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with after and before attribute' do
|
||||
let(:params) do
|
||||
{
|
||||
after: conversation.messages.first.id,
|
||||
before: conversation.messages.last.id
|
||||
}
|
||||
end
|
||||
|
||||
it 'filter conversations by status' do
|
||||
result = message_finder.perform
|
||||
expect(result.count).to be 5
|
||||
expect(result.last.id).to be conversation.messages[-2].id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
97
research/chatwoot/spec/finders/notification_finder_spec.rb
Normal file
97
research/chatwoot/spec/finders/notification_finder_spec.rb
Normal file
@@ -0,0 +1,97 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe NotificationFinder do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:user) { create(:user, account: account) }
|
||||
let(:notification_finder) { described_class.new(user, account, params) }
|
||||
|
||||
before do
|
||||
create(:notification, :snoozed, account: account, user: user)
|
||||
create_list(:notification, 2, :read, account: account, user: user)
|
||||
create_list(:notification, 3, account: account, user: user)
|
||||
end
|
||||
|
||||
describe '#notifications' do
|
||||
subject { notification_finder.notifications }
|
||||
|
||||
context 'with default params (empty)' do
|
||||
let(:params) { {} }
|
||||
|
||||
it 'returns all unread and unsnoozed notifications, ordered by last activity' do
|
||||
expect(subject.size).to eq(3)
|
||||
expect(subject).to match_array(subject.sort_by(&:last_activity_at).reverse)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with params including read and snoozed statuses' do
|
||||
let(:params) { { includes: %w[read snoozed] } }
|
||||
|
||||
it 'returns all notifications, including read and snoozed' do
|
||||
expect(subject.size).to eq(6)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with params including only read status' do
|
||||
let(:params) { { includes: ['read'] } }
|
||||
|
||||
it 'returns all notifications expect the snoozed' do
|
||||
expect(subject.size).to eq(5)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with params including only snoozed status' do
|
||||
let(:params) { { includes: ['snoozed'] } }
|
||||
|
||||
it 'rreturns all notifications only expect the read' do
|
||||
expect(subject.size).to eq(4)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with ascending sort order' do
|
||||
let(:params) { { sort_order: :asc } }
|
||||
|
||||
it 'returns notifications in ascending order by last activity' do
|
||||
expect(subject.first.last_activity_at).to be < subject.last.last_activity_at
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'counts' do
|
||||
subject { notification_finder }
|
||||
|
||||
context 'without specific filters' do
|
||||
let(:params) { {} }
|
||||
|
||||
it 'correctly reports unread and total counts' do
|
||||
expect(subject.unread_count).to eq(3)
|
||||
expect(subject.count).to eq(3)
|
||||
end
|
||||
|
||||
it 'avoids duplicate filtering in unread_count method' do
|
||||
# Test the logical fix: when no 'read' filter is included,
|
||||
# @notifications is already filtered to unread, so unread_count
|
||||
# should just count without adding another read_at filter
|
||||
|
||||
allow(subject.instance_variable_get(:@notifications)).to receive(:where).and_call_original
|
||||
allow(subject.instance_variable_get(:@notifications)).to receive(:count).and_call_original
|
||||
|
||||
result = subject.unread_count
|
||||
|
||||
# Should return correct count without additional where clause
|
||||
expect(result).to eq(3)
|
||||
|
||||
# The fix ensures that when params[:includes] doesn't contain 'read',
|
||||
# unread_count uses @notifications.count instead of @notifications.where(read_at: nil).count
|
||||
end
|
||||
end
|
||||
|
||||
context 'with filters applied' do
|
||||
let(:params) { { includes: %w[read snoozed] } }
|
||||
|
||||
it 'adjusts counts based on included statuses' do
|
||||
expect(subject.unread_count).to eq(4) # 3 unread + 1 snoozed (which is unread)
|
||||
expect(subject.count).to eq(6) # all notifications including read and snoozed
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user