"""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='' ) ) # 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=( 'Google News' "Testhttps://example.com/news" "Test news item" ), ) ) 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": "watcher@example.com", "minimum_severity": "low", "company_ids": [company["id"]], }, headers=headers, ) baseline_html = ( "Pricing" "

Pricing

The base plan is $49/month.

" "" ) changed_html = ( "Pricing" "

Pricing

The base plan is $99/month.

" "" ) 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"] == ["watcher@example.com"]] assert len(alert_emails) == 1 assert alert["title"] in alert_emails[0]["message"]