"""Schema 漂移检测 API 端点测试。""" import pytest pytestmark = pytest.mark.skip(reason="Go backend endpoint not implemented") 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