feat: Go后端LDAP真实对接 — AES-256-GCM密码加解密 + 登录LDAP fallback
- 新增 crypto.go: AES-256-GCM 加解密,与 Python backend 兼容 - auth.go: Login 添加 LDAP fallback(本地优先→LDAP绑定→自动创建本地用户) - ldap.go: 实现真实 LDAP 连接/用户搜索/密码验证/OU列表/用户列表 - 所有 LDAP 操作解密 admin_password 后使用 - 新增/更新密码存储时加密 - router.go: 补充 delete-user 路由 - 添加 go-ldap/v3 依赖
This commit is contained in:
1 parent
622923821b
commit
88e5f4e7ee
71 files changed
+20390
-64
No files matched your search
@@ -0,0 +1,759 @@
|
||||
"""审核规则管理端点全面测试。
|
||||
|
||||
覆盖所有 9 个端点的正常/异常路径,包括:
|
||||
- 权限校验(admin 可读写, dba 只读, developer/guest 禁止访问)
|
||||
- 列表过滤(rule_type, severity, db_type, is_enabled)
|
||||
- 创建/更新/删除/切换启用状态
|
||||
- 统计摘要
|
||||
- 种子数据(V1 seed/{db_type} 和 V2 seed-v2)
|
||||
"""
|
||||
import pytest
|
||||
from tests.conftest import auth_header
|
||||
|
||||
# ── 通用测试载荷 ──────────────────────────────────────────────
|
||||
VALID_RULE_PAYLOAD = {
|
||||
"name": "禁止SELECT星号",
|
||||
"description": "禁止使用 SELECT * 查询",
|
||||
"rule_type": "performance",
|
||||
"severity": "warning",
|
||||
"db_type": "mysql",
|
||||
"pattern": r"SELECT\s+\*",
|
||||
"value": None,
|
||||
"is_enabled": True,
|
||||
"message": "禁止使用 SELECT *,请明确指定字段",
|
||||
}
|
||||
|
||||
PREFIX = "/api/v1/audit-rules"
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# 一、列表端点 GET /api/v1/audit-rules
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestListAuditRules:
|
||||
"""GET /audit-rules — 列表 & 过滤"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin列表返回200(self, client, admin_token):
|
||||
"""admin 角色可正常获取列表"""
|
||||
resp = await client.get(PREFIX, headers=auth_header(admin_token))
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json(), list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dba列表返回200(self, client, dba_token):
|
||||
"""dba 角色可正常获取列表(只读权限)"""
|
||||
resp = await client.get(PREFIX, headers=auth_header(dba_token))
|
||||
assert resp.status_code == 200
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_developer无权限返回403(self, client, developer_token):
|
||||
"""developer 角色无权限"""
|
||||
resp = await client.get(PREFIX, headers=auth_header(developer_token))
|
||||
assert resp.status_code == 403
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_guest无权限返回403(self, client, guest_token):
|
||||
"""guest 角色无权限"""
|
||||
resp = await client.get(PREFIX, headers=auth_header(guest_token))
|
||||
assert resp.status_code == 403
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test无token返回401(self, client):
|
||||
"""未携带 JWT 应返回 401"""
|
||||
resp = await client.get(PREFIX)
|
||||
assert resp.status_code == 401
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test按rule_type过滤(self, client, admin_token):
|
||||
"""通过 rule_type 查询参数过滤"""
|
||||
# 创建一条 performance 规则
|
||||
await client.post(
|
||||
PREFIX,
|
||||
json={**VALID_RULE_PAYLOAD, "name": "perf-rule"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
resp = await client.get(
|
||||
f"{PREFIX}?rule_type=performance", headers=auth_header(admin_token)
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
for rule in resp.json():
|
||||
assert rule["rule_type"] == "performance"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test按severity过滤(self, client, admin_token):
|
||||
"""通过 severity 查询参数过滤"""
|
||||
await client.post(
|
||||
PREFIX,
|
||||
json={**VALID_RULE_PAYLOAD, "name": "sev-rule", "severity": "error"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
resp = await client.get(
|
||||
f"{PREFIX}?severity=error", headers=auth_header(admin_token)
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
for rule in resp.json():
|
||||
assert rule["severity"] == "error"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test按db_type过滤(self, client, admin_token):
|
||||
"""通过 db_type 过滤,应同时包含 db_type='all' 的规则"""
|
||||
await client.post(
|
||||
PREFIX,
|
||||
json={**VALID_RULE_PAYLOAD, "name": "mysql-rule", "db_type": "mysql"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
resp = await client.get(
|
||||
f"{PREFIX}?db_type=mysql", headers=auth_header(admin_token)
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
for rule in resp.json():
|
||||
assert rule["db_type"] in ("mysql", "all")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test按is_enabled过滤(self, client, admin_token):
|
||||
"""通过 is_enabled 过滤已启用/已禁用规则"""
|
||||
# 创建一条规则后禁用
|
||||
create = await client.post(
|
||||
PREFIX,
|
||||
json={**VALID_RULE_PAYLOAD, "name": "disabled-rule"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
rule_id = create.json()["id"]
|
||||
await client.post(
|
||||
f"{PREFIX}/{rule_id}/toggle", headers=auth_header(admin_token)
|
||||
)
|
||||
|
||||
# 只查已禁用的
|
||||
resp = await client.get(
|
||||
f"{PREFIX}?is_enabled=false", headers=auth_header(admin_token)
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
for rule in resp.json():
|
||||
assert rule["is_enabled"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test多条件联合过滤(self, client, admin_token):
|
||||
"""同时传入多个过滤参数"""
|
||||
await client.post(
|
||||
PREFIX,
|
||||
json={
|
||||
**VALID_RULE_PAYLOAD,
|
||||
"name": "multi-filter",
|
||||
"rule_type": "DDL",
|
||||
"severity": "error",
|
||||
},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
resp = await client.get(
|
||||
f"{PREFIX}?rule_type=DDL&severity=error&is_enabled=true",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
for rule in resp.json():
|
||||
assert rule["rule_type"] == "DDL"
|
||||
assert rule["severity"] == "error"
|
||||
assert rule["is_enabled"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test空数据库返回空列表(self, client, admin_token):
|
||||
"""无数据时返回空列表"""
|
||||
resp = await client.get(PREFIX, headers=auth_header(admin_token))
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == []
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# 二、创建端点 POST /api/v1/audit-rules
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestCreateAuditRule:
|
||||
"""POST /audit-rules — 创建规则"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test创建成功返回201(self, client, admin_token):
|
||||
"""正常创建,返回 201 + 完整对象"""
|
||||
resp = await client.post(
|
||||
PREFIX, json=VALID_RULE_PAYLOAD, headers=auth_header(admin_token)
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
data = resp.json()
|
||||
assert data["id"] is not None
|
||||
assert data["name"] == VALID_RULE_PAYLOAD["name"]
|
||||
assert data["rule_type"] == "performance"
|
||||
assert data["severity"] == "warning"
|
||||
assert data["is_enabled"] is True
|
||||
assert "created_at" in data
|
||||
assert "updated_at" in data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test重复名称返回400(self, client, admin_token):
|
||||
"""同名规则不允许重复创建"""
|
||||
payload = {**VALID_RULE_PAYLOAD, "name": "dup-name"}
|
||||
await client.post(PREFIX, json=payload, headers=auth_header(admin_token))
|
||||
resp = await client.post(PREFIX, json=payload, headers=auth_header(admin_token))
|
||||
assert resp.status_code == 400
|
||||
assert "已存在" in resp.json()["detail"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dba创建返回403(self, client, dba_token):
|
||||
"""dba 只有只读权限,不能创建"""
|
||||
resp = await client.post(
|
||||
PREFIX,
|
||||
json={**VALID_RULE_PAYLOAD, "name": "dba-create"},
|
||||
headers=auth_header(dba_token),
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_developer创建返回403(self, client, developer_token):
|
||||
"""developer 无权限"""
|
||||
resp = await client.post(
|
||||
PREFIX,
|
||||
json={**VALID_RULE_PAYLOAD, "name": "dev-create"},
|
||||
headers=auth_header(developer_token),
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test创建时仅必填字段(self, client, admin_token):
|
||||
"""只提供 name/rule_type/message 三个必填字段也可成功"""
|
||||
resp = await client.post(
|
||||
PREFIX,
|
||||
json={
|
||||
"name": "minimal-rule",
|
||||
"rule_type": "syntax",
|
||||
"message": "语法检查",
|
||||
},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
data = resp.json()
|
||||
assert data["severity"] == "warning" # 默认值
|
||||
assert data["db_type"] == "mysql" # 默认值
|
||||
assert data["is_enabled"] is True # 默认值
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test创建全部rule_type合法(self, client, admin_token):
|
||||
"""所有合法 rule_type 枚举均可创建"""
|
||||
for rt in ("syntax", "performance", "safety", "naming", "DDL", "DML"):
|
||||
resp = await client.post(
|
||||
PREFIX,
|
||||
json={
|
||||
"name": f"rt-{rt}",
|
||||
"rule_type": rt,
|
||||
"message": f"类型{rt}规则",
|
||||
},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 201, f"rule_type={rt} 创建失败"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test创建全部severity合法(self, client, admin_token):
|
||||
"""所有合法 severity 枚举均可创建"""
|
||||
for sev in ("error", "warning", "info"):
|
||||
resp = await client.post(
|
||||
PREFIX,
|
||||
json={
|
||||
"name": f"sev-{sev}",
|
||||
"rule_type": "syntax",
|
||||
"severity": sev,
|
||||
"message": f"严重度{sev}",
|
||||
},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 201, f"severity={sev} 创建失败"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test非法rule_type返回422(self, client, admin_token):
|
||||
"""非法 rule_type 应触发 Pydantic 校验错误"""
|
||||
resp = await client.post(
|
||||
PREFIX,
|
||||
json={
|
||||
"name": "bad-type",
|
||||
"rule_type": "invalid_type",
|
||||
"message": "测试",
|
||||
},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test缺少必填字段返回422(self, client, admin_token):
|
||||
"""缺少 name 字段"""
|
||||
resp = await client.post(
|
||||
PREFIX,
|
||||
json={"rule_type": "syntax", "message": "测试"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test创建带value和pattern(self, client, admin_token):
|
||||
"""附带 pattern 和 value 的规则可正常创建"""
|
||||
resp = await client.post(
|
||||
PREFIX,
|
||||
json={
|
||||
"name": "pattern-rule",
|
||||
"rule_type": "safety",
|
||||
"pattern": r"DROP\s+TABLE",
|
||||
"value": "100",
|
||||
"message": "禁止删表",
|
||||
},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
assert resp.json()["pattern"] == r"DROP\s+TABLE"
|
||||
assert resp.json()["value"] == "100"
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# 三、统计摘要 GET /api/v1/audit-rules/stats/summary
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestRuleStats:
|
||||
"""GET /audit-rules/stats/summary — 统计摘要"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test空数据库统计(self, client, admin_token):
|
||||
"""无数据时 total=0"""
|
||||
resp = await client.get(f"{PREFIX}/stats/summary", headers=auth_header(admin_token))
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["total"] == 0
|
||||
assert data["enabled"] == 0
|
||||
assert data["disabled"] == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test统计数据正确(self, client, admin_token):
|
||||
"""创建规则后统计数据应更新"""
|
||||
# 创建2条启用
|
||||
await client.post(
|
||||
PREFIX,
|
||||
json={"name": "stat1", "rule_type": "DDL", "message": "t1"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
await client.post(
|
||||
PREFIX,
|
||||
json={"name": "stat2", "rule_type": "DML", "message": "t2"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
# 创建1条后禁用
|
||||
create = await client.post(
|
||||
PREFIX,
|
||||
json={"name": "stat3", "rule_type": "DDL", "message": "t3"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
rid = create.json()["id"]
|
||||
await client.post(f"{PREFIX}/{rid}/toggle", headers=auth_header(admin_token))
|
||||
|
||||
resp = await client.get(f"{PREFIX}/stats/summary", headers=auth_header(admin_token))
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["total"] == 3
|
||||
assert data["enabled"] == 2
|
||||
assert data["disabled"] == 1
|
||||
assert data["by_type"]["DDL"] == 2
|
||||
assert data["by_type"]["DML"] == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dba可查看统计(self, client, dba_token):
|
||||
"""dba 有只读权限"""
|
||||
resp = await client.get(f"{PREFIX}/stats/summary", headers=auth_header(dba_token))
|
||||
assert resp.status_code == 200
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_developer不可查看统计(self, client, developer_token):
|
||||
"""developer 无权限"""
|
||||
resp = await client.get(f"{PREFIX}/stats/summary", headers=auth_header(developer_token))
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# 四、种子数据 V1 POST /api/v1/audit-rules/seed/{db_type}
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestSeedAuditRules:
|
||||
"""POST /audit-rules/seed/{db_type} — 批量种子数据"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mysql种子数据创建成功(self, client, admin_token):
|
||||
"""MySQL 种子数据可正常导入"""
|
||||
resp = await client.post(f"{PREFIX}/seed/mysql", headers=auth_header(admin_token))
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json(), list)
|
||||
assert len(resp.json()) > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_postgres种子数据创建成功(self, client, admin_token):
|
||||
"""PostgreSQL 种子数据可正常导入"""
|
||||
resp = await client.post(f"{PREFIX}/seed/postgres", headers=auth_header(admin_token))
|
||||
assert resp.status_code == 200
|
||||
assert len(resp.json()) > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_redis种子数据创建成功(self, client, admin_token):
|
||||
"""Redis 种子数据可正常导入"""
|
||||
resp = await client.post(f"{PREFIX}/seed/redis", headers=auth_header(admin_token))
|
||||
assert resp.status_code == 200
|
||||
assert len(resp.json()) > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test不支持的db_type返回400(self, client, admin_token):
|
||||
"""不支持的数据库类型应返回 400"""
|
||||
resp = await client.post(f"{PREFIX}/seed/oracle", headers=auth_header(admin_token))
|
||||
assert resp.status_code == 400
|
||||
assert "不支持的数据库类型" in resp.json()["detail"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dba无种子写入权限(self, client, dba_token):
|
||||
"""dba 无写权限"""
|
||||
resp = await client.post(f"{PREFIX}/seed/mysql", headers=auth_header(dba_token))
|
||||
assert resp.status_code == 403
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test重复种子数据跳过已存在(self, client, admin_token):
|
||||
"""第二次导入相同种子时,已存在的规则会被跳过"""
|
||||
r1 = await client.post(f"{PREFIX}/seed/mysql", headers=auth_header(admin_token))
|
||||
r2 = await client.post(f"{PREFIX}/seed/mysql", headers=auth_header(admin_token))
|
||||
assert r1.status_code == 200
|
||||
assert r2.status_code == 200
|
||||
# 第二次不应创建新规则(列表应为空)
|
||||
assert len(r2.json()) == 0
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# 五、获取单条 GET /api/v1/audit-rules/{rule_id}
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestGetAuditRule:
|
||||
"""GET /audit-rules/{rule_id} — 获取单条"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test获取存在的规则(self, client, admin_token):
|
||||
"""正常获取已创建的规则"""
|
||||
create = await client.post(
|
||||
PREFIX,
|
||||
json={**VALID_RULE_PAYLOAD, "name": "get-one"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
rule_id = create.json()["id"]
|
||||
resp = await client.get(
|
||||
f"{PREFIX}/{rule_id}", headers=auth_header(admin_token)
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["name"] == "get-one"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test获取不存在的规则返回404(self, client, admin_token):
|
||||
"""不存在的 ID 返回 404"""
|
||||
resp = await client.get(f"{PREFIX}/99999", headers=auth_header(admin_token))
|
||||
assert resp.status_code == 404
|
||||
assert "不存在" in resp.json()["detail"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dba可获取规则(self, client, admin_token, dba_token):
|
||||
"""dba 有只读权限"""
|
||||
create = await client.post(
|
||||
PREFIX,
|
||||
json={**VALID_RULE_PAYLOAD, "name": "dba-get"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
rule_id = create.json()["id"]
|
||||
resp = await client.get(
|
||||
f"{PREFIX}/{rule_id}", headers=auth_header(dba_token)
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_developer不可获取规则(self, client, admin_token, developer_token):
|
||||
"""developer 无权限"""
|
||||
create = await client.post(
|
||||
PREFIX,
|
||||
json={**VALID_RULE_PAYLOAD, "name": "dev-get"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
rule_id = create.json()["id"]
|
||||
resp = await client.get(
|
||||
f"{PREFIX}/{rule_id}", headers=auth_header(developer_token)
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# 六、更新 PUT /api/v1/audit-rules/{rule_id}
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestUpdateAuditRule:
|
||||
"""PUT /audit-rules/{rule_id} — 更新规则"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test更新部分字段成功(self, client, admin_token):
|
||||
"""只更新 severity 和 message"""
|
||||
create = await client.post(
|
||||
PREFIX,
|
||||
json={**VALID_RULE_PAYLOAD, "name": "update-partial"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
rule_id = create.json()["id"]
|
||||
resp = await client.put(
|
||||
f"{PREFIX}/{rule_id}",
|
||||
json={"severity": "error", "message": "更新后的消息"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["severity"] == "error"
|
||||
assert data["message"] == "更新后的消息"
|
||||
# 未修改的字段保持不变
|
||||
assert data["name"] == "update-partial"
|
||||
assert data["rule_type"] == "performance"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test更新不存在的规则返回404(self, client, admin_token):
|
||||
"""更新不存在的 ID"""
|
||||
resp = await client.put(
|
||||
f"{PREFIX}/99999",
|
||||
json={"severity": "info"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dba无更新权限返回403(self, client, admin_token, dba_token):
|
||||
"""dba 无写权限"""
|
||||
create = await client.post(
|
||||
PREFIX,
|
||||
json={**VALID_RULE_PAYLOAD, "name": "dba-update"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
rule_id = create.json()["id"]
|
||||
resp = await client.put(
|
||||
f"{PREFIX}/{rule_id}",
|
||||
json={"severity": "info"},
|
||||
headers=auth_header(dba_token),
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test更新全部字段(self, client, admin_token):
|
||||
"""一次更新所有可修改字段"""
|
||||
create = await client.post(
|
||||
PREFIX,
|
||||
json={**VALID_RULE_PAYLOAD, "name": "full-update"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
rule_id = create.json()["id"]
|
||||
resp = await client.put(
|
||||
f"{PREFIX}/{rule_id}",
|
||||
json={
|
||||
"name": "full-update-v2",
|
||||
"description": "新描述",
|
||||
"rule_type": "safety",
|
||||
"severity": "error",
|
||||
"db_type": "postgres",
|
||||
"pattern": r"ALTER\s+TABLE",
|
||||
"value": "200",
|
||||
"is_enabled": False,
|
||||
"message": "全新的消息",
|
||||
},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["name"] == "full-update-v2"
|
||||
assert data["description"] == "新描述"
|
||||
assert data["rule_type"] == "safety"
|
||||
assert data["severity"] == "error"
|
||||
assert data["db_type"] == "postgres"
|
||||
assert data["pattern"] == r"ALTER\s+TABLE"
|
||||
assert data["value"] == "200"
|
||||
assert data["is_enabled"] is False
|
||||
assert data["message"] == "全新的消息"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test更新is_enabled字段(self, client, admin_token):
|
||||
"""通过 PUT 单独更新 is_enabled"""
|
||||
create = await client.post(
|
||||
PREFIX,
|
||||
json={**VALID_RULE_PAYLOAD, "name": "toggle-via-put"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
rule_id = create.json()["id"]
|
||||
assert create.json()["is_enabled"] is True
|
||||
|
||||
resp = await client.put(
|
||||
f"{PREFIX}/{rule_id}",
|
||||
json={"is_enabled": False},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["is_enabled"] is False
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# 七、删除 DELETE /api/v1/audit-rules/{rule_id}
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestDeleteAuditRule:
|
||||
"""DELETE /audit-rules/{rule_id} — 删除规则"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test删除成功(self, client, admin_token):
|
||||
"""删除后 GET 应返回 404"""
|
||||
create = await client.post(
|
||||
PREFIX,
|
||||
json={**VALID_RULE_PAYLOAD, "name": "del-me"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
rule_id = create.json()["id"]
|
||||
resp = await client.delete(
|
||||
f"{PREFIX}/{rule_id}", headers=auth_header(admin_token)
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["message"] == "规则已删除"
|
||||
|
||||
# 确认已删除
|
||||
get = await client.get(
|
||||
f"{PREFIX}/{rule_id}", headers=auth_header(admin_token)
|
||||
)
|
||||
assert get.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test删除不存在的规则返回404(self, client, admin_token):
|
||||
"""删除不存在的 ID"""
|
||||
resp = await client.delete(
|
||||
f"{PREFIX}/99999", headers=auth_header(admin_token)
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dba无删除权限返回403(self, client, admin_token, dba_token):
|
||||
"""dba 无删除权限"""
|
||||
create = await client.post(
|
||||
PREFIX,
|
||||
json={**VALID_RULE_PAYLOAD, "name": "dba-del"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
rule_id = create.json()["id"]
|
||||
resp = await client.delete(
|
||||
f"{PREFIX}/{rule_id}", headers=auth_header(dba_token)
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# 八、切换启用 POST /api/v1/audit-rules/{rule_id}/toggle
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestToggleAuditRule:
|
||||
"""POST /audit-rules/{rule_id}/toggle — 切换启用/禁用"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test切换启用到禁用再恢复(self, client, admin_token):
|
||||
"""连续 toggle 两次:启用→禁用→启用"""
|
||||
create = await client.post(
|
||||
PREFIX,
|
||||
json={**VALID_RULE_PAYLOAD, "name": "toggle-twice"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
rule_id = create.json()["id"]
|
||||
assert create.json()["is_enabled"] is True
|
||||
|
||||
# 第一次 toggle:禁用
|
||||
r1 = await client.post(
|
||||
f"{PREFIX}/{rule_id}/toggle", headers=auth_header(admin_token)
|
||||
)
|
||||
assert r1.status_code == 200
|
||||
assert r1.json()["is_enabled"] is False
|
||||
|
||||
# 第二次 toggle:恢复启用
|
||||
r2 = await client.post(
|
||||
f"{PREFIX}/{rule_id}/toggle", headers=auth_header(admin_token)
|
||||
)
|
||||
assert r2.status_code == 200
|
||||
assert r2.json()["is_enabled"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test切换不存在的规则返回404(self, client, admin_token):
|
||||
"""不存在的 ID 返回 404"""
|
||||
resp = await client.post(
|
||||
f"{PREFIX}/99999/toggle", headers=auth_header(admin_token)
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dba可切换规则(self, client, admin_token, dba_token):
|
||||
"""dba 有 toggle 权限(只读类操作中 toggle 是特殊写操作,需确认)"""
|
||||
create = await client.post(
|
||||
PREFIX,
|
||||
json={**VALID_RULE_PAYLOAD, "name": "dba-toggle"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
rule_id = create.json()["id"]
|
||||
resp = await client.post(
|
||||
f"{PREFIX}/{rule_id}/toggle", headers=auth_header(dba_token)
|
||||
)
|
||||
# 端点 require_role("admin", "dba") — dba 有权限
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["is_enabled"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_developer不可切换(self, client, admin_token, developer_token):
|
||||
"""developer 无权限"""
|
||||
create = await client.post(
|
||||
PREFIX,
|
||||
json={**VALID_RULE_PAYLOAD, "name": "dev-toggle"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
rule_id = create.json()["id"]
|
||||
resp = await client.post(
|
||||
f"{PREFIX}/{rule_id}/toggle", headers=auth_header(developer_token)
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# 九、V2 种子数据 POST /api/v1/audit-rules/seed-v2
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestSeedV2AuditRules:
|
||||
"""POST /audit-rules/seed-v2 — V2 种子数据"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_v2种子数据创建成功(self, client, admin_token):
|
||||
"""V2 种子数据可正常导入"""
|
||||
resp = await client.post(f"{PREFIX}/seed-v2", headers=auth_header(admin_token))
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json(), list)
|
||||
assert len(resp.json()) > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_v2种子数据去重(self, client, admin_token):
|
||||
"""重复导入 V2 种子时应跳过已存在的规则"""
|
||||
await client.post(f"{PREFIX}/seed-v2", headers=auth_header(admin_token))
|
||||
resp = await client.post(f"{PREFIX}/seed-v2", headers=auth_header(admin_token))
|
||||
assert resp.status_code == 200
|
||||
# 第二次不会创建新规则
|
||||
assert len(resp.json()) == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dba无v2种子写入权限(self, client, dba_token):
|
||||
"""dba 无写权限"""
|
||||
resp = await client.post(f"{PREFIX}/seed-v2", headers=auth_header(dba_token))
|
||||
assert resp.status_code == 403
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_developer无v2种子权限(self, client, developer_token):
|
||||
"""developer 无权限"""
|
||||
resp = await client.post(f"{PREFIX}/seed-v2", headers=auth_header(developer_token))
|
||||
assert resp.status_code == 403
|
||||
Reference in new issue
Block a user