-
-
Notifications
You must be signed in to change notification settings - Fork 559
feat: support cross-process interception via setupRemoteServer
#1617
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
commit: |
79ae842
to
e3f4c44
Compare
e3f4c44
to
b0b0381
Compare
0eb583f
to
bc0e0a7
Compare
🐞 Life-cycle event forwarding order sensitivityLCE are sensitive to order. But when forwarded, some events may arrive faster over HTTP, breaking that order: FAIL test/node/msw-api/setup-remote-server/life-cycle-event-forwarding.node.test.ts > emits correct events for the request handled in the test process
AssertionError: expected [ …(4) ] to deeply equal [ …(4) ]
- Expected
+ Received
Array [
Array [
"[request:start] GET https://example.com/resource ade0140740d15",
],
Array [
- "[request:match] GET https://example.com/resource ade0140740d15",
+ "[request:end] GET https://example.com/resource ade0140740d15",
],
Array [
- "[request:end] GET https://example.com/resource ade0140740d15",
+ "[request:match] GET https://example.com/resource ade0140740d15",
],
Array [
"[response:mocked] GET https://example.com/resource ade0140740d15 200 {\"mocked\":true}",
],
] Need to solve this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concerns
Is it safe to introduce a WebSocket server that will be, effectively, routing HTTP messages over the local network (during tests only)?
- Yes. If someone can intercept that WebSocket communication, they are already in your machine and can do things far worse than that.
When adding new WebSocket server, please double-check that it's protected by Cross-site WebSocket hijacking too. I'm mentioning this explicitly as it's been common flaw in building and testing tools.
tldr; is that any website was able to connect to Vite and Vitest websocket server by opening connection to localhost
, e.g. new WebSocket('ws://localhost:51204/__vitest_api__')
. The HTTP APIs would reject such connections automatically, but WS didn't. Imagine someone hosting cool-msw-tips.com
that checks if MSW related WS servers are open.
Great stuff. I really hope this makes it into a release. A question, though. Wouldn't it be easier to expose a simple HTTP API to control mocking remotely? With websockets, a service that doesn't expose a websocket API suddenly has to worry about having the infra to support websockets only because it uses them for testing. I'm primarily looking at this from the angle of testing your service when it's deployed to a testing environment, not only running the service and the tests on your machine. |
@MilosRasic, I've started this feature over WebSockets but has long moved to using HTTP as the protocol for remote interception. I plan on releasing it that way, too. We will likely migrate to WebSockets anyway to support intercepting WebSockets as event-driven protocol will capture that use case more elegantly while still allowing for HTTP interception. These protocols are meant as purely internal though. Regardless of what MSW choses to use under the hood, it will not something you will be interacting with in any way. Your request handlers remain the only means to control the network. These HTTP or WebSocket servers are internal and serve as the source of the network, not the means to control it. |
@kettanaito I think I understand, but now I'm not sure any more 🙂 If my service is running on Machine A and my tests on Machine T, the two would communicate using the protocol you have chosen for implementation, correct? If that happens to be WebSockets, I could encounter issues if my infrastructure wasn't set up to support them, since my service on Machine A doesn't expose a WebSockets API as part of its own API. That's the only concern I have. |
While that's technically possible, both HTTP and WebSocket are globally available protocols in Node.js. MSW doesn't support custom environments. If you end up running one, it becomes your responsibility to procure all the necessary things for MSW to work (or just skip the features that don't). Does this make sense? |
Thanks @kettanaito Upon further consideration, I don't think either HTTP or WebSocket control would be a match for testing in a real production-like environment. If MSW's internal HTTP request or WebSocket connection is handled by one instance of the service under test in my testing environment, only that instance will be configured to use the MSW mock. How can I guarantee that the test request will be handled by the same instance? I don't think it can work, but this will still be handy for those who wish to test only locally or in CI. |
@MilosRasic, not sure which use case you have in mind, but cross-process request interception is agnostic of how your system/test is structured. Neither is it limited to tests. You use it to intercept and control network of one process from another. Speaking of testing in particular, CPRI does require you to provision test case - app instance isolation to prevent your often single running app instance from acting as a shared state (imagine two test cases with CPRI listing different network behaviors. Ouch!). That's precisely why test cases in this pull request rely on I intend CPRI primarily for testing full-stack behaviors, like React Server Components. In that context, you'd want to do two things to ensure reliable tests:
You can see both of these in action in |
Hey @kettanaito I was just reading through the history of this pr as I've just published a somewhat similar solution that leverages MSW called Mocky Balboa. The core use case is to mock outbound http requests on your SSR frameworks server runtime from your test suite. I'm not sure if you're still continuing on the path of using WebSockets for the Playwright uses a Node.js runtime for their test workers, but Cypress executes test code in the browser. I'd imagine a primary use case for this feature is going to be for UI testing applications running on SSR frameworks. |
Intention
Introduce an API that allows one process to modify the traffic of another process. The most apparent application for this is testing server-side behaviors of a JavaScript application:
This API is designed exclusively for use cases when the request-issuing process and the request-resolving process (i.e. where you run MSW) are two different processes.
Important
While the feature itself is unopinionated in how it's used, in practice it implies that you either have a fixed list of handlers OR you provide test-app isolation by spawning an app instance per test case. That is so handler overrides in one test don't affect the remote network for other tests. The app becomes a shared state here with no means to distinguish who is the consumer (test -> client -> server chain loses any identifiers).
Proposed API
With consideration to the existing MSW user experience, I suggest we add a
setupRemoteServer()
API that implements theSetupApi
interface and has a similar API tosetupServer
. The main user-facing distinction here is thatsetupRemoteServer
is affecting a remote process, as indicated by the name.The
.listen()
and.close()
methods of the remote server become async since they now establish and terminate an internal server instance respectively.You can then operate with the
remote
server as you would with a regularsetupServer
, keeping in mind that it doesn't affect the current process (your test) but instead, any remote process that runssetupServer
(your app).By fully extending the
SetupApi
, thesetupRemoteServer
API provides the user with full network-managing capabilities. This includes defining initial and runtime request handlers, as well as observing the outgoing traffic of a remote process using the Life-cycle API (remote.events.on(event, listener)
). I think this is a nice familiarity that also provides the user with more power when it comes to controlling the network.Implementation
I've considered multiple ways of implementing this feature. Listing them below.
(Chosen) WebSocket server
The
setupRemoteServer
API can establish an internal WebSocket server that can route the outgoing traffic from any server-side MSW instance anywhere and deliver it to the remote server to potentially resolve.Technically, the WebSocket server acts as a resolution point (i.e. your handlers) while the remote MSW process acts as a request supplier (similar to how the Service Worker acts in the browser).
Very roughly, this implies that the regular
setupServer
instances now have a fixed request handler that tries to check if any outgoing request is potentially handled by an existing remote WebSocket server:If no WebSocket server was found or establishing a connection with it fails within a sensible timeout period (~500ms), the
setupServer
instance of the app continues to operate as normal.IPC
The test process and the app process can utilize IPC (interprocess communication) to implement a messaging protocol. Using that protocol, the app can signal back any outgoing requests and the test can try resolving them against the request handlers you defined immediately in the test.
This approach is similar to the WebSocket approach above with the exception that it relies on IPC instead of a standalone running server. With that, it also gains its biggest disadvantage: the app process must be a child process of the test process. This is not easy to guarantee. Depending on the framework's internal implementation, the user may not achieve this parent/child relationship, and the IPC implementation will not work.
Given such a demanding requirement, I've decided not to use this implementation.
Limitations
useRemoteServer()
affects the network resolution for the entire app. This means that you cannot have multiple tests that override request handlers for the same app at the same time. I think this is more than reasonable since you know you're running 1 app instance that can only behave in a single way at a single point in time. Still, I expect users to be confused when they parallelize their E2E tests and suddenly see some network behaviors leaking across the test cases.Concerns
setupRemoteServer
only affects the server-side network behavior of any running application process with the server-side MSW integration? To affect the client-side network behavior from a test you have to 1) havesetupWorker
integration in the app; 2) set a globalwindow.worker
instance; 3) usewindow.worker.use()
to add runtime request handlers. This stays as it is right now, no changes here.The API is TBD and is subjected to change.
Roadmap
rest.all()
handler.response
insetupServer
.ReadableStream
from the remote request handler (may consider transferringReadableStream
over the WS messages instead ofArrayBuffer
, if that's allowed).ReadableStream
transfer over WebSockets that would be great.remotePort
andport
an implementation detail ofsetupRemoteServer
andsetupServer({ remote: true })
. The developer mustn't care about those.use()
test (may have something to do with the handlers management refactoring as a part of theserver.boundary()
).setupServer
(that it doesn't emit them for internal requests).setupWorker
support (see feat: support cross-process interception viasetupRemoteServer
#1617 (comment)).socket.io
in favor ofws
if we don't need anything socketio-specificforwardLifeCycleEvents()
).ListenOptions
onsetupRemoteServer()
, likeonUnhandledRequest
.spyOnLifeCycleEvents
test utility importingvitest
when it can also be run in the browser.setupRemoteServer
#1617 (comment)setupRemoteServer
#1617 (review)).Blockers
socket.io-parser
are broken for the CJS build).setupRemoteServer
#1617 (comment)).