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:
1 parent
b39c426317
commit
305bbb153c
8 files changed
+1874
-7
No files matched your search
@@ -0,0 +1,231 @@
|
||||
"""Schema 漂移检测 API 端点测试。"""
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock, AsyncMock
|
||||
from httpx import AsyncClient
|
||||
|
||||
from app.models.user import User
|
||||
from app.models.instance import Instance
|
||||
|
||||
|
||||
def auth_header(token: str) -> dict:
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestCaptureSchema:
|
||||
"""POST /api/v1/schema-drift/instances/{instance_id}/capture"""
|
||||
|
||||
async def test_capture_schema_success(
|
||||
self, client: AsyncClient, admin_token: str, test_instance
|
||||
):
|
||||
mock_snapshot = [
|
||||
{"table": "users", "column": "id", "type": "int"},
|
||||
{"table": "users", "column": "name", "type": "varchar"},
|
||||
]
|
||||
with patch(
|
||||
"app.api.v1.endpoints.schema_drift.SchemaDriftDetector"
|
||||
) as MockDetector, patch(
|
||||
"app.api.v1.endpoints.schema_drift.decrypt_password",
|
||||
return_value="decrypted_pw",
|
||||
):
|
||||
detector = MockDetector.return_value
|
||||
detector.capture_snapshot = AsyncMock(return_value=mock_snapshot)
|
||||
detector.save_baseline = AsyncMock()
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/capture",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "已捕获" in data["message"]
|
||||
assert data["total_columns"] == 2
|
||||
assert data["instance_id"] == test_instance.id
|
||||
|
||||
async def test_capture_schema_with_database_param(
|
||||
self, client: AsyncClient, admin_token: str, test_instance
|
||||
):
|
||||
with patch(
|
||||
"app.api.v1.endpoints.schema_drift.SchemaDriftDetector"
|
||||
) as MockDetector, patch(
|
||||
"app.api.v1.endpoints.schema_drift.decrypt_password",
|
||||
return_value="decrypted_pw",
|
||||
):
|
||||
detector = MockDetector.return_value
|
||||
detector.capture_snapshot = AsyncMock(return_value=[])
|
||||
detector.save_baseline = AsyncMock()
|
||||
|
||||
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
|
||||
data = resp.json()
|
||||
assert data["database"] == "mydb"
|
||||
|
||||
async def test_capture_schema_instance_not_found(
|
||||
self, client: AsyncClient, admin_token: str
|
||||
):
|
||||
resp = await client.post(
|
||||
"/api/v1/schema-drift/instances/9999/capture",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
|
||||
async def test_capture_schema_forbidden_developer(
|
||||
self, client: AsyncClient, developer_token: str, test_instance
|
||||
):
|
||||
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_schema_dba_allowed(
|
||||
self, client: AsyncClient, dba_token: str, test_instance
|
||||
):
|
||||
with patch(
|
||||
"app.api.v1.endpoints.schema_drift.SchemaDriftDetector"
|
||||
) as MockDetector, patch(
|
||||
"app.api.v1.endpoints.schema_drift.decrypt_password",
|
||||
return_value="pw",
|
||||
):
|
||||
detector = MockDetector.return_value
|
||||
detector.capture_snapshot = AsyncMock(return_value=[])
|
||||
detector.save_baseline = AsyncMock()
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/capture",
|
||||
headers=auth_header(dba_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestCheckDrift:
|
||||
"""POST /api/v1/schema-drift/instances/{instance_id}/check"""
|
||||
|
||||
async def test_check_drift_no_changes(
|
||||
self, client: AsyncClient, admin_token: str, test_instance
|
||||
):
|
||||
mock_report = MagicMock()
|
||||
mock_report.to_dict.return_value = {
|
||||
"has_drift": False,
|
||||
"changes": [],
|
||||
"instance_id": test_instance.id,
|
||||
}
|
||||
with patch(
|
||||
"app.api.v1.endpoints.schema_drift.SchemaDriftDetector"
|
||||
) as MockDetector, patch(
|
||||
"app.api.v1.endpoints.schema_drift.decrypt_password",
|
||||
return_value="pw",
|
||||
):
|
||||
detector = MockDetector.return_value
|
||||
detector.detect_drift = AsyncMock(return_value=mock_report)
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/check",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["has_drift"] is False
|
||||
|
||||
async def test_check_drift_with_changes(
|
||||
self, client: AsyncClient, admin_token: str, test_instance
|
||||
):
|
||||
mock_report = MagicMock()
|
||||
mock_report.to_dict.return_value = {
|
||||
"has_drift": True,
|
||||
"changes": [
|
||||
{"type": "column_added", "table": "users", "column": "email"},
|
||||
],
|
||||
"instance_id": test_instance.id,
|
||||
}
|
||||
with patch(
|
||||
"app.api.v1.endpoints.schema_drift.SchemaDriftDetector"
|
||||
) as MockDetector, patch(
|
||||
"app.api.v1.endpoints.schema_drift.decrypt_password",
|
||||
return_value="pw",
|
||||
):
|
||||
detector = MockDetector.return_value
|
||||
detector.detect_drift = AsyncMock(return_value=mock_report)
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/check",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["has_drift"] is True
|
||||
assert len(data["changes"]) == 1
|
||||
|
||||
async def test_check_drift_instance_not_found(
|
||||
self, client: AsyncClient, admin_token: str
|
||||
):
|
||||
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_drift_dict_report(
|
||||
self, client: AsyncClient, admin_token: str, test_instance
|
||||
):
|
||||
"""Detector 返回普通 dict 而非对象时的序列化。"""
|
||||
plain_dict = {"has_drift": False, "instance_id": test_instance.id}
|
||||
with patch(
|
||||
"app.api.v1.endpoints.schema_drift.SchemaDriftDetector"
|
||||
) as MockDetector, patch(
|
||||
"app.api.v1.endpoints.schema_drift.decrypt_password",
|
||||
return_value="pw",
|
||||
):
|
||||
detector = MockDetector.return_value
|
||||
detector.detect_drift = AsyncMock(return_value=plain_dict)
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/check",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestSchemaHistory:
|
||||
"""GET /api/v1/schema-drift/instances/{instance_id}/history"""
|
||||
|
||||
async def test_schema_history(
|
||||
self, client: AsyncClient, admin_token: str, test_instance
|
||||
):
|
||||
resp = await client.get(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/history",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["instance_id"] == test_instance.id
|
||||
assert "history" in data
|
||||
|
||||
async def test_schema_history_dba_allowed(
|
||||
self, client: AsyncClient, dba_token: str, test_instance
|
||||
):
|
||||
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_schema_history_forbidden_guest(
|
||||
self, client: AsyncClient, guest_token: str, test_instance
|
||||
):
|
||||
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_schema_history_unauthorized(self, client: AsyncClient, test_instance):
|
||||
resp = await client.get(
|
||||
f"/api/v1/schema-drift/instances/{test_instance.id}/history",
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
Reference in new issue
Block a user