Consider support for mocking websockets
blueridanus opened this issue · 10 comments
Would this addition be welcome? I could look into writing a PR for this.
I'd like to understand the design before going through a PR!
I'm definitely interested in the feature 😄
Cool! Not entirely sure on design myself, but here's one possibility (for the user-facing part at least):
use wiremock::ws::*;
Mock::given(ws::Upgrade) // new kind of matcher
.and(path("/hello_ws"))
// this could simply echo ws messages back to client. could also have another case which takes a closure
.respond_with_ws(ws::EchoResponder)
.mount(&mock_server)
.await;
What does ws::EchoResponder
look like in terms of implementation? What trait is it implementing?
I would imagine you want a separate Respond
trait like StreamingRespond
which looks something like:
#[async_trait]
pub trait StreamingRespond: Send + Sync {
// Required method
async fn respond(&self, request: &Request, rx: Receiver<Bytes>, tx: Sender<Bytes>) -> ResponseTemplate;
}
This might then be generic enough to allow for mocking grpc servers as well (exercise left to reader). I figured normal ResponseTemplate
can still be returned because the upgrade may be rejected and a HTTP response like unauthorised may be returned in some instances (or for grpc you always end up with a http status code at the end anyway).
The choice of channels to use and whether you use/expose Bytes
via the public API may need more work and then how it's injected into the mock but that's my initial gut instinct (hopefully not completely off base). Also async-trait is naturally another choice being made here which would be another dependency added into the mix (hopefully not too egregious of one)
I figured something similar, but WebSockets specific (e.g. WsRespond
). I think the more generic approach above could be even better, as long as some attention is paid to the possibility of Transfer-Encoding: chunked
HTTP requests (which that trait name seems to suggest).
Yeah, I think for other things such as websockets or grpc you would need to bring in a library like tungstenite and tonic for the transport stuff that's not as visible to user (i.e. websocket handshaking etc). So they'd have to be optional dependencies (no need to add bloat for people).
I guess that means adding some sort of transport style middleware to handle serializing/deserializing to things like websocket TCP frames and also doing any handshaking etc that has to be done. Maybe it would look a bit like:
use wiremock::ws::*;
#[async_trait]
impl StreamingRespond for Streamer {
// left as an exercise to the reader
}
Mock::given(ws::Upgrade) // new kind of matcher
.and(path("/hello_ws"))
.transport(transport::Websocket) // Specify what transport will be used to send the bytes over
.streaming_respond_with(Streamer::default()) // Specify how data received from transport will be handled
.mount(&mock_server)
.await;
An advantage of forgoing the generic approach would be handlers/implementors getting typed messages (e.g. Text(..)
, Binary(..)
, Ping(..)
etc for WS) instead of raw bytes, although this could potentially be addressed by making the StreamingRespond
trait even more generic with an associated message type.
I lean towards having the interface being specific to websockets—we can always generalize later if/when we decide to support something like gRPC (which hasn't been the case for the past ~3 years).
Especially if a special-cased interface allows us to provide better ergonomics, which is the paramount focus in wiremock
—writing tests is annoying enough.
Sure thing, I was running ahead to testing one of our gRPC services as well as the websocket ones 😅
I was also thinking from the generics on StreamingResponse how it would turn into a tower template soup so probably the best approach tbh
I can help if you need early adopters to test and contribute based upon some familiarity with WebSockets as well as using wiremock
.
(Currently working on a project that could use this, and we're already using wiremock
thus would like to keep it all in same the family. Thank you for doing this!)
Edit: fixed typo.