diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 79404258..8d6f2d09 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -67,6 +67,10 @@ See [#618](https://github.com/graphql-rust/juniper/pull/618). `#[graphql(arguments(argA(name = "test")))]` (see [#631](https://github.com/graphql-rust/juniper/pull/631)) +- Integration tests: + Rename `http::tests::HTTPIntegration` as `http::tests::HttpIntegration` + and add support for `application/graphql` POST request. + # [[0.14.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper-0.14.2) - Fix incorrect validation with non-executed operations [#455](https://github.com/graphql-rust/juniper/issues/455) diff --git a/juniper/src/http/mod.rs b/juniper/src/http/mod.rs index 79e3d8d8..ff829d67 100644 --- a/juniper/src/http/mod.rs +++ b/juniper/src/http/mod.rs @@ -367,15 +367,23 @@ pub mod tests { pub content_type: String, } - /// Normalized way to make requests to the http framework - /// integration we are testing. - pub trait HTTPIntegration { + /// Normalized way to make requests to the HTTP framework integration we are testing. + pub trait HttpIntegration { + /// Sends GET HTTP request to this integration with the provided `url` parameters string, + /// and returns response returned by this integration. fn get(&self, url: &str) -> TestResponse; - fn post(&self, url: &str, body: &str) -> TestResponse; + + /// Sends POST HTTP request to this integration with the provided JSON-encoded `body`, and + /// returns response returned by this integration. + fn post_json(&self, url: &str, body: &str) -> TestResponse; + + /// Sends POST HTTP request to this integration with the provided raw GraphQL query as + /// `body`, and returns response returned by this integration. + fn post_graphql(&self, url: &str, body: &str) -> TestResponse; } #[allow(missing_docs)] - pub fn run_http_test_suite(integration: &T) { + pub fn run_http_test_suite(integration: &T) { println!("Running HTTP Test suite for integration"); println!(" - test_simple_get"); @@ -404,6 +412,12 @@ pub mod tests { println!(" - test_duplicate_keys"); test_duplicate_keys(integration); + + println!(" - test_graphql_post"); + test_graphql_post(integration); + + println!(" - test_invalid_graphql_post"); + test_invalid_graphql_post(integration); } fn unwrap_json_response(response: &TestResponse) -> Json { @@ -416,7 +430,7 @@ pub mod tests { .expect("Could not parse JSON object") } - fn test_simple_get(integration: &T) { + fn test_simple_get(integration: &T) { // {hero{name}} let response = integration.get("/?query=%7Bhero%7Bname%7D%7D"); @@ -430,7 +444,7 @@ pub mod tests { ); } - fn test_encoded_get(integration: &T) { + fn test_encoded_get(integration: &T) { // query { human(id: "1000") { id, name, appearsIn, homePlanet } } let response = integration.get( "/?query=query%20%7B%20human(id%3A%20%221000%22)%20%7B%20id%2C%20name%2C%20appearsIn%2C%20homePlanet%20%7D%20%7D"); @@ -460,7 +474,7 @@ pub mod tests { ); } - fn test_get_with_variables(integration: &T) { + fn test_get_with_variables(integration: &T) { // query($id: String!) { human(id: $id) { id, name, appearsIn, homePlanet } } // with variables = { "id": "1000" } let response = integration.get( @@ -491,8 +505,8 @@ pub mod tests { ); } - fn test_simple_post(integration: &T) { - let response = integration.post("/", r#"{"query": "{hero{name}}"}"#); + fn test_simple_post(integration: &T) { + let response = integration.post_json("/", r#"{"query": "{hero{name}}"}"#); assert_eq!(response.status_code, 200); assert_eq!(response.content_type, "application/json"); @@ -500,12 +514,12 @@ pub mod tests { assert_eq!( unwrap_json_response(&response), serde_json::from_str::(r#"{"data": {"hero": {"name": "R2-D2"}}}"#) - .expect("Invalid JSON constant in test") + .expect("Invalid JSON constant in test"), ); } - fn test_batched_post(integration: &T) { - let response = integration.post( + fn test_batched_post(integration: &T) { + let response = integration.post_json( "/", r#"[{"query": "{hero{name}}"}, {"query": "{hero{name}}"}]"#, ); @@ -516,42 +530,57 @@ pub mod tests { assert_eq!( unwrap_json_response(&response), serde_json::from_str::( - r#"[{"data": {"hero": {"name": "R2-D2"}}}, {"data": {"hero": {"name": "R2-D2"}}}]"# + r#"[{"data": {"hero": {"name": "R2-D2"}}}, {"data": {"hero": {"name": "R2-D2"}}}]"#, ) - .expect("Invalid JSON constant in test") + .expect("Invalid JSON constant in test"), ); } - fn test_empty_batched_post(integration: &T) { - let response = integration.post("/", "[]"); + fn test_empty_batched_post(integration: &T) { + let response = integration.post_json("/", "[]"); assert_eq!(response.status_code, 400); } - fn test_invalid_json(integration: &T) { + fn test_invalid_json(integration: &T) { let response = integration.get("/?query=blah"); assert_eq!(response.status_code, 400); - let response = integration.post("/", r#"blah"#); + let response = integration.post_json("/", r#"blah"#); assert_eq!(response.status_code, 400); } - fn test_invalid_field(integration: &T) { + fn test_invalid_field(integration: &T) { // {hero{blah}} let response = integration.get("/?query=%7Bhero%7Bblah%7D%7D"); assert_eq!(response.status_code, 400); - let response = integration.post("/", r#"{"query": "{hero{blah}}"}"#); + let response = integration.post_json("/", r#"{"query": "{hero{blah}}"}"#); assert_eq!(response.status_code, 400); } - fn test_duplicate_keys(integration: &T) { + fn test_duplicate_keys(integration: &T) { // {hero{name}} 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"); assert_eq!(response.status_code, 400); - let response = integration.post( - "/", - r#" - {"query": "{hero{name}}", "query": "{hero{name}}"} - "#, - ); + let response = + integration.post_json("/", r#"{"query": "{hero{name}}", "query": "{hero{name}}"}"#); assert_eq!(response.status_code, 400); } + + fn test_graphql_post(integration: &T) { + let resp = integration.post_graphql("/", r#"{hero{name}}"#); + + assert_eq!(resp.status_code, 200); + assert_eq!(resp.content_type, "application/json"); + + assert_eq!( + unwrap_json_response(&resp), + serde_json::from_str::(r#"{"data": {"hero": {"name": "R2-D2"}}}"#) + .expect("Invalid JSON constant in test"), + ); + } + + fn test_invalid_graphql_post(integration: &T) { + let resp = integration.post_graphql("/", r#"{hero{name}"#); + + assert_eq!(resp.status_code, 400); + } } diff --git a/juniper_actix/src/lib.rs b/juniper_actix/src/lib.rs index 8b2cce09..e85f536d 100644 --- a/juniper_actix/src/lib.rs +++ b/juniper_actix/src/lib.rs @@ -127,15 +127,13 @@ where 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), + let mut response = match gql_response.is_ok() { + true => HttpResponse::Ok(), + false => HttpResponse::BadRequest(), }; - Ok(response) + Ok(response + .content_type("application/json") + .body(body_response)) } /// Actix GraphQL Handler for POST requests @@ -160,13 +158,15 @@ where .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)), - } + Some("application/json") => { + let body = String::from_request(&req, &mut payload.into_inner()).await?; + serde_json::from_str::>(&body).map_err(ErrorBadRequest) + } + Some("application/graphql") => { + let body = String::from_request(&req, &mut payload.into_inner()).await?; + Ok(GraphQLBatchRequest::Single(GraphQLRequest::new( + body, None, None, + ))) } _ => Err(ErrorUnsupportedMediaType( "GraphQL requests should have content type `application/json` or `application/graphql`", @@ -223,7 +223,7 @@ mod tests { 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}, + http::tests::{run_http_test_suite, HttpIntegration, TestResponse}, tests::{model::Database, schema::Query}, EmptyMutation, EmptySubscription, RootNode, }; @@ -446,63 +446,54 @@ mod tests { assert!(result.is_err()); } - pub struct TestActixWebIntegration {} + 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( + impl TestActixWebIntegration { + fn make_request(&self, req: test::TestRequest) -> TestResponse { + actix_rt::System::new("request").block_on(async move { + let 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; + test::init_service(App::new().data(schema).route("/", web::to(index))).await; - let resp = test::call_service(&mut app, req).await; - let test_response = make_test_response(resp).await; - test_response + let resp = test::call_service(&mut app, req.to_request()).await; + make_test_response(resp).await }) } } - 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(); + impl HttpIntegration for TestActixWebIntegration { + fn get(&self, url: &str) -> TestResponse { + self.make_request(test::TestRequest::get().uri(url)) + } + + fn post_json(&self, url: &str, body: &str) -> TestResponse { + self.make_request( + test::TestRequest::post() + .header("content-type", "application/json") + .set_payload(body.to_string()) + .uri(url), + ) + } + + fn post_graphql(&self, url: &str, body: &str) -> TestResponse { + self.make_request( + test::TestRequest::post() + .header("content-type", "application/graphql") + .set_payload(body.to_string()) + .uri(url), + ) + } + } + + async fn make_test_response(mut resp: ServiceResponse) -> TestResponse { + let body = take_response_body_string(&mut resp).await; + let status_code = resp.status().as_u16(); + let content_type = resp.headers().get(CONTENT_TYPE).unwrap(); TestResponse { status_code: status_code as i32, body: Some(body), @@ -512,6 +503,6 @@ mod tests { #[test] fn test_actix_web_integration() { - run_http_test_suite(&TestActixWebIntegration {}); + run_http_test_suite(&TestActixWebIntegration); } } diff --git a/juniper_hyper/CHANGELOG.md b/juniper_hyper/CHANGELOG.md index 1acd9f3b..a6d0d298 100644 --- a/juniper_hyper/CHANGELOG.md +++ b/juniper_hyper/CHANGELOG.md @@ -4,7 +4,9 @@ ## Breaking Changes -- `juniper_hyper::graphiql` now requires a second parameter for subscriptions +- `juniper_hyper::graphiql` now requires a second parameter for subscriptions. +- `juniper_hyper::graphql` now executes the schema asynchronously. For blocking synchronous execution consider `juniper_hyper::graphql_sync` for use. +- `400 Bad Request` is now returned if POST HTTP request contains no or invalid `Content-Type` header. # [[0.5.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_hyper-0.5.2) diff --git a/juniper_hyper/src/lib.rs b/juniper_hyper/src/lib.rs index 710a609c..3d1f5554 100644 --- a/juniper_hyper/src/lib.rs +++ b/juniper_hyper/src/lib.rs @@ -8,17 +8,17 @@ use hyper::{ Body, Method, Request, Response, StatusCode, }; use juniper::{ - http::{GraphQLBatchRequest, GraphQLRequest as JuniperGraphQLRequest}, + http::{GraphQLBatchRequest, GraphQLRequest as JuniperGraphQLRequest, GraphQLRequest}, GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync, InputValue, RootNode, ScalarValue, }; use serde_json::error::Error as SerdeError; use std::{error::Error, fmt, string::FromUtf8Error, sync::Arc}; use url::form_urlencoded; -pub async fn graphql( +pub async fn graphql_sync( root_node: Arc>, context: Arc, - request: Request, + req: Request, ) -> Result, hyper::Error> where S: ScalarValue + Send + Sync + 'static, @@ -30,31 +30,16 @@ where MutationT::TypeInfo: Send + Sync, SubscriptionT::TypeInfo: Send + Sync, { - match *request.method() { - Method::GET => { - let gql_req = parse_get_req(request); - - match gql_req { - Ok(gql_req) => Ok(execute_request(root_node, context, gql_req).await), - Err(err) => Ok(render_error(err)), - } - } - Method::POST => { - let gql_req = parse_post_req(request.into_body()).await; - - match gql_req { - Ok(gql_req) => Ok(execute_request(root_node, context, gql_req).await), - Err(err) => Ok(render_error(err)), - } - } - _ => Ok(new_response(StatusCode::METHOD_NOT_ALLOWED)), - } + Ok(match parse_req(req).await { + Ok(req) => execute_request_sync(root_node, context, req).await, + Err(resp) => resp, + }) } -pub async fn graphql_async( +pub async fn graphql( root_node: Arc>, context: Arc, - request: Request, + req: Request, ) -> Result, hyper::Error> where S: ScalarValue + Send + Sync + 'static, @@ -66,25 +51,31 @@ where MutationT::TypeInfo: Send + Sync, SubscriptionT::TypeInfo: Send + Sync, { - match *request.method() { - Method::GET => { - let gql_req = parse_get_req(request); + Ok(match parse_req(req).await { + Ok(req) => execute_request(root_node, context, req).await, + Err(resp) => resp, + }) +} - match gql_req { - Ok(gql_req) => Ok(execute_request_async(root_node, context, gql_req).await), - Err(err) => Ok(render_error(err)), - } - } +async fn parse_req( + req: Request, +) -> Result, Response> { + match *req.method() { + Method::GET => parse_get_req(req), Method::POST => { - let gql_req = parse_post_req(request.into_body()).await; - - match gql_req { - Ok(gql_req) => Ok(execute_request_async(root_node, context, gql_req).await), - Err(err) => Ok(render_error(err)), + let content_type = req + .headers() + .get(header::CONTENT_TYPE) + .map(HeaderValue::to_str); + match content_type { + Some(Ok("application/json")) => parse_post_json_req(req.into_body()).await, + Some(Ok("application/graphql")) => parse_post_graphql_req(req.into_body()).await, + _ => return Err(new_response(StatusCode::BAD_REQUEST)), } } - _ => Ok(new_response(StatusCode::METHOD_NOT_ALLOWED)), + _ => return Err(new_response(StatusCode::METHOD_NOT_ALLOWED)), } + .map_err(|e| render_error(e)) } fn parse_get_req( @@ -100,7 +91,7 @@ fn parse_get_req( }) } -async fn parse_post_req( +async fn parse_post_json_req( body: Body, ) -> Result, GraphQLRequestError> { let chunk = hyper::body::to_bytes(body) @@ -114,6 +105,21 @@ async fn parse_post_req( .map_err(GraphQLRequestError::BodyJSONError) } +async fn parse_post_graphql_req( + body: Body, +) -> Result, GraphQLRequestError> { + let chunk = hyper::body::to_bytes(body) + .await + .map_err(GraphQLRequestError::BodyHyper)?; + + let query = String::from_utf8(chunk.iter().cloned().collect()) + .map_err(GraphQLRequestError::BodyUtf8)?; + + Ok(GraphQLBatchRequest::Single(GraphQLRequest::new( + query, None, None, + ))) +} + pub async fn graphiql( graphql_endpoint: &str, subscriptions_endpoint: Option<&str>, @@ -146,7 +152,7 @@ fn render_error(err: GraphQLRequestError) -> Response { resp } -async fn execute_request( +async fn execute_request_sync( root_node: Arc>, context: Arc, request: GraphQLBatchRequest, @@ -177,7 +183,7 @@ where resp } -async fn execute_request_async( +async fn execute_request( root_node: Arc>, context: Arc, request: GraphQLBatchRequest, @@ -319,19 +325,34 @@ mod tests { use reqwest::{self, Response as ReqwestResponse}; use std::{net::SocketAddr, sync::Arc, thread, time::Duration}; - struct TestHyperIntegration; + struct TestHyperIntegration { + port: u16, + } - impl http_tests::HTTPIntegration for TestHyperIntegration { + impl http_tests::HttpIntegration for TestHyperIntegration { fn get(&self, url: &str) -> http_tests::TestResponse { - let url = format!("http://127.0.0.1:3001/graphql{}", url); + let url = format!("http://127.0.0.1:{}/graphql{}", self.port, url); make_test_response(reqwest::get(&url).expect(&format!("failed GET {}", url))) } - fn post(&self, url: &str, body: &str) -> http_tests::TestResponse { - let url = format!("http://127.0.0.1:3001/graphql{}", url); + fn post_json(&self, url: &str, body: &str) -> http_tests::TestResponse { + let url = format!("http://127.0.0.1:{}/graphql{}", self.port, url); let client = reqwest::Client::new(); let res = client .post(&url) + .header(reqwest::header::CONTENT_TYPE, "application/json") + .body(body.to_string()) + .send() + .expect(&format!("failed POST {}", url)); + make_test_response(res) + } + + fn post_graphql(&self, url: &str, body: &str) -> http_tests::TestResponse { + let url = format!("http://127.0.0.1:{}/graphql{}", self.port, url); + let client = reqwest::Client::new(); + let res = client + .post(&url) + .header(reqwest::header::CONTENT_TYPE, "application/graphql") .body(body.to_string()) .send() .expect(&format!("failed POST {}", url)); @@ -356,9 +377,9 @@ mod tests { } } - #[tokio::test] - async fn test_hyper_integration() { - let addr: SocketAddr = ([127, 0, 0, 1], 3001).into(); + async fn run_hyper_integration(is_sync: bool) { + let port = if is_sync { 3002 } else { 3001 }; + let addr: SocketAddr = ([127, 0, 0, 1], port).into(); let db = Arc::new(Database::new()); let root_node = Arc::new(RootNode::new( @@ -386,11 +407,15 @@ mod tests { }; async move { if matches { - super::graphql(root_node, ctx, req).await + if is_sync { + super::graphql_sync(root_node, ctx, req).await + } else { + super::graphql(root_node, ctx, req).await + } } else { - let mut response = Response::new(Body::empty()); - *response.status_mut() = StatusCode::NOT_FOUND; - Ok(response) + let mut resp = Response::new(Body::empty()); + *resp.status_mut() = StatusCode::NOT_FOUND; + Ok(resp) } } })) @@ -409,7 +434,7 @@ mod tests { tokio::task::spawn_blocking(move || { thread::sleep(Duration::from_millis(10)); // wait 10ms for server to bind - let integration = TestHyperIntegration; + let integration = TestHyperIntegration { port }; http_tests::run_http_test_suite(&integration); shutdown.abort(); }); @@ -418,4 +443,14 @@ mod tests { eprintln!("server error: {}", e); } } + + #[tokio::test] + async fn test_hyper_integration() { + run_hyper_integration(false).await + } + + #[tokio::test] + async fn test_sync_hyper_integration() { + run_hyper_integration(true).await + } } diff --git a/juniper_iron/CHANGELOG.md b/juniper_iron/CHANGELOG.md index 83d83dad..5c4574d9 100644 --- a/juniper_iron/CHANGELOG.md +++ b/juniper_iron/CHANGELOG.md @@ -4,7 +4,8 @@ ## Breaking Changes -- `juniper_iron::GraphiQLHandler::new` now requires a second parameter for subscriptions +- `juniper_iron::GraphiQLHandler::new` now requires a second parameter for subscriptions. +- `400 Bad Request` is now returned if POST HTTP request contains no or invalid `Content-Type` header. # [[0.6.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_iron-0.6.2) diff --git a/juniper_iron/src/lib.rs b/juniper_iron/src/lib.rs index de704574..dc506465 100644 --- a/juniper_iron/src/lib.rs +++ b/juniper_iron/src/lib.rs @@ -114,10 +114,17 @@ extern crate iron_test; #[cfg(test)] extern crate url; -use iron::{itry, method, middleware::Handler, mime::Mime, prelude::*, status}; +use iron::{ + headers::ContentType, + itry, method, + middleware::Handler, + mime::{Mime, TopLevel}, + prelude::*, + status, +}; use urlencoded::{UrlDecodingError, UrlEncodedQuery}; -use std::{error::Error, fmt, io::Read}; +use std::{error::Error, fmt, io::Read, ops::Deref as _}; use serde_json::error::Error as SerdeError; @@ -228,32 +235,41 @@ where } fn handle_get(&self, req: &mut Request) -> IronResult> { - let url_query_string = req + let url_query = req .get_mut::() .map_err(GraphQLIronError::Url)?; - let input_query = parse_url_param(url_query_string.remove("query"))? + let query = parse_url_param(url_query.remove("query"))? .ok_or_else(|| GraphQLIronError::InvalidData("No query provided"))?; - let operation_name = parse_url_param(url_query_string.remove("operationName"))?; - let variables = parse_variable_param(url_query_string.remove("variables"))?; + let operation_name = parse_url_param(url_query.remove("operationName"))?; + let variables = parse_variable_param(url_query.remove("variables"))?; Ok(GraphQLBatchRequest::Single(http::GraphQLRequest::new( - input_query, + query, operation_name, variables, ))) } - fn handle_post(&self, req: &mut Request) -> IronResult> { - let mut request_payload = String::new(); - itry!(req.body.read_to_string(&mut request_payload)); + fn handle_post_json(&self, req: &mut Request) -> IronResult> { + let mut payload = String::new(); + itry!(req.body.read_to_string(&mut payload)); Ok( - serde_json::from_str::>(request_payload.as_str()) + serde_json::from_str::>(payload.as_str()) .map_err(GraphQLIronError::Serde)?, ) } + fn handle_post_graphql(&self, req: &mut Request) -> IronResult> { + let mut payload = String::new(); + itry!(req.body.read_to_string(&mut payload)); + + Ok(GraphQLBatchRequest::Single(http::GraphQLRequest::new( + payload, None, None, + ))) + } + fn execute_sync( &self, context: &CtxT, @@ -313,7 +329,14 @@ where let graphql_request = match req.method { method::Get => self.handle_get(&mut req)?, - method::Post => self.handle_post(&mut req)?, + method::Post => match req.headers.get::().map(ContentType::deref) { + Some(Mime(TopLevel::Application, sub_lvl, _)) => match sub_lvl.as_str() { + "json" => self.handle_post_json(&mut req)?, + "graphql" => self.handle_post_graphql(&mut req)?, + _ => return Ok(Response::with(status::BadRequest)), + }, + _ => return Ok(Response::with(status::BadRequest)), + }, _ => return Ok(Response::with(status::MethodNotAllowed)), }; @@ -388,7 +411,11 @@ impl From for IronError { #[cfg(test)] mod tests { use super::*; - use iron::{Handler, Headers, Url}; + use iron::{ + headers::ContentType, + mime::{Mime, SubLevel, TopLevel}, + Handler, Headers, Url, + }; use iron_test::{request, response}; use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; @@ -411,7 +438,7 @@ mod tests { .path() .iter() .map(|x| (*x).to_string()) - .collect::>() + .collect::>() .join("/"); format!( "http://localhost:3000{}?{}", @@ -422,21 +449,31 @@ mod tests { struct TestIronIntegration; - impl http_tests::HTTPIntegration for TestIronIntegration { + impl http_tests::HttpIntegration for TestIronIntegration { fn get(&self, url: &str) -> http_tests::TestResponse { - let result = request::get(&fixup_url(url), Headers::new(), &make_handler()); - match result { - Ok(response) => make_test_response(response), - Err(e) => make_test_error_response(e), - } + request::get(&fixup_url(url), Headers::new(), &make_handler()) + .map(make_test_response) + .unwrap_or_else(make_test_error_response) } - fn post(&self, url: &str, body: &str) -> http_tests::TestResponse { - let result = request::post(&fixup_url(url), Headers::new(), body, &make_handler()); - match result { - Ok(response) => make_test_response(response), - Err(e) => make_test_error_response(e), - } + fn post_json(&self, url: &str, body: &str) -> http_tests::TestResponse { + let mut headers = Headers::new(); + headers.set(ContentType::json()); + request::post(&fixup_url(url), headers, body, &make_handler()) + .map(make_test_response) + .unwrap_or_else(make_test_error_response) + } + + fn post_graphql(&self, url: &str, body: &str) -> http_tests::TestResponse { + let mut headers = Headers::new(); + headers.set(ContentType(Mime( + TopLevel::Application, + SubLevel::Ext("graphql".into()), + vec![], + ))); + request::post(&fixup_url(url), headers, body, &make_handler()) + .map(make_test_response) + .unwrap_or_else(make_test_error_response) } } diff --git a/juniper_rocket/CHANGELOG.md b/juniper_rocket/CHANGELOG.md index 5359987b..880ef341 100644 --- a/juniper_rocket/CHANGELOG.md +++ b/juniper_rocket/CHANGELOG.md @@ -2,6 +2,7 @@ - Compatibility with the latest `juniper`. - Rocket integration does not require default features. +- Support `application/graphql` POST requests. ## Breaking Changes diff --git a/juniper_rocket/src/lib.rs b/juniper_rocket/src/lib.rs index 8816ac8d..e6d29ca2 100644 --- a/juniper_rocket/src/lib.rs +++ b/juniper_rocket/src/lib.rs @@ -47,7 +47,7 @@ use rocket::{ request::{FormItems, FromForm, FromFormValue}, response::{content, Responder, Response}, Data, - Outcome::{Failure, Forward, Success}, + Outcome::{Forward, Success}, Request, }; @@ -271,20 +271,26 @@ where { type Error = String; - fn from_data(request: &Request, data: Data) -> FromDataOutcome { - if !request.content_type().map_or(false, |ct| ct.is_json()) { - return Forward(data); - } + fn from_data(req: &Request, data: Data) -> FromDataOutcome { + let content_type = req + .content_type() + .map(|ct| (ct.top().as_str(), ct.sub().as_str())); + let is_json = match content_type { + Some(("application", "json")) => true, + Some(("application", "graphql")) => false, + _ => return Forward(data), + }; let mut body = String::new(); - if let Err(e) = data.open().read_to_string(&mut body) { - return Failure((Status::InternalServerError, format!("{:?}", e))); - } + data.open() + .read_to_string(&mut body) + .map_err(|e| Err((Status::InternalServerError, format!("{:?}", e))))?; - match serde_json::from_str(&body) { - Ok(value) => Success(GraphQLRequest(value)), - Err(failure) => Failure((Status::BadRequest, format!("{}", failure))), - } + Success(GraphQLRequest(if is_json { + serde_json::from_str(&body).map_err(|e| Err((Status::BadRequest, format!("{}", e))))? + } else { + GraphQLBatchRequest::Single(http::GraphQLRequest::new(body, None, None)) + })) } } @@ -456,16 +462,25 @@ mod tests { client: Client, } - impl http_tests::HTTPIntegration for TestRocketIntegration { + impl http_tests::HttpIntegration for TestRocketIntegration { fn get(&self, url: &str) -> http_tests::TestResponse { let req = &self.client.get(url); make_test_response(req) } - fn post(&self, url: &str, body: &str) -> http_tests::TestResponse { + fn post_json(&self, url: &str, body: &str) -> http_tests::TestResponse { let req = &self.client.post(url).header(ContentType::JSON).body(body); 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); + make_test_response(req) + } } #[test] diff --git a/juniper_rocket_async/CHANGELOG.md b/juniper_rocket_async/CHANGELOG.md index 6778af05..d2889166 100644 --- a/juniper_rocket_async/CHANGELOG.md +++ b/juniper_rocket_async/CHANGELOG.md @@ -2,6 +2,7 @@ - Compatibility with the latest `juniper`. - Rocket integration does not require default features. +- Support `application/graphql` POST requests. # [[0.5.1] 2019-10-24](https://github.com/graphql-rust/juniper/releases/tag/juniper_rocket-0.5.1) diff --git a/juniper_rocket_async/src/lib.rs b/juniper_rocket_async/src/lib.rs index efbb8c30..1a978815 100644 --- a/juniper_rocket_async/src/lib.rs +++ b/juniper_rocket_async/src/lib.rs @@ -294,12 +294,17 @@ where { type Error = String; - fn from_data(request: &Request, data: Data) -> FromDataFuture<'static, Self, Self::Error> { + fn from_data(req: &Request, data: Data) -> FromDataFuture<'static, Self, Self::Error> { use tokio::io::AsyncReadExt as _; - if !request.content_type().map_or(false, |ct| ct.is_json()) { - return Box::pin(async move { Forward(data) }); - } + let content_type = req + .content_type() + .map(|ct| (ct.top().as_str(), ct.sub().as_str())); + let is_json = match content_type { + Some(("application", "json")) => true, + Some(("application", "graphql")) => false, + _ => return Box::pin(async move { Forward(data) }), + }; Box::pin(async move { let mut body = String::new(); @@ -308,10 +313,14 @@ where return Failure((Status::InternalServerError, format!("{:?}", e))); } - match serde_json::from_str(&body) { - Ok(value) => Success(GraphQLRequest(value)), - Err(failure) => Failure((Status::BadRequest, format!("{}", failure))), - } + Success(GraphQLRequest(if is_json { + match serde_json::from_str(&body) { + Ok(req) => req, + Err(e) => return Failure((Status::BadRequest, format!("{}", e))), + } + } else { + GraphQLBatchRequest::Single(http::GraphQLRequest::new(body, None, None)) + })) }) } } @@ -495,18 +504,28 @@ mod tests { client: Client, } - impl http_tests::HTTPIntegration for TestRocketIntegration { + 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(&self, url: &str, body: &str) -> http_tests::TestResponse { + 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)) + } } #[test] diff --git a/juniper_warp/CHANGELOG.md b/juniper_warp/CHANGELOG.md index 9cbd277e..70a8fe90 100644 --- a/juniper_warp/CHANGELOG.md +++ b/juniper_warp/CHANGELOG.md @@ -6,10 +6,11 @@ to `juniper` to be reused in other http integrations, since this implementation ## Breaking Changes -- Update `playground_filter` to support subscription endpoint URLs -- Update `warp` to 0.2 +- Update `playground_filter` to support subscription endpoint URLs. +- Update `warp` to 0.2. - Rename synchronous `execute` to `execute_sync`, add asynchronous `execute` -- `juniper_warp::graphiql_filter` now requires a second parameter for subscriptions +- `juniper_warp::graphiql_filter` now requires a second parameter for subscriptions. +- `make_graphql_filter` and `make_graphql_filter_sync` now ignore POST HTTP requests with no or invalid `Content-Type` header. # [[0.5.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_warp-0.5.2) diff --git a/juniper_warp/Cargo.toml b/juniper_warp/Cargo.toml index f1de53c2..d27cf233 100644 --- a/juniper_warp/Cargo.toml +++ b/juniper_warp/Cargo.toml @@ -12,18 +12,19 @@ edition = "2018" subscriptions = ["juniper_subscriptions"] [dependencies] -warp = "0.2" +bytes = "0.5" +failure = "0.1.7" futures = "0.3.1" juniper = { version = "0.14.2", path = "../juniper", default-features = false } -juniper_subscriptions = { path = "../juniper_subscriptions", optional = true} -tokio = { version = "0.2", features = ["rt-core", "blocking"] } +juniper_subscriptions = { path = "../juniper_subscriptions", optional = true } serde = { version = "1.0.75", features = ["derive"] } serde_json = "1.0.24" -failure = "0.1.7" +tokio = { version = "0.2", features = ["blocking", "rt-core"] } +warp = "0.2" [dev-dependencies] -juniper = { version = "0.14.2", path = "../juniper", features = ["expose-test-schema", "serde_json"] } env_logger = "0.5.11" +juniper = { version = "0.14.2", path = "../juniper", features = ["expose-test-schema", "serde_json"] } log = "0.4.3" percent-encoding = "1.0" -tokio = { version = "0.2", features = ["rt-core", "macros", "blocking"] } +tokio = { version = "0.2", features = ["blocking", "macros", "rt-core"] } diff --git a/juniper_warp/src/lib.rs b/juniper_warp/src/lib.rs index 471d68bc..8579be70 100644 --- a/juniper_warp/src/lib.rs +++ b/juniper_warp/src/lib.rs @@ -40,12 +40,16 @@ Check the LICENSE file for details. #![deny(warnings)] #![doc(html_root_url = "https://docs.rs/juniper_warp/0.2.0")] -use std::{pin::Pin, sync::Arc}; +use std::{collections::HashMap, str, sync::Arc}; -use futures::{Future, FutureExt as _, TryFutureExt}; -use juniper::{http::GraphQLBatchRequest, ScalarValue}; +use bytes::Bytes; +use futures::{FutureExt as _, TryFutureExt}; +use juniper::{ + http::{GraphQLBatchRequest, GraphQLRequest}, + ScalarValue, +}; use tokio::task; -use warp::{filters::BoxedFilter, Filter}; +use warp::{body, filters::BoxedFilter, header, http, query, Filter}; /// Make a filter for graphql queries/mutations. /// @@ -110,7 +114,7 @@ use warp::{filters::BoxedFilter, Filter}; pub fn make_graphql_filter( schema: juniper::RootNode<'static, Query, Mutation, Subscription, S>, context_extractor: BoxedFilter<(Context,)>, -) -> BoxedFilter<(warp::http::Response>,)> +) -> BoxedFilter<(http::Response>,)> where S: ScalarValue + Send + Sync + 'static, Context: Send + Sync + 'static, @@ -122,65 +126,90 @@ where Subscription::TypeInfo: Send + Sync, { let schema = Arc::new(schema); - let post_schema = schema.clone(); + let post_json_schema = schema.clone(); + let post_graphql_schema = schema.clone(); - let handle_post_request = move |context: Context, request: GraphQLBatchRequest| { - let schema = post_schema.clone(); - - Box::pin(async move { - let res = request.execute(&schema, &context).await; + let handle_post_json_request = move |context: Context, req: GraphQLBatchRequest| { + let schema = post_json_schema.clone(); + async move { + let resp = req.execute(&schema, &context).await; Ok::<_, warp::Rejection>(build_response( - serde_json::to_vec(&res) - .map(|json| (json, res.is_ok())) + serde_json::to_vec(&resp) + .map(|json| (json, resp.is_ok())) .map_err(Into::into), )) - }) + } }; - - let post_filter = warp::post() + let post_json_filter = warp::post() + .and(header::exact_ignore_case( + "content-type", + "application/json", + )) .and(context_extractor.clone()) - .and(warp::body::json()) - .and_then(handle_post_request); + .and(body::json()) + .and_then(handle_post_json_request); - let handle_get_request = - move |context: Context, mut request: std::collections::HashMap| { - let schema = schema.clone(); + let handle_post_graphql_request = move |context: Context, body: Bytes| { + let schema = post_graphql_schema.clone(); + async move { + let query = str::from_utf8(body.as_ref()).map_err(|e| { + failure::format_err!("Request body query is not a valid UTF-8 string: {}", e) + })?; + let req = GraphQLRequest::new(query.into(), None, None); - async move { - let variables = match request.remove("variables") { - None => None, - Some(vs) => serde_json::from_str(&vs)?, - }; + let resp = req.execute(&schema, &context).await; - let graphql_request = juniper::http::GraphQLRequest::new( - request.remove("query").ok_or_else(|| { - failure::format_err!("Missing GraphQL query string in query parameters") - })?, - request.get("operation_name").map(|s| s.to_owned()), - variables, - ); + Ok((serde_json::to_vec(&resp)?, resp.is_ok())) + } + .then(|res| async { Ok::<_, warp::Rejection>(build_response(res)) }) + }; + let post_graphql_filter = warp::post() + .and(header::exact_ignore_case( + "content-type", + "application/graphql", + )) + .and(context_extractor.clone()) + .and(body::bytes()) + .and_then(handle_post_graphql_request); - let response = graphql_request.execute(&schema, &context).await; + let handle_get_request = move |context: Context, mut qry: HashMap| { + let schema = schema.clone(); + async move { + let req = GraphQLRequest::new( + qry.remove("query").ok_or_else(|| { + failure::format_err!("Missing GraphQL query string in query parameters") + })?, + qry.remove("operation_name"), + qry.remove("variables") + .map(|vs| serde_json::from_str(&vs)) + .transpose()?, + ); - Ok((serde_json::to_vec(&response)?, response.is_ok())) - } - .then(|result| async move { Ok::<_, warp::Rejection>(build_response(result)) }) - }; + let resp = req.execute(&schema, &context).await; + Ok((serde_json::to_vec(&resp)?, resp.is_ok())) + } + .then(|res| async move { Ok::<_, warp::Rejection>(build_response(res)) }) + }; let get_filter = warp::get() .and(context_extractor) - .and(warp::filters::query::query()) + .and(query::query()) .and_then(handle_get_request); - get_filter.or(post_filter).unify().boxed() + get_filter + .or(post_json_filter) + .unify() + .or(post_graphql_filter) + .unify() + .boxed() } /// Make a synchronous filter for graphql endpoint. pub fn make_graphql_filter_sync( schema: juniper::RootNode<'static, Query, Mutation, Subscription, S>, context_extractor: BoxedFilter<(Context,)>, -) -> BoxedFilter<(warp::http::Response>,)> +) -> BoxedFilter<(http::Response>,)> where S: ScalarValue + Send + Sync + 'static, Context: Send + Sync + 'static, @@ -189,105 +218,116 @@ where Subscription: juniper::GraphQLType + Send + Sync + 'static, { let schema = Arc::new(schema); - let post_schema = schema.clone(); + let post_json_schema = schema.clone(); + let post_graphql_schema = schema.clone(); - let handle_post_request = - move |context: Context, request: GraphQLBatchRequest| -> Response { - let schema = post_schema.clone(); + let handle_post_json_request = move |context: Context, req: GraphQLBatchRequest| { + let schema = post_json_schema.clone(); + async move { + let res = task::spawn_blocking(move || { + let resp = req.execute_sync(&schema, &context); + Ok((serde_json::to_vec(&resp)?, resp.is_ok())) + }) + .await?; - Box::pin( - async move { - let result = task::spawn_blocking(move || { - let response = request.execute_sync(&schema, &context); - Ok((serde_json::to_vec(&response)?, response.is_ok())) - }) - .await?; - - Ok(build_response(result)) - } - .map_err(|e: task::JoinError| warp::reject::custom(JoinError(e))), - ) - }; - - let post_filter = warp::post() - .and(context_extractor.clone()) - .and(warp::body::json()) - .and_then(handle_post_request); - - let handle_get_request = move |context: Context, - mut request: std::collections::HashMap| - -> Response { - let schema = schema.clone(); - - Box::pin( - async move { - let result = task::spawn_blocking(move || { - let variables = match request.remove("variables") { - None => None, - Some(vs) => serde_json::from_str(&vs)?, - }; - - let graphql_request = juniper::http::GraphQLRequest::new( - request.remove("query").ok_or_else(|| { - failure::format_err!("Missing GraphQL query string in query parameters") - })?, - request.get("operation_name").map(|s| s.to_owned()), - variables, - ); - - let response = graphql_request.execute_sync(&schema, &context); - Ok((serde_json::to_vec(&response)?, response.is_ok())) - }) - .await?; - - Ok(build_response(result)) - } - .map_err(|e: task::JoinError| warp::reject::custom(JoinError(e))), - ) + Ok(build_response(res)) + } + .map_err(|e: task::JoinError| warp::reject::custom(JoinError(e))) }; + let post_json_filter = warp::post() + .and(header::exact_ignore_case( + "content-type", + "application/json", + )) + .and(context_extractor.clone()) + .and(body::json()) + .and_then(handle_post_json_request); + let handle_post_graphql_request = move |context: Context, body: Bytes| { + let schema = post_graphql_schema.clone(); + async move { + let res = task::spawn_blocking(move || { + let query = str::from_utf8(body.as_ref()).map_err(|e| { + failure::format_err!("Request body is not a valid UTF-8 string: {}", e) + })?; + let req = GraphQLRequest::new(query.into(), None, None); + + let resp = req.execute_sync(&schema, &context); + Ok((serde_json::to_vec(&resp)?, resp.is_ok())) + }) + .await?; + + Ok(build_response(res)) + } + .map_err(|e: task::JoinError| warp::reject::custom(JoinError(e))) + }; + let post_graphql_filter = warp::post() + .and(header::exact_ignore_case( + "content-type", + "application/graphql", + )) + .and(context_extractor.clone()) + .and(body::bytes()) + .and_then(handle_post_graphql_request); + + let handle_get_request = move |context: Context, mut qry: HashMap| { + let schema = schema.clone(); + async move { + let res = task::spawn_blocking(move || { + let req = GraphQLRequest::new( + qry.remove("query").ok_or_else(|| { + failure::format_err!("Missing GraphQL query string in query parameters") + })?, + qry.remove("operation_name"), + qry.remove("variables") + .map(|vs| serde_json::from_str(&vs)) + .transpose()?, + ); + + let resp = req.execute_sync(&schema, &context); + Ok((serde_json::to_vec(&resp)?, resp.is_ok())) + }) + .await?; + + Ok(build_response(res)) + } + .map_err(|e: task::JoinError| warp::reject::custom(JoinError(e))) + }; let get_filter = warp::get() .and(context_extractor) - .and(warp::filters::query::query()) + .and(query::query()) .and_then(handle_get_request); - get_filter.or(post_filter).unify().boxed() + get_filter + .or(post_json_filter) + .unify() + .or(post_graphql_filter) + .unify() + .boxed() } -/// Error raised by `tokio_threadpool` if the thread pool -/// has been shutdown +/// Error raised by `tokio_threadpool` if the thread pool has been shutdown. /// -/// Wrapper type is needed as inner type does not implement `warp::reject::Reject` +/// Wrapper type is needed as inner type does not implement `warp::reject::Reject`. +#[derive(Debug)] pub struct JoinError(task::JoinError); impl warp::reject::Reject for JoinError {} -impl std::fmt::Debug for JoinError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "JoinError({:?})", self.0) - } -} - -fn build_response( - response: Result<(Vec, bool), failure::Error>, -) -> warp::http::Response> { +fn build_response(response: Result<(Vec, bool), failure::Error>) -> http::Response> { match response { - Ok((body, is_ok)) => warp::http::Response::builder() + Ok((body, is_ok)) => http::Response::builder() .status(if is_ok { 200 } else { 400 }) .header("content-type", "application/json") .body(body) .expect("response is valid"), - Err(_) => warp::http::Response::builder() - .status(warp::http::StatusCode::INTERNAL_SERVER_ERROR) + Err(_) => http::Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) .body(Vec::new()) .expect("status code is valid"), } } -type Response = Pin< - Box>, warp::reject::Rejection>> + Send>, ->; - /// Create a filter that replies with an HTML page containing GraphiQL. This does not handle routing, so you can mount it on any endpoint. /// /// For example: @@ -318,7 +358,7 @@ type Response = Pin< pub fn graphiql_filter( graphql_endpoint_url: &'static str, subscriptions_endpoint: Option<&'static str>, -) -> warp::filters::BoxedFilter<(warp::http::Response>,)> { +) -> warp::filters::BoxedFilter<(http::Response>,)> { warp::any() .map(move || graphiql_response(graphql_endpoint_url, subscriptions_endpoint)) .boxed() @@ -327,8 +367,8 @@ pub fn graphiql_filter( fn graphiql_response( graphql_endpoint_url: &'static str, subscriptions_endpoint: Option<&'static str>, -) -> warp::http::Response> { - warp::http::Response::builder() +) -> http::Response> { + http::Response::builder() .header("content-type", "text/html;charset=utf-8") .body( juniper::http::graphiql::graphiql_source(graphql_endpoint_url, subscriptions_endpoint) @@ -341,7 +381,7 @@ fn graphiql_response( pub fn playground_filter( graphql_endpoint_url: &'static str, subscriptions_endpoint_url: Option<&'static str>, -) -> warp::filters::BoxedFilter<(warp::http::Response>,)> { +) -> warp::filters::BoxedFilter<(http::Response>,)> { warp::any() .map(move || playground_response(graphql_endpoint_url, subscriptions_endpoint_url)) .boxed() @@ -350,8 +390,8 @@ pub fn playground_filter( fn playground_response( graphql_endpoint_url: &'static str, subscriptions_endpoint_url: Option<&'static str>, -) -> warp::http::Response> { - warp::http::Response::builder() +) -> http::Response> { + http::Response::builder() .header("content-type", "text/html;charset=utf-8") .body( juniper::http::playground::playground_source( @@ -780,100 +820,122 @@ mod tests { } } -//TODO: update warp tests -//#[cfg(test)] -//mod tests_http_harness { -// use super::*; -// use juniper::{ -// http::tests::{run_http_test_suite, HTTPIntegration, TestResponse}, -// tests::{model::Database, schema::Query}, -// EmptyMutation, EmptySubscription, RootNode, -// }; -// use warp::{self, Filter}; -// -// type Schema = -// juniper::RootNode<'static, Query, EmptyMutation, EmptySubscription>; -// -// fn warp_server() -> warp::filters::BoxedFilter<(warp::http::Response>,)> { -// let schema: Schema = RootNode::new( -// Query, -// EmptyMutation::::new(), -// EmptySubscription::::new(), -// ); -// -// let state = warp::any().map(move || Database::new()); -// let filter = warp::filters::path::end().and(make_graphql_filter(schema, state.boxed())); -// -// filter.boxed() -// } -// -// struct TestWarpIntegration { -// filter: warp::filters::BoxedFilter<(warp::http::Response>,)>, -// } -// -// // This can't be implemented with the From trait since TestResponse is not defined in this crate. -// fn test_response_from_http_response(response: warp::http::Response>) -> TestResponse { -// TestResponse { -// status_code: response.status().as_u16() as i32, -// body: Some(String::from_utf8(response.body().to_owned()).unwrap()), -// content_type: response -// .headers() -// .get("content-type") -// .expect("missing content-type header in warp response") -// .to_str() -// .expect("invalid content-type string") -// .to_owned(), -// } -// } -// -// impl HTTPIntegration for TestWarpIntegration { -// fn get(&self, url: &str) -> TestResponse { -// use percent_encoding::{percent_encode, DEFAULT_ENCODE_SET}; -// let url: String = percent_encode(url.replace("/?", "").as_bytes(), DEFAULT_ENCODE_SET) -// .into_iter() -// .collect::>() -// .join(""); -// -// let response = warp::test::request() -// .method("GET") -// .path(&format!("/?{}", url)) -// .filter(&self.filter) -// .await -// .unwrap_or_else(|rejection| { -// warp::http::Response::builder() -// .status(rejection.status()) -// .header("content-type", "application/json") -// .body(Vec::new()) -// .unwrap() -// }); -// test_response_from_http_response(response) -// } -// -// fn post(&self, url: &str, body: &str) -> TestResponse { -// let response = warp::test::request() -// .method("POST") -// .header("content-type", "application/json") -// .path(url) -// .body(body) -// .filter(&self.filter) -// .await -// .unwrap_or_else(|rejection| { -// warp::http::Response::builder() -// .status(rejection.status()) -// .header("content-type", "application/json") -// .body(Vec::new()) -// .unwrap() -// }); -// test_response_from_http_response(response) -// } -// } -// -// #[test] -// fn test_warp_integration() { -// let integration = TestWarpIntegration { -// filter: warp_server(), -// }; -// -// run_http_test_suite(&integration); -// } -//} +#[cfg(test)] +mod tests_http_harness { + use super::*; + use juniper::{ + http::tests::{run_http_test_suite, HttpIntegration, TestResponse}, + tests::{model::Database, schema::Query}, + EmptyMutation, EmptySubscription, RootNode, + }; + use warp::{ + self, + filters::{path, BoxedFilter}, + Filter, + }; + + struct TestWarpIntegration { + filter: BoxedFilter<(http::Response>,)>, + } + + impl TestWarpIntegration { + fn new(is_sync: bool) -> Self { + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + ); + let state = warp::any().map(move || Database::new()); + + let filter = path::end().and(if is_sync { + make_graphql_filter_sync(schema, state.boxed()) + } else { + make_graphql_filter(schema, state.boxed()) + }); + Self { + filter: filter.boxed(), + } + } + + fn make_request(&self, req: warp::test::RequestBuilder) -> TestResponse { + let mut rt = tokio::runtime::Runtime::new().expect("Failed to create tokio::Runtime"); + make_test_response(rt.block_on(async move { + req.filter(&self.filter).await.unwrap_or_else(|rejection| { + let code = if rejection.is_not_found() { + http::StatusCode::NOT_FOUND + } else if let Some(body::BodyDeserializeError { .. }) = rejection.find() { + http::StatusCode::BAD_REQUEST + } else { + http::StatusCode::INTERNAL_SERVER_ERROR + }; + http::Response::builder() + .status(code) + .header("content-type", "application/json") + .body(Vec::new()) + .unwrap() + }) + })) + } + } + + impl HttpIntegration for TestWarpIntegration { + fn get(&self, url: &str) -> TestResponse { + use percent_encoding::{utf8_percent_encode, QUERY_ENCODE_SET}; + + let url: String = utf8_percent_encode(&url.replace("/?", ""), QUERY_ENCODE_SET) + .into_iter() + .collect::>() + .join(""); + + self.make_request( + warp::test::request() + .method("GET") + .path(&format!("/?{}", url)), + ) + } + + fn post_json(&self, url: &str, body: &str) -> TestResponse { + self.make_request( + warp::test::request() + .method("POST") + .header("content-type", "application/json") + .path(url) + .body(body), + ) + } + + fn post_graphql(&self, url: &str, body: &str) -> TestResponse { + self.make_request( + warp::test::request() + .method("POST") + .header("content-type", "application/graphql") + .path(url) + .body(body), + ) + } + } + + fn make_test_response(resp: http::Response>) -> TestResponse { + TestResponse { + status_code: resp.status().as_u16() as i32, + body: Some(String::from_utf8(resp.body().to_owned()).unwrap()), + content_type: resp + .headers() + .get("content-type") + .expect("missing content-type header in warp response") + .to_str() + .expect("invalid content-type string") + .to_owned(), + } + } + + #[test] + fn test_warp_integration() { + run_http_test_suite(&TestWarpIntegration::new(false)); + } + + #[test] + fn test_sync_warp_integration() { + run_http_test_suite(&TestWarpIntegration::new(true)); + } +}