Initial commit: CI Agent competitive-intelligence monitoring app

FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring,
source collection, LLM-based change analysis, enrichment, and account
security (Turnstile, escalating lockout, email verification).
This commit is contained in:
2026-08-05 10:48:20 -04:00
commit 1a4c80958f
365 changed files with 43541 additions and 0 deletions
+200
View File
@@ -0,0 +1,200 @@
"""Alerts API: ownership isolation, filters, and read/resolve mutations.
Alerts are only ever created internally by alert_service (never via a user
POST), so tests seed rows directly through db_session against the same
user_id the HTTP client is authenticated as (looked up via GET /auth/me)."""
from __future__ import annotations
import uuid
import pytest
from app.models.alert import Alert
from app.models.company import Company
from app.models.detected_change import DetectedChange
from app.models.enums import ChangeType, MonitoringRunTrigger, SeverityLevel, SourceType
from app.models.monitoring_run import MonitoringRun
from app.models.snapshot import Snapshot
from app.models.source import Source
def _register_and_login(client) -> tuple[dict[str, str], uuid.UUID]:
email = f"user-{uuid.uuid4().hex[:12]}@example.com"
client.post(
"/api/v1/auth/register",
json={"email": email, "password": "correct-horse-1", "display_name": "Test User"},
)
tokens = client.post(
"/api/v1/auth/login", json={"email": email, "password": "correct-horse-1"}
).json()
headers = {"Authorization": f"Bearer {tokens['access_token']}"}
user_id = uuid.UUID(client.get("/api/v1/auth/me", headers=headers).json()["id"])
return headers, user_id
async def _make_alert(
db_session,
user_id: uuid.UUID,
*,
severity: SeverityLevel = SeverityLevel.HIGH,
read: bool = False,
resolved: bool = False,
) -> Alert:
company = Company(
id=uuid.uuid4(), user_id=user_id, name="Acme Mobility", slug=f"acme-{uuid.uuid4().hex[:6]}"
)
db_session.add(company)
await db_session.flush()
source = Source(
company_id=company.id,
source_type=SourceType.WEBSITE,
name="Website",
base_url="https://acme.example",
)
db_session.add(source)
await db_session.flush()
run = MonitoringRun(company_id=company.id, trigger_type=MonitoringRunTrigger.MANUAL)
db_session.add(run)
await db_session.flush()
snapshot = Snapshot(
company_id=company.id,
source_id=source.id,
snapshot_type=source.source_type.value,
hash="hash1",
structured_summary={},
text_summary="",
monitoring_run_id=run.id,
)
db_session.add(snapshot)
await db_session.flush()
change = DetectedChange(
company_id=company.id,
source_id=source.id,
monitoring_run_id=run.id,
current_snapshot_id=snapshot.id,
change_type=ChangeType.NEW_DOCUMENT,
raw_diff={},
significance_score=0.6,
confidence_score=0.75,
severity=severity,
summary="Change detected",
)
db_session.add(change)
await db_session.flush()
alert = Alert(
company_id=company.id,
detected_change_id=change.id,
user_id=user_id,
title="New hire announced",
summary="A new VP of Engineering was announced.",
why_it_matters="Signals a scaling push.",
severity=severity,
confidence=0.75,
read=read,
resolved=resolved,
)
db_session.add(alert)
await db_session.commit()
await db_session.refresh(alert)
return alert
@pytest.mark.asyncio
async def test_list_alerts_scoped_to_owner(client, db_session):
owner_headers, owner_id = _register_and_login(client)
_other_headers, other_id = _register_and_login(client)
await _make_alert(db_session, owner_id)
await _make_alert(db_session, other_id)
resp = client.get("/api/v1/alerts", headers=owner_headers)
assert resp.status_code == 200
body = resp.json()
assert len(body) == 1
@pytest.mark.asyncio
async def test_list_alerts_filters_by_severity(client, db_session):
headers, user_id = _register_and_login(client)
await _make_alert(db_session, user_id, severity=SeverityLevel.CRITICAL)
await _make_alert(db_session, user_id, severity=SeverityLevel.LOW)
resp = client.get("/api/v1/alerts", headers=headers, params={"severity": "critical"})
assert resp.status_code == 200
body = resp.json()
assert len(body) == 1
assert body[0]["severity"] == "critical"
@pytest.mark.asyncio
async def test_list_alerts_filters_by_read_and_resolved(client, db_session):
headers, user_id = _register_and_login(client)
await _make_alert(db_session, user_id, read=True, resolved=False)
await _make_alert(db_session, user_id, read=False, resolved=False)
resp = client.get("/api/v1/alerts", headers=headers, params={"read": "false"})
assert resp.status_code == 200
body = resp.json()
assert len(body) == 1
assert body[0]["read"] is False
@pytest.mark.asyncio
async def test_get_alert_detail_includes_deliveries(client, db_session):
headers, user_id = _register_and_login(client)
alert = await _make_alert(db_session, user_id)
resp = client.get(f"/api/v1/alerts/{alert.id}", headers=headers)
assert resp.status_code == 200
body = resp.json()
assert body["id"] == str(alert.id)
assert body["deliveries"] == []
@pytest.mark.asyncio
async def test_get_alert_not_owned_returns_404(client, db_session):
_owner_headers, owner_id = _register_and_login(client)
other_headers, _other_id = _register_and_login(client)
alert = await _make_alert(db_session, owner_id)
resp = client.get(f"/api/v1/alerts/{alert.id}", headers=other_headers)
assert resp.status_code == 404
@pytest.mark.asyncio
async def test_mark_alert_read(client, db_session):
headers, user_id = _register_and_login(client)
alert = await _make_alert(db_session, user_id, read=False)
resp = client.post(f"/api/v1/alerts/{alert.id}/read", headers=headers)
assert resp.status_code == 200
assert resp.json()["read"] is True
@pytest.mark.asyncio
async def test_resolve_alert(client, db_session):
headers, user_id = _register_and_login(client)
alert = await _make_alert(db_session, user_id, resolved=False)
resp = client.post(f"/api/v1/alerts/{alert.id}/resolve", headers=headers)
assert resp.status_code == 200
assert resp.json()["resolved"] is True
@pytest.mark.asyncio
async def test_patch_alert_updates_both_flags(client, db_session):
headers, user_id = _register_and_login(client)
alert = await _make_alert(db_session, user_id)
resp = client.patch(
f"/api/v1/alerts/{alert.id}", json={"read": True, "resolved": True}, headers=headers
)
assert resp.status_code == 200
body = resp.json()
assert body["read"] is True
assert body["resolved"] is True