209 lines
5.5 KiB
Python
209 lines
5.5 KiB
Python
import uuid
|
|
from types import SimpleNamespace
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from app.api import skills as skills_api
|
|
from app.core.security import get_current_user
|
|
from app.main import app
|
|
|
|
|
|
class FakeScalarResult:
|
|
def __init__(self, value):
|
|
self._value = value
|
|
|
|
def scalar_one_or_none(self):
|
|
return self._value
|
|
|
|
|
|
class TrapList(list):
|
|
def __iter__(self):
|
|
raise AssertionError("newly created skills should not iterate over lazy files")
|
|
|
|
|
|
class FakeSession:
|
|
def __init__(self, *, skill=None):
|
|
self.skill = skill
|
|
self.added = []
|
|
self.deleted = []
|
|
self.committed = False
|
|
|
|
async def execute(self, _query):
|
|
return FakeScalarResult(self.skill)
|
|
|
|
def add(self, value):
|
|
self.added.append(value)
|
|
|
|
async def flush(self):
|
|
return None
|
|
|
|
async def delete(self, value):
|
|
self.deleted.append(value)
|
|
|
|
async def commit(self):
|
|
self.committed = True
|
|
|
|
|
|
class FakeAsyncSessionFactory:
|
|
def __init__(self, session):
|
|
self.session = session
|
|
|
|
def __call__(self):
|
|
return self
|
|
|
|
async def __aenter__(self):
|
|
return self.session
|
|
|
|
async def __aexit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
|
|
class FakeQuery:
|
|
def where(self, *_args, **_kwargs):
|
|
return self
|
|
|
|
def options(self, *_args, **_kwargs):
|
|
return self
|
|
|
|
def order_by(self, *_args, **_kwargs):
|
|
return self
|
|
|
|
|
|
class RaiseOnInstanceAccess:
|
|
def __get__(self, instance, owner):
|
|
if instance is None:
|
|
return self
|
|
raise AssertionError("newly created skills should not iterate over lazy files")
|
|
|
|
|
|
class QueryField:
|
|
def is_(self, _value):
|
|
return self
|
|
|
|
def __eq__(self, _other):
|
|
return self
|
|
|
|
|
|
class FakeSkill:
|
|
folder_name = QueryField()
|
|
tenant_id = QueryField()
|
|
files = RaiseOnInstanceAccess()
|
|
|
|
def __init__(self, **kwargs):
|
|
self.id = uuid.uuid4()
|
|
for key, value in kwargs.items():
|
|
setattr(self, key, value)
|
|
|
|
|
|
@pytest.fixture
|
|
def org_admin_user():
|
|
return SimpleNamespace(
|
|
id=uuid.uuid4(),
|
|
role="org_admin",
|
|
tenant_id=uuid.uuid4(),
|
|
is_active=True,
|
|
department_id=None,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def platform_admin_user():
|
|
return SimpleNamespace(
|
|
id=uuid.uuid4(),
|
|
role="platform_admin",
|
|
tenant_id=uuid.uuid4(),
|
|
is_active=True,
|
|
department_id=None,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
transport = httpx.ASGITransport(app=app)
|
|
|
|
async def _build():
|
|
return httpx.AsyncClient(transport=transport, base_url="http://test")
|
|
|
|
return _build
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_org_admin_can_delete_custom_skill_via_browse(monkeypatch, client, org_admin_user):
|
|
skill = SimpleNamespace(
|
|
id=uuid.uuid4(),
|
|
folder_name="tenant-skill",
|
|
tenant_id=org_admin_user.tenant_id,
|
|
is_builtin=False,
|
|
files=[],
|
|
)
|
|
session = FakeSession(skill=skill)
|
|
|
|
monkeypatch.setattr(skills_api, "async_session", FakeAsyncSessionFactory(session))
|
|
app.dependency_overrides[get_current_user] = lambda: org_admin_user
|
|
|
|
async with await client() as ac:
|
|
response = await ac.delete("/api/skills/browse/delete", params={"path": "tenant-skill"})
|
|
|
|
app.dependency_overrides.clear()
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {"ok": True}
|
|
assert session.deleted == [skill]
|
|
assert session.committed is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_org_admin_can_delete_custom_skill_directly(monkeypatch, client, org_admin_user):
|
|
skill = SimpleNamespace(
|
|
id=uuid.uuid4(),
|
|
folder_name="tenant-skill",
|
|
tenant_id=org_admin_user.tenant_id,
|
|
is_builtin=False,
|
|
)
|
|
session = FakeSession(skill=skill)
|
|
|
|
monkeypatch.setattr(skills_api, "async_session", FakeAsyncSessionFactory(session))
|
|
app.dependency_overrides[get_current_user] = lambda: org_admin_user
|
|
|
|
async with await client() as ac:
|
|
response = await ac.delete(f"/api/skills/{skill.id}")
|
|
|
|
app.dependency_overrides.clear()
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {"ok": True}
|
|
assert session.deleted == [skill]
|
|
assert session.committed is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_browse_write_creates_tenant_skill_without_iterating_lazy_files(
|
|
monkeypatch, client, platform_admin_user
|
|
):
|
|
session = FakeSession(skill=None)
|
|
|
|
monkeypatch.setattr(skills_api, "async_session", FakeAsyncSessionFactory(session))
|
|
monkeypatch.setattr(skills_api, "select", lambda *_args, **_kwargs: FakeQuery())
|
|
monkeypatch.setattr(skills_api, "selectinload", lambda *_args, **_kwargs: None)
|
|
monkeypatch.setattr(skills_api, "Skill", FakeSkill)
|
|
app.dependency_overrides[get_current_user] = lambda: platform_admin_user
|
|
|
|
async with await client() as ac:
|
|
response = await ac.put(
|
|
"/api/skills/browse/write",
|
|
json={"path": "tenant-skill/SKILL.md", "content": "# test"},
|
|
)
|
|
|
|
app.dependency_overrides.clear()
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {"ok": True}
|
|
created_skill = next(value for value in session.added if isinstance(value, FakeSkill))
|
|
created_file = next(value for value in session.added if isinstance(value, skills_api.SkillFile))
|
|
assert created_skill.folder_name == "tenant-skill"
|
|
assert created_skill.tenant_id == platform_admin_user.tenant_id
|
|
assert created_file.path == "SKILL.md"
|
|
assert created_file.content == "# test"
|
|
assert session.committed is True
|