From a47d1c543056b47c4a6ec01946ee6bfbeaaa653f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jord=C3=A3o=20Rodrigues=20Oliveira=20Rosario?= Date: Tue, 21 Apr 2020 03:21:02 -0300 Subject: [PATCH] Add actix-web integration (#603) --- Cargo.toml | 1 + juniper/release.toml | 3 + juniper_actix/.gitignore | 4 + juniper_actix/CHANGELOG.md | 3 + juniper_actix/Cargo.toml | 33 ++ juniper_actix/LICENSE | 25 ++ juniper_actix/README.md | 34 ++ juniper_actix/examples/actix_server.rs | 64 +++ juniper_actix/src/lib.rs | 517 +++++++++++++++++++++++++ 9 files changed, 684 insertions(+) create mode 100644 juniper_actix/.gitignore create mode 100644 juniper_actix/CHANGELOG.md create mode 100644 juniper_actix/Cargo.toml create mode 100644 juniper_actix/LICENSE create mode 100644 juniper_actix/README.md create mode 100644 juniper_actix/examples/actix_server.rs create mode 100644 juniper_actix/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 4ed0d26f..a42be792 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "juniper_rocket_async", "juniper_subscriptions", "juniper_warp", + "juniper_actix", ] exclude = [ "docs/book/tests", diff --git a/juniper/release.toml b/juniper/release.toml index 8ef4cd80..72391149 100644 --- a/juniper/release.toml +++ b/juniper/release.toml @@ -30,4 +30,7 @@ pre-release-replacements = [ {file="../juniper_warp/Cargo.toml", search="\\[dev-dependencies\\.juniper\\]\nversion = \"[^\"]+\"", replace="[dev-dependencies.juniper]\nversion = \"{{version}}\""}, # Subscriptions {file="../juniper_subscriptions/Cargo.toml", search="juniper = \\{ version = \"[^\"]+\"", replace="juniper = { version = \"{{version}}\""}, + # Actix-Web + {file="../juniper_actix/Cargo.toml", search="juniper = \\{ version = \"[^\"]+\"", replace="juniper = { version = \"{{version}}\""}, + {file="../juniper_actix/Cargo.toml", search="\\[dev-dependencies\\.juniper\\]\nversion = \"[^\"]+\"", replace="[dev-dependencies.juniper]\nversion = \"{{version}}\""}, ] diff --git a/juniper_actix/.gitignore b/juniper_actix/.gitignore new file mode 100644 index 00000000..0d722487 --- /dev/null +++ b/juniper_actix/.gitignore @@ -0,0 +1,4 @@ +/target +/examples/**/target/**/* +**/*.rs.bk +Cargo.lock diff --git a/juniper_actix/CHANGELOG.md b/juniper_actix/CHANGELOG.md new file mode 100644 index 00000000..05232472 --- /dev/null +++ b/juniper_actix/CHANGELOG.md @@ -0,0 +1,3 @@ +# master + +- Initial Release diff --git a/juniper_actix/Cargo.toml b/juniper_actix/Cargo.toml new file mode 100644 index 00000000..5480cae9 --- /dev/null +++ b/juniper_actix/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "juniper_actix" +version = "0.1.0" +authors = ["Jordao Rosario "] +description = "Juniper GraphQL integration with Actix" +license = "BSD-2-Clause" +documentation = "https://docs.rs/juniper_actix" +repository = "https://github.com/graphql-rust/juniper" +edition = "2018" + + +[dependencies] +actix = "0.9.0" +actix-rt = "1.0.0" +actix-web = { version = "2.0.0", features = ["rustls"] } +actix-web-actors = "2.0.0" +futures = { version = "0.3.1", features = ["compat"] } +juniper = { version = "0.14.2", path = "../juniper", default-features = false } +tokio = { version = "0.2", features = ["time"] } +serde_json = "1.0.24" +serde_derive = "1.0.75" +failure = "0.1.7" +serde = "1.0.75" + +[dev-dependencies] +juniper = { version = "0.14.2", path = "../juniper", features = ["expose-test-schema", "serde_json"] } +env_logger = "0.5.11" +log = "0.4.3" +percent-encoding = "1.0" +tokio = { version = "0.2", features = ["rt-core", "macros", "blocking"] } +actix-cors = "0.2.0" +actix-identity = "0.2.0" +bytes = "0.5.4" diff --git a/juniper_actix/LICENSE b/juniper_actix/LICENSE new file mode 100644 index 00000000..6771d89f --- /dev/null +++ b/juniper_actix/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2018, Jordao Rosario +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/juniper_actix/README.md b/juniper_actix/README.md new file mode 100644 index 00000000..5f52cb98 --- /dev/null +++ b/juniper_actix/README.md @@ -0,0 +1,34 @@ +# juniper_actix + +This repository contains the [actix][actix] web server integration for +[Juniper][Juniper], a [GraphQL][GraphQL] implementation for Rust, its inspired and some parts are copied from [juniper_warp][juniper_warp]. + +## Documentation + +For documentation, including guides and examples, check out [Juniper][Juniper]. + +A basic usage example can also be found in the [API documentation][documentation]. + +## Examples + +Check [examples/actix_server][example] for example code of a working actix +server with GraphQL handlers. + +## Links + +* [Juniper][Juniper] +* [API Reference][documentation] +* [actix][actix] + +## License + +This project is under the BSD-2 license. + +Check the LICENSE file for details. + +[actix]: https://github.com/actix/actix-web +[Juniper]: https://github.com/graphql-rust/juniper +[GraphQL]: http://graphql.org +[documentation]: https://docs.rs/juniper_actix +[example]: https://github.com/graphql-rust/juniper/blob/master/juniper_actix/examples/actix_server.rs +[juniper_warp]:https://docs.rs/juniper_warp \ No newline at end of file diff --git a/juniper_actix/examples/actix_server.rs b/juniper_actix/examples/actix_server.rs new file mode 100644 index 00000000..daf63411 --- /dev/null +++ b/juniper_actix/examples/actix_server.rs @@ -0,0 +1,64 @@ +#![deny(warnings)] + +extern crate log; +use actix_cors::Cors; +use actix_web::{middleware, web, App, Error, HttpResponse, HttpServer}; +use juniper::{ + tests::{model::Database, schema::Query}, + EmptyMutation, EmptySubscription, RootNode, +}; +use juniper_actix::{ + graphiql_handler as gqli_handler, graphql_handler, playground_handler as play_handler, +}; + +type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription>; + +fn schema() -> Schema { + Schema::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + ) +} + +async fn graphiql_handler() -> Result { + gqli_handler("/", None).await +} +async fn playground_handler() -> Result { + play_handler("/", None).await +} +async fn graphql( + req: actix_web::HttpRequest, + payload: actix_web::web::Payload, + schema: web::Data, +) -> Result { + let context = Database::new(); + graphql_handler(&schema, &context, req, payload).await +} + +#[actix_rt::main] +async fn main() -> std::io::Result<()> { + ::std::env::set_var("RUST_LOG", "actix_web=info"); + env_logger::init(); + let server = HttpServer::new(move || { + App::new() + .data(schema()) + .wrap(middleware::Compress::default()) + .wrap(middleware::Logger::default()) + .wrap( + Cors::new() + .allowed_methods(vec!["POST", "GET"]) + .supports_credentials() + .max_age(3600) + .finish(), + ) + .service( + web::resource("/") + .route(web::post().to(graphql)) + .route(web::get().to(graphql)), + ) + .service(web::resource("/playground").route(web::get().to(playground_handler))) + .service(web::resource("/graphiql").route(web::get().to(graphiql_handler))) + }); + server.bind("127.0.0.1:8080").unwrap().run().await +} diff --git a/juniper_actix/src/lib.rs b/juniper_actix/src/lib.rs new file mode 100644 index 00000000..7e2241b7 --- /dev/null +++ b/juniper_actix/src/lib.rs @@ -0,0 +1,517 @@ +/*! + +# juniper_actix + +This repository contains the [actix][actix] web server integration for +[Juniper][Juniper], a [GraphQL][GraphQL] implementation for Rust, its inspired and some parts are copied from [juniper_warp][juniper_warp] + +## Documentation + +For documentation, including guides and examples, check out [Juniper][Juniper]. + +A basic usage example can also be found in the [API documentation][documentation]. + +## Examples + +Check [examples/actix_server][example] for example code of a working actix +server with GraphQL handlers. + +## Links + +* [Juniper][Juniper] +* [API Reference][documentation] +* [actix][actix] + +## License + +This project is under the BSD-2 license. + +Check the LICENSE file for details. + +[actix]: https://github.com/actix/actix-web +[Juniper]: https://github.com/graphql-rust/juniper +[GraphQL]: http://graphql.org +[documentation]: https://docs.rs/juniper_actix +[example]: https://github.com/graphql-rust/juniper/blob/master/juniper_actix/examples/actix_server.rs +[juniper_warp]: https://github.com/graphql-rust/juniper/juniper_warp +*/ + +#![deny(missing_docs)] +#![deny(warnings)] +#![doc(html_root_url = "https://docs.rs/juniper_actix/0.1.0")] + +// use futures::{FutureExt as _}; +use actix_web::{ + error::{ErrorBadRequest, ErrorMethodNotAllowed, ErrorUnsupportedMediaType}, + http::{header::CONTENT_TYPE, Method}, + web, Error, FromRequest, HttpRequest, HttpResponse, +}; +use juniper::{ + http::{ + graphiql::graphiql_source, playground::playground_source, GraphQLBatchRequest, + GraphQLRequest, + }, + ScalarValue, +}; +use serde::Deserialize; + +#[serde(deny_unknown_fields)] +#[derive(Deserialize, Clone, PartialEq, Debug)] +struct GetGraphQLRequest { + query: String, + #[serde(rename = "operationName")] + operation_name: Option, + variables: Option, +} + +impl From for GraphQLRequest +where + S: ScalarValue, +{ + fn from(get_req: GetGraphQLRequest) -> Self { + let GetGraphQLRequest { + query, + operation_name, + variables, + } = get_req; + let variables = match variables { + Some(variables) => Some(serde_json::from_str(&variables).unwrap()), + None => None, + }; + Self::new(query, operation_name, variables) + } +} + +/// Actix Web GraphQL Handler for GET and POST requests +pub async fn graphql_handler( + schema: &juniper::RootNode<'static, Query, Mutation, Subscription, S>, + context: &Context, + req: HttpRequest, + payload: actix_web::web::Payload, +) -> Result +where + S: ScalarValue + Send + Sync + 'static, + Context: Send + Sync + 'static, + Query: juniper::GraphQLTypeAsync + Send + Sync + 'static, + Query::TypeInfo: Send + Sync, + Mutation: juniper::GraphQLTypeAsync + Send + Sync + 'static, + Mutation::TypeInfo: Send + Sync, + Subscription: juniper::GraphQLSubscriptionType + Send + Sync + 'static, + Subscription::TypeInfo: Send + Sync, +{ + match *req.method() { + Method::POST => post_graphql_handler(schema, context, req, payload).await, + Method::GET => get_graphql_handler(schema, context, req).await, + _ => Err(ErrorMethodNotAllowed( + "GraphQL requests can only be sent with GET or POST", + )), + } +} +/// Actix GraphQL Handler for GET requests +pub async fn get_graphql_handler( + schema: &juniper::RootNode<'static, Query, Mutation, Subscription, S>, + context: &Context, + req: HttpRequest, +) -> Result +where + S: ScalarValue + Send + Sync + 'static, + Context: Send + Sync + 'static, + Query: juniper::GraphQLTypeAsync + Send + Sync + 'static, + Query::TypeInfo: Send + Sync, + Mutation: juniper::GraphQLTypeAsync + Send + Sync + 'static, + Mutation::TypeInfo: Send + Sync, + Subscription: juniper::GraphQLSubscriptionType + Send + Sync + 'static, + Subscription::TypeInfo: Send + Sync, +{ + let get_req = web::Query::::from_query(req.query_string())?; + let req = GraphQLRequest::from(get_req.into_inner()); + let gql_response = req.execute(schema, context).await; + let body_response = serde_json::to_string(&gql_response)?; + let response = match gql_response.is_ok() { + true => HttpResponse::Ok() + .content_type("application/json") + .body(body_response), + false => HttpResponse::BadRequest() + .content_type("application/json") + .body(body_response), + }; + Ok(response) +} + +/// Actix GraphQL Handler for POST requests +pub async fn post_graphql_handler( + schema: &juniper::RootNode<'static, Query, Mutation, Subscription, S>, + context: &Context, + req: HttpRequest, + payload: actix_web::web::Payload, +) -> Result +where + S: ScalarValue + Send + Sync + 'static, + Context: Send + Sync + 'static, + Query: juniper::GraphQLTypeAsync + Send + Sync + 'static, + Query::TypeInfo: Send + Sync, + Mutation: juniper::GraphQLTypeAsync + Send + Sync + 'static, + Mutation::TypeInfo: Send + Sync, + Subscription: juniper::GraphQLSubscriptionType + Send + Sync + 'static, + Subscription::TypeInfo: Send + Sync, +{ + let content_type_header = req + .headers() + .get(CONTENT_TYPE) + .and_then(|hv| hv.to_str().ok()); + let req = match content_type_header { + Some("application/json") | Some("application/graphql") => { + let body_string = String::from_request(&req, &mut payload.into_inner()).await; + let body_string = body_string?; + match serde_json::from_str::>(&body_string) { + Ok(req) => Ok(req), + Err(err) => Err(ErrorBadRequest(err)), + } + } + _ => Err(ErrorUnsupportedMediaType( + "GraphQL requests should have content type `application/json` or `application/graphql`", + )), + }?; + let gql_batch_response = req.execute(schema, context).await; + let gql_response = serde_json::to_string(&gql_batch_response)?; + let mut response = match gql_batch_response.is_ok() { + true => HttpResponse::Ok(), + false => HttpResponse::BadRequest(), + }; + Ok(response.content_type("application/json").body(gql_response)) +} + +/// Create a handler that replies with an HTML page containing GraphiQL. This does not handle routing, so you can mount it on any endpoint +/// +/// For example: +/// +/// ``` +/// # extern crate actix; +/// # extern crate juniper_actix; +/// # +/// # use juniper_actix::graphiql_handler; +/// # use actix_web::{web, App}; +/// +/// let app = App::new() +/// .route("/", web::get().to(|| graphiql_handler("/graphql", Some("/graphql/subscriptions")))); +/// ``` +#[allow(dead_code)] +pub async fn graphiql_handler( + graphql_endpoint_url: &str, + subscriptions_endpoint_url: Option<&'static str>, +) -> Result { + let html = graphiql_source(graphql_endpoint_url, subscriptions_endpoint_url); + Ok(HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(html)) +} + +/// Create a handler that replies with an HTML page containing GraphQL Playground. This does not handle routing, so you cant mount it on any endpoint. +pub async fn playground_handler( + graphql_endpoint_url: &str, + subscriptions_endpoint_url: Option<&'static str>, +) -> Result { + let html = playground_source(graphql_endpoint_url, subscriptions_endpoint_url); + Ok(HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(html)) +} + +#[cfg(test)] +mod tests { + use super::*; + use actix_web::{dev::ServiceResponse, http, http::header::CONTENT_TYPE, test, App}; + use futures::StreamExt; + use juniper::{ + http::tests::{run_http_test_suite, HTTPIntegration, TestResponse}, + tests::{model::Database, schema::Query}, + EmptyMutation, EmptySubscription, RootNode, + }; + + type Schema = + juniper::RootNode<'static, Query, EmptyMutation, EmptySubscription>; + + async fn take_response_body_string(resp: &mut ServiceResponse) -> String { + let (response_body, ..) = resp + .take_body() + .map(|body_out| body_out.unwrap().to_vec()) + .into_future() + .await; + match response_body { + Some(response_body) => String::from_utf8(response_body).unwrap(), + None => String::from(""), + } + } + + async fn index( + req: HttpRequest, + payload: actix_web::web::Payload, + schema: web::Data, + ) -> Result { + let context = Database::new(); + graphql_handler(&schema, &context, req, payload).await + } + + #[actix_rt::test] + async fn graphiql_response_does_not_panic() { + let result = graphiql_handler("/abcd", None).await; + assert!(result.is_ok()) + } + + #[actix_rt::test] + async fn graphiql_endpoint_matches() { + async fn graphql_handler() -> Result { + graphiql_handler("/abcd", None).await + } + let mut app = + test::init_service(App::new().route("/", web::get().to(graphql_handler))).await; + let req = test::TestRequest::get() + .uri("/") + .header("accept", "text/html") + .to_request(); + + let resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), http::StatusCode::OK); + } + + #[actix_rt::test] + async fn graphiql_endpoint_returns_graphiql_source() { + async fn graphql_handler() -> Result { + graphiql_handler("/dogs-api/graphql", Some("/dogs-api/subscriptions")).await + } + let mut app = + test::init_service(App::new().route("/", web::get().to(graphql_handler))).await; + let req = test::TestRequest::get() + .uri("/") + .header("accept", "text/html") + .to_request(); + + let mut resp = test::call_service(&mut app, req).await; + let body = take_response_body_string(&mut resp).await; + assert_eq!(resp.status(), http::StatusCode::OK); + assert_eq!( + resp.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap(), + "text/html; charset=utf-8" + ); + assert!(body.contains("")); + assert!(body.contains( + "" + )) + } + + #[actix_rt::test] + async fn playground_endpoint_matches() { + async fn graphql_handler() -> Result { + playground_handler("/abcd", None).await + } + let mut app = + test::init_service(App::new().route("/", web::get().to(graphql_handler))).await; + let req = test::TestRequest::get() + .uri("/") + .header("accept", "text/html") + .to_request(); + + let resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), http::StatusCode::OK); + } + + #[actix_rt::test] + async fn playground_endpoint_returns_playground_source() { + async fn graphql_handler() -> Result { + playground_handler("/dogs-api/graphql", Some("/dogs-api/subscriptions")).await + } + let mut app = + test::init_service(App::new().route("/", web::get().to(graphql_handler))).await; + let req = test::TestRequest::get() + .uri("/") + .header("accept", "text/html") + .to_request(); + + let mut resp = test::call_service(&mut app, req).await; + let body = take_response_body_string(&mut resp).await; + assert_eq!(resp.status(), http::StatusCode::OK); + assert_eq!( + resp.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap(), + "text/html; charset=utf-8" + ); + assert!(body.contains("GraphQLPlayground.init(root, { endpoint: '/dogs-api/graphql', subscriptionEndpoint: '/dogs-api/subscriptions' })")); + } + + #[actix_rt::test] + async fn graphql_post_works_json_post() { + let schema: Schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + ); + + let req = test::TestRequest::post() + .header("content-type", "application/json") + .set_payload( + r##"{ "variables": null, "query": "{ hero(episode: NEW_HOPE) { name } }" }"##, + ) + .uri("/") + .to_request(); + + let mut app = + test::init_service(App::new().data(schema).route("/", web::post().to(index))).await; + + let mut resp = test::call_service(&mut app, req).await; + + assert_eq!(resp.status(), http::StatusCode::OK); + assert_eq!( + take_response_body_string(&mut resp).await, + r#"{"data":{"hero":{"name":"R2-D2"}}}"# + ); + assert_eq!( + resp.headers().get("content-type").unwrap(), + "application/json", + ); + } + + #[actix_rt::test] + async fn graphql_get_works() { + let schema: Schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + ); + + let req = test::TestRequest::get() + .header("content-type", "application/json") + .uri("/?query=%7B%20hero%28episode%3A%20NEW_HOPE%29%20%7B%20name%20%7D%20%7D&variables=null") + .to_request(); + + let mut app = + test::init_service(App::new().data(schema).route("/", web::get().to(index))).await; + + let mut resp = test::call_service(&mut app, req).await; + + assert_eq!(resp.status(), http::StatusCode::OK); + assert_eq!( + take_response_body_string(&mut resp).await, + r#"{"data":{"hero":{"name":"R2-D2"}}}"# + ); + assert_eq!( + resp.headers().get("content-type").unwrap(), + "application/json", + ); + } + + #[actix_rt::test] + async fn batch_request_works() { + use juniper::{ + tests::{model::Database, schema::Query}, + EmptyMutation, EmptySubscription, RootNode, + }; + + let schema: Schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + ); + + let req = test::TestRequest::post() + .header("content-type", "application/json") + .set_payload( + r##"[ + { "variables": null, "query": "{ hero(episode: NEW_HOPE) { name } }" }, + { "variables": null, "query": "{ hero(episode: EMPIRE) { id name } }" } + ]"##, + ) + .uri("/") + .to_request(); + + let mut app = + test::init_service(App::new().data(schema).route("/", web::post().to(index))).await; + + let mut resp = test::call_service(&mut app, req).await; + + assert_eq!(resp.status(), http::StatusCode::OK); + assert_eq!( + take_response_body_string(&mut resp).await, + r#"[{"data":{"hero":{"name":"R2-D2"}}},{"data":{"hero":{"id":"1000","name":"Luke Skywalker"}}}]"# + ); + assert_eq!( + resp.headers().get("content-type").unwrap(), + "application/json", + ); + } + + #[test] + fn batch_request_deserialization_can_fail() { + let json = r#"blah"#; + let result: Result = serde_json::from_str(json); + + assert!(result.is_err()); + } + + pub struct TestActixWebIntegration {} + + impl HTTPIntegration for TestActixWebIntegration { + fn get(&self, url: &str) -> TestResponse { + let url = url.to_string(); + actix_rt::System::new("get_request").block_on(async move { + let schema: Schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + ); + let req = test::TestRequest::get() + .header("content-type", "application/json") + .uri(&url.clone()) + .to_request(); + + let mut app = + test::init_service(App::new().data(schema).route("/", web::get().to(index))) + .await; + + let resp = test::call_service(&mut app, req).await; + let test_response = make_test_response(resp).await; + test_response + }) + } + + fn post(&self, url: &str, body: &str) -> TestResponse { + let url = url.to_string(); + let body = body.to_string(); + actix_rt::System::new("post_request").block_on(async move { + let schema: Schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + ); + + let req = test::TestRequest::post() + .header("content-type", "application/json") + .set_payload(body) + .uri(&url.clone()) + .to_request(); + + let mut app = + test::init_service(App::new().data(schema).route("/", web::post().to(index))) + .await; + + let resp = test::call_service(&mut app, req).await; + let test_response = make_test_response(resp).await; + test_response + }) + } + } + + async fn make_test_response(mut response: ServiceResponse) -> TestResponse { + let body = take_response_body_string(&mut response).await; + let status_code = response.status().as_u16(); + let content_type = response.headers().get(CONTENT_TYPE).unwrap(); + TestResponse { + status_code: status_code as i32, + body: Some(body), + content_type: content_type.to_str().unwrap().to_string(), + } + } + + #[test] + fn test_actix_web_integration() { + run_http_test_suite(&TestActixWebIntegration {}); + } +}