307 lines
8.6 KiB
Ruby
307 lines
8.6 KiB
Ruby
# 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
|