Files
KiloStar/tests/unit/test_api_custom_toolset_auth.py
T
zhaoxi 6d658b4f4d feat: 工具系统迁移 + 重型插件骨架 + 前端交互增强
- 工具系统从 kilostar/plugin/tool_plugin/ 迁移到 data/toolset/(manifest.json 声明式)
- 新增 plugin_runtime 模块:BaseOrganization / GlobalPluginManager / loader / tool_bridge
- 新增 org_task + org_task_event 表及 DAO(alembic 0009)
- 新增 /api/v1/plugin 路由(submit/status/stream/install/reload)
- 新增 data/plugin/example_dept 示例重型插件
- regulatory_node 支持聊天历史上下文注入
- send_file 改为 artifact 存盘 + SSE 推送下载链接
- 前端 WorkflowFileCard 组件 + ToolSettings README 渲染
- utils 整理:合并 access/role_check、standalone_proxy→ray_compat、删除废弃模块
- 项目结构文档移至 docs/STRUCTURE.md 并详细展开

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 05:20:00 +00:00

141 lines
4.5 KiB
Python

"""``api/resource.py`` Custom toolset 路由:归属鉴权。"""
from __future__ import annotations
import types
from unittest.mock import AsyncMock
import pytest
from fastapi import FastAPI
from httpx import AsyncClient, ASGITransport
from kilostar.api.resource import resource_router
from kilostar.core.postgres_database.model import UserAuthority
from kilostar.utils.access import Accessor, TokenData
def _fake_user(user_id: str = "alice"):
return TokenData(user_id=user_id, username=user_id)
@pytest.fixture
def app_with_user(monkeypatch):
"""挂上 resource_router;用 dependency_overrides 跳过 JWT,并把 get_authority 默认放成 USER。"""
import kilostar.utils.access as access_mod
app = FastAPI()
app.include_router(resource_router)
app.dependency_overrides[Accessor.get_current_user] = lambda: _fake_user("alice")
async def _default_authority(uid):
return UserAuthority.USER
monkeypatch.setattr(access_mod, "get_authority", _default_authority)
return app
@pytest.mark.asyncio
async def test_get_custom_toolset_forbidden_for_non_owner(
app_with_user, fake_actors
):
gsm = types.SimpleNamespace()
gsm.get_custom_toolset = types.SimpleNamespace(
remote=AsyncMock(
return_value={"toolset_id": "t1", "owner_id": "bob", "tools": []}
)
)
fake_actors.register("global_state_machine", gsm)
transport = ASGITransport(app=app_with_user)
async with AsyncClient(transport=transport, base_url="http://test") as client:
resp = await client.get("/api/v1/resource/custom-toolset/t1")
assert resp.status_code == 403
@pytest.mark.asyncio
async def test_get_custom_toolset_allowed_for_owner(app_with_user, fake_actors):
gsm = types.SimpleNamespace()
gsm.get_custom_toolset = types.SimpleNamespace(
remote=AsyncMock(
return_value={"toolset_id": "t1", "owner_id": "alice", "tools": []}
)
)
fake_actors.register("global_state_machine", gsm)
transport = ASGITransport(app=app_with_user)
async with AsyncClient(transport=transport, base_url="http://test") as client:
resp = await client.get("/api/v1/resource/custom-toolset/t1")
assert resp.status_code == 200
assert resp.json()["owner_id"] == "alice"
@pytest.mark.asyncio
async def test_get_custom_toolset_allowed_for_admin(
app_with_user, fake_actors, monkeypatch
):
gsm = types.SimpleNamespace()
gsm.get_custom_toolset = types.SimpleNamespace(
remote=AsyncMock(
return_value={"toolset_id": "t1", "owner_id": "bob", "tools": []}
)
)
fake_actors.register("global_state_machine", gsm)
async def _admin(uid):
return UserAuthority.SUPER_ADMINISTRATOR
import kilostar.utils.access as access_mod
monkeypatch.setattr(access_mod, "get_authority", _admin)
transport = ASGITransport(app=app_with_user)
async with AsyncClient(transport=transport, base_url="http://test") as client:
resp = await client.get("/api/v1/resource/custom-toolset/t1")
assert resp.status_code == 200
@pytest.mark.asyncio
async def test_list_custom_toolsets_filters_by_owner(app_with_user, fake_actors):
all_sets = [
{"toolset_id": "t1", "owner_id": "alice"},
{"toolset_id": "t2", "owner_id": "bob"},
]
gsm = types.SimpleNamespace()
gsm.list_custom_toolsets = types.SimpleNamespace(
remote=AsyncMock(return_value=all_sets)
)
fake_actors.register("global_state_machine", gsm)
transport = ASGITransport(app=app_with_user)
async with AsyncClient(transport=transport, base_url="http://test") as client:
resp = await client.get("/api/v1/resource/custom-toolset")
assert resp.status_code == 200
body = resp.json()
assert len(body["toolsets"]) == 1
assert body["toolsets"][0]["toolset_id"] == "t1"
@pytest.mark.asyncio
async def test_delete_custom_toolset_forbidden_for_non_owner(
app_with_user, fake_actors
):
gsm = types.SimpleNamespace()
gsm.get_custom_toolset = types.SimpleNamespace(
remote=AsyncMock(
return_value={"toolset_id": "t1", "owner_id": "bob"}
)
)
delete_mock = AsyncMock(return_value=True)
gsm.delete_custom_toolset = types.SimpleNamespace(remote=delete_mock)
fake_actors.register("global_state_machine", gsm)
transport = ASGITransport(app=app_with_user)
async with AsyncClient(transport=transport, base_url="http://test") as client:
resp = await client.delete("/api/v1/resource/custom-toolset/t1")
assert resp.status_code == 403
delete_mock.assert_not_called()