本次提交修复了多个测试文件中的问题: 1. 将 ChannelType 枚举调用改为字符串实例化方式 2. 修正了日志断言、异步mock使用、配置参数等多处测试细节 3. 新增了会话聚合根、跨渠道关联策略等单元测试用例 4. 修复了路由测试中的路径方法错误与断言逻辑 5. 调整了依赖导入与测试夹具的兼容性 6. 统一了重试回退调度的列表/元组使用规范
418 lines
13 KiB
Python
418 lines
13 KiB
Python
"""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
|
||
|
||
|
||
# =============================================================================
|
||
# === 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
|