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:
@@ -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
|
||||
Reference in New Issue
Block a user