mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-06 20:15:17 +02:00
feat(chat): Introduce centralized thread metadata management and update chat visibility handling with new hooks for thread mutations
This commit is contained in:
parent
0cfe5e52bd
commit
8b704b2fef
10 changed files with 832 additions and 105 deletions
|
|
@ -0,0 +1,279 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue