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,173 @@
|
||||
"""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"]
|
||||
Reference in New Issue
Block a user