"""系统设置 API 测试 — GET/PUT 设置 + 公共查询设置 + 查询集成。""" import pytest from httpx import AsyncClient, ASGITransport from app.main import app @pytest.fixture async def client(): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as c: yield c async def _admin_token(client: AsyncClient) -> str: resp = await client.post("/api/v1/auth/login", json={ "username": "admin", "password": "123456" }) assert resp.status_code == 200 return resp.json()["access_token"] def _auth_header(token: str) -> dict: return {"Authorization": f"Bearer {token}"} async def _ensure_user(client: AsyncClient, admin_token: str, username: str, role: str = "developer") -> str: """确保用户存在并返回 token。""" resp = await client.post("/api/v1/auth/login", json={"username": username, "password": "123456"}) if resp.status_code == 200: return resp.json()["access_token"] # 创建 await client.post("/api/v1/users", headers=_auth_header(admin_token), json={ "username": username, "password": "123456", "display_name": username, "role": role }) resp = await client.post("/api/v1/auth/login", json={"username": username, "password": "123456"}) assert resp.status_code == 200 return resp.json()["access_token"] # ── 1. GET /settings ── @pytest.mark.asyncio async def test_get_settings(client): """管理员可以获取所有系统设置。""" token = await _admin_token(client) resp = await client.get("/api/v1/settings", headers=_auth_header(token)) assert resp.status_code == 200 data = resp.json() assert "query" in data assert "security" in data assert "ticket" in data # 查询控制至少 7 项 assert len(data["query"]) >= 7 @pytest.mark.asyncio async def test_get_settings_non_admin(client): """非管理员不能获取设置。""" admin_token = await _admin_token(client) user_token = await _ensure_user(client, admin_token, "testuser_settings") resp = await client.get("/api/v1/settings", headers=_auth_header(user_token)) # require_role("admin") 应该返回 403 assert resp.status_code == 403 # ── 2. PUT /settings ── @pytest.mark.asyncio async def test_update_settings_boolean(client): """更新布尔设置项。""" token = await _admin_token(client) resp = await client.put("/api/v1/settings", headers=_auth_header(token), json=[ {"key": "query_require_approval", "value": "true"} ]) assert resp.status_code == 200 assert "query_require_approval" in resp.json()["updated"] # 验证 resp = await client.get("/api/v1/settings", headers=_auth_header(token)) items = resp.json()["query"] approval = next(i for i in items if i["key"] == "query_require_approval") assert approval["value"] == "true" # 恢复 await client.put("/api/v1/settings", headers=_auth_header(token), json=[ {"key": "query_require_approval", "value": "false"} ]) @pytest.mark.asyncio async def test_update_settings_integer(client): """更新整数设置项。""" token = await _admin_token(client) resp = await client.put("/api/v1/settings", headers=_auth_header(token), json=[ {"key": "query_max_limit", "value": "5000"} ]) assert resp.status_code == 200 # 恢复 await client.put("/api/v1/settings", headers=_auth_header(token), json=[ {"key": "query_max_limit", "value": "10000"} ]) @pytest.mark.asyncio async def test_update_settings_validation(client): """设置值校验。""" token = await _admin_token(client) # 布尔值错误 resp = await client.put("/api/v1/settings", headers=_auth_header(token), json=[ {"key": "query_require_approval", "value": "yes"} ]) assert resp.status_code == 400 # 整数范围错误 resp = await client.put("/api/v1/settings", headers=_auth_header(token), json=[ {"key": "query_max_limit", "value": "10"} ]) assert resp.status_code == 400 # 超时范围错误 resp = await client.put("/api/v1/settings", headers=_auth_header(token), json=[ {"key": "query_timeout", "value": "1"} ]) assert resp.status_code == 400 # ── 3. GET /settings/public/query ── @pytest.mark.asyncio async def test_public_query_settings(client): """公共查询设置无需鉴权。""" resp = await client.get("/api/v1/settings/public/query") assert resp.status_code == 200 data = resp.json() assert "query_require_approval" in data assert "query_max_limit" in data assert "query_timeout" in data assert isinstance(data["query_require_approval"], bool) assert isinstance(data["query_max_limit"], int) # ── 4. 查询集成 — 审批开关 ── @pytest.mark.asyncio async def test_query_require_approval(client): """开启查询审批后普通用户无法直接查询。""" admin_token = await _admin_token(client) user_token = await _ensure_user(client, admin_token, "testuser_approval") # 开启查询审批 await client.put("/api/v1/settings", headers=_auth_header(admin_token), json=[ {"key": "query_require_approval", "value": "true"} ]) # 普通用户查询应被拦截 resp = await client.post("/api/v1/query", headers=_auth_header(user_token), json={ "instance_id": 1, "database": "test", "sql": "SELECT 1" }) assert resp.status_code == 403 assert "审批" in resp.json()["detail"] # 管理员仍然可以查询(不被审批限制拦截,可能因连接报其他错) resp = await client.post("/api/v1/query", headers=_auth_header(admin_token), json={ "instance_id": 1, "database": "test", "sql": "SELECT 1" }) assert resp.status_code != 403, "管理员不应被审批限制拦截" # 恢复 await client.put("/api/v1/settings", headers=_auth_header(admin_token), json=[ {"key": "query_require_approval", "value": "false"} ]) # ── 5. 查询集成 — 语句长度检查 ── @pytest.mark.asyncio async def test_query_max_length(client): """超长查询语句被拦截。""" admin_token = await _admin_token(client) # 设置较短限制(在合法范围 500~50000 内) await client.put("/api/v1/settings", headers=_auth_header(admin_token), json=[ {"key": "query_max_length", "value": "500"} ]) # 超长 SQL(> 500 字符) long_sql = "SELECT * FROM users WHERE id = " + "1" * 600 resp = await client.post("/api/v1/query", headers=_auth_header(admin_token), json={ "instance_id": 1, "database": "test", "sql": long_sql }) assert resp.status_code == 400, f"Expected 400, got {resp.status_code}: {resp.text}" assert "过长" in resp.json()["detail"] # 正常长度 SQL 不被拦截(可能因连接失败但不应是400长度错误) short_sql = "SELECT 1" resp = await client.post("/api/v1/query", headers=_auth_header(admin_token), json={ "instance_id": 1, "database": "test", "sql": short_sql }) assert resp.status_code != 400 or "过长" not in resp.text # 恢复 await client.put("/api/v1/settings", headers=_auth_header(admin_token), json=[ {"key": "query_max_length", "value": "50000"} ]) # ── 6. 批量更新 ── @pytest.mark.asyncio async def test_bulk_update(client): """批量更新多个设置。""" token = await _admin_token(client) resp = await client.put("/api/v1/settings", headers=_auth_header(token), json=[ {"key": "query_timeout", "value": "60"}, {"key": "query_max_limit", "value": "2000"}, ]) assert resp.status_code == 200 assert len(resp.json()["updated"]) == 2 # 恢复 await client.put("/api/v1/settings", headers=_auth_header(token), json=[ {"key": "query_timeout", "value": "30"}, {"key": "query_max_limit", "value": "10000"}, ]) # ── 7. 设置种子数据自动初始化 ── @pytest.mark.asyncio async def test_settings_auto_seed(client): """设置接口自动初始化种子数据。""" token = await _admin_token(client) resp = await client.get("/api/v1/settings", headers=_auth_header(token)) assert resp.status_code == 200 data = resp.json() # 确认至少 12 个设置项 total = sum(len(v) for v in data.values()) assert total >= 12