test: add pytest infrastructure and poll() tests
- Add pytest and pytest-asyncio as optional dev dependencies - Configure asyncio_mode=auto and pythonpath=[src] in pyproject.toml - Write three tests at the poll() seam: * single-page response publishes all Flights and returns the count * paginated response follows link:rel=next and publishes all pages * HTTP error from raise_for_status() propagates out of poll() Tests use injected stub HTTP session and mock Kafka engine; no real network or broker required. Closes #4 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
78c69e2cc7
commit
7644d47bc8
@ -23,6 +23,17 @@ dependencies = [
|
||||
[project.scripts]
|
||||
schiphol = "src.main:main"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=8.0",
|
||||
"pytest-asyncio>=0.24",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["src"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
121
tests/test_poll.py
Normal file
121
tests/test_poll.py
Normal file
@ -0,0 +1,121 @@
|
||||
"""Tests for poll() — the Schiphol API fetch-and-publish sweep."""
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from main import poll
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test doubles
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class _NoOpSpan:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
class StubTracer:
|
||||
def start_as_current_span(self, name, attributes=None):
|
||||
return _NoOpSpan()
|
||||
|
||||
|
||||
class StubResponse:
|
||||
"""Minimal requests.Response lookalike."""
|
||||
|
||||
def __init__(self, flights, *, next_url=None, raise_on_status=False):
|
||||
self._flights = flights
|
||||
self._raise = raise_on_status
|
||||
self.headers = {}
|
||||
if next_url:
|
||||
self.headers["link"] = f'<{next_url}>; rel="next"'
|
||||
|
||||
def raise_for_status(self):
|
||||
if self._raise:
|
||||
raise requests.HTTPError("500 Server Error")
|
||||
|
||||
def json(self):
|
||||
return {"flights": self._flights}
|
||||
|
||||
|
||||
class StubSession:
|
||||
"""Returns pre-configured StubResponse objects keyed by URL."""
|
||||
|
||||
def __init__(self, responses_by_url: dict):
|
||||
self._responses = responses_by_url
|
||||
|
||||
def get(self, url):
|
||||
return self._responses[url]
|
||||
|
||||
|
||||
class MockEngine:
|
||||
"""Captures every send() call so tests can assert on Kafka output."""
|
||||
|
||||
def __init__(self):
|
||||
self.sent: list[dict] = []
|
||||
|
||||
async def send(self, topic, *, value, key):
|
||||
self.sent.append({"topic": topic, "value": value, "key": key})
|
||||
|
||||
|
||||
class FakeApiSettings:
|
||||
base_url = "https://api.example.com/flights"
|
||||
|
||||
|
||||
class FakeKafkaSettings:
|
||||
topic = "flights-test"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_single_page_publishes_all_flights_and_returns_count():
|
||||
"""poll() publishes every Flight on a single-page response and returns the count."""
|
||||
flights = [{"id": "1", "flightName": "KL100"}, {"id": "2", "flightName": "KL200"}]
|
||||
session = StubSession({FakeApiSettings.base_url: StubResponse(flights)})
|
||||
engine = MockEngine()
|
||||
|
||||
count = await poll(session, engine, FakeApiSettings(), FakeKafkaSettings(), StubTracer())
|
||||
|
||||
assert count == 2
|
||||
assert len(engine.sent) == 2
|
||||
published_keys = {msg["key"] for msg in engine.sent}
|
||||
assert published_keys == {"1", "2"}
|
||||
|
||||
|
||||
async def test_pagination_publishes_flights_across_all_pages():
|
||||
"""poll() follows link: rel="next" headers and publishes Flights from every page."""
|
||||
page1_url = FakeApiSettings.base_url
|
||||
page2_url = "https://api.example.com/flights?page=2"
|
||||
|
||||
page1_flights = [{"id": "A"}, {"id": "B"}]
|
||||
page2_flights = [{"id": "C"}, {"id": "D"}, {"id": "E"}]
|
||||
|
||||
session = StubSession(
|
||||
{
|
||||
page1_url: StubResponse(page1_flights, next_url=page2_url),
|
||||
page2_url: StubResponse(page2_flights),
|
||||
}
|
||||
)
|
||||
engine = MockEngine()
|
||||
|
||||
count = await poll(session, engine, FakeApiSettings(), FakeKafkaSettings(), StubTracer())
|
||||
|
||||
assert count == 5
|
||||
assert len(engine.sent) == 5
|
||||
|
||||
|
||||
async def test_http_error_propagates_out_of_poll():
|
||||
"""poll() lets HTTP errors from raise_for_status() propagate to the caller."""
|
||||
session = StubSession(
|
||||
{FakeApiSettings.base_url: StubResponse([], raise_on_status=True)}
|
||||
)
|
||||
engine = MockEngine()
|
||||
|
||||
with pytest.raises(requests.HTTPError):
|
||||
await poll(session, engine, FakeApiSettings(), FakeKafkaSettings(), StubTracer())
|
||||
Loading…
Reference in New Issue
Block a user