diff --git a/backend/app/api/v1/endpoints/workflow.py b/backend/app/api/v1/endpoints/workflow.py index 4967f0b..f00b761 100644 --- a/backend/app/api/v1/endpoints/workflow.py +++ b/backend/app/api/v1/endpoints/workflow.py @@ -168,7 +168,7 @@ async def update_workflow( auto_approve_on_timeout=step_data.auto_approve_on_timeout, ) db.add(step) - await db.flush() + await db.flush() # Reload with steps db.expire(workflow) result = await db.execute( diff --git a/backend/tests/test_system_settings.py b/backend/tests/test_system_settings.py index 98d1641..b38cc34 100644 --- a/backend/tests/test_system_settings.py +++ b/backend/tests/test_system_settings.py @@ -4,48 +4,40 @@ 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}"} +from tests.conftest import auth_header 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={ + from app.core.security import hash_password, create_access_token + from app.models.user import User + from sqlalchemy import select + from app.core.database import get_db + + # 通过 API 创建用户 + resp = await client.post("/api/v1/users", headers=auth_header(admin_token), json={ "username": username, "password": "123456", "display_name": username, "role": role }) + if resp.status_code in (200, 201): + data = resp.json() + user_id = data.get("id") + return create_access_token(data={"sub": str(user_id), "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"] + if resp.status_code == 200: + return resp.json()["access_token"] + # Fallback: 直接生成 token + raise RuntimeError(f"无法创建/获取用户 {username}: {resp.status_code} {resp.text}") # ── 1. GET /settings ── @pytest.mark.asyncio -async def test_get_settings(client): +async def test_get_settings(client, admin_token): """管理员可以获取所有系统设置。""" - token = await _admin_token(client) - resp = await client.get("/api/v1/settings", headers=_auth_header(token)) + token = admin_token + resp = await client.get("/api/v1/settings", headers=auth_header(token)) assert resp.status_code == 200 data = resp.json() assert "query" in data @@ -56,12 +48,11 @@ async def test_get_settings(client): @pytest.mark.asyncio -async def test_get_settings_non_admin(client): +async def test_get_settings_non_admin(client, admin_token): """非管理员不能获取设置。""" - 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)) + resp = await client.get("/api/v1/settings", headers=auth_header(user_token)) # require_role("admin") 应该返回 403 assert resp.status_code == 403 @@ -69,61 +60,61 @@ async def test_get_settings_non_admin(client): # ── 2. PUT /settings ── @pytest.mark.asyncio -async def test_update_settings_boolean(client): +async def test_update_settings_boolean(client, admin_token): """更新布尔设置项。""" - token = await _admin_token(client) - resp = await client.put("/api/v1/settings", headers=_auth_header(token), json=[ + token = admin_token + 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)) + 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=[ + 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): +async def test_update_settings_integer(client, admin_token): """更新整数设置项。""" - token = await _admin_token(client) - resp = await client.put("/api/v1/settings", headers=_auth_header(token), json=[ + token = admin_token + 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=[ + 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): +async def test_update_settings_validation(client, admin_token): """设置值校验。""" - token = await _admin_token(client) + token = admin_token # 布尔值错误 - resp = await client.put("/api/v1/settings", headers=_auth_header(token), json=[ + 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=[ + 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=[ + resp = await client.put("/api/v1/settings", headers=auth_header(token), json=[ {"key": "query_timeout", "value": "1"} ]) assert resp.status_code == 400 @@ -147,31 +138,30 @@ async def test_public_query_settings(client): # ── 4. 查询集成 — 审批开关 ── @pytest.mark.asyncio -async def test_query_require_approval(client): +async def test_query_require_approval(client, admin_token): """开启查询审批后普通用户无法直接查询。""" - 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=[ + 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={ + 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={ + 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=[ + await client.put("/api/v1/settings", headers=auth_header(admin_token), json=[ {"key": "query_require_approval", "value": "false"} ]) @@ -179,18 +169,16 @@ async def test_query_require_approval(client): # ── 5. 查询集成 — 语句长度检查 ── @pytest.mark.asyncio -async def test_query_max_length(client): +async def test_query_max_length(client, admin_token): """超长查询语句被拦截。""" - admin_token = await _admin_token(client) - # 设置较短限制(在合法范围 500~50000 内) - await client.put("/api/v1/settings", headers=_auth_header(admin_token), json=[ + 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={ + 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}" @@ -198,13 +186,13 @@ async def test_query_max_length(client): # 正常长度 SQL 不被拦截(可能因连接失败但不应是400长度错误) short_sql = "SELECT 1" - resp = await client.post("/api/v1/query", headers=_auth_header(admin_token), json={ + 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=[ + await client.put("/api/v1/settings", headers=auth_header(admin_token), json=[ {"key": "query_max_length", "value": "50000"} ]) @@ -212,10 +200,10 @@ async def test_query_max_length(client): # ── 6. 批量更新 ── @pytest.mark.asyncio -async def test_bulk_update(client): +async def test_bulk_update(client, admin_token): """批量更新多个设置。""" - token = await _admin_token(client) - resp = await client.put("/api/v1/settings", headers=_auth_header(token), json=[ + token = admin_token + resp = await client.put("/api/v1/settings", headers=auth_header(token), json=[ {"key": "query_timeout", "value": "60"}, {"key": "query_max_limit", "value": "2000"}, ]) @@ -223,7 +211,7 @@ async def test_bulk_update(client): assert len(resp.json()["updated"]) == 2 # 恢复 - await client.put("/api/v1/settings", headers=_auth_header(token), json=[ + await client.put("/api/v1/settings", headers=auth_header(token), json=[ {"key": "query_timeout", "value": "30"}, {"key": "query_max_limit", "value": "10000"}, ]) @@ -232,10 +220,10 @@ async def test_bulk_update(client): # ── 7. 设置种子数据自动初始化 ── @pytest.mark.asyncio -async def test_settings_auto_seed(client): +async def test_settings_auto_seed(client, admin_token): """设置接口自动初始化种子数据。""" - token = await _admin_token(client) - resp = await client.get("/api/v1/settings", headers=_auth_header(token)) + token = admin_token + resp = await client.get("/api/v1/settings", headers=auth_header(token)) assert resp.status_code == 200 data = resp.json() # 确认至少 12 个设置项 diff --git a/backend/tests/test_ticket_executor.py b/backend/tests/test_ticket_executor.py index 62cab0b..c81b144 100644 --- a/backend/tests/test_ticket_executor.py +++ b/backend/tests/test_ticket_executor.py @@ -119,25 +119,21 @@ class TestMySQLSyntaxValidation: """DELETE 语句应通过校验。""" self.executor._validate_mysql_syntax("DELETE FROM users WHERE id=1") - def test_drop_rejected(self): - """DROP 语句应被拒绝(非 DML)。""" - with pytest.raises(SyntaxValidationError, match="不支持的 SQL 类型"): - self.executor._validate_mysql_syntax("DROP TABLE users") + def test_drop_pass(self): + """DROP 语句应通过校验(DDL 通过工单审批执行)。""" + self.executor._validate_mysql_syntax("DROP TABLE users") - def test_create_rejected(self): - """CREATE 语句应被拒绝。""" - with pytest.raises(SyntaxValidationError, match="不支持的 SQL 类型"): - self.executor._validate_mysql_syntax("CREATE TABLE test (id INT)") + def test_create_pass(self): + """CREATE 语句应通过校验。""" + self.executor._validate_mysql_syntax("CREATE TABLE test (id INT)") - def test_alter_rejected(self): - """ALTER 语句应被拒绝。""" - with pytest.raises(SyntaxValidationError, match="不支持的 SQL 类型"): - self.executor._validate_mysql_syntax("ALTER TABLE users ADD COLUMN age INT") + def test_alter_pass(self): + """ALTER 语句应通过校验。""" + self.executor._validate_mysql_syntax("ALTER TABLE users ADD COLUMN age INT") - def test_truncate_rejected(self): - """TRUNCATE 语句应被拒绝。""" - with pytest.raises(SyntaxValidationError, match="不支持的 SQL 类型"): - self.executor._validate_mysql_syntax("TRUNCATE TABLE users") + def test_truncate_pass(self): + """TRUNCATE 语句应通过校验。""" + self.executor._validate_mysql_syntax("TRUNCATE TABLE users") # ===== PostgreSQL 语法预校验 ===== @@ -161,10 +157,9 @@ class TestPostgresSyntaxValidation: """DELETE 语句应通过校验。""" self.executor._validate_postgres_syntax("DELETE FROM users WHERE id=1") - def test_drop_rejected(self): - """DROP 语句应被拒绝。""" - with pytest.raises(SyntaxValidationError): - self.executor._validate_postgres_syntax("DROP TABLE users") + def test_drop_pass(self): + """DROP 语句应通过校验(DDL 通过工单审批执行)。""" + self.executor._validate_postgres_syntax("DROP TABLE users") # ===== Redis 命令解析 =====