from fastapi.testclient import TestClient from services.customer_service.app import app as customer_app from services.interaction_service.app import app as interaction_app from services.ivr_service.app import app as ivr_app from services.routing_service.app import app as routing_app from services.shared.db import get_session from services.shared.sql_models import Queue def test_customer_and_interaction_lifecycle(): customer_client = TestClient(customer_app) interaction_client = TestClient(interaction_app) customer = customer_client.post( "/customers", json={"display_name": "Test Client", "phones": ["+77010000001"], "preferred_phone": "+77010000001"}, ) assert customer.status_code == 200 customer_id = customer.json()["customer_id"] interaction = interaction_client.post( "/interactions", json={ "channel": "voice", "subject": "Need support", "customer_id": customer_id, "queue_id": "q_main", "priority": 2, }, headers={"X-User": "operator", "X-Role": "operator"}, ) assert interaction.status_code == 200 interaction_id = interaction.json()["interaction_id"] assigned = interaction_client.patch( f"/interactions/{interaction_id}/assign", json={"assignee": "operator_a"}, headers={"X-User": "supervisor", "X-Role": "supervisor"}, ) assert assigned.status_code == 200 assert assigned.json()["status"] == "in_progress" escalated = interaction_client.post( f"/interactions/{interaction_id}/escalate", json={"target_queue_id": "line2"}, headers={"X-User": "operator", "X-Role": "operator"}, ) assert escalated.status_code == 200 assert escalated.json()["status"] == "escalated" closed = interaction_client.patch( f"/interactions/{interaction_id}/status", json={"status": "closed"}, headers={"X-User": "operator", "X-Role": "operator"}, ) assert closed.status_code == 200 assert closed.json()["status"] == "closed" timeline = interaction_client.get(f"/interactions/{interaction_id}/timeline") assert timeline.status_code == 200 assert len(timeline.json()["events"]) >= 4 def test_queue_rules_and_route(): routing_client = TestClient(routing_app) created = routing_client.post( "/queues", json={ "name": "General Queue", "description": "Main line", "rules": [ { "channel": "voice", "priority": 2, "strategy": "round_robin", "sla_seconds": 45, } ], }, headers={"X-User": "admin", "X-Role": "admin"}, ) assert created.status_code == 200 queue_id = created.json()["queue_id"] route = routing_client.post( f"/queues/{queue_id}/route?channel=voice&priority=2", headers={"X-User": "supervisor", "X-Role": "supervisor"}, ) assert route.status_code == 200 assert route.json()["sla_seconds"] == 45 def test_list_queues_tolerates_legacy_rules_json_object(): routing_client = TestClient(routing_app) session = get_session() try: session.add( Queue( queue_id="que_legacy_rules", name="Legacy Queue", description="Created before rules_json normalization", rules_json="{}", created_at="2026-03-15T00:00:00+00:00", ) ) session.commit() finally: session.close() listed = routing_client.get( "/queues", headers={"X-User": "admin", "X-Role": "admin"}, ) assert listed.status_code == 200 payload = listed.json() queue = next(item for item in payload if item["queue_id"] == "que_legacy_rules") assert queue["rules"] == [] def test_queue_route_requires_privileged_role(): routing_client = TestClient(routing_app) created = routing_client.post( "/queues", json={ "name": "Guarded Queue", "description": "RBAC", "rules": [ { "channel": "voice", "priority": 3, "strategy": "round_robin", "sla_seconds": 30, } ], }, headers={"X-User": "admin", "X-Role": "admin"}, ) assert created.status_code == 200 denied = routing_client.post( f"/queues/{created.json()['queue_id']}/route?channel=voice&priority=3", headers={"X-User": "operator", "X-Role": "operator"}, ) assert denied.status_code == 403 def test_admin_can_delete_queue(): routing_client = TestClient(routing_app) created = routing_client.post( "/queues", json={ "name": "Disposable Queue", "description": "To delete", "rules": [ { "channel": "voice", "priority": 3, "strategy": "round_robin", "sla_seconds": 30, } ], }, headers={"X-User": "admin", "X-Role": "admin"}, ) assert created.status_code == 200 queue_id = created.json()["queue_id"] deleted = routing_client.delete( f"/queues/{queue_id}", headers={"X-User": "admin", "X-Role": "admin"}, ) assert deleted.status_code == 200 assert deleted.json()["deleted"] is True route = routing_client.post( f"/queues/{queue_id}/route?channel=voice&priority=3", headers={"X-User": "admin", "X-Role": "admin"}, ) assert route.status_code == 404 def test_supervisor_cannot_delete_queue(): routing_client = TestClient(routing_app) created = routing_client.post( "/queues", json={ "name": "Protected Queue", "description": "RBAC delete", "rules": [ { "channel": "voice", "priority": 3, "strategy": "round_robin", "sla_seconds": 30, } ], }, headers={"X-User": "admin", "X-Role": "admin"}, ) assert created.status_code == 200 denied = routing_client.delete( f"/queues/{created.json()['queue_id']}", headers={"X-User": "supervisor", "X-Role": "supervisor"}, ) assert denied.status_code == 403 def test_route_supports_completed_ivr_session_and_rejects_incomplete_session(): routing_client = TestClient(routing_app) ivr_client = TestClient(ivr_app) origin_queue = routing_client.post( "/queues", json={ "name": "IVR Origin Queue", "description": "voice origin", "rules": [ { "channel": "voice", "priority": 3, "strategy": "round_robin", "sla_seconds": 30, } ], }, headers={"X-User": "admin", "X-Role": "admin"}, ) assert origin_queue.status_code == 200 origin_queue_id = origin_queue.json()["queue_id"] resolved_queue = routing_client.post( "/queues", json={ "name": "IVR Resolved Queue", "description": "voice support", "rules": [ { "channel": "voice", "priority": 3, "strategy": "round_robin", "sla_seconds": 55, } ], }, headers={"X-User": "admin", "X-Role": "admin"}, ) assert resolved_queue.status_code == 200 resolved_queue_id = resolved_queue.json()["queue_id"] flow = ivr_client.post( "/ivr/flows", json={ "name": "Route IVR", "description": "route override", "queue_id": origin_queue_id, "entry_node_id": "root", "flow_json": { "nodes": [ { "node_id": "root", "prompt_text": "Press 2 for support", "is_terminal": False, "invalid_target_node_id": "root", "options": [{"digit": "2", "target_node_id": "support"}], }, { "node_id": "support", "prompt_text": "Support queue", "is_terminal": True, "outcome_code": "support_route", "resolved_queue_id": resolved_queue_id, "resolved_queue_code": "support", "options": [], }, ] }, "is_active": True, }, headers={"X-User": "admin", "X-Role": "admin"}, ) assert flow.status_code == 200 active_session = ivr_client.post( "/ivr/sessions/start", json={"call_id": "call_route_ivr", "queue_id": origin_queue_id}, headers={"X-User": "supervisor", "X-Role": "supervisor"}, ) assert active_session.status_code == 200 session_id = active_session.json()["session"]["session_id"] incomplete = routing_client.post( f"/queues/{origin_queue_id}/route?channel=voice&priority=3&ivr_session_id={session_id}", headers={"X-User": "supervisor", "X-Role": "supervisor"}, ) assert incomplete.status_code == 400 stepped = ivr_client.post( f"/ivr/sessions/{session_id}/dtmf", json={"digit": "2"}, headers={"X-User": "supervisor", "X-Role": "supervisor"}, ) assert stepped.status_code == 200 assert stepped.json()["session"]["status"] == "completed" routed = routing_client.post( f"/queues/{origin_queue_id}/route?channel=voice&priority=3&ivr_session_id={session_id}", headers={"X-User": "supervisor", "X-Role": "supervisor"}, ) assert routed.status_code == 200 payload = routed.json() assert payload["queue_id"] == resolved_queue_id assert payload["original_queue_id"] == origin_queue_id assert payload["resolved_queue_id"] == resolved_queue_id assert payload["ivr_session_id"] == session_id assert payload["ivr_outcome_code"] == "support_route" assert payload["sla_seconds"] == 55