diff --git a/Cargo.lock b/Cargo.lock index d49b1cc166..809d65a101 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3527,6 +3527,7 @@ dependencies = [ "serde", "serde_json", "serde_path_to_error", + "serde_qs", "serde_urlencoded", "serde_yaml", "stripmargin", diff --git a/Cargo.toml b/Cargo.toml index c1aac19c26..68aa777403 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ derive_setters = "0.1.6" thiserror = "1.0.56" serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } +serde_qs = "0.12" serde_yaml = "0.9" serde_urlencoded = "0.7.1" url = { version = "2", features = ["serde"] } diff --git a/cloudflare/src/file.rs b/cloudflare/src/file.rs index a8f29cfd89..16f40381a6 100644 --- a/cloudflare/src/file.rs +++ b/cloudflare/src/file.rs @@ -22,7 +22,9 @@ impl CloudflareFileIO { } } -// TODO: avoid the unsafe impl +// Multi-threading is not enabled in Cloudflare, +// so this doesn't matter, and makes API compliance +// way easier. unsafe impl Sync for CloudflareFileIO {} unsafe impl Send for CloudflareFileIO {} diff --git a/cloudflare/src/handle.rs b/cloudflare/src/handle.rs index eb686e1906..43f9752db3 100644 --- a/cloudflare/src/handle.rs +++ b/cloudflare/src/handle.rs @@ -1,15 +1,12 @@ +use std::borrow::Borrow; use std::collections::HashMap; use std::rc::Rc; use std::sync::{Arc, RwLock}; -use anyhow::anyhow; +use hyper::{Body, Method, Request, Response}; use lazy_static::lazy_static; use tailcall::async_graphql_hyper::GraphQLRequest; -use tailcall::blueprint::Blueprint; -use tailcall::config::reader::ConfigReader; -use tailcall::config::ConfigSet; -use tailcall::http::{graphiql, handle_request, AppContext}; -use tailcall::target_runtime::TargetRuntime; +use tailcall::http::{graphiql, handle_request, showcase, AppContext}; use crate::http::{to_request, to_response}; use crate::init_runtime; @@ -20,67 +17,71 @@ lazy_static! { /// /// The handler which handles requests on cloudflare /// -pub async fn fetch(req: worker::Request, env: worker::Env) -> anyhow::Result { +pub async fn fetch( + req: worker::Request, + env: worker::Env, + _: worker::Context, +) -> anyhow::Result { log::info!( "{} {:?}", req.method().to_string(), req.url().map(|u| u.to_string()) ); - let env = Rc::new(env); - let hyper_req = to_request(req).await?; - if hyper_req.method() == hyper::Method::GET { - let response = graphiql(&hyper_req)?; - return to_response(response).await; - } - let query = hyper_req - .uri() - .query() - .ok_or(anyhow!("Unable parse extract query"))?; - let query = serde_qs::from_str::>(query)?; - let config_path = query - .get("config") - .ok_or(anyhow!("The key 'config' not found in the query"))? - .clone(); + let req = to_request(req).await?; - log::info!("config-url: {}", config_path); - let app_ctx = get_app_ctx(env, config_path.as_str()).await?; - let resp = handle_request::(hyper_req, app_ctx).await?; + // Quick exit to GraphiQL + // + // Has to be done here, since when using GraphiQL, a config query parameter is not specified, + // and get_app_ctx will fail without it. + if req.method() == Method::GET { + return to_response(graphiql(&req)?).await; + } + let env = Rc::new(env); + let app_ctx = match get_app_ctx(env, &req).await? { + Ok(app_ctx) => app_ctx, + Err(e) => return to_response(e).await, + }; + let resp = handle_request::(req, app_ctx).await?; to_response(resp).await } -/// -/// Reads the configuration from the CONFIG environment variable. -/// -async fn get_config(runtime: TargetRuntime, file_path: &str) -> anyhow::Result { - let reader = ConfigReader::init(runtime); - let config = reader.read(&file_path).await?; - Ok(config) -} - /// /// Initializes the worker once and caches the app context /// for future requests. /// -async fn get_app_ctx(env: Rc, file_path: &str) -> anyhow::Result> { +async fn get_app_ctx( + env: Rc, + req: &Request, +) -> anyhow::Result, Response>> { // Read context from cache - if let Some(app_ctx) = read_app_ctx() { - if app_ctx.0 == file_path { - log::info!("Using cached application context"); - return Ok(app_ctx.clone().1); + let file_path = req + .uri() + .query() + .and_then(|x| serde_qs::from_str::>(x).ok()) + .and_then(|x| x.get("config").cloned()); + + if let Some(file_path) = &file_path { + if let Some(app_ctx) = read_app_ctx() { + if app_ctx.0 == file_path.borrow() { + log::info!("Using cached application context"); + return Ok(Ok(app_ctx.clone().1)); + } } } - // Create new context + let runtime = init_runtime(env)?; - let cfg = get_config(runtime.clone(), file_path).await?; - log::info!("Configuration read ... ok"); - log::debug!("\n{}", cfg.to_sdl()); - let blueprint = Blueprint::try_from(&cfg)?; - log::info!("Blueprint generated ... ok"); - let app_ctx = Arc::new(AppContext::new(blueprint, runtime)); - *APP_CTX.write().unwrap() = Some((file_path.to_string(), app_ctx.clone())); - log::info!("Initialized new application context"); - Ok(app_ctx) + match showcase::create_app_ctx::(req, runtime, true).await? { + Ok(app_ctx) => { + let app_ctx: Arc = Arc::new(app_ctx); + if let Some(file_path) = file_path { + *APP_CTX.write().unwrap() = Some((file_path, app_ctx.clone())); + } + log::info!("Initialized new application context"); + Ok(Ok(app_ctx)) + } + Err(e) => Ok(Err(e)), + } } fn read_app_ctx() -> Option<(String, Arc)> { diff --git a/cloudflare/src/lib.rs b/cloudflare/src/lib.rs index 9edb6b3f3c..84dcf91422 100644 --- a/cloudflare/src/lib.rs +++ b/cloudflare/src/lib.rs @@ -18,7 +18,6 @@ pub fn init_env(env: Rc) -> Arc { } pub fn init_file(env: Rc, bucket_id: String) -> anyhow::Result> { - #[allow(clippy::arc_with_non_send_sync)] Ok(Arc::new(file::CloudflareFileIO::init(env, bucket_id)?)) } @@ -36,6 +35,9 @@ pub fn init_runtime(env: Rc) -> anyhow::Result { let bucket_id = env_io .get("BUCKET") .ok_or(anyhow!("BUCKET var is not set"))?; + + log::debug!("R2 Bucket ID: {}", bucket_id); + Ok(TargetRuntime { http: http.clone(), http2_only: http.clone(), @@ -49,9 +51,9 @@ pub fn init_runtime(env: Rc) -> anyhow::Result { async fn fetch( req: worker::Request, env: worker::Env, - _: worker::Context, + ctx: worker::Context, ) -> anyhow::Result { - let result = handle::fetch(req, env).await; + let result = handle::fetch(req, env, ctx).await; match result { Ok(response) => Ok(response), diff --git a/generated/.tailcallrc.graphql b/generated/.tailcallrc.graphql index 773870a667..1712d500c8 100644 --- a/generated/.tailcallrc.graphql +++ b/generated/.tailcallrc.graphql @@ -272,6 +272,10 @@ directive @server( """ script: Script """ + `showcase` enables the /showcase/graphql endpoint. + """ + showcase: Boolean + """ This configuration defines local variables for server operations. Useful for storing constant configurations, secrets, or shared information. """ diff --git a/generated/.tailcallrc.schema.json b/generated/.tailcallrc.schema.json index a7004ee807..902c509c10 100644 --- a/generated/.tailcallrc.schema.json +++ b/generated/.tailcallrc.schema.json @@ -1419,6 +1419,13 @@ ], "description": "A link to an external JS file that listens on every HTTP request response event." }, + "showcase": { + "description": "`showcase` enables the /showcase/graphql endpoint.", + "type": [ + "boolean", + "null" + ] + }, "vars": { "allOf": [ { diff --git a/src/blueprint/server.rs b/src/blueprint/server.rs index 5d5298b3c5..b374417324 100644 --- a/src/blueprint/server.rs +++ b/src/blueprint/server.rs @@ -18,6 +18,7 @@ pub struct Server { pub enable_query_validation: bool, pub enable_response_validation: bool, pub enable_batch_requests: bool, + pub enable_showcase: bool, pub global_response_timeout: i64, pub worker: usize, pub port: u16, @@ -100,6 +101,7 @@ impl TryFrom for Server { enable_query_validation: (config_server).enable_query_validation(), enable_response_validation: (config_server).enable_http_validation(), enable_batch_requests: (config_server).enable_batch_requests(), + enable_showcase: (config_server).enable_showcase(), global_response_timeout: (config_server).get_global_response_timeout(), http, worker: (config_server).get_workers(), diff --git a/src/cli/javascript/mod.rs b/src/cli/javascript/mod.rs index 789894e0b8..b662fc28ab 100644 --- a/src/cli/javascript/mod.rs +++ b/src/cli/javascript/mod.rs @@ -9,7 +9,7 @@ pub use runtime::Runtime; use crate::{blueprint, HttpIO}; -pub fn init_http(http: impl HttpIO, script: blueprint::Script) -> Arc { +pub fn init_http(http: impl HttpIO, script: blueprint::Script) -> Arc { let script_io = Runtime::new(script); Arc::new(HttpFilter::new(http, script_io)) } diff --git a/src/config/server.rs b/src/config/server.rs index 4fdddf4c37..68081ca2be 100644 --- a/src/config/server.rs +++ b/src/config/server.rs @@ -34,6 +34,9 @@ pub struct Server { /// `globalResponseTimeout` sets the maximum query duration before termination, acting as a safeguard against long-running queries. pub global_response_timeout: Option, #[serde(default, skip_serializing_if = "is_default")] + /// `showcase` enables the /showcase/graphql endpoint. + pub showcase: Option, + #[serde(default, skip_serializing_if = "is_default")] /// `workers` sets the number of worker threads. @default the number of system cores. pub workers: Option, #[serde(default, skip_serializing_if = "is_default")] @@ -118,6 +121,9 @@ impl Server { pub fn enable_batch_requests(&self) -> bool { self.batch_requests.unwrap_or(false) } + pub fn enable_showcase(&self) -> bool { + self.showcase.unwrap_or(false) + } pub fn get_hostname(&self) -> String { self.hostname.clone().unwrap_or("127.0.0.1".to_string()) @@ -150,6 +156,7 @@ impl Server { self.global_response_timeout = other .global_response_timeout .or(self.global_response_timeout); + self.showcase = other.showcase.or(self.showcase); self.workers = other.workers.or(self.workers); self.port = other.port.or(self.port); self.hostname = other.hostname.or(self.hostname); diff --git a/src/http/mod.rs b/src/http/mod.rs index c6e441be92..17a137bd25 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -7,6 +7,7 @@ mod request_context; mod request_handler; mod request_template; mod response; +pub mod showcase; pub use cache::*; pub use data_loader::*; diff --git a/src/http/request_context.rs b/src/http/request_context.rs index 6ad2a7441e..12d0abd607 100644 --- a/src/http/request_context.rs +++ b/src/http/request_context.rs @@ -98,20 +98,20 @@ impl RequestContext { } impl From<&AppContext> for RequestContext { - fn from(server_ctx: &AppContext) -> Self { + fn from(app_ctx: &AppContext) -> Self { Self { - h_client: server_ctx.runtime.http.clone(), - h2_client: server_ctx.runtime.http2_only.clone(), - server: server_ctx.blueprint.server.clone(), - upstream: server_ctx.blueprint.upstream.clone(), + h_client: app_ctx.runtime.http.clone(), + h2_client: app_ctx.runtime.http2_only.clone(), + server: app_ctx.blueprint.server.clone(), + upstream: app_ctx.blueprint.upstream.clone(), req_headers: HeaderMap::new(), - http_data_loaders: server_ctx.http_data_loaders.clone(), - gql_data_loaders: server_ctx.gql_data_loaders.clone(), - cache: server_ctx.runtime.cache.clone(), - grpc_data_loaders: server_ctx.grpc_data_loaders.clone(), + http_data_loaders: app_ctx.http_data_loaders.clone(), + gql_data_loaders: app_ctx.gql_data_loaders.clone(), + cache: app_ctx.runtime.cache.clone(), + grpc_data_loaders: app_ctx.grpc_data_loaders.clone(), min_max_age: Arc::new(Mutex::new(None)), cache_public: Arc::new(Mutex::new(None)), - env_vars: server_ctx.runtime.env.clone(), + env_vars: app_ctx.runtime.env.clone(), } } } diff --git a/src/http/request_handler.rs b/src/http/request_handler.rs index 783335d7e8..79e92f2f64 100644 --- a/src/http/request_handler.rs +++ b/src/http/request_handler.rs @@ -9,7 +9,7 @@ use hyper::{Body, HeaderMap, Request, Response, StatusCode}; use serde::de::DeserializeOwned; use super::request_context::RequestContext; -use super::AppContext; +use super::{showcase, AppContext}; use crate::async_graphql_hyper::{GraphQLRequestLike, GraphQLResponse}; pub fn graphiql(req: &Request) -> Result> { @@ -36,19 +36,19 @@ fn not_found() -> Result> { .body(Body::empty())?) } -fn create_request_context(req: &Request, server_ctx: &AppContext) -> RequestContext { - let upstream = server_ctx.blueprint.upstream.clone(); +fn create_request_context(req: &Request, app_ctx: &AppContext) -> RequestContext { + let upstream = app_ctx.blueprint.upstream.clone(); let allowed = upstream.allowed_headers; let headers = create_allowed_headers(req.headers(), &allowed); - RequestContext::from(server_ctx).req_headers(headers) + RequestContext::from(app_ctx).req_headers(headers) } fn update_cache_control_header( response: GraphQLResponse, - server_ctx: &AppContext, + app_ctx: &AppContext, req_ctx: Arc, ) -> GraphQLResponse { - if server_ctx.blueprint.server.enable_cache_control_header { + if app_ctx.blueprint.server.enable_cache_control_header { let ttl = req_ctx.get_min_max_age().unwrap_or(0); let cache_public_flag = req_ctx.is_cache_public().unwrap_or(true); return response.set_cache_control(ttl, cache_public_flag); @@ -56,29 +56,26 @@ fn update_cache_control_header( response } -pub fn update_response_headers(resp: &mut hyper::Response, server_ctx: &AppContext) { - if !server_ctx.blueprint.server.response_headers.is_empty() { +pub fn update_response_headers(resp: &mut hyper::Response, app_ctx: &AppContext) { + if !app_ctx.blueprint.server.response_headers.is_empty() { resp.headers_mut() - .extend(server_ctx.blueprint.server.response_headers.clone()); + .extend(app_ctx.blueprint.server.response_headers.clone()); } } pub async fn graphql_request( req: Request, - server_ctx: &AppContext, + app_ctx: &AppContext, ) -> Result> { - let req_ctx = Arc::new(create_request_context(&req, server_ctx)); + let req_ctx = Arc::new(create_request_context(&req, app_ctx)); let bytes = hyper::body::to_bytes(req.into_body()).await?; let request = serde_json::from_slice::(&bytes); match request { Ok(request) => { - let mut response = request - .data(req_ctx.clone()) - .execute(&server_ctx.schema) - .await; - response = update_cache_control_header(response, server_ctx, req_ctx); + let mut response = request.data(req_ctx.clone()).execute(&app_ctx.schema).await; + response = update_cache_control_header(response, app_ctx, req_ctx); let mut resp = response.to_response()?; - update_response_headers(&mut resp, server_ctx); + update_response_headers(&mut resp, app_ctx); Ok(resp) } Err(err) => { @@ -110,13 +107,29 @@ fn create_allowed_headers(headers: &HeaderMap, allowed: &BTreeSet) -> He pub async fn handle_request( req: Request, - state: Arc, + app_ctx: Arc, ) -> Result> { match *req.method() { - hyper::Method::POST if req.uri().path().ends_with("/graphql") => { - graphql_request::(req, state.as_ref()).await + // NOTE: + // The first check for the route should be for `/graphql` + // This is always going to be the most used route. + hyper::Method::POST if req.uri().path() == "/graphql" => { + graphql_request::(req, app_ctx.as_ref()).await } - hyper::Method::GET if state.blueprint.server.enable_graphiql => graphiql(&req), + hyper::Method::POST + if app_ctx.blueprint.server.enable_showcase + && req.uri().path() == "/showcase/graphql" => + { + let app_ctx = + match showcase::create_app_ctx::(&req, app_ctx.runtime.clone(), false).await? { + Ok(app_ctx) => app_ctx, + Err(res) => return Ok(res), + }; + + graphql_request::(req, &app_ctx).await + } + + hyper::Method::GET if app_ctx.blueprint.server.enable_graphiql => graphiql(&req), _ => not_found(), } } diff --git a/src/http/showcase.rs b/src/http/showcase.rs new file mode 100644 index 0000000000..b289cb1dc8 --- /dev/null +++ b/src/http/showcase.rs @@ -0,0 +1,113 @@ +use std::collections::HashMap; + +use anyhow::Result; +use async_graphql::ServerError; +use hyper::{Body, Request, Response}; +use serde::de::DeserializeOwned; +use url::Url; + +use super::AppContext; +use crate::async_graphql_hyper::{GraphQLRequestLike, GraphQLResponse}; +use crate::blueprint::Blueprint; +use crate::config::reader::ConfigReader; +use crate::target_runtime::TargetRuntime; + +pub async fn create_app_ctx( + req: &Request, + runtime: TargetRuntime, + enable_fs: bool, +) -> Result>> { + let config_url = req + .uri() + .query() + .and_then(|x| serde_qs::from_str::>(x).ok()) + .and_then(|x| x.get("config").cloned()); + + let config_url = if let Some(config_url) = config_url { + config_url + } else { + let mut response = async_graphql::Response::default(); + let server_error = ServerError::new("No Config URL specified", None); + response.errors = vec![server_error]; + return Ok(Err(GraphQLResponse::from(response).to_response()?)); + }; + + if !enable_fs && Url::parse(&config_url).is_err() { + let mut response = async_graphql::Response::default(); + let server_error = ServerError::new("Invalid Config URL specified", None); + response.errors = vec![server_error]; + return Ok(Err(GraphQLResponse::from(response).to_response()?)); + } + + let reader = ConfigReader::init(runtime.clone()); + let config = match reader.read(config_url).await { + Ok(config) => config, + Err(e) => { + let mut response = async_graphql::Response::default(); + let server_error = ServerError::new(format!("Failed to read config: {}", e), None); + response.errors = vec![server_error]; + return Ok(Err(GraphQLResponse::from(response).to_response()?)); + } + }; + + let blueprint = match Blueprint::try_from(&config) { + Ok(blueprint) => blueprint, + Err(e) => { + let mut response = async_graphql::Response::default(); + let server_error = ServerError::new(format!("{}", e), None); + response.errors = vec![server_error]; + return Ok(Err(GraphQLResponse::from(response).to_response()?)); + } + }; + + Ok(Ok(AppContext::new(blueprint, runtime))) +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use hyper::Request; + use serde_json::json; + + use crate::async_graphql_hyper::GraphQLRequest; + use crate::blueprint::Upstream; + use crate::cli::init_runtime; + use crate::http::handle_request; + use crate::http::showcase::create_app_ctx; + + #[tokio::test] + async fn works_with_file() { + let req = Request::builder() + .method("POST") + .uri("http://upstream/showcase/graphql?config=.%2Ftests%2Fhttp%2Fconfig%2Fsimple.graphql") + .body(hyper::Body::from(json!({ + "query": "query { user { name } }" + }).to_string())) + .unwrap(); + + let runtime = init_runtime(&Upstream::default(), None); + let app = create_app_ctx::(&req, runtime, true) + .await + .unwrap() + .unwrap(); + + let req = Request::builder() + .method("POST") + .uri("http://upstream/graphql?config=.%2Ftests%2Fhttp%2Fconfig%2Fsimple.graphql") + .body(hyper::Body::from( + json!({ + "query": "query { user { name } }" + }) + .to_string(), + )) + .unwrap(); + + let res = handle_request::(req, Arc::new(app)) + .await + .unwrap(); + + println!("{:#?}", res); + assert!(res.status().is_success()) + } +} diff --git a/testconv/src/common.rs b/testconv/src/common.rs index 1067ff145a..e1037e6be9 100644 --- a/testconv/src/common.rs +++ b/testconv/src/common.rs @@ -36,7 +36,11 @@ pub struct APIResponse { #[serde(skip_serializing_if = "IndexMap::is_empty")] pub headers: IndexMap, #[serde(default)] + #[serde(skip_serializing_if = "serde_json::Value::is_null")] pub body: serde_json::Value, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + text_body: Option, } fn default_status() -> u16 { diff --git a/tests/execution/showcase.md b/tests/execution/showcase.md new file mode 100644 index 0000000000..a3c3fa4888 --- /dev/null +++ b/tests/execution/showcase.md @@ -0,0 +1,77 @@ +# Showcase GraphQL Request + +#### server: + +```graphql +schema @server(showcase: true) { + query: Query +} + +type User { + not_id: Int + not_name: String +} + +type Query { + not_user: User @http(path: "/users/1", baseURL: "http://jsonplaceholder.typicode.com") +} +``` + +#### mock: + +```yml +- request: + method: GET + url: http://jsonplaceholder.typicode.com/users/1 + headers: + test: test + body: null + response: + status: 200 + body: + id: 1 + name: foo +- request: + method: GET + url: http://example.com/simple.graphql + body: null + response: + status: 200 + textBody: |2- + schema { query: Query } + type User { id: Int name: String } + type Query { user: User @http(path: "/users/1", baseURL: "http://jsonplaceholder.typicode.com") } +- request: + method: GET + url: http://example.com/invalid.graphql + body: null + response: + status: 200 + body: dsjfsjdfjdsfjkdskjfjkds +``` + +#### assert: + +```yml +- method: POST + url: http://localhost:8080/showcase/graphql?config=http%3A%2F%2Fexample.com%2Fsimple.graphql + body: + query: query { user { name } } +- method: POST + url: http://localhost:8080/showcase/graphql + body: + query: query { user { name } } +- method: POST + url: http://localhost:8080/showcase/graphql?config=.%2Ftests%2Fhttp%2Fconfig%2Fsimple.graphql + body: + query: query { user { name } } +- method: POST + url: http://localhost:8080/showcase/graphql?config=http%3A%2F%2Fexample.com%2Finvalid.graphql + body: + query: query { user { name } } +- method: POST + url: http://localhost:8080/showcase/graphql?config=http%3A%2F%2Fexample.com%2Fsimple.graphql + body: + query: + foo: bar +``` diff --git a/tests/execution_spec.rs b/tests/execution_spec.rs index bd188c6243..d31b095235 100644 --- a/tests/execution_spec.rs +++ b/tests/execution_spec.rs @@ -58,6 +58,9 @@ struct APIResponse { headers: BTreeMap, #[serde(default)] body: serde_json::Value, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + text_body: Option, } pub struct Env { @@ -544,15 +547,20 @@ impl HttpIO for MockHttpClient { } // Special Handling for GRPC - if is_grpc { + if let Some(body) = mock_response.0.text_body { + // Return plaintext body if specified + let body = string_to_bytes(&body); + response.body = Bytes::from_iter(body); + } else if is_grpc { + // Special Handling for GRPC let body = string_to_bytes(mock_response.0.body.as_str().unwrap()); response.body = Bytes::from_iter(body); - Ok(response) } else { let body = serde_json::to_vec(&mock_response.0.body)?; response.body = Bytes::from_iter(body); - Ok(response) } + + Ok(response) } } @@ -734,6 +742,7 @@ async fn assert_spec(spec: ExecutionSpec) { &hyper::body::to_bytes(response.into_body()).await.unwrap(), ) .unwrap(), + text_body: None, }; let snapshot_name = format!("{}_assert_{}", spec.safe_name, i); diff --git a/tests/graphql_spec.rs b/tests/graphql_spec.rs index 8b67e7ec75..99fdbc6818 100644 --- a/tests/graphql_spec.rs +++ b/tests/graphql_spec.rs @@ -329,8 +329,8 @@ async fn test_execution() -> std::io::Result<()> { .to_result() .unwrap(); let runtime = init_runtime(&blueprint.upstream, None); - let server_ctx = AppContext::new(blueprint, runtime); - let schema = &server_ctx.schema; + let app_ctx = AppContext::new(blueprint, runtime); + let schema = &app_ctx.schema; for q in spec.test_queries { let mut headers = HeaderMap::new(); @@ -338,7 +338,7 @@ async fn test_execution() -> std::io::Result<()> { HeaderName::from_static("authorization"), HeaderValue::from_static("1"), ); - let req_ctx = Arc::new(RequestContext::from(&server_ctx).req_headers(headers)); + let req_ctx = Arc::new(RequestContext::from(&app_ctx).req_headers(headers)); let req = Request::from(q.query.as_str()).data(req_ctx.clone()); let res = schema.execute(req).await; let json = serde_json::to_string(&res).unwrap(); diff --git a/tests/http/config/showcase.graphql b/tests/http/config/showcase.graphql new file mode 100644 index 0000000000..57d3936ba4 --- /dev/null +++ b/tests/http/config/showcase.graphql @@ -0,0 +1,12 @@ +schema @server(showcase: true) { + query: Query +} + +type User { + not_id: Int + not_name: String +} + +type Query { + not_user: User @http(path: "/users/1", baseURL: "http://jsonplaceholder.typicode.com") +} diff --git a/tests/http_spec.rs b/tests/http_spec.rs index 66e3932926..20219a5649 100644 --- a/tests/http_spec.rs +++ b/tests/http_spec.rs @@ -57,6 +57,8 @@ struct APIResponse { headers: BTreeMap, #[serde(default)] body: serde_json::Value, + #[serde(default)] + text_body: Option, } pub struct Env { @@ -325,16 +327,20 @@ impl HttpIO for MockHttpClient { response.headers.insert(header_name, header_value); } - // Special Handling for GRPC - if is_grpc { + if let Some(body) = mock_response.0.text_body { + // Return plaintext body if specified + let body = string_to_bytes(&body); + response.body = Bytes::from_iter(body); + } else if is_grpc { + // Special Handling for GRPC let body = string_to_bytes(mock_response.0.body.as_str().unwrap()); response.body = Bytes::from_iter(body); - Ok(response) } else { let body = serde_json::to_vec(&mock_response.0.body)?; response.body = Bytes::from_iter(body); - Ok(response) } + + Ok(response) } } diff --git a/tests/snapshots/execution_spec__showcase.md_assert_0.snap b/tests/snapshots/execution_spec__showcase.md_assert_0.snap new file mode 100644 index 0000000000..c1d6f66212 --- /dev/null +++ b/tests/snapshots/execution_spec__showcase.md_assert_0.snap @@ -0,0 +1,17 @@ +--- +source: tests/execution_spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "user": { + "name": "foo" + } + } + } +} diff --git a/tests/snapshots/execution_spec__showcase.md_assert_1.snap b/tests/snapshots/execution_spec__showcase.md_assert_1.snap new file mode 100644 index 0000000000..b8e6363028 --- /dev/null +++ b/tests/snapshots/execution_spec__showcase.md_assert_1.snap @@ -0,0 +1,18 @@ +--- +source: tests/execution_spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": null, + "errors": [ + { + "message": "No Config URL specified" + } + ] + } +} diff --git a/tests/snapshots/execution_spec__showcase.md_assert_2.snap b/tests/snapshots/execution_spec__showcase.md_assert_2.snap new file mode 100644 index 0000000000..33505dda26 --- /dev/null +++ b/tests/snapshots/execution_spec__showcase.md_assert_2.snap @@ -0,0 +1,18 @@ +--- +source: tests/execution_spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": null, + "errors": [ + { + "message": "Invalid Config URL specified" + } + ] + } +} diff --git a/tests/snapshots/execution_spec__showcase.md_assert_3.snap b/tests/snapshots/execution_spec__showcase.md_assert_3.snap new file mode 100644 index 0000000000..645d519413 --- /dev/null +++ b/tests/snapshots/execution_spec__showcase.md_assert_3.snap @@ -0,0 +1,18 @@ +--- +source: tests/execution_spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": null, + "errors": [ + { + "message": "Failed to read config: Validation Error\n• --> 1:1\n |\n1 | \"dsjfsjdfjdsfjkdskjfjkds\"\n | ^---\n |\n = expected type_system_definition\n" + } + ] + } +} diff --git a/tests/snapshots/execution_spec__showcase.md_assert_4.snap b/tests/snapshots/execution_spec__showcase.md_assert_4.snap new file mode 100644 index 0000000000..1b88aa8980 --- /dev/null +++ b/tests/snapshots/execution_spec__showcase.md_assert_4.snap @@ -0,0 +1,18 @@ +--- +source: tests/execution_spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": null, + "errors": [ + { + "message": "Unexpected GraphQL Request: invalid type: map, expected a string at line 1 column 9" + } + ] + } +} diff --git a/tests/snapshots/execution_spec__showcase.md_client.snap b/tests/snapshots/execution_spec__showcase.md_client.snap new file mode 100644 index 0000000000..966bfb4895 --- /dev/null +++ b/tests/snapshots/execution_spec__showcase.md_client.snap @@ -0,0 +1,16 @@ +--- +source: tests/execution_spec.rs +expression: client +--- +type Query { + not_user: User +} + +type User { + not_id: Int + not_name: String +} + +schema { + query: Query +} diff --git a/tests/snapshots/execution_spec__showcase.md_merged.snap b/tests/snapshots/execution_spec__showcase.md_merged.snap new file mode 100644 index 0000000000..403c78fe18 --- /dev/null +++ b/tests/snapshots/execution_spec__showcase.md_merged.snap @@ -0,0 +1,16 @@ +--- +source: tests/execution_spec.rs +expression: merged +--- +schema @server(showcase: true) @upstream { + query: Query +} + +type Query { + not_user: User @http(baseURL: "http://jsonplaceholder.typicode.com", path: "/users/1") +} + +type User { + not_id: Int + not_name: String +}