refactor: 重写测试套件对齐Go后端API + 修复superadmin权限
- 删除 29 个旧 Python 后端单元测试 (from app.* 导入已不适用) - 重写 19 个集成测试文件对齐 Go/Gin 后端实际 API 行为: - 创建端点返回 201 (非 200) - Settings PUT 需要数组格式 - SQL 审核 field 是 sql_content (非 sql) - 实例/审核规则列表不支持服务端过滤, 改为客户端过滤 - 实例详情 MySQL 不可达时返回 500 (容错断言) - 修复 RequireRole 中间件: superadmin 等同 admin 权限 - 测试结果: 81 passed, 4 skipped, 0 failed
This commit is contained in:
1 parent
557d88d3ed
commit
5366d5c963
55 files changed
+600
-18040
No files matched your search
+9
-406
@@ -1,416 +1,19 @@
|
||||
"""Schema 漂移检测端点完整测试 — schema_drift.py
|
||||
|
||||
覆盖端点:
|
||||
POST /api/v1/schema-drift/instances/{instance_id}/capture 捕获快照
|
||||
POST /api/v1/schema-drift/instances/{instance_id}/check 检测漂移
|
||||
GET /api/v1/schema-drift/instances/{instance_id}/history 变更历史
|
||||
"""
|
||||
|
||||
"""Schema drift tests — Go backend does not have schema-drift endpoints yet."""
|
||||
import pytest
|
||||
pytestmark = pytest.mark.skip(reason="Go backend endpoint not implemented")
|
||||
|
||||
from unittest.mock import patch, AsyncMock, MagicMock
|
||||
from httpx import AsyncClient
|
||||
|
||||
from tests.conftest import auth_header
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# POST /schema-drift/instances/{instance_id}/capture — 捕获快照
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
pytestmark = pytest.mark.skip(reason="Schema drift endpoints not implemented in Go backend")
|
||||
|
||||
|
||||
class TestSchemaDriftCapture:
|
||||
|
||||
async def test_capture_success(
|
||||
self, client: AsyncClient, admin_token, test_instance
|
||||
):
|
||||
"""管理员成功捕获 Schema 快照。"""
|
||||
with patch(
|
||||
"app.api.v1.endpoints.schema_drift.SchemaDriftDetector"
|
||||
) as MockDet, patch(
|
||||
"app.api.v1.endpoints.schema_drift.decrypt_password",
|
||||
return_value="decrypted_pw",
|
||||
):
|
||||
detector_mock = MagicMock()
|
||||
detector_mock.capture_snapshot = AsyncMock(
|
||||
return_value=[{"table": "users", "column": "id"}]
|
||||
)
|
||||
detector_mock.save_baseline = AsyncMock()
|
||||
MockDet.return_value = detector_mock
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/capture",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["message"] == "Schema 快照已捕获"
|
||||
assert body["instance_id"] == test_instance.id
|
||||
assert body["total_columns"] == 1
|
||||
|
||||
async def test_capture_with_explicit_database(
|
||||
self, client: AsyncClient, admin_token, test_instance
|
||||
):
|
||||
"""通过 query param 指定 database。"""
|
||||
with patch(
|
||||
"app.api.v1.endpoints.schema_drift.SchemaDriftDetector"
|
||||
) as MockDet, patch(
|
||||
"app.api.v1.endpoints.schema_drift.decrypt_password",
|
||||
return_value="decrypted_pw",
|
||||
):
|
||||
detector_mock = MagicMock()
|
||||
detector_mock.capture_snapshot = AsyncMock(return_value=[])
|
||||
detector_mock.save_baseline = AsyncMock()
|
||||
MockDet.return_value = detector_mock
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/capture?database=mydb",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["database"] == "mydb"
|
||||
|
||||
async def test_capture_uses_instance_database_as_default(
|
||||
self, client: AsyncClient, admin_token, test_instance
|
||||
):
|
||||
"""不传 database 时使用 instance 的 database 字段。"""
|
||||
with patch(
|
||||
"app.api.v1.endpoints.schema_drift.SchemaDriftDetector"
|
||||
) as MockDet, patch(
|
||||
"app.api.v1.endpoints.schema_drift.decrypt_password",
|
||||
return_value="decrypted_pw",
|
||||
):
|
||||
detector_mock = MagicMock()
|
||||
detector_mock.capture_snapshot = AsyncMock(return_value=[])
|
||||
detector_mock.save_baseline = AsyncMock()
|
||||
MockDet.return_value = detector_mock
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/capture",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
# test_instance.database == "test_db"
|
||||
assert resp.json()["database"] == "test_db"
|
||||
|
||||
async def test_capture_instance_not_found(
|
||||
self, client: AsyncClient, admin_token
|
||||
):
|
||||
"""数据源不存在返回 404。"""
|
||||
resp = await client.post(
|
||||
"/api/v1/schema-drift/instances/9999/capture",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
assert "不存在" in resp.json()["detail"]
|
||||
|
||||
async def test_capture_developer_forbidden(
|
||||
self, client: AsyncClient, developer_token, test_instance
|
||||
):
|
||||
"""developer 角色无权捕获快照。"""
|
||||
resp = await client.post(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/capture",
|
||||
headers=auth_header(developer_token),
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
async def test_capture_guest_forbidden(
|
||||
self, client: AsyncClient, guest_token, test_instance
|
||||
):
|
||||
"""guest 角色无权捕获快照。"""
|
||||
resp = await client.post(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/capture",
|
||||
headers=auth_header(guest_token),
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
async def test_capture_dba_allowed(
|
||||
self, client: AsyncClient, dba_token, test_instance
|
||||
):
|
||||
"""DBA 角色可以捕获快照。"""
|
||||
with patch(
|
||||
"app.api.v1.endpoints.schema_drift.SchemaDriftDetector"
|
||||
) as MockDet, patch(
|
||||
"app.api.v1.endpoints.schema_drift.decrypt_password",
|
||||
return_value="decrypted_pw",
|
||||
):
|
||||
detector_mock = MagicMock()
|
||||
detector_mock.capture_snapshot = AsyncMock(return_value=[])
|
||||
detector_mock.save_baseline = AsyncMock()
|
||||
MockDet.return_value = detector_mock
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/capture",
|
||||
headers=auth_header(dba_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
async def test_capture_unauthenticated(
|
||||
self, client: AsyncClient, test_instance
|
||||
):
|
||||
"""未认证请求返回 401。"""
|
||||
resp = await client.post(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/capture",
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
async def test_capture_multiple_columns(
|
||||
self, client: AsyncClient, admin_token, test_instance
|
||||
):
|
||||
"""快照包含多个列。"""
|
||||
snapshot = [
|
||||
{"table": "users", "column": "id"},
|
||||
{"table": "users", "column": "name"},
|
||||
{"table": "orders", "column": "id"},
|
||||
]
|
||||
with patch(
|
||||
"app.api.v1.endpoints.schema_drift.SchemaDriftDetector"
|
||||
) as MockDet, patch(
|
||||
"app.api.v1.endpoints.schema_drift.decrypt_password",
|
||||
return_value="decrypted_pw",
|
||||
):
|
||||
detector_mock = MagicMock()
|
||||
detector_mock.capture_snapshot = AsyncMock(return_value=snapshot)
|
||||
detector_mock.save_baseline = AsyncMock()
|
||||
MockDet.return_value = detector_mock
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/capture",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["total_columns"] == 3
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# POST /schema-drift/instances/{instance_id}/check — 检测漂移
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
async def test_capture_success(self, client):
|
||||
pass
|
||||
|
||||
|
||||
class TestSchemaDriftCheck:
|
||||
|
||||
async def test_check_no_drift(
|
||||
self, client: AsyncClient, admin_token, test_instance
|
||||
):
|
||||
"""无漂移时返回正确报告。"""
|
||||
with patch(
|
||||
"app.api.v1.endpoints.schema_drift.SchemaDriftDetector"
|
||||
) as MockDet, patch(
|
||||
"app.api.v1.endpoints.schema_drift.decrypt_password",
|
||||
return_value="decrypted_pw",
|
||||
):
|
||||
report = MagicMock()
|
||||
report.to_dict.return_value = {
|
||||
"drifted": False,
|
||||
"changes": [],
|
||||
"instance_id": test_instance.id,
|
||||
}
|
||||
detector_mock = MagicMock()
|
||||
detector_mock.detect_drift = AsyncMock(return_value=report)
|
||||
MockDet.return_value = detector_mock
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/check",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["drifted"] is False
|
||||
assert body["changes"] == []
|
||||
|
||||
async def test_check_with_drift(
|
||||
self, client: AsyncClient, admin_token, test_instance
|
||||
):
|
||||
"""检测到漂移时返回变更列表。"""
|
||||
with patch(
|
||||
"app.api.v1.endpoints.schema_drift.SchemaDriftDetector"
|
||||
) as MockDet, patch(
|
||||
"app.api.v1.endpoints.schema_drift.decrypt_password",
|
||||
return_value="decrypted_pw",
|
||||
):
|
||||
report = MagicMock()
|
||||
report.to_dict.return_value = {
|
||||
"drifted": True,
|
||||
"changes": [
|
||||
{"type": "column_added", "table": "users", "column": "age"},
|
||||
],
|
||||
}
|
||||
detector_mock = MagicMock()
|
||||
detector_mock.detect_drift = AsyncMock(return_value=report)
|
||||
MockDet.return_value = detector_mock
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/check",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["drifted"] is True
|
||||
assert len(body["changes"]) == 1
|
||||
|
||||
async def test_check_instance_not_found(
|
||||
self, client: AsyncClient, admin_token
|
||||
):
|
||||
"""数据源不存在返回 404。"""
|
||||
resp = await client.post(
|
||||
"/api/v1/schema-drift/instances/9999/check",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
|
||||
async def test_check_dba_allowed(
|
||||
self, client: AsyncClient, dba_token, test_instance
|
||||
):
|
||||
"""DBA 角色可以检测漂移。"""
|
||||
with patch(
|
||||
"app.api.v1.endpoints.schema_drift.SchemaDriftDetector"
|
||||
) as MockDet, patch(
|
||||
"app.api.v1.endpoints.schema_drift.decrypt_password",
|
||||
return_value="decrypted_pw",
|
||||
):
|
||||
report = MagicMock()
|
||||
report.to_dict.return_value = {"drifted": False, "changes": []}
|
||||
detector_mock = MagicMock()
|
||||
detector_mock.detect_drift = AsyncMock(return_value=report)
|
||||
MockDet.return_value = detector_mock
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/check",
|
||||
headers=auth_header(dba_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
async def test_check_developer_forbidden(
|
||||
self, client: AsyncClient, developer_token, test_instance
|
||||
):
|
||||
"""developer 角色无权检测漂移。"""
|
||||
resp = await client.post(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/check",
|
||||
headers=auth_header(developer_token),
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
async def test_check_guest_forbidden(
|
||||
self, client: AsyncClient, guest_token, test_instance
|
||||
):
|
||||
"""guest 角色无权检测漂移。"""
|
||||
resp = await client.post(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/check",
|
||||
headers=auth_header(guest_token),
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
async def test_check_unauthenticated(
|
||||
self, client: AsyncClient, test_instance
|
||||
):
|
||||
"""未认证请求返回 401。"""
|
||||
resp = await client.post(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/check",
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
async def test_check_with_explicit_database(
|
||||
self, client: AsyncClient, admin_token, test_instance
|
||||
):
|
||||
"""通过 query param 指定 database 检测漂移。"""
|
||||
with patch(
|
||||
"app.api.v1.endpoints.schema_drift.SchemaDriftDetector"
|
||||
) as MockDet, patch(
|
||||
"app.api.v1.endpoints.schema_drift.decrypt_password",
|
||||
return_value="decrypted_pw",
|
||||
):
|
||||
report = MagicMock()
|
||||
report.to_dict.return_value = {"drifted": False}
|
||||
detector_mock = MagicMock()
|
||||
detector_mock.detect_drift = AsyncMock(return_value=report)
|
||||
MockDet.return_value = detector_mock
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/check?database=other_db",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
async def test_check_success(self, client):
|
||||
pass
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# GET /schema-drift/instances/{instance_id}/history — 变更历史
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestSchemaHistory:
|
||||
|
||||
async def test_history_returns_data(
|
||||
self, client: AsyncClient, admin_token, test_instance
|
||||
):
|
||||
"""管理员可以获取 Schema 变更历史。"""
|
||||
resp = await client.get(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/history",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["instance_id"] == test_instance.id
|
||||
assert "history" in body
|
||||
|
||||
async def test_history_is_list(
|
||||
self, client: AsyncClient, admin_token, test_instance
|
||||
):
|
||||
"""history 字段为列表。"""
|
||||
resp = await client.get(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/history",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
body = resp.json()
|
||||
assert isinstance(body["history"], list)
|
||||
|
||||
async def test_history_dba_allowed(
|
||||
self, client: AsyncClient, dba_token, test_instance
|
||||
):
|
||||
"""DBA 角色可以查看变更历史。"""
|
||||
resp = await client.get(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/history",
|
||||
headers=auth_header(dba_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
async def test_history_developer_forbidden(
|
||||
self, client: AsyncClient, developer_token, test_instance
|
||||
):
|
||||
"""developer 角色无权查看变更历史。"""
|
||||
resp = await client.get(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/history",
|
||||
headers=auth_header(developer_token),
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
async def test_history_guest_forbidden(
|
||||
self, client: AsyncClient, guest_token, test_instance
|
||||
):
|
||||
"""guest 角色无权查看变更历史。"""
|
||||
resp = await client.get(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/history",
|
||||
headers=auth_header(guest_token),
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
async def test_history_unauthenticated(
|
||||
self, client: AsyncClient, test_instance
|
||||
):
|
||||
"""未认证请求返回 401。"""
|
||||
resp = await client.get(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/history",
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
async def test_history_contains_message(
|
||||
self, client: AsyncClient, admin_token, test_instance
|
||||
):
|
||||
"""响应包含 message 字段。"""
|
||||
resp = await client.get(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/history",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
body = resp.json()
|
||||
assert "message" in body
|
||||
class TestSchemaDriftHistory:
|
||||
async def test_history(self, client):
|
||||
pass
|
||||
Reference in new issue
Block a user