""" 集成测试: 查询执行 + 查询历史 + 限流 + SQL助手 + 数据库浏览 覆盖: 查询权限/历史隔离/设置生效/SQL助手/数据库浏览接口 """ import pytest from unittest.mock import AsyncMock, patch, MagicMock from httpx import AsyncClient def _auth(token: str) -> dict: return {"Authorization": f"Bearer {token}"} async def _create_mysql_instance(client, admin_token): resp = await client.post("/api/v1/instances", json={ "name": "查询测试MySQL", "db_type": "mysql", "host": "127.0.0.1", "port": 3306, "username": "root", "password": "test", "database": "test_db", "env": "development", }, headers=_auth(admin_token)) assert resp.status_code == 201 return resp.json()["id"] async def _create_redis_instance(client, admin_token): resp = await client.post("/api/v1/instances", json={ "name": "查询测试Redis", "db_type": "redis", "host": "127.0.0.1", "port": 6379, "username": "", "password": "test", "database": "0", "env": "development", }, headers=_auth(admin_token)) assert resp.status_code == 201 return resp.json()["id"] # ══════════════════════════════════════════════════════════════ # 1. 查询权限控制 # ══════════════════════════════════════════════════════════════ class TestQueryPermissions: async def test_unauthenticated_query_401(self, client, admin_token): iid = await _create_mysql_instance(client, admin_token) resp = await client.post("/api/v1/query/execute", json={ "instance_id": iid, "database": "test_db", "sql": "SELECT 1", }) assert resp.status_code == 401 async def test_guest_cannot_query(self, client, admin_token, guest_token): iid = await _create_mysql_instance(client, admin_token) resp = await client.post("/api/v1/query/execute", json={ "instance_id": iid, "database": "test_db", "sql": "SELECT 1", }, headers=_auth(guest_token)) assert resp.status_code in (403, 401) # ══════════════════════════════════════════════════════════════ # 2. 查询历史 # ══════════════════════════════════════════════════════════════ class TestQueryHistory: async def test_my_history_empty(self, client, admin_token): resp = await client.get("/api/v1/query/history", headers=_auth(admin_token)) assert resp.status_code in (200, 404) async def test_history_user_isolation(self, client, admin_token, developer_token): """developer 不应该看到 admin 的查询历史""" # admin 查询历史 admin_history = await client.get("/api/v1/query/history", headers=_auth(admin_token)) # developer 查询历史 dev_history = await client.get("/api/v1/query/history", headers=_auth(developer_token)) # 两者应该独立 assert admin_history.status_code in (200, 404) assert dev_history.status_code in (200, 404) # ══════════════════════════════════════════════════════════════ # 3. 查询设置生效 # ══════════════════════════════════════════════════════════════ class TestQuerySettings: async def test_query_max_length_setting(self, client, admin_token): """设置 query_max_length 后应生效""" # 更新设置 await client.put("/api/v1/settings", json=[ {"key": "query_max_length", "value": "500"}, ], headers=_auth(admin_token)) # 读取验证设置已更新 resp = await client.get("/api/v1/settings", headers=_auth(admin_token)) assert resp.status_code == 200 # ══════════════════════════════════════════════════════════════ # 4. SQL 助手 # ══════════════════════════════════════════════════════════════ class TestSQLAssistant: async def test_format_sql(self, client, admin_token): resp = await client.post("/api/v1/sql-assistant", json={ "sql": "select id,name from users where id=1", "action": "format", }, headers=_auth(admin_token)) assert resp.status_code in (200, 404) # 404 if endpoint not in this version async def test_explain_sql(self, client, admin_token): resp = await client.post("/api/v1/sql-assistant", json={ "sql": "SELECT * FROM users", "action": "explain", }, headers=_auth(admin_token)) assert resp.status_code in (200, 404) # ══════════════════════════════════════════════════════════════ # 5. 数据库浏览接口 # ══════════════════════════════════════════════════════════════ class TestDatabaseBrowse: async def test_list_databases(self, client, admin_token): iid = await _create_mysql_instance(client, admin_token) resp = await client.get(f"/api/v1/instances/{iid}/databases", headers=_auth(admin_token)) # 无真实连接可能返回错误,但不应该500 assert resp.status_code in (200, 400, 500) async def test_list_tables(self, client, admin_token): iid = await _create_mysql_instance(client, admin_token) resp = await client.get(f"/api/v1/instances/{iid}/tables", headers=_auth(admin_token)) assert resp.status_code in (200, 400, 500) async def test_instance_detail_databases(self, client, admin_token): iid = await _create_mysql_instance(client, admin_token) resp = await client.get(f"/api/v1/instances/{iid}/detail/databases", headers=_auth(admin_token)) assert resp.status_code in (200, 400, 500) async def test_instance_detail_tables(self, client, admin_token): iid = await _create_mysql_instance(client, admin_token) resp = await client.get(f"/api/v1/instances/{iid}/detail/tables", headers=_auth(admin_token)) assert resp.status_code in (200, 400, 500) async def test_instance_detail_status(self, client, admin_token): iid = await _create_mysql_instance(client, admin_token) resp = await client.get(f"/api/v1/instances/{iid}/detail/status", headers=_auth(admin_token)) assert resp.status_code in (200, 400, 500) async def test_instance_detail_variables(self, client, admin_token): iid = await _create_mysql_instance(client, admin_token) resp = await client.get(f"/api/v1/instances/{iid}/detail/variables", headers=_auth(admin_token)) assert resp.status_code in (200, 400, 500) async def test_instance_detail_processlist(self, client, admin_token): iid = await _create_mysql_instance(client, admin_token) resp = await client.get(f"/api/v1/instances/{iid}/detail/processlist", headers=_auth(admin_token)) assert resp.status_code in (200, 400, 500) # ══════════════════════════════════════════════════════════════ # 6. Redis 专用接口 # ══════════════════════════════════════════════════════════════ class TestRedisEndpoints: async def test_redis_info(self, client, admin_token): rid = await _create_redis_instance(client, admin_token) resp = await client.get(f"/api/v1/redis/{rid}/info", headers=_auth(admin_token)) assert resp.status_code in (200, 400, 500) async def test_redis_keys(self, client, admin_token): rid = await _create_redis_instance(client, admin_token) resp = await client.get(f"/api/v1/redis/{rid}/keys", headers=_auth(admin_token)) assert resp.status_code in (200, 400, 500) async def test_redis_slowlog(self, client, admin_token): rid = await _create_redis_instance(client, admin_token) resp = await client.get(f"/api/v1/redis/{rid}/slowlog", headers=_auth(admin_token)) assert resp.status_code in (200, 400, 500) async def test_redis_clients(self, client, admin_token): rid = await _create_redis_instance(client, admin_token) resp = await client.get(f"/api/v1/redis/{rid}/clients", headers=_auth(admin_token)) assert resp.status_code in (200, 400, 500) # ══════════════════════════════════════════════════════════════ # 7. 备份接口 # ══════════════════════════════════════════════════════════════ class TestBackupEndpoints: async def test_list_backups(self, client, admin_token): resp = await client.get("/api/v1/backups", headers=_auth(admin_token)) assert resp.status_code in (200, 404) # ══════════════════════════════════════════════════════════════ # 8. DB 类型接口 # ══════════════════════════════════════════════════════════════ class TestDBTypesEndpoint: async def test_get_db_types(self, client, admin_token): resp = await client.get("/api/v1/db-types", headers=_auth(admin_token)) assert resp.status_code in (200, 404) async def test_get_mysql_commands(self, client, admin_token): resp = await client.get("/api/v1/db-types/mysql/commands", headers=_auth(admin_token)) assert resp.status_code in (200, 404) async def test_get_redis_tips(self, client, admin_token): resp = await client.get("/api/v1/db-types/redis/tips", headers=_auth(admin_token)) assert resp.status_code in (200, 404) # ══════════════════════════════════════════════════════════════ # 9. 认证端点 # ══════════════════════════════════════════════════════════════ class TestAuthEndpoints: async def test_login_success(self, client, admin_token): """admin_token fixture 证明 login 能正常工作""" assert admin_token is not None async def test_login_invalid_credentials(self, client, admin_user): resp = await client.post("/api/v1/auth/login", json={ "username": "admin", "password": "wrong_password", }) assert resp.status_code in (401, 400) async def test_login_nonexistent_user(self, client): resp = await client.post("/api/v1/auth/login", json={ "username": "nonexistent", "password": "test", }) assert resp.status_code in (401, 400) async def test_get_current_user(self, client, admin_token): resp = await client.get("/api/v1/users/me", headers=_auth(admin_token)) assert resp.status_code == 200 data = resp.json() assert data["role"] == "admin"