132 lines
4.7 KiB
Python
132 lines
4.7 KiB
Python
"""Tests for scheduler polling logic."""
|
|
from datetime import datetime, timedelta
|
|
|
|
from services.scheduler.app import (
|
|
DEFAULT_CADENCES,
|
|
MAX_RETRY_COUNT,
|
|
build_job_payload,
|
|
compute_backoff,
|
|
get_cadence_for_source,
|
|
is_source_due,
|
|
)
|
|
|
|
|
|
class TestGetCadenceForSource:
|
|
def test_default_cadence_market_api(self):
|
|
assert get_cadence_for_source("market_api", None) == 60
|
|
|
|
def test_default_cadence_news_api(self):
|
|
assert get_cadence_for_source("news_api", {}) == 300
|
|
|
|
def test_default_cadence_unknown_type(self):
|
|
assert get_cadence_for_source("unknown", None) == 600
|
|
|
|
def test_override_from_config(self):
|
|
config = {"polling_interval_seconds": 120}
|
|
assert get_cadence_for_source("market_api", config) == 120
|
|
|
|
def test_override_minimum_clamp(self):
|
|
config = {"polling_interval_seconds": 5}
|
|
assert get_cadence_for_source("market_api", config) == 10
|
|
|
|
def test_invalid_override_falls_back(self):
|
|
config = {"polling_interval_seconds": "not_a_number"}
|
|
assert get_cadence_for_source("news_api", config) == DEFAULT_CADENCES["news_api"]
|
|
|
|
|
|
class TestComputeBackoff:
|
|
def test_first_retry(self):
|
|
assert compute_backoff(0) == 60
|
|
|
|
def test_second_retry(self):
|
|
assert compute_backoff(1) == 120
|
|
|
|
def test_capped_at_max(self):
|
|
assert compute_backoff(20) == 3600
|
|
|
|
|
|
class TestIsSourceDue:
|
|
def _now(self):
|
|
return datetime(2026, 4, 11, 12, 0, 0)
|
|
|
|
def test_never_run_is_due(self):
|
|
assert is_source_due("market_api", None, None, None, 0, None, self._now())
|
|
|
|
def test_completed_within_cadence_not_due(self):
|
|
last = self._now() - timedelta(seconds=30)
|
|
assert not is_source_due("market_api", None, last, "completed", 0, None, self._now())
|
|
|
|
def test_completed_past_cadence_is_due(self):
|
|
last = self._now() - timedelta(seconds=120)
|
|
assert is_source_due("market_api", None, last, "completed", 0, None, self._now())
|
|
|
|
def test_running_not_due(self):
|
|
last = self._now() - timedelta(seconds=5)
|
|
assert not is_source_due("market_api", None, last, "running", 0, None, self._now())
|
|
|
|
def test_failed_within_backoff_not_due(self):
|
|
last = self._now() - timedelta(seconds=30)
|
|
next_retry = self._now() + timedelta(seconds=30)
|
|
assert not is_source_due("market_api", None, last, "failed", 1, next_retry, self._now())
|
|
|
|
def test_failed_past_backoff_is_due(self):
|
|
last = self._now() - timedelta(seconds=120)
|
|
next_retry = self._now() - timedelta(seconds=10)
|
|
assert is_source_due("market_api", None, last, "failed", 1, next_retry, self._now())
|
|
|
|
def test_failed_max_retries_not_due(self):
|
|
last = self._now() - timedelta(seconds=120)
|
|
assert not is_source_due(
|
|
"market_api", None, last, "failed", MAX_RETRY_COUNT, None, self._now()
|
|
)
|
|
|
|
def test_custom_cadence_respected(self):
|
|
config = {"polling_interval_seconds": 600}
|
|
last = self._now() - timedelta(seconds=300)
|
|
assert not is_source_due("market_api", config, last, "completed", 0, None, self._now())
|
|
|
|
last_old = self._now() - timedelta(seconds=700)
|
|
assert is_source_due("market_api", config, last_old, "completed", 0, None, self._now())
|
|
|
|
|
|
class TestBuildJobPayload:
|
|
def test_payload_structure(self):
|
|
source = {
|
|
"source_id": "sid-1",
|
|
"company_id": "cid-1",
|
|
"ticker": "AAPL",
|
|
"legal_name": "Apple Inc.",
|
|
"source_type": "news_api",
|
|
"source_name": "NewsAPI",
|
|
"config": {"endpoint": "/v2/everything"},
|
|
"credibility_score": 0.8,
|
|
}
|
|
now = datetime(2026, 4, 11, 12, 0, 0)
|
|
job = build_job_payload(source, ["Apple", "iPhone"], now)
|
|
|
|
assert job["source_id"] == "sid-1"
|
|
assert job["company_id"] == "cid-1"
|
|
assert job["ticker"] == "AAPL"
|
|
assert job["legal_name"] == "Apple Inc."
|
|
assert job["aliases"] == ["Apple", "iPhone"]
|
|
assert job["source_type"] == "news_api"
|
|
assert job["config"] == {"endpoint": "/v2/everything"}
|
|
assert job["credibility_score"] == 0.8
|
|
assert job["scheduled_at"] == now.isoformat()
|
|
|
|
def test_payload_null_config(self):
|
|
source = {
|
|
"source_id": "sid-2",
|
|
"company_id": "cid-2",
|
|
"ticker": "MSFT",
|
|
"legal_name": "Microsoft Corp.",
|
|
"source_type": "market_api",
|
|
"source_name": "Polygon",
|
|
"config": None,
|
|
"credibility_score": None,
|
|
}
|
|
job = build_job_payload(source, [], datetime(2026, 4, 11, 12, 0, 0))
|
|
assert job["config"] == {}
|
|
assert job["credibility_score"] == 0.5
|
|
assert job["aliases"] == []
|