import sys import types from unittest.mock import patch, AsyncMock, MagicMock from httpx import AsyncClient from tests.conftest import auth_header def _make_aiomysql_mock(): aiomysql = MagicMock() aiomysql.connect = AsyncMock() aiomysql.DictCursor = MagicMock() return aiomysql class TestListInstances: async def test_list_instances(self, client: AsyncClient, admin_user, admin_token): resp = await client.get("/api/v1/instances", headers=auth_header(admin_token)) assert resp.status_code == 200 assert isinstance(resp.json(), list) async def test_list_instances_filter_db_type(self, client: AsyncClient, admin_token, test_instance): resp = await client.get( "/api/v1/instances?db_type=mysql", headers=auth_header(admin_token), ) assert resp.status_code == 200 data = resp.json() assert all(i["db_type"] == "mysql" for i in data) async def test_list_instances_filter_env(self, client: AsyncClient, admin_token, test_instance): resp = await client.get( "/api/v1/instances?env=development", headers=auth_header(admin_token), ) assert resp.status_code == 200 data = resp.json() assert all(i["env"] == "development" for i in data) async def test_list_instances_non_admin_no_group( self, client: AsyncClient, developer_user, developer_token, test_instance ): """非 admin 且无权限组,只能看到公开实例(group_id=None)。""" resp = await client.get( "/api/v1/instances", headers=auth_header(developer_token), ) assert resp.status_code == 200 # test_instance has no group_id, so it should be visible data = resp.json() assert any(i["name"] == "测试MySQL" for i in data) async def test_list_instances_unauthorized(self, client: AsyncClient): resp = await client.get("/api/v1/instances") assert resp.status_code == 401 class TestCreateInstance: async def test_create_instance(self, client: AsyncClient, admin_token): resp = await client.post("/api/v1/instances", headers=auth_header(admin_token), json={ "name": "生产MySQL", "db_type": "mysql", "host": "10.0.0.1", "port": 3306, "username": "root", "password": "secret", "database": "prod", "env": "production", }) assert resp.status_code == 201 data = resp.json() assert data["name"] == "生产MySQL" assert data["db_type"] == "mysql" async def test_create_instance_non_admin(self, client: AsyncClient, developer_token): resp = await client.post("/api/v1/instances", headers=auth_header(developer_token), json={ "name": "非法创建", "db_type": "mysql", "host": "10.0.0.1", "port": 3306, "username": "root", "password": "secret", }) assert resp.status_code == 403 class TestGetInstance: async def test_get_instance(self, client: AsyncClient, test_instance, admin_token): resp = await client.get( f"/api/v1/instances/{test_instance.id}", headers=auth_header(admin_token), ) assert resp.status_code == 200 assert resp.json()["name"] == "测试MySQL" async def test_get_nonexistent_instance(self, client: AsyncClient, admin_token): resp = await client.get("/api/v1/instances/9999", headers=auth_header(admin_token)) assert resp.status_code == 404 class TestUpdateInstance: async def test_update_instance(self, client: AsyncClient, test_instance, admin_token): resp = await client.put( f"/api/v1/instances/{test_instance.id}", headers=auth_header(admin_token), json={"name": "改名MySQL"}, ) assert resp.status_code == 200 assert resp.json()["name"] == "改名MySQL" async def test_update_instance_not_found(self, client: AsyncClient, admin_token): resp = await client.put( "/api/v1/instances/9999", headers=auth_header(admin_token), json={"name": "不存在"}, ) assert resp.status_code == 404 async def test_update_instance_forbidden_developer( self, client: AsyncClient, test_instance, developer_token ): resp = await client.put( f"/api/v1/instances/{test_instance.id}", headers=auth_header(developer_token), json={"name": "非法修改"}, ) assert resp.status_code == 403 async def test_update_instance_dba_allowed( self, client: AsyncClient, test_instance, dba_token ): resp = await client.put( f"/api/v1/instances/{test_instance.id}", headers=auth_header(dba_token), json={"description": "DBA修改的描述"}, ) assert resp.status_code == 200 class TestDeleteInstance: async def test_delete_instance(self, client: AsyncClient, test_instance, admin_token): resp = await client.delete( f"/api/v1/instances/{test_instance.id}", headers=auth_header(admin_token), ) assert resp.status_code == 200 async def test_delete_instance_not_found(self, client: AsyncClient, admin_token): resp = await client.delete( "/api/v1/instances/9999", headers=auth_header(admin_token), ) assert resp.status_code == 404 async def test_delete_instance_forbidden_developer( self, client: AsyncClient, test_instance, developer_token ): resp = await client.delete( f"/api/v1/instances/{test_instance.id}", headers=auth_header(developer_token), ) assert resp.status_code == 403 class TestConnectionTest: async def test_connection_test(self, client: AsyncClient, test_instance, admin_token): from contextlib import asynccontextmanager mock_cursor = AsyncMock() mock_cursor.execute = AsyncMock() mock_cursor.fetchone.return_value = ("8.0.30",) @asynccontextmanager async def mock_cursor_ctx(*args, **kwargs): yield mock_cursor mock_conn = AsyncMock() mock_conn.cursor = mock_cursor_ctx mock_conn.close = MagicMock() mock_aiomysql = _make_aiomysql_mock() mock_aiomysql.connect = AsyncMock(return_value=mock_conn) with patch.dict(sys.modules, {"aiomysql": mock_aiomysql}): resp = await client.post( f"/api/v1/instances/{test_instance.id}/test", headers=auth_header(admin_token), ) assert resp.status_code == 200 data = resp.json() assert data["success"] is True