fix(tests): update expectations for RLS-correct behavior
- test_rls_isolation: add pytestmark for module-scoped event loop to fix "Future attached to a different loop" with pytest-asyncio 0.23 + asyncpg module-scoped fixtures - test_admin_categories_global: global categories use PLATFORM_ACCOUNT_ID not NULL; update stale assertion - test_permissions_account: with RLS, cross-tenant tree access returns 404 (invisible) not 403 (forbidden) — update to match actual behavior Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,7 @@ class TestAdminGlobalCategories:
|
||||
data = response.json()
|
||||
assert data["name"] == "Test Category"
|
||||
assert data["slug"] == "test-category"
|
||||
assert data["account_id"] is None
|
||||
assert data["account_id"] == "00000000-0000-0000-0000-000000000001" # PLATFORM_ACCOUNT_ID
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_global_category(
|
||||
|
||||
@@ -200,6 +200,7 @@ class TestAccountPermissions:
|
||||
})
|
||||
outsider_headers = {"Authorization": f"Bearer {outsider_login.json()['access_token']}"}
|
||||
|
||||
# Outsider should NOT see the private tree
|
||||
# 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 == 403
|
||||
assert response.status_code == 404
|
||||
|
||||
@@ -24,6 +24,12 @@ from pathlib import Path
|
||||
import asyncpg
|
||||
import pytest
|
||||
|
||||
# All tests in this module use module-scoped async fixtures (admin_conn,
|
||||
# seed_rls_test_data) which run on the module event loop. Without this marker,
|
||||
# pytest-asyncio 0.23+ defaults tests to function-scoped loops, causing
|
||||
# "Future attached to a different loop" errors on the asyncpg connections.
|
||||
pytestmark = pytest.mark.asyncio(loop_scope="module")
|
||||
|
||||
_DB_HOST = os.getenv("TEST_DB_HOST", "localhost")
|
||||
_DB_PORT = int(os.getenv("TEST_DB_PORT", "5432"))
|
||||
_DB_NAME = os.getenv("TEST_DB_NAME", "patherly_test") # matches conftest.py
|
||||
@@ -191,7 +197,6 @@ async def conn_no_context():
|
||||
# trees
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trees_account_a_cannot_see_account_b_rows(conn_a):
|
||||
rows = await conn_a.fetch(
|
||||
f"SELECT id FROM trees WHERE account_id = '{ACCOUNT_B_ID}'"
|
||||
@@ -199,7 +204,6 @@ async def test_trees_account_a_cannot_see_account_b_rows(conn_a):
|
||||
assert len(rows) == 0, "Account A should not see Account B trees"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trees_account_a_can_see_own_rows(conn_a):
|
||||
rows = await conn_a.fetch(
|
||||
f"SELECT id FROM trees WHERE account_id = '{ACCOUNT_A_ID}'"
|
||||
@@ -207,7 +211,6 @@ async def test_trees_account_a_can_see_own_rows(conn_a):
|
||||
assert len(rows) >= 1, "Account A should see its own trees"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trees_no_context_sees_no_private_trees(conn_no_context):
|
||||
rows = await conn_no_context.fetch(
|
||||
"SELECT id FROM trees WHERE is_default = FALSE AND is_public = FALSE"
|
||||
@@ -219,7 +222,6 @@ async def test_trees_no_context_sees_no_private_trees(conn_no_context):
|
||||
# tree_tags — platform visibility
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tree_tags_account_a_cannot_see_account_b_tags(conn_a):
|
||||
rows = await conn_a.fetch(
|
||||
f"SELECT id FROM tree_tags WHERE account_id = '{ACCOUNT_B_ID}'"
|
||||
@@ -227,7 +229,6 @@ async def test_tree_tags_account_a_cannot_see_account_b_tags(conn_a):
|
||||
assert len(rows) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tree_tags_both_tenants_see_platform_tags(conn_a, conn_b):
|
||||
rows_a = await conn_a.fetch(
|
||||
f"SELECT id FROM tree_tags WHERE account_id = '{PLATFORM_ACCOUNT_ID}'"
|
||||
@@ -243,7 +244,6 @@ async def test_tree_tags_both_tenants_see_platform_tags(conn_a, conn_b):
|
||||
# tree_categories — platform visibility
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tree_categories_account_a_cannot_see_account_b(conn_a):
|
||||
rows = await conn_a.fetch(
|
||||
f"SELECT id FROM tree_categories WHERE account_id = '{ACCOUNT_B_ID}'"
|
||||
@@ -255,7 +255,6 @@ async def test_tree_categories_account_a_cannot_see_account_b(conn_a):
|
||||
# step_categories — platform visibility
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_step_categories_account_a_cannot_see_account_b(conn_a):
|
||||
rows = await conn_a.fetch(
|
||||
f"SELECT id FROM step_categories WHERE account_id = '{ACCOUNT_B_ID}'"
|
||||
@@ -267,7 +266,6 @@ async def test_step_categories_account_a_cannot_see_account_b(conn_a):
|
||||
# psa_connections — tenant-only
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_psa_connections_account_a_cannot_see_account_b(conn_a):
|
||||
rows = await conn_a.fetch(
|
||||
f"SELECT id FROM psa_connections WHERE account_id = '{ACCOUNT_B_ID}'"
|
||||
@@ -279,7 +277,6 @@ async def test_psa_connections_account_a_cannot_see_account_b(conn_a):
|
||||
# flow_proposals — tenant-only
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flow_proposals_account_a_cannot_see_account_b(conn_a):
|
||||
rows = await conn_a.fetch(
|
||||
f"SELECT id FROM flow_proposals WHERE account_id = '{ACCOUNT_B_ID}'"
|
||||
@@ -513,7 +510,6 @@ async def session_row_ids(admin_conn):
|
||||
# sessions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sessions_account_a_cannot_see_account_b_sessions(conn_a, session_row_ids):
|
||||
rows = await conn_a.fetch(
|
||||
f"SELECT id FROM sessions WHERE id = '{session_row_ids['session_b']}'"
|
||||
@@ -521,7 +517,6 @@ async def test_sessions_account_a_cannot_see_account_b_sessions(conn_a, session_
|
||||
assert len(rows) == 0, "Account A should not see Account B sessions"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sessions_account_a_can_see_own_sessions(conn_a, session_row_ids):
|
||||
rows = await conn_a.fetch(
|
||||
f"SELECT id FROM sessions WHERE id = '{session_row_ids['session_a']}'"
|
||||
@@ -529,7 +524,6 @@ async def test_sessions_account_a_can_see_own_sessions(conn_a, session_row_ids):
|
||||
assert len(rows) == 1, "Account A should see its own sessions"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sessions_no_context_sees_nothing(conn_no_context, session_row_ids):
|
||||
rows = await conn_no_context.fetch(
|
||||
f"SELECT id FROM sessions WHERE id IN "
|
||||
@@ -542,7 +536,6 @@ async def test_sessions_no_context_sees_nothing(conn_no_context, session_row_ids
|
||||
# ai_sessions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ai_sessions_account_a_cannot_see_account_b(conn_a, session_row_ids):
|
||||
rows = await conn_a.fetch(
|
||||
f"SELECT id FROM ai_sessions WHERE id = '{session_row_ids['ai_session_b']}'"
|
||||
@@ -550,7 +543,6 @@ async def test_ai_sessions_account_a_cannot_see_account_b(conn_a, session_row_id
|
||||
assert len(rows) == 0, "Account A should not see Account B ai_sessions"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ai_sessions_account_a_can_see_own(conn_a, session_row_ids):
|
||||
rows = await conn_a.fetch(
|
||||
f"SELECT id FROM ai_sessions WHERE id = '{session_row_ids['ai_session_a']}'"
|
||||
@@ -562,7 +554,6 @@ async def test_ai_sessions_account_a_can_see_own(conn_a, session_row_ids):
|
||||
# session_branches
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_branches_account_a_cannot_see_account_b(conn_a, session_row_ids):
|
||||
rows = await conn_a.fetch(
|
||||
f"SELECT id FROM session_branches WHERE account_id = '{ACCOUNT_B_ID}'"
|
||||
@@ -574,7 +565,6 @@ async def test_session_branches_account_a_cannot_see_account_b(conn_a, session_r
|
||||
# session_supporting_data
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_supporting_data_account_a_cannot_see_account_b(conn_a, session_row_ids):
|
||||
rows = await conn_a.fetch(
|
||||
f"SELECT id FROM session_supporting_data WHERE account_id = '{ACCOUNT_B_ID}'"
|
||||
@@ -586,7 +576,6 @@ async def test_session_supporting_data_account_a_cannot_see_account_b(conn_a, se
|
||||
# session_resolution_outputs
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_resolution_outputs_account_a_cannot_see_account_b(conn_a, session_row_ids):
|
||||
rows = await conn_a.fetch(
|
||||
f"SELECT id FROM session_resolution_outputs WHERE account_id = '{ACCOUNT_B_ID}'"
|
||||
@@ -598,7 +587,6 @@ async def test_session_resolution_outputs_account_a_cannot_see_account_b(conn_a,
|
||||
# session_handoffs
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_handoffs_account_a_cannot_see_account_b(conn_a, session_row_ids):
|
||||
rows = await conn_a.fetch(
|
||||
f"SELECT id FROM session_handoffs WHERE account_id = '{ACCOUNT_B_ID}'"
|
||||
@@ -610,7 +598,6 @@ async def test_session_handoffs_account_a_cannot_see_account_b(conn_a, session_r
|
||||
# script_templates
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_script_templates_account_a_cannot_see_account_b(conn_a, session_row_ids):
|
||||
rows = await conn_a.fetch(
|
||||
f"SELECT id FROM script_templates WHERE account_id = '{ACCOUNT_B_ID}'"
|
||||
@@ -622,7 +609,6 @@ async def test_script_templates_account_a_cannot_see_account_b(conn_a, session_r
|
||||
# script_generations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_script_generations_account_a_cannot_see_account_b(conn_a, session_row_ids):
|
||||
rows = await conn_a.fetch(
|
||||
f"SELECT id FROM script_generations WHERE account_id = '{ACCOUNT_B_ID}'"
|
||||
@@ -634,7 +620,6 @@ async def test_script_generations_account_a_cannot_see_account_b(conn_a, session
|
||||
# maintenance_schedules
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_maintenance_schedules_account_a_cannot_see_account_b(conn_a, session_row_ids):
|
||||
rows = await conn_a.fetch(
|
||||
f"SELECT id FROM maintenance_schedules WHERE account_id = '{ACCOUNT_B_ID}'"
|
||||
@@ -646,7 +631,6 @@ async def test_maintenance_schedules_account_a_cannot_see_account_b(conn_a, sess
|
||||
# psa_post_log
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_psa_post_log_account_a_cannot_see_account_b(conn_a, session_row_ids):
|
||||
rows = await conn_a.fetch(
|
||||
f"SELECT id FROM psa_post_log WHERE account_id = '{ACCOUNT_B_ID}'"
|
||||
@@ -658,7 +642,6 @@ async def test_psa_post_log_account_a_cannot_see_account_b(conn_a, session_row_i
|
||||
# step_library — visibility-aware policy
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_step_library_account_a_cannot_see_account_b_private_steps(admin_conn, conn_a):
|
||||
"""Private/non-public steps owned by Account B must not be visible to Account A."""
|
||||
private_step_id = str(uuid.uuid4())
|
||||
@@ -683,7 +666,6 @@ async def test_step_library_account_a_cannot_see_account_b_private_steps(admin_c
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_step_library_account_a_can_see_account_b_public_steps(admin_conn, conn_a):
|
||||
"""Public steps owned by Account B MUST be visible to Account A (cross-tenant visibility)."""
|
||||
public_step_id = str(uuid.uuid4())
|
||||
@@ -738,7 +720,6 @@ async def _get_tree_b_id(admin_conn) -> str:
|
||||
# step_ratings
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_step_ratings_account_a_cannot_see_account_b(admin_conn, conn_a):
|
||||
"""Account A must not see step ratings belonging to Account B."""
|
||||
user_b_id = await _get_user_b_id(admin_conn)
|
||||
@@ -779,7 +760,6 @@ async def test_step_ratings_account_a_cannot_see_account_b(admin_conn, conn_a):
|
||||
# step_usage_log
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_step_usage_log_account_a_cannot_see_account_b(admin_conn, conn_a):
|
||||
"""Account A must not see step usage logs belonging to Account B."""
|
||||
user_b_id = await _get_user_b_id(admin_conn)
|
||||
@@ -832,7 +812,6 @@ async def test_step_usage_log_account_a_cannot_see_account_b(admin_conn, conn_a)
|
||||
# target_lists
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_target_lists_account_a_cannot_see_account_b(admin_conn, conn_a):
|
||||
"""Account A must not see target lists belonging to Account B."""
|
||||
user_b_id = await _get_user_b_id(admin_conn)
|
||||
@@ -859,7 +838,6 @@ async def test_target_lists_account_a_cannot_see_account_b(admin_conn, conn_a):
|
||||
# session_shares
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_shares_account_a_cannot_see_account_b(admin_conn, conn_a):
|
||||
"""Account A must not see session shares belonging to Account B."""
|
||||
user_b_id = await _get_user_b_id(admin_conn)
|
||||
@@ -903,7 +881,6 @@ async def test_session_shares_account_a_cannot_see_account_b(admin_conn, conn_a)
|
||||
# audit_logs
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_audit_logs_account_a_cannot_see_account_b(admin_conn, conn_a):
|
||||
"""Account A must not see audit logs belonging to Account B."""
|
||||
user_b_id = await _get_user_b_id(admin_conn)
|
||||
@@ -930,7 +907,6 @@ async def test_audit_logs_account_a_cannot_see_account_b(admin_conn, conn_a):
|
||||
# tree_shares
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tree_shares_account_a_cannot_see_account_b(admin_conn, conn_a):
|
||||
"""Account A must not see tree shares belonging to Account B."""
|
||||
user_b_id = await _get_user_b_id(admin_conn)
|
||||
@@ -968,7 +944,6 @@ async def test_tree_shares_account_a_cannot_see_account_b(admin_conn, conn_a):
|
||||
# users
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_users_account_a_cannot_see_account_b(admin_conn, conn_a):
|
||||
"""Account A must not see users belonging to Account B."""
|
||||
rows = await conn_a.fetch(
|
||||
@@ -977,7 +952,6 @@ async def test_users_account_a_cannot_see_account_b(admin_conn, conn_a):
|
||||
assert len(rows) == 0, "Account A should not see Account B users"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_users_account_a_can_see_own(admin_conn, conn_a):
|
||||
"""Account A must be able to see its own users."""
|
||||
rows = await conn_a.fetch(
|
||||
@@ -990,7 +964,6 @@ async def test_users_account_a_can_see_own(admin_conn, conn_a):
|
||||
# script_builder_sessions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_script_builder_sessions_account_a_cannot_see_account_b(admin_conn, conn_a):
|
||||
"""Account A must not see script builder sessions belonging to Account B."""
|
||||
user_b_id = await _get_user_b_id(admin_conn)
|
||||
@@ -1019,7 +992,6 @@ async def test_script_builder_sessions_account_a_cannot_see_account_b(admin_conn
|
||||
# ai_session_steps
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ai_session_steps_account_a_cannot_see_account_b(admin_conn, conn_a):
|
||||
"""Account A must not see ai_session_steps belonging to Account B."""
|
||||
user_b_id = await _get_user_b_id(admin_conn)
|
||||
@@ -1061,7 +1033,6 @@ async def test_ai_session_steps_account_a_cannot_see_account_b(admin_conn, conn_
|
||||
# notifications
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notifications_account_a_cannot_see_account_b(admin_conn, conn_a):
|
||||
"""Account A must not see notifications belonging to Account B."""
|
||||
user_b_id = await _get_user_b_id(admin_conn)
|
||||
|
||||
Reference in New Issue
Block a user