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).
139 lines
4.7 KiB
Python
139 lines
4.7 KiB
Python
"""Celery Beat's sync_schedules task: dynamic due-schedule discovery,
|
|
idempotent skip of companies already mid-run, and delegation to
|
|
run_monitoring.delay for everything else."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import UTC, datetime, timedelta
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from sqlalchemy import select
|
|
|
|
from app.models.company import Company
|
|
from app.models.enums import MonitoringFrequency, MonitoringRunTrigger, SourceType
|
|
from app.models.monitor_configuration import MonitorConfiguration
|
|
from app.models.monitoring_run import MonitoringRun
|
|
from app.models.source import Source
|
|
from app.repositories.monitoring_run_repository import MonitoringRunRepository
|
|
from app.tasks.scheduler import _sync_schedules_async
|
|
|
|
|
|
async def _make_due_company(db_session, *, next_run_offset_minutes: int, enabled: bool = True):
|
|
company = Company(
|
|
id=uuid.uuid4(),
|
|
user_id=uuid.uuid4(),
|
|
name="Scheduled Co",
|
|
slug=f"scheduled-co-{uuid.uuid4().hex[:6]}",
|
|
)
|
|
db_session.add(company)
|
|
db_session.add(
|
|
MonitorConfiguration(
|
|
company_id=company.id,
|
|
frequency_type=MonitoringFrequency.WEEKLY,
|
|
enabled=enabled,
|
|
next_run=datetime.now(UTC) + timedelta(minutes=next_run_offset_minutes),
|
|
)
|
|
)
|
|
await db_session.commit()
|
|
return company
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sync_schedules_enqueues_due_companies(db_session):
|
|
company = await _make_due_company(db_session, next_run_offset_minutes=-5) # 5 min overdue
|
|
|
|
with patch("app.tasks.collection.run_monitoring.delay") as mock_delay:
|
|
await _sync_schedules_async()
|
|
|
|
mock_delay.assert_called_once()
|
|
|
|
result = await db_session.execute(
|
|
select(MonitoringRun).where(MonitoringRun.company_id == company.id)
|
|
)
|
|
run = result.scalar_one()
|
|
assert run.trigger_type == MonitoringRunTrigger.SCHEDULED
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sync_schedules_skips_not_yet_due_companies(db_session):
|
|
await _make_due_company(db_session, next_run_offset_minutes=60) # due in the future
|
|
|
|
with patch("app.tasks.collection.run_monitoring.delay") as mock_delay:
|
|
await _sync_schedules_async()
|
|
|
|
mock_delay.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sync_schedules_skips_disabled_configs(db_session):
|
|
await _make_due_company(db_session, next_run_offset_minutes=-5, enabled=False)
|
|
|
|
with patch("app.tasks.collection.run_monitoring.delay") as mock_delay:
|
|
await _sync_schedules_async()
|
|
|
|
mock_delay.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sync_schedules_does_not_double_enqueue_a_company_with_an_active_run(db_session):
|
|
company = await _make_due_company(db_session, next_run_offset_minutes=-5)
|
|
|
|
run_repo = MonitoringRunRepository(db_session)
|
|
await run_repo.create(company_id=company.id, trigger_type=MonitoringRunTrigger.MANUAL)
|
|
await db_session.commit()
|
|
|
|
with patch("app.tasks.collection.run_monitoring.delay") as mock_delay:
|
|
await _sync_schedules_async()
|
|
|
|
mock_delay.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sync_schedules_enqueues_via_a_source_override_even_when_company_default_isnt_due(
|
|
db_session,
|
|
):
|
|
"""A source with its own faster cadence (e.g. "check news daily") must
|
|
be able to trigger a run even while the company's own default schedule
|
|
(e.g. weekly) isn't due yet - this is the whole point of per-source
|
|
scheduling."""
|
|
company = await _make_due_company(db_session, next_run_offset_minutes=60) # not due
|
|
db_session.add(
|
|
Source(
|
|
company_id=company.id,
|
|
source_type=SourceType.RSS,
|
|
name="Daily News",
|
|
active=True,
|
|
frequency_type=MonitoringFrequency.DAILY,
|
|
next_check=datetime.now(UTC) - timedelta(minutes=5), # overdue
|
|
)
|
|
)
|
|
await db_session.commit()
|
|
|
|
with patch("app.tasks.collection.run_monitoring.delay") as mock_delay:
|
|
await _sync_schedules_async()
|
|
|
|
mock_delay.assert_called_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sync_schedules_skips_when_neither_company_default_nor_any_source_is_due(db_session):
|
|
company = await _make_due_company(db_session, next_run_offset_minutes=60) # not due
|
|
db_session.add(
|
|
Source(
|
|
company_id=company.id,
|
|
source_type=SourceType.RSS,
|
|
name="Daily News",
|
|
active=True,
|
|
frequency_type=MonitoringFrequency.DAILY,
|
|
next_check=datetime.now(UTC) + timedelta(hours=12), # not due yet
|
|
)
|
|
)
|
|
await db_session.commit()
|
|
|
|
with patch("app.tasks.collection.run_monitoring.delay") as mock_delay:
|
|
await _sync_schedules_async()
|
|
|
|
mock_delay.assert_not_called()
|