LukeMathWalker / wiremock-rs

HTTP mocking to test Rust applications.
Apache License 2.0
607 stars 69 forks source link

Consider support for mocking websockets #113

Open blueridanus opened 1 year ago

blueridanus commented 1 year ago

Would this addition be welcome? I could look into writing a PR for this.

LukeMathWalker commented 1 year ago

I'd like to understand the design before going through a PR! I'm definitely interested in the feature 😄

blueridanus commented 1 year ago

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;
LukeMathWalker commented 1 year ago

What does ws::EchoResponder look like in terms of implementation? What trait is it implementing?

xd009642 commented 1 year ago

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)

blueridanus commented 1 year ago

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).

xd009642 commented 1 year ago

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;
blueridanus commented 1 year ago

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.

LukeMathWalker commented 1 year ago

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.

xd009642 commented 1 year ago

Sure thing, I was running ahead to testing one of our gRPC services as well as the websocket ones :sweat_smile:

I was also thinking from the generics on StreamingResponse how it would turn into a tower template soup so probably the best approach tbh

dpezely commented 1 year ago

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.