491 lines
13 KiB
Python
491 lines
13 KiB
Python
|
|
"""
|
||
|
|
Test script for Ticket Module
|
||
|
|
==============================
|
||
|
|
|
||
|
|
Tester database schema, funktioner, constraints og services.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import asyncio
|
||
|
|
import sys
|
||
|
|
from datetime import date, datetime
|
||
|
|
from decimal import Decimal
|
||
|
|
|
||
|
|
# Add parent directory to path
|
||
|
|
sys.path.insert(0, '/app')
|
||
|
|
|
||
|
|
# Initialize database pool before importing services
|
||
|
|
from app.core.database import init_db, execute_query, execute_insert, execute_update
|
||
|
|
from app.ticket.backend.ticket_service import TicketService
|
||
|
|
from app.ticket.backend.models import (
|
||
|
|
TTicketCreate,
|
||
|
|
TicketStatus,
|
||
|
|
TicketPriority,
|
||
|
|
TicketSource,
|
||
|
|
WorkType,
|
||
|
|
BillingMethod
|
||
|
|
)
|
||
|
|
|
||
|
|
def print_test(name: str, passed: bool, details: str = ""):
|
||
|
|
"""Print test result"""
|
||
|
|
emoji = "✅" if passed else "❌"
|
||
|
|
print(f"{emoji} {name}")
|
||
|
|
if details:
|
||
|
|
print(f" {details}")
|
||
|
|
print()
|
||
|
|
|
||
|
|
|
||
|
|
def test_tables_exist():
|
||
|
|
"""Test 1: Check all tables are created"""
|
||
|
|
print("=" * 60)
|
||
|
|
print("TEST 1: Database Tables")
|
||
|
|
print("=" * 60)
|
||
|
|
|
||
|
|
tables = [
|
||
|
|
'tticket_metadata',
|
||
|
|
'tticket_tickets',
|
||
|
|
'tticket_comments',
|
||
|
|
'tticket_attachments',
|
||
|
|
'tticket_worklog',
|
||
|
|
'tticket_prepaid_cards',
|
||
|
|
'tticket_prepaid_transactions',
|
||
|
|
'tticket_email_log',
|
||
|
|
'tticket_audit_log'
|
||
|
|
]
|
||
|
|
|
||
|
|
for table in tables:
|
||
|
|
result = execute_query(
|
||
|
|
"""
|
||
|
|
SELECT EXISTS (
|
||
|
|
SELECT FROM information_schema.tables
|
||
|
|
WHERE table_name = %s
|
||
|
|
)
|
||
|
|
""",
|
||
|
|
(table,),
|
||
|
|
fetchone=True
|
||
|
|
)
|
||
|
|
print_test(f"Table '{table}' exists", result['exists'])
|
||
|
|
|
||
|
|
# Check views
|
||
|
|
views = [
|
||
|
|
'tticket_open_tickets',
|
||
|
|
'tticket_billable_worklog',
|
||
|
|
'tticket_prepaid_balances',
|
||
|
|
'tticket_stats_by_status'
|
||
|
|
]
|
||
|
|
|
||
|
|
for view in views:
|
||
|
|
result = execute_query(
|
||
|
|
"""
|
||
|
|
SELECT EXISTS (
|
||
|
|
SELECT FROM information_schema.views
|
||
|
|
WHERE table_name = %s
|
||
|
|
)
|
||
|
|
""",
|
||
|
|
(view,),
|
||
|
|
fetchone=True
|
||
|
|
)
|
||
|
|
print_test(f"View '{view}' exists", result['exists'])
|
||
|
|
|
||
|
|
|
||
|
|
def test_ticket_number_generation():
|
||
|
|
"""Test 2: Auto-generate ticket number"""
|
||
|
|
print("=" * 60)
|
||
|
|
print("TEST 2: Ticket Number Generation")
|
||
|
|
print("=" * 60)
|
||
|
|
|
||
|
|
# Insert ticket without ticket_number (should auto-generate)
|
||
|
|
ticket_id = execute_insert(
|
||
|
|
"""
|
||
|
|
INSERT INTO tticket_tickets (subject, description, status)
|
||
|
|
VALUES (%s, %s, %s)
|
||
|
|
""",
|
||
|
|
("Test Ticket", "Testing auto-generation", "open")
|
||
|
|
)
|
||
|
|
|
||
|
|
ticket = execute_query(
|
||
|
|
"SELECT ticket_number FROM tticket_tickets WHERE id = %s",
|
||
|
|
(ticket_id,),
|
||
|
|
fetchone=True
|
||
|
|
)
|
||
|
|
|
||
|
|
# Check format: TKT-YYYYMMDD-XXX
|
||
|
|
import re
|
||
|
|
pattern = r'TKT-\d{8}-\d{3}'
|
||
|
|
matches = re.match(pattern, ticket['ticket_number'])
|
||
|
|
|
||
|
|
print_test(
|
||
|
|
"Ticket number auto-generated",
|
||
|
|
matches is not None,
|
||
|
|
f"Generated: {ticket['ticket_number']}"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Test generating multiple tickets
|
||
|
|
ticket_id2 = execute_insert(
|
||
|
|
"""
|
||
|
|
INSERT INTO tticket_tickets (subject, status)
|
||
|
|
VALUES (%s, %s)
|
||
|
|
""",
|
||
|
|
("Test Ticket 2", "open")
|
||
|
|
)
|
||
|
|
|
||
|
|
ticket2 = execute_query(
|
||
|
|
"SELECT ticket_number FROM tticket_tickets WHERE id = %s",
|
||
|
|
(ticket_id2,),
|
||
|
|
fetchone=True
|
||
|
|
)
|
||
|
|
|
||
|
|
print_test(
|
||
|
|
"Sequential ticket numbers",
|
||
|
|
ticket2['ticket_number'] > ticket['ticket_number'],
|
||
|
|
f"First: {ticket['ticket_number']}, Second: {ticket2['ticket_number']}"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_prepaid_card_constraints():
|
||
|
|
"""Test 3: Prepaid card constraints (1 active per customer)"""
|
||
|
|
print("=" * 60)
|
||
|
|
print("TEST 3: Prepaid Card Constraints")
|
||
|
|
print("=" * 60)
|
||
|
|
|
||
|
|
# Create first active card for customer 1
|
||
|
|
card1_id = execute_insert(
|
||
|
|
"""
|
||
|
|
INSERT INTO tticket_prepaid_cards
|
||
|
|
(customer_id, purchased_hours, price_per_hour, total_amount, status)
|
||
|
|
VALUES (%s, %s, %s, %s, %s)
|
||
|
|
""",
|
||
|
|
(1, Decimal('10.0'), Decimal('850.0'), Decimal('8500.0'), 'active')
|
||
|
|
)
|
||
|
|
|
||
|
|
card1 = execute_query(
|
||
|
|
"SELECT card_number, remaining_hours FROM tticket_prepaid_cards WHERE id = %s",
|
||
|
|
(card1_id,),
|
||
|
|
fetchone=True
|
||
|
|
)
|
||
|
|
|
||
|
|
print_test(
|
||
|
|
"First prepaid card created",
|
||
|
|
card1 is not None,
|
||
|
|
f"Card: {card1['card_number']}, Balance: {card1['remaining_hours']} hours"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Try to create second active card for same customer (should fail due to UNIQUE constraint)
|
||
|
|
try:
|
||
|
|
card2_id = execute_insert(
|
||
|
|
"""
|
||
|
|
INSERT INTO tticket_prepaid_cards
|
||
|
|
(customer_id, purchased_hours, price_per_hour, total_amount, status)
|
||
|
|
VALUES (%s, %s, %s, %s, %s)
|
||
|
|
""",
|
||
|
|
(1, Decimal('20.0'), Decimal('850.0'), Decimal('17000.0'), 'active')
|
||
|
|
)
|
||
|
|
print_test(
|
||
|
|
"Cannot create 2nd active card (UNIQUE constraint)",
|
||
|
|
False,
|
||
|
|
"ERROR: Constraint not enforced!"
|
||
|
|
)
|
||
|
|
except Exception as e:
|
||
|
|
print_test(
|
||
|
|
"Cannot create 2nd active card (UNIQUE constraint)",
|
||
|
|
"unique" in str(e).lower() or "duplicate" in str(e).lower(),
|
||
|
|
f"Constraint enforced: {str(e)[:80]}"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Create inactive card for same customer (should work)
|
||
|
|
card3_id = execute_insert(
|
||
|
|
"""
|
||
|
|
INSERT INTO tticket_prepaid_cards
|
||
|
|
(customer_id, purchased_hours, price_per_hour, total_amount, status)
|
||
|
|
VALUES (%s, %s, %s, %s, %s)
|
||
|
|
""",
|
||
|
|
(1, Decimal('5.0'), Decimal('850.0'), Decimal('4250.0'), 'depleted')
|
||
|
|
)
|
||
|
|
|
||
|
|
print_test(
|
||
|
|
"Can create inactive card for same customer",
|
||
|
|
card3_id is not None,
|
||
|
|
"Multiple inactive cards allowed"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_generated_column():
|
||
|
|
"""Test 4: Generated column (remaining_hours)"""
|
||
|
|
print("=" * 60)
|
||
|
|
print("TEST 4: Generated Column (remaining_hours)")
|
||
|
|
print("=" * 60)
|
||
|
|
|
||
|
|
# Create card
|
||
|
|
card_id = execute_insert(
|
||
|
|
"""
|
||
|
|
INSERT INTO tticket_prepaid_cards
|
||
|
|
(customer_id, purchased_hours, price_per_hour, total_amount, status)
|
||
|
|
VALUES (%s, %s, %s, %s, %s)
|
||
|
|
""",
|
||
|
|
(2, Decimal('20.0'), Decimal('850.0'), Decimal('17000.0'), 'active')
|
||
|
|
)
|
||
|
|
|
||
|
|
card = execute_query(
|
||
|
|
"SELECT purchased_hours, used_hours, remaining_hours FROM tticket_prepaid_cards WHERE id = %s",
|
||
|
|
(card_id,),
|
||
|
|
fetchone=True
|
||
|
|
)
|
||
|
|
|
||
|
|
print_test(
|
||
|
|
"Initial remaining_hours calculated",
|
||
|
|
card['remaining_hours'] == Decimal('20.0'),
|
||
|
|
f"Purchased: {card['purchased_hours']}, Used: {card['used_hours']}, Remaining: {card['remaining_hours']}"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Use some hours
|
||
|
|
execute_update(
|
||
|
|
"UPDATE tticket_prepaid_cards SET used_hours = %s WHERE id = %s",
|
||
|
|
(Decimal('5.5'), card_id)
|
||
|
|
)
|
||
|
|
|
||
|
|
card = execute_query(
|
||
|
|
"SELECT purchased_hours, used_hours, remaining_hours FROM tticket_prepaid_cards WHERE id = %s",
|
||
|
|
(card_id,),
|
||
|
|
fetchone=True
|
||
|
|
)
|
||
|
|
|
||
|
|
expected = Decimal('14.5')
|
||
|
|
print_test(
|
||
|
|
"remaining_hours auto-updates",
|
||
|
|
card['remaining_hours'] == expected,
|
||
|
|
f"After using 5.5h: {card['remaining_hours']}h (expected: {expected}h)"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_ticket_service():
|
||
|
|
"""Test 5: Ticket Service business logic"""
|
||
|
|
print("=" * 60)
|
||
|
|
print("TEST 5: Ticket Service")
|
||
|
|
print("=" * 60)
|
||
|
|
|
||
|
|
# Test create ticket
|
||
|
|
ticket_data = TTicketCreate(
|
||
|
|
subject="Test Service Ticket",
|
||
|
|
description="Testing TicketService",
|
||
|
|
priority=TicketPriority.HIGH,
|
||
|
|
customer_id=1,
|
||
|
|
source=TicketSource.MANUAL
|
||
|
|
)
|
||
|
|
|
||
|
|
ticket = TicketService.create_ticket(ticket_data, user_id=1)
|
||
|
|
|
||
|
|
print_test(
|
||
|
|
"TicketService.create_ticket() works",
|
||
|
|
ticket is not None and 'id' in ticket,
|
||
|
|
f"Created ticket: {ticket['ticket_number']}"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Test status transition validation
|
||
|
|
is_valid, error = TicketService.validate_status_transition('open', 'in_progress')
|
||
|
|
print_test(
|
||
|
|
"Valid status transition (open → in_progress)",
|
||
|
|
is_valid and error is None,
|
||
|
|
"Transition allowed"
|
||
|
|
)
|
||
|
|
|
||
|
|
is_valid, error = TicketService.validate_status_transition('closed', 'open')
|
||
|
|
print_test(
|
||
|
|
"Invalid status transition (closed → open)",
|
||
|
|
not is_valid and error is not None,
|
||
|
|
f"Transition blocked: {error}"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Test update status
|
||
|
|
try:
|
||
|
|
updated = TicketService.update_ticket_status(
|
||
|
|
ticket['id'],
|
||
|
|
'in_progress',
|
||
|
|
user_id=1,
|
||
|
|
note="Starting work"
|
||
|
|
)
|
||
|
|
|
||
|
|
print_test(
|
||
|
|
"TicketService.update_ticket_status() works",
|
||
|
|
updated['status'] == 'in_progress',
|
||
|
|
f"Status changed: {ticket['status']} → {updated['status']}"
|
||
|
|
)
|
||
|
|
except Exception as e:
|
||
|
|
print_test(
|
||
|
|
"TicketService.update_ticket_status() works",
|
||
|
|
False,
|
||
|
|
f"ERROR: {e}"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Test add comment
|
||
|
|
try:
|
||
|
|
comment = TicketService.add_comment(
|
||
|
|
ticket['id'],
|
||
|
|
"Test comment from service",
|
||
|
|
user_id=1,
|
||
|
|
is_internal=False
|
||
|
|
)
|
||
|
|
|
||
|
|
print_test(
|
||
|
|
"TicketService.add_comment() works",
|
||
|
|
comment is not None and 'id' in comment,
|
||
|
|
f"Comment added (ID: {comment['id']})"
|
||
|
|
)
|
||
|
|
except Exception as e:
|
||
|
|
print_test(
|
||
|
|
"TicketService.add_comment() works",
|
||
|
|
False,
|
||
|
|
f"ERROR: {e}"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_worklog_creation():
|
||
|
|
"""Test 6: Worklog creation"""
|
||
|
|
print("=" * 60)
|
||
|
|
print("TEST 6: Worklog")
|
||
|
|
print("=" * 60)
|
||
|
|
|
||
|
|
# Get a ticket
|
||
|
|
ticket = execute_query(
|
||
|
|
"SELECT id FROM tticket_tickets LIMIT 1",
|
||
|
|
fetchone=True
|
||
|
|
)
|
||
|
|
|
||
|
|
if not ticket:
|
||
|
|
print_test("Worklog creation", False, "No tickets found to test worklog")
|
||
|
|
return
|
||
|
|
|
||
|
|
# Create worklog entry
|
||
|
|
worklog_id = execute_insert(
|
||
|
|
"""
|
||
|
|
INSERT INTO tticket_worklog
|
||
|
|
(ticket_id, work_date, hours, work_type, billing_method, status, user_id)
|
||
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||
|
|
""",
|
||
|
|
(ticket['id'], date.today(), Decimal('2.5'), 'support', 'invoice', 'draft', 1)
|
||
|
|
)
|
||
|
|
|
||
|
|
worklog = execute_query(
|
||
|
|
"SELECT * FROM tticket_worklog WHERE id = %s",
|
||
|
|
(worklog_id,),
|
||
|
|
fetchone=True
|
||
|
|
)
|
||
|
|
|
||
|
|
print_test(
|
||
|
|
"Worklog entry created",
|
||
|
|
worklog is not None,
|
||
|
|
f"ID: {worklog_id}, Hours: {worklog['hours']}, Status: {worklog['status']}"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Check view
|
||
|
|
billable_count = execute_query(
|
||
|
|
"SELECT COUNT(*) as count FROM tticket_billable_worklog WHERE status = 'billable'",
|
||
|
|
fetchone=True
|
||
|
|
)
|
||
|
|
|
||
|
|
print_test(
|
||
|
|
"Billable worklog view accessible",
|
||
|
|
billable_count is not None,
|
||
|
|
f"Found {billable_count['count']} billable entries"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_audit_logging():
|
||
|
|
"""Test 7: Audit logging"""
|
||
|
|
print("=" * 60)
|
||
|
|
print("TEST 7: Audit Logging")
|
||
|
|
print("=" * 60)
|
||
|
|
|
||
|
|
# Check if audit entries were created
|
||
|
|
audit_count = execute_query(
|
||
|
|
"SELECT COUNT(*) as count FROM tticket_audit_log",
|
||
|
|
fetchone=True
|
||
|
|
)
|
||
|
|
|
||
|
|
print_test(
|
||
|
|
"Audit log has entries",
|
||
|
|
audit_count['count'] > 0,
|
||
|
|
f"Found {audit_count['count']} audit entries"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Check recent audit entries
|
||
|
|
recent = execute_query(
|
||
|
|
"""
|
||
|
|
SELECT action, entity_type, created_at
|
||
|
|
FROM tticket_audit_log
|
||
|
|
ORDER BY created_at DESC
|
||
|
|
LIMIT 5
|
||
|
|
"""
|
||
|
|
)
|
||
|
|
|
||
|
|
if recent:
|
||
|
|
print("Recent audit entries:")
|
||
|
|
for entry in recent:
|
||
|
|
print(f" - {entry['action']} on {entry['entity_type']} at {entry['created_at']}")
|
||
|
|
print()
|
||
|
|
|
||
|
|
|
||
|
|
def test_views():
|
||
|
|
"""Test 8: Database views"""
|
||
|
|
print("=" * 60)
|
||
|
|
print("TEST 8: Database Views")
|
||
|
|
print("=" * 60)
|
||
|
|
|
||
|
|
# Test tticket_open_tickets view
|
||
|
|
open_tickets = execute_query(
|
||
|
|
"SELECT ticket_number, comment_count, age_hours FROM tticket_open_tickets LIMIT 5"
|
||
|
|
)
|
||
|
|
|
||
|
|
print_test(
|
||
|
|
"tticket_open_tickets view works",
|
||
|
|
open_tickets is not None,
|
||
|
|
f"Found {len(open_tickets)} open tickets"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Test tticket_stats_by_status view
|
||
|
|
stats = execute_query(
|
||
|
|
"SELECT status, ticket_count FROM tticket_stats_by_status"
|
||
|
|
)
|
||
|
|
|
||
|
|
if stats:
|
||
|
|
print("Ticket statistics by status:")
|
||
|
|
for stat in stats:
|
||
|
|
print(f" - {stat['status']}: {stat['ticket_count']} tickets")
|
||
|
|
print()
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
"""Run all tests"""
|
||
|
|
print("\n")
|
||
|
|
print("🎫 TICKET MODULE TEST SUITE")
|
||
|
|
print("=" * 60)
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Initialize database pool
|
||
|
|
print("🔌 Initializing database connection...")
|
||
|
|
init_db()
|
||
|
|
print("✅ Database connected\n")
|
||
|
|
|
||
|
|
try:
|
||
|
|
test_tables_exist()
|
||
|
|
test_ticket_number_generation()
|
||
|
|
test_prepaid_card_constraints()
|
||
|
|
test_generated_column()
|
||
|
|
test_ticket_service()
|
||
|
|
test_worklog_creation()
|
||
|
|
test_audit_logging()
|
||
|
|
test_views()
|
||
|
|
|
||
|
|
print("=" * 60)
|
||
|
|
print("✅ ALL TESTS COMPLETED")
|
||
|
|
print("=" * 60)
|
||
|
|
print()
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
print(f"\n❌ TEST SUITE FAILED: {e}\n")
|
||
|
|
import traceback
|
||
|
|
traceback.print_exc()
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|