"""Integration tests for account-based permissions.""" import pytest from httpx import AsyncClient class TestAccountPermissions: """Test suite for account-based permission checks.""" @pytest.mark.asyncio async def test_viewer_cannot_create_tree(self, client: AsyncClient, test_db): """Test that viewers cannot create trees.""" from sqlalchemy import select from app.models.user import User from uuid import UUID as PyUUID # Register a user reg_resp = await client.post("/api/v1/auth/register", json={ "email": "viewer@example.com", "password": "ViewerPass123!", "name": "Viewer User" }) assert reg_resp.status_code == 201 user_id = PyUUID(reg_resp.json()["id"]) # Demote to viewer via ORM result = await test_db.execute(select(User).where(User.id == user_id)) user = result.scalar_one() user.account_role = "viewer" await test_db.commit() # Login as viewer login_resp = await client.post("/api/v1/auth/login/json", json={ "email": "viewer@example.com", "password": "ViewerPass123!" }) viewer_headers = {"Authorization": f"Bearer {login_resp.json()['access_token']}"} # Try to create tree response = await client.post("/api/v1/trees", json={ "name": "Viewer Tree", "tree_structure": {"id": "root", "type": "solution", "title": "Test", "description": "Test", "solution": "Test solution"} }, headers=viewer_headers) assert response.status_code == 403 @pytest.mark.asyncio async def test_viewer_can_list_trees(self, client: AsyncClient, auth_headers: dict, test_db): """Test that viewers can browse/list trees.""" from sqlalchemy import select from app.models.user import User from uuid import UUID as PyUUID # Create a public tree as the regular user first await client.post("/api/v1/trees", json={ "name": "Public Tree", "tree_structure": {"id": "root", "type": "solution", "title": "T", "description": "T", "solution": "Test solution"}, "is_public": True }, headers=auth_headers) # Register viewer reg_resp = await client.post("/api/v1/auth/register", json={ "email": "viewer2@example.com", "password": "ViewerPass123!", "name": "Viewer 2" }) user_id = PyUUID(reg_resp.json()["id"]) result = await test_db.execute(select(User).where(User.id == user_id)) user = result.scalar_one() user.account_role = "viewer" await test_db.commit() login_resp = await client.post("/api/v1/auth/login/json", json={ "email": "viewer2@example.com", "password": "ViewerPass123!" }) viewer_headers = {"Authorization": f"Bearer {login_resp.json()['access_token']}"} # Viewer can list trees response = await client.get("/api/v1/trees", headers=viewer_headers) assert response.status_code == 200 @pytest.mark.asyncio async def test_owner_can_edit_account_members_tree(self, client: AsyncClient, auth_headers: dict, test_db): """Test that account owner can edit trees created by account members.""" from sqlalchemy import select from app.models.user import User from uuid import UUID as PyUUID # Get owner's account me_resp = await client.get("/api/v1/auth/me", headers=auth_headers) account_id = me_resp.json()["account_id"] # Create invite invite_resp = await client.post( "/api/v1/accounts/me/invites", json={"email": "engineer@example.com", "role": "engineer"}, headers=auth_headers ) invite_code = invite_resp.json()["code"] # Register engineer in same account reg_resp = await client.post("/api/v1/auth/register", json={ "email": "engineer@example.com", "password": "EngineerPass123!", "name": "Engineer", "account_invite_code": invite_code }) assert reg_resp.status_code == 201 assert reg_resp.json()["account_id"] == account_id # Login as engineer eng_login = await client.post("/api/v1/auth/login/json", json={ "email": "engineer@example.com", "password": "EngineerPass123!" }) eng_headers = {"Authorization": f"Bearer {eng_login.json()['access_token']}"} # Engineer creates a tree tree_resp = await client.post("/api/v1/trees", json={ "name": "Engineer's Tree", "tree_structure": {"id": "root", "type": "solution", "title": "T", "description": "T", "solution": "Test solution"} }, headers=eng_headers) assert tree_resp.status_code == 201 tree_id = tree_resp.json()["id"] # Owner can edit engineer's tree update_resp = await client.put( f"/api/v1/trees/{tree_id}", json={"name": "Owner Updated Name"}, headers=auth_headers ) assert update_resp.status_code == 200 assert update_resp.json()["name"] == "Owner Updated Name" @pytest.mark.asyncio async def test_account_scoped_visibility(self, client: AsyncClient, auth_headers: dict): """Test that account members can see each other's non-public trees.""" # Get owner's account me_resp = await client.get("/api/v1/auth/me", headers=auth_headers) account_id = me_resp.json()["account_id"] # Owner creates a private tree tree_resp = await client.post("/api/v1/trees", json={ "name": "Private Account Tree", "tree_structure": {"id": "root", "type": "solution", "title": "T", "description": "T", "solution": "Test solution"}, "is_public": False }, headers=auth_headers) assert tree_resp.status_code == 201 tree_id = tree_resp.json()["id"] # Create invite and add member invite_resp = await client.post( "/api/v1/accounts/me/invites", json={"email": "teammate@example.com", "role": "engineer"}, headers=auth_headers ) invite_code = invite_resp.json()["code"] await client.post("/api/v1/auth/register", json={ "email": "teammate@example.com", "password": "TeammatePass123!", "name": "Teammate", "account_invite_code": invite_code }) mate_login = await client.post("/api/v1/auth/login/json", json={ "email": "teammate@example.com", "password": "TeammatePass123!" }) mate_headers = {"Authorization": f"Bearer {mate_login.json()['access_token']}"} # Teammate should see the private tree (same account) response = await client.get(f"/api/v1/trees/{tree_id}", headers=mate_headers) assert response.status_code == 200 assert response.json()["name"] == "Private Account Tree" @pytest.mark.asyncio async def test_different_account_cannot_see_private_tree(self, client: AsyncClient, auth_headers: dict): """Test that users from different accounts cannot see private trees.""" # Owner creates a private tree tree_resp = await client.post("/api/v1/trees", json={ "name": "Secret Tree", "tree_structure": {"id": "root", "type": "solution", "title": "T", "description": "T", "solution": "Test solution"}, "is_public": False }, headers=auth_headers) assert tree_resp.status_code == 201 tree_id = tree_resp.json()["id"] # Register a completely separate user (different account) await client.post("/api/v1/auth/register", json={ "email": "outsider@example.com", "password": "OutsiderPass123!", "name": "Outsider" }) outsider_login = await client.post("/api/v1/auth/login/json", json={ "email": "outsider@example.com", "password": "OutsiderPass123!" }) outsider_headers = {"Authorization": f"Bearer {outsider_login.json()['access_token']}"} # Outsider should NOT see the private tree. # With RLS, the tree is invisible to other tenants — 404 not 403. response = await client.get(f"/api/v1/trees/{tree_id}", headers=outsider_headers) assert response.status_code == 404