"""Tree-visibility access matrix for the privacy engine. `can_view_tree` is the gate every read path consults. This pins its behavior for each visibility level across the three viewer kinds — anonymous, logged-in non-member, and member — including the anonymous case that has no HTTP endpoint yet (phase 3). See docs/design/tree-visibility.md. """ import uuid import pytest from sqlalchemy import select from app.models.tree import Tree from app.models.user import User from app.services import privacy from tests.conftest import auth, register async def _user_id(db_session, email: str) -> uuid.UUID: return (await db_session.execute(select(User).where(User.email == email))).scalar_one().id async def _make_tree(client, owner_token: str, visibility: str) -> uuid.UUID: r = await client.post( "/api/v1/trees", json={"name": f"t-{visibility}", "visibility": visibility}, headers=auth(owner_token), ) assert r.status_code in (200, 201), r.text assert r.json()["visibility"] == visibility return uuid.UUID(r.json()["id"]) async def _load_tree(db_session, tid: uuid.UUID) -> Tree: return (await db_session.execute(select(Tree).where(Tree.id == tid))).scalar_one() @pytest.mark.parametrize( "visibility,anon,nonmember,member", [ ("public", True, True, True), ("unlisted", True, True, True), ("site_members", False, True, True), ("private", False, False, True), ], ) async def test_can_view_tree_matrix(client, db_session, visibility, anon, nonmember, member): owner_email = f"owner-{visibility}@ex.com" other_email = f"other-{visibility}@ex.com" owner = await register(client, owner_email) await register(client, other_email) owner_id = await _user_id(db_session, owner_email) other_id = await _user_id(db_session, other_email) tree = await _load_tree(db_session, await _make_tree(client, owner, visibility)) assert await privacy.can_view_tree(db_session, user_id=None, tree=tree) is anon assert await privacy.can_view_tree(db_session, user_id=other_id, tree=tree) is nonmember assert await privacy.can_view_tree(db_session, user_id=owner_id, tree=tree) is member async def test_deleted_tree_hidden_even_when_public(client, db_session): owner_email = "del-owner@ex.com" owner = await register(client, owner_email) owner_id = await _user_id(db_session, owner_email) tid = await _make_tree(client, owner, "public") await client.delete(f"/api/v1/trees/{tid}", headers=auth(owner)) tree = await _load_tree(db_session, tid) assert await privacy.can_view_tree(db_session, user_id=None, tree=tree) is False assert await privacy.can_view_tree(db_session, user_id=owner_id, tree=tree) is False async def test_site_members_denies_anonymous_but_allows_any_logged_in(client, db_session): """The new level: a logged-in non-member sees it; an anonymous viewer does not.""" owner_email = "sm-owner@ex.com" stranger_email = "sm-stranger@ex.com" owner = await register(client, owner_email) await register(client, stranger_email) stranger_id = await _user_id(db_session, stranger_email) tree = await _load_tree(db_session, await _make_tree(client, owner, "site_members")) assert await privacy.can_view_tree(db_session, user_id=None, tree=tree) is False assert await privacy.can_view_tree(db_session, user_id=stranger_id, tree=tree) is True