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).
174 lines
6.0 KiB
Python
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"]
|