- P0: 实例访问控制全线接入权限组 (829测试) - P1: permissions字段激活 + 操作级权限检查 (843测试) - 前端: 删除数据权限页面, 权限组合并为唯一入口 - 新增: CodeMirror编辑器 + 查询模板 + 慢查询分析 - 新增: MongoDB管理页面 + Dashboard增强 - 修复: Pydantic V2迁移 + 前端TS错误修复
712 lines
23 KiB
Python
712 lines
23 KiB
Python
"""MongoDB 管理 API 端点测试 — 所有 MongoConnector 方法均 mock。"""
|
||
import pytest
|
||
from unittest.mock import AsyncMock, patch
|
||
|
||
from tests.conftest import auth_header
|
||
from app.models.instance import Instance
|
||
|
||
|
||
# ── 辅助工具 ──────────────────────────────────────────────────────
|
||
|
||
MONGO_INSTANCE_DATA = {
|
||
"name": "test-mongo",
|
||
"db_type": "mongo",
|
||
"host": "127.0.0.1",
|
||
"port": 27017,
|
||
"username": "root",
|
||
"password": "test",
|
||
"database": "test_mongo",
|
||
"env": "development",
|
||
"status": "disconnected",
|
||
}
|
||
|
||
MYSQL_INSTANCE_DATA = {
|
||
"name": "Test MySQL",
|
||
"db_type": "mysql",
|
||
"host": "127.0.0.1",
|
||
"port": 3306,
|
||
"username": "root",
|
||
"password": "test123",
|
||
"database": "test_db",
|
||
"env": "development",
|
||
}
|
||
|
||
|
||
async def _create_mongo_instance(client, admin_token):
|
||
"""创建一个 mongo 类型的实例并返回其 ID。"""
|
||
resp = await client.post(
|
||
"/api/v1/instances",
|
||
json=MONGO_INSTANCE_DATA,
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 201
|
||
return resp.json()["id"]
|
||
|
||
|
||
async def _create_mysql_instance(client, admin_token):
|
||
"""创建一个 mysql 类型的实例并返回其 ID。"""
|
||
resp = await client.post(
|
||
"/api/v1/instances",
|
||
json=MYSQL_INSTANCE_DATA,
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 201
|
||
return resp.json()["id"]
|
||
|
||
|
||
# ====================================================================
|
||
# 1. 数据库列表端点 GET /mongo/{id}/databases
|
||
# ====================================================================
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_list_databases_success(mock_cls, client, admin_token):
|
||
"""管理员列出 mongo 数据库 — 正常返回。"""
|
||
mock_cls.list_databases = AsyncMock(
|
||
return_value=["test_mongo", "myapp", "analytics"]
|
||
)
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
resp = await client.get(
|
||
f"/api/v1/mongo/{iid}/databases",
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert "databases" in data
|
||
assert data["databases"] == ["test_mongo", "myapp", "analytics"]
|
||
mock_cls.list_databases.assert_awaited_once()
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_list_databases_empty(mock_cls, client, admin_token):
|
||
"""数据库列表为空时返回空列表。"""
|
||
mock_cls.list_databases = AsyncMock(return_value=[])
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
resp = await client.get(
|
||
f"/api/v1/mongo/{iid}/databases",
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 200
|
||
assert resp.json()["databases"] == []
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_list_databases_connector_error(mock_cls, client, admin_token):
|
||
"""MongoConnector 抛出异常时返回 500。"""
|
||
mock_cls.list_databases = AsyncMock(side_effect=Exception("连接超时"))
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
resp = await client.get(
|
||
f"/api/v1/mongo/{iid}/databases",
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 500
|
||
assert "获取数据库列表失败" in resp.json()["detail"]
|
||
|
||
|
||
# ====================================================================
|
||
# 2. 集合列表端点 GET /mongo/{id}/databases/{db}/collections
|
||
# ====================================================================
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_list_collections_success(mock_cls, client, admin_token):
|
||
"""列出指定数据库下的集合 — 正常返回。"""
|
||
mock_cls.list_collections = AsyncMock(
|
||
return_value=["users", "orders", "products"]
|
||
)
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
resp = await client.get(
|
||
f"/api/v1/mongo/{iid}/databases/myapp/collections",
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["database"] == "myapp"
|
||
assert data["collections"] == ["users", "orders", "products"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_list_collections_error(mock_cls, client, admin_token):
|
||
"""获取集合列表失败时返回 500。"""
|
||
mock_cls.list_collections = AsyncMock(side_effect=Exception("权限不足"))
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
resp = await client.get(
|
||
f"/api/v1/mongo/{iid}/databases/myapp/collections",
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 500
|
||
assert "获取集合列表失败" in resp.json()["detail"]
|
||
|
||
|
||
# ====================================================================
|
||
# 3. 集合统计 GET /mongo/{id}/collections/{coll}/stats?database=xxx
|
||
# ====================================================================
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_collection_stats_success(mock_cls, client, admin_token):
|
||
"""获取集合统计信息 — 正常返回。"""
|
||
mock_stats = {
|
||
"namespace": "myapp.users",
|
||
"document_count": 5000,
|
||
"size_bytes": 1048576,
|
||
"avg_document_size_bytes": 209,
|
||
"storage_size_bytes": 524288,
|
||
"index_count": 3,
|
||
"index_size_bytes": 131072,
|
||
"capped": False,
|
||
}
|
||
mock_cls.get_collection_stats = AsyncMock(return_value=mock_stats)
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
resp = await client.get(
|
||
f"/api/v1/mongo/{iid}/collections/users/stats?database=myapp",
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["document_count"] == 5000
|
||
assert data["index_count"] == 3
|
||
mock_cls.get_collection_stats.assert_awaited_once()
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_collection_stats_uses_instance_default_db(mock_cls, client, admin_token):
|
||
"""未传 database 参数时使用实例默认数据库。"""
|
||
mock_stats = {"namespace": "test_mongo.users", "document_count": 100}
|
||
mock_cls.get_collection_stats = AsyncMock(return_value=mock_stats)
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
# 实例默认 database = "test_mongo",不传 database 参数
|
||
resp = await client.get(
|
||
f"/api/v1/mongo/{iid}/collections/users/stats",
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 200
|
||
assert resp.json()["document_count"] == 100
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_collection_stats_no_database(mock_cls, client, admin_token, db):
|
||
"""实例无默认数据库且未传 database 参数 — 返回 400。"""
|
||
mock_cls.get_collection_stats = AsyncMock(return_value={})
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
# 手动清空实例的 database 字段
|
||
from sqlalchemy import select
|
||
result = await db.execute(
|
||
select(Instance).where(Instance.id == iid)
|
||
)
|
||
inst = result.scalar_one()
|
||
inst.database = None
|
||
await db.commit()
|
||
|
||
resp = await client.get(
|
||
f"/api/v1/mongo/{iid}/collections/users/stats",
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 400
|
||
assert "未指定数据库名称" in resp.json()["detail"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_collection_stats_error(mock_cls, client, admin_token):
|
||
"""获取集合统计异常时返回 500。"""
|
||
mock_cls.get_collection_stats = AsyncMock(
|
||
side_effect=Exception("集合不存在")
|
||
)
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
resp = await client.get(
|
||
f"/api/v1/mongo/{iid}/collections/users/stats?database=myapp",
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 500
|
||
assert "获取集合统计失败" in resp.json()["detail"]
|
||
|
||
|
||
# ====================================================================
|
||
# 4. 只读查询 POST /mongo/{id}/query
|
||
# ====================================================================
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_query_find_success(mock_cls, client, admin_token):
|
||
"""执行 find 查询 — 正常返回。"""
|
||
mock_result = [{"_id": "1", "name": "Alice"}, {"_id": "2", "name": "Bob"}]
|
||
mock_cls.execute_query_json = AsyncMock(return_value=mock_result)
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
body = {
|
||
"database": "myapp",
|
||
"collection": "users",
|
||
"operation": "find",
|
||
"filter": {"age": {"$gt": 18}},
|
||
"limit": 100,
|
||
}
|
||
resp = await client.post(
|
||
f"/api/v1/mongo/{iid}/query",
|
||
json=body,
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["success"] is True
|
||
assert data["operation"] == "find"
|
||
assert len(data["result"]) == 2
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_query_aggregate_success(mock_cls, client, admin_token):
|
||
"""执行 aggregate 聚合查询 — 正常返回。"""
|
||
mock_result = [{"_id": "groupA", "total": 42}]
|
||
mock_cls.execute_query_json = AsyncMock(return_value=mock_result)
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
body = {
|
||
"database": "myapp",
|
||
"collection": "orders",
|
||
"operation": "aggregate",
|
||
"pipeline": [{"$group": {"_id": "$category", "total": {"$sum": 1}}}],
|
||
}
|
||
resp = await client.post(
|
||
f"/api/v1/mongo/{iid}/query",
|
||
json=body,
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 200
|
||
assert resp.json()["operation"] == "aggregate"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_query_write_operation_rejected(mock_cls, client, admin_token):
|
||
"""查询端点拒绝写操作(如 insertOne)— 返回 403。"""
|
||
mock_cls.execute_query_json = AsyncMock(return_value={})
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
body = {
|
||
"database": "myapp",
|
||
"collection": "users",
|
||
"operation": "insertOne",
|
||
"document": {"name": "hack"},
|
||
}
|
||
resp = await client.post(
|
||
f"/api/v1/mongo/{iid}/query",
|
||
json=body,
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 403
|
||
assert "写操作" in resp.json()["detail"] or "禁止" in resp.json()["detail"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_query_unknown_operation_rejected(mock_cls, client, admin_token):
|
||
"""查询端点拒绝未知操作 — 返回 403。"""
|
||
mock_cls.execute_query_json = AsyncMock(return_value={})
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
body = {
|
||
"database": "myapp",
|
||
"collection": "users",
|
||
"operation": "dropCollection",
|
||
}
|
||
resp = await client.post(
|
||
f"/api/v1/mongo/{iid}/query",
|
||
json=body,
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 403
|
||
assert "未知" in resp.json()["detail"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_query_no_database(mock_cls, client, admin_token, db):
|
||
"""查询时无数据库名称 — 返回 400。"""
|
||
mock_cls.execute_query_json = AsyncMock(return_value=[])
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
# 清空实例默认 database
|
||
from sqlalchemy import select
|
||
result = await db.execute(
|
||
select(Instance).where(Instance.id == iid)
|
||
)
|
||
inst = result.scalar_one()
|
||
inst.database = None
|
||
await db.commit()
|
||
|
||
body = {
|
||
"collection": "users",
|
||
"operation": "find",
|
||
}
|
||
resp = await client.post(
|
||
f"/api/v1/mongo/{iid}/query",
|
||
json=body,
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 400
|
||
assert "未指定数据库名称" in resp.json()["detail"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_query_connector_error(mock_cls, client, admin_token):
|
||
"""查询端点 connector 异常 — 返回 500。"""
|
||
mock_cls.execute_query_json = AsyncMock(
|
||
side_effect=Exception("cursor timeout")
|
||
)
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
body = {
|
||
"database": "myapp",
|
||
"collection": "users",
|
||
"operation": "find",
|
||
}
|
||
resp = await client.post(
|
||
f"/api/v1/mongo/{iid}/query",
|
||
json=body,
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 500
|
||
assert "查询执行失败" in resp.json()["detail"]
|
||
|
||
|
||
# ====================================================================
|
||
# 5. 变更操作 POST /mongo/{id}/execute (仅 admin/dba)
|
||
# ====================================================================
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_execute_insert_one_admin(mock_cls, client, admin_token):
|
||
"""admin 执行 insertOne 变更 — 正常返回。"""
|
||
mock_cls.execute_change_json = AsyncMock(
|
||
return_value="507f1f77bcf86cd799439011"
|
||
)
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
body = {
|
||
"database": "myapp",
|
||
"collection": "users",
|
||
"operation": "insertOne",
|
||
"document": {"name": "Alice", "age": 25},
|
||
}
|
||
resp = await client.post(
|
||
f"/api/v1/mongo/{iid}/execute",
|
||
json=body,
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["success"] is True
|
||
assert data["operation"] == "insertOne"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_execute_delete_many_dba(mock_cls, client, dba_token, admin_token):
|
||
"""dba 执行 deleteMany 变更 — 正常返回。"""
|
||
mock_cls.execute_change_json = AsyncMock(return_value="删除 3 条文档")
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
body = {
|
||
"database": "myapp",
|
||
"collection": "logs",
|
||
"operation": "deleteMany",
|
||
"filter": {"level": "debug"},
|
||
}
|
||
resp = await client.post(
|
||
f"/api/v1/mongo/{iid}/execute",
|
||
json=body,
|
||
headers=auth_header(dba_token),
|
||
)
|
||
assert resp.status_code == 200
|
||
assert resp.json()["success"] is True
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_execute_update_many_success(mock_cls, client, admin_token):
|
||
"""admin 执行 updateMany 变更 — 正常返回。"""
|
||
mock_cls.execute_change_json = AsyncMock(
|
||
return_value="匹配 5 条,修改 5 条"
|
||
)
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
body = {
|
||
"database": "myapp",
|
||
"collection": "users",
|
||
"operation": "updateMany",
|
||
"filter": {"status": "inactive"},
|
||
"update": {"$set": {"status": "archived"}},
|
||
}
|
||
resp = await client.post(
|
||
f"/api/v1/mongo/{iid}/execute",
|
||
json=body,
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 200
|
||
assert "匹配 5 条" in resp.json()["result"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_execute_no_database(mock_cls, client, admin_token, db):
|
||
"""变更操作无数据库名称 — 返回 400。"""
|
||
mock_cls.execute_change_json = AsyncMock(return_value="")
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
# 清空实例默认 database
|
||
from sqlalchemy import select
|
||
result = await db.execute(
|
||
select(Instance).where(Instance.id == iid)
|
||
)
|
||
inst = result.scalar_one()
|
||
inst.database = None
|
||
await db.commit()
|
||
|
||
body = {
|
||
"collection": "users",
|
||
"operation": "insertOne",
|
||
"document": {"name": "test"},
|
||
}
|
||
resp = await client.post(
|
||
f"/api/v1/mongo/{iid}/execute",
|
||
json=body,
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 400
|
||
assert "未指定数据库名称" in resp.json()["detail"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_execute_connector_error(mock_cls, client, admin_token):
|
||
"""变更操作 connector 异常 — 返回 500。"""
|
||
mock_cls.execute_change_json = AsyncMock(
|
||
side_effect=Exception("写入失败")
|
||
)
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
body = {
|
||
"database": "myapp",
|
||
"collection": "users",
|
||
"operation": "insertOne",
|
||
"document": {"name": "test"},
|
||
}
|
||
resp = await client.post(
|
||
f"/api/v1/mongo/{iid}/execute",
|
||
json=body,
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 500
|
||
assert "变更执行失败" in resp.json()["detail"]
|
||
|
||
|
||
# ====================================================================
|
||
# 6. 测试连接 POST /mongo/{id}/test-connection
|
||
# ====================================================================
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_test_connection_success(mock_cls, client, admin_token):
|
||
"""测试连接成功 — 返回带 version 的结果。"""
|
||
from app.services.mongo_connector import ConnectionResult
|
||
mock_result = ConnectionResult(
|
||
success=True, version="7.0.4", latency_ms=12, message="连接成功"
|
||
)
|
||
mock_cls.test_connection = AsyncMock(return_value=mock_result)
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
resp = await client.post(
|
||
f"/api/v1/mongo/{iid}/test-connection",
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["success"] is True
|
||
assert data["version"] == "7.0.4"
|
||
assert data["latency_ms"] == 12
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_test_connection_failure(mock_cls, client, admin_token):
|
||
"""测试连接失败 — 返回 500。"""
|
||
mock_cls.test_connection = AsyncMock(
|
||
side_effect=Exception("network unreachable")
|
||
)
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
resp = await client.post(
|
||
f"/api/v1/mongo/{iid}/test-connection",
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 500
|
||
assert "连接测试失败" in resp.json()["detail"]
|
||
|
||
|
||
# ====================================================================
|
||
# 7. 实例验证:非 mongo 类型 / 实例不存在
|
||
# ====================================================================
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_non_mongo_instance_rejected(client, admin_token):
|
||
"""MySQL 实例访问 mongo 端点 — 返回 400。"""
|
||
iid = await _create_mysql_instance(client, admin_token)
|
||
|
||
resp = await client.get(
|
||
f"/api/v1/mongo/{iid}/databases",
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 400
|
||
assert "MongoDB" in resp.json()["detail"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_non_mongo_instance_rejected_query(client, admin_token):
|
||
"""MySQL 实例访问 mongo 查询端点 — 返回 400。"""
|
||
iid = await _create_mysql_instance(client, admin_token)
|
||
|
||
body = {
|
||
"database": "myapp",
|
||
"collection": "users",
|
||
"operation": "find",
|
||
}
|
||
resp = await client.post(
|
||
f"/api/v1/mongo/{iid}/query",
|
||
json=body,
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 400
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_non_mongo_instance_rejected_execute(client, admin_token):
|
||
"""MySQL 实例访问 mongo 变更端点 — 返回 400。"""
|
||
iid = await _create_mysql_instance(client, admin_token)
|
||
|
||
body = {
|
||
"database": "myapp",
|
||
"collection": "users",
|
||
"operation": "insertOne",
|
||
"document": {"name": "test"},
|
||
}
|
||
resp = await client.post(
|
||
f"/api/v1/mongo/{iid}/execute",
|
||
json=body,
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 400
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_instance_not_found(client, admin_token):
|
||
"""不存在的实例 ID — 返回 404。"""
|
||
resp = await client.get(
|
||
"/api/v1/mongo/99999/databases",
|
||
headers=auth_header(admin_token),
|
||
)
|
||
assert resp.status_code == 404
|
||
assert "不存在" in resp.json()["detail"]
|
||
|
||
|
||
# ====================================================================
|
||
# 8. 权限检查:developer 不能执行变更操作
|
||
# ====================================================================
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_execute_requires_admin_or_dba(client, developer_token, admin_token):
|
||
"""developer 角色无法执行变更操作 — 返回 403。"""
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
body = {
|
||
"database": "myapp",
|
||
"collection": "users",
|
||
"operation": "insertOne",
|
||
"document": {"name": "unauthorized"},
|
||
}
|
||
resp = await client.post(
|
||
f"/api/v1/mongo/{iid}/execute",
|
||
json=body,
|
||
headers=auth_header(developer_token),
|
||
)
|
||
assert resp.status_code == 403
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_query_allowed_for_developer(mock_cls, client, developer_token, admin_token):
|
||
"""developer 可以执行只读查询(get_current_user 即可)。"""
|
||
mock_cls.execute_query_json = AsyncMock(return_value=[{"name": "Alice"}])
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
body = {
|
||
"database": "myapp",
|
||
"collection": "users",
|
||
"operation": "find",
|
||
}
|
||
resp = await client.post(
|
||
f"/api/v1/mongo/{iid}/query",
|
||
json=body,
|
||
headers=auth_header(developer_token),
|
||
)
|
||
assert resp.status_code == 200
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_test_connection_allowed_for_developer(mock_cls, client, developer_token, admin_token):
|
||
"""developer 可以测试连接(get_current_user 即可)。"""
|
||
from app.services.mongo_connector import ConnectionResult
|
||
mock_result = ConnectionResult(
|
||
success=True, version="7.0.0", latency_ms=5, message="连接成功"
|
||
)
|
||
mock_cls.test_connection = AsyncMock(return_value=mock_result)
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
resp = await client.post(
|
||
f"/api/v1/mongo/{iid}/test-connection",
|
||
headers=auth_header(developer_token),
|
||
)
|
||
assert resp.status_code == 200
|
||
|
||
|
||
# ====================================================================
|
||
# 9. dba 角色可以执行变更操作
|
||
# ====================================================================
|
||
|
||
@pytest.mark.asyncio
|
||
@patch("app.api.v1.endpoints.mongo.MongoConnector")
|
||
async def test_execute_dba_allowed(mock_cls, client, dba_token, admin_token):
|
||
"""dba 角色可以执行变更操作 — 正常返回。"""
|
||
mock_cls.execute_change_json = AsyncMock(return_value="507f1f77bcf86cd799439011")
|
||
iid = await _create_mongo_instance(client, admin_token)
|
||
|
||
body = {
|
||
"database": "myapp",
|
||
"collection": "users",
|
||
"operation": "insertOne",
|
||
"document": {"name": "dba_user"},
|
||
}
|
||
resp = await client.post(
|
||
f"/api/v1/mongo/{iid}/execute",
|
||
json=body,
|
||
headers=auth_header(dba_token),
|
||
)
|
||
assert resp.status_code == 200
|
||
assert resp.json()["success"] is True
|