Files
chatwoot/script/assignment_v2/test_helpers.rb
T

307 lines
8.6 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# frozen_string_literal: true
# Shared test helpers for Assignment v2 feature testing
#
# Key Design Decisions & Fixes:
#
# 1. Password Requirements:
# - Users require complex passwords: 'Password123!' (uppercase + special char)
#
# 2. Unique Naming:
# - All entities (inboxes, policies, agents) get unique names using SecureRandom.hex(4)
# - Prevents validation errors across multiple test runs
#
# 3. Conversation Creation:
# - Uses FactoryBot to handle complex associations automatically
# - ContactInbox requires source_id (cannot be blank)
# - FactoryBot factory handles: contact → contact_inbox → conversation chain
#
# 4. Policy Linking:
# - RateLimiter reads from inbox.auto_assignment_config (not policy directly)
# - link_policy_to_inbox syncs policy settings to inbox config
# - Must call inbox.reload after linking to pick up association
#
# 5. Redis Access:
# - Use Redis::Alfred.keys_count() instead of Redis::Alfred.redis.keys.count
# - Use Redis::Alfred.scan_each() instead of Redis::Alfred.redis.keys for cleanup
#
# 6. Assignment Job:
# - Must use keyword argument: perform_now(inbox_id: id)
# - Not positional: perform_now(id) will fail
#
module AssignmentV2TestHelpers
# Default test account ID
DEFAULT_ACCOUNT_ID = 2
# Color codes for terminal output
COLORS = {
green: "\e[32m",
red: "\e[31m",
yellow: "\e[33m",
blue: "\e[34m",
reset: "\e[0m"
}.freeze
def log(msg, prefix: '==>', color: nil)
colored_prefix = color ? "#{COLORS[color]}#{prefix}#{COLORS[:reset]}" : prefix
puts "\n#{colored_prefix} #{msg}"
end
def success(msg)
log(msg, prefix: '✓', color: :green)
end
def error(msg)
log(msg, prefix: '✗', color: :red)
end
def warning(msg)
log(msg, prefix: '⚠', color: :yellow)
end
def info(msg)
log(msg, prefix: '', color: :blue)
end
def section(title)
puts "\n#{'=' * 60}"
puts " #{title}"
puts '=' * 60
end
def assert(condition, success_msg, error_msg)
if condition
success(success_msg)
true
else
error(error_msg)
false
end
end
def assert_equal(actual, expected, description)
if actual == expected
success("#{description}: #{actual}")
true
else
error("#{description}: Expected #{expected}, got #{actual}")
false
end
end
def get_test_account
account = Account.find_by(id: DEFAULT_ACCOUNT_ID)
raise "Account #{DEFAULT_ACCOUNT_ID} not found. Please create it first." unless account
# Enable assignment_v2 feature
account.enable_features('assignment_v2')
account
end
def create_test_inbox(account, name: 'Test Inbox')
unique_name = "#{name} #{SecureRandom.hex(4)}"
channel = Channel::WebWidget.create!(
account: account,
website_url: 'https://test.com',
widget_color: '#0000FF'
)
inbox = account.inboxes.create!(
name: unique_name,
channel: channel,
enable_auto_assignment: true
)
info("Created inbox: #{inbox.name} (ID: #{inbox.id})")
inbox
end
def create_test_policy(account, **options)
defaults = {
name: 'Test Policy',
conversation_priority: 'earliest_created',
enabled: true
}
merged_options = defaults.merge(options)
# Always make name unique
merged_options[:name] = "#{merged_options[:name]} #{SecureRandom.hex(4)}"
policy = account.assignment_policies.create!(merged_options)
info("Created policy: #{policy.name} (ID: #{policy.id})")
policy
end
def link_policy_to_inbox(inbox, policy)
InboxAssignmentPolicy.find_or_create_by!(
inbox: inbox,
assignment_policy: policy
)
# Also set inbox config for rate limiter (RateLimiter reads from inbox.auto_assignment_config)
inbox.update!(
auto_assignment_config: {
'fair_distribution_limit' => policy.fair_distribution_limit,
'fair_distribution_window' => policy.fair_distribution_window,
'conversation_priority' => policy.conversation_priority
}.compact
)
inbox.reload # Reload to pick up the association
info("Linked policy #{policy.id} to inbox #{inbox.id}")
inbox
end
def create_test_agent(account, inbox, name:, email: nil, online: true)
unique_id = SecureRandom.hex(4)
email ||= "#{name.downcase.tr(' ', '_')}_#{unique_id}@test.com"
unique_name = "#{name} #{unique_id}"
user = account.users.create!(
email: email,
password: 'Password123!',
password_confirmation: 'Password123!',
name: unique_name,
confirmed_at: Time.zone.now
)
# Add to inbox
inbox.inbox_members.create!(user: user)
# Set online status
# Note: inbox.available_agents filters by status == 'online', so we set both presence and status
if online
OnlineStatusTracker.update_presence(account.id, 'User', user.id)
OnlineStatusTracker.set_status(account.id, user.id, 'online')
else
# For offline agents, set status to 'offline' (they won't be in available_agents)
OnlineStatusTracker.set_status(account.id, user.id, 'offline')
end
info("Created agent: #{user.name} (#{user.email}, online: #{online})")
user
end
def create_test_conversation(inbox, **options)
contact_name = options.delete(:contact_name) || "Customer #{SecureRandom.hex(4)}"
# Use FactoryBot if available
if defined?(FactoryBot)
FactoryBot.create(
:conversation,
options.merge(
inbox: inbox,
account: inbox.account,
contact: FactoryBot.create(:contact, account: inbox.account, name: contact_name)
)
)
else
# Fallback to manual creation
defaults = {
status: 'open',
assignee_id: nil,
created_at: Time.zone.now,
last_activity_at: Time.zone.now
}
contact = inbox.contacts.create!(
account: inbox.account,
name: contact_name
)
contact_inbox = ContactInbox.create!(
contact: contact,
inbox: inbox,
source_id: "test_#{SecureRandom.uuid}"
)
inbox.conversations.create!(
defaults.merge(options).merge(
account: inbox.account,
contact: contact,
contact_inbox: contact_inbox
)
)
end
end
def create_bulk_conversations(inbox, count:, **options)
conversations = count.times.map do |i|
create_test_conversation(
inbox,
**options, contact_name: "Customer #{i + 1}",
created_at: (count - i).minutes.ago,
last_activity_at: options[:last_activity_at] || (count - i).minutes.ago
)
end
info("Created #{count} test conversations")
conversations
end
def run_assignment(inbox)
info("Running assignment job for inbox #{inbox.id}...")
before_count = inbox.conversations.unassigned.count
AutoAssignment::AssignmentJob.perform_now(inbox_id: inbox.id)
after_count = inbox.conversations.unassigned.count
assigned_count = before_count - after_count
info("Assigned: #{assigned_count} conversations (#{before_count}#{after_count} unassigned)")
assigned_count
end
def show_assignment_distribution(inbox, agents)
log('Assignment Distribution:', color: :blue)
agents.each do |agent|
count = inbox.conversations.where(assignee: agent, status: 'open').count
puts " #{agent.name}: #{count} conversations"
end
unassigned = inbox.conversations.unassigned.count
puts " Unassigned: #{unassigned} conversations"
end
def count_redis_keys(pattern)
Redis::Alfred.keys_count(pattern)
end
def cleanup_redis_keys(pattern)
# Use scan to avoid blocking Redis with KEYS command
Redis::Alfred.scan_each(match: pattern) do |key|
Redis::Alfred.del(key)
end
end
def cleanup_test_data(account, inbox: nil)
if inbox
info("Cleaning up inbox #{inbox.id}...")
inbox.conversations.destroy_all
inbox.inbox_members.destroy_all
InboxAssignmentPolicy.where(inbox: inbox).destroy_all
inbox.destroy
else
info("Cleaning up account #{account.id} test data...")
account.conversations.where('created_at > ?', 1.hour.ago).destroy_all
account.inbox_members.joins(:user).where(users: { email: [/.+@test\.com/] }).destroy_all
account.inboxes.where('name LIKE ?', '%Test%').destroy_all
account.assignment_policies.where('name LIKE ?', '%Test%').destroy_all
account.users.where('email LIKE ?', '%@test.com%').destroy_all
end
success('Cleanup complete')
end
def enterprise_available?
defined?(Enterprise)
end
def skip_if_not_enterprise
return if enterprise_available?
warning('Enterprise features not available, skipping test')
exit(0)
end
end