Skip to content

Add integration test for OpenStack events WebSocket endpoint#2412

Open
berendt wants to merge 1 commit into
mainfrom
implement/issue-2407-websocket-integration-test
Open

Add integration test for OpenStack events WebSocket endpoint#2412
berendt wants to merge 1 commit into
mainfrom
implement/issue-2407-websocket-integration-test

Conversation

@berendt

@berendt berendt commented Jun 22, 2026

Copy link
Copy Markdown
Member

Closes #2407

What this does

Adds an integration test for the OpenStack events WebSocket endpoint GET /v1/events/openstack, driving it through fastapi.testclient.TestClient. The endpoint upgrades to a WebSocket served by the in-process websocket_manager (osism/services/websocket_manager.py), which broadcasts via an in-process asyncio queue — so the test exercises the full connect / set-filters / broadcast / disconnect path end-to-end without any service beyond the TestClient.

Change set (single commit)

35d94ea — Add integration test for OpenStack events WebSocket endpoint

  • tests/integration/test_api_websocket.py (new, 133 lines) — five test cases covering:
    • test_websocket_connect_is_accepted — the WebSocket upgrade is accepted.
    • test_set_filters_is_acknowledged — a set_filters message is processed and acknowledged verbatim (filter_update / success, echoing the event/node/service filters).
    • test_matching_event_is_delivered — an event matching the connection's filters is delivered intact (compared against EventMessage.to_dict()).
    • test_non_matching_event_is_filtered_out — a non-matching event is dropped; a FIFO-following sentinel event proves the drop without relying on an absence/timeout.
    • test_disconnect_drops_connection_count — disconnect runs the finally cleanup and the connection count returns to its pre-connect value.
  • Pipfile / Pipfile.lock — add pytest-timeout==2.4.0 to dev dependencies.

Notes for the reviewer

  • Module-scoped client fixture. The global websocket_manager owns module-level asyncio primitives (event_queue, _lock) that bind to the first event loop that touches them and raise "bound to a different event loop" otherwise. A single module-scoped TestClient keeps one loop alive across the module so those primitives stay valid. osism.api is imported lazily inside the fixture because importing it wires the event bridge to Redis at module load — safe only in the integration environment.
  • Loop-bound queue. Events are enqueued from the app's event loop via client.portal.call(websocket_manager.add_event, ...) rather than from the test thread, because the queue is loop-bound.
  • Divergence from the issue: the issue (Integration test: WebSocket /v1/events/openstack (FastAPI TestClient) #2407) only asks for the integration test. This PR additionally adds pytest-timeout==2.4.0 as a dev dependency and applies pytest.mark.timeout(30) to the module. Without it, the blocking ws.receive_json() calls (Starlette's untimed queue.get()) would hang forever on a regression and only die on the CI wall-clock timeout; the cap turns such a hang into a precise 30-second failure at the exact receive_json() call. The test is also tagged integration to share the FastAPI/httpx setup with the sibling facts test and stay in the same Tier 2 batch.

Implemented by planwerk-review a24f4d9 with Claude:claude-opus-4-8

Cover the /v1/events/openstack WebSocket and the in-process
websocket_manager via the FastAPI TestClient: connection accept,
filter-message acknowledgment, delivery of a matching event,
filtering-out of a non-matching event, and connection-count cleanup
on disconnect.

The TestClient fixture is module-scoped because the manager's
module-level asyncio primitives bind to the first event loop that
touches them; sharing one loop across the module keeps them valid.
Events are pushed onto the loop-bound queue via the app's portal.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Christian Berendt <berendt@osism.tech>
@berendt berendt marked this pull request as ready for review June 22, 2026 19:50
@berendt berendt requested a review from ideaship June 22, 2026 19:50

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • Several tests duplicate the WebSocket URL and common set_filters payloads; consider extracting small helper functions or constants to reduce repetition and make future changes to the endpoint or filter shape easier.
  • The tests directly inspect websocket_manager.connections, which tightly couples them to the internal implementation; if possible, prefer asserting externally observable behavior (e.g., via events or metrics helpers) so that refactoring the manager internals won’t require test rewrites.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Several tests duplicate the WebSocket URL and common `set_filters` payloads; consider extracting small helper functions or constants to reduce repetition and make future changes to the endpoint or filter shape easier.
- The tests directly inspect `websocket_manager.connections`, which tightly couples them to the internal implementation; if possible, prefer asserting externally observable behavior (e.g., via events or metrics helpers) so that refactoring the manager internals won’t require test rewrites.

## Individual Comments

### Comment 1
<location path="tests/integration/test_api_websocket.py" line_range="95-104" />
<code_context>
+def test_non_matching_event_is_filtered_out(client):
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen assertions in `test_non_matching_event_is_filtered_out` to validate the full payload

This test only checks the `id` and `event_type` of the sentinel message. To better guard against regressions, consider asserting equality with `sentinel.to_dict()` (as in `test_matching_event_is_delivered`) and/or explicitly asserting that the `received` body does not equal `non_matching.to_dict()` so the entire payload is validated, not just two fields.

Suggested implementation:

```python
    # The full payload of the delivered event should match the sentinel
    assert received == sentinel.to_dict()
    # And it must not match the non-matching event that was filtered out
    assert received != non_matching.to_dict()

```

If the existing assertions in `test_non_matching_event_is_filtered_out` differ from the `SEARCH` block above (for example, if the keys or attribute names are slightly different), adjust the `SEARCH` section to match the exact current assertions and keep the `REPLACE` section as-is.

Also ensure that within `test_non_matching_event_is_filtered_out`:
1. `non_matching` is defined in the same scope as the new assertions (as in your snippet it is).
2. A `sentinel` event is created, enqueued, and assigned to the `sentinel` variable before `received = ws.receive_json()` so that `sentinel.to_dict()` is meaningful, similar to how it is done in `test_matching_event_is_delivered`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +95 to +104
def test_non_matching_event_is_filtered_out(client):
"""An event that does not match the filters is not delivered."""
with client.websocket_connect("/v1/events/openstack") as ws:
ws.send_json(
{"action": "set_filters", "event_filters": ["compute.instance.create.end"]}
)
ws.receive_json() # filter acknowledgment

non_matching = EventMessage(
"baremetal.node.power_set.end", "openstack", {"node": str(uuid.uuid4())}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Strengthen assertions in test_non_matching_event_is_filtered_out to validate the full payload

This test only checks the id and event_type of the sentinel message. To better guard against regressions, consider asserting equality with sentinel.to_dict() (as in test_matching_event_is_delivered) and/or explicitly asserting that the received body does not equal non_matching.to_dict() so the entire payload is validated, not just two fields.

Suggested implementation:

    # The full payload of the delivered event should match the sentinel
    assert received == sentinel.to_dict()
    # And it must not match the non-matching event that was filtered out
    assert received != non_matching.to_dict()

If the existing assertions in test_non_matching_event_is_filtered_out differ from the SEARCH block above (for example, if the keys or attribute names are slightly different), adjust the SEARCH section to match the exact current assertions and keep the REPLACE section as-is.

Also ensure that within test_non_matching_event_is_filtered_out:

  1. non_matching is defined in the same scope as the new assertions (as in your snippet it is).
  2. A sentinel event is created, enqueued, and assigned to the sentinel variable before received = ws.receive_json() so that sentinel.to_dict() is meaningful, similar to how it is done in test_matching_event_is_delivered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Ready

Development

Successfully merging this pull request may close these issues.

Integration test: WebSocket /v1/events/openstack (FastAPI TestClient)

2 participants