"""Monitoring run endpoints, exercised via HTTP with CELERY_TASK_ALWAYS_EAGER so `.delay()` runs the task synchronously in-process (see app/tasks/base.py for why that needs a threaded asyncio bridge to work from an async route handler). No live network: every collector's discovery/collection call is respx-mocked.""" from __future__ import annotations import json import uuid from unittest.mock import patch import httpx import pytest 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 _create_company(client, headers, **overrides): payload = { "name": f"Run Test Co {uuid.uuid4().hex[:6]}", "frequency_type": "weekly", **overrides, } return client.post("/api/v1/companies", json=payload, headers=headers).json() def _mock_empty_discovery(): """Every discoverable collector type returns nothing (or a harmless stub), so a first run doesn't need per-source mocks for whatever discovery happens to find. 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, unlike GitHub/SEC which discover.() itself.""" 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='' ) ) 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_run_now_executes_and_reports_final_status(client): headers = _register_and_login(client) company = _create_company(client, headers) with patch("socket.getaddrinfo", return_value=[(2, 1, 6, "", ("93.184.216.34", 0))]): with respx.mock: _mock_empty_discovery() resp = client.post(f"/api/v1/companies/{company['id']}/run", headers=headers) assert resp.status_code == 202 run = resp.json() assert run["trigger_type"] == "manual" # Eager mode means the task already finished by the time .delay() returns. detail = client.get(f"/api/v1/runs/{run['id']}", headers=headers).json() assert detail["status"] == "successful" assert detail["completed_at"] is not None async def test_run_now_is_idempotent_for_an_active_run(db_session, settings): """A run-now call while one is already queued/running returns that same run instead of enqueuing a duplicate. Exercised at the service layer directly: under CELERY_TASK_ALWAYS_EAGER a `.delay()` call runs the task to completion before returning, so an HTTP-level test can never observe an in-flight run to dedupe against.""" from app.models.company import Company from app.models.enums import MonitoringRunTrigger from app.models.monitor_configuration import MonitorConfiguration from app.repositories.monitoring_run_repository import MonitoringRunRepository from app.services import monitoring_service user_id = uuid.uuid4() company = Company( id=uuid.uuid4(), user_id=user_id, name="Idempotency Co", slug=f"idempotency-co-{uuid.uuid4().hex[:6]}", ) db_session.add(company) db_session.add(MonitorConfiguration(company_id=company.id)) await db_session.commit() run_repo = MonitoringRunRepository(db_session) existing = await run_repo.create( company_id=company.id, trigger_type=MonitoringRunTrigger.SCHEDULED ) await db_session.commit() with patch("app.tasks.collection.run_monitoring.delay") as mock_delay: result = await monitoring_service.enqueue_run_now(db_session, settings, user_id, company.id) assert result.id == existing.id mock_delay.assert_not_called() async def test_run_now_enforces_daily_manual_run_cap(db_session, settings): from app.core.errors import RateLimitedError from app.models.company import Company from app.models.enums import MonitoringRunStatus, MonitoringRunTrigger from app.models.monitor_configuration import MonitorConfiguration from app.repositories.monitoring_run_repository import MonitoringRunRepository from app.services import monitoring_service user_id = uuid.uuid4() company = Company( id=uuid.uuid4(), user_id=user_id, name="Rate Limited Co", slug=f"rate-limited-co-{uuid.uuid4().hex[:6]}", ) db_session.add(company) db_session.add(MonitorConfiguration(company_id=company.id)) await db_session.commit() run_repo = MonitoringRunRepository(db_session) for _ in range(2): run = await run_repo.create(company_id=company.id, trigger_type=MonitoringRunTrigger.MANUAL) await run_repo.mark_finished(run, status=MonitoringRunStatus.SUCCESSFUL, error_summary=None) await db_session.commit() capped_settings = settings.model_copy(update={"max_manual_runs_per_day": 2}) with pytest.raises(RateLimitedError): await monitoring_service.enqueue_run_now(db_session, capped_settings, user_id, company.id) def test_run_history_and_single_run_scoped_to_owner(client): owner_headers = _register_and_login(client) other_headers = _register_and_login(client) company = _create_company(client, owner_headers) with patch("socket.getaddrinfo", return_value=[(2, 1, 6, "", ("93.184.216.34", 0))]): with respx.mock: _mock_empty_discovery() run = client.post( f"/api/v1/companies/{company['id']}/run", headers=owner_headers ).json() assert ( client.get(f"/api/v1/companies/{company['id']}/runs", headers=other_headers).status_code == 404 ) assert client.get(f"/api/v1/runs/{run['id']}", headers=other_headers).status_code == 404 assert client.get(f"/api/v1/runs/{run['id']}", headers=owner_headers).status_code == 200 def test_successful_first_run_generates_a_baseline_report(client): headers = _register_and_login(client) company = _create_company(client, headers) client.post( f"/api/v1/companies/{company['id']}/sources", json={ "source_type": "custom_url", "name": "Pricing", "base_url": "https://example.com/pricing", }, headers=headers, ) 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="Pricing" "

Pricing

Plans start at $10/month.

" "", ) ) client.post(f"/api/v1/companies/{company['id']}/run", headers=headers) reports = client.get(f"/api/v1/companies/{company['id']}/reports", headers=headers).json() assert len(reports) == 1 assert reports[0]["report_type"] == "baseline" assert reports[0]["model_provider"] == "mock" def test_manual_run_does_not_disrupt_next_scheduled_run(client): headers = _register_and_login(client) company = _create_company(client, headers) original_next_run = company["monitor_configuration"]["next_run"] with patch("socket.getaddrinfo", return_value=[(2, 1, 6, "", ("93.184.216.34", 0))]): with respx.mock: _mock_empty_discovery() client.post(f"/api/v1/companies/{company['id']}/run", headers=headers) refreshed = client.get(f"/api/v1/companies/{company['id']}", headers=headers).json() assert refreshed["monitor_configuration"]["next_run"] == original_next_run assert refreshed["monitor_configuration"]["last_run"] is not None