初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,189 @@
|
||||
"""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"
|
||||
Reference in new issue
Block a user