mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-22 07:56:48 +00:00
refactor(tests): reorganize tests into unittest/ and e2e/ directories
- Move all unit tests from tests/ to tests/unittest/ - Add tests/e2e/ directory for end-to-end tests - Update conftest.py for new test structure - Add new tests for auth dependencies, policies, route injection - Add new tests for run callbacks, create store, execution artifacts - Remove obsolete tests for deleted persistence layer Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
"""Cross-user isolation tests for current app-owned storage adapters.
|
||||
|
||||
These tests exercise isolation by binding different ``ActorContext``
|
||||
values around the app-layer storage adapters. The safety property is:
|
||||
|
||||
data written under user A is not visible to user B through the same
|
||||
adapter surface unless a call explicitly opts out with ``user_id=None``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
|
||||
from app.infra.storage import AppRunEventStore, FeedbackStoreAdapter, RunStoreAdapter, ThreadMetaStorage, ThreadMetaStoreAdapter
|
||||
from deerflow.runtime.actor_context import AUTO, ActorContext, bind_actor_context, reset_actor_context
|
||||
from store.persistence import MappedBase
|
||||
|
||||
|
||||
USER_A = "user-a"
|
||||
USER_B = "user-b"
|
||||
|
||||
|
||||
async def _make_components(tmp_path):
|
||||
engine = create_async_engine(f"sqlite+aiosqlite:///{tmp_path / 'isolation.db'}", future=True)
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(MappedBase.metadata.create_all)
|
||||
|
||||
session_factory = async_sessionmaker(
|
||||
bind=engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
autoflush=False,
|
||||
)
|
||||
|
||||
thread_store = ThreadMetaStorage(ThreadMetaStoreAdapter(session_factory))
|
||||
return (
|
||||
engine,
|
||||
thread_store,
|
||||
RunStoreAdapter(session_factory),
|
||||
FeedbackStoreAdapter(session_factory),
|
||||
AppRunEventStore(session_factory),
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _as_user(user_id: str):
|
||||
token = bind_actor_context(ActorContext(user_id=user_id))
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
reset_actor_context(token)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.no_auto_user
|
||||
async def test_thread_meta_cross_user_isolation(tmp_path):
|
||||
engine, thread_store, _, _, _ = await _make_components(tmp_path)
|
||||
try:
|
||||
with _as_user(USER_A):
|
||||
await thread_store.ensure_thread(thread_id="t-alpha")
|
||||
with _as_user(USER_B):
|
||||
await thread_store.ensure_thread(thread_id="t-beta")
|
||||
|
||||
with _as_user(USER_A):
|
||||
assert (await thread_store.get_thread("t-alpha")) is not None
|
||||
assert await thread_store.get_thread("t-beta") is None
|
||||
rows = await thread_store.search_threads()
|
||||
assert [row.thread_id for row in rows] == ["t-alpha"]
|
||||
|
||||
with _as_user(USER_B):
|
||||
assert (await thread_store.get_thread("t-beta")) is not None
|
||||
assert await thread_store.get_thread("t-alpha") is None
|
||||
rows = await thread_store.search_threads()
|
||||
assert [row.thread_id for row in rows] == ["t-beta"]
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.no_auto_user
|
||||
async def test_runs_cross_user_isolation(tmp_path):
|
||||
engine, thread_store, run_store, _, _ = await _make_components(tmp_path)
|
||||
try:
|
||||
with _as_user(USER_A):
|
||||
await thread_store.ensure_thread(thread_id="t-alpha")
|
||||
await run_store.create("run-a1", "t-alpha")
|
||||
await run_store.create("run-a2", "t-alpha")
|
||||
|
||||
with _as_user(USER_B):
|
||||
await thread_store.ensure_thread(thread_id="t-beta")
|
||||
await run_store.create("run-b1", "t-beta")
|
||||
|
||||
with _as_user(USER_A):
|
||||
assert (await run_store.get("run-a1")) is not None
|
||||
assert await run_store.get("run-b1") is None
|
||||
rows = await run_store.list_by_thread("t-alpha")
|
||||
assert {row["run_id"] for row in rows} == {"run-a1", "run-a2"}
|
||||
assert await run_store.list_by_thread("t-beta") == []
|
||||
|
||||
with _as_user(USER_B):
|
||||
assert await run_store.get("run-a1") is None
|
||||
rows = await run_store.list_by_thread("t-beta")
|
||||
assert [row["run_id"] for row in rows] == ["run-b1"]
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.no_auto_user
|
||||
async def test_run_events_cross_user_isolation(tmp_path):
|
||||
engine, thread_store, _, _, event_store = await _make_components(tmp_path)
|
||||
try:
|
||||
with _as_user(USER_A):
|
||||
await thread_store.ensure_thread(thread_id="t-alpha")
|
||||
await event_store.put_batch(
|
||||
[
|
||||
{
|
||||
"thread_id": "t-alpha",
|
||||
"run_id": "run-a1",
|
||||
"event_type": "human_message",
|
||||
"category": "message",
|
||||
"content": "User A private question",
|
||||
},
|
||||
{
|
||||
"thread_id": "t-alpha",
|
||||
"run_id": "run-a1",
|
||||
"event_type": "ai_message",
|
||||
"category": "message",
|
||||
"content": "User A private answer",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
with _as_user(USER_B):
|
||||
await thread_store.ensure_thread(thread_id="t-beta")
|
||||
await event_store.put_batch(
|
||||
[
|
||||
{
|
||||
"thread_id": "t-beta",
|
||||
"run_id": "run-b1",
|
||||
"event_type": "human_message",
|
||||
"category": "message",
|
||||
"content": "User B private question",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
with _as_user(USER_A):
|
||||
msgs = await event_store.list_messages("t-alpha")
|
||||
contents = [msg["content"] for msg in msgs]
|
||||
assert "User A private question" in contents
|
||||
assert "User A private answer" in contents
|
||||
assert "User B private question" not in contents
|
||||
assert await event_store.list_messages("t-beta") == []
|
||||
assert await event_store.list_events("t-beta", "run-b1") == []
|
||||
assert await event_store.count_messages("t-beta") == 0
|
||||
|
||||
with _as_user(USER_B):
|
||||
msgs = await event_store.list_messages("t-beta")
|
||||
contents = [msg["content"] for msg in msgs]
|
||||
assert "User B private question" in contents
|
||||
assert "User A private question" not in contents
|
||||
assert await event_store.count_messages("t-alpha") == 0
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.no_auto_user
|
||||
async def test_feedback_cross_user_isolation(tmp_path):
|
||||
engine, thread_store, _, feedback_store, _ = await _make_components(tmp_path)
|
||||
try:
|
||||
with _as_user(USER_A):
|
||||
await thread_store.ensure_thread(thread_id="t-alpha")
|
||||
a_feedback = await feedback_store.create(
|
||||
run_id="run-a1",
|
||||
thread_id="t-alpha",
|
||||
rating=1,
|
||||
user_id=USER_A,
|
||||
comment="A liked this",
|
||||
)
|
||||
|
||||
with _as_user(USER_B):
|
||||
await thread_store.ensure_thread(thread_id="t-beta")
|
||||
b_feedback = await feedback_store.create(
|
||||
run_id="run-b1",
|
||||
thread_id="t-beta",
|
||||
rating=-1,
|
||||
user_id=USER_B,
|
||||
comment="B disliked this",
|
||||
)
|
||||
|
||||
with _as_user(USER_A):
|
||||
assert (await feedback_store.get(a_feedback["feedback_id"])) is not None
|
||||
assert await feedback_store.get(b_feedback["feedback_id"]) is not None
|
||||
assert await feedback_store.list_by_run("t-beta", "run-b1", user_id=USER_A) == []
|
||||
|
||||
with _as_user(USER_B):
|
||||
assert await feedback_store.list_by_run("t-alpha", "run-a1", user_id=USER_B) == []
|
||||
rows = await feedback_store.list_by_run("t-beta", "run-b1", user_id=USER_B)
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["comment"] == "B disliked this"
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.no_auto_user
|
||||
async def test_repository_without_context_raises(tmp_path):
|
||||
engine, thread_store, _, _, _ = await _make_components(tmp_path)
|
||||
try:
|
||||
with pytest.raises(RuntimeError, match="no actor context is set"):
|
||||
await thread_store.search_threads(user_id=AUTO)
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.no_auto_user
|
||||
async def test_explicit_none_bypasses_filter(tmp_path):
|
||||
engine, thread_store, _, _, _ = await _make_components(tmp_path)
|
||||
try:
|
||||
with _as_user(USER_A):
|
||||
await thread_store.ensure_thread(thread_id="t-alpha")
|
||||
with _as_user(USER_B):
|
||||
await thread_store.ensure_thread(thread_id="t-beta")
|
||||
|
||||
rows = await thread_store.search_threads(user_id=None)
|
||||
assert {row.thread_id for row in rows} == {"t-alpha", "t-beta"}
|
||||
assert await thread_store.get_thread("t-alpha", user_id=None) is not None
|
||||
assert await thread_store.get_thread("t-beta", user_id=None) is not None
|
||||
finally:
|
||||
await engine.dispose()
|
||||
Reference in New Issue
Block a user