Skip to content
4 changes: 4 additions & 0 deletions juniper/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
85 changes: 57 additions & 28 deletions juniper/src/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: HTTPIntegration>(integration: &T) {
pub fn run_http_test_suite<T: HttpIntegration>(integration: &T) {
println!("Running HTTP Test suite for integration");

println!(" - test_simple_get");
Expand Down Expand Up @@ -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 {
Expand All @@ -416,7 +430,7 @@ pub mod tests {
.expect("Could not parse JSON object")
}

fn test_simple_get<T: HTTPIntegration>(integration: &T) {
fn test_simple_get<T: HttpIntegration>(integration: &T) {
// {hero{name}}
let response = integration.get("/?query=%7Bhero%7Bname%7D%7D");

Expand All @@ -430,7 +444,7 @@ pub mod tests {
);
}

fn test_encoded_get<T: HTTPIntegration>(integration: &T) {
fn test_encoded_get<T: HttpIntegration>(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");
Expand Down Expand Up @@ -460,7 +474,7 @@ pub mod tests {
);
}

fn test_get_with_variables<T: HTTPIntegration>(integration: &T) {
fn test_get_with_variables<T: HttpIntegration>(integration: &T) {
// query($id: String!) { human(id: $id) { id, name, appearsIn, homePlanet } }
// with variables = { "id": "1000" }
let response = integration.get(
Expand Down Expand Up @@ -491,21 +505,21 @@ pub mod tests {
);
}

fn test_simple_post<T: HTTPIntegration>(integration: &T) {
let response = integration.post("/", r#"{"query": "{hero{name}}"}"#);
fn test_simple_post<T: HttpIntegration>(integration: &T) {
let response = integration.post_json("/", r#"{"query": "{hero{name}}"}"#);

assert_eq!(response.status_code, 200);
assert_eq!(response.content_type, "application/json");

assert_eq!(
unwrap_json_response(&response),
serde_json::from_str::<Json>(r#"{"data": {"hero": {"name": "R2-D2"}}}"#)
.expect("Invalid JSON constant in test")
.expect("Invalid JSON constant in test"),
);
}

fn test_batched_post<T: HTTPIntegration>(integration: &T) {
let response = integration.post(
fn test_batched_post<T: HttpIntegration>(integration: &T) {
let response = integration.post_json(
"/",
r#"[{"query": "{hero{name}}"}, {"query": "{hero{name}}"}]"#,
);
Expand All @@ -516,42 +530,57 @@ pub mod tests {
assert_eq!(
unwrap_json_response(&response),
serde_json::from_str::<Json>(
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<T: HTTPIntegration>(integration: &T) {
let response = integration.post("/", "[]");
fn test_empty_batched_post<T: HttpIntegration>(integration: &T) {
let response = integration.post_json("/", "[]");
assert_eq!(response.status_code, 400);
}

fn test_invalid_json<T: HTTPIntegration>(integration: &T) {
fn test_invalid_json<T: HttpIntegration>(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<T: HTTPIntegration>(integration: &T) {
fn test_invalid_field<T: HttpIntegration>(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<T: HTTPIntegration>(integration: &T) {
fn test_duplicate_keys<T: HttpIntegration>(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<T: HttpIntegration>(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::<Json>(r#"{"data": {"hero": {"name": "R2-D2"}}}"#)
.expect("Invalid JSON constant in test"),
);
}

fn test_invalid_graphql_post<T: HttpIntegration>(integration: &T) {
let resp = integration.post_graphql("/", r#"{hero{name}"#);

assert_eq!(resp.status_code, 400);
}
}
105 changes: 48 additions & 57 deletions juniper_actix/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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::<GraphQLBatchRequest<S>>(&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::<GraphQLBatchRequest<S>>(&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`",
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -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(
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::<Database>::new(),
EmptySubscription::<Database>::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;
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
})
}
}

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::<Database>::new(),
EmptySubscription::<Database>::new(),
);
impl HttpIntegration for TestActixWebIntegration {
fn get(&self, url: &str) -> TestResponse {
self.make_request(test::TestRequest::get().uri(url))
}

let req = test::TestRequest::post()
fn post_json(&self, url: &str, body: &str) -> TestResponse {
self.make_request(
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;
.set_payload(body.to_string())
.uri(url),
)
}

let resp = test::call_service(&mut app, req).await;
let test_response = make_test_response(resp).await;
test_response
})
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 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();
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),
Expand All @@ -512,6 +503,6 @@ mod tests {

#[test]
fn test_actix_web_integration() {
run_http_test_suite(&TestActixWebIntegration {});
run_http_test_suite(&TestActixWebIntegration);
}
}
4 changes: 3 additions & 1 deletion juniper_hyper/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading