Files
opsxsql/backend/tests/test_backup.py
T
logan 4c9427e546 sync: 同步 sql-platform-pro 最新代码 — 权限体系安全修复 v0.8.1
- P0: 实例访问控制全线接入权限组 (829测试)
- P1: permissions字段激活 + 操作级权限检查 (843测试)
- 前端: 删除数据权限页面, 权限组合并为唯一入口
- 新增: CodeMirror编辑器 + 查询模板 + 慢查询分析
- 新增: MongoDB管理页面 + Dashboard增强
- 修复: Pydantic V2迁移 + 前端TS错误修复
2026-05-23 14:41:06 +08:00

396 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""备份管理 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
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)