From 3f277ba8b4f0777016b4021763a7189717192b1f Mon Sep 17 00:00:00 2001 From: Magnus Hallin Date: Wed, 14 Jun 2017 18:21:05 +0200 Subject: [PATCH] Add Rocket integration to the HTTP test suite --- .travis.yml | 2 +- examples/rocket-server.rs | 13 ++- src/integrations/rocket_handlers.rs | 145 +++++++++++++++++++++++++++- 3 files changed, 155 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 32eb7b67..27c10a8e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,7 @@ script: - export TEST_FEATURES="iron-handlers expose-test-schema" - | if [ "$TRAVIS_RUST_VERSION" = "nightly" ]; then - export TEST_FEATURES="$TEST_FEATURES rocket-handlers" + export TEST_FEATURES="$TEST_FEATURES rocket-handlers rocket/testing" fi - cargo test --verbose --features "$TEST_FEATURES" diff --git a/examples/rocket-server.rs b/examples/rocket-server.rs index c5b35d2d..ab6acf7a 100644 --- a/examples/rocket-server.rs +++ b/examples/rocket-server.rs @@ -19,11 +19,20 @@ fn graphiql() -> content::HTML { rocket_handlers::graphiql_source("/graphql") } +#[get("/graphql?")] +fn get_graphql_handler( + context: State, + request: rocket_handlers::GraphQLRequest, + schema: State, +) -> rocket_handlers::GraphQLResponse { + request.execute(&schema, &context) +} + #[post("/graphql", data="")] fn post_graphql_handler( context: State, request: rocket_handlers::GraphQLRequest, - schema: State + schema: State, ) -> rocket_handlers::GraphQLResponse { request.execute(&schema, &context) } @@ -32,6 +41,6 @@ fn main() { rocket::ignite() .manage(Database::new()) .manage(Schema::new(Database::new(), EmptyMutation::::new())) - .mount("/", routes![graphiql, post_graphql_handler]) + .mount("/", routes![graphiql, get_graphql_handler, post_graphql_handler]) .launch(); } \ No newline at end of file diff --git a/src/integrations/rocket_handlers.rs b/src/integrations/rocket_handlers.rs index c8573d03..c1aa5adf 100644 --- a/src/integrations/rocket_handlers.rs +++ b/src/integrations/rocket_handlers.rs @@ -1,14 +1,17 @@ use std::io::{Cursor, Read}; +use std::error::Error; use serde_json; use rocket::Request; -use rocket::data::{FromData, Outcome}; +use rocket::request::{FromForm, FormItems, FromFormValue}; +use rocket::data::{FromData, Outcome as FromDataOutcome}; use rocket::response::{Responder, Response, content}; use rocket::http::{ContentType, Status}; use rocket::Data; use rocket::Outcome::{Forward, Failure, Success}; +use ::InputValue; use ::http; use types::base::GraphQLType; @@ -39,10 +42,62 @@ impl GraphQLRequest { } } +impl<'f> FromForm<'f> for GraphQLRequest { + type Error = String; + + fn from_form_items(form_items: &mut FormItems<'f>) -> Result { + let mut query = None; + let mut operation_name = None; + let mut variables = None; + + for (key, value) in form_items { + match key { + "query" => { + if query.is_some() { + return Err("Query parameter must not occur more than once".to_owned()); + } + else { + query = Some(String::from_form_value(value)?); + } + } + "operation_name" => { + if operation_name.is_some() { + return Err("Operation name parameter must not occur more than once".to_owned()); + } + else { + operation_name = Some(String::from_form_value(value)?); + } + } + "variables" => { + if variables.is_some() { + return Err("Variables parameter must not occur more than once".to_owned()); + } + else { + variables = Some(serde_json::from_str::(&String::from_form_value(value)?) + .map_err(|err| err.description().to_owned())?); + } + } + _ => {} + } + } + + if let Some(query) = query { + Ok(GraphQLRequest(http::GraphQLRequest::new( + query, + operation_name, + variables + ))) + } + else { + Err("Query parameter missing".to_owned()) + } + } +} + impl FromData for GraphQLRequest { type Error = String; - fn from_data(request: &Request, data: Data) -> Outcome { + fn from_data(request: &Request, data: Data) -> FromDataOutcome { if !request.content_type().map_or(false, |ct| ct.is_json()) { return Forward(data); } @@ -72,3 +127,89 @@ impl<'r> Responder<'r> for GraphQLResponse { .finalize()) } } + +#[cfg(test)] +mod tests { + use rocket; + use rocket::Rocket; + use rocket::http::{ContentType, Method}; + use rocket::State; + use rocket::testing::MockRequest; + + use ::RootNode; + use ::tests::model::Database; + use ::http::tests as http_tests; + use types::scalars::EmptyMutation; + + type Schema = RootNode<'static, Database, EmptyMutation>; + + #[get("/?")] + fn get_graphql_handler( + context: State, + request: super::GraphQLRequest, + schema: State, + ) -> super::GraphQLResponse { + request.execute(&schema, &context) + } + + #[post("/", data="")] + fn post_graphql_handler( + context: State, + request: super::GraphQLRequest, + schema: State, + ) -> super::GraphQLResponse { + request.execute(&schema, &context) + } + + struct TestRocketIntegration { + rocket: Rocket, + } + + impl http_tests::HTTPIntegration for TestRocketIntegration + { + fn get(&self, url: &str) -> http_tests::TestResponse { + make_test_response(&self.rocket, MockRequest::new( + Method::Get, + url)) + } + + fn post(&self, url: &str, body: &str) -> http_tests::TestResponse { + make_test_response( + &self.rocket, + MockRequest::new( + Method::Post, + url, + ).header(ContentType::JSON).body(body)) + } + } + + #[test] + fn test_rocket_integration() { + let integration = TestRocketIntegration { + rocket: make_rocket(), + }; + + http_tests::run_http_test_suite(&integration); + } + + fn make_rocket() -> Rocket { + rocket::ignite() + .manage(Database::new()) + .manage(Schema::new(Database::new(), EmptyMutation::::new())) + .mount("/", routes![post_graphql_handler, get_graphql_handler]) + } + + fn make_test_response<'r>(rocket: &'r Rocket, mut request: MockRequest<'r>) -> http_tests::TestResponse { + let mut response = request.dispatch_with(&rocket); + let status_code = response.status().code as i32; + let content_type = response.header_values("content-type").collect::>().into_iter().next() + .expect("No content type header from handler").to_owned(); + let body = response.body().expect("No body returned from GraphQL handler").into_string(); + + http_tests::TestResponse { + status_code: status_code, + body: body, + content_type: content_type, + } + } +} \ No newline at end of file