Files
CIAgent/apps/api/tests/unit/test_alerting_e2e.py
T
saksham 1a4c80958f 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).
2026-08-05 10:48:20 -04:00

174 lines
6.0 KiB
Python

"""End-to-end proof that a real monitoring run wires all the way through to
a delivered notification: baseline run -> content changes -> second run's
DetectedChange crosses the company's (LOW) severity threshold -> Alert
created -> dispatched to the registered email destination -> smtplib
(mocked) actually gets called. Everything else is the real HTTP API +
Celery-eager pipeline, exactly as a user would trigger it from the
dashboard; only DNS and the outbound SMTP socket are mocked."""
from __future__ import annotations
import json
import uuid
from unittest.mock import patch
import httpx
import respx
def _register_and_login(client) -> dict[str, str]:
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()
return {"Authorization": f"Bearer {tokens['access_token']}"}
def _mock_empty_discovery():
respx.get("https://api.github.com/search/users").mock(
return_value=httpx.Response(200, text=json.dumps({"items": []}))
)
respx.get("https://www.sec.gov/cgi-bin/browse-edgar").mock(
return_value=httpx.Response(
200, text='<?xml version="1.0"?><feed xmlns="http://www.w3.org/2005/Atom"></feed>'
)
)
# RSS (Google News) and GOV_CONTRACT (USASpending) are always discovered
# unconditionally - see discovery_service.py's _PREVIEWABLE_TYPES - so
# their collect() calls need mocking here too.
respx.get(url__regex=r"https://news\.google\.com/rss/search.*").mock(
return_value=httpx.Response(
200,
text=(
'<?xml version="1.0"?><rss version="2.0"><channel><title>Google News</title>'
"<item><title>Test</title><link>https://example.com/news</link>"
"<description>Test news item</description></item></channel></rss>"
),
)
)
respx.post("https://api.usaspending.gov/api/v2/search/spending_by_award/").mock(
return_value=httpx.Response(200, json={"results": []})
)
def test_monitoring_run_detecting_a_change_produces_alert_and_email(client, monkeypatch):
sent_emails = []
class FakeSmtp:
def __init__(self, host, port, timeout=10):
pass
def __enter__(self):
return self
def __exit__(self, *exc):
return False
def starttls(self):
pass
def login(self, username, password):
pass
def sendmail(self, from_addr, to_addrs, message):
sent_emails.append({"to": to_addrs, "message": message})
monkeypatch.setattr("app.notifications.smtp_email.smtplib.SMTP", FakeSmtp)
headers = _register_and_login(client)
company = client.post(
"/api/v1/companies",
json={
"name": f"Acme Mobility {uuid.uuid4().hex[:6]}",
"frequency_type": "weekly",
"severity_threshold": "low",
},
headers=headers,
).json()
client.post(
f"/api/v1/companies/{company['id']}/sources",
json={
"source_type": "custom_url",
"name": "Pricing",
"base_url": "https://example.com/pricing",
},
headers=headers,
)
client.post(
"/api/v1/notification-destinations",
json={
"type": "email",
"destination_value": "[email protected]",
"minimum_severity": "low",
"company_ids": [company["id"]],
},
headers=headers,
)
baseline_html = (
"<html><head><title>Pricing</title></head><body>"
"<article><h1>Pricing</h1><p>The base plan is $49/month.</p></article>"
"</body></html>"
)
changed_html = (
"<html><head><title>Pricing</title></head><body>"
"<article><h1>Pricing</h1><p>The base plan is $99/month.</p></article>"
"</body></html>"
)
with patch("socket.getaddrinfo", return_value=[(2, 1, 6, "", ("93.184.216.34", 0))]):
with respx.mock:
_mock_empty_discovery()
respx.get("https://example.com/robots.txt").mock(return_value=httpx.Response(404))
respx.get("https://example.com/pricing").mock(
return_value=httpx.Response(200, html=baseline_html)
)
baseline_run = client.post(
f"/api/v1/companies/{company['id']}/run", headers=headers
).json()
with respx.mock:
_mock_empty_discovery()
respx.get("https://example.com/robots.txt").mock(return_value=httpx.Response(404))
respx.get("https://example.com/pricing").mock(
return_value=httpx.Response(200, html=changed_html)
)
second_run = client.post(
f"/api/v1/companies/{company['id']}/run", headers=headers
).json()
assert (
client.get(f"/api/v1/runs/{baseline_run['id']}", headers=headers).json()["status"]
== "successful"
)
assert (
client.get(f"/api/v1/runs/{second_run['id']}", headers=headers).json()["status"]
== "successful"
)
alerts = client.get(
"/api/v1/alerts", headers=headers, params={"company_id": company["id"]}
).json()
assert len(alerts) == 1
alert = alerts[0]
assert alert["severity"] in {"low", "medium", "high", "critical"}
detail = client.get(f"/api/v1/alerts/{alert['id']}", headers=headers).json()
assert len(detail["deliveries"]) == 1
assert detail["deliveries"][0]["status"] == "sent"
# Registration now also sends a verification-code email through this
# same SMTP path (no RESEND_API_KEY configured in tests, so
# security_email_service falls back to SmtpEmailProvider) - filter to
# the alert email specifically rather than assuming it's the only one.
alert_emails = [e for e in sent_emails if e["to"] == ["[email protected]"]]
assert len(alert_emails) == 1
assert alert["title"] in alert_emails[0]["message"]