mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-06 20:15:17 +02:00
280 lines
8 KiB
Python
280 lines
8 KiB
Python
|
|
"""Integration tests for new-chat thread visibility invariants.
|
||
|
|
|
||
|
|
These tests exercise the route handlers directly with real DB-backed
|
||
|
|
users, memberships, and permissions. The important contract is that a
|
||
|
|
thread shared with a search space stays shared across normal metadata
|
||
|
|
updates until the creator explicitly makes it private again.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import uuid
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
import pytest_asyncio
|
||
|
|
from fastapi import HTTPException
|
||
|
|
from sqlalchemy import select
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
|
||
|
|
from app.db import (
|
||
|
|
ChatVisibility,
|
||
|
|
SearchSpace,
|
||
|
|
SearchSpaceMembership,
|
||
|
|
SearchSpaceRole,
|
||
|
|
User,
|
||
|
|
)
|
||
|
|
from app.routes import new_chat_routes
|
||
|
|
from app.schemas.new_chat import (
|
||
|
|
NewChatThreadCreate,
|
||
|
|
NewChatThreadUpdate,
|
||
|
|
NewChatThreadVisibilityUpdate,
|
||
|
|
)
|
||
|
|
|
||
|
|
pytestmark = pytest.mark.integration
|
||
|
|
|
||
|
|
|
||
|
|
@pytest_asyncio.fixture
|
||
|
|
async def db_member(db_session: AsyncSession, db_search_space: SearchSpace) -> User:
|
||
|
|
member = User(
|
||
|
|
id=uuid.uuid4(),
|
||
|
|
email="member@surfsense.net",
|
||
|
|
hashed_password="hashed",
|
||
|
|
is_active=True,
|
||
|
|
is_superuser=False,
|
||
|
|
is_verified=True,
|
||
|
|
)
|
||
|
|
db_session.add(member)
|
||
|
|
await db_session.flush()
|
||
|
|
|
||
|
|
role = (
|
||
|
|
(
|
||
|
|
await db_session.execute(
|
||
|
|
select(SearchSpaceRole).where(
|
||
|
|
SearchSpaceRole.search_space_id == db_search_space.id,
|
||
|
|
SearchSpaceRole.name == "Editor",
|
||
|
|
)
|
||
|
|
)
|
||
|
|
)
|
||
|
|
.scalars()
|
||
|
|
.one()
|
||
|
|
)
|
||
|
|
db_session.add(
|
||
|
|
SearchSpaceMembership(
|
||
|
|
user_id=member.id,
|
||
|
|
search_space_id=db_search_space.id,
|
||
|
|
role_id=role.id,
|
||
|
|
is_owner=False,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
await db_session.flush()
|
||
|
|
return member
|
||
|
|
|
||
|
|
|
||
|
|
async def _create_thread(
|
||
|
|
db_session: AsyncSession,
|
||
|
|
db_user: User,
|
||
|
|
db_search_space: SearchSpace,
|
||
|
|
*,
|
||
|
|
title: str = "Visibility Invariant Chat",
|
||
|
|
):
|
||
|
|
return await new_chat_routes.create_thread(
|
||
|
|
NewChatThreadCreate(
|
||
|
|
title=title,
|
||
|
|
archived=False,
|
||
|
|
search_space_id=db_search_space.id,
|
||
|
|
visibility=ChatVisibility.PRIVATE,
|
||
|
|
),
|
||
|
|
session=db_session,
|
||
|
|
user=db_user,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _active_thread_ids(response) -> set[int]:
|
||
|
|
return {thread.id for thread in response.threads}
|
||
|
|
|
||
|
|
|
||
|
|
def _search_thread_ids(response) -> set[int]:
|
||
|
|
return {thread.id for thread in response}
|
||
|
|
|
||
|
|
|
||
|
|
async def test_private_thread_is_hidden_from_other_search_space_member(
|
||
|
|
db_session: AsyncSession,
|
||
|
|
db_user: User,
|
||
|
|
db_member: User,
|
||
|
|
db_search_space: SearchSpace,
|
||
|
|
):
|
||
|
|
thread = await _create_thread(db_session, db_user, db_search_space)
|
||
|
|
|
||
|
|
member_threads = await new_chat_routes.list_threads(
|
||
|
|
search_space_id=db_search_space.id,
|
||
|
|
session=db_session,
|
||
|
|
user=db_member,
|
||
|
|
)
|
||
|
|
member_search = await new_chat_routes.search_threads(
|
||
|
|
search_space_id=db_search_space.id,
|
||
|
|
title="Visibility",
|
||
|
|
session=db_session,
|
||
|
|
user=db_member,
|
||
|
|
)
|
||
|
|
|
||
|
|
assert thread.id not in _active_thread_ids(member_threads)
|
||
|
|
assert thread.id not in _search_thread_ids(member_search)
|
||
|
|
with pytest.raises(HTTPException) as exc_info:
|
||
|
|
await new_chat_routes.get_thread_full(
|
||
|
|
thread_id=thread.id,
|
||
|
|
session=db_session,
|
||
|
|
user=db_member,
|
||
|
|
)
|
||
|
|
assert exc_info.value.status_code == 403
|
||
|
|
|
||
|
|
|
||
|
|
async def test_creator_can_share_thread_and_member_can_list_search_read_it(
|
||
|
|
db_session: AsyncSession,
|
||
|
|
db_user: User,
|
||
|
|
db_member: User,
|
||
|
|
db_search_space: SearchSpace,
|
||
|
|
):
|
||
|
|
thread = await _create_thread(db_session, db_user, db_search_space)
|
||
|
|
|
||
|
|
updated = await new_chat_routes.update_thread_visibility(
|
||
|
|
thread_id=thread.id,
|
||
|
|
visibility_update=NewChatThreadVisibilityUpdate(
|
||
|
|
visibility=ChatVisibility.SEARCH_SPACE,
|
||
|
|
),
|
||
|
|
session=db_session,
|
||
|
|
user=db_user,
|
||
|
|
)
|
||
|
|
|
||
|
|
member_threads = await new_chat_routes.list_threads(
|
||
|
|
search_space_id=db_search_space.id,
|
||
|
|
session=db_session,
|
||
|
|
user=db_member,
|
||
|
|
)
|
||
|
|
member_search = await new_chat_routes.search_threads(
|
||
|
|
search_space_id=db_search_space.id,
|
||
|
|
title="Visibility",
|
||
|
|
session=db_session,
|
||
|
|
user=db_member,
|
||
|
|
)
|
||
|
|
full_thread = await new_chat_routes.get_thread_full(
|
||
|
|
thread_id=thread.id,
|
||
|
|
session=db_session,
|
||
|
|
user=db_member,
|
||
|
|
)
|
||
|
|
|
||
|
|
assert updated.visibility == ChatVisibility.SEARCH_SPACE
|
||
|
|
assert thread.id in _active_thread_ids(member_threads)
|
||
|
|
assert thread.id in _search_thread_ids(member_search)
|
||
|
|
assert full_thread["id"] == thread.id
|
||
|
|
assert full_thread["visibility"] == ChatVisibility.SEARCH_SPACE
|
||
|
|
|
||
|
|
|
||
|
|
async def test_rename_and_archive_do_not_reset_shared_visibility(
|
||
|
|
db_session: AsyncSession,
|
||
|
|
db_user: User,
|
||
|
|
db_search_space: SearchSpace,
|
||
|
|
):
|
||
|
|
thread = await _create_thread(db_session, db_user, db_search_space)
|
||
|
|
await new_chat_routes.update_thread_visibility(
|
||
|
|
thread_id=thread.id,
|
||
|
|
visibility_update=NewChatThreadVisibilityUpdate(
|
||
|
|
visibility=ChatVisibility.SEARCH_SPACE,
|
||
|
|
),
|
||
|
|
session=db_session,
|
||
|
|
user=db_user,
|
||
|
|
)
|
||
|
|
|
||
|
|
renamed = await new_chat_routes.update_thread(
|
||
|
|
thread_id=thread.id,
|
||
|
|
thread_update=NewChatThreadUpdate(title="Renamed Shared Chat"),
|
||
|
|
session=db_session,
|
||
|
|
user=db_user,
|
||
|
|
)
|
||
|
|
archived = await new_chat_routes.update_thread(
|
||
|
|
thread_id=thread.id,
|
||
|
|
thread_update=NewChatThreadUpdate(archived=True),
|
||
|
|
session=db_session,
|
||
|
|
user=db_user,
|
||
|
|
)
|
||
|
|
|
||
|
|
assert renamed.visibility == ChatVisibility.SEARCH_SPACE
|
||
|
|
assert archived.visibility == ChatVisibility.SEARCH_SPACE
|
||
|
|
assert archived.archived is True
|
||
|
|
|
||
|
|
|
||
|
|
async def test_non_creator_cannot_change_shared_thread_back_to_private(
|
||
|
|
db_session: AsyncSession,
|
||
|
|
db_user: User,
|
||
|
|
db_member: User,
|
||
|
|
db_search_space: SearchSpace,
|
||
|
|
):
|
||
|
|
thread = await _create_thread(db_session, db_user, db_search_space)
|
||
|
|
await new_chat_routes.update_thread_visibility(
|
||
|
|
thread_id=thread.id,
|
||
|
|
visibility_update=NewChatThreadVisibilityUpdate(
|
||
|
|
visibility=ChatVisibility.SEARCH_SPACE,
|
||
|
|
),
|
||
|
|
session=db_session,
|
||
|
|
user=db_user,
|
||
|
|
)
|
||
|
|
|
||
|
|
with pytest.raises(HTTPException) as exc_info:
|
||
|
|
await new_chat_routes.update_thread_visibility(
|
||
|
|
thread_id=thread.id,
|
||
|
|
visibility_update=NewChatThreadVisibilityUpdate(
|
||
|
|
visibility=ChatVisibility.PRIVATE,
|
||
|
|
),
|
||
|
|
session=db_session,
|
||
|
|
user=db_member,
|
||
|
|
)
|
||
|
|
|
||
|
|
assert exc_info.value.status_code == 403
|
||
|
|
|
||
|
|
|
||
|
|
async def test_creator_can_make_shared_thread_private_again(
|
||
|
|
db_session: AsyncSession,
|
||
|
|
db_user: User,
|
||
|
|
db_member: User,
|
||
|
|
db_search_space: SearchSpace,
|
||
|
|
):
|
||
|
|
thread = await _create_thread(db_session, db_user, db_search_space)
|
||
|
|
await new_chat_routes.update_thread_visibility(
|
||
|
|
thread_id=thread.id,
|
||
|
|
visibility_update=NewChatThreadVisibilityUpdate(
|
||
|
|
visibility=ChatVisibility.SEARCH_SPACE,
|
||
|
|
),
|
||
|
|
session=db_session,
|
||
|
|
user=db_user,
|
||
|
|
)
|
||
|
|
|
||
|
|
private_again = await new_chat_routes.update_thread_visibility(
|
||
|
|
thread_id=thread.id,
|
||
|
|
visibility_update=NewChatThreadVisibilityUpdate(
|
||
|
|
visibility=ChatVisibility.PRIVATE,
|
||
|
|
),
|
||
|
|
session=db_session,
|
||
|
|
user=db_user,
|
||
|
|
)
|
||
|
|
member_threads = await new_chat_routes.list_threads(
|
||
|
|
search_space_id=db_search_space.id,
|
||
|
|
session=db_session,
|
||
|
|
user=db_member,
|
||
|
|
)
|
||
|
|
member_search = await new_chat_routes.search_threads(
|
||
|
|
search_space_id=db_search_space.id,
|
||
|
|
title="Visibility",
|
||
|
|
session=db_session,
|
||
|
|
user=db_member,
|
||
|
|
)
|
||
|
|
|
||
|
|
assert private_again.visibility == ChatVisibility.PRIVATE
|
||
|
|
assert thread.id not in _active_thread_ids(member_threads)
|
||
|
|
assert thread.id not in _search_thread_ids(member_search)
|
||
|
|
with pytest.raises(HTTPException) as exc_info:
|
||
|
|
await new_chat_routes.get_thread_full(
|
||
|
|
thread_id=thread.id,
|
||
|
|
session=db_session,
|
||
|
|
user=db_member,
|
||
|
|
)
|
||
|
|
assert exc_info.value.status_code == 403
|