"""备份管理 API 端点测试。 测试范围: - GET /api/v1/backups — 列表(支持 instance_id 过滤) - POST /api/v1/backups — 创建备份(201, 自动生成 filename) - GET /api/v1/backups/{id} — 备份详情(404 不存在) 覆盖要点: - CRUD 基本流程 - 权限控制(admin/dba 可访问, developer/guest → 403) - instance_id 过滤 - filename 自动生成(database_instance_id.sql) - 404 不存在的备份 """ import pytest pytestmark = pytest.mark.skip(reason="Go backend endpoint not implemented") from httpx import AsyncClient from tests.conftest import auth_header BASE = "/api/v1/backups" # ══════════════════════════════════════════════════════════════════ # 权限测试 — developer / guest → 403 # ══════════════════════════════════════════════════════════════════ class TestBackupPermissions: """备份路由权限控制:仅 admin / dba 可访问。""" @pytest.mark.asyncio async def test_list_backups_developer_forbidden( self, client: AsyncClient, developer_token: str ): """developer 访问列表 → 403。""" resp = await client.get(BASE, headers=auth_header(developer_token)) assert resp.status_code == 403 @pytest.mark.asyncio async def test_list_backups_guest_forbidden( self, client: AsyncClient, guest_token: str ): """guest 访问列表 → 403。""" resp = await client.get(BASE, headers=auth_header(guest_token)) assert resp.status_code == 403 @pytest.mark.asyncio async def test_create_backup_developer_forbidden( self, client: AsyncClient, developer_token: str ): """developer 创建备份 → 403。""" resp = await client.post( BASE, json={"title": "t", "instance_id": 1, "database": "db"}, headers=auth_header(developer_token), ) assert resp.status_code == 403 @pytest.mark.asyncio async def test_create_backup_guest_forbidden( self, client: AsyncClient, guest_token: str ): """guest 创建备份 → 403。""" resp = await client.post( BASE, json={"title": "t", "instance_id": 1, "database": "db"}, headers=auth_header(guest_token), ) assert resp.status_code == 403 @pytest.mark.asyncio async def test_get_backup_developer_forbidden( self, client: AsyncClient, developer_token: str ): """developer 获取详情 → 403。""" resp = await client.get(f"{BASE}/1", headers=auth_header(developer_token)) assert resp.status_code == 403 @pytest.mark.asyncio async def test_get_backup_guest_forbidden( self, client: AsyncClient, guest_token: str ): """guest 获取详情 → 403。""" resp = await client.get(f"{BASE}/1", headers=auth_header(guest_token)) assert resp.status_code == 403 @pytest.mark.asyncio async def test_unauthenticated_access(self, client: AsyncClient): """未认证 → 401。""" resp = await client.get(BASE) assert resp.status_code == 401 # ══════════════════════════════════════════════════════════════════ # 创建备份 — POST /api/v1/backups # ══════════════════════════════════════════════════════════════════ class TestCreateBackup: """创建备份测试。""" @pytest.mark.asyncio async def test_create_backup_admin( self, client: AsyncClient, admin_token: str, test_instance ): """admin 创建备份 → 201,字段完整。""" payload = { "title": "每日全量备份", "instance_id": test_instance.id, "database": "production_db", "remark": "定时任务触发", } resp = await client.post(BASE, json=payload, headers=auth_header(admin_token)) assert resp.status_code == 201 data = resp.json() assert data["id"] is not None assert data["title"] == "每日全量备份" assert data["instance_id"] == test_instance.id assert data["database"] == "production_db" assert data["status"] == "pending" assert data["size_bytes"] == 0 assert data["remark"] == "定时任务触发" # created_by 应为 admin 用户 id assert data["created_by"] is not None # created_at 应有值 assert data["created_at"] is not None @pytest.mark.asyncio async def test_create_backup_filename_auto_generated( self, client: AsyncClient, admin_token: str, test_instance ): """filename 自动生成为 {database}_{instance_id}.sql。""" payload = { "title": "测试文件名", "instance_id": test_instance.id, "database": "mydb", } resp = await client.post(BASE, json=payload, headers=auth_header(admin_token)) assert resp.status_code == 201 data = resp.json() expected_filename = f"mydb_{test_instance.id}.sql" assert data["filename"] == expected_filename @pytest.mark.asyncio async def test_create_backup_dba( self, client: AsyncClient, dba_token: str, test_instance ): """dba 创建备份 → 201。""" payload = { "title": "DBA手动备份", "instance_id": test_instance.id, "database": "orders", } resp = await client.post(BASE, json=payload, headers=auth_header(dba_token)) assert resp.status_code == 201 assert resp.json()["title"] == "DBA手动备份" @pytest.mark.asyncio async def test_create_backup_without_remark( self, client: AsyncClient, admin_token: str, test_instance ): """不传 remark → remark 为 None。""" payload = { "title": "无备注备份", "instance_id": test_instance.id, "database": "test_db", } resp = await client.post(BASE, json=payload, headers=auth_header(admin_token)) assert resp.status_code == 201 assert resp.json()["remark"] is None @pytest.mark.asyncio async def test_create_backup_sets_created_by( self, client: AsyncClient, dba_token: str, dba_user, test_instance ): """created_by 应为当前登录用户 id。""" payload = { "title": "验证created_by", "instance_id": test_instance.id, "database": "hr_db", } resp = await client.post(BASE, json=payload, headers=auth_header(dba_token)) assert resp.status_code == 201 assert resp.json()["created_by"] == dba_user.id @pytest.mark.asyncio async def test_create_backup_nonexistent_instance_id( self, client: AsyncClient, admin_token: str ): """instance_id 可以是不存在的(无外键约束),仍然创建成功。""" payload = { "title": "幽灵实例备份", "instance_id": 99999, "database": "ghost_db", } resp = await client.post(BASE, json=payload, headers=auth_header(admin_token)) assert resp.status_code == 201 assert resp.json()["instance_id"] == 99999 # ══════════════════════════════════════════════════════════════════ # 获取备份详情 — GET /api/v1/backups/{backup_id} # ══════════════════════════════════════════════════════════════════ class TestGetBackup: """获取备份详情测试。""" @pytest.mark.asyncio async def test_get_backup_by_id( self, client: AsyncClient, admin_token: str, test_instance ): """创建后通过 id 获取 → 数据一致。""" # 先创建 payload = { "title": "查询测试", "instance_id": test_instance.id, "database": "inventory", "remark": "验证查询", } create_resp = await client.post( BASE, json=payload, headers=auth_header(admin_token) ) assert create_resp.status_code == 201 backup_id = create_resp.json()["id"] # 再查询 get_resp = await client.get( f"{BASE}/{backup_id}", headers=auth_header(admin_token) ) assert get_resp.status_code == 200 data = get_resp.json() assert data["id"] == backup_id assert data["title"] == "查询测试" assert data["database"] == "inventory" assert data["remark"] == "验证查询" @pytest.mark.asyncio async def test_get_backup_not_found(self, client: AsyncClient, admin_token: str): """获取不存在的 backup_id → 404。""" resp = await client.get(f"{BASE}/99999", headers=auth_header(admin_token)) assert resp.status_code == 404 assert "不存在" in resp.json()["detail"] @pytest.mark.asyncio async def test_get_backup_dba( self, client: AsyncClient, dba_token: str, admin_token: str, test_instance ): """dba 可以获取 admin 创建的备份。""" payload = { "title": "跨角色查看", "instance_id": test_instance.id, "database": "shared_db", } create_resp = await client.post( BASE, json=payload, headers=auth_header(admin_token) ) backup_id = create_resp.json()["id"] get_resp = await client.get( f"{BASE}/{backup_id}", headers=auth_header(dba_token) ) assert get_resp.status_code == 200 assert get_resp.json()["id"] == backup_id # ══════════════════════════════════════════════════════════════════ # 列表查询 — GET /api/v1/backups # ══════════════════════════════════════════════════════════════════ class TestListBackups: """备份列表查询测试。""" @pytest.mark.asyncio async def test_list_backups_empty(self, client: AsyncClient, admin_token: str): """无备份时返回空列表。""" resp = await client.get(BASE, headers=auth_header(admin_token)) assert resp.status_code == 200 assert resp.json() == [] @pytest.mark.asyncio async def test_list_backups_returns_created( self, client: AsyncClient, admin_token: str, test_instance ): """创建后列表包含该备份。""" payload = { "title": "列表验证", "instance_id": test_instance.id, "database": "catalog", } await client.post(BASE, json=payload, headers=auth_header(admin_token)) resp = await client.get(BASE, headers=auth_header(admin_token)) assert resp.status_code == 200 data = resp.json() assert len(data) >= 1 assert any(b["title"] == "列表验证" for b in data) @pytest.mark.asyncio async def test_list_backups_filter_by_instance_id( self, client: AsyncClient, admin_token: str, test_instance ): """instance_id 过滤:只返回对应实例的备份。""" # 创建两个不同 instance_id 的备份 await client.post( BASE, json={"title": "目标实例", "instance_id": test_instance.id, "database": "a"}, headers=auth_header(admin_token), ) await client.post( BASE, json={"title": "其他实例", "instance_id": 99999, "database": "b"}, headers=auth_header(admin_token), ) resp = await client.get( BASE, params={"instance_id": test_instance.id}, headers=auth_header(admin_token), ) assert resp.status_code == 200 data = resp.json() assert len(data) == 1 assert data[0]["title"] == "目标实例" assert data[0]["instance_id"] == test_instance.id @pytest.mark.asyncio async def test_list_backups_filter_nonexistent_instance( self, client: AsyncClient, admin_token: str, test_instance ): """过滤不存在的 instance_id → 空列表。""" # 先创建一个备份确保数据库不为空 await client.post( BASE, json={ "title": "占位", "instance_id": test_instance.id, "database": "x", }, headers=auth_header(admin_token), ) resp = await client.get( BASE, params={"instance_id": 88888}, headers=auth_header(admin_token), ) assert resp.status_code == 200 assert resp.json() == [] @pytest.mark.asyncio async def test_list_backups_dba( self, client: AsyncClient, dba_token: str, admin_token: str, test_instance ): """dba 可以查看备份列表。""" await client.post( BASE, json={ "title": "DBA列表", "instance_id": test_instance.id, "database": "dba_db", }, headers=auth_header(admin_token), ) resp = await client.get(BASE, headers=auth_header(dba_token)) assert resp.status_code == 200 assert len(resp.json()) >= 1 @pytest.mark.asyncio async def test_list_backups_multiple_ordered_desc( self, client: AsyncClient, admin_token: str, test_instance ): """多条备份按 id 降序排列。""" for i in range(3): await client.post( BASE, json={ "title": f"备份{i}", "instance_id": test_instance.id, "database": f"db{i}", }, headers=auth_header(admin_token), ) resp = await client.get(BASE, headers=auth_header(admin_token)) assert resp.status_code == 200 data = resp.json() assert len(data) == 3 # 按 id 降序 ids = [b["id"] for b in data] assert ids == sorted(ids, reverse=True)