feat(api): parse endpoints pass EventBus into store writes
- api.parse_837 / parse_835: pass request.app.state.event_bus into store.add() - conftest: autouse fixture wires a fresh EventBus onto app.state for every test, since TestClient does not invoke the FastAPI lifespan handler unless used as a context manager - test_pubsub: split get_event_bus coverage into a raises-when-missing test and a returns-attached-bus test, both save/restore app.state.event_bus around the assertion so the autouse fixture's bus is preserved Phase 2 complete: db init moved to lifespan, EventBus is the process-wide publish point, and the two ingest endpoints publish claim_written / remittance_written / activity_recorded events on every store.add().
This commit is contained in:
@@ -279,7 +279,7 @@ async def parse_837(
|
|||||||
parsed_at=utcnow(),
|
parsed_at=utcnow(),
|
||||||
result=result,
|
result=result,
|
||||||
)
|
)
|
||||||
store.add(rec)
|
store.add(rec, event_bus=request.app.state.event_bus)
|
||||||
|
|
||||||
if _client_wants_json(request):
|
if _client_wants_json(request):
|
||||||
body = json.loads(result.model_dump_json())
|
body = json.loads(result.model_dump_json())
|
||||||
@@ -487,7 +487,7 @@ async def parse_835_endpoint(
|
|||||||
parsed_at=utcnow(),
|
parsed_at=utcnow(),
|
||||||
result=result,
|
result=result,
|
||||||
)
|
)
|
||||||
store.add(rec)
|
store.add(rec, event_bus=request.app.state.event_bus)
|
||||||
|
|
||||||
if _client_wants_json(request):
|
if _client_wants_json(request):
|
||||||
body = json.loads(result.model_dump_json())
|
body = json.loads(result.model_dump_json())
|
||||||
|
|||||||
@@ -19,10 +19,22 @@ import pytest
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _auto_init_db(tmp_path, monkeypatch):
|
def _auto_init_db(tmp_path, monkeypatch):
|
||||||
"""Point CYCLONE_DB_URL at a per-test SQLite file and init the schema."""
|
"""Point CYCLONE_DB_URL at a per-test SQLite file and init the schema.
|
||||||
|
|
||||||
|
Also wires a fresh ``EventBus`` onto ``app.state`` because ``TestClient``
|
||||||
|
does not invoke the FastAPI lifespan handler unless used as a context
|
||||||
|
manager. The bus is reset between tests so subscribers don't leak.
|
||||||
|
"""
|
||||||
monkeypatch.setenv("CYCLONE_DB_URL", f"sqlite:///{tmp_path}/test.db")
|
monkeypatch.setenv("CYCLONE_DB_URL", f"sqlite:///{tmp_path}/test.db")
|
||||||
from cyclone import db
|
from cyclone import db
|
||||||
|
from cyclone.api import app
|
||||||
|
from cyclone.pubsub import EventBus
|
||||||
|
|
||||||
db._reset_for_tests()
|
db._reset_for_tests()
|
||||||
db.init_db()
|
db.init_db()
|
||||||
yield
|
app.state.event_bus = EventBus()
|
||||||
db._reset_for_tests()
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
app.state.event_bus = None
|
||||||
|
db._reset_for_tests()
|
||||||
@@ -100,10 +100,35 @@ async def test_multiple_concurrent_subscribers_each_get_all_events():
|
|||||||
|
|
||||||
|
|
||||||
def test_get_event_bus_raises_when_app_state_uninitialized():
|
def test_get_event_bus_raises_when_app_state_uninitialized():
|
||||||
"""get_event_bus() late-imports cyclone.api; without a running app,
|
"""get_event_bus() late-imports cyclone.api; if the lifespan handler
|
||||||
accessing app.state.event_bus raises AttributeError."""
|
hasn't set ``app.state.event_bus`` (or it's been cleared), the
|
||||||
# Don't import cyclone.api at module level — that would force a load
|
AttributeError must surface so callers can fail loudly."""
|
||||||
# and could mask the real failure. Just call the helper and expect
|
from cyclone.api import app
|
||||||
# the AttributeError to surface from app.state.
|
|
||||||
with pytest.raises((AttributeError, RuntimeError)):
|
# The autouse conftest fixture sets app.state.event_bus for every test
|
||||||
get_event_bus()
|
# so that endpoint code can call request.app.state.event_bus. We pop
|
||||||
|
# the key entirely here so __getattr__ raises AttributeError rather
|
||||||
|
# than returning None for an explicitly-None slot.
|
||||||
|
state_dict = app.state._state
|
||||||
|
saved = state_dict.pop("event_bus", None)
|
||||||
|
try:
|
||||||
|
with pytest.raises((AttributeError, RuntimeError)):
|
||||||
|
get_event_bus()
|
||||||
|
finally:
|
||||||
|
if saved is not None:
|
||||||
|
state_dict["event_bus"] = saved
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_event_bus_returns_app_state_bus():
|
||||||
|
"""When the lifespan handler has initialised the bus, get_event_bus()
|
||||||
|
returns it. Validates the happy path under the same late-import."""
|
||||||
|
from cyclone.api import app
|
||||||
|
from cyclone.pubsub import EventBus
|
||||||
|
|
||||||
|
saved = getattr(app.state, "event_bus", None)
|
||||||
|
bus = EventBus()
|
||||||
|
app.state.event_bus = bus
|
||||||
|
try:
|
||||||
|
assert get_event_bus() is bus
|
||||||
|
finally:
|
||||||
|
app.state.event_bus = saved
|
||||||
|
|||||||
Reference in New Issue
Block a user