ForcePilot/backend/test/integration/api/channels/test_session_router.py
Kris 696f5f44ec chore: 完成多模块迭代优化与测试覆盖
这一批提交包含:
1. 配置项敏感字段标记与测试用例修复
2. 路由绑定乐观锁支持与静态路径校验
3. 微信WOC插件能力适配与新增单元测试
4. 多个适配器的接口对齐与测试补全
5. 新增定时任务清理处理器与依赖注入容器测试
6. 错误码体系扩展与整合测试
7. 删除临时验证脚本与代码清理
2026-07-08 12:58:43 +08:00

446 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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