"""回滚 SQL API 测试 — 覆盖 generate-rollback 和 preview 端点。""" import pytest from unittest.mock import patch, AsyncMock, MagicMock from httpx import AsyncClient from tests.conftest import auth_header async def _create_ticket(client: AsyncClient, admin_token: str, instance_id: int) -> dict: """Helper: create a ticket and return the response body.""" payload = { "title": "测试工单-回滚验证", "instance_id": instance_id, "target_database": "test_db", "sql_content": "UPDATE users SET name='test' WHERE id=1", "reason": "执行回滚测试用例", "priority": "medium", "db_type": "mysql", } resp = await client.post( "/api/v1/tickets", json=payload, headers=auth_header(admin_token) ) return resp # ────────────────────────── Generate Rollback Tests ────────────────────────── class TestGenerateRollback: @pytest.mark.asyncio async def test_generate_success(self, client, admin_token, test_instance): """生成回滚 SQL — mock RollbackGenerator 以避免真实连接。""" ticket_resp = await _create_ticket(client, admin_token, test_instance.id) assert ticket_resp.status_code == 201 ticket_id = ticket_resp.json()["id"] with patch("app.api.v1.endpoints.rollback.RollbackGenerator") as MockGen: instance_mock = MagicMock() instance_mock.generate = AsyncMock(return_value="SELECT * FROM users WHERE id=1") MockGen.return_value = instance_mock resp = await client.post( f"/api/v1/tickets/{ticket_id}/generate-rollback", json={"auto_save": False}, headers=auth_header(admin_token), ) assert resp.status_code == 200 body = resp.json() assert body["ticket_id"] == ticket_id assert "UPDATE" in body["original_sql"] assert body["rollback_sql"] == "SELECT * FROM users WHERE id=1" assert body["saved"] is False @pytest.mark.asyncio async def test_generate_with_auto_save(self, client, admin_token, test_instance): """auto_save=True 应保存回滚 SQL 到工单。""" ticket_resp = await _create_ticket(client, admin_token, test_instance.id) ticket_id = ticket_resp.json()["id"] with patch("app.api.v1.endpoints.rollback.RollbackGenerator") as MockGen: instance_mock = MagicMock() instance_mock.generate = AsyncMock(return_value="DELETE FROM users WHERE id=1 AND name='test'") MockGen.return_value = instance_mock resp = await client.post( f"/api/v1/tickets/{ticket_id}/generate-rollback", json={"auto_save": True}, headers=auth_header(admin_token), ) assert resp.status_code == 200 assert resp.json()["saved"] is True @pytest.mark.asyncio async def test_generate_ticket_not_found(self, client, admin_token): resp = await client.post( "/api/v1/tickets/9999/generate-rollback", json={"auto_save": False}, headers=auth_header(admin_token), ) assert resp.status_code == 404 @pytest.mark.asyncio async def test_generate_developer_forbidden(self, client, developer_token, test_instance): resp = await client.post( "/api/v1/tickets/1/generate-rollback", json={"auto_save": False}, headers=auth_header(developer_token), ) assert resp.status_code == 403 @pytest.mark.asyncio async def test_generate_guest_forbidden(self, client, guest_token): resp = await client.post( "/api/v1/tickets/1/generate-rollback", json={"auto_save": False}, headers=auth_header(guest_token), ) assert resp.status_code == 403 # ────────────────────────── Preview Rollback Tests ────────────────────────── class TestPreviewRollback: @pytest.mark.asyncio async def test_preview_success(self, client, admin_token, test_instance): with patch("app.api.v1.endpoints.rollback.RollbackGenerator") as MockGen: instance_mock = MagicMock() instance_mock.preview = AsyncMock(return_value="SELECT * FROM users WHERE id=1") MockGen.return_value = instance_mock resp = await client.post( "/api/v1/rollback/preview", json={ "instance_id": test_instance.id, "database": "test_db", "sql": "UPDATE users SET name='x' WHERE id=1", }, headers=auth_header(admin_token), ) assert resp.status_code == 200 body = resp.json() assert "UPDATE" in body["original_sql"] assert body["rollback_sql"] == "SELECT * FROM users WHERE id=1" @pytest.mark.asyncio async def test_preview_instance_not_found(self, client, admin_token): with patch("app.api.v1.endpoints.rollback.RollbackGenerator") as MockGen: MockGen.return_value = MagicMock() resp = await client.post( "/api/v1/rollback/preview", json={ "instance_id": 9999, "database": "test_db", "sql": "DELETE FROM t WHERE id=1", }, headers=auth_header(admin_token), ) assert resp.status_code == 404 @pytest.mark.asyncio async def test_preview_any_authenticated_user(self, client, developer_token, test_instance): """preview 允许所有认证用户 (get_current_user)。""" with patch("app.api.v1.endpoints.rollback.RollbackGenerator") as MockGen: instance_mock = MagicMock() instance_mock.preview = AsyncMock(return_value="-- 回滚SQL预览") MockGen.return_value = instance_mock resp = await client.post( "/api/v1/rollback/preview", json={ "instance_id": test_instance.id, "database": "test_db", "sql": "UPDATE t SET a=1 WHERE id=1", }, headers=auth_header(developer_token), ) assert resp.status_code == 200 @pytest.mark.asyncio async def test_preview_no_auth_401(self, client): resp = await client.post( "/api/v1/rollback/preview", json={ "instance_id": 1, "database": "test_db", "sql": "UPDATE t SET a=1", }, ) assert resp.status_code == 401 @pytest.mark.asyncio async def test_preview_validation_error(self, client, admin_token): """缺少必填字段 → 422。""" resp = await client.post( "/api/v1/rollback/preview", json={"instance_id": 1}, headers=auth_header(admin_token), ) assert resp.status_code == 422