juniper/http/
mod.rs

1//! Utilities for building HTTP endpoints in a library-agnostic manner
2
3pub mod graphiql;
4pub mod playground;
5
6use serde::{
7    Deserialize, Serialize, de,
8    ser::{self, SerializeMap},
9};
10
11use crate::{
12    FieldError, GraphQLError, GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync, RootNode,
13    Value, Variables,
14    ast::InputValue,
15    executor::{ExecutionError, ValuesStream},
16    value::{DefaultScalarValue, ScalarValue},
17};
18
19/// The expected structure of the decoded JSON document for either POST or GET requests.
20///
21/// For POST, you can use Serde to deserialize the incoming JSON data directly
22/// into this struct - it derives Deserialize for exactly this reason.
23///
24/// For GET, you will need to parse the query string and extract "query",
25/// "operationName", and "variables" manually.
26#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
27pub struct GraphQLRequest<S = DefaultScalarValue>
28where
29    S: ScalarValue,
30{
31    /// GraphQL query representing this request.
32    pub query: String,
33
34    /// Optional name of the operation associated with this request.
35    #[serde(rename = "operationName")]
36    pub operation_name: Option<String>,
37
38    /// Optional variables to execute the GraphQL operation with.
39    // TODO: Use `Variables` instead of `InputValue`?
40    #[serde(bound(
41        deserialize = "InputValue<S>: Deserialize<'de>",
42        serialize = "InputValue<S>: Serialize",
43    ))]
44    pub variables: Option<InputValue<S>>,
45}
46
47impl<S> GraphQLRequest<S>
48where
49    S: ScalarValue,
50{
51    // TODO: Remove in 0.17 `juniper` version.
52    /// Returns the `operation_name` associated with this request.
53    #[deprecated(since = "0.16.0", note = "Use the direct field access instead.")]
54    pub fn operation_name(&self) -> Option<&str> {
55        self.operation_name.as_deref()
56    }
57
58    /// Returns operation [`Variables`] defined withing this request.
59    pub fn variables(&self) -> Variables<S> {
60        self.variables
61            .as_ref()
62            .and_then(|iv| {
63                iv.to_object_value()
64                    .map(|o| o.into_iter().map(|(k, v)| (k.into(), v.clone())).collect())
65            })
66            .unwrap_or_default()
67    }
68
69    /// Construct a new GraphQL request from parts
70    pub fn new(
71        query: String,
72        operation_name: Option<String>,
73        variables: Option<InputValue<S>>,
74    ) -> Self {
75        Self {
76            query,
77            operation_name,
78            variables,
79        }
80    }
81
82    /// Execute a GraphQL request synchronously using the specified schema and context
83    ///
84    /// This is a simple wrapper around the `execute_sync` function exposed at the
85    /// top level of this crate.
86    pub fn execute_sync<QueryT, MutationT, SubscriptionT>(
87        &self,
88        root_node: &RootNode<QueryT, MutationT, SubscriptionT, S>,
89        context: &QueryT::Context,
90    ) -> GraphQLResponse<S>
91    where
92        S: ScalarValue,
93        QueryT: GraphQLType<S>,
94        MutationT: GraphQLType<S, Context = QueryT::Context>,
95        SubscriptionT: GraphQLType<S, Context = QueryT::Context>,
96    {
97        GraphQLResponse(crate::execute_sync(
98            &self.query,
99            self.operation_name.as_deref(),
100            root_node,
101            &self.variables(),
102            context,
103        ))
104    }
105
106    /// Execute a GraphQL request using the specified schema and context
107    ///
108    /// This is a simple wrapper around the `execute` function exposed at the
109    /// top level of this crate.
110    pub async fn execute<'a, QueryT, MutationT, SubscriptionT>(
111        &'a self,
112        root_node: &'a RootNode<QueryT, MutationT, SubscriptionT, S>,
113        context: &'a QueryT::Context,
114    ) -> GraphQLResponse<S>
115    where
116        QueryT: GraphQLTypeAsync<S>,
117        QueryT::TypeInfo: Sync,
118        QueryT::Context: Sync,
119        MutationT: GraphQLTypeAsync<S, Context = QueryT::Context>,
120        MutationT::TypeInfo: Sync,
121        SubscriptionT: GraphQLType<S, Context = QueryT::Context> + Sync,
122        SubscriptionT::TypeInfo: Sync,
123        S: ScalarValue + Send + Sync,
124    {
125        let op = self.operation_name.as_deref();
126        let vars = &self.variables();
127        let res = crate::execute(&self.query, op, root_node, vars, context).await;
128        GraphQLResponse(res)
129    }
130}
131
132/// Resolve a GraphQL subscription into `Value<ValuesStream<S>` using the
133/// specified schema and context.
134/// This is a wrapper around the `resolve_into_stream` function exposed at the top
135/// level of this crate.
136pub async fn resolve_into_stream<'req, 'rn, 'ctx, 'a, QueryT, MutationT, SubscriptionT, S>(
137    req: &'req GraphQLRequest<S>,
138    root_node: &'rn RootNode<QueryT, MutationT, SubscriptionT, S>,
139    context: &'ctx QueryT::Context,
140) -> Result<(Value<ValuesStream<'a, S>>, Vec<ExecutionError<S>>), GraphQLError>
141where
142    'req: 'a,
143    'rn: 'a,
144    'ctx: 'a,
145    QueryT: GraphQLTypeAsync<S>,
146    QueryT::TypeInfo: Sync,
147    QueryT::Context: Sync,
148    MutationT: GraphQLTypeAsync<S, Context = QueryT::Context>,
149    MutationT::TypeInfo: Sync,
150    SubscriptionT: GraphQLSubscriptionType<S, Context = QueryT::Context>,
151    SubscriptionT::TypeInfo: Sync,
152    S: ScalarValue + Send + Sync,
153{
154    let op = req.operation_name.as_deref();
155    let vars = req.variables();
156
157    crate::resolve_into_stream(&req.query, op, root_node, &vars, context).await
158}
159
160/// Simple wrapper around the result from executing a GraphQL query
161///
162/// This struct implements Serialize, so you can simply serialize this
163/// to JSON and send it over the wire. Use the `is_ok` method to determine
164/// whether to send a 200 or 400 HTTP status code.
165#[derive(Clone, Debug, PartialEq)]
166pub struct GraphQLResponse<S = DefaultScalarValue>(
167    Result<(Value<S>, Vec<ExecutionError<S>>), GraphQLError>,
168);
169
170impl<S> GraphQLResponse<S>
171where
172    S: ScalarValue,
173{
174    /// Constructs a new [`GraphQLResponse`] from the provided execution [`Result`].
175    #[must_use]
176    pub fn from_result(r: Result<(Value<S>, Vec<ExecutionError<S>>), GraphQLError>) -> Self {
177        Self(r)
178    }
179
180    /// Unwraps this [`GraphQLResponse`] into its underlying execution [`Result`].
181    pub fn into_result(self) -> Result<(Value<S>, Vec<ExecutionError<S>>), GraphQLError> {
182        self.0
183    }
184
185    /// Constructs an error [`GraphQLResponse`] outside the normal execution flow.
186    #[must_use]
187    pub fn error(error: FieldError<S>) -> Self {
188        Self(Ok((Value::null(), vec![ExecutionError::at_origin(error)])))
189    }
190
191    /// Indicates whether this [`GraphQLResponse`] contains a successful execution [`Result`].
192    ///
193    /// **NOTE**: There still might be errors in the response even though it's considered OK.
194    ///           This is by design in GraphQL.
195    #[must_use]
196    pub fn is_ok(&self) -> bool {
197        self.0.is_ok()
198    }
199}
200
201impl<T> Serialize for GraphQLResponse<T>
202where
203    T: Serialize + ScalarValue,
204    Value<T>: Serialize,
205    ExecutionError<T>: Serialize,
206{
207    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
208    where
209        S: ser::Serializer,
210    {
211        match self.0 {
212            Ok((ref res, ref err)) => {
213                let mut map = serializer.serialize_map(None)?;
214
215                map.serialize_key("data")?;
216                map.serialize_value(res)?;
217
218                if !err.is_empty() {
219                    map.serialize_key("errors")?;
220                    map.serialize_value(err)?;
221                }
222
223                map.end()
224            }
225            Err(ref err) => {
226                let mut map = serializer.serialize_map(Some(1))?;
227                map.serialize_key("errors")?;
228                map.serialize_value(err)?;
229                map.end()
230            }
231        }
232    }
233}
234
235/// Simple wrapper around GraphQLRequest to allow the handling of Batch requests.
236#[derive(Debug, Deserialize, PartialEq)]
237#[serde(untagged)]
238#[serde(bound = "InputValue<S>: Deserialize<'de>")]
239pub enum GraphQLBatchRequest<S = DefaultScalarValue>
240where
241    S: ScalarValue,
242{
243    /// A single operation request.
244    Single(GraphQLRequest<S>),
245
246    /// A batch operation request.
247    ///
248    /// Empty batch is considered as invalid value, so cannot be deserialized.
249    #[serde(deserialize_with = "deserialize_non_empty_batch")]
250    Batch(Vec<GraphQLRequest<S>>),
251}
252
253fn deserialize_non_empty_batch<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
254where
255    D: de::Deserializer<'de>,
256    T: Deserialize<'de>,
257{
258    use de::Error as _;
259
260    let v = Vec::<T>::deserialize(deserializer)?;
261    if v.is_empty() {
262        Err(D::Error::invalid_length(
263            0,
264            &"non-empty batch of GraphQL requests",
265        ))
266    } else {
267        Ok(v)
268    }
269}
270
271impl<S> GraphQLBatchRequest<S>
272where
273    S: ScalarValue,
274{
275    /// Execute a GraphQL batch request synchronously using the specified schema and context
276    ///
277    /// This is a simple wrapper around the `execute_sync` function exposed in GraphQLRequest.
278    pub fn execute_sync<'a, QueryT, MutationT, SubscriptionT>(
279        &'a self,
280        root_node: &'a RootNode<QueryT, MutationT, SubscriptionT, S>,
281        context: &QueryT::Context,
282    ) -> GraphQLBatchResponse<S>
283    where
284        QueryT: GraphQLType<S>,
285        MutationT: GraphQLType<S, Context = QueryT::Context>,
286        SubscriptionT: GraphQLType<S, Context = QueryT::Context>,
287    {
288        match *self {
289            Self::Single(ref req) => {
290                GraphQLBatchResponse::Single(req.execute_sync(root_node, context))
291            }
292            Self::Batch(ref reqs) => GraphQLBatchResponse::Batch(
293                reqs.iter()
294                    .map(|req| req.execute_sync(root_node, context))
295                    .collect(),
296            ),
297        }
298    }
299
300    /// Executes a GraphQL request using the specified schema and context
301    ///
302    /// This is a simple wrapper around the `execute` function exposed in
303    /// GraphQLRequest
304    pub async fn execute<'a, QueryT, MutationT, SubscriptionT>(
305        &'a self,
306        root_node: &'a RootNode<QueryT, MutationT, SubscriptionT, S>,
307        context: &'a QueryT::Context,
308    ) -> GraphQLBatchResponse<S>
309    where
310        QueryT: GraphQLTypeAsync<S>,
311        QueryT::TypeInfo: Sync,
312        QueryT::Context: Sync,
313        MutationT: GraphQLTypeAsync<S, Context = QueryT::Context>,
314        MutationT::TypeInfo: Sync,
315        SubscriptionT: GraphQLSubscriptionType<S, Context = QueryT::Context>,
316        SubscriptionT::TypeInfo: Sync,
317        S: Send + Sync,
318    {
319        match self {
320            Self::Single(req) => {
321                let resp = req.execute(root_node, context).await;
322                GraphQLBatchResponse::Single(resp)
323            }
324            Self::Batch(reqs) => {
325                let resps = futures::future::join_all(
326                    reqs.iter().map(|req| req.execute(root_node, context)),
327                )
328                .await;
329                GraphQLBatchResponse::Batch(resps)
330            }
331        }
332    }
333
334    /// The operation names of the request.
335    pub fn operation_names(&self) -> Vec<Option<&str>> {
336        match self {
337            Self::Single(req) => vec![req.operation_name.as_deref()],
338            Self::Batch(reqs) => reqs.iter().map(|r| r.operation_name.as_deref()).collect(),
339        }
340    }
341}
342
343/// Simple wrapper around the result (GraphQLResponse) from executing a GraphQLBatchRequest
344///
345/// This struct implements Serialize, so you can simply serialize this
346/// to JSON and send it over the wire. use the `is_ok` to determine
347/// wheter to send a 200 or 400 HTTP status code.
348#[derive(Serialize)]
349#[serde(untagged)]
350pub enum GraphQLBatchResponse<S = DefaultScalarValue>
351where
352    S: ScalarValue,
353{
354    /// Result of a single operation in a GraphQL request.
355    Single(GraphQLResponse<S>),
356    /// Result of a batch operation in a GraphQL request.
357    Batch(Vec<GraphQLResponse<S>>),
358}
359
360impl<S: ScalarValue> GraphQLBatchResponse<S> {
361    /// Returns if all the GraphQLResponse in this operation are ok,
362    /// you can use it to determine wheter to send a 200 or 400 HTTP status code.
363    pub fn is_ok(&self) -> bool {
364        match self {
365            Self::Single(resp) => resp.is_ok(),
366            Self::Batch(resps) => resps.iter().all(GraphQLResponse::is_ok),
367        }
368    }
369}
370
371#[cfg(feature = "expose-test-schema")]
372pub mod tests {
373    //! HTTP integration tests.
374
375    use std::time::Duration;
376
377    use serde_json::Value as Json;
378
379    /// Normalized response content we expect to get back from
380    /// the http framework integration we are testing.
381    #[derive(Debug)]
382    pub struct TestResponse {
383        /// Status code of the HTTP response.
384        pub status_code: i32,
385
386        /// Body of the HTTP response, if any.
387        pub body: Option<String>,
388
389        /// `Content-Type` header value of the HTTP response.
390        pub content_type: String,
391    }
392
393    /// Normalized way to make requests to the HTTP framework integration we are testing.
394    pub trait HttpIntegration {
395        /// Sends GET HTTP request to this integration with the provided `url` parameters string,
396        /// and returns response returned by this integration.
397        fn get(&self, url: &str) -> TestResponse;
398
399        /// Sends POST HTTP request to this integration with the provided JSON-encoded `body`, and
400        /// returns response returned by this integration.
401        fn post_json(&self, url: &str, body: &str) -> TestResponse;
402
403        /// Sends POST HTTP request to this integration with the provided raw GraphQL query as
404        /// `body`, and returns response returned by this integration.
405        fn post_graphql(&self, url: &str, body: &str) -> TestResponse;
406    }
407
408    /// Runs integration tests suite for the provided [`HttpIntegration`].
409    pub fn run_http_test_suite<T: HttpIntegration>(integration: &T) {
410        println!("Running HTTP Test suite for integration");
411
412        println!("  - test_simple_get");
413        test_simple_get(integration);
414
415        println!("  - test_encoded_get");
416        test_encoded_get(integration);
417
418        println!("  - test_get_with_variables");
419        test_get_with_variables(integration);
420
421        println!("  - test_post_with_variables");
422        test_post_with_variables(integration);
423
424        println!("  - test_simple_post");
425        test_simple_post(integration);
426
427        println!("  - test_batched_post");
428        test_batched_post(integration);
429
430        println!("  - test_empty_batched_post");
431        test_empty_batched_post(integration);
432
433        println!("  - test_invalid_json");
434        test_invalid_json(integration);
435
436        println!("  - test_invalid_field");
437        test_invalid_field(integration);
438
439        println!("  - test_duplicate_keys");
440        test_duplicate_keys(integration);
441
442        println!("  - test_graphql_post");
443        test_graphql_post(integration);
444
445        println!("  - test_invalid_graphql_post");
446        test_invalid_graphql_post(integration);
447    }
448
449    fn unwrap_json_response(response: &TestResponse) -> Json {
450        serde_json::from_str::<Json>(
451            response
452                .body
453                .as_ref()
454                .expect("No data returned from request"),
455        )
456        .expect("Could not parse JSON object")
457    }
458
459    fn test_simple_get<T: HttpIntegration>(integration: &T) {
460        // {hero{name}}
461        let response = integration.get("/?query=%7Bhero%7Bname%7D%7D");
462
463        assert_eq!(response.status_code, 200);
464        assert_eq!(response.content_type.as_str(), "application/json");
465
466        assert_eq!(
467            unwrap_json_response(&response),
468            serde_json::from_str::<Json>(r#"{"data": {"hero": {"name": "R2-D2"}}}"#)
469                .expect("Invalid JSON constant in test")
470        );
471    }
472
473    fn test_encoded_get<T: HttpIntegration>(integration: &T) {
474        // query { human(id: "1000") { id, name, appearsIn, homePlanet } }
475        let response = integration.get(
476            "/?query=query%20%7B%20human(id%3A%20%221000%22)%20%7B%20id%2C%20name%2C%20appearsIn%2C%20homePlanet%20%7D%20%7D");
477
478        assert_eq!(response.status_code, 200);
479        assert_eq!(response.content_type.as_str(), "application/json");
480
481        assert_eq!(
482            unwrap_json_response(&response),
483            serde_json::from_str::<Json>(
484                r#"{
485                    "data": {
486                        "human": {
487                            "appearsIn": [
488                                "NEW_HOPE",
489                                "EMPIRE",
490                                "JEDI"
491                                ],
492                                "homePlanet": "Tatooine",
493                                "name": "Luke Skywalker",
494                                "id": "1000"
495                            }
496                        }
497                    }"#
498            )
499            .expect("Invalid JSON constant in test")
500        );
501    }
502
503    fn test_get_with_variables<T: HttpIntegration>(integration: &T) {
504        // query($id: String!) { human(id: $id) { id, name, appearsIn, homePlanet } }
505        // with variables = { "id": "1000" }
506        let response = integration.get(
507            "/?query=query(%24id%3A%20String!)%20%7B%20human(id%3A%20%24id)%20%7B%20id%2C%20name%2C%20appearsIn%2C%20homePlanet%20%7D%20%7D&variables=%7B%20%22id%22%3A%20%221000%22%20%7D");
508
509        assert_eq!(response.status_code, 200);
510        assert_eq!(response.content_type, "application/json");
511
512        assert_eq!(
513            unwrap_json_response(&response),
514            serde_json::from_str::<Json>(
515                r#"{
516                    "data": {
517                        "human": {
518                            "appearsIn": [
519                                "NEW_HOPE",
520                                "EMPIRE",
521                                "JEDI"
522                            ],
523                            "homePlanet": "Tatooine",
524                            "name": "Luke Skywalker",
525                            "id": "1000"
526                        }
527                    }
528                }"#
529            )
530            .expect("Invalid JSON constant in test")
531        );
532    }
533
534    fn test_post_with_variables<T: HttpIntegration>(integration: &T) {
535        let response = integration.post_json(
536            "/",
537            r#"{
538                "query":
539                    "query($id: String!) { human(id: $id) { id, name, appearsIn, homePlanet } }",
540                "variables": {"id": "1000"}
541            }"#,
542        );
543
544        assert_eq!(response.status_code, 200);
545        assert_eq!(response.content_type, "application/json");
546
547        assert_eq!(
548            unwrap_json_response(&response),
549            serde_json::from_str::<Json>(
550                r#"{
551                    "data": {
552                        "human": {
553                            "appearsIn": [
554                                "NEW_HOPE",
555                                "EMPIRE",
556                                "JEDI"
557                            ],
558                            "homePlanet": "Tatooine",
559                            "name": "Luke Skywalker",
560                            "id": "1000"
561                        }
562                    }
563                }"#
564            )
565            .expect("Invalid JSON constant in test")
566        );
567    }
568
569    fn test_simple_post<T: HttpIntegration>(integration: &T) {
570        let response = integration.post_json("/", r#"{"query": "{hero{name}}"}"#);
571
572        assert_eq!(response.status_code, 200);
573        assert_eq!(response.content_type, "application/json");
574
575        assert_eq!(
576            unwrap_json_response(&response),
577            serde_json::from_str::<Json>(r#"{"data": {"hero": {"name": "R2-D2"}}}"#)
578                .expect("Invalid JSON constant in test"),
579        );
580    }
581
582    fn test_batched_post<T: HttpIntegration>(integration: &T) {
583        let response = integration.post_json(
584            "/",
585            r#"[{"query": "{hero{name}}"}, {"query": "{hero{name}}"}]"#,
586        );
587
588        assert_eq!(response.status_code, 200);
589        assert_eq!(response.content_type, "application/json");
590
591        assert_eq!(
592            unwrap_json_response(&response),
593            serde_json::from_str::<Json>(
594                r#"[{"data": {"hero": {"name": "R2-D2"}}}, {"data": {"hero": {"name": "R2-D2"}}}]"#,
595            )
596            .expect("Invalid JSON constant in test"),
597        );
598    }
599
600    fn test_empty_batched_post<T: HttpIntegration>(integration: &T) {
601        let response = integration.post_json("/", "[]");
602        assert_eq!(response.status_code, 400);
603    }
604
605    fn test_invalid_json<T: HttpIntegration>(integration: &T) {
606        let response = integration.get("/?query=blah");
607        assert_eq!(response.status_code, 400);
608        let response = integration.post_json("/", r#"blah"#);
609        assert_eq!(response.status_code, 400);
610    }
611
612    fn test_invalid_field<T: HttpIntegration>(integration: &T) {
613        // {hero{blah}}
614        let response = integration.get("/?query=%7Bhero%7Bblah%7D%7D");
615        assert_eq!(response.status_code, 400);
616        let response = integration.post_json("/", r#"{"query": "{hero{blah}}"}"#);
617        assert_eq!(response.status_code, 400);
618    }
619
620    fn test_duplicate_keys<T: HttpIntegration>(integration: &T) {
621        // {hero{name}}
622        let response = integration.get("/?query=%7B%22query%22%3A%20%22%7Bhero%7Bname%7D%7D%22%2C%20%22query%22%3A%20%22%7Bhero%7Bname%7D%7D%22%7D");
623        assert_eq!(response.status_code, 400);
624        let response =
625            integration.post_json("/", r#"{"query": "{hero{name}}", "query": "{hero{name}}"}"#);
626        assert_eq!(response.status_code, 400);
627    }
628
629    fn test_graphql_post<T: HttpIntegration>(integration: &T) {
630        let resp = integration.post_graphql("/", r#"{hero{name}}"#);
631
632        assert_eq!(resp.status_code, 200);
633        assert_eq!(resp.content_type, "application/json");
634
635        assert_eq!(
636            unwrap_json_response(&resp),
637            serde_json::from_str::<Json>(r#"{"data": {"hero": {"name": "R2-D2"}}}"#)
638                .expect("Invalid JSON constant in test"),
639        );
640    }
641
642    fn test_invalid_graphql_post<T: HttpIntegration>(integration: &T) {
643        let resp = integration.post_graphql("/", r#"{hero{name}"#);
644
645        assert_eq!(resp.status_code, 400);
646    }
647
648    /// Normalized way to make requests to the WebSocket framework integration we are testing.
649    pub trait WsIntegration {
650        /// Runs a test with the given messages
651        fn run(
652            &self,
653            messages: Vec<WsIntegrationMessage>,
654        ) -> impl Future<Output = Result<(), anyhow::Error>>;
655    }
656
657    /// WebSocket framework integration message.
658    pub enum WsIntegrationMessage {
659        /// Send a message through a WebSocket.
660        Send(Json),
661
662        /// Expects a message to come through a WebSocket, with the specified timeout.
663        Expect(Json, Duration),
664    }
665
666    /// Default value in milliseconds for how long to wait for an incoming WebSocket message.
667    pub const WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT: Duration = Duration::from_millis(100);
668
669    /// Integration tests for the [legacy `graphql-ws` GraphQL over WebSocket Protocol][old].
670    ///
671    /// [old]: https://github.com/apollographql/subscriptions-transport-ws/blob/v0.11.0/PROTOCOL.md
672    pub mod graphql_ws {
673        use serde_json::json;
674
675        use super::{WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT, WsIntegration, WsIntegrationMessage};
676
677        /// Runs integration tests suite for the [legacy `graphql-ws` GraphQL over WebSocket
678        /// Protocol][0].
679        ///
680        /// [0]:https://github.com/apollographql/subscriptions-transport-ws/blob/v0.11.0/PROTOCOL.md
681        pub async fn run_test_suite<T: WsIntegration>(integration: &T) {
682            println!("Running `graphql-ws` test suite for integration");
683
684            println!("  - graphql_ws::test_simple_subscription");
685            test_simple_subscription(integration).await;
686
687            println!("  - graphql_ws::test_invalid_json");
688            test_invalid_json(integration).await;
689
690            println!("  - graphql_ws::test_invalid_query");
691            test_invalid_query(integration).await;
692        }
693
694        async fn test_simple_subscription<T: WsIntegration>(integration: &T) {
695            let messages = vec![
696                WsIntegrationMessage::Send(json!({
697                    "type": "connection_init",
698                    "payload": {},
699                })),
700                WsIntegrationMessage::Expect(
701                    json!({"type": "connection_ack"}),
702                    WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
703                ),
704                WsIntegrationMessage::Expect(
705                    json!({"type": "ka"}),
706                    WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
707                ),
708                WsIntegrationMessage::Send(json!({
709                    "id": "1",
710                    "type": "start",
711                    "payload": {
712                        "variables": {},
713                        "extensions": {},
714                        "operationName": null,
715                        "query": "subscription { asyncHuman { id, name, homePlanet } }",
716                    },
717                })),
718                WsIntegrationMessage::Expect(
719                    json!({
720                        "type": "data",
721                        "id": "1",
722                        "payload": {
723                            "data": {
724                                "asyncHuman": {
725                                    "id": "1000",
726                                    "name": "Luke Skywalker",
727                                    "homePlanet": "Tatooine",
728                                },
729                            },
730                        },
731                    }),
732                    WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
733                ),
734            ];
735
736            integration.run(messages).await.unwrap();
737        }
738
739        async fn test_invalid_json<T: WsIntegration>(integration: &T) {
740            let messages = vec![
741                WsIntegrationMessage::Send(json!({"whatever": "invalid value"})),
742                WsIntegrationMessage::Expect(
743                    json!({
744                        "type": "connection_error",
745                        "payload": {
746                            "message": "`serde` error: missing field `type` at line 1 column 28",
747                        },
748                    }),
749                    WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
750                ),
751            ];
752
753            integration.run(messages).await.unwrap();
754        }
755
756        async fn test_invalid_query<T: WsIntegration>(integration: &T) {
757            let messages = vec![
758                WsIntegrationMessage::Send(json!({
759                    "type": "connection_init",
760                    "payload": {},
761                })),
762                WsIntegrationMessage::Expect(
763                    json!({"type": "connection_ack"}),
764                    WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
765                ),
766                WsIntegrationMessage::Expect(
767                    json!({"type": "ka"}),
768                    WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
769                ),
770                WsIntegrationMessage::Send(json!({
771                    "id": "1",
772                    "type": "start",
773                    "payload": {
774                        "variables": {},
775                        "extensions": {},
776                        "operationName": null,
777                        "query": "subscription { asyncHuman }",
778                    },
779                })),
780                WsIntegrationMessage::Expect(
781                    json!({
782                        "type": "error",
783                        "id": "1",
784                        "payload": [{
785                            "message": "Field \"asyncHuman\" of type \"Human!\" must have a selection \
786                                        of subfields. Did you mean \"asyncHuman { ... }\"?",
787                            "locations": [{
788                                "line": 1,
789                                "column": 16,
790                            }],
791                        }],
792                    }),
793                    WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
794                ),
795            ];
796
797            integration.run(messages).await.unwrap();
798        }
799    }
800
801    /// Integration tests for the [new `graphql-transport-ws` GraphQL over WebSocket Protocol][new].
802    ///
803    /// [new]: https://github.com/enisdenjo/graphql-ws/blob/v5.14.0/PROTOCOL.md
804    pub mod graphql_transport_ws {
805        use serde_json::json;
806
807        use super::{WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT, WsIntegration, WsIntegrationMessage};
808
809        /// Runs integration tests suite the [new `graphql-transport-ws` GraphQL over WebSocket
810        /// Protocol][new].
811        ///
812        /// [new]: https://github.com/enisdenjo/graphql-ws/blob/v5.14.0/PROTOCOL.md
813        pub async fn run_test_suite<T: WsIntegration>(integration: &T) {
814            println!("Running `graphql-transport-ws` test suite for integration");
815
816            println!("  - graphql_ws::test_simple_subscription");
817            test_simple_subscription(integration).await;
818
819            println!("  - graphql_ws::test_invalid_json");
820            test_invalid_json(integration).await;
821
822            println!("  - graphql_ws::test_invalid_query");
823            test_invalid_query(integration).await;
824        }
825
826        async fn test_simple_subscription<T: WsIntegration>(integration: &T) {
827            let messages = vec![
828                WsIntegrationMessage::Send(json!({
829                    "type": "connection_init",
830                    "payload": {},
831                })),
832                WsIntegrationMessage::Expect(
833                    json!({"type": "connection_ack"}),
834                    WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
835                ),
836                WsIntegrationMessage::Expect(
837                    json!({"type": "pong"}),
838                    WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
839                ),
840                WsIntegrationMessage::Send(json!({"type": "ping"})),
841                WsIntegrationMessage::Expect(
842                    json!({"type": "pong"}),
843                    WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
844                ),
845                WsIntegrationMessage::Send(json!({
846                    "id": "1",
847                    "type": "subscribe",
848                    "payload": {
849                        "variables": {},
850                        "extensions": {},
851                        "operationName": null,
852                        "query": "subscription { asyncHuman { id, name, homePlanet } }",
853                    },
854                })),
855                WsIntegrationMessage::Expect(
856                    json!({
857                        "id": "1",
858                        "type": "next",
859                        "payload": {
860                            "data": {
861                                "asyncHuman": {
862                                    "id": "1000",
863                                    "name": "Luke Skywalker",
864                                    "homePlanet": "Tatooine",
865                                },
866                            },
867                        },
868                    }),
869                    WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
870                ),
871            ];
872
873            integration.run(messages).await.unwrap();
874        }
875
876        async fn test_invalid_json<T: WsIntegration>(integration: &T) {
877            let messages = vec![
878                WsIntegrationMessage::Send(json!({"whatever": "invalid value"})),
879                WsIntegrationMessage::Expect(
880                    json!({
881                        "code": 4400,
882                        "description": "`serde` error: missing field `type` at line 1 column 28",
883                    }),
884                    WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
885                ),
886            ];
887
888            integration.run(messages).await.unwrap();
889        }
890
891        async fn test_invalid_query<T: WsIntegration>(integration: &T) {
892            let messages = vec![
893                WsIntegrationMessage::Send(json!({
894                    "type": "connection_init",
895                    "payload": {},
896                })),
897                WsIntegrationMessage::Expect(
898                    json!({"type": "connection_ack"}),
899                    WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
900                ),
901                WsIntegrationMessage::Expect(
902                    json!({"type": "pong"}),
903                    WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
904                ),
905                WsIntegrationMessage::Send(json!({"type": "ping"})),
906                WsIntegrationMessage::Expect(
907                    json!({"type": "pong"}),
908                    WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
909                ),
910                WsIntegrationMessage::Send(json!({
911                    "id": "1",
912                    "type": "subscribe",
913                    "payload": {
914                        "variables": {},
915                        "extensions": {},
916                        "operationName": null,
917                        "query": "subscription { asyncHuman }",
918                    },
919                })),
920                WsIntegrationMessage::Expect(
921                    json!({
922                        "type": "error",
923                        "id": "1",
924                        "payload": [{
925                            "message": "Field \"asyncHuman\" of type \"Human!\" must have a selection \
926                                        of subfields. Did you mean \"asyncHuman { ... }\"?",
927                            "locations": [{
928                                "line": 1,
929                                "column": 16,
930                            }],
931                        }],
932                    }),
933                    WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
934                ),
935            ];
936
937            integration.run(messages).await.unwrap();
938        }
939    }
940}