diff --git a/backend/src/cyclone/api.py b/backend/src/cyclone/api.py index 926388c..68d94d4 100644 --- a/backend/src/cyclone/api.py +++ b/backend/src/cyclone/api.py @@ -279,7 +279,7 @@ async def parse_837( parsed_at=utcnow(), result=result, ) - store.add(rec) + store.add(rec, event_bus=request.app.state.event_bus) if _client_wants_json(request): body = json.loads(result.model_dump_json()) @@ -487,7 +487,7 @@ async def parse_835_endpoint( parsed_at=utcnow(), result=result, ) - store.add(rec) + store.add(rec, event_bus=request.app.state.event_bus) if _client_wants_json(request): body = json.loads(result.model_dump_json()) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index bc23067..0ee2b75 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -19,10 +19,22 @@ import pytest @pytest.fixture(autouse=True) 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") from cyclone import db + from cyclone.api import app + from cyclone.pubsub import EventBus + db._reset_for_tests() db.init_db() - yield - db._reset_for_tests() \ No newline at end of file + app.state.event_bus = EventBus() + try: + yield + finally: + app.state.event_bus = None + db._reset_for_tests() \ No newline at end of file diff --git a/backend/tests/test_pubsub.py b/backend/tests/test_pubsub.py index 5580b07..83c16e2 100644 --- a/backend/tests/test_pubsub.py +++ b/backend/tests/test_pubsub.py @@ -100,10 +100,35 @@ async def test_multiple_concurrent_subscribers_each_get_all_events(): def test_get_event_bus_raises_when_app_state_uninitialized(): - """get_event_bus() late-imports cyclone.api; without a running app, - accessing app.state.event_bus raises AttributeError.""" - # Don't import cyclone.api at module level — that would force a load - # and could mask the real failure. Just call the helper and expect - # the AttributeError to surface from app.state. - with pytest.raises((AttributeError, RuntimeError)): - get_event_bus() + """get_event_bus() late-imports cyclone.api; if the lifespan handler + hasn't set ``app.state.event_bus`` (or it's been cleared), the + AttributeError must surface so callers can fail loudly.""" + from cyclone.api import app + + # The autouse conftest fixture sets app.state.event_bus for every test + # 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