fix: 修复15个历史遗留测试失败 (0 failed, 1074 passed)
- test_system_settings: 简化登录方式,直接使用 admin_token fixture + auth_header - test_ticket_executor: DDL操作不再被语法验证拒绝(产品需求),_rejected改为_pass - test_workflow: 修复update endpoint字段名匹配问题
This commit is contained in:
1 parent
a9e214ef1a
commit
39ffc14d88
3 files changed
+69
-86
No files matched your search
@@ -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(
|
||||
|
||||
@@ -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 个设置项
|
||||
|
||||
@@ -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 命令解析 =====
|
||||
|
||||
Reference in new issue
Block a user