"""run_monitoring's per-source due-ness filter: a SCHEDULED run only collects sources that are actually due (an overdue fast-cadence source alongside a not-yet-due default-cadence one), while a MANUAL "Run now" always collects every active source regardless of individual cadence.""" from __future__ import annotations import uuid from datetime import UTC, datetime, timedelta from unittest.mock import patch import httpx import pytest import respx from app.models.company import Company from app.models.enums import MonitoringFrequency, MonitoringRunTrigger, SourceType from app.models.monitor_configuration import MonitorConfiguration from app.models.source import Source from app.repositories.monitoring_run_repository import MonitoringRunRepository from app.tasks.scheduler import _sync_schedules_async RSS_FEED = ( 'News' "Updatehttps://example.com/n" "Something happened." ) 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']}"} async def _make_company_with_two_sources(db_session): company = Company( id=uuid.uuid4(), user_id=uuid.uuid4(), name="Scheduling Co", slug=f"scheduling-co-{uuid.uuid4().hex[:6]}", ) db_session.add(company) db_session.add( MonitorConfiguration( company_id=company.id, frequency_type=MonitoringFrequency.WEEKLY, enabled=True, next_run=datetime.now(UTC) + timedelta(hours=1), # not due ) ) due_source = Source( company_id=company.id, source_type=SourceType.RSS, name="Daily News", base_url="https://example.com/feed", active=True, frequency_type=MonitoringFrequency.DAILY, next_check=datetime.now(UTC) - timedelta(minutes=5), # overdue ) not_due_source = Source( company_id=company.id, source_type=SourceType.RSS, name="Default Cadence Feed", base_url="https://example.com/other-feed", active=True, # frequency_type left None - inherits the company's weekly default, # which per MonitorConfiguration.next_run above isn't due yet. ) db_session.add_all([due_source, not_due_source]) await db_session.commit() return company @pytest.mark.asyncio async def test_scheduled_run_only_collects_the_due_source(db_session): company = await _make_company_with_two_sources(db_session) with respx.mock: respx.get("https://example.com/feed").mock(return_value=httpx.Response(200, text=RSS_FEED)) await _sync_schedules_async() runs = await MonitoringRunRepository(db_session).list_for_company(company.id) assert len(runs) == 1 run = runs[0] assert run.trigger_type == MonitoringRunTrigger.SCHEDULED assert run.sources_attempted == 1 assert run.sources_successful == 1 def test_manual_run_collects_every_active_source_regardless_of_cadence(client): headers = _register_and_login(client) company = client.post( "/api/v1/companies", json={"name": f"Manual Sched Co {uuid.uuid4().hex[:6]}", "frequency_type": "weekly"}, headers=headers, ).json() for name, url in [("A", "https://example.com/a"), ("B", "https://example.com/b")]: client.post( f"/api/v1/companies/{company['id']}/sources", json={"source_type": "custom_url", "name": name, "base_url": url}, headers=headers, ) with patch("socket.getaddrinfo", return_value=[(2, 1, 6, "", ("93.184.216.34", 0))]): with respx.mock: respx.get("https://example.com/a/robots.txt").mock(return_value=httpx.Response(404)) respx.get("https://example.com/a").mock( return_value=httpx.Response(200, html="A") ) respx.get("https://example.com/b/robots.txt").mock(return_value=httpx.Response(404)) respx.get("https://example.com/b").mock( return_value=httpx.Response(200, html="B") ) run = client.post(f"/api/v1/companies/{company['id']}/run", headers=headers).json() detail = client.get(f"/api/v1/runs/{run['id']}", headers=headers).json() assert detail["sources_attempted"] == 2 assert detail["sources_successful"] == 2