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,126 @@
|
||||
"""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 = (
|
||||
'<?xml version="1.0"?><rss version="2.0"><channel><title>News</title>'
|
||||
"<item><title>Update</title><link>https://example.com/n</link>"
|
||||
"<description>Something happened.</description></item></channel></rss>"
|
||||
)
|
||||
|
||||
|
||||
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="<html><body>A</body></html>")
|
||||
)
|
||||
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="<html><body>B</body></html>")
|
||||
)
|
||||
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
|
||||
Reference in New Issue
Block a user