diff --git a/book/src/servers/rocket.md b/book/src/servers/rocket.md index bf9768120..fe2f6cfbb 100644 --- a/book/src/servers/rocket.md +++ b/book/src/servers/rocket.md @@ -19,4 +19,4 @@ Included in the source is a [small example][example] which sets up a basic Graph [graphiql]: https://github.com/graphql/graphiql [rocket]: https://rocket.rs/ [juniper_rocket]: https://github.com/graphql-rust/juniper/tree/master/juniper_rocket -[example]: https://github.com/graphql-rust/juniper/blob/master/juniper_rocket/examples/rocket_server.rs +[example]: https://github.com/graphql-rust/juniper/blob/master/juniper_rocket/examples/simple.rs diff --git a/juniper_rocket/CHANGELOG.md b/juniper_rocket/CHANGELOG.md index 3f273a456..ba1a3db97 100644 --- a/juniper_rocket/CHANGELOG.md +++ b/juniper_rocket/CHANGELOG.md @@ -17,10 +17,15 @@ All user visible changes to `juniper_rocket` crate will be documented in this fi - `AsRef` and `AsMut` implementation for `GraphQLRequest` to its inner type. ([#968], [#930]) +### Changed + +- Made `subscriptions_endpoint_url` argument polymorphic in `graphiql_source()` and `playground_source()`. ([#1223]) + [#930]: /../../issues/930 [#968]: /../../pull/968 [#1205]: /../../pull/1205 [#1220]: /../../pull/1220 +[#1223]: /../../pull/1223 diff --git a/juniper_rocket/README.md b/juniper_rocket/README.md index 806baf244..0ea4276e9 100644 --- a/juniper_rocket/README.md +++ b/juniper_rocket/README.md @@ -24,7 +24,7 @@ A basic usage example can also be found in the [API docs][`juniper_rocket`]. ## Examples -Check [`examples/rocket_server.rs`][1] for example code of a working [`rocket`] server with [GraphQL] handlers. +Check [`examples/simple.rs`][1] for example code of a working [`rocket`] server with [GraphQL] handlers. @@ -43,4 +43,4 @@ This project is licensed under [BSD 2-Clause License](https://github.com/graphql [Juniper Book]: https://graphql-rust.github.io [Rust]: https://www.rust-lang.org -[1]: https://github.com/graphql-rust/juniper/blob/master/juniper_rocket/examples/rocket_server.rs +[1]: https://github.com/graphql-rust/juniper/blob/master/juniper_rocket/examples/simple.rs diff --git a/juniper_rocket/examples/rocket_server.rs b/juniper_rocket/examples/rocket_server.rs deleted file mode 100644 index a37464f65..000000000 --- a/juniper_rocket/examples/rocket_server.rs +++ /dev/null @@ -1,48 +0,0 @@ -use juniper::{ - tests::fixtures::starwars::schema::{Database, Query}, - EmptyMutation, EmptySubscription, RootNode, -}; -use rocket::{response::content, State}; - -type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription>; - -#[rocket::get("/")] -fn graphiql() -> content::RawHtml { - juniper_rocket::graphiql_source("/graphql", None) -} - -#[rocket::get("/graphql?")] -fn get_graphql_handler( - context: &State, - request: juniper_rocket::GraphQLRequest, - schema: &State, -) -> juniper_rocket::GraphQLResponse { - request.execute_sync(schema, context) -} - -#[rocket::post("/graphql", data = "")] -fn post_graphql_handler( - context: &State, - request: juniper_rocket::GraphQLRequest, - schema: &State, -) -> juniper_rocket::GraphQLResponse { - request.execute_sync(schema, context) -} - -#[rocket::main] -async fn main() { - let _ = rocket::build() - .manage(Database::new()) - .manage(Schema::new( - Query, - EmptyMutation::::new(), - EmptySubscription::::new(), - )) - .mount( - "/", - rocket::routes![graphiql, get_graphql_handler, post_graphql_handler], - ) - .launch() - .await - .expect("server to launch"); -} diff --git a/juniper_rocket/examples/simple.rs b/juniper_rocket/examples/simple.rs new file mode 100644 index 000000000..f3a6d905a --- /dev/null +++ b/juniper_rocket/examples/simple.rs @@ -0,0 +1,68 @@ +use juniper::{ + tests::fixtures::starwars::schema::{Database, Query}, + EmptyMutation, EmptySubscription, RootNode, +}; +use rocket::{response::content::RawHtml, routes, State}; + +type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription>; + +#[rocket::get("/")] +async fn homepage() -> RawHtml<&'static str> { + RawHtml( + "

