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