"""Integration tests for channels session_router endpoints.""" from __future__ import annotations import httpx import pytest from .conftest import ( BASE_URL, NON_EXISTENT_SESSION_ID, ) pytestmark = [pytest.mark.asyncio, pytest.mark.integration] SESSIONS_URL = f"{BASE_URL}/sessions" # ============================================================================= # === Auth three-tier for list sessions === # ============================================================================= async def test_list_sessions_requires_auth(test_client: httpx.AsyncClient): # Act response = await test_client.get(SESSIONS_URL) # Assert assert response.status_code == 401, response.text async def test_list_sessions_forbids_standard_user(test_client: httpx.AsyncClient, standard_user): # Act response = await test_client.get(SESSIONS_URL, headers=standard_user["headers"]) # Assert assert response.status_code == 403, response.text async def test_admin_can_list_sessions(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get(SESSIONS_URL, headers=admin_headers) # Assert assert response.status_code == 200, response.text payload = response.json() assert payload["success"] is True assert isinstance(payload["data"], dict) assert "items" in payload["data"] # ============================================================================= # === Pagination validation for list sessions === # ============================================================================= async def test_list_sessions_rejects_limit_zero(test_client: httpx.AsyncClient, admin_headers): # Act — limit=0 violates ge=1 response = await test_client.get( SESSIONS_URL, params={"limit": 0}, headers=admin_headers, ) # Assert assert response.status_code == 422, response.text async def test_list_sessions_rejects_limit_over_max(test_client: httpx.AsyncClient, admin_headers): # Act — limit=1001 violates le=1000 response = await test_client.get( SESSIONS_URL, params={"limit": 1001}, headers=admin_headers, ) # Assert assert response.status_code == 422, response.text # ============================================================================= # === GET /sessions/events — SSE real-time event stream (BE-14) === # ============================================================================= async def test_stream_session_events_requires_auth(test_client: httpx.AsyncClient): # Act response = await test_client.get(f"{SESSIONS_URL}/events") # Assert assert response.status_code == 401, response.text async def test_stream_session_events_forbids_standard_user(test_client: httpx.AsyncClient, standard_user): # Act response = await test_client.get(f"{SESSIONS_URL}/events", headers=standard_user["headers"]) # Assert assert response.status_code == 403, response.text async def test_admin_can_open_session_events_stream(test_client: httpx.AsyncClient, admin_headers): # Act — SSE 长连接,使用 stream 上下文仅读取响应头后立即关闭, # 避免等待 15s 心跳或无限流。status_code 与 content-type 在响应头中 # 立即可用,无需消费 body。 async with test_client.stream("GET", f"{SESSIONS_URL}/events", headers=admin_headers) as response: # Assert assert response.status_code == 200, response.text assert response.headers["content-type"].startswith("text/event-stream") async def test_events_static_path_not_captured_as_session_id(test_client: httpx.AsyncClient, admin_headers): # Act — /sessions/events 是静态路径,必须先于 /sessions/{session_id} 声明, # 避免 "events" 被捕获为 session_id。若路由顺序错误,此请求会走 # getSession("events") 并返回 404,而非 SSE 端点的 200。 async with test_client.stream("GET", f"{SESSIONS_URL}/events", headers=admin_headers) as response: # Assert — 命中 SSE 端点返回 200 + text/event-stream,证明静态路径 # 未被动态参数捕获 assert response.status_code == 200, response.text assert response.headers["content-type"].startswith("text/event-stream") # ============================================================================= # === batch-close === # ============================================================================= async def test_batch_close_sessions_requires_auth(test_client: httpx.AsyncClient): # Arrange body = {"session_ids": [NON_EXISTENT_SESSION_ID]} # Act response = await test_client.post(f"{SESSIONS_URL}/batch-close", json=body) # Assert assert response.status_code == 401, response.text async def test_batch_close_sessions_forbids_standard_user(test_client: httpx.AsyncClient, standard_user): # Arrange body = {"session_ids": [NON_EXISTENT_SESSION_ID]} # Act response = await test_client.post( f"{SESSIONS_URL}/batch-close", json=body, headers=standard_user["headers"], ) # Assert assert response.status_code == 403, response.text async def test_batch_close_sessions_rejects_empty_body(test_client: httpx.AsyncClient, admin_headers): # Arrange — session_ids 与 filter 同时为空,dispatch 校验返回 400 body = {"session_ids": None, "filter": None} # Act response = await test_client.post( f"{SESSIONS_URL}/batch-close", json=body, headers=admin_headers, ) # Assert assert response.status_code in (400, 422), response.text async def test_batch_close_sessions_with_non_existent_ids_returns_success( test_client: httpx.AsyncClient, admin_headers ): # Arrange — 不存在的 session_ids,部分成功语义下应返回 200 body = {"session_ids": [NON_EXISTENT_SESSION_ID], "max_count": 100, "reason": "pytest"} # Act response = await test_client.post( f"{SESSIONS_URL}/batch-close", json=body, headers=admin_headers, ) # Assert assert response.status_code == 200, response.text payload = response.json() assert payload["success"] is True # ============================================================================= # === get session detail === # ============================================================================= async def test_get_session_requires_auth(test_client: httpx.AsyncClient): # Act response = await test_client.get(f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}") # Assert assert response.status_code == 401, response.text async def test_get_session_forbids_standard_user(test_client: httpx.AsyncClient, standard_user): # Act response = await test_client.get( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}", headers=standard_user["headers"], ) # Assert assert response.status_code == 403, response.text async def test_get_session_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}", headers=admin_headers, ) # Assert assert response.status_code == 404, response.text # ============================================================================= # === merge session === # ============================================================================= async def test_merge_session_requires_auth(test_client: httpx.AsyncClient): # Arrange body = {"source_conversation_id": "conv_xxx", "reason": "pytest"} # Act response = await test_client.post( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/merge", json=body, ) # Assert assert response.status_code == 401, response.text async def test_merge_session_forbids_standard_user(test_client: httpx.AsyncClient, standard_user): # Arrange body = {"source_conversation_id": "conv_xxx", "reason": "pytest"} # Act response = await test_client.post( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/merge", json=body, headers=standard_user["headers"], ) # Assert assert response.status_code == 403, response.text async def test_merge_session_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Arrange body = {"source_conversation_id": "conv_xxx", "reason": "pytest"} # Act response = await test_client.post( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/merge", json=body, headers=admin_headers, ) # Assert assert response.status_code == 404, response.text # ============================================================================= # === transfer session owner === # ============================================================================= async def test_transfer_session_requires_auth(test_client: httpx.AsyncClient): # Arrange body = {"new_owner_id": "owner_xxx"} # Act response = await test_client.post( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/transfer", json=body, ) # Assert assert response.status_code == 401, response.text async def test_transfer_session_forbids_standard_user(test_client: httpx.AsyncClient, standard_user): # Arrange body = {"new_owner_id": "owner_xxx"} # Act response = await test_client.post( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/transfer", json=body, headers=standard_user["headers"], ) # Assert assert response.status_code == 403, response.text async def test_transfer_session_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Arrange body = {"new_owner_id": "owner_xxx"} # Act response = await test_client.post( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/transfer", json=body, headers=admin_headers, ) # Assert assert response.status_code == 404, response.text # ============================================================================= # === close session === # ============================================================================= async def test_close_session_requires_auth(test_client: httpx.AsyncClient): # Arrange body = {"reason": "pytest"} # Act response = await test_client.post( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/close", json=body, ) # Assert assert response.status_code == 401, response.text async def test_close_session_forbids_standard_user(test_client: httpx.AsyncClient, standard_user): # Arrange body = {"reason": "pytest"} # Act response = await test_client.post( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/close", json=body, headers=standard_user["headers"], ) # Assert assert response.status_code == 403, response.text async def test_close_session_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Arrange body = {"reason": "pytest"} # Act response = await test_client.post( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/close", json=body, headers=admin_headers, ) # Assert assert response.status_code == 404, response.text # ============================================================================= # === list session messages === # ============================================================================= async def test_list_session_messages_requires_auth(test_client: httpx.AsyncClient): # Act response = await test_client.get(f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/messages") # Assert assert response.status_code == 401, response.text async def test_list_session_messages_forbids_standard_user(test_client: httpx.AsyncClient, standard_user): # Act response = await test_client.get( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/messages", headers=standard_user["headers"], ) # Assert assert response.status_code == 403, response.text async def test_list_session_messages_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/messages", headers=admin_headers, ) # Assert assert response.status_code == 404, response.text async def test_list_session_messages_rejects_invalid_limit(test_client: httpx.AsyncClient, admin_headers): # Act — limit=0 violates ge=1 response = await test_client.get( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/messages", params={"limit": 0}, headers=admin_headers, ) # Assert — session 不存在或参数非法,至少不应是 200 assert response.status_code in (404, 422), response.text # ============================================================================= # === session stats === # ============================================================================= async def test_get_session_stats_requires_auth(test_client: httpx.AsyncClient): # Act response = await test_client.get(f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/stats") # Assert assert response.status_code == 401, response.text async def test_get_session_stats_forbids_standard_user(test_client: httpx.AsyncClient, standard_user): # Act response = await test_client.get( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/stats", headers=standard_user["headers"], ) # Assert assert response.status_code == 403, response.text async def test_get_session_stats_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/stats", headers=admin_headers, ) # Assert assert response.status_code == 404, response.text # ============================================================================= # === bind-user / unbind-user / identity (P3) === # ============================================================================= async def test_bind_session_user_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Arrange body = {"user_uid": "non_existent_user_uid"} # Act response = await test_client.post( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/bind-user", json=body, headers=admin_headers, ) # Assert assert response.status_code == 404, response.text async def test_unbind_session_user_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.delete( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/bind-user", headers=admin_headers, ) # Assert assert response.status_code == 404, response.text async def test_get_session_identity_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/identity", headers=admin_headers, ) # Assert assert response.status_code == 404, response.text