chore: add scripts to test
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user