190 lines
5.4 KiB
Python
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"
|