""" 集成测试: 所有接口CRUD + 权限矩阵 覆盖: 用户/实例/分组/工单/工作流/公告/审计日志/Dashboard/字典/脱敏 """ import pytest from httpx import AsyncClient def _auth(token: str) -> dict: return {"Authorization": f"Bearer {token}"} # ══════════════════════════════════════════════════════════════ # 1. 用户管理 CRUD + 权限 # ══════════════════════════════════════════════════════════════ class TestUserCRUD: async def test_admin_list_users(self, client, admin_token): resp = await client.get("/api/v1/users", headers=_auth(admin_token)) assert resp.status_code == 200 async def test_admin_create_user(self, client, admin_token): resp = await client.post("/api/v1/users", json={ "username": "newuser", "display_name": "新用户", "role": "developer", "password": "Test123456", }, headers=_auth(admin_token)) assert resp.status_code in (200, 201) async def test_developer_cannot_create_user(self, client, developer_token): resp = await client.post("/api/v1/users", json={ "username": "hack", "display_name": "hack", "role": "admin", "password": "Test123456", }, headers=_auth(developer_token)) assert resp.status_code == 403 async def test_guest_cannot_list_users(self, client, guest_token): resp = await client.get("/api/v1/users", headers=_auth(guest_token)) assert resp.status_code in (403, 200) # 可能允许列表 async def test_get_me(self, client, admin_token): resp = await client.get("/api/v1/users/me", headers=_auth(admin_token)) assert resp.status_code == 200 assert resp.json()["username"] == "admin" async def test_unauthenticated_get_me(self, client): resp = await client.get("/api/v1/users/me") assert resp.status_code == 401 # ══════════════════════════════════════════════════════════════ # 2. 实例管理 CRUD + 权限 # ══════════════════════════════════════════════════════════════ class TestInstanceCRUD: async def test_create_mysql_instance(self, client, admin_token): resp = await client.post("/api/v1/instances", json={ "name": "MySQL测试", "db_type": "mysql", "host": "10.0.0.1", "port": 3306, "username": "root", "password": "pass", "database": "app_db", "env": "development", }, headers=_auth(admin_token)) assert resp.status_code == 201 data = resp.json() assert data["db_type"] == "mysql" assert data["id"] is not None async def test_list_instances(self, client, admin_token): resp = await client.get("/api/v1/instances", headers=_auth(admin_token)) assert resp.status_code == 200 async def test_get_accessible_instances(self, client, developer_token): resp = await client.get("/api/v1/instances/accessible", headers=_auth(developer_token)) assert resp.status_code == 200 async def test_update_instance(self, client, admin_token): # 先创建 inst = await client.post("/api/v1/instances", json={ "name": "待更新", "db_type": "mysql", "host": "10.0.0.1", "port": 3306, "username": "root", "password": "pass", "database": "app_db", "env": "development", }, headers=_auth(admin_token)) iid = inst.json()["id"] # 更新 resp = await client.put(f"/api/v1/instances/{iid}", json={ "name": "已更新", "env": "production", }, headers=_auth(admin_token)) assert resp.status_code == 200 async def test_delete_instance(self, client, admin_token): inst = await client.post("/api/v1/instances", json={ "name": "待删除", "db_type": "mysql", "host": "10.0.0.1", "port": 3306, "username": "root", "password": "pass", "database": "app_db", "env": "development", }, headers=_auth(admin_token)) iid = inst.json()["id"] resp = await client.delete(f"/api/v1/instances/{iid}", headers=_auth(admin_token)) assert resp.status_code in (200, 204) async def test_supported_db_types(self, client, admin_token): resp = await client.get("/api/v1/instances/supported-types", headers=_auth(admin_token)) assert resp.status_code in (200, 404) async def test_developer_cannot_create_instance(self, client, developer_token): resp = await client.post("/api/v1/instances", json={ "name": "非法创建", "db_type": "mysql", "host": "10.0.0.1", "port": 3306, "username": "root", "password": "pass", "database": "app_db", }, headers=_auth(developer_token)) assert resp.status_code in (403, 201) # 可能允许 # ══════════════════════════════════════════════════════════════ # 3. 实例分组 CRUD # ══════════════════════════════════════════════════════════════ class TestInstanceGroupCRUD: async def test_create_group(self, client, admin_token): resp = await client.post("/api/v1/instance-groups", json={ "name": "生产环境组", "description": "所有生产实例", }, headers=_auth(admin_token)) assert resp.status_code in (200, 201) return resp.json()["id"] async def test_list_groups(self, client, admin_token): resp = await client.get("/api/v1/instance-groups", headers=_auth(admin_token)) assert resp.status_code == 200 async def test_full_group_lifecycle(self, client, admin_token): # 创建 grp = await client.post("/api/v1/instance-groups", json={ "name": "生命周期组", "description": "测试", }, headers=_auth(admin_token)) gid = grp.json()["id"] # 更新 upd = await client.put(f"/api/v1/instance-groups/{gid}", json={ "name": "已更新组名", "description": "更新描述", }, headers=_auth(admin_token)) assert upd.status_code == 200 # 创建实例并添加到组 inst = await client.post("/api/v1/instances", json={ "name": "组成员", "db_type": "mysql", "host": "10.0.0.1", "port": 3306, "username": "root", "password": "pass", "database": "app_db", }, headers=_auth(admin_token)) iid = inst.json()["id"] add = await client.post( f"/api/v1/instance-groups/{gid}/instances/{iid}", headers=_auth(admin_token)) assert add.status_code in (200, 201) # 移除实例 remove = await client.delete( f"/api/v1/instance-groups/{gid}/instances/{iid}", headers=_auth(admin_token)) assert remove.status_code in (200, 204) # 删除组 delete = await client.delete(f"/api/v1/instance-groups/{gid}", headers=_auth(admin_token)) assert delete.status_code in (200, 204) # ══════════════════════════════════════════════════════════════ # 4. 工作流 CRUD + 权限 # ══════════════════════════════════════════════════════════════ class TestWorkflowCRUD: async def test_create_workflow_admin_only(self, client, admin_token, developer_token): # admin 可创建 resp = await client.post("/api/v1/workflows", json={ "name": "审批流1", "description": "测试", "is_default": True, "is_active": True, "steps": [ {"step_order": 1, "step_name": "DBA审批", "step_type": "role", "required_role": "dba", "approval_mode": "any"}, ] }, headers=_auth(admin_token)) assert resp.status_code == 201 async def test_developer_cannot_create_workflow(self, client, developer_token): resp = await client.post("/api/v1/workflows", json={ "name": "非法创建", "steps": [], }, headers=_auth(developer_token)) assert resp.status_code == 403 async def test_workflow_set_default(self, client, admin_token): wf = await client.post("/api/v1/workflows", json={ "name": "默认流", "is_default": False, "is_active": True, "steps": [], }, headers=_auth(admin_token)) wid = wf.json()["id"] resp = await client.post(f"/api/v1/workflows/{wid}/set-default", headers=_auth(admin_token)) assert resp.status_code == 200 # ══════════════════════════════════════════════════════════════ # 5. 公告 CRUD + 权限 # ══════════════════════════════════════════════════════════════ class TestAnnouncementCRUD: async def test_full_lifecycle(self, client, admin_token, developer_token): # 创建 resp = await client.post("/api/v1/announcements", json={ "title": "测试公告", "content": "公告内容", }, headers=_auth(admin_token)) assert resp.status_code in (200, 201) aid = resp.json()["id"] # 所有人可看列表 list_resp = await client.get("/api/v1/announcements", headers=_auth(developer_token)) assert list_resp.status_code == 200 # 更新 upd = await client.put(f"/api/v1/announcements/{aid}", json={ "title": "更新标题", "content": "更新内容", }, headers=_auth(admin_token)) assert upd.status_code == 200 # developer 不能创建 fail = await client.post("/api/v1/announcements", json={ "title": "非法", "content": "test", }, headers=_auth(developer_token)) assert fail.status_code == 403 # 删除 delete = await client.delete(f"/api/v1/announcements/{aid}", headers=_auth(admin_token)) assert delete.status_code in (200, 204) # ══════════════════════════════════════════════════════════════ # 6. Dashboard # ══════════════════════════════════════════════════════════════ class TestDashboardEndpoints: async def test_stats(self, client, admin_token): resp = await client.get("/api/v1/dashboard/stats", headers=_auth(admin_token)) assert resp.status_code == 200 async def test_overview(self, client, admin_token): resp = await client.get("/api/v1/dashboard/overview", headers=_auth(admin_token)) assert resp.status_code == 200 async def test_trend(self, client, admin_token): resp = await client.get("/api/v1/dashboard/trend", headers=_auth(admin_token)) assert resp.status_code == 200 async def test_developer_can_view_dashboard(self, client, developer_token): resp = await client.get("/api/v1/dashboard/stats", headers=_auth(developer_token)) assert resp.status_code in (200, 403) # ══════════════════════════════════════════════════════════════ # 7. 审计日志 # ══════════════════════════════════════════════════════════════ class TestAuditLogEndpoints: async def test_list_audit_logs(self, client, admin_token): resp = await client.get("/api/v1/audit-logs", headers=_auth(admin_token)) assert resp.status_code == 200 async def test_audit_logs_pagination(self, client, admin_token): resp = await client.get("/api/v1/audit-logs?page=1&page_size=10", headers=_auth(admin_token)) assert resp.status_code == 200 # ══════════════════════════════════════════════════════════════ # 8. 数据字典 # ══════════════════════════════════════════════════════════════ class TestDictionaryEndpoints: async def test_mysql_dictionary(self, client, admin_token): resp = await client.get("/api/v1/dictionary/mysql", headers=_auth(admin_token)) assert resp.status_code == 200 async def test_redis_dictionary(self, client, admin_token): resp = await client.get("/api/v1/dictionary/redis", headers=_auth(admin_token)) assert resp.status_code == 200 async def test_mysql_commands(self, client, admin_token): resp = await client.get("/api/v1/dictionary/mysql/commands", headers=_auth(admin_token)) assert resp.status_code == 200 async def test_redis_tips(self, client, admin_token): resp = await client.get("/api/v1/dictionary/redis/tips", headers=_auth(admin_token)) assert resp.status_code == 200 # ══════════════════════════════════════════════════════════════ # 9. 脱敏规则 # ══════════════════════════════════════════════════════════════ class TestMaskingRuleCRUD: async def test_masking_rules_lifecycle(self, client, admin_token): # 先创建实例 inst = await client.post("/api/v1/instances", json={ "name": "脱敏测试", "db_type": "mysql", "host": "10.0.0.1", "port": 3306, "username": "root", "password": "pass", "database": "app_db", }, headers=_auth(admin_token)) iid = inst.json()["id"] # 列表 list_resp = await client.get(f"/api/v1/instances/{iid}/masking-rules", headers=_auth(admin_token)) assert list_resp.status_code == 200 # ══════════════════════════════════════════════════════════════ # 10. 全局权限矩阵验证 # ══════════════════════════════════════════════════════════════ class TestPermissionMatrix: """验证每个角色对每个模块的权限""" async def test_guest_write_forbidden(self, client, guest_token): """guest 不能写任何东西""" # 创建实例 r1 = await client.post("/api/v1/instances", json={ "name": "x", "db_type": "mysql", "host": "1.1.1.1", "port": 3306, "username": "r", "password": "p", "database": "d", }, headers=_auth(guest_token)) assert r1.status_code == 403 # 创建工单 r2 = await client.post("/api/v1/tickets", json={ "title": "x", "db_type": "mysql", "instance_id": 1, "target_database": "d", "sql_content": "SELECT 1", "reason": "x", }, headers=_auth(guest_token)) assert r2.status_code == 403 async def test_unauthenticated_all_forbidden(self, client): """未认证用户所有写操作 401""" endpoints = [ ("POST", "/api/v1/instances", {"name": "x", "db_type": "mysql", "host": "1.1.1.1", "port": 3306, "username": "r", "password": "p", "database": "d"}), ("POST", "/api/v1/tickets", {"title": "x", "db_type": "mysql", "instance_id": 1, "target_database": "d", "sql_content": "SELECT 1", "reason": "x"}), ("POST", "/api/v1/announcements", {"title": "x", "content": "x"}), ("POST", "/api/v1/workflows", {"name": "x", "steps": []}), ] for method, url, body in endpoints: if method == "POST": resp = await client.post(url, json=body) assert resp.status_code == 401, f"{method} {url} should be 401"