""" 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()