mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-31 19:17:48 +00:00
chore(refactor): Improve conversation permission filtering (#11166)
1. Add permission filter service to separate permission filtering logic from conversation queries 2. Implement hierarchical permissions with cleaner logic: - conversation_manage gives access to all conversations - conversation_unassigned_manage gives access to unassigned and user's conversations - conversation_participating_manage gives access only to user's conversations --------- Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -91,3 +91,6 @@ yarn-debug.log*
|
|||||||
# Vite uses dotenv and suggests to ignore local-only env files. See
|
# Vite uses dotenv and suggests to ignore local-only env files. See
|
||||||
# https://vitejs.dev/guide/env-and-mode.html#env-files
|
# https://vitejs.dev/guide/env-and-mode.html#env-files
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
|
# Claude.ai config file
|
||||||
|
CLAUDE.md
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::Contacts::BaseController
|
class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::Contacts::BaseController
|
||||||
def index
|
def index
|
||||||
@conversations = Current.account.conversations.includes(
|
# Start with all conversations for this contact
|
||||||
|
conversations = Current.account.conversations.includes(
|
||||||
:assignee, :contact, :inbox, :taggings
|
:assignee, :contact, :inbox, :taggings
|
||||||
).where(inbox_id: inbox_ids, contact_id: @contact.id).order(last_activity_at: :desc).limit(20)
|
).where(contact_id: @contact.id)
|
||||||
end
|
|
||||||
|
|
||||||
private
|
# Apply permission-based filtering using the existing service
|
||||||
|
conversations = Conversations::PermissionFilterService.new(
|
||||||
|
conversations,
|
||||||
|
Current.user,
|
||||||
|
Current.account
|
||||||
|
).perform
|
||||||
|
|
||||||
def inbox_ids
|
# Only allow conversations from inboxes the user has access to
|
||||||
if Current.user.administrator? || Current.user.agent?
|
inbox_ids = Current.user.assigned_inboxes.pluck(:id)
|
||||||
Current.user.assigned_inboxes.pluck(:id)
|
conversations = conversations.where(inbox_id: inbox_ids)
|
||||||
else
|
|
||||||
[]
|
@conversations = conversations.order(last_activity_at: :desc).limit(20)
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
end
|
end
|
||||||
|
|
||||||
def filter
|
def filter
|
||||||
result = ::Conversations::FilterService.new(params.permit!, current_user).perform
|
result = ::Conversations::FilterService.new(params.permit!, current_user, current_account).perform
|
||||||
@conversations = result[:conversations]
|
@conversations = result[:conversations]
|
||||||
@conversations_count = result[:count]
|
@conversations_count = result[:count]
|
||||||
rescue CustomExceptions::CustomFilter::InvalidAttribute,
|
rescue CustomExceptions::CustomFilter::InvalidAttribute,
|
||||||
|
|||||||
@@ -93,6 +93,12 @@ class ConversationFinder
|
|||||||
|
|
||||||
def find_all_conversations
|
def find_all_conversations
|
||||||
find_conversation_by_inbox
|
find_conversation_by_inbox
|
||||||
|
# Apply permission-based filtering
|
||||||
|
@conversations = Conversations::PermissionFilterService.new(
|
||||||
|
@conversations,
|
||||||
|
current_user,
|
||||||
|
current_account
|
||||||
|
).perform
|
||||||
filter_by_conversation_type if params[:conversation_type]
|
filter_by_conversation_type if params[:conversation_type]
|
||||||
@conversations
|
@conversations
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
class Conversations::FilterService < FilterService
|
class Conversations::FilterService < FilterService
|
||||||
ATTRIBUTE_MODEL = 'conversation_attribute'.freeze
|
ATTRIBUTE_MODEL = 'conversation_attribute'.freeze
|
||||||
|
|
||||||
def initialize(params, user, filter_account = nil)
|
def initialize(params, user, account)
|
||||||
@account = filter_account || Current.account
|
@account = account
|
||||||
super(params, user)
|
super(params, user)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -24,9 +24,25 @@ class Conversations::FilterService < FilterService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def base_relation
|
def base_relation
|
||||||
@account.conversations.includes(
|
conversations = @account.conversations.includes(
|
||||||
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :messages, :contact_inbox
|
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :messages, :contact_inbox
|
||||||
)
|
)
|
||||||
|
|
||||||
|
account_user = @account.account_users.find_by(user_id: @user.id)
|
||||||
|
is_administrator = account_user&.role == 'administrator'
|
||||||
|
|
||||||
|
# Ensure we only include conversations from inboxes the user has access to
|
||||||
|
unless is_administrator
|
||||||
|
inbox_ids = @user.inboxes.where(account_id: @account.id).pluck(:id)
|
||||||
|
conversations = conversations.where(inbox_id: inbox_ids)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Apply permission-based filtering
|
||||||
|
Conversations::PermissionFilterService.new(
|
||||||
|
conversations,
|
||||||
|
@user,
|
||||||
|
@account
|
||||||
|
).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_page
|
def current_page
|
||||||
|
|||||||
17
app/services/conversations/permission_filter_service.rb
Normal file
17
app/services/conversations/permission_filter_service.rb
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
class Conversations::PermissionFilterService
|
||||||
|
attr_reader :conversations, :user, :account
|
||||||
|
|
||||||
|
def initialize(conversations, user, account)
|
||||||
|
@conversations = conversations
|
||||||
|
@user = user
|
||||||
|
@account = account
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform
|
||||||
|
# The base implementation simply returns all conversations
|
||||||
|
# Enterprise edition extends this with permission-based filtering
|
||||||
|
conversations
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Conversations::PermissionFilterService.prepend_mod_with('Conversations::PermissionFilterService')
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
module Enterprise::Conversations::PermissionFilterService
|
||||||
|
def perform
|
||||||
|
account_user = AccountUser.find_by(account_id: account.id, user_id: user.id)
|
||||||
|
permissions = account_user&.permissions || []
|
||||||
|
user_role = account_user&.role
|
||||||
|
|
||||||
|
# Skip filtering for administrators
|
||||||
|
return conversations if user_role == 'administrator'
|
||||||
|
# Skip filtering for regular agents (without custom roles/permissions)
|
||||||
|
return conversations if user_role == 'agent' && account_user&.custom_role_id.nil?
|
||||||
|
|
||||||
|
filter_by_permissions(permissions)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def filter_by_permissions(permissions)
|
||||||
|
# Permission-based filtering with hierarchy
|
||||||
|
# conversation_manage > conversation_unassigned_manage > conversation_participating_manage
|
||||||
|
if permissions.include?('conversation_manage')
|
||||||
|
conversations
|
||||||
|
elsif permissions.include?('conversation_unassigned_manage')
|
||||||
|
filter_unassigned_and_mine
|
||||||
|
elsif permissions.include?('conversation_participating_manage')
|
||||||
|
conversations.assigned_to(user)
|
||||||
|
else
|
||||||
|
Conversation.none
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_unassigned_and_mine
|
||||||
|
mine = conversations.assigned_to(user)
|
||||||
|
unassigned = conversations.unassigned
|
||||||
|
|
||||||
|
Conversation.from("(#{mine.to_sql} UNION #{unassigned.to_sql}) as conversations")
|
||||||
|
.where(account_id: account.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe '/api/v1/accounts/{account.id}/contacts/:id/conversations enterprise', type: :request do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:contact) { create(:contact, account: account) }
|
||||||
|
let(:inbox) { create(:inbox, account: account) }
|
||||||
|
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: inbox) }
|
||||||
|
|
||||||
|
describe 'GET /api/v1/accounts/{account.id}/contacts/:id/conversations with custom role permissions' do
|
||||||
|
context 'with user having custom role' do
|
||||||
|
let(:agent_with_custom_role) { create(:user, account: account, role: :agent) }
|
||||||
|
let(:custom_role) { create(:custom_role, account: account) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:inbox_member, user: agent_with_custom_role, inbox: inbox)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with conversation_participating_manage permission' do
|
||||||
|
let(:assigned_conversation) do
|
||||||
|
create(:conversation, account: account, inbox: inbox, contact: contact,
|
||||||
|
contact_inbox: contact_inbox, assignee: agent_with_custom_role)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
# Create a conversation assigned to this agent
|
||||||
|
assigned_conversation
|
||||||
|
|
||||||
|
# Create another conversation that shouldn't be visible
|
||||||
|
create(:conversation, account: account, inbox: inbox, contact: contact,
|
||||||
|
contact_inbox: contact_inbox, assignee: create(:user, account: account, role: :agent))
|
||||||
|
|
||||||
|
# Set up permissions
|
||||||
|
custom_role.update!(permissions: %w[conversation_participating_manage])
|
||||||
|
|
||||||
|
# Associate the custom role with the agent
|
||||||
|
account_user = AccountUser.find_by(user: agent_with_custom_role, account: account)
|
||||||
|
account_user.update!(role: :agent, custom_role: custom_role)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns only conversations assigned to the agent' do
|
||||||
|
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/conversations",
|
||||||
|
headers: agent_with_custom_role.create_new_auth_token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
json_response = response.parsed_body
|
||||||
|
|
||||||
|
# Should only return the conversation assigned to this agent
|
||||||
|
expect(json_response['payload'].length).to eq 1
|
||||||
|
expect(json_response['payload'][0]['id']).to eq assigned_conversation.display_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with conversation_unassigned_manage permission' do
|
||||||
|
let(:unassigned_conversation) do
|
||||||
|
create(:conversation, account: account, inbox: inbox, contact: contact,
|
||||||
|
contact_inbox: contact_inbox, assignee: nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:assigned_conversation) do
|
||||||
|
create(:conversation, account: account, inbox: inbox, contact: contact,
|
||||||
|
contact_inbox: contact_inbox, assignee: agent_with_custom_role)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
# Create the conversations
|
||||||
|
unassigned_conversation
|
||||||
|
assigned_conversation
|
||||||
|
create(:conversation, account: account, inbox: inbox, contact: contact,
|
||||||
|
contact_inbox: contact_inbox, assignee: create(:user, account: account, role: :agent))
|
||||||
|
|
||||||
|
# Set up permissions
|
||||||
|
custom_role.update!(permissions: %w[conversation_unassigned_manage])
|
||||||
|
|
||||||
|
# Associate the custom role with the agent
|
||||||
|
account_user = AccountUser.find_by(user: agent_with_custom_role, account: account)
|
||||||
|
account_user.update!(role: :agent, custom_role: custom_role)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns unassigned conversations AND conversations assigned to the agent' do
|
||||||
|
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/conversations",
|
||||||
|
headers: agent_with_custom_role.create_new_auth_token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
json_response = response.parsed_body
|
||||||
|
|
||||||
|
# Should return both unassigned and assigned to this agent conversations
|
||||||
|
expect(json_response['payload'].length).to eq 2
|
||||||
|
conversation_ids = json_response['payload'].pluck('id')
|
||||||
|
expect(conversation_ids).to include(unassigned_conversation.display_id)
|
||||||
|
expect(conversation_ids).to include(assigned_conversation.display_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with conversation_manage permission' do
|
||||||
|
before do
|
||||||
|
# Create multiple conversations
|
||||||
|
3.times do
|
||||||
|
create(:conversation, account: account, inbox: inbox, contact: contact,
|
||||||
|
contact_inbox: contact_inbox)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Set up permissions
|
||||||
|
custom_role.update!(permissions: %w[conversation_manage])
|
||||||
|
|
||||||
|
# Associate the custom role with the agent
|
||||||
|
account_user = AccountUser.find_by(user: agent_with_custom_role, account: account)
|
||||||
|
account_user.update!(role: :agent, custom_role: custom_role)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns all conversations' do
|
||||||
|
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/conversations",
|
||||||
|
headers: agent_with_custom_role.create_new_auth_token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
json_response = response.parsed_body
|
||||||
|
|
||||||
|
# Should return all conversations in this inbox
|
||||||
|
expect(json_response['payload'].length).to eq 3
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Enterprise::Conversations::PermissionFilterService do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
# Create conversations with different states
|
||||||
|
let!(:assigned_conversation) { create(:conversation, account: account, inbox: inbox, assignee: agent) }
|
||||||
|
let!(:unassigned_conversation) { create(:conversation, account: account, inbox: inbox, assignee: nil) }
|
||||||
|
let!(:another_assigned_conversation) { create(:conversation, account: account, inbox: inbox, assignee: create(:user, account: account)) }
|
||||||
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||||
|
let(:agent) { create(:user, account: account, role: :agent) }
|
||||||
|
let!(:inbox) { create(:inbox, account: account) }
|
||||||
|
|
||||||
|
# This inbox_member is used to establish the agent's access to the inbox
|
||||||
|
before { create(:inbox_member, user: agent, inbox: inbox) }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
context 'when user is an administrator' do
|
||||||
|
it 'returns all conversations' do
|
||||||
|
result = Conversations::PermissionFilterService.new(
|
||||||
|
account.conversations,
|
||||||
|
admin,
|
||||||
|
account
|
||||||
|
).perform
|
||||||
|
|
||||||
|
expect(result).to include(assigned_conversation)
|
||||||
|
expect(result).to include(unassigned_conversation)
|
||||||
|
expect(result).to include(another_assigned_conversation)
|
||||||
|
expect(result.count).to eq(3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is a regular agent' do
|
||||||
|
it 'returns all conversations in assigned inboxes' do
|
||||||
|
inbox_ids = agent.inboxes.where(account_id: account.id).pluck(:id)
|
||||||
|
|
||||||
|
result = Conversations::PermissionFilterService.new(
|
||||||
|
account.conversations.where(inbox_id: inbox_ids),
|
||||||
|
agent,
|
||||||
|
account
|
||||||
|
).perform
|
||||||
|
|
||||||
|
expect(result).to include(assigned_conversation)
|
||||||
|
expect(result).to include(unassigned_conversation)
|
||||||
|
expect(result).to include(another_assigned_conversation)
|
||||||
|
expect(result.count).to eq(3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user has conversation_manage permission' do
|
||||||
|
# Test with a new clean state for each test case
|
||||||
|
it 'returns all conversations' do
|
||||||
|
# Create a new isolated test environment
|
||||||
|
test_account = create(:account)
|
||||||
|
test_inbox = create(:inbox, account: test_account)
|
||||||
|
|
||||||
|
# Create test agent
|
||||||
|
test_agent = create(:user, account: test_account, role: :agent)
|
||||||
|
create(:inbox_member, user: test_agent, inbox: test_inbox)
|
||||||
|
|
||||||
|
# Create custom role with conversation_manage permission
|
||||||
|
test_custom_role = create(:custom_role, account: test_account, permissions: ['conversation_manage'])
|
||||||
|
account_user = AccountUser.find_by(user: test_agent, account: test_account)
|
||||||
|
account_user.update(role: :agent, custom_role: test_custom_role)
|
||||||
|
|
||||||
|
# Create some conversations
|
||||||
|
assigned_conversation = create(:conversation, account: test_account, inbox: test_inbox, assignee: test_agent)
|
||||||
|
unassigned_conversation = create(:conversation, account: test_account, inbox: test_inbox, assignee: nil)
|
||||||
|
other_assigned_conversation = create(:conversation, account: test_account, inbox: test_inbox, assignee: create(:user, account: test_account))
|
||||||
|
|
||||||
|
# Run the test
|
||||||
|
result = Conversations::PermissionFilterService.new(
|
||||||
|
test_account.conversations,
|
||||||
|
test_agent,
|
||||||
|
test_account
|
||||||
|
).perform
|
||||||
|
|
||||||
|
# Should have access to all conversations
|
||||||
|
expect(result.count).to eq(3)
|
||||||
|
expect(result).to include(assigned_conversation)
|
||||||
|
expect(result).to include(unassigned_conversation)
|
||||||
|
expect(result).to include(other_assigned_conversation)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user has conversation_participating_manage permission' do
|
||||||
|
it 'returns only conversations assigned to the agent' do
|
||||||
|
# Create a new isolated test environment
|
||||||
|
test_account = create(:account)
|
||||||
|
test_inbox = create(:inbox, account: test_account)
|
||||||
|
|
||||||
|
# Create test agent
|
||||||
|
test_agent = create(:user, account: test_account, role: :agent)
|
||||||
|
create(:inbox_member, user: test_agent, inbox: test_inbox)
|
||||||
|
|
||||||
|
# Create a custom role with only the conversation_participating_manage permission
|
||||||
|
test_custom_role = create(:custom_role, account: test_account, permissions: %w[conversation_participating_manage])
|
||||||
|
|
||||||
|
account_user = AccountUser.find_by(user: test_agent, account: test_account)
|
||||||
|
account_user.update(role: :agent, custom_role: test_custom_role)
|
||||||
|
|
||||||
|
# Create some conversations
|
||||||
|
other_conversation = create(:conversation, account: test_account, inbox: test_inbox)
|
||||||
|
assigned_conversation = create(:conversation, account: test_account, inbox: test_inbox, assignee: test_agent)
|
||||||
|
|
||||||
|
# Run the test
|
||||||
|
result = Conversations::PermissionFilterService.new(
|
||||||
|
test_account.conversations,
|
||||||
|
test_agent,
|
||||||
|
test_account
|
||||||
|
).perform
|
||||||
|
|
||||||
|
# Should only see conversations assigned to this agent
|
||||||
|
expect(result.count).to eq(1)
|
||||||
|
expect(result.first.assignee).to eq(test_agent)
|
||||||
|
expect(result).to include(assigned_conversation)
|
||||||
|
expect(result).not_to include(other_conversation)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user has conversation_unassigned_manage permission' do
|
||||||
|
it 'returns unassigned conversations AND mine' do
|
||||||
|
# Create a new isolated test environment
|
||||||
|
test_account = create(:account)
|
||||||
|
test_inbox = create(:inbox, account: test_account)
|
||||||
|
|
||||||
|
# Create test agent
|
||||||
|
test_agent = create(:user, account: test_account, role: :agent)
|
||||||
|
create(:inbox_member, user: test_agent, inbox: test_inbox)
|
||||||
|
|
||||||
|
# Create a custom role with only the conversation_unassigned_manage permission
|
||||||
|
test_custom_role = create(:custom_role, account: test_account, permissions: %w[conversation_unassigned_manage])
|
||||||
|
|
||||||
|
account_user = AccountUser.find_by(user: test_agent, account: test_account)
|
||||||
|
account_user.update(role: :agent, custom_role: test_custom_role)
|
||||||
|
|
||||||
|
# Create some conversations
|
||||||
|
assigned_conversation = create(:conversation, account: test_account, inbox: test_inbox, assignee: test_agent)
|
||||||
|
unassigned_conversation = create(:conversation, account: test_account, inbox: test_inbox, assignee: nil)
|
||||||
|
other_assigned_conversation = create(:conversation, account: test_account, inbox: test_inbox, assignee: create(:user, account: test_account))
|
||||||
|
|
||||||
|
# Run the test
|
||||||
|
result = Conversations::PermissionFilterService.new(
|
||||||
|
test_account.conversations,
|
||||||
|
test_agent,
|
||||||
|
test_account
|
||||||
|
).perform
|
||||||
|
|
||||||
|
# Should see unassigned conversations AND conversations assigned to this agent
|
||||||
|
expect(result.count).to eq(2)
|
||||||
|
expect(result).to include(unassigned_conversation)
|
||||||
|
expect(result).to include(assigned_conversation)
|
||||||
|
|
||||||
|
# Should NOT include conversations assigned to others
|
||||||
|
expect(result).not_to include(other_assigned_conversation)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user has both participating and unassigned permissions (hierarchical test)' do
|
||||||
|
it 'gives higher priority to unassigned_manage over participating_manage' do
|
||||||
|
# Create a new isolated test environment
|
||||||
|
test_account = create(:account)
|
||||||
|
test_inbox = create(:inbox, account: test_account)
|
||||||
|
|
||||||
|
# Create test agent
|
||||||
|
test_agent = create(:user, account: test_account, role: :agent)
|
||||||
|
create(:inbox_member, user: test_agent, inbox: test_inbox)
|
||||||
|
|
||||||
|
# Create a custom role with both participating and unassigned permissions
|
||||||
|
permissions = %w[conversation_participating_manage conversation_unassigned_manage]
|
||||||
|
test_custom_role = create(:custom_role, account: test_account, permissions: permissions)
|
||||||
|
|
||||||
|
account_user = AccountUser.find_by(user: test_agent, account: test_account)
|
||||||
|
account_user.update(role: :agent, custom_role: test_custom_role)
|
||||||
|
|
||||||
|
# Create some conversations
|
||||||
|
assigned_to_agent = create(:conversation, account: test_account, inbox: test_inbox, assignee: test_agent)
|
||||||
|
unassigned_conversation = create(:conversation, account: test_account, inbox: test_inbox, assignee: nil)
|
||||||
|
other_assigned_conversation = create(:conversation, account: test_account, inbox: test_inbox, assignee: create(:user, account: test_account))
|
||||||
|
|
||||||
|
# Run the test
|
||||||
|
result = Conversations::PermissionFilterService.new(
|
||||||
|
test_account.conversations,
|
||||||
|
test_agent,
|
||||||
|
test_account
|
||||||
|
).perform
|
||||||
|
|
||||||
|
# Should behave the same as conversation_unassigned_manage test
|
||||||
|
# - Show both unassigned and assigned to this agent
|
||||||
|
# - Do not show conversations assigned to others
|
||||||
|
expect(result.count).to eq(2)
|
||||||
|
expect(result).to include(unassigned_conversation)
|
||||||
|
expect(result).to include(assigned_to_agent)
|
||||||
|
expect(result).not_to include(other_assigned_conversation)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
## This spec is to ensure alignment between frontend and backend filters
|
||||||
|
# ref: https://github.com/chatwoot/chatwoot/pull/11111
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Conversations::FilterService do
|
||||||
|
describe 'Frontend alignment tests' do
|
||||||
|
let!(:account) { create(:account) }
|
||||||
|
let!(:user_1) { create(:user, account: account, role: :administrator) }
|
||||||
|
let!(:inbox) { create(:inbox, account: account) }
|
||||||
|
let!(:params) { { payload: [], page: 1 } }
|
||||||
|
|
||||||
|
before do
|
||||||
|
account.conversations.destroy_all
|
||||||
|
|
||||||
|
# Create inbox membership
|
||||||
|
create(:inbox_member, user: user_1, inbox: inbox)
|
||||||
|
|
||||||
|
# Create custom attribute definition for conversation_type
|
||||||
|
create(:custom_attribute_definition,
|
||||||
|
attribute_key: 'conversation_type',
|
||||||
|
account: account,
|
||||||
|
attribute_model: 'conversation_attribute',
|
||||||
|
attribute_display_type: 'list',
|
||||||
|
attribute_values: %w[platinum silver gold regular])
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with A AND B OR C filter chain' do
|
||||||
|
let(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user_1) }
|
||||||
|
let(:filter_payload) do
|
||||||
|
[
|
||||||
|
{
|
||||||
|
attribute_key: 'status',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: ['open'],
|
||||||
|
query_operator: 'AND'
|
||||||
|
}.with_indifferent_access,
|
||||||
|
{
|
||||||
|
attribute_key: 'priority',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: ['urgent'],
|
||||||
|
query_operator: 'OR'
|
||||||
|
}.with_indifferent_access,
|
||||||
|
{
|
||||||
|
attribute_key: 'display_id',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: ['12345'],
|
||||||
|
query_operator: nil
|
||||||
|
}.with_indifferent_access
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
conversation.update!(
|
||||||
|
status: 'open',
|
||||||
|
priority: 'urgent',
|
||||||
|
display_id: '12345',
|
||||||
|
additional_attributes: { 'browser_language': 'en' }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'matches when all conditions are true' do
|
||||||
|
params[:payload] = filter_payload
|
||||||
|
result = described_class.new(params, user_1, account).perform
|
||||||
|
expect(result[:conversations].length).to be 1
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'matches when first condition is false but third is true' do
|
||||||
|
conversation.update!(status: 'resolved', priority: 'urgent', display_id: '12345')
|
||||||
|
params[:payload] = filter_payload
|
||||||
|
result = described_class.new(params, user_1, account).perform
|
||||||
|
expect(result[:conversations].length).to be 1
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'matches when first and second condition is false but third is true' do
|
||||||
|
conversation.update!(status: 'resolved', priority: 'low', display_id: '12345')
|
||||||
|
params[:payload] = filter_payload
|
||||||
|
result = described_class.new(params, user_1, account).perform
|
||||||
|
expect(result[:conversations].length).to be 1
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not match when all conditions are false' do
|
||||||
|
conversation.update!(status: 'resolved', priority: 'low', display_id: '67890')
|
||||||
|
params[:payload] = filter_payload
|
||||||
|
result = described_class.new(params, user_1, account).perform
|
||||||
|
expect(result[:conversations].length).to be 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with A OR B AND C filter chain' do
|
||||||
|
let(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user_1) }
|
||||||
|
let(:filter_payload) do
|
||||||
|
[
|
||||||
|
{
|
||||||
|
attribute_key: 'status',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: ['open'],
|
||||||
|
query_operator: 'OR'
|
||||||
|
}.with_indifferent_access,
|
||||||
|
{
|
||||||
|
attribute_key: 'priority',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: ['low'],
|
||||||
|
query_operator: 'AND'
|
||||||
|
}.with_indifferent_access,
|
||||||
|
{
|
||||||
|
attribute_key: 'display_id',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: ['67890'],
|
||||||
|
query_operator: nil
|
||||||
|
}.with_indifferent_access
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
conversation.update!(
|
||||||
|
status: 'open',
|
||||||
|
priority: 'urgent',
|
||||||
|
display_id: '12345',
|
||||||
|
additional_attributes: { 'browser_language': 'en' }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'matches when first condition is true' do
|
||||||
|
params[:payload] = filter_payload
|
||||||
|
result = described_class.new(params, user_1, account).perform
|
||||||
|
expect(result[:conversations].length).to be 1
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'matches when second and third conditions are true' do
|
||||||
|
conversation.update!(status: 'resolved', priority: 'low', display_id: '67890')
|
||||||
|
params[:payload] = filter_payload
|
||||||
|
result = described_class.new(params, user_1, account).perform
|
||||||
|
expect(result[:conversations].length).to be 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with complex filter chain A AND B OR C AND D' do
|
||||||
|
let(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user_1) }
|
||||||
|
let(:filter_payload) do
|
||||||
|
[
|
||||||
|
{
|
||||||
|
attribute_key: 'status',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: ['open'],
|
||||||
|
query_operator: 'AND'
|
||||||
|
}.with_indifferent_access,
|
||||||
|
{
|
||||||
|
attribute_key: 'priority',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: ['urgent'],
|
||||||
|
query_operator: 'OR'
|
||||||
|
}.with_indifferent_access,
|
||||||
|
{
|
||||||
|
attribute_key: 'display_id',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: ['67890'],
|
||||||
|
query_operator: 'AND'
|
||||||
|
}.with_indifferent_access,
|
||||||
|
{
|
||||||
|
attribute_key: 'browser_language',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: ['tr'],
|
||||||
|
query_operator: nil
|
||||||
|
}.with_indifferent_access
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
conversation.update!(
|
||||||
|
status: 'open',
|
||||||
|
priority: 'urgent',
|
||||||
|
display_id: '12345',
|
||||||
|
additional_attributes: { 'browser_language': 'en' },
|
||||||
|
custom_attributes: { conversation_type: 'platinum' }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'matches when first two conditions are true' do
|
||||||
|
params[:payload] = filter_payload
|
||||||
|
result = described_class.new(params, user_1, account).perform
|
||||||
|
expect(result[:conversations].length).to be 1
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'matches when last two conditions are true' do
|
||||||
|
conversation.update!(
|
||||||
|
status: 'resolved',
|
||||||
|
priority: 'low',
|
||||||
|
display_id: '67890',
|
||||||
|
additional_attributes: { 'browser_language': 'tr' }
|
||||||
|
)
|
||||||
|
params[:payload] = filter_payload
|
||||||
|
result = described_class.new(params, user_1, account).perform
|
||||||
|
expect(result[:conversations].length).to be 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with mixed operators filter chain' do
|
||||||
|
let(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user_1) }
|
||||||
|
let(:filter_payload) do
|
||||||
|
[
|
||||||
|
{
|
||||||
|
attribute_key: 'status',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: ['open'],
|
||||||
|
query_operator: 'AND'
|
||||||
|
}.with_indifferent_access,
|
||||||
|
{
|
||||||
|
attribute_key: 'priority',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: ['urgent'],
|
||||||
|
query_operator: 'OR'
|
||||||
|
}.with_indifferent_access,
|
||||||
|
{
|
||||||
|
attribute_key: 'display_id',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: ['67890'],
|
||||||
|
query_operator: 'AND'
|
||||||
|
}.with_indifferent_access,
|
||||||
|
{
|
||||||
|
attribute_key: 'conversation_type',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: ['platinum'],
|
||||||
|
custom_attribute_type: '',
|
||||||
|
query_operator: nil
|
||||||
|
}.with_indifferent_access
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
conversation.update!(
|
||||||
|
status: 'open',
|
||||||
|
priority: 'urgent',
|
||||||
|
display_id: '12345',
|
||||||
|
additional_attributes: { 'browser_language': 'en' },
|
||||||
|
custom_attributes: { conversation_type: 'platinum' }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'matches when all conditions in the chain are true' do
|
||||||
|
params[:payload] = filter_payload
|
||||||
|
result = described_class.new(params, user_1, account).perform
|
||||||
|
expect(result[:conversations].length).to be 1
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not match when the last condition is false' do
|
||||||
|
conversation.update!(custom_attributes: { conversation_type: 'silver' })
|
||||||
|
params[:payload] = filter_payload
|
||||||
|
result = described_class.new(params, user_1, account).perform
|
||||||
|
expect(result[:conversations].length).to be 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -23,7 +23,6 @@ describe Conversations::FilterService do
|
|||||||
before do
|
before do
|
||||||
create(:inbox_member, user: user_1, inbox: inbox)
|
create(:inbox_member, user: user_1, inbox: inbox)
|
||||||
create(:inbox_member, user: user_2, inbox: inbox)
|
create(:inbox_member, user: user_2, inbox: inbox)
|
||||||
Current.account = account
|
|
||||||
|
|
||||||
en_conversation_1.update!(custom_attributes: { conversation_additional_information: 'test custom data' })
|
en_conversation_1.update!(custom_attributes: { conversation_additional_information: 'test custom data' })
|
||||||
en_conversation_2.update!(custom_attributes: { conversation_additional_information: 'test custom data', conversation_type: 'platinum' })
|
en_conversation_2.update!(custom_attributes: { conversation_additional_information: 'test custom data', conversation_type: 'platinum' })
|
||||||
@@ -72,7 +71,7 @@ describe Conversations::FilterService do
|
|||||||
|
|
||||||
it 'filter conversations by additional_attributes and status' do
|
it 'filter conversations by additional_attributes and status' do
|
||||||
params[:payload] = payload
|
params[:payload] = payload
|
||||||
result = filter_service.new(params, user_1).perform
|
result = filter_service.new(params, user_1, account).perform
|
||||||
conversations = Conversation.where("additional_attributes ->> 'browser_language' IN (?) AND status IN (?)", ['en'], [1, 2])
|
conversations = Conversation.where("additional_attributes ->> 'browser_language' IN (?) AND status IN (?)", ['en'], [1, 2])
|
||||||
expect(result[:count][:all_count]).to be conversations.count
|
expect(result[:count][:all_count]).to be conversations.count
|
||||||
end
|
end
|
||||||
@@ -88,7 +87,7 @@ describe Conversations::FilterService do
|
|||||||
custom_attribute_type: ''
|
custom_attribute_type: ''
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
]
|
]
|
||||||
result = filter_service.new(params, user_1).perform
|
result = filter_service.new(params, user_1, account).perform
|
||||||
expect(result[:conversations].length).to eq 1
|
expect(result[:conversations].length).to eq 1
|
||||||
expect(result[:conversations][0][:id]).to eq conversation.id
|
expect(result[:conversations][0][:id]).to eq conversation.id
|
||||||
end
|
end
|
||||||
@@ -107,7 +106,7 @@ describe Conversations::FilterService do
|
|||||||
custom_attribute_type: ''
|
custom_attribute_type: ''
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
]
|
]
|
||||||
result = filter_service.new(params, user_1).perform
|
result = filter_service.new(params, user_1, account).perform
|
||||||
expect(result[:conversations].length).to eq 2
|
expect(result[:conversations].length).to eq 2
|
||||||
expect(result[:conversations].pluck(:id)).to include(high_priority.id, urgent_priority.id)
|
expect(result[:conversations].pluck(:id)).to include(high_priority.id, urgent_priority.id)
|
||||||
end
|
end
|
||||||
@@ -127,7 +126,7 @@ describe Conversations::FilterService do
|
|||||||
custom_attribute_type: ''
|
custom_attribute_type: ''
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
]
|
]
|
||||||
result = filter_service.new(params, user_1).perform
|
result = filter_service.new(params, user_1, account).perform
|
||||||
|
|
||||||
# Only include conversations with medium and low priority, excluding high and urgent
|
# Only include conversations with medium and low priority, excluding high and urgent
|
||||||
expect(result[:conversations].length).to eq 2
|
expect(result[:conversations].length).to eq 2
|
||||||
@@ -137,7 +136,7 @@ describe Conversations::FilterService do
|
|||||||
it 'filter conversations by additional_attributes and status with pagination' do
|
it 'filter conversations by additional_attributes and status with pagination' do
|
||||||
params[:payload] = payload
|
params[:payload] = payload
|
||||||
params[:page] = 2
|
params[:page] = 2
|
||||||
result = filter_service.new(params, user_1).perform
|
result = filter_service.new(params, user_1, account).perform
|
||||||
conversations = Conversation.where("additional_attributes ->> 'browser_language' IN (?) AND status IN (?)", ['en'], [1, 2])
|
conversations = Conversation.where("additional_attributes ->> 'browser_language' IN (?) AND status IN (?)", ['en'], [1, 2])
|
||||||
expect(result[:count][:all_count]).to be conversations.count
|
expect(result[:count][:all_count]).to be conversations.count
|
||||||
end
|
end
|
||||||
@@ -156,7 +155,7 @@ describe Conversations::FilterService do
|
|||||||
create(:conversation, account: account, inbox: inbox, assignee: user_1, campaign_id: campaign_1.id,
|
create(:conversation, account: account, inbox: inbox, assignee: user_1, campaign_id: campaign_1.id,
|
||||||
status: 'pending', additional_attributes: { 'browser_language': 'tr' })
|
status: 'pending', additional_attributes: { 'browser_language': 'tr' })
|
||||||
|
|
||||||
result = filter_service.new(params, user_1).perform
|
result = filter_service.new(params, user_1, account).perform
|
||||||
expect(result[:count][:all_count]).to be 2
|
expect(result[:count][:all_count]).to be 2
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -174,7 +173,7 @@ describe Conversations::FilterService do
|
|||||||
create(:conversation, account: account, inbox: inbox, assignee: user_1, campaign_id: campaign_1.id,
|
create(:conversation, account: account, inbox: inbox, assignee: user_1, campaign_id: campaign_1.id,
|
||||||
status: 'pending', additional_attributes: { 'browser_language': 'tr' })
|
status: 'pending', additional_attributes: { 'browser_language': 'tr' })
|
||||||
|
|
||||||
result = filter_service.new(params, user_1).perform
|
result = filter_service.new(params, user_1, account).perform
|
||||||
|
|
||||||
expect(result[:count][:all_count]).to be 1
|
expect(result[:count][:all_count]).to be 1
|
||||||
expect(result[:conversations].first.additional_attributes['browser_language']).to eq 'fr'
|
expect(result[:conversations].first.additional_attributes['browser_language']).to eq 'fr'
|
||||||
@@ -184,7 +183,7 @@ describe Conversations::FilterService do
|
|||||||
payload = [{ attribute_key: 'conversation_type', filter_operator: 'not_equal_to', values: 'platinum', query_operator: nil,
|
payload = [{ attribute_key: 'conversation_type', filter_operator: 'not_equal_to', values: 'platinum', query_operator: nil,
|
||||||
custom_attribute_type: 'conversation_attribute' }.with_indifferent_access]
|
custom_attribute_type: 'conversation_attribute' }.with_indifferent_access]
|
||||||
params[:payload] = payload
|
params[:payload] = payload
|
||||||
result = filter_service.new(params, user_1).perform
|
result = filter_service.new(params, user_1, account).perform
|
||||||
conversations = Conversation.where(
|
conversations = Conversation.where(
|
||||||
"custom_attributes ->> 'conversation_type' NOT IN (?) OR custom_attributes ->> 'conversation_type' IS NULL", ['platinum']
|
"custom_attributes ->> 'conversation_type' NOT IN (?) OR custom_attributes ->> 'conversation_type' IS NULL", ['platinum']
|
||||||
)
|
)
|
||||||
@@ -213,7 +212,7 @@ describe Conversations::FilterService do
|
|||||||
query_operator: nil
|
query_operator: nil
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
]
|
]
|
||||||
result = filter_service.new(params, user_1).perform
|
result = filter_service.new(params, user_1, account).perform
|
||||||
expect(result[:count][:all_count]).to be 1
|
expect(result[:count][:all_count]).to be 1
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -237,7 +236,7 @@ describe Conversations::FilterService do
|
|||||||
custom_attribute_type: ''
|
custom_attribute_type: ''
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
]
|
]
|
||||||
result = filter_service.new(params, user_1).perform
|
result = filter_service.new(params, user_1, account).perform
|
||||||
|
|
||||||
expect(result[:count][:all_count]).to be 2
|
expect(result[:count][:all_count]).to be 2
|
||||||
expect(result[:conversations].pluck(:campaign_id).sort).to eq [campaign_2.id, campaign_1.id].sort
|
expect(result[:conversations].pluck(:campaign_id).sort).to eq [campaign_2.id, campaign_1.id].sort
|
||||||
@@ -264,7 +263,7 @@ describe Conversations::FilterService do
|
|||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
]
|
]
|
||||||
|
|
||||||
expect { filter_service.new(params, user_1).perform }.to raise_error(CustomExceptions::CustomFilter::InvalidQueryOperator)
|
expect { filter_service.new(params, user_1, account).perform }.to raise_error(CustomExceptions::CustomFilter::InvalidQueryOperator)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -296,7 +295,7 @@ describe Conversations::FilterService do
|
|||||||
query_operator: nil
|
query_operator: nil
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
]
|
]
|
||||||
result = filter_service.new(params, user_1).perform
|
result = filter_service.new(params, user_1, account).perform
|
||||||
expect(result[:conversations].length).to be 1
|
expect(result[:conversations].length).to be 1
|
||||||
expect(result[:conversations][0][:id]).to be user_2_assigned_conversation.id
|
expect(result[:conversations][0][:id]).to be user_2_assigned_conversation.id
|
||||||
end
|
end
|
||||||
@@ -324,7 +323,7 @@ describe Conversations::FilterService do
|
|||||||
query_operator: nil
|
query_operator: nil
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
]
|
]
|
||||||
result = filter_service.new(params, user_1).perform
|
result = filter_service.new(params, user_1, account).perform
|
||||||
expect(result[:conversations].length).to be 1
|
expect(result[:conversations].length).to be 1
|
||||||
expect(result[:conversations][0][:id]).to be user_2_assigned_conversation.id
|
expect(result[:conversations][0][:id]).to be user_2_assigned_conversation.id
|
||||||
end
|
end
|
||||||
@@ -346,7 +345,7 @@ describe Conversations::FilterService do
|
|||||||
custom_attribute_type: ''
|
custom_attribute_type: ''
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
]
|
]
|
||||||
result = filter_service.new(params, user_1).perform
|
result = filter_service.new(params, user_1, account).perform
|
||||||
expect(result[:conversations].length).to be 1
|
expect(result[:conversations].length).to be 1
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -367,7 +366,7 @@ describe Conversations::FilterService do
|
|||||||
custom_attribute_type: nil
|
custom_attribute_type: nil
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
]
|
]
|
||||||
result = filter_service.new(params, user_1).perform
|
result = filter_service.new(params, user_1, account).perform
|
||||||
expect(result[:conversations].length).to be 1
|
expect(result[:conversations].length).to be 1
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -393,7 +392,7 @@ describe Conversations::FilterService do
|
|||||||
custom_attribute_type: ''
|
custom_attribute_type: ''
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
]
|
]
|
||||||
result = filter_service.new(params, user_1).perform
|
result = filter_service.new(params, user_1, account).perform
|
||||||
expect(result[:conversations].length).to be 1
|
expect(result[:conversations].length).to be 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -413,7 +412,7 @@ describe Conversations::FilterService do
|
|||||||
custom_attribute_type: ''
|
custom_attribute_type: ''
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
]
|
]
|
||||||
result = filter_service.new(params, user_1).perform
|
result = filter_service.new(params, user_1, account).perform
|
||||||
expected_count = Conversation.where('created_at > ?', DateTime.parse('2022-01-20')).count
|
expected_count = Conversation.where('created_at > ?', DateTime.parse('2022-01-20')).count
|
||||||
expect(result[:conversations].length).to be expected_count
|
expect(result[:conversations].length).to be expected_count
|
||||||
end
|
end
|
||||||
@@ -435,7 +434,7 @@ describe Conversations::FilterService do
|
|||||||
custom_attribute_type: ''
|
custom_attribute_type: ''
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
]
|
]
|
||||||
result = filter_service.new(params, user_1).perform
|
result = filter_service.new(params, user_1, account).perform
|
||||||
expected_count = Conversation.where("created_at > ? AND custom_attributes->>'conversation_type' = ?", DateTime.parse('2022-01-20'),
|
expected_count = Conversation.where("created_at > ? AND custom_attributes->>'conversation_type' = ?", DateTime.parse('2022-01-20'),
|
||||||
'platinum').count
|
'platinum').count
|
||||||
|
|
||||||
@@ -471,7 +470,7 @@ describe Conversations::FilterService do
|
|||||||
expected_count = Conversation.where("last_activity_at < ? AND custom_attributes->>'conversation_type' = ?", (Time.zone.today - 3.days),
|
expected_count = Conversation.where("last_activity_at < ? AND custom_attributes->>'conversation_type' = ?", (Time.zone.today - 3.days),
|
||||||
'platinum').count
|
'platinum').count
|
||||||
|
|
||||||
result = filter_service.new(params, user_1).perform
|
result = filter_service.new(params, user_1, account).perform
|
||||||
expect(result[:conversations].length).to be expected_count
|
expect(result[:conversations].length).to be expected_count
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -488,7 +487,7 @@ describe Conversations::FilterService do
|
|||||||
|
|
||||||
expected_count = Conversation.where('last_activity_at < ?', (Time.zone.today - 2.days)).count
|
expected_count = Conversation.where('last_activity_at < ?', (Time.zone.today - 2.days)).count
|
||||||
|
|
||||||
result = filter_service.new(params, user_1).perform
|
result = filter_service.new(params, user_1, account).perform
|
||||||
expect(result[:conversations].length).to be expected_count
|
expect(result[:conversations].length).to be expected_count
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -522,240 +521,35 @@ describe Conversations::FilterService do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'Frontend alignment tests' do
|
describe '#base_relation' do
|
||||||
let!(:account) { create(:account) }
|
let!(:account) { create(:account) }
|
||||||
let!(:user_1) { create(:user, account: account) }
|
let!(:user_1) { create(:user, account: account, role: :agent) }
|
||||||
let!(:inbox) { create(:inbox, account: account) }
|
let!(:admin) { create(:user, account: account, role: :administrator) }
|
||||||
|
let!(:inbox_1) { create(:inbox, account: account) }
|
||||||
|
let!(:inbox_2) { create(:inbox, account: account) }
|
||||||
let!(:params) { { payload: [], page: 1 } }
|
let!(:params) { { payload: [], page: 1 } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
account.conversations.destroy_all
|
account.conversations.destroy_all
|
||||||
|
|
||||||
|
# Make user_1 a regular agent with access to inbox_1 only
|
||||||
|
create(:inbox_member, user: user_1, inbox: inbox_1)
|
||||||
|
|
||||||
|
# Create conversations in both inboxes
|
||||||
|
create(:conversation, account: account, inbox: inbox_1)
|
||||||
|
create(:conversation, account: account, inbox: inbox_2)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with A AND B OR C filter chain' do
|
it 'returns all conversations for administrators, even for inboxes they are not members of' do
|
||||||
let(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user_1) }
|
service = filter_service.new(params, admin, account)
|
||||||
let(:filter_payload) do
|
result = service.perform
|
||||||
[
|
expect(result[:conversations].count).to eq 2
|
||||||
{
|
|
||||||
attribute_key: 'status',
|
|
||||||
filter_operator: 'equal_to',
|
|
||||||
values: ['open'],
|
|
||||||
query_operator: 'AND'
|
|
||||||
}.with_indifferent_access,
|
|
||||||
{
|
|
||||||
attribute_key: 'priority',
|
|
||||||
filter_operator: 'equal_to',
|
|
||||||
values: ['urgent'],
|
|
||||||
query_operator: 'OR'
|
|
||||||
}.with_indifferent_access,
|
|
||||||
{
|
|
||||||
attribute_key: 'display_id',
|
|
||||||
filter_operator: 'equal_to',
|
|
||||||
values: ['12345'],
|
|
||||||
query_operator: nil
|
|
||||||
}.with_indifferent_access
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
conversation.update!(
|
|
||||||
status: 'open',
|
|
||||||
priority: 'urgent',
|
|
||||||
display_id: '12345',
|
|
||||||
additional_attributes: { 'browser_language': 'en' }
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'matches when all conditions are true' do
|
|
||||||
params[:payload] = filter_payload
|
|
||||||
result = described_class.new(params, user_1).perform
|
|
||||||
expect(result[:conversations].length).to be 1
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'matches when first condition is false but third is true' do
|
|
||||||
conversation.update!(status: 'resolved', priority: 'urgent', display_id: '12345')
|
|
||||||
params[:payload] = filter_payload
|
|
||||||
result = described_class.new(params, user_1).perform
|
|
||||||
expect(result[:conversations].length).to be 1
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'matches when first and second condition is false but third is true' do
|
|
||||||
conversation.update!(status: 'resolved', priority: 'low', display_id: '12345')
|
|
||||||
params[:payload] = filter_payload
|
|
||||||
result = described_class.new(params, user_1).perform
|
|
||||||
expect(result[:conversations].length).to be 1
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not match when all conditions are false' do
|
|
||||||
conversation.update!(status: 'resolved', priority: 'low', display_id: '67890')
|
|
||||||
params[:payload] = filter_payload
|
|
||||||
result = described_class.new(params, user_1).perform
|
|
||||||
expect(result[:conversations].length).to be 0
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with A OR B AND C filter chain' do
|
it 'filters conversations by inbox membership for non-administrators' do
|
||||||
let(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user_1) }
|
service = filter_service.new(params, user_1, account)
|
||||||
let(:filter_payload) do
|
result = service.perform
|
||||||
[
|
expect(result[:conversations].count).to eq 1
|
||||||
{
|
|
||||||
attribute_key: 'status',
|
|
||||||
filter_operator: 'equal_to',
|
|
||||||
values: ['open'],
|
|
||||||
query_operator: 'OR'
|
|
||||||
}.with_indifferent_access,
|
|
||||||
{
|
|
||||||
attribute_key: 'priority',
|
|
||||||
filter_operator: 'equal_to',
|
|
||||||
values: ['low'],
|
|
||||||
query_operator: 'AND'
|
|
||||||
}.with_indifferent_access,
|
|
||||||
{
|
|
||||||
attribute_key: 'display_id',
|
|
||||||
filter_operator: 'equal_to',
|
|
||||||
values: ['67890'],
|
|
||||||
query_operator: nil
|
|
||||||
}.with_indifferent_access
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
conversation.update!(
|
|
||||||
status: 'open',
|
|
||||||
priority: 'urgent',
|
|
||||||
display_id: '12345',
|
|
||||||
additional_attributes: { 'browser_language': 'en' }
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'matches when first condition is true' do
|
|
||||||
params[:payload] = filter_payload
|
|
||||||
result = described_class.new(params, user_1).perform
|
|
||||||
expect(result[:conversations].length).to be 1
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'matches when second and third conditions are true' do
|
|
||||||
conversation.update!(status: 'resolved', priority: 'low', display_id: '67890')
|
|
||||||
params[:payload] = filter_payload
|
|
||||||
result = described_class.new(params, user_1).perform
|
|
||||||
expect(result[:conversations].length).to be 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with complex filter chain A AND B OR C AND D' do
|
|
||||||
let(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user_1) }
|
|
||||||
let(:filter_payload) do
|
|
||||||
[
|
|
||||||
{
|
|
||||||
attribute_key: 'status',
|
|
||||||
filter_operator: 'equal_to',
|
|
||||||
values: ['open'],
|
|
||||||
query_operator: 'AND'
|
|
||||||
}.with_indifferent_access,
|
|
||||||
{
|
|
||||||
attribute_key: 'priority',
|
|
||||||
filter_operator: 'equal_to',
|
|
||||||
values: ['urgent'],
|
|
||||||
query_operator: 'OR'
|
|
||||||
}.with_indifferent_access,
|
|
||||||
{
|
|
||||||
attribute_key: 'display_id',
|
|
||||||
filter_operator: 'equal_to',
|
|
||||||
values: ['67890'],
|
|
||||||
query_operator: 'AND'
|
|
||||||
}.with_indifferent_access,
|
|
||||||
{
|
|
||||||
attribute_key: 'browser_language',
|
|
||||||
filter_operator: 'equal_to',
|
|
||||||
values: ['tr'],
|
|
||||||
query_operator: nil
|
|
||||||
}.with_indifferent_access
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
conversation.update!(
|
|
||||||
status: 'open',
|
|
||||||
priority: 'urgent',
|
|
||||||
display_id: '12345',
|
|
||||||
additional_attributes: { 'browser_language': 'en' },
|
|
||||||
custom_attributes: { conversation_type: 'platinum' }
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'matches when first two conditions are true' do
|
|
||||||
params[:payload] = filter_payload
|
|
||||||
result = described_class.new(params, user_1).perform
|
|
||||||
expect(result[:conversations].length).to be 1
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'matches when last two conditions are true' do
|
|
||||||
conversation.update!(
|
|
||||||
status: 'resolved',
|
|
||||||
priority: 'low',
|
|
||||||
display_id: '67890',
|
|
||||||
additional_attributes: { 'browser_language': 'tr' }
|
|
||||||
)
|
|
||||||
params[:payload] = filter_payload
|
|
||||||
result = described_class.new(params, user_1).perform
|
|
||||||
expect(result[:conversations].length).to be 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with mixed operators filter chain' do
|
|
||||||
let(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user_1) }
|
|
||||||
let(:filter_payload) do
|
|
||||||
[
|
|
||||||
{
|
|
||||||
attribute_key: 'status',
|
|
||||||
filter_operator: 'equal_to',
|
|
||||||
values: ['open'],
|
|
||||||
query_operator: 'AND'
|
|
||||||
}.with_indifferent_access,
|
|
||||||
{
|
|
||||||
attribute_key: 'priority',
|
|
||||||
filter_operator: 'equal_to',
|
|
||||||
values: ['urgent'],
|
|
||||||
query_operator: 'OR'
|
|
||||||
}.with_indifferent_access,
|
|
||||||
{
|
|
||||||
attribute_key: 'display_id',
|
|
||||||
filter_operator: 'equal_to',
|
|
||||||
values: ['67890'],
|
|
||||||
query_operator: 'AND'
|
|
||||||
}.with_indifferent_access,
|
|
||||||
{
|
|
||||||
attribute_key: 'conversation_type',
|
|
||||||
filter_operator: 'equal_to',
|
|
||||||
values: ['platinum'],
|
|
||||||
custom_attribute_type: '',
|
|
||||||
query_operator: nil
|
|
||||||
}.with_indifferent_access
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
conversation.update!(
|
|
||||||
status: 'open',
|
|
||||||
priority: 'urgent',
|
|
||||||
display_id: '12345',
|
|
||||||
additional_attributes: { 'browser_language': 'en' },
|
|
||||||
custom_attributes: { conversation_type: 'platinum' }
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'matches when all conditions in the chain are true' do
|
|
||||||
params[:payload] = filter_payload
|
|
||||||
result = described_class.new(params, user_1).perform
|
|
||||||
expect(result[:conversations].length).to be 1
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not match when the last condition is false' do
|
|
||||||
conversation.update!(custom_attributes: { conversation_type: 'silver' })
|
|
||||||
params[:payload] = filter_payload
|
|
||||||
result = described_class.new(params, user_1).perform
|
|
||||||
expect(result[:conversations].length).to be 1
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Conversations::PermissionFilterService do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let!(:conversation) { create(:conversation, account: account, inbox: inbox) }
|
||||||
|
let!(:another_conversation) { create(:conversation, account: account, inbox: inbox) }
|
||||||
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||||
|
let(:agent) { create(:user, account: account, role: :agent) }
|
||||||
|
let!(:inbox) { create(:inbox, account: account) }
|
||||||
|
|
||||||
|
# This inbox_member is used to establish the agent's access to the inbox
|
||||||
|
before { create(:inbox_member, user: agent, inbox: inbox) }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
context 'when user is an administrator' do
|
||||||
|
it 'returns all conversations' do
|
||||||
|
result = described_class.new(
|
||||||
|
account.conversations,
|
||||||
|
admin,
|
||||||
|
account
|
||||||
|
).perform
|
||||||
|
|
||||||
|
expect(result).to include(conversation)
|
||||||
|
expect(result).to include(another_conversation)
|
||||||
|
expect(result.count).to eq(2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is an agent' do
|
||||||
|
it 'returns all conversations with no further filtering' do
|
||||||
|
inbox_ids = agent.inboxes.where(account_id: account.id).pluck(:id)
|
||||||
|
|
||||||
|
# The base implementation returns all conversations
|
||||||
|
# expecting the caller to filter by assigned inboxes
|
||||||
|
result = described_class.new(
|
||||||
|
account.conversations.where(inbox_id: inbox_ids),
|
||||||
|
agent,
|
||||||
|
account
|
||||||
|
).perform
|
||||||
|
|
||||||
|
expect(result).to include(conversation)
|
||||||
|
expect(result).to include(another_conversation)
|
||||||
|
expect(result.count).to eq(2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user