"""`readable_notes_clause` — the set-based ACL predicate behind list queries. It exists because `get_note_permission` answers "may I read THIS note?" one row at a time, which a list query can't use. The two must stay in step: anything readable one way has to be findable the other, or a record shared with you is openable by id but invisible in every list — the bug this predicate fixed. No DB: the group lookup is stubbed and the resulting clause is compiled to SQL so the shape can be asserted directly. """ from unittest.mock import AsyncMock, MagicMock, patch import pytest def _session_returning_groups(group_ids: list[int]): """A stub async session whose only query — the caller's group ids — returns `group_ids`.""" result = MagicMock() result.scalars.return_value.all.return_value = group_ids session = AsyncMock() session.__aenter__ = AsyncMock(return_value=session) session.__aexit__ = AsyncMock(return_value=False) session.execute = AsyncMock(return_value=result) return session async def _compiled_clause(group_ids: list[int]) -> str: from scribe.services.access import readable_notes_clause with patch("scribe.services.access.async_session") as cls: cls.return_value = _session_returning_groups(group_ids) clause = await readable_notes_clause(user_id=7) return str(clause.compile(compile_kwargs={"literal_binds": True})) async def _compiled_browse_clause(group_ids: list[int]) -> str: from scribe.services.access import browsable_notes_clause with patch("scribe.services.access.async_session") as cls: cls.return_value = _session_returning_groups(group_ids) clause = await browsable_notes_clause(user_id=7) return str(clause.compile(compile_kwargs={"literal_binds": True})) @pytest.mark.asyncio async def test_clause_covers_ownership_and_both_share_paths(): sql = await _compiled_clause(group_ids=[]) # 1. ownership assert "notes.user_id = 7" in sql # 2/3. a direct or group share on the note itself assert "note_shares" in sql assert "shared_with_user_id = 7" in sql # 4. inheritance from a shared project assert "project_shares" in sql assert "notes.project_id IN" in sql @pytest.mark.asyncio async def test_group_membership_widens_both_share_lookups(): sql = await _compiled_clause(group_ids=[3, 9]) assert "shared_with_group_id IN (3, 9)" in sql # Both the note-level and project-level share lookups gain the group arm. assert sql.count("shared_with_group_id IN (3, 9)") == 2 @pytest.mark.asyncio async def test_no_groups_means_no_group_arm(): """A user in no groups must not produce an empty `IN ()`, which would be invalid SQL on some backends and always-false on others.""" sql = await _compiled_clause(group_ids=[]) assert "shared_with_group_id" not in sql @pytest.mark.asyncio async def test_owner_only_result_is_never_the_whole_table(): """Guard against the predicate degrading to something always-true — that would expose every user's notes to every other user.""" sql = await _compiled_clause(group_ids=[]) assert "true" not in sql.lower() assert "1 = 1" not in sql # --- browse scope: the trust boundary --------------------------------------- @pytest.mark.asyncio async def test_browse_scope_excludes_direct_note_shares(): """The whole point of the narrower scope (decision note 2094): a record shared directly with you must NOT appear in a passive list. If `note_shares` leaks in here, someone else's one-off lands in the operator's own browse list, facet counts, and skill manifest as though they had recorded it.""" sql = await _compiled_browse_clause(group_ids=[3]) assert "note_shares" not in sql @pytest.mark.asyncio async def test_browse_scope_keeps_ownership_and_project_access(): sql = await _compiled_browse_clause(group_ids=[]) assert "notes.user_id = 7" in sql # your own assert "project_shares" in sql # a project shared with you assert "projects" in sql # a project you own @pytest.mark.asyncio async def test_browse_scope_is_strictly_narrower_than_read_scope(): """Browse must never surface something read scope wouldn't also allow — otherwise a list could show a record the caller can't open.""" browse = await _compiled_browse_clause(group_ids=[3]) read = await _compiled_clause(group_ids=[3]) assert "note_shares" in read and "note_shares" not in browse for shared_arm in ("notes.user_id = 7", "project_shares"): assert shared_arm in browse and shared_arm in read