diff --git a/tests/core/http.rs b/tests/core/http.rs index 09e8020ea3..55bb972cd0 100644 --- a/tests/core/http.rs +++ b/tests/core/http.rs @@ -101,6 +101,11 @@ impl HttpIO for Http { self.spec_path ))?; + if let Some(delay) = execution_mock.mock.delay { + // add delay to the request if there's a delay in the mock. + let _ = tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await; + } + execution_mock.actual_hits.fetch_add(1, Ordering::Relaxed); // Clone the response from the mock to avoid borrowing issues. diff --git a/tests/core/model.rs b/tests/core/model.rs index bac0edc406..d237f480d1 100644 --- a/tests/core/model.rs +++ b/tests/core/model.rs @@ -28,6 +28,10 @@ mod default { 1 } + pub fn concurrency() -> usize { + 1 + } + pub fn assert_hits() -> bool { true } @@ -42,6 +46,8 @@ pub struct Mock { pub assert_hits: bool, #[serde(default = "default::expected_hits")] pub expected_hits: usize, + #[serde(default)] + pub delay: Option, } #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] @@ -57,6 +63,8 @@ pub struct APIRequest { pub test_traces: bool, #[serde(default)] pub test_metrics: bool, + #[serde(default = "default::concurrency")] + pub concurrency: usize, } #[derive(Serialize, Deserialize, Clone, Debug)] diff --git a/tests/core/snapshots/test-dedupe.md_0.snap b/tests/core/snapshots/test-dedupe.md_0.snap new file mode 100644 index 0000000000..888b1eb108 --- /dev/null +++ b/tests/core/snapshots/test-dedupe.md_0.snap @@ -0,0 +1,40 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "posts": [ + { + "id": 1, + "userId": 1, + "user": { + "id": 1, + "name": "user-1" + }, + "duplicateUser": { + "id": 1, + "name": "user-1" + } + }, + { + "id": 2, + "userId": 2, + "user": { + "id": 2, + "name": "user-2" + }, + "duplicateUser": { + "id": 2, + "name": "user-2" + } + } + ] + } + } +} diff --git a/tests/core/snapshots/test-dedupe.md_client.snap b/tests/core/snapshots/test-dedupe.md_client.snap new file mode 100644 index 0000000000..fd9e443470 --- /dev/null +++ b/tests/core/snapshots/test-dedupe.md_client.snap @@ -0,0 +1,24 @@ +--- +source: tests/core/spec.rs +expression: formatted +--- +type Post { + body: String + id: Int + title: String + user: User + userId: Int! +} + +type Query { + posts: [Post] +} + +type User { + id: Int + name: String +} + +schema { + query: Query +} diff --git a/tests/core/snapshots/test-dedupe.md_merged.snap b/tests/core/snapshots/test-dedupe.md_merged.snap new file mode 100644 index 0000000000..ee9bcdee4d --- /dev/null +++ b/tests/core/snapshots/test-dedupe.md_merged.snap @@ -0,0 +1,30 @@ +--- +source: tests/core/spec.rs +expression: formatter +--- +schema @server(port: 8000) @upstream(batch: {delay: 1, headers: []}) { + query: Query +} + +type Post { + body: String + id: Int + title: String + user: User + @http( + url: "http://jsonplaceholder.typicode.com/users" + batchKey: ["id"] + query: [{key: "id", value: "{{.value.userId}}"}] + dedupe: true + ) + userId: Int! +} + +type Query { + posts: [Post] @http(url: "http://jsonplaceholder.typicode.com/posts?id=1", dedupe: true) +} + +type User { + id: Int + name: String +} diff --git a/tests/core/spec.rs b/tests/core/spec.rs index 6bc43656bb..2866aefa4c 100644 --- a/tests/core/spec.rs +++ b/tests/core/spec.rs @@ -8,7 +8,7 @@ use std::{fs, panic}; use anyhow::Context; use colored::Colorize; use futures_util::future::join_all; -use http::Request; +use http::{Request, Response}; use hyper::Body; use serde::{Deserialize, Serialize}; use tailcall::core::app_context::AppContext; @@ -282,29 +282,63 @@ async fn run_test( app_ctx: Arc, request: &APIRequest, ) -> anyhow::Result> { - let body = request - .body - .as_ref() - .map(|body| Body::from(body.to_bytes())) - .unwrap_or_default(); - - let method = request.method.clone(); - let headers = request.headers.clone(); - let url = request.url.clone(); - let req = headers + let request_count = request.concurrency; + + let futures = (0..request_count).map(|_| { + let app_ctx = app_ctx.clone(); + let body = request + .body + .as_ref() + .map(|body| Body::from(body.to_bytes())) + .unwrap_or_default(); + + let method = request.method.clone(); + let headers = request.headers.clone(); + let url = request.url.clone(); + + tokio::spawn(async move { + let req = headers + .into_iter() + .fold( + Request::builder() + .method(method.to_hyper()) + .uri(url.as_str()), + |acc, (key, value)| acc.header(key, value), + ) + .body(body)?; + + if app_ctx.blueprint.server.enable_batch_requests { + handle_request::(req, app_ctx).await + } else { + handle_request::(req, app_ctx).await + } + }) + }); + + let responses = join_all(futures).await; + + // Unwrap the Result from join_all and the individual task results + let responses = responses .into_iter() - .fold( - Request::builder() - .method(method.to_hyper()) - .uri(url.as_str()), - |acc, (key, value)| acc.header(key, value), - ) - .body(body)?; - - // TODO: reuse logic from server.rs to select the correct handler - if app_ctx.blueprint.server.enable_batch_requests { - handle_request::(req, app_ctx).await - } else { - handle_request::(req, app_ctx).await + .map(|res| res.map_err(anyhow::Error::from).and_then(|inner| inner)) + .collect::, _>>()?; + + let mut base_response = None; + + // ensure all the received responses are the same. + for response in responses { + let (head, body) = response.into_parts(); + let body = hyper::body::to_bytes(body).await?; + + if let Some((_, base_body)) = &base_response { + if *base_body != body { + return Err(anyhow::anyhow!("Responses are not the same.")); + } + } else { + base_response = Some((head, body)); + } } + + let (head, body) = base_response.ok_or_else(|| anyhow::anyhow!("No Response received."))?; + Ok(Response::from_parts(head, Body::from(body))) } diff --git a/tests/execution/test-dedupe.md b/tests/execution/test-dedupe.md new file mode 100644 index 0000000000..f041c5615f --- /dev/null +++ b/tests/execution/test-dedupe.md @@ -0,0 +1,65 @@ +# testing dedupe functionality + +```graphql @config +schema @server(port: 8000) @upstream(batch: {delay: 1}) { + query: Query +} + +type Query { + posts: [Post] @http(url: "http://jsonplaceholder.typicode.com/posts?id=1", dedupe: true) +} + +type Post { + id: Int + title: String + body: String + userId: Int! + user: User + @http( + url: "http://jsonplaceholder.typicode.com/users" + query: [{key: "id", value: "{{.value.userId}}"}] + batchKey: ["id"] + dedupe: true + ) +} + +type User { + id: Int + name: String +} +``` + +```yml @mock +- request: + method: GET + url: http://jsonplaceholder.typicode.com/posts?id=1 + expectedHits: 1 + delay: 10 + response: + status: 200 + body: + - id: 1 + userId: 1 + - id: 2 + userId: 2 +- request: + method: GET + url: http://jsonplaceholder.typicode.com/users?id=1&id=2 + expectedHits: 1 + delay: 10 + response: + status: 200 + body: + - id: 1 + name: user-1 + - id: 2 + name: user-2 +``` + +```yml @test +- method: POST + url: http://localhost:8080/graphql + concurrency: 10 + body: + query: query { posts { id, userId user { id name } duplicateUser:user { id name } } } +```