Files
call-center/tests/test_operator_core.py
T

323 lines
10 KiB
Python

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