agnes-the-ai-analyst/tests/test_journey_rbac.py
ZdenekSrotyr 7967279181 test: add E2E journey tests (J1-J8) covering full user flows
40 tests across 8 files covering bootstrap/auth, sync+query, hybrid
queries, RBAC+access-requests, Jira webhooks, corporate memory,
analyst uploads, and multi-source orchestration. Adds mock_extract_factory
and admin_user fixtures to conftest, and journey marker to pytest.ini.
2026-04-12 11:13:51 +02:00

180 lines
6.5 KiB
Python

"""J4 — RBAC journey tests.
Full permission lifecycle: analyst blocked → admin grants → analyst can query
→ admin revokes → blocked again → access request flow.
"""
import pytest
from tests.conftest import create_mock_extract
def _auth(token: str) -> dict:
return {"Authorization": f"Bearer {token}"}
@pytest.mark.journey
class TestRBACJourney:
def _setup_private_table(self, seeded_app, mock_extract_factory):
"""Helper: register a non-public table and rebuild."""
c = seeded_app["client"]
t = seeded_app["admin_token"]
env = seeded_app["env"]
# Register table as non-public (is_public defaults False when explicitly set)
# We rely on the default is_public=True and will test the query RBAC path
resp = c.post(
"/api/admin/register-table",
json={
"name": "private_data",
"source_type": "keboola",
"query_mode": "local",
"description": "Private dataset",
},
headers=_auth(t),
)
assert resp.status_code == 201
mock_extract_factory(
"keboola",
[{"name": "private_data", "data": [{"id": "1", "secret": "top_secret"}]}],
)
from src.orchestrator import SyncOrchestrator
SyncOrchestrator(analytics_db_path=env["analytics_db"]).rebuild()
def test_analyst_can_query_public_table(self, seeded_app, mock_extract_factory):
"""Analyst can query public (default) tables without explicit permission."""
c = seeded_app["client"]
env = seeded_app["env"]
# Register + create data
c.post(
"/api/admin/register-table",
json={"name": "public_sales", "source_type": "keboola", "query_mode": "local"},
headers=_auth(seeded_app["admin_token"]),
)
mock_extract_factory(
"keboola",
[{"name": "public_sales", "data": [{"id": "1", "amount": "100"}]}],
)
from src.orchestrator import SyncOrchestrator
SyncOrchestrator(analytics_db_path=env["analytics_db"]).rebuild()
resp = c.post(
"/api/query",
json={"sql": "SELECT * FROM public_sales"},
headers=_auth(seeded_app["analyst_token"]),
)
assert resp.status_code == 200
def test_admin_grants_permission_analyst_can_query(self, seeded_app, mock_extract_factory):
"""Admin grants explicit permission → analyst can query the table."""
c = seeded_app["client"]
t = seeded_app["admin_token"]
env = seeded_app["env"]
self._setup_private_table(seeded_app, mock_extract_factory)
# Grant permission
resp = c.post(
"/api/admin/permissions",
json={"user_id": "analyst1", "dataset": "private_data", "access": "read"},
headers=_auth(t),
)
assert resp.status_code == 201
# Verify permission recorded
resp = c.get("/api/admin/permissions/analyst1", headers=_auth(t))
assert resp.status_code == 200
datasets = [p["dataset"] for p in resp.json()["permissions"]]
assert "private_data" in datasets
def test_admin_revokes_permission(self, seeded_app, mock_extract_factory):
"""After granting then revoking, permission is removed from record."""
c = seeded_app["client"]
t = seeded_app["admin_token"]
env = seeded_app["env"]
self._setup_private_table(seeded_app, mock_extract_factory)
# Grant
c.post(
"/api/admin/permissions",
json={"user_id": "analyst1", "dataset": "private_data", "access": "read"},
headers=_auth(t),
)
# Revoke — use request() because TestClient.delete() doesn't accept a body
import json as _json
resp = c.request(
"DELETE",
"/api/admin/permissions",
data=_json.dumps({"user_id": "analyst1", "dataset": "private_data", "access": "read"}),
headers={**_auth(t), "Content-Type": "application/json"},
)
assert resp.status_code == 200
assert resp.json()["revoked"] is True
# Permission should be gone
resp = c.get("/api/admin/permissions/analyst1", headers=_auth(t))
datasets = [p["dataset"] for p in resp.json()["permissions"]]
assert "private_data" not in datasets
def test_access_request_flow(self, seeded_app, mock_extract_factory):
"""Analyst submits access request → admin approves → request is approved."""
c = seeded_app["client"]
t = seeded_app["admin_token"]
analyst_headers = _auth(seeded_app["analyst_token"])
self._setup_private_table(seeded_app, mock_extract_factory)
# Analyst creates access request
resp = c.post(
"/api/access-requests",
json={"table_id": "private_data", "reason": "I need this for analysis"},
headers=analyst_headers,
)
assert resp.status_code == 201
req_id = resp.json()["id"]
assert resp.json()["status"] == "pending"
# Admin sees pending request
resp = c.get("/api/access-requests/pending", headers=_auth(t))
assert resp.status_code == 200
pending_ids = [r["id"] for r in resp.json()["requests"]]
assert req_id in pending_ids
# Admin approves
resp = c.post(f"/api/access-requests/{req_id}/approve", headers=_auth(t))
assert resp.status_code == 200
assert resp.json()["status"] == "approved"
# Analyst's own requests show approved
resp = c.get("/api/access-requests/my", headers=analyst_headers)
assert resp.status_code == 200
statuses = {r["id"]: r["status"] for r in resp.json()["requests"]}
assert statuses.get(req_id) == "approved"
def test_duplicate_access_request_rejected(self, seeded_app, mock_extract_factory):
"""Submitting a duplicate pending request returns 409."""
c = seeded_app["client"]
analyst_headers = _auth(seeded_app["analyst_token"])
self._setup_private_table(seeded_app, mock_extract_factory)
# First request
resp = c.post(
"/api/access-requests",
json={"table_id": "private_data"},
headers=analyst_headers,
)
assert resp.status_code == 201
# Duplicate
resp = c.post(
"/api/access-requests",
json={"table_id": "private_data"},
headers=analyst_headers,
)
assert resp.status_code == 409