Consolidates the scattered per-analyst pages into /me/activity (usage analytics) and /me/profile (account hub). /me/stats and /profile/sessions 301-redirect; /profile, /me/debug, /tokens are removed with every internal link repointed. Includes an XSS fix in the /me/activity page hero, the user_id-keyed session-lookup alignment, and the v0.54.15 release cut. Co-developed by @ZdenekSrotyr and @cvrysanek.
150 lines
5.5 KiB
Python
150 lines
5.5 KiB
Python
"""Tests for the /me/profile/refetch-groups POST endpoint.
|
|
|
|
The GET /me/debug page no longer exists — its content was folded into
|
|
/me/profile as a collapsible "Session & troubleshooting" section.
|
|
|
|
The refetch-groups POST:
|
|
- Must be 404 (not 403) when ``AGNES_DEBUG_AUTH`` is unset / falsy.
|
|
- Must return the documented diff shape and perform zero database writes.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import uuid
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def fresh_db(monkeypatch):
|
|
"""Per-test DATA_DIR + JWT secret so the system DB is fresh."""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
monkeypatch.setenv("DATA_DIR", tmp)
|
|
monkeypatch.setenv("TESTING", "1")
|
|
monkeypatch.setenv("JWT_SECRET_KEY", "test-jwt-secret-key-minimum-32-chars!!")
|
|
yield tmp
|
|
|
|
|
|
def _make_user_and_session(conn, email: str = "u@example.com"):
|
|
"""Create a non-admin user, return (user_id, session_jwt)."""
|
|
from src.repositories.users import UserRepository
|
|
from app.auth.jwt import create_access_token
|
|
|
|
uid = str(uuid.uuid4())
|
|
UserRepository(conn).create(
|
|
id=uid, email=email, name=email.split("@")[0]
|
|
)
|
|
token = create_access_token(user_id=uid, email=email)
|
|
return uid, token
|
|
|
|
|
|
def _client():
|
|
from fastapi.testclient import TestClient
|
|
from app.main import app
|
|
return TestClient(app)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Refetch endpoint — dry-run, zero DB writes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestRefetchDryRun:
|
|
def test_404_when_flag_off(self, fresh_db, monkeypatch):
|
|
monkeypatch.delenv("AGNES_DEBUG_AUTH", raising=False)
|
|
from src.db import get_system_db, close_system_db
|
|
conn = get_system_db()
|
|
try:
|
|
_, sess = _make_user_and_session(conn)
|
|
finally:
|
|
conn.close()
|
|
close_system_db()
|
|
|
|
c = _client()
|
|
resp = c.post("/me/profile/refetch-groups", cookies={"access_token": sess})
|
|
assert resp.status_code == 404
|
|
|
|
def test_returns_diff_shape_and_does_not_write(self, fresh_db, monkeypatch):
|
|
"""Mocked Google response, refetch must return the documented shape
|
|
AND not change any user_group_members rows."""
|
|
monkeypatch.setenv("AGNES_DEBUG_AUTH", "true")
|
|
# Mock fetch to return a deterministic list (no real Google call).
|
|
monkeypatch.setenv(
|
|
"GOOGLE_ADMIN_SDK_MOCK_GROUPS",
|
|
"grp_admin@example.com,grp_finance@example.com",
|
|
)
|
|
|
|
from src.db import get_system_db, close_system_db
|
|
conn = get_system_db()
|
|
try:
|
|
uid, sess = _make_user_and_session(conn, email="m@example.com")
|
|
before_rows = conn.execute(
|
|
"SELECT user_id, group_id, source FROM user_group_members "
|
|
"WHERE user_id = ?", [uid],
|
|
).fetchall()
|
|
finally:
|
|
conn.close()
|
|
close_system_db()
|
|
|
|
c = _client()
|
|
resp = c.post("/me/profile/refetch-groups", cookies={"access_token": sess})
|
|
assert resp.status_code == 200, resp.text
|
|
data = resp.json()
|
|
|
|
# Documented shape — keys present, types right.
|
|
for key in (
|
|
"soft_failed", "prefix", "fetched", "fetched_relevant",
|
|
"current_names", "current_external_ids",
|
|
"would_add", "would_remove", "applied",
|
|
):
|
|
assert key in data, f"missing key {key!r}"
|
|
assert data["applied"] is False
|
|
assert data["soft_failed"] is False
|
|
assert isinstance(data["fetched"], list)
|
|
assert isinstance(data["would_add"], list)
|
|
|
|
# Zero DB writes — snapshot before/after must match exactly.
|
|
conn = get_system_db()
|
|
try:
|
|
after_rows = conn.execute(
|
|
"SELECT user_id, group_id, source FROM user_group_members "
|
|
"WHERE user_id = ?", [uid],
|
|
).fetchall()
|
|
finally:
|
|
conn.close()
|
|
close_system_db()
|
|
assert before_rows == after_rows
|
|
|
|
def test_soft_fail_marker_when_mock_unset_and_real_path_unconfigured(
|
|
self, fresh_db, monkeypatch
|
|
):
|
|
"""Without the mock env and without GOOGLE_ADMIN_SDK_SUBJECT, the
|
|
real path returns soft-fail; the endpoint reports it as such."""
|
|
monkeypatch.setenv("AGNES_DEBUG_AUTH", "true")
|
|
monkeypatch.delenv("GOOGLE_ADMIN_SDK_MOCK_GROUPS", raising=False)
|
|
monkeypatch.delenv("GOOGLE_ADMIN_SDK_SUBJECT", raising=False)
|
|
|
|
from src.db import get_system_db, close_system_db
|
|
conn = get_system_db()
|
|
try:
|
|
_, sess = _make_user_and_session(conn, email="sf@example.com")
|
|
finally:
|
|
conn.close()
|
|
close_system_db()
|
|
|
|
c = _client()
|
|
resp = c.post("/me/profile/refetch-groups", cookies={"access_token": sess})
|
|
assert resp.status_code == 200, resp.text
|
|
data = resp.json()
|
|
# On the keyless-DWD branch, fetch_user_groups returns [] on missing
|
|
# subject (legacy fail-soft as empty list); on the prefix-mapping
|
|
# branch it returns None. Tolerate either — endpoint reports
|
|
# soft_failed=True when None, False+empty list when [].
|
|
if data["soft_failed"]:
|
|
assert data["fetched"] == []
|
|
else:
|
|
# Real path returned [] — also a valid shape; assert no writes
|
|
# happened by virtue of applied=False + DB snapshot below.
|
|
assert data["fetched"] == []
|
|
assert data["applied"] is False
|