Files
hunjing/backend/tests/api/test_admin_characters.py
T
2026-08-21 16:00:13 +08:00

190 lines
5.4 KiB
Python

"""Admin 人物模板管理 API 测试(§19.8)。
验证系统模板的 CRUD、引用检查、权限隔离。
"""
from collections.abc import AsyncGenerator
import pytest
from httpx import ASGITransport, AsyncClient
from app.main import app
@pytest.fixture
async def admin_client(
admin_headers: dict[str, str],
) -> AsyncGenerator[AsyncClient, None]:
transport = ASGITransport(app=app)
async with AsyncClient(
transport=transport,
base_url="http://test",
headers=admin_headers,
) as http_client:
yield http_client
@pytest.fixture
async def user_client(
auth_headers: dict[str, str],
) -> AsyncGenerator[AsyncClient, None]:
transport = ASGITransport(app=app)
async with AsyncClient(
transport=transport,
base_url="http://test",
headers=auth_headers,
) as http_client:
yield http_client
@pytest.mark.asyncio
async def test_admin_list_templates(admin_client: AsyncClient) -> None:
"""管理员可以列出所有系统模板。"""
resp = await admin_client.get("/api/v1/admin/characters/templates")
assert resp.status_code == 200
body = resp.json()
assert isinstance(body, list)
# 至少包含 default 模板
keys = [t["key"] for t in body]
assert "default" in keys
@pytest.mark.asyncio
async def test_admin_get_template(admin_client: AsyncClient) -> None:
"""管理员可以获取单个模板详情。"""
# 先列出获取 key
list_resp = await admin_client.get("/api/v1/admin/characters/templates")
assert list_resp.status_code == 200
templates = list_resp.json()
if not templates:
pytest.skip("无模板可用")
key = templates[0]["key"]
resp = await admin_client.get(
f"/api/v1/admin/characters/templates/{key}",
)
assert resp.status_code == 200
body = resp.json()
assert body["key"] == key
assert "profile" in body
assert "model_policy" in body
@pytest.mark.asyncio
async def test_admin_create_template(admin_client: AsyncClient) -> None:
"""管理员可以创建新系统模板。"""
payload = {
"key": "test_create_char",
"name": "测试人物",
"display_name": "测试",
"language": "zh-CN",
"profile": {
"description": "自动化测试创建",
"speaking_style": "简洁",
},
"model_policy": {
"memory_extraction_model_hint": "memory-extraction",
},
}
resp = await admin_client.post(
"/api/v1/admin/characters/templates",
json=payload,
)
assert resp.status_code == 201, resp.text
body = resp.json()
assert body["key"] == "test_create_char"
assert body["name"] == "测试人物"
@pytest.mark.asyncio
async def test_admin_create_duplicate_key(admin_client: AsyncClient) -> None:
"""重复 key 应返回 409。"""
payload = {
"key": "test_dup_key",
"name": "首次",
}
resp = await admin_client.post(
"/api/v1/admin/characters/templates",
json=payload,
)
assert resp.status_code == 201
# 重复创建
resp2 = await admin_client.post(
"/api/v1/admin/characters/templates",
json=payload,
)
assert resp2.status_code == 409
@pytest.mark.asyncio
async def test_admin_update_template(admin_client: AsyncClient) -> None:
"""管理员可以部分更新模板。"""
# 先创建
key = "test_update_tpl"
await admin_client.post(
"/api/v1/admin/characters/templates",
json={"key": key, "name": "原名"},
)
# 更新
resp = await admin_client.patch(
f"/api/v1/admin/characters/templates/{key}",
json={"name": "新名", "profile": {"description": "新描述"}},
)
assert resp.status_code == 200
body = resp.json()
assert body["name"] == "新名"
@pytest.mark.asyncio
async def test_admin_delete_template(admin_client: AsyncClient) -> None:
"""管理员可以删除未被引用的模板。"""
key = "test_del_tpl"
resp = await admin_client.post(
"/api/v1/admin/characters/templates",
json={"key": key, "name": "待删除"},
)
assert resp.status_code == 201
resp = await admin_client.delete(
f"/api/v1/admin/characters/templates/{key}",
)
assert resp.status_code == 204
# 确认已删除
get_resp = await admin_client.get(
f"/api/v1/admin/characters/templates/{key}",
)
assert get_resp.status_code == 404
@pytest.mark.asyncio
async def test_non_admin_cannot_list_templates(user_client: AsyncClient) -> None:
"""非管理员不能通过用户端 API 列出模板。"""
resp = await user_client.get("/api/v1/characters/templates")
assert resp.status_code in (403,)
@pytest.mark.asyncio
async def test_non_admin_cannot_create_from_template(
user_client: AsyncClient,
) -> None:
"""非管理员不能带 template_key 创建角色。"""
resp = await user_client.post(
"/api/v1/characters",
json={"name": "hacker_attempt", "template_key": "default"},
)
assert resp.status_code in (403,)
@pytest.mark.asyncio
async def test_admin_can_create_from_template(
admin_client: AsyncClient,
) -> None:
"""管理员可以从模板创建角色。"""
resp = await admin_client.post(
"/api/v1/characters",
json={"name": "admin_char_test", "template_key": "default"},
)
assert resp.status_code == 200, resp.text
body = resp.json()
assert body["status"] == "active"