[ACM-16232] Add support for search-api Subscriptions#6097
[ACM-16232] Add support for search-api Subscriptions#6097zlayne wants to merge 3 commits intostolostron:mainfrom
Conversation
Generated-by: Cursor (auto) Signed-off-by: zlayne <zlayne@redhat.com>
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: zlayne The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
📝 WalkthroughWalkthroughAdds WebSocket-based GraphQL subscription transport for search: backend upgrades ChangesSearch WebSocket Subscriptions
Sequence DiagramsequenceDiagram
participant Browser as Browser (graphql-ws)
participant Backend as Backend Server
participant SearchAPI as Search API
Browser->>Backend: HTTP/2 Upgrade Request /multicloud/proxy/search
Backend->>Backend: Rewrite URL (strip /multicloud/proxy)
Backend->>SearchAPI: Open upstream WSS (wss://..., HTTPS agent with CA)
SearchAPI-->>Backend: Upgrade Accept
Backend-->>Browser: Upgrade Accept
Browser->>Backend: connection_init (no Authorization)
Backend->>Backend: inject Authorization into payload
Backend->>SearchAPI: connection_init (with Authorization)
SearchAPI-->>Backend: connection_ack
Backend-->>Browser: connection_ack
Browser->>Backend: subscription: watch($input)
Backend->>SearchAPI: subscription: watch($input)
SearchAPI-->>Backend: event frames
Backend-->>Browser: event frames
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
backend/test/routes/searchWebSocket.test.ts (1)
4-25: 🏗️ Heavy liftThe new suite only covers the JSON helper, not the relay path.
The feature logic lives in
searchWebSocketitself: unauthenticated upgrades, upgrade completion, and the first-frame rewrite/forwarding path. A regression there would still pass this file, so please add at least one test that exercises the websocket flow rather than onlyinjectSearchWsConnectionInitAuthorization.As per coding guidelines, "Verify test coverage is meaningful, not just for metrics" and "Before claiming what the code under test does or allows, confirm it from the actual source (e.g. config, implementation); do not infer behavior from test names or comments alone".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/test/routes/searchWebSocket.test.ts` around lines 4 - 25, The test suite only verifies the JSON helper injectSearchWsConnectionInitAuthorization; add at least one integration-style test that exercises the searchWebSocket flow itself (the upgrade handling, unauthenticated upgrade path, upgrade completion, and the first-frame rewrite/forwarding) so regressions in the actual handler are caught. Specifically, create a test that calls the searchWebSocket handler (or spins up a lightweight http server and performs a WebSocket upgrade against it) and asserts that: unauthenticated upgrade requests are handled per implementation, the upgrade completion path is executed, and the first incoming connection_init frame is rewritten to include the Authorization header (or forwarded unchanged when Bearer present) before being proxied — referencing the searchWebSocket entrypoint and injectSearchWsConnectionInitAuthorization to locate the relevant logic. Ensure the test mocks/stubs the downstream socket/proxy behavior to observe the forwarded first-frame and any upgrade completion callbacks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/src/routes/search.ts`:
- Around line 175-177: The code builds headers with `Bearer ${token}` even when
`getAuthenticatedToken(req, socket)` returns a falsy token, causing
unauthenticated websocket upgrades to be forwarded; modify the `search` flow to
immediately stop the websocket path when `token` is falsy (e.g., send an auth
failure response / close the socket or return a 401) and skip creating `headers`
and calling `getSearchRequestOptions`; only construct `headers:
OutgoingHttpHeaders = { authorization: \`Bearer ${token}\` }` and call
`getSearchRequestOptions(headers)` when `token` is truthy.
---
Nitpick comments:
In `@backend/test/routes/searchWebSocket.test.ts`:
- Around line 4-25: The test suite only verifies the JSON helper
injectSearchWsConnectionInitAuthorization; add at least one integration-style
test that exercises the searchWebSocket flow itself (the upgrade handling,
unauthenticated upgrade path, upgrade completion, and the first-frame
rewrite/forwarding) so regressions in the actual handler are caught.
Specifically, create a test that calls the searchWebSocket handler (or spins up
a lightweight http server and performs a WebSocket upgrade against it) and
asserts that: unauthenticated upgrade requests are handled per implementation,
the upgrade completion path is executed, and the first incoming connection_init
frame is rewritten to include the Authorization header (or forwarded unchanged
when Bearer present) before being proxied — referencing the searchWebSocket
entrypoint and injectSearchWsConnectionInitAuthorization to locate the relevant
logic. Ensure the test mocks/stubs the downstream socket/proxy behavior to
observe the forwarded first-frame and any upgrade completion callbacks.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 620ef262-4f4f-4fbc-8bb0-c41c9c0b24df
⛔ Files ignored due to path filters (2)
backend/package-lock.jsonis excluded by!**/package-lock.jsonfrontend/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (8)
backend/package.jsonbackend/src/lib/server.tsbackend/src/routes/search.tsbackend/test/routes/searchWebSocket.test.tsfrontend/package.jsonfrontend/src/routes/Search/search-sdk/search-client.tsfrontend/src/routes/Search/search-sdk/search-sdk.tsfrontend/src/routes/Search/search-sdk/subscription.graphql
Signed-off-by: zlayne <zlayne@redhat.com>
Generated-by: Cursor (auto) Signed-off-by: zlayne <zlayne@redhat.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/test/routes/searchWebSocket.test.ts`:
- Around line 247-277: The tests currently only check mockWsInstance exists and
thus don't verify the upstream URL or headers; update each test (in
searchWebSocket.test.ts) to assert the ws constructor was called with the
expected URL and header args instead of just checking mockWsInstance.
Specifically, replace expect(mockWsInstance).toBeDefined() with assertions on
mockWsConstructor (or whatever jest mock wraps the ws client) to inspect
mockWsConstructor.mock.calls[0][0] equals the expected URL (e.g.,
"wss://search.example.com/graphql" when port===443 and
"wss://search.example.com:4010/graphql" when port!==443) and
mockWsConstructor.mock.calls[0][1] (or the headers object) contains the
forwarded 'sec-websocket-protocol' value when using makeMockReq with that
header; reference searchWebSocket, makeMockReq, mockGetSearchOpts,
mockWsConstructor/mockWsInstance to locate the mocks and replace the
non-verifying expect with these concrete call-argument checks.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 378340bd-c598-420b-a7e0-4fdfb93f96a4
📒 Files selected for processing (1)
backend/test/routes/searchWebSocket.test.ts
| it('constructs upstream URL without port when port is 443', async () => { | ||
| const socket = makeMockSocket() | ||
| mockGetAuthToken.mockResolvedValue('tok') | ||
| mockGetSearchOpts.mockResolvedValue({ hostname: 'search.example.com', port: 443, path: '/graphql' }) | ||
|
|
||
| await searchWebSocket(makeMockReq(), socket, Buffer.alloc(0)) | ||
|
|
||
| // If upstream was created without throwing we verify it exists; URL is wss://host/path (no port) | ||
| expect(mockWsInstance).toBeDefined() | ||
| }) | ||
|
|
||
| it('constructs upstream URL with port when port is not 443', async () => { | ||
| const socket = makeMockSocket() | ||
| mockGetAuthToken.mockResolvedValue('tok') | ||
| mockGetSearchOpts.mockResolvedValue({ hostname: 'search.example.com', port: 4010, path: '/graphql' }) | ||
|
|
||
| await searchWebSocket(makeMockReq(), socket, Buffer.alloc(0)) | ||
|
|
||
| expect(mockWsInstance).toBeDefined() | ||
| }) | ||
|
|
||
| it('uses the sec-websocket-protocol header from the browser request for the upstream', async () => { | ||
| const socket = makeMockSocket() | ||
| mockGetAuthToken.mockResolvedValue('tok') | ||
| mockGetSearchOpts.mockResolvedValue(DEFAULT_OPTIONS) | ||
|
|
||
| await searchWebSocket(makeMockReq({ 'sec-websocket-protocol': 'graphql-ws' }), socket, Buffer.alloc(0)) | ||
|
|
||
| // Upstream WS was created; the test verifies the function completes without error | ||
| expect(mockWsInstance).toBeDefined() | ||
| }) |
There was a problem hiding this comment.
URL/protocol tests are non-verifying and can pass on regressions.
These tests currently pass even if URL or protocol forwarding breaks, because mockWsInstance is always defined from beforeEach. Assert the mocked ws constructor arguments (URL + headers) directly.
Proposed fix
+import WebSocket from 'ws'
...
+const mockWebSocketCtor = WebSocket as unknown as jest.Mock
...
it('constructs upstream URL without port when port is 443', async () => {
const socket = makeMockSocket()
mockGetAuthToken.mockResolvedValue('tok')
mockGetSearchOpts.mockResolvedValue({ hostname: 'search.example.com', port: 443, path: '/graphql' })
await searchWebSocket(makeMockReq(), socket, Buffer.alloc(0))
- // If upstream was created without throwing we verify it exists; URL is wss://host/path (no port)
- expect(mockWsInstance).toBeDefined()
+ expect(mockWebSocketCtor).toHaveBeenCalledWith(
+ 'wss://search.example.com/graphql',
+ expect.objectContaining({
+ headers: expect.objectContaining({ host: 'search.example.com' }),
+ })
+ )
})
it('constructs upstream URL with port when port is not 443', async () => {
const socket = makeMockSocket()
mockGetAuthToken.mockResolvedValue('tok')
mockGetSearchOpts.mockResolvedValue({ hostname: 'search.example.com', port: 4010, path: '/graphql' })
await searchWebSocket(makeMockReq(), socket, Buffer.alloc(0))
- expect(mockWsInstance).toBeDefined()
+ expect(mockWebSocketCtor).toHaveBeenCalledWith(
+ 'wss://search.example.com:4010/graphql',
+ expect.objectContaining({
+ headers: expect.objectContaining({ host: 'search.example.com:4010' }),
+ })
+ )
})
it('uses the sec-websocket-protocol header from the browser request for the upstream', async () => {
const socket = makeMockSocket()
mockGetAuthToken.mockResolvedValue('tok')
mockGetSearchOpts.mockResolvedValue(DEFAULT_OPTIONS)
await searchWebSocket(makeMockReq({ 'sec-websocket-protocol': 'graphql-ws' }), socket, Buffer.alloc(0))
- // Upstream WS was created; the test verifies the function completes without error
- expect(mockWsInstance).toBeDefined()
+ expect(mockWebSocketCtor).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.objectContaining({
+ headers: expect.objectContaining({ 'sec-websocket-protocol': 'graphql-ws' }),
+ })
+ )
})As per coding guidelines, **/*.test.{ts,tsx}: "Verify test coverage is meaningful, not just for metrics" and "Check that tests actually test the described behavior".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('constructs upstream URL without port when port is 443', async () => { | |
| const socket = makeMockSocket() | |
| mockGetAuthToken.mockResolvedValue('tok') | |
| mockGetSearchOpts.mockResolvedValue({ hostname: 'search.example.com', port: 443, path: '/graphql' }) | |
| await searchWebSocket(makeMockReq(), socket, Buffer.alloc(0)) | |
| // If upstream was created without throwing we verify it exists; URL is wss://host/path (no port) | |
| expect(mockWsInstance).toBeDefined() | |
| }) | |
| it('constructs upstream URL with port when port is not 443', async () => { | |
| const socket = makeMockSocket() | |
| mockGetAuthToken.mockResolvedValue('tok') | |
| mockGetSearchOpts.mockResolvedValue({ hostname: 'search.example.com', port: 4010, path: '/graphql' }) | |
| await searchWebSocket(makeMockReq(), socket, Buffer.alloc(0)) | |
| expect(mockWsInstance).toBeDefined() | |
| }) | |
| it('uses the sec-websocket-protocol header from the browser request for the upstream', async () => { | |
| const socket = makeMockSocket() | |
| mockGetAuthToken.mockResolvedValue('tok') | |
| mockGetSearchOpts.mockResolvedValue(DEFAULT_OPTIONS) | |
| await searchWebSocket(makeMockReq({ 'sec-websocket-protocol': 'graphql-ws' }), socket, Buffer.alloc(0)) | |
| // Upstream WS was created; the test verifies the function completes without error | |
| expect(mockWsInstance).toBeDefined() | |
| }) | |
| it('constructs upstream URL without port when port is 443', async () => { | |
| const socket = makeMockSocket() | |
| mockGetAuthToken.mockResolvedValue('tok') | |
| mockGetSearchOpts.mockResolvedValue({ hostname: 'search.example.com', port: 443, path: '/graphql' }) | |
| await searchWebSocket(makeMockReq(), socket, Buffer.alloc(0)) | |
| expect(mockWebSocketCtor).toHaveBeenCalledWith( | |
| 'wss://search.example.com/graphql', | |
| expect.objectContaining({ | |
| headers: expect.objectContaining({ host: 'search.example.com' }), | |
| }) | |
| ) | |
| }) | |
| it('constructs upstream URL with port when port is not 443', async () => { | |
| const socket = makeMockSocket() | |
| mockGetAuthToken.mockResolvedValue('tok') | |
| mockGetSearchOpts.mockResolvedValue({ hostname: 'search.example.com', port: 4010, path: '/graphql' }) | |
| await searchWebSocket(makeMockReq(), socket, Buffer.alloc(0)) | |
| expect(mockWebSocketCtor).toHaveBeenCalledWith( | |
| 'wss://search.example.com:4010/graphql', | |
| expect.objectContaining({ | |
| headers: expect.objectContaining({ host: 'search.example.com:4010' }), | |
| }) | |
| ) | |
| }) | |
| it('uses the sec-websocket-protocol header from the browser request for the upstream', async () => { | |
| const socket = makeMockSocket() | |
| mockGetAuthToken.mockResolvedValue('tok') | |
| mockGetSearchOpts.mockResolvedValue(DEFAULT_OPTIONS) | |
| await searchWebSocket(makeMockReq({ 'sec-websocket-protocol': 'graphql-ws' }), socket, Buffer.alloc(0)) | |
| expect(mockWebSocketCtor).toHaveBeenCalledWith( | |
| expect.any(String), | |
| expect.objectContaining({ | |
| headers: expect.objectContaining({ 'sec-websocket-protocol': 'graphql-ws' }), | |
| }) | |
| ) | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/test/routes/searchWebSocket.test.ts` around lines 247 - 277, The
tests currently only check mockWsInstance exists and thus don't verify the
upstream URL or headers; update each test (in searchWebSocket.test.ts) to assert
the ws constructor was called with the expected URL and header args instead of
just checking mockWsInstance. Specifically, replace
expect(mockWsInstance).toBeDefined() with assertions on mockWsConstructor (or
whatever jest mock wraps the ws client) to inspect
mockWsConstructor.mock.calls[0][0] equals the expected URL (e.g.,
"wss://search.example.com/graphql" when port===443 and
"wss://search.example.com:4010/graphql" when port!==443) and
mockWsConstructor.mock.calls[0][1] (or the headers object) contains the
forwarded 'sec-websocket-protocol' value when using makeMockReq with that
header; reference searchWebSocket, makeMockReq, mockGetSearchOpts,
mockWsConstructor/mockWsInstance to locate the mocks and replace the
non-verifying expect with these concrete call-argument checks.
Generated-by: Cursor (auto)
📝 Summary
Ticket Summary (Title):
Adds support for Search API GraphQL Subscriptions.
Developers will now be able to utilize the
useSearchSubscriptionSubscriptionhook to start a subscription and watch for change events on resources that match the provided search parameters.Ticket Link:
https://issues.redhat.com/browse/ACM-16232
Type of Change:
✅ Checklist
General
ACM-12340 Fix bug with...)If Feature
If Bugfix
🗒️ Notes for Reviewers
Summary by CodeRabbit
New Features
Tests
Chores