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:
chihlasm
2026-04-12 03:48:30 +00:00
parent a48660700a
commit 52553d62d2
3 changed files with 10 additions and 38 deletions

View File

@@ -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(

View File

@@ -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

View File

@@ -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)