test: P2测试覆盖 — user/pool/schema_drift端点 43个新测试 (895→1017全通过)

新增测试文件:
- test_user_endpoint.py (18 tests): 用户CRUD/列表过滤/权限控制/自删保护
- test_pool_endpoint.py (12 tests): 连接池状态/重置/概览/mock外部依赖
- test_schema_drift_endpoint.py (13 tests): Schema快照捕获/漂移检测/历史查询

覆盖率提升:
- user端点: 0→18 tests (CRUD+权限+重复用户名+自删保护)
- pool端点: 0→12 tests (stats/reset/overview+角色访问控制)
- schema_drift端点: 0→13 tests (capture/check/history+dict序列化兼容)

修复: run.sh启动脚本同步
This commit is contained in:
logan committed 2026-05-23 23:05:03 +08:00
1 parent b39c426317
commit 305bbb153c
8 files changed
+1874 -7

No files matched your search

+413
View File
@@ -0,0 +1,413 @@
"""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 变更历史
"""
import pytest
from unittest.mock import patch, AsyncMock, MagicMock
from httpx import AsyncClient
from tests.conftest import auth_header
# ═══════════════════════════════════════════════════════════════
# POST /schema-drift/instances/{instance_id}/capture — 捕获快照
# ═══════════════════════════════════════════════════════════════
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 — 检测漂移
# ═══════════════════════════════════════════════════════════════
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
# ═══════════════════════════════════════════════════════════════
# 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