mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 15:51:59 +00:00
158 lines
8.0 KiB
Python
158 lines
8.0 KiB
Python
"""对话按人隔离(D-d)与应用编辑器的助手对话(D-e 的归属核对)。
|
|
|
|
- 新建对话记下建立人(actor_of:关登录时是自报的署名,开登录时是认证过的用户名);
|
|
- 开登录后使用者只列得出、读得到、改得动、删得掉自己建的对话;建立人为空的老对话只有搭建者以上看得到;
|
|
- 应用编辑器的助手对话(kind=app)只对能管理这个应用的人开放;
|
|
- ensure_conversation_access:接受 conversation_id 的接口(助手的生成流等)按同一套规则核对,不存在的当没有历史;
|
|
- 关登录时一切照旧。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
from sqlalchemy import delete, select
|
|
|
|
from app.api.conversations import ensure_conversation_access
|
|
from app.api.deps import Principal, reset_principal, set_principal
|
|
from app.db.base import SessionLocal
|
|
from app.db.models import App, AppGrant, Conversation, ConversationTurn
|
|
from tests.fakes import auth_env
|
|
|
|
|
|
#: 这些测试里出现的人:收尾时只删他们建的对话、本文件建的应用,不碰同一进程里别的测试的数据
|
|
PEOPLE = ("uma", "ula", "bob", "bea", "张三")
|
|
|
|
|
|
async def _clear(*app_ids: str, conversation_ids: tuple[str, ...] = ()) -> None:
|
|
async with SessionLocal() as s:
|
|
mine = Conversation.created_by.in_(PEOPLE) | Conversation.id.in_(list(conversation_ids))
|
|
ids = list((await s.execute(select(Conversation.id).where(mine))).scalars())
|
|
await s.execute(delete(ConversationTurn).where(ConversationTurn.conversation_id.in_(ids)))
|
|
await s.execute(delete(Conversation).where(Conversation.id.in_(ids)))
|
|
await s.execute(delete(AppGrant).where(AppGrant.app_id.in_(list(app_ids))))
|
|
await s.execute(delete(App).where(App.id.in_(list(app_ids))))
|
|
await s.commit()
|
|
|
|
|
|
@pytest.fixture
|
|
async def world():
|
|
"""开账号密码登录、开问数据;使用者 uma、ula,搭建者 bob、bea;一段关登录时留下的老对话,一个 bob 建的应用。"""
|
|
await auth_env.reset()
|
|
await _clear()
|
|
async with SessionLocal() as s:
|
|
legacy = Conversation(kind="chat", title="老对话")
|
|
app_row = App(name="报销", created_by="bob")
|
|
s.add_all([legacy, app_row])
|
|
await s.flush()
|
|
app_conv = Conversation(kind="app", app_id=app_row.id, title="应用润色", created_by="bob")
|
|
s.add(app_conv)
|
|
await s.commit()
|
|
ids = {"legacy": legacy.id, "app": app_row.id, "app_conv": app_conv.id}
|
|
admin = await auth_env.turn_on_local()
|
|
assert (await admin.put("/api/auth/settings", json={"user_can_chat": True})).status_code == 200
|
|
for name, role in (("uma", "user"), ("ula", "user"), ("bob", "builder"), ("bea", "builder")):
|
|
await auth_env.make_user(name, role)
|
|
clients = {name: await auth_env.login(name) for name in ("uma", "ula", "bob", "bea")}
|
|
try:
|
|
yield {"admin": admin, **clients, "ids": ids}
|
|
finally:
|
|
await _clear(ids["app"], conversation_ids=(ids["legacy"],))
|
|
await auth_env.reset()
|
|
|
|
|
|
async def _create(client, title: str) -> str:
|
|
r = await client.post("/api/conversations", json={"kind": "chat", "title": title})
|
|
assert r.status_code == 201, r.text
|
|
return r.json()["id"]
|
|
|
|
|
|
async def test_a_user_only_sees_their_own_conversations(world):
|
|
uma, ula, bob = world["uma"], world["ula"], world["bob"]
|
|
mine = await _create(uma, "我的")
|
|
theirs = await _create(ula, "别人的")
|
|
legacy = world["ids"]["legacy"]
|
|
|
|
got = (await uma.get("/api/conversations")).json()
|
|
assert [c["id"] for c in got] == [mine] and got[0]["created_by"] == "uma", "只列自己建的,老对话不列"
|
|
assert (await uma.get(f"/api/conversations/{mine}")).status_code == 200
|
|
for other in (theirs, legacy):
|
|
assert (await uma.get(f"/api/conversations/{other}")).status_code == 403
|
|
assert (await uma.patch(f"/api/conversations/{other}", json={"title": "改"})).status_code == 403
|
|
assert (await uma.post(f"/api/conversations/{other}/turns", json={"question": "接着问"})).status_code == 403
|
|
assert (await uma.delete(f"/api/conversations/{other}")).status_code == 403
|
|
turn = (await ula.post(f"/api/conversations/{theirs}/turns", json={"question": "我的问题"})).json()
|
|
assert (await uma.patch(f"/api/conversations/{theirs}/turns/{turn['id']}", json={"answer": "x"})).status_code == 403
|
|
|
|
assert (await uma.patch(f"/api/conversations/{mine}", json={"title": "改名"})).status_code == 200
|
|
assert (await uma.post(f"/api/conversations/{mine}/turns", json={"question": "问"})).status_code == 201
|
|
assert (await uma.delete(f"/api/conversations/{mine}")).status_code == 204
|
|
|
|
# 搭建者以上照旧:看得到使用者的和老对话
|
|
seen = {c["id"] for c in (await bob.get("/api/conversations")).json()}
|
|
assert {theirs, legacy} <= seen
|
|
assert (await bob.get(f"/api/conversations/{legacy}")).status_code == 200
|
|
|
|
|
|
async def test_app_assistant_conversations_need_manage(world):
|
|
ids = world["ids"]
|
|
path = f"/api/conversations/{ids['app_conv']}"
|
|
assert (await world["bob"].get(path)).status_code == 200, "应用的建立人"
|
|
assert (await world["admin"].get(path)).status_code == 200
|
|
assert (await world["bea"].get(path)).status_code == 403, "没有 manage 的搭建者"
|
|
assert ids["app_conv"] not in {c["id"] for c in (await world["bea"].get(
|
|
"/api/conversations", params={"kind": "app"})).json()}
|
|
assert [c["id"] for c in (await world["bob"].get(
|
|
"/api/conversations", params={"kind": "app", "app_id": ids["app"]})).json()] == [ids["app_conv"]]
|
|
async with SessionLocal() as s:
|
|
s.add(AppGrant(app_id=ids["app"], principal_kind="user", principal="bea", role="manage"))
|
|
await s.commit()
|
|
assert (await world["bea"].get(path)).status_code == 200, "有 manage 授权的搭建者"
|
|
assert (await world["uma"].get(path)).status_code == 403
|
|
|
|
|
|
async def test_ensure_conversation_access_for_other_endpoints(world):
|
|
ids = world["ids"]
|
|
mine = await _create(world["uma"], "我的")
|
|
uma = Principal(username="uma", role="user", verified=True, mode="local")
|
|
token = set_principal(uma)
|
|
try:
|
|
async with SessionLocal() as s:
|
|
assert await ensure_conversation_access(s, None) is None
|
|
assert await ensure_conversation_access(s, "nope") is None, "不存在的当没有历史,不报错"
|
|
assert (await ensure_conversation_access(s, mine)).id == mine
|
|
for other in (ids["legacy"], ids["app_conv"]):
|
|
with pytest.raises(HTTPException) as e:
|
|
await ensure_conversation_access(s, other)
|
|
assert e.value.status_code == 403
|
|
finally:
|
|
reset_principal(token)
|
|
# api/deps.py 里那一份是同一个函数(给 api/copilot.py 这类接口调)
|
|
from app.api import deps
|
|
|
|
token = set_principal(Principal(username="bob", role="builder", verified=True, mode="local"))
|
|
try:
|
|
async with SessionLocal() as s:
|
|
assert (await deps.ensure_conversation_access(s, ids["legacy"])).id == ids["legacy"]
|
|
finally:
|
|
reset_principal(token)
|
|
|
|
|
|
async def test_login_off_records_the_self_reported_name_and_changes_nothing_else():
|
|
await auth_env.reset()
|
|
async with SessionLocal() as s:
|
|
legacy = Conversation(kind="chat", title="老对话")
|
|
s.add(legacy)
|
|
await s.commit()
|
|
legacy_id = legacy.id
|
|
try:
|
|
async with auth_env.client(**{"X-Actor": "%E5%BC%A0%E4%B8%89"}) as c:
|
|
created = (await c.post("/api/conversations", json={"kind": "chat"})).json()
|
|
assert created["created_by"] == "张三"
|
|
seen = {x["id"] for x in (await c.get("/api/conversations")).json()}
|
|
assert {legacy_id, created["id"]} <= seen, "关登录时都看得到"
|
|
assert (await c.get(f"/api/conversations/{legacy_id}")).status_code == 200
|
|
async with SessionLocal() as s:
|
|
assert await ensure_conversation_access(s, created["id"]) is not None
|
|
finally:
|
|
await _clear(conversation_ids=(legacy_id,))
|