juniper_rocket/simple example

\ +
visit GraphiQL
\ + \ + ", + ) +} + +#[rocket::get("/graphiql")] +fn graphiql() -> RawHtml { + juniper_rocket::graphiql_source("/graphql", None) +} + +#[rocket::get("/playground")] +fn playground() -> RawHtml { + juniper_rocket::playground_source("/graphql", None) +} + +// GET request accepts query parameters like these: +// ?query= +// &operationName= +// &variables= +// See details here: https://graphql.org/learn/serving-over-http#get-request +#[rocket::get("/graphql?")] +async fn get_graphql( + db: &State, + request: juniper_rocket::GraphQLRequest, + schema: &State, +) -> juniper_rocket::GraphQLResponse { + request.execute(schema, db).await +} + +#[rocket::post("/graphql", data = "")] +async fn post_graphql( + db: &State, + request: juniper_rocket::GraphQLRequest, + schema: &State, +) -> juniper_rocket::GraphQLResponse { + request.execute(schema, db).await +} + +#[rocket::main] +async fn main() { + _ = rocket::build() + .manage(Database::new()) + .manage(Schema::new( + Query, + EmptyMutation::new(), + EmptySubscription::new(), + )) + .mount( + "/", + routes![homepage, graphiql, playground, get_graphql, post_graphql], + ) + .launch() + .await + .expect("server to launch"); +} diff --git a/juniper_rocket/src/lib.rs b/juniper_rocket/src/lib.rs index 436b9a9f3..934047d17 100644 --- a/juniper_rocket/src/lib.rs +++ b/juniper_rocket/src/lib.rs @@ -7,7 +7,7 @@ use rocket::{ form::{error::ErrorKind, DataField, Error, Errors, FromForm, Options, ValueField}, http::{ContentType, Status}, outcome::Outcome, - response::{self, content, Responder, Response}, + response::{self, content::RawHtml, Responder, Response}, Data, Request, }; @@ -17,11 +17,50 @@ use juniper::{ InputValue, RootNode, ScalarValue, }; -/// Simple wrapper around an incoming GraphQL request +/// Simple wrapper around an incoming GraphQL request. /// -/// See the `http` module for more information. This type can be constructed -/// automatically from both GET and POST routes by implementing the `FromForm` -/// and `FromData` traits. +/// See the [`http`] module for more information. This type can be constructed automatically from +/// both GET and POST routes, as implements [`FromForm`] and [`FromData`] traits. +/// +/// # Example +/// +/// ```rust +/// use juniper::{ +/// tests::fixtures::starwars::schema::{Database, Query}, +/// EmptyMutation, EmptySubscription, RootNode, +/// }; +/// use rocket::{routes, State}; +/// +/// type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription>; +/// +/// // GET request accepts query parameters like these: +/// // ?query= +/// // &operationName= +/// // &variables= +/// // See details here: https://graphql.org/learn/serving-over-http#get-request +/// #[rocket::get("/graphql?")] +/// async fn get_graphql_handler( +/// db: &State, +/// request: juniper_rocket::GraphQLRequest, +/// schema: &State, +/// ) -> juniper_rocket::GraphQLResponse { +/// request.execute(schema, db).await +/// } +/// +/// #[rocket::post("/graphql", data = "")] +/// async fn post_graphql_handler( +/// db: &State, +/// request: juniper_rocket::GraphQLRequest, +/// schema: &State, +/// ) -> juniper_rocket::GraphQLResponse { +/// request.execute(schema, db).await +/// } +/// +/// let rocket = rocket::build() +/// .manage(Database::new()) +/// .manage(Schema::new(Query, EmptyMutation::new(), EmptySubscription::new())) +/// .mount("/", routes![get_graphql_handler, post_graphql_handler]); +/// ``` #[derive(Debug, PartialEq)] pub struct GraphQLRequest(GraphQLBatchRequest) where @@ -42,25 +81,59 @@ impl AsMut> for GraphQLRequest { /// Simple wrapper around the result of executing a GraphQL query pub struct GraphQLResponse(pub Status, pub String); -/// Generate an HTML page containing GraphiQL -pub fn graphiql_source( +/// Generates a [`RawHtml`] page containing [GraphiQL]. +/// +/// This does not handle routing, so you can mount it on any endpoint. +/// +/// # Example +/// +/// ```rust +/// use rocket::{response::content::RawHtml, routes}; +/// +/// #[rocket::get("/graphiql")] +/// fn graphiql() -> RawHtml { +/// juniper_rocket::graphiql_source("/graphql", "/subscriptions") +/// } +/// +/// let rocket = rocket::build().mount("/", routes![graphiql]); +/// ``` +/// +/// [GraphiQL]: https://github.com/graphql/graphiql +pub fn graphiql_source<'a>( graphql_endpoint_url: &str, - subscriptions_endpoint_url: Option<&str>, -) -> content::RawHtml { - content::RawHtml(juniper::http::graphiql::graphiql_source( + subscriptions_endpoint_url: impl Into>, +) -> RawHtml { + RawHtml(http::graphiql::graphiql_source( graphql_endpoint_url, - subscriptions_endpoint_url, + subscriptions_endpoint_url.into(), )) } -/// Generate an HTML page containing GraphQL Playground -pub fn playground_source( +/// Generates a [`RawHtml`] page containing [GraphQL Playground]. +/// +/// This does not handle routing, so you can mount it on any endpoint. +/// +/// # Example +/// +/// ```rust +/// use rocket::{response::content::RawHtml, routes}; +/// +/// #[rocket::get("/playground")] +/// fn playground() -> RawHtml { +/// juniper_rocket::playground_source("/graphql", "/subscriptions") +/// } +/// +/// let rocket = rocket::build().mount("/", routes![playground]); +/// ``` +/// +/// [GraphQL Playground]: https://github.com/prisma/graphql-playground +pub fn playground_source<'a>( graphql_endpoint_url: &str, - subscriptions_endpoint_url: Option<&str>, -) -> content::RawHtml { - content::RawHtml(juniper::http::playground::playground_source( + subscriptions_endpoint_url: impl Into>, +) -> RawHtml { + RawHtml(http::playground::playground_source( graphql_endpoint_url, - subscriptions_endpoint_url, + subscriptions_endpoint_url.into(), )) } @@ -126,7 +199,7 @@ where } impl GraphQLResponse { - /// Constructs an error response outside of the normal execution flow + /// Constructs an error response outside of the normal execution flow. /// /// # Examples /// @@ -194,7 +267,7 @@ impl<'f, S: ScalarValue> GraphQLContext<'f, S> { fn operation_name(&mut self, value: String) { if self.operation_name.is_some() { - let error = Error::from(ErrorKind::Duplicate).with_name("operation_name"); + let error = Error::from(ErrorKind::Duplicate).with_name("operationName"); self.errors.push(error) } else { @@ -243,7 +316,7 @@ where fn push_value(ctx: &mut Self::Context, field: ValueField<'f>) { match field.name.key().map(|key| key.as_str()) { Some("query") => ctx.query(field.value.into()), - Some("operation_name") => ctx.operation_name(field.value.into()), + Some("operation_name" | "operationName") => ctx.operation_name(field.value.into()), Some("variables") => ctx.variables(field.value.into()), Some(key) => { if ctx.opts.strict { @@ -348,30 +421,39 @@ impl<'r, 'o: 'r> Responder<'r, 'o> for GraphQLResponse { #[cfg(test)] mod fromform_tests { - use super::*; - use juniper::InputValue; + use std::borrow::Cow; + + use juniper::{http, InputValue}; use rocket::{ - form::{error::ErrorKind, Error, Form, Strict}, + form::{error::ErrorKind, Error, Errors, Form, Strict}, http::RawStr, }; - use std::borrow::Cow; + + use super::GraphQLRequest; fn check_error(input: &str, expected_errors: Vec, strict: bool) { let errors = if strict { - let result = Form::>::parse_encoded(RawStr::new(input)); - assert!(result.is_err()); - result.unwrap_err() + let res = Form::>::parse_encoded(RawStr::new(input)); + + assert!(res.is_err(), "result: {:#?}", res.unwrap()); + + res.unwrap_err() } else { - let result = Form::::parse_encoded(RawStr::new(input)); - assert!(result.is_err()); - result.unwrap_err() + let res = Form::::parse_encoded(RawStr::new(input)); + + assert!(res.is_err(), "result: {:#?}", res.unwrap()); + + res.unwrap_err() }; + assert_eq!(errors.len(), expected_errors.len()); + for (error, expected) in errors.iter().zip(&expected_errors) { match (&error.kind, &expected.kind) { (ErrorKind::Unknown, ErrorKind::Unknown) => (), (kind_a, kind_b) => assert_eq!(kind_a, kind_b), }; + assert_eq!(error.name, expected.name); assert_eq!(error.value, expected.value); assert_eq!(error.entity, expected.entity); @@ -379,7 +461,7 @@ mod fromform_tests { } #[test] - fn test_empty_form() { + fn empty_form() { check_error( "", vec![Error::from(ErrorKind::Missing).with_name("query")], @@ -388,7 +470,7 @@ mod fromform_tests { } #[test] - fn test_no_query() { + fn no_query() { check_error( "operation_name=foo&variables={}", vec![Error::from(ErrorKind::Missing).with_name("query")], @@ -397,7 +479,7 @@ mod fromform_tests { } #[test] - fn test_strict() { + fn strict() { check_error( "query=test&foo=bar", vec![Error::from(ErrorKind::Unknown).with_name("foo")], @@ -406,7 +488,7 @@ mod fromform_tests { } #[test] - fn test_duplicate_query() { + fn duplicate_query() { check_error( "query=foo&query=bar", vec![Error::from(ErrorKind::Duplicate).with_name("query")], @@ -415,16 +497,16 @@ mod fromform_tests { } #[test] - fn test_duplicate_operation_name() { + fn duplicate_operation_name() { check_error( - "query=test&operation_name=op1&operation_name=op2", - vec![Error::from(ErrorKind::Duplicate).with_name("operation_name")], + "query=test&operationName=op1&operationName=op2", + vec![Error::from(ErrorKind::Duplicate).with_name("operationName")], false, ); } #[test] - fn test_duplicate_variables() { + fn duplicate_variables() { check_error( "query=test&variables={}&variables={}", vec![Error::from(ErrorKind::Duplicate).with_name("variables")], @@ -433,7 +515,7 @@ mod fromform_tests { } #[test] - fn test_variables_invalid_json() { + fn variables_invalid_json() { check_error( "query=test&variables=NOT_JSON", vec![Error::from(ErrorKind::Validation(Cow::Owned( @@ -445,174 +527,45 @@ mod fromform_tests { } #[test] - fn test_variables_valid_json() { + fn variables_valid_json() { let result: Result = Form::parse_encoded(RawStr::new(r#"query=test&variables={"foo":"bar"}"#)); + assert!(result.is_ok()); + let variables = ::serde_json::from_str::(r#"{"foo":"bar"}"#).unwrap(); - let expected = GraphQLRequest(GraphQLBatchRequest::Single(http::GraphQLRequest::new( - "test".into(), - None, - Some(variables), - ))); + let expected = GraphQLRequest(http::GraphQLBatchRequest::Single( + http::GraphQLRequest::new("test".into(), None, Some(variables)), + )); + assert_eq!(result.unwrap(), expected); } #[test] - fn test_variables_encoded_json() { + fn variables_encoded_json() { let result: Result = Form::parse_encoded(RawStr::new( r#"query=test&variables={"foo":"x%20y%26%3F+z"}"#, )); let variables = ::serde_json::from_str::(r#"{"foo":"x y&? z"}"#).unwrap(); - let expected = GraphQLRequest(GraphQLBatchRequest::Single(http::GraphQLRequest::new( - "test".into(), - None, - Some(variables), - ))); + let expected = GraphQLRequest(http::GraphQLBatchRequest::Single( + http::GraphQLRequest::new("test".into(), None, Some(variables)), + )); + assert_eq!(result.unwrap(), expected); } #[test] - fn test_url_decode() { + fn url_decode() { let result: Result = Form::parse_encoded(RawStr::new( - "query=%25foo%20bar+baz%26%3F&operation_name=test", + "query=%25foo%20bar+baz%26%3F&operationName=test", )); - assert!(result.is_ok()); - let expected = GraphQLRequest(GraphQLBatchRequest::Single(http::GraphQLRequest::new( - "%foo bar baz&?".into(), - Some("test".into()), - None, - ))); - assert_eq!(result.unwrap(), expected); - } -} - -#[cfg(test)] -mod tests { - - use juniper::{ - http::tests as http_tests, - tests::fixtures::starwars::schema::{Database, Query}, - EmptyMutation, EmptySubscription, RootNode, - }; - use rocket::{ - self, get, - http::ContentType, - local::asynchronous::{Client, LocalResponse}, - post, routes, Build, Rocket, State, - }; - - type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription>; - - #[get("/?")] - fn get_graphql_handler( - context: &State, - request: super::GraphQLRequest, - schema: &State, - ) -> super::GraphQLResponse { - request.execute_sync(schema, context) - } - #[post("/", data = "")] - fn post_graphql_handler( - context: &State, - request: super::GraphQLRequest, - schema: &State, - ) -> super::GraphQLResponse { - request.execute_sync(schema, context) - } - - struct TestRocketIntegration { - client: Client, - } - - impl http_tests::HttpIntegration for TestRocketIntegration { - fn get(&self, url: &str) -> http_tests::TestResponse { - let req = self.client.get(url); - let req = futures::executor::block_on(req.dispatch()); - futures::executor::block_on(make_test_response(req)) - } - - fn post_json(&self, url: &str, body: &str) -> http_tests::TestResponse { - let req = self.client.post(url).header(ContentType::JSON).body(body); - let req = futures::executor::block_on(req.dispatch()); - futures::executor::block_on(make_test_response(req)) - } - - fn post_graphql(&self, url: &str, body: &str) -> http_tests::TestResponse { - let req = self - .client - .post(url) - .header(ContentType::new("application", "graphql")) - .body(body); - let req = futures::executor::block_on(req.dispatch()); - futures::executor::block_on(make_test_response(req)) - } - } - - #[rocket::async_test] - async fn test_rocket_integration() { - let rocket = make_rocket(); - let client = Client::untracked(rocket).await.expect("valid rocket"); - let integration = TestRocketIntegration { client }; - - http_tests::run_http_test_suite(&integration); - } - - #[rocket::async_test] - async fn test_operation_names() { - #[post("/", data = "")] - fn post_graphql_assert_operation_name_handler( - context: &State, - request: super::GraphQLRequest, - schema: &State, - ) -> super::GraphQLResponse { - assert_eq!(request.operation_names(), vec![Some("TestQuery")]); - request.execute_sync(schema, context) - } - - let rocket = make_rocket_without_routes() - .mount("/", routes![post_graphql_assert_operation_name_handler]); - let client = Client::untracked(rocket).await.expect("valid rocket"); - - let resp = client - .post("/") - .header(ContentType::JSON) - .body(r#"{"query": "query TestQuery {hero{name}}", "operationName": "TestQuery"}"#) - .dispatch() - .await; - let resp = make_test_response(resp); - - assert_eq!(resp.await.status_code, 200); - } - - fn make_rocket() -> Rocket { - make_rocket_without_routes().mount("/", routes![post_graphql_handler, get_graphql_handler]) - } + assert!(result.is_ok()); - fn make_rocket_without_routes() -> Rocket { - Rocket::build().manage(Database::new()).manage(Schema::new( - Query, - EmptyMutation::::new(), - EmptySubscription::::new(), - )) - } + let expected = GraphQLRequest(http::GraphQLBatchRequest::Single( + http::GraphQLRequest::new("%foo bar baz&?".into(), Some("test".into()), None), + )); - async fn make_test_response(response: LocalResponse<'_>) -> http_tests::TestResponse { - let status_code = response.status().code as i32; - let content_type = response - .content_type() - .expect("No content type header from handler") - .to_string(); - let body = response - .into_string() - .await - .expect("No body returned from GraphQL handler"); - - http_tests::TestResponse { - status_code, - body: Some(body), - content_type, - } + assert_eq!(result.unwrap(), expected); } } diff --git a/juniper_rocket/tests/http_test_suite.rs b/juniper_rocket/tests/http_test_suite.rs new file mode 100644 index 000000000..3e7ca823d --- /dev/null +++ b/juniper_rocket/tests/http_test_suite.rs @@ -0,0 +1,155 @@ +use futures::executor; +use juniper::{ + http::tests::{run_http_test_suite, HttpIntegration, TestResponse}, + tests::fixtures::starwars::schema::{Database, Query}, + EmptyMutation, EmptySubscription, RootNode, +}; +use juniper_rocket::{GraphQLRequest, GraphQLResponse}; +use rocket::{ + get, + http::ContentType, + local::asynchronous::{Client, LocalResponse}, + post, routes, Build, Rocket, State, +}; + +type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription>; + +fn bootstrap_rocket() -> Rocket { + Rocket::build().manage(Database::new()).manage(Schema::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + )) +} + +fn make_rocket() -> Rocket { + #[get("/?")] + async fn get_handler( + context: &State, + request: GraphQLRequest, + schema: &State, + ) -> GraphQLResponse { + request.execute(schema, context).await + } + + #[post("/", data = "")] + async fn post_handler( + context: &State, + request: GraphQLRequest, + schema: &State, + ) -> GraphQLResponse { + request.execute(schema, context).await + } + + bootstrap_rocket().mount("/", routes![post_handler, get_handler]) +} + +fn make_sync_rocket() -> Rocket { + #[get("/?")] + fn get_handler_sync( + context: &State, + request: GraphQLRequest, + schema: &State, + ) -> GraphQLResponse { + request.execute_sync(schema, context) + } + + #[post("/", data = "")] + fn post_handler_sync( + context: &State, + request: GraphQLRequest, + schema: &State, + ) -> GraphQLResponse { + request.execute_sync(schema, context) + } + + bootstrap_rocket().mount("/", routes![post_handler_sync, get_handler_sync]) +} + +struct TestRocketIntegration { + client: Client, +} + +async fn into_test_response(response: LocalResponse<'_>) -> TestResponse { + let status_code = response.status().code as i32; + let content_type = response + .content_type() + .expect("no `Content-Type` header from handler") + .to_string(); + let body = response + .into_string() + .await + .expect("no body returned from GraphQL handler"); + + TestResponse { + status_code, + content_type, + body: Some(body), + } +} + +impl HttpIntegration for TestRocketIntegration { + fn get(&self, url: &str) -> TestResponse { + let req = self.client.get(url); + let resp = executor::block_on(req.dispatch()); + executor::block_on(into_test_response(resp)) + } + + fn post_json(&self, url: &str, body: &str) -> TestResponse { + let req = self.client.post(url).header(ContentType::JSON).body(body); + let resp = executor::block_on(req.dispatch()); + executor::block_on(into_test_response(resp)) + } + + fn post_graphql(&self, url: &str, body: &str) -> TestResponse { + let req = self + .client + .post(url) + .header(ContentType::new("application", "graphql")) + .body(body); + let resp = executor::block_on(req.dispatch()); + executor::block_on(into_test_response(resp)) + } +} + +#[rocket::async_test] +async fn test_rocket_integration() { + let rocket = make_rocket(); + let client = Client::untracked(rocket).await.expect("valid rocket"); + + run_http_test_suite(&TestRocketIntegration { client }); +} + +#[rocket::async_test] +async fn test_sync_rocket_integration() { + let rocket = make_sync_rocket(); + let client = Client::untracked(rocket).await.expect("valid rocket"); + + run_http_test_suite(&TestRocketIntegration { client }); +} + +#[rocket::async_test] +async fn test_operation_names() { + #[post("/", data = "")] + async fn post_graphql_assert_operation_name_handler( + context: &State, + request: GraphQLRequest, + schema: &State, + ) -> GraphQLResponse { + assert_eq!(request.operation_names(), vec![Some("TestQuery")]); + request.execute(schema, context).await + } + + let rocket = bootstrap_rocket().mount("/", routes![post_graphql_assert_operation_name_handler]); + let client = Client::untracked(rocket).await.expect("valid rocket"); + + let resp = client + .post("/") + .header(ContentType::JSON) + .body(r#"{"query": "query TestQuery {hero{name}}", "operationName": "TestQuery"}"#) + .dispatch() + .await; + let resp = into_test_response(resp).await; + + assert_eq!(resp.status_code, 200, "response: {resp:#?}"); +}