Add actix-web integration (#603)
This commit is contained in:
parent
2ba9f71b0c
commit
a47d1c5430
9 changed files with 684 additions and 0 deletions
|
@ -12,6 +12,7 @@ members = [
|
|||
"juniper_rocket_async",
|
||||
"juniper_subscriptions",
|
||||
"juniper_warp",
|
||||
"juniper_actix",
|
||||
]
|
||||
exclude = [
|
||||
"docs/book/tests",
|
||||
|
|
|
@ -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}}\""},
|
||||
]
|
||||
|
|
4
juniper_actix/.gitignore
vendored
Normal file
4
juniper_actix/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
/target
|
||||
/examples/**/target/**/*
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
3
juniper_actix/CHANGELOG.md
Normal file
3
juniper_actix/CHANGELOG.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# master
|
||||
|
||||
- Initial Release
|
33
juniper_actix/Cargo.toml
Normal file
33
juniper_actix/Cargo.toml
Normal file
|
@ -0,0 +1,33 @@
|
|||
[package]
|
||||
name = "juniper_actix"
|
||||
version = "0.1.0"
|
||||
authors = ["Jordao Rosario <jordao.rosario01@gmail.com>"]
|
||||
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"
|
25
juniper_actix/LICENSE
Normal file
25
juniper_actix/LICENSE
Normal file
|
@ -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.
|
34
juniper_actix/README.md
Normal file
34
juniper_actix/README.md
Normal file
|
@ -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
|
64
juniper_actix/examples/actix_server.rs
Normal file
64
juniper_actix/examples/actix_server.rs
Normal file
|
@ -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<Database>, EmptySubscription<Database>>;
|
||||
|
||||
fn schema() -> Schema {
|
||||
Schema::new(
|
||||
Query,
|
||||
EmptyMutation::<Database>::new(),
|
||||
EmptySubscription::<Database>::new(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn graphiql_handler() -> Result<HttpResponse, Error> {
|
||||
gqli_handler("/", None).await
|
||||
}
|
||||
async fn playground_handler() -> Result<HttpResponse, Error> {
|
||||
play_handler("/", None).await
|
||||
}
|
||||
async fn graphql(
|
||||
req: actix_web::HttpRequest,
|
||||
payload: actix_web::web::Payload,
|
||||
schema: web::Data<Schema>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
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
|
||||
}
|
517
juniper_actix/src/lib.rs
Normal file
517
juniper_actix/src/lib.rs
Normal file
|
@ -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<String>,
|
||||
variables: Option<String>,
|
||||
}
|
||||
|
||||
impl<S> From<GetGraphQLRequest> for GraphQLRequest<S>
|
||||
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<Query, Mutation, Subscription, Context, S>(
|
||||
schema: &juniper::RootNode<'static, Query, Mutation, Subscription, S>,
|
||||
context: &Context,
|
||||
req: HttpRequest,
|
||||
payload: actix_web::web::Payload,
|
||||
) -> Result<HttpResponse, Error>
|
||||
where
|
||||
S: ScalarValue + Send + Sync + 'static,
|
||||
Context: Send + Sync + 'static,
|
||||
Query: juniper::GraphQLTypeAsync<S, Context = Context> + Send + Sync + 'static,
|
||||
Query::TypeInfo: Send + Sync,
|
||||
Mutation: juniper::GraphQLTypeAsync<S, Context = Context> + Send + Sync + 'static,
|
||||
Mutation::TypeInfo: Send + Sync,
|
||||
Subscription: juniper::GraphQLSubscriptionType<S, Context = Context> + 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<Query, Mutation, Subscription, Context, S>(
|
||||
schema: &juniper::RootNode<'static, Query, Mutation, Subscription, S>,
|
||||
context: &Context,
|
||||
req: HttpRequest,
|
||||
) -> Result<HttpResponse, Error>
|
||||
where
|
||||
S: ScalarValue + Send + Sync + 'static,
|
||||
Context: Send + Sync + 'static,
|
||||
Query: juniper::GraphQLTypeAsync<S, Context = Context> + Send + Sync + 'static,
|
||||
Query::TypeInfo: Send + Sync,
|
||||
Mutation: juniper::GraphQLTypeAsync<S, Context = Context> + Send + Sync + 'static,
|
||||
Mutation::TypeInfo: Send + Sync,
|
||||
Subscription: juniper::GraphQLSubscriptionType<S, Context = Context> + Send + Sync + 'static,
|
||||
Subscription::TypeInfo: Send + Sync,
|
||||
{
|
||||
let get_req = web::Query::<GetGraphQLRequest>::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<Query, Mutation, Subscription, Context, S>(
|
||||
schema: &juniper::RootNode<'static, Query, Mutation, Subscription, S>,
|
||||
context: &Context,
|
||||
req: HttpRequest,
|
||||
payload: actix_web::web::Payload,
|
||||
) -> Result<HttpResponse, Error>
|
||||
where
|
||||
S: ScalarValue + Send + Sync + 'static,
|
||||
Context: Send + Sync + 'static,
|
||||
Query: juniper::GraphQLTypeAsync<S, Context = Context> + Send + Sync + 'static,
|
||||
Query::TypeInfo: Send + Sync,
|
||||
Mutation: juniper::GraphQLTypeAsync<S, Context = Context> + Send + Sync + 'static,
|
||||
Mutation::TypeInfo: Send + Sync,
|
||||
Subscription: juniper::GraphQLSubscriptionType<S, Context = Context> + 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::<GraphQLBatchRequest<S>>(&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<HttpResponse, Error> {
|
||||
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<HttpResponse, Error> {
|
||||
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<Database>, EmptySubscription<Database>>;
|
||||
|
||||
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<Schema>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
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<HttpResponse, Error> {
|
||||
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<HttpResponse, Error> {
|
||||
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("<script>var GRAPHQL_URL = '/dogs-api/graphql';</script>"));
|
||||
assert!(body.contains(
|
||||
"<script>var GRAPHQL_SUBSCRIPTIONS_URL = '/dogs-api/subscriptions';</script>"
|
||||
))
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn playground_endpoint_matches() {
|
||||
async fn graphql_handler() -> Result<HttpResponse, Error> {
|
||||
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<HttpResponse, Error> {
|
||||
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::<Database>::new(),
|
||||
EmptySubscription::<Database>::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::<Database>::new(),
|
||||
EmptySubscription::<Database>::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::<Database>::new(),
|
||||
EmptySubscription::<Database>::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<GraphQLBatchRequest, _> = 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::<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;
|
||||
|
||||
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::<Database>::new(),
|
||||
EmptySubscription::<Database>::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 {});
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue