Files
sakshamandClaude Sonnet 5 18305b545c Backfill sparse report sections, add enrichment section refresh, and bootstrap first-admin
Reports: the LLM reliably used company_enrichment for prose fields but
inconsistently populated the parallel Finding-list/string-list fields from
the same evidence, even with progressively more explicit prompting. Add a
code-level backfill (products, recent developments, financial signals,
strategic initiatives, regulatory signals, risks/opportunities mirrored
from SWOT, unknowns, monitoring recommendations) that only ever fills in
what the model left empty, never overwrites what it produced.

Enrichment tab: reorder sections (Products/Recent updates before
Customers/Competitors) and add a per-section "Refresh" button that
re-fetches just one of NinjaPear's six independent per-company endpoints
when it came back empty - confirmed live that a data-coverage gap (e.g.
Amazon returning no products) is real provider behavior, not a bug.

Auth: the first account registered on a deployment with zero existing
admins is now auto-promoted to admin, closing the chicken-and-egg gap
where the only path to admin access was direct DB access. Self-heals if
the last admin ever deletes their account.

Also bumps nginx's proxy_read_timeout for api.ciagent.org to cover the
enrichment refresh's synchronous funding-endpoint call (up to 5 minutes
per NinjaPear's docs).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-06 17:41:02 -04:00

289 lines
11 KiB
Python

"""Auth flow tests. Runs under AUTH_MODE=jwt (the suite default) except where
`local_mode_client` explicitly exercises the local-dev-user path."""
from __future__ import annotations
import uuid
from datetime import UTC, datetime, timedelta
from fastapi.testclient import TestClient
from app.core.config import get_settings
from app.main import app
from app.models.enums import SecurityEventType
from app.models.user import LOCAL_DEV_USER_ID
from app.repositories.user_security_event_repository import UserSecurityEventRepository
def _unique_email() -> str:
return f"user-{uuid.uuid4().hex[:12]}@example.com"
def test_register_then_login(client):
email = _unique_email()
register_resp = client.post(
"/api/v1/auth/register",
json={
"email": email,
"password": "correct-horse-1",
"display_name": "Test User",
},
)
assert register_resp.status_code == 201
body = register_resp.json()
assert body["email"] == email
assert "password" not in body
assert "password_hash" not in body
login_resp = client.post(
"/api/v1/auth/login", json={"email": email, "password": "correct-horse-1"}
)
assert login_resp.status_code == 200
tokens = login_resp.json()
assert tokens["access_token"]
assert tokens["refresh_token"]
assert tokens["token_type"] == "bearer"
def test_register_duplicate_email_conflicts(client):
email = _unique_email()
payload = {"email": email, "password": "correct-horse-1", "display_name": "Test User"}
first = client.post("/api/v1/auth/register", json=payload)
assert first.status_code == 201
second = client.post("/api/v1/auth/register", json=payload)
assert second.status_code == 409
def test_register_rejects_weak_password(client):
resp = client.post(
"/api/v1/auth/register",
json={"email": _unique_email(), "password": "allletters", "display_name": "Test User"},
)
assert resp.status_code == 422
async def test_first_registration_on_an_admin_less_deployment_becomes_admin(db_session, settings):
"""Bootstraps admin access on a fresh deployment - without this, the
only way to ever get an admin account is direct DB access. Patches
count_admins() to simulate a genuinely admin-less deployment rather
than manipulating the shared session-wide test DB's real admin count
(conftest.py seeds one admin precisely so ordinary registrations in
other tests never accidentally trip this path)."""
from unittest.mock import patch
from app.repositories.user_repository import UserRepository
from app.schemas.auth import RegisterRequest
from app.services.auth_service import register
payload = RegisterRequest(
email=_unique_email(), password="correct-horse-1", display_name="First User"
)
with patch.object(UserRepository, "count_admins", return_value=0):
user = await register(db_session, settings, "127.0.0.1", payload)
assert user.is_admin is True
async def test_registration_after_an_admin_already_exists_is_not_promoted(db_session, settings):
from app.schemas.auth import RegisterRequest
from app.services.auth_service import register
payload = RegisterRequest(
email=_unique_email(), password="correct-horse-1", display_name="Second User"
)
user = await register(db_session, settings, "127.0.0.1", payload)
assert user.is_admin is False
def test_login_wrong_password_rejected(client):
email = _unique_email()
client.post(
"/api/v1/auth/register",
json={"email": email, "password": "correct-horse-1", "display_name": "Test User"},
)
resp = client.post("/api/v1/auth/login", json={"email": email, "password": "wrong-password-1"})
assert resp.status_code == 401
def test_me_requires_bearer_token(client):
resp = client.get("/api/v1/auth/me")
assert resp.status_code == 401
def test_me_returns_current_user(client):
email = _unique_email()
client.post(
"/api/v1/auth/register",
json={"email": email, "password": "correct-horse-1", "display_name": "Test User"},
)
login = client.post(
"/api/v1/auth/login", json={"email": email, "password": "correct-horse-1"}
).json()
resp = client.get(
"/api/v1/auth/me", headers={"Authorization": f"Bearer {login['access_token']}"}
)
assert resp.status_code == 200
body = resp.json()
assert body["email"] == email
assert body["auth_mode"] == "jwt"
def test_refresh_rotates_and_invalidates_old_token(client):
email = _unique_email()
client.post(
"/api/v1/auth/register",
json={"email": email, "password": "correct-horse-1", "display_name": "Test User"},
)
login = client.post(
"/api/v1/auth/login", json={"email": email, "password": "correct-horse-1"}
).json()
refresh_resp = client.post(
"/api/v1/auth/refresh", json={"refresh_token": login["refresh_token"]}
)
assert refresh_resp.status_code == 200
new_tokens = refresh_resp.json()
assert new_tokens["refresh_token"] != login["refresh_token"]
# The old refresh token was rotated out and must not be reusable.
reuse_resp = client.post("/api/v1/auth/refresh", json={"refresh_token": login["refresh_token"]})
assert reuse_resp.status_code == 401
def test_logout_revokes_refresh_token(client):
email = _unique_email()
client.post(
"/api/v1/auth/register",
json={"email": email, "password": "correct-horse-1", "display_name": "Test User"},
)
login = client.post(
"/api/v1/auth/login", json={"email": email, "password": "correct-horse-1"}
).json()
logout_resp = client.post("/api/v1/auth/logout", json={"refresh_token": login["refresh_token"]})
assert logout_resp.status_code == 204
reuse_resp = client.post("/api/v1/auth/refresh", json={"refresh_token": login["refresh_token"]})
assert reuse_resp.status_code == 401
def test_local_mode_me_returns_fixed_dev_user_without_token(local_mode_client):
resp = local_mode_client.get("/api/v1/auth/me")
assert resp.status_code == 200
body = resp.json()
assert body["email"] == "[email protected]"
assert body["auth_mode"] == "local"
def test_register_login_available_even_in_local_mode(local_mode_client):
"""Registering/logging in a real account must always be possible,
regardless of AUTH_MODE - the loopback convenience only affects whether
a request can skip auth entirely, not whether real accounts exist."""
email = _unique_email()
register_resp = local_mode_client.post(
"/api/v1/auth/register",
json={"email": email, "password": "correct-horse-1", "display_name": "X"},
)
assert register_resp.status_code == 201
login_resp = local_mode_client.post(
"/api/v1/auth/login", json={"email": email, "password": "correct-horse-1"}
)
assert login_resp.status_code == 200
assert login_resp.json()["access_token"]
def test_security_events_requires_auth(client):
resp = client.get("/api/v1/auth/security-events")
assert resp.status_code == 401
def _login_success_count(events: list[dict]) -> int:
return sum(1 for e in events if e["event_type"] == "login_success")
def test_local_dev_sign_in_is_logged_to_account_activity(local_mode_client):
"""The local-dev bypass has no real login step - hitting any
authenticated endpoint (here /auth/me, the same check the frontend
performs on app load) must still show up as a sign-in."""
local_mode_client.get("/api/v1/auth/me")
events = local_mode_client.get("/api/v1/auth/security-events").json()
assert any(e["event_type"] == "login_success" for e in events)
def test_local_dev_sign_in_is_not_logged_twice_within_cooldown(local_mode_client):
"""Every authenticated request runs get_or_create_local_user - without
a cooldown, browsing the app at all would flood Account activity with
one login_success per request."""
local_mode_client.get("/api/v1/auth/me")
before = _login_success_count(local_mode_client.get("/api/v1/auth/security-events").json())
local_mode_client.get("/api/v1/auth/me")
after = _login_success_count(local_mode_client.get("/api/v1/auth/security-events").json())
assert after == before
async def test_local_dev_sign_in_logs_again_after_cooldown_expires(local_mode_client, db_session):
local_mode_client.get("/api/v1/auth/me")
before = _login_success_count(local_mode_client.get("/api/v1/auth/security-events").json())
event_repo = UserSecurityEventRepository(db_session)
last_login = await event_repo.most_recent_of_type(
LOCAL_DEV_USER_ID, SecurityEventType.LOGIN_SUCCESS
)
assert last_login is not None
last_login.created_at = datetime.now(UTC) - timedelta(minutes=31)
await db_session.commit()
local_mode_client.get("/api/v1/auth/me")
after = _login_success_count(local_mode_client.get("/api/v1/auth/security-events").json())
assert after == before + 1
def test_security_events_returns_own_login_events_only(client):
email_a = _unique_email()
client.post(
"/api/v1/auth/register",
json={"email": email_a, "password": "correct-horse-1", "display_name": "A"},
)
login_a = client.post(
"/api/v1/auth/login", json={"email": email_a, "password": "correct-horse-1"}
).json()
email_b = _unique_email()
client.post(
"/api/v1/auth/register",
json={"email": email_b, "password": "correct-horse-1", "display_name": "B"},
)
client.post("/api/v1/auth/login", json={"email": email_b, "password": "correct-horse-1"})
resp = client.get(
"/api/v1/auth/security-events",
headers={"Authorization": f"Bearer {login_a['access_token']}"},
)
assert resp.status_code == 200
events = resp.json()
event_types = {e["event_type"] for e in events}
# registration writes email_verification_sent, login writes login_success -
# both belong to user A only, never user B's events.
assert event_types == {"email_verification_sent", "login_success"}
def test_local_mode_setting_alone_does_not_bypass_auth_for_non_loopback_callers():
"""AUTH_MODE=local is not a blanket switch - a request that isn't
actually from loopback (e.g. a LAN/WAN caller, or here Starlette's
TestClient default fake peer) still needs a real bearer token."""
settings = get_settings().model_copy(update={"auth_mode": "local"})
app.dependency_overrides[get_settings] = lambda: settings
try:
with TestClient(app) as non_loopback_client: # default peer: ("testclient", 50000)
resp = non_loopback_client.get("/api/v1/auth/me")
assert resp.status_code == 401
finally:
app.dependency_overrides.pop(get_settings, None)