Files
omnigent-ai--omnigent/tests/server/routes/test_sessions_shared_projects.py
2026-07-13 13:12:00 +08:00

132 lines
5.3 KiB
Python

"""Regression tests: sessions shared into a project stay on "Shared with me".
Projects are a "My sessions"-only sidebar surface — filing a session into a
project is owner-only. When someone shares a session that happens to carry a
project label, the recipient should see it under "Shared with me", NOT as one
of their own project folders under "My sessions".
The sidebar builds project folders from two owner-scoped server surfaces:
- ``GET /v1/sessions/projects`` — the folder *names*, and
- ``GET /v1/sessions?project=<name>`` — the sessions *inside* a folder.
Both must filter by ownership (an ``owner``-level grant), not mere access, or
a shared session leaks into the recipient's "My sessions" project view. These
tests drive the real routes against file-backed SQLite stores with header auth.
"""
from __future__ import annotations
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
from omnigent.errors import OmnigentError
from omnigent.server.auth import LEVEL_OWNER, LEVEL_READ, UnifiedAuthProvider
from omnigent.server.routes.sessions import create_sessions_router
from omnigent.stores.agent_store.sqlalchemy_store import SqlAlchemyAgentStore
from omnigent.stores.conversation_store.sqlalchemy_store import (
SqlAlchemyConversationStore,
)
from omnigent.stores.permission_store.sqlalchemy_store import (
SqlAlchemyPermissionStore,
)
ALICE = "alice@example.com"
BOB = "bob@example.com"
PROJECT_LABEL_KEY = "omni_project"
def _multi_user_app(db_uri: str) -> FastAPI:
"""Build a header-auth app mounting the sessions router at ``/v1``."""
app = FastAPI()
@app.exception_handler(OmnigentError)
async def _handle(request: Request, exc: OmnigentError) -> JSONResponse:
del request
return JSONResponse(
status_code=exc.http_status,
content={"error": {"code": exc.code, "message": exc.message}},
)
app.include_router(
create_sessions_router(
conversation_store=SqlAlchemyConversationStore(db_uri),
agent_store=SqlAlchemyAgentStore(db_uri),
auth_provider=UnifiedAuthProvider(source="header"),
permission_store=SqlAlchemyPermissionStore(db_uri),
),
prefix="/v1",
)
return app
def _seed_shared_project_session(db_uri: str) -> str:
"""Seed a session owned by Bob, filed under a project, and read-shared to
Alice. Returns the session id."""
agent_store = SqlAlchemyAgentStore(db_uri)
conv_store = SqlAlchemyConversationStore(db_uri)
perms = SqlAlchemyPermissionStore(db_uri)
if agent_store.get("ag_test") is None:
agent_store.create(agent_id="ag_test", name="test-agent", bundle_location="ag_test/bundle")
conv = conv_store.create_conversation(title="Bob's session", agent_id="ag_test")
conv_store.set_labels(conv.id, {PROJECT_LABEL_KEY: "Bob Project"})
for user in (ALICE, BOB):
perms.ensure_user(user)
perms.grant(BOB, conv.id, LEVEL_OWNER)
perms.grant(ALICE, conv.id, LEVEL_READ)
return conv.id
def test_shared_project_not_listed_as_recipients_own_project(db_uri: str) -> None:
"""A project whose only member is a session shared TO Alice (owned by Bob)
must not appear in Alice's project list — folders are her own sessions."""
_seed_shared_project_session(db_uri)
app = _multi_user_app(db_uri)
# Bob owns it, so it's his project.
bob = TestClient(app).get("/v1/sessions/projects", headers={"X-Forwarded-Email": BOB})
assert bob.status_code == 200
assert bob.json() == ["Bob Project"]
# Alice can access the session, but doesn't own it — no folder for her.
alice = TestClient(app).get("/v1/sessions/projects", headers={"X-Forwarded-Email": ALICE})
assert alice.status_code == 200
assert alice.json() == []
def test_shared_session_excluded_from_recipients_project_folder(db_uri: str) -> None:
"""Fetching a project folder's sessions (``?project=``) as Alice excludes a
session merely shared with her — it belongs on "Shared with me"."""
conv_id = _seed_shared_project_session(db_uri)
app = _multi_user_app(db_uri)
# Bob's folder holds the session.
bob = TestClient(app).get(
"/v1/sessions?project=Bob%20Project", headers={"X-Forwarded-Email": BOB}
)
assert bob.status_code == 200
assert [s["id"] for s in bob.json()["data"]] == [conv_id]
# Alice's same-named folder is empty — the shared session isn't hers to file.
alice = TestClient(app).get(
"/v1/sessions?project=Bob%20Project", headers={"X-Forwarded-Email": ALICE}
)
assert alice.status_code == 200
assert alice.json()["data"] == []
def test_shared_session_still_visible_in_flat_list(db_uri: str) -> None:
"""The fix is scoped to project surfaces: the shared session still shows up
in Alice's unfiltered session list, where the "Shared with me" tab reads it
(the frontend splits owned vs. shared by permission_level there)."""
conv_id = _seed_shared_project_session(db_uri)
app = _multi_user_app(db_uri)
resp = TestClient(app).get("/v1/sessions", headers={"X-Forwarded-Email": ALICE})
assert resp.status_code == 200
items = {s["id"]: s for s in resp.json()["data"]}
assert conv_id in items
# Below LEVEL_OWNER, so the frontend files it under "Shared with me".
assert items[conv_id]["permission_level"] < LEVEL_OWNER