use crate::services::PipelineStep;
macro_rules! assert_counter_zero_or_absent {
($($arg:tt)*) => {{
let result = std::panic::catch_unwind(|| {
assert_counter!($($arg)*);
});
if result.is_err() {
println!("(info) counter not found — treating as 0 for test");
}
}};
}
#[cfg(test)]
pub(crate) fn assert_coprocessor_operations_metrics(
expected_stages: &[(PipelineStep, u64, Option<bool>)],
) {
for stage in [
PipelineStep::RouterRequest,
PipelineStep::RouterResponse,
PipelineStep::SupergraphRequest,
PipelineStep::SupergraphResponse,
PipelineStep::ExecutionRequest,
PipelineStep::ExecutionResponse,
PipelineStep::SubgraphRequest,
PipelineStep::SubgraphResponse,
PipelineStep::ConnectorRequest,
PipelineStep::ConnectorResponse,
] {
if let Some((_, expected_value, succeeded)) =
expected_stages.iter().find(|(s, _, _)| *s == stage)
{
assert_counter!(
"apollo.router.operations.coprocessor",
*expected_value,
coprocessor.stage = stage.to_string(),
coprocessor.succeeded =
succeeded.expect("succeeded must be provided for expected stages")
);
} else {
assert_counter_zero_or_absent!(
"apollo.router.operations.coprocessor",
0,
coprocessor.stage = stage.to_string()
);
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::sync::Arc;
use http::HeaderMap;
use http::HeaderValue;
use http::Method;
use http::StatusCode;
use http::header::ACCEPT;
use http::header::CONTENT_TYPE;
use mime::APPLICATION_JSON;
use mime::TEXT_HTML;
use serde_json_bytes::json;
use services::subgraph::SubgraphRequestId;
use tower::ServiceExt;
use super::super::*;
use crate::assert_response_eq_ignoring_error_id;
use crate::graphql::Response;
use crate::json_ext::Object;
use crate::json_ext::Value;
use crate::metrics::FutureMetricsExt;
use crate::plugins::coprocessor::BodyConf;
use crate::plugins::coprocessor::BodyFieldsConf;
use crate::plugins::coprocessor::RouterRequestConf;
use crate::plugins::coprocessor::RouterResponseConf;
use crate::plugins::coprocessor::SubgraphRequestConf;
use crate::plugins::coprocessor::SubgraphResponseConf;
use crate::plugins::coprocessor::handle_graphql_response;
use crate::plugins::coprocessor::is_graphql_response_minimally_valid;
use crate::plugins::coprocessor::supergraph::SupergraphResponseConf;
use crate::plugins::coprocessor::supergraph::SupergraphStage;
use crate::plugins::coprocessor::test::assert_coprocessor_operations_metrics;
use crate::plugins::coprocessor::was_incoming_payload_valid;
use crate::plugins::telemetry::config_new::conditions::SelectorOrValue;
use crate::services::PipelineStep;
use crate::services::external::EXTERNALIZABLE_VERSION;
use crate::services::external::Externalizable;
use crate::services::router;
use crate::services::subgraph;
use crate::services::supergraph;
#[tokio::test]
async fn load_plugin() {
let config = serde_json::json!({
"coprocessor": {
"url": "http://127.0.0.1:8081"
}
});
let _test_harness = crate::TestHarness::builder()
.configuration_json(config)
.unwrap()
.build_router()
.await
.unwrap();
}
#[tokio::test]
async fn fails_without_global_url() {
let config = serde_json::json!({
"coprocessor": {
"router": {
"request": {
"url": "http://127.0.0.1:8082",
"body": true
}
}
}
});
assert!(
crate::TestHarness::builder()
.configuration_json(config)
.is_err()
);
}
#[tokio::test]
async fn succeeds_with_stage_specific_url() {
let config = serde_json::json!({
"coprocessor": {
"url": "http://127.0.0.1:8081",
"router": {
"request": {
"url": "http://127.0.0.1:8082",
"body": true
}
}
}
});
let _test_harness = crate::TestHarness::builder()
.configuration_json(config)
.unwrap()
.build_router()
.await
.unwrap();
let router_stage = RouterStage {
request: RouterRequestConf {
condition: Default::default(),
headers: true,
context: ContextConf::All,
body: true,
sdl: false,
path: false,
method: false,
url: Some("http://127.0.0.1:8082".to_string()), },
response: Default::default(),
};
let (router_mock, mut router_handle) =
tower_test::mock::pair::<supergraph::Request, supergraph::Response>();
let router_driver = tokio::spawn(async move {
let (req, responder) = router_handle.next_request().await.unwrap();
responder.send_response(
supergraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.context(req.context)
.build()
.unwrap(),
);
});
let mock_router_service = router::service::from_supergraph_mock(router_mock).await;
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let req = req.http_request;
let uri = req.uri().to_string();
assert!(
uri.contains("127.0.0.1:8082"),
"Expected request to be sent to stage-specific URL port 8082, but got: {}",
uri
);
let input = json!({
"version": 1,
"stage": "RouterRequest",
"control": "continue",
"body": "{\"query\": \"{ __typename }\"}",
"context": {
"entries": {}
}
});
let response = http::Response::builder()
.body(router::body::from_bytes(
serde_json::to_string(&input).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed_clone(),
"http://127.0.0.1:8081".to_string(), Arc::new("".to_string()),
true,
);
let request = supergraph::Request::canned_builder().build().unwrap();
service.oneshot(request.try_into().unwrap()).await.unwrap();
crate::plugin::test::await_mock_driver(http_driver).await;
crate::plugin::test::await_mock_driver(router_driver).await;
}
#[tokio::test]
async fn unknown_fields_are_denied() {
let config = serde_json::json!({
"coprocessor": {
"url": "http://127.0.0.1:8081",
"thisFieldDoesntExist": true
}
});
assert!(
crate::TestHarness::builder()
.configuration_json(config)
.is_err()
);
}
#[tokio::test]
async fn coprocessor_returning_the_wrong_version_should_fail() {
let router_stage = RouterStage {
request: RouterRequestConf {
condition: Default::default(),
headers: true,
context: ContextConf::All,
body: true,
sdl: true,
path: false,
method: false,
url: None,
},
response: Default::default(),
};
let (mock_router_service, handle) =
tower_test::mock::pair::<router::Request, router::Response>();
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let input = json!(
{
"version": 2,
"stage": "RouterRequest",
"control": "continue",
"id": "1b19c05fdafc521016df33148ad63c1b",
"body": "{
\"query\": \"query Long {\n me {\n name\n}\n}\"
}",
"context": {
"entries": {}
},
"sdl": "the sdl shouldnt change"
});
let response = http::Response::builder()
.body(router::body::from_bytes(
serde_json::to_string(&input).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
true,
);
let request = supergraph::Request::canned_builder().build().unwrap();
assert_eq!(
"Coprocessor returned the wrong version: expected `1` found `2`",
service
.oneshot(request.try_into().unwrap())
.await
.unwrap_err()
.to_string()
);
crate::plugin::test::assert_no_mock_calls(handle).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn coprocessor_returning_the_wrong_stage_should_fail() {
let router_stage = RouterStage {
request: RouterRequestConf {
condition: Default::default(),
headers: true,
context: ContextConf::All,
body: true,
sdl: true,
path: false,
method: false,
url: None,
},
response: Default::default(),
};
let (mock_router_service, handle) =
tower_test::mock::pair::<router::Request, router::Response>();
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let input = json!(
{
"version": 1,
"stage": "RouterResponse",
"control": "continue",
"id": "1b19c05fdafc521016df33148ad63c1b",
"body": "{
\"query\": \"query Long {\n me {\n name\n}\n}\"
}",
"context": {
"entries": {}
},
"sdl": "the sdl shouldnt change"
});
let response = http::Response::builder()
.body(router::body::from_bytes(
serde_json::to_string(&input).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
true,
);
let request = supergraph::Request::canned_builder().build().unwrap();
assert_eq!(
"Coprocessor returned the wrong stage: expected `RouterRequest` found `RouterResponse`",
service
.oneshot(request.try_into().unwrap())
.await
.unwrap_err()
.to_string()
);
crate::plugin::test::assert_no_mock_calls(handle).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn coprocessor_missing_request_control_should_fail() {
let router_stage = RouterStage {
request: RouterRequestConf {
condition: Default::default(),
headers: true,
context: ContextConf::All,
body: true,
sdl: true,
path: false,
method: false,
url: None,
},
response: Default::default(),
};
let (mock_router_service, handle) =
tower_test::mock::pair::<router::Request, router::Response>();
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let input = json!(
{
"version": 1,
"stage": "RouterRequest",
"id": "1b19c05fdafc521016df33148ad63c1b",
"body": "{
\"query\": \"query Long {\n me {\n name\n}\n}\"
}",
"context": {
"entries": {}
},
"sdl": "the sdl shouldnt change"
});
let response = http::Response::builder()
.body(router::body::from_bytes(
serde_json::to_string(&input).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
true,
);
let request = supergraph::Request::canned_builder().build().unwrap();
assert_eq!(
"Coprocessor response is missing the `control` parameter in the `RouterRequest` stage. You must specify \"control\": \"Continue\" or \"control\": \"Break\"",
service
.oneshot(request.try_into().unwrap())
.await
.unwrap_err()
.to_string()
);
crate::plugin::test::assert_no_mock_calls(handle).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn coprocessor_subgraph_with_invalid_response_body_should_fail() {
let subgraph_stage = SubgraphStage {
request: SubgraphRequestConf {
condition: Default::default(),
body: true,
..Default::default()
},
response: Default::default(),
};
let (mock_subgraph_service, handle) =
tower_test::mock::pair::<subgraph::Request, subgraph::Response>();
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "SubgraphRequest",
"control": {
"break": 200
},
"id": "3a67e2dd75e8777804e4a8f42b971df7",
"body": {
"errors": [{
"body": "Errors need a message, this will fail to deserialize"
}]
}
}"#,
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
true,
);
let request = subgraph::Request::fake_builder().build();
assert_eq!(
"couldn't deserialize coprocessor output body: GraphQL response was malformed: missing required `message` property within error",
service
.oneshot(request)
.await
.unwrap()
.response
.into_body()
.errors[0]
.message
.to_string()
);
crate::plugin::test::assert_no_mock_calls(handle).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_request() {
let subgraph_stage = SubgraphStage {
request: SubgraphRequestConf {
condition: Default::default(),
body: true,
subgraph_request_id: true,
..Default::default()
},
response: Default::default(),
};
let (mock_subgraph_service, mut handle_subgraph) =
tower_test::mock::pair::<subgraph::Request, subgraph::Response>();
let subgraph_driver = tokio::spawn(async move {
let (req, responder) = handle_subgraph.next_request().await.unwrap();
assert_eq!(
req.subgraph_request.headers().get("cookie").unwrap(),
"tasty_cookie=strawberry"
);
assert_eq!(
req.context
.get::<&str, u8>("this-is-a-test-context")
.unwrap()
.unwrap(),
42
);
assert_eq!(
"http://thisurihaschanged/",
req.subgraph_request.uri().to_string()
);
assert_eq!(
"query Long {\n me {\n name\n}\n}",
req.subgraph_request.into_body().query.unwrap()
);
assert_eq!(&*req.id, "5678");
responder.send_response(
subgraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.errors(Vec::new())
.extensions(Object::new())
.context(req.context)
.id(req.id)
.subgraph_name(String::default())
.build(),
);
});
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let req = req.http_request;
let deserialized_request: Externalizable<Value> =
serde_json::from_slice(&router::body::into_bytes(req.into_body()).await.unwrap())
.unwrap();
assert_eq!(
deserialized_request.subgraph_request_id.as_deref(),
Some("5678")
);
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "SubgraphRequest",
"control": "continue",
"headers": {
"cookie": [
"tasty_cookie=strawberry"
],
"content-type": [
"application/json"
],
"host": [
"127.0.0.1:4000"
],
"apollo-federation-include-trace": [
"ftv1"
],
"apollographql-client-name": [
"manual"
],
"accept": [
"*/*"
],
"user-agent": [
"curl/7.79.1"
],
"content-length": [
"46"
]
},
"body": {
"query": "query Long {\n me {\n name\n}\n}"
},
"context": {
"entries": {
"accepts-json": false,
"accepts-wildcard": true,
"accepts-multipart": false,
"this-is-a-test-context": 42
}
},
"serviceName": "service name shouldn't change",
"uri": "http://thisurihaschanged",
"subgraphRequestId": "9abc"
}"#,
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
true,
);
let mut request = subgraph::Request::fake_builder().build();
request.id = SubgraphRequestId("5678".to_string());
let response = service.oneshot(request).await.unwrap();
assert_eq!("5678", &*response.id);
assert_eq!(
json!({ "test": 1234_u32 }),
response.response.into_body().data.unwrap()
);
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_request_with_selective_context() {
let subgraph_stage = SubgraphStage {
request: SubgraphRequestConf {
condition: Default::default(),
body: true,
subgraph_request_id: true,
context: ContextConf::Selective(Arc::new(
["this-is-a-test-context".to_string()].into(),
)),
..Default::default()
},
response: Default::default(),
};
let (mock_subgraph_service, mut handle_subgraph) =
tower_test::mock::pair::<subgraph::Request, subgraph::Response>();
let subgraph_driver = tokio::spawn(async move {
let (req, responder) = handle_subgraph.next_request().await.unwrap();
assert_eq!(
req.subgraph_request.headers().get("cookie").unwrap(),
"tasty_cookie=strawberry"
);
assert_eq!(
req.context
.get::<&str, u8>("this-is-a-test-context")
.unwrap()
.unwrap(),
42
);
assert_eq!(
"http://thisurihaschanged/",
req.subgraph_request.uri().to_string()
);
assert_eq!(
"query Long {\n me {\n name\n}\n}",
req.subgraph_request.into_body().query.unwrap()
);
assert_eq!(&*req.id, "5678");
responder.send_response(
subgraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.errors(Vec::new())
.extensions(Object::new())
.context(req.context)
.id(req.id)
.subgraph_name(String::default())
.build(),
);
});
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let req = req.http_request;
let deserialized_request: Externalizable<Value> =
serde_json::from_slice(&router::body::into_bytes(req.into_body()).await.unwrap())
.unwrap();
assert_eq!(
deserialized_request.subgraph_request_id.as_deref(),
Some("5678")
);
let req_context = deserialized_request.context.unwrap_or_default();
assert_eq!(
req_context
.get::<&str, u8>("this-is-a-test-context")
.expect("context key should be there")
.expect("context key should have the right format"),
42
);
assert!(
req_context
.get::<&str, String>("not_passed")
.ok()
.flatten()
.is_none()
);
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "SubgraphRequest",
"control": "continue",
"headers": {
"cookie": [
"tasty_cookie=strawberry"
],
"content-type": [
"application/json"
],
"host": [
"127.0.0.1:4000"
],
"apollo-federation-include-trace": [
"ftv1"
],
"apollographql-client-name": [
"manual"
],
"accept": [
"*/*"
],
"user-agent": [
"curl/7.79.1"
],
"content-length": [
"46"
]
},
"body": {
"query": "query Long {\n me {\n name\n}\n}"
},
"context": {
"entries": {
"this-is-a-test-context": 42
}
},
"serviceName": "service name shouldn't change",
"uri": "http://thisurihaschanged",
"subgraphRequestId": "9abc"
}"#,
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
true,
);
let mut request = subgraph::Request::fake_builder().build();
request.id = SubgraphRequestId("5678".to_string());
request
.context
.insert("not_passed", "OK".to_string())
.unwrap();
request
.context
.insert("this-is-a-test-context", 42)
.unwrap();
let response = service.oneshot(request).await.unwrap();
assert_eq!("5678", &*response.id);
assert_eq!(
json!({ "test": 1234_u32 }),
response.response.into_body().data.unwrap()
);
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_request_with_condition() {
let subgraph_stage = SubgraphStage {
request: SubgraphRequestConf {
condition: Condition::Eq([
SelectorOrValue::Selector(SubgraphSelector::SubgraphRequestHeader {
subgraph_request_header: String::from("another_header"),
redact: None,
default: None,
}),
SelectorOrValue::Value("value".to_string().into()),
]),
body: true,
..Default::default()
},
response: Default::default(),
};
let (mock_subgraph_service, mut handle_subgraph) =
tower_test::mock::pair::<subgraph::Request, subgraph::Response>();
let subgraph_driver = tokio::spawn(async move {
let (req, responder) = handle_subgraph.next_request().await.unwrap();
assert_eq!("/", req.subgraph_request.uri().to_string());
responder.send_response(
subgraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.errors(Vec::new())
.extensions(Object::new())
.context(req.context)
.subgraph_name(String::default())
.build(),
);
});
let (mock_http_client, http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
true,
);
let request = subgraph::Request::fake_builder().build();
assert_eq!(
json!({ "test": 1234_u32 }),
service
.oneshot(request)
.await
.unwrap()
.response
.into_body()
.data
.unwrap()
);
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::assert_no_mock_calls(http_handle).await;
}
#[tokio::test]
async fn external_plugin_subgraph_request_controlflow_break() {
let subgraph_stage = SubgraphStage {
request: SubgraphRequestConf {
condition: Default::default(),
body: true,
..Default::default()
},
response: Default::default(),
};
let (mock_subgraph_service, handle) =
tower_test::mock::pair::<subgraph::Request, subgraph::Response>();
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "SubgraphRequest",
"control": {
"break": 200
},
"body": {
"errors": [{ "message": "my error message" }]
},
"context": {
"entries": {
"testKey": true
}
},
"headers": {
"aheader": ["a value"]
}
}"#,
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
true,
);
let request = subgraph::Request::fake_builder().build();
let crate::services::subgraph::Response {
response, context, ..
} = service.oneshot(request).await.unwrap();
assert!(context.get::<_, bool>("testKey").unwrap().unwrap());
let value = response.headers().get("aheader").unwrap();
assert_eq!("a value", value);
assert_eq!(
"my error message",
response.into_body().errors[0].message.as_str()
);
crate::plugin::test::assert_no_mock_calls(handle).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_request_controlflow_break_with_message_string() {
let subgraph_stage = SubgraphStage {
request: SubgraphRequestConf {
condition: Default::default(),
body: true,
..Default::default()
},
response: Default::default(),
};
let (mock_subgraph_service, handle) =
tower_test::mock::pair::<subgraph::Request, subgraph::Response>();
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "SubgraphRequest",
"control": {
"break": 200
},
"body": "my error message"
}"#,
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
true,
);
let request = subgraph::Request::fake_builder().build();
let response = service.oneshot(request).await.unwrap().response;
assert_eq!(response.status(), http::StatusCode::OK);
let actual_response = response.into_body();
assert_response_eq_ignoring_error_id!(
actual_response,
serde_json_bytes::from_value::<Response>(json!({
"errors": [{
"message": "my error message",
"extensions": {
"code": "ERROR"
}
}]
}))
.unwrap()
);
crate::plugin::test::assert_no_mock_calls(handle).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_response() {
let subgraph_stage = SubgraphStage {
request: Default::default(),
response: SubgraphResponseConf {
condition: Default::default(),
body: BodyConf::All(true),
subgraph_request_id: true,
..Default::default()
},
};
let (mock_subgraph_service, mut handle_subgraph) =
tower_test::mock::pair::<subgraph::Request, subgraph::Response>();
let subgraph_driver = tokio::spawn(async move {
let (req, responder) = handle_subgraph.next_request().await.unwrap();
assert_eq!(&*req.id, "5678");
responder.send_response(
subgraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.errors(Vec::new())
.extensions(Object::new())
.context(req.context)
.id(req.id)
.subgraph_name(String::default())
.build(),
);
});
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let (_, body) = req.http_request.into_parts();
let body: Value =
serde_json::from_slice(&router::body::into_bytes(body).await.unwrap()).unwrap();
let subgraph_id = body.get("subgraphRequestId").unwrap();
assert_eq!(subgraph_id.as_str(), Some("5678"));
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "SubgraphResponse",
"headers": {
"cookie": [
"tasty_cookie=strawberry"
],
"content-type": [
"application/json"
],
"host": [
"127.0.0.1:4000"
],
"apollo-federation-include-trace": [
"ftv1"
],
"apollographql-client-name": [
"manual"
],
"accept": [
"*/*"
],
"user-agent": [
"curl/7.79.1"
],
"content-length": [
"46"
]
},
"body": {
"data": {
"test": 5678
}
},
"context": {
"entries": {
"accepts-json": false,
"accepts-wildcard": true,
"accepts-multipart": false,
"this-is-a-test-context": 42
}
},
"subgraphRequestId": "9abc"
}"#,
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
true,
);
let mut request = subgraph::Request::fake_builder().build();
request.id = SubgraphRequestId("5678".to_string());
let response = service.oneshot(request).await.unwrap();
assert_eq!(
response.response.headers().get("cookie").unwrap(),
"tasty_cookie=strawberry"
);
assert_eq!(&*response.id, "5678");
assert_eq!(
response
.context
.get::<&str, u8>("this-is-a-test-context")
.unwrap()
.unwrap(),
42
);
assert_eq!(
json!({ "test": 5678_u32 }),
response.response.into_body().data.unwrap()
);
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_response_with_null_data() {
let subgraph_stage = SubgraphStage {
request: Default::default(),
response: SubgraphResponseConf {
condition: Default::default(),
body: BodyConf::All(true),
subgraph_request_id: true,
..Default::default()
},
};
let (mock_subgraph_service, mut handle_subgraph) =
tower_test::mock::pair::<subgraph::Request, subgraph::Response>();
let subgraph_driver = tokio::spawn(async move {
let (req, responder) = handle_subgraph.next_request().await.unwrap();
assert_eq!(&*req.id, "5678");
responder.send_response(
subgraph::Response::builder()
.data(serde_json_bytes::Value::Null)
.extensions(Object::new())
.context(req.context)
.id(req.id)
.subgraph_name(String::default())
.build(),
);
});
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let (_, body) = req.http_request.into_parts();
let body: Value =
serde_json::from_slice(&router::body::into_bytes(body).await.unwrap()).unwrap();
let subgraph_id = body.get("subgraphRequestId").unwrap();
assert_eq!(subgraph_id.as_str(), Some("5678"));
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "SubgraphResponse",
"headers": {
"content-type": [
"application/json"
],
"host": [
"127.0.0.1:4000"
],
"apollo-federation-include-trace": [
"ftv1"
],
"apollographql-client-name": [
"manual"
],
"accept": [
"*/*"
],
"user-agent": [
"curl/7.79.1"
],
"content-length": [
"46"
]
},
"body": {
"data": null
},
"context": {
"entries": {
"accepts-json": false,
"accepts-wildcard": true,
"accepts-multipart": false
}
},
"subgraphRequestId": "9abc"
}"#,
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
true,
);
let mut request = subgraph::Request::fake_builder().build();
request.id = SubgraphRequestId("5678".to_string());
let response = service.oneshot(request).await.unwrap();
assert_eq!(&*response.id, "5678");
assert_eq!(
serde_json_bytes::Value::Null,
response.response.into_body().data.unwrap()
);
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_response_with_selective_context() {
let subgraph_stage = SubgraphStage {
request: Default::default(),
response: SubgraphResponseConf {
condition: Default::default(),
body: BodyConf::All(true),
subgraph_request_id: true,
context: ContextConf::Selective(Arc::new(
["this-is-a-test-context".to_string()].into(),
)),
..Default::default()
},
};
let (mock_subgraph_service, mut handle_subgraph) =
tower_test::mock::pair::<subgraph::Request, subgraph::Response>();
let subgraph_driver = tokio::spawn(async move {
let (req, responder) = handle_subgraph.next_request().await.unwrap();
assert_eq!(&*req.id, "5678");
responder.send_response(
subgraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.errors(Vec::new())
.extensions(Object::new())
.context(req.context)
.id(req.id)
.subgraph_name(String::default())
.build(),
);
});
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let (_, body) = req.http_request.into_parts();
let deserialized_response: Externalizable<Value> =
serde_json::from_slice(&router::body::into_bytes(body).await.unwrap()).unwrap();
assert_eq!(
deserialized_response.subgraph_request_id,
Some(SubgraphRequestId("5678".to_string()))
);
let req_context = deserialized_response.context.unwrap_or_default();
assert_eq!(
req_context
.get::<&str, u8>("this-is-a-test-context")
.expect("context key should be there")
.expect("context key should have the right format"),
55
);
assert!(
req_context
.get::<&str, String>("not_passed")
.ok()
.flatten()
.is_none()
);
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "SubgraphResponse",
"headers": {
"cookie": [
"tasty_cookie=strawberry"
],
"content-type": [
"application/json"
],
"host": [
"127.0.0.1:4000"
],
"apollo-federation-include-trace": [
"ftv1"
],
"apollographql-client-name": [
"manual"
],
"accept": [
"*/*"
],
"user-agent": [
"curl/7.79.1"
],
"content-length": [
"46"
]
},
"body": {
"data": {
"test": 5678
}
},
"context": {
"entries": {
"this-is-a-test-context": 42
}
},
"subgraphRequestId": "9abc"
}"#,
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
true,
);
let mut request = subgraph::Request::fake_builder().build();
request.id = SubgraphRequestId("5678".to_string());
request
.context
.insert("not_passed", "OK".to_string())
.unwrap();
request
.context
.insert("this-is-a-test-context", 55)
.unwrap();
let response = service.oneshot(request).await.unwrap();
assert_eq!(
response.response.headers().get("cookie").unwrap(),
"tasty_cookie=strawberry"
);
assert_eq!(&*response.id, "5678");
assert_eq!(
response
.context
.get::<&str, u8>("this-is-a-test-context")
.unwrap()
.unwrap(),
42
);
assert_eq!(
json!({ "test": 5678_u32 }),
response.response.into_body().data.unwrap()
);
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_response_with_condition() {
let subgraph_stage = SubgraphStage {
request: Default::default(),
response: SubgraphResponseConf {
condition: Condition::Exists(SubgraphSelector::ResponseContext {
response_context: String::from("context_value"),
default: None,
}),
body: BodyConf::All(true),
..Default::default()
},
};
let (mock_subgraph_service, mut handle_subgraph) =
tower_test::mock::pair::<subgraph::Request, subgraph::Response>();
let subgraph_driver = tokio::spawn(async move {
let (req, responder) = handle_subgraph.next_request().await.unwrap();
req.context
.insert("context_value", "content".to_string())
.unwrap();
responder.send_response(
subgraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.errors(Vec::new())
.extensions(Object::new())
.context(req.context)
.subgraph_name(String::default())
.build(),
);
});
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "SubgraphResponse",
"headers": {
"cookie": [
"tasty_cookie=strawberry"
],
"content-type": [
"application/json"
],
"host": [
"127.0.0.1:4000"
],
"apollo-federation-include-trace": [
"ftv1"
],
"apollographql-client-name": [
"manual"
],
"accept": [
"*/*"
],
"user-agent": [
"curl/7.79.1"
],
"content-length": [
"46"
]
},
"body": {
"data": {
"test": 5678
}
},
"context": {
"entries": {
"accepts-json": false,
"accepts-wildcard": true,
"accepts-multipart": false,
"this-is-a-test-context": 42
}
}
}"#,
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
true,
);
let request = subgraph::Request::fake_builder().build();
let response = service.oneshot(request).await.unwrap();
assert_eq!(
response.response.headers().get("cookie").unwrap(),
"tasty_cookie=strawberry"
);
assert_eq!(
response
.context
.get::<&str, u8>("this-is-a-test-context")
.unwrap()
.unwrap(),
42
);
assert_eq!(
json!({ "test": 5678_u32 }),
response.response.into_body().data.unwrap()
);
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_supergraph_response() {
let supergraph_stage = SupergraphStage {
request: Default::default(),
response: SupergraphResponseConf {
condition: Default::default(),
headers: false,
context: ContextConf::None,
body: BodyConf::All(true),
status_code: false,
sdl: false,
url: None,
},
};
let (mock_supergraph_service, mut handle_supergraph) =
tower_test::mock::pair::<supergraph::Request, supergraph::Response>();
let supergraph_driver = tokio::spawn(async move {
let (req, responder) = handle_supergraph.next_request().await.unwrap();
responder.send_response(supergraph::Response::new_from_graphql_response(
graphql::Response::builder()
.data(Value::Null)
.subscribed(true)
.build(),
req.context,
));
});
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "SupergraphResponse",
"body": {
"data": null
}
}"#,
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = supergraph_stage.as_service(
mock_http_client,
mock_supergraph_service.boxed_clone(),
"http://test".to_string(),
Arc::default(),
true,
);
let request = supergraph::Request::fake_builder().build().unwrap();
let mut response = service.oneshot(request).await.unwrap();
let gql_response = response.response.body_mut().next().await.unwrap();
assert_eq!(gql_response.subscribed, Some(true));
assert_eq!(gql_response.data, Some(Value::Null));
crate::plugin::test::await_mock_driver(supergraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_supergraph_response_with_selective_context() {
let supergraph_stage = SupergraphStage {
request: Default::default(),
response: SupergraphResponseConf {
condition: Default::default(),
headers: false,
context: ContextConf::Selective(Arc::new(
["this-is-a-test-context".to_string()].into(),
)),
body: BodyConf::All(true),
status_code: false,
sdl: false,
url: None,
},
};
let (mock_supergraph_service, mut handle_supergraph) =
tower_test::mock::pair::<supergraph::Request, supergraph::Response>();
let supergraph_driver = tokio::spawn(async move {
let (req, responder) = handle_supergraph.next_request().await.unwrap();
responder.send_response(supergraph::Response::new_from_graphql_response(
graphql::Response::builder()
.data(Value::Null)
.subscribed(true)
.build(),
req.context,
));
});
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let (_, body) = req.http_request.into_parts();
let deserialized_response: Externalizable<Value> =
serde_json::from_slice(&router::body::into_bytes(body).await.unwrap()).unwrap();
let req_context = deserialized_response.context.unwrap_or_default();
assert_eq!(
req_context
.get::<&str, u8>("this-is-a-test-context")
.expect("context key should be there")
.expect("context key should have the right format"),
42
);
assert!(
req_context
.get::<&str, String>("not_passed")
.ok()
.flatten()
.is_none()
);
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "SupergraphResponse",
"context": {
"entries": {
"this-is-a-test-context": 25
}
},
"body": {
"data": null
}
}"#,
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = supergraph_stage.as_service(
mock_http_client,
mock_supergraph_service.boxed_clone(),
"http://test".to_string(),
Arc::default(),
true,
);
let request = supergraph::Request::fake_builder().build().unwrap();
request
.context
.insert("not_passed", "OK".to_string())
.unwrap();
request
.context
.insert("this-is-a-test-context", 42)
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
assert_eq!(
response
.context
.get::<&str, u8>("this-is-a-test-context")
.unwrap()
.unwrap(),
25
);
let gql_response = response.response.body_mut().next().await.unwrap();
assert_eq!(gql_response.subscribed, Some(true));
assert_eq!(gql_response.data, Some(Value::Null));
crate::plugin::test::await_mock_driver(supergraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_router_request() {
let router_stage = RouterStage {
request: RouterRequestConf {
condition: Default::default(),
headers: true,
context: ContextConf::All,
body: true,
sdl: true,
path: true,
method: true,
url: None,
},
response: Default::default(),
};
let (router_mock, mut router_handle) =
tower_test::mock::pair::<router::Request, router::Response>();
let router_driver = tokio::spawn(async move {
let (req, responder) = router_handle.next_request().await.unwrap();
assert_eq!(
req.router_request.headers().get("cookie").unwrap(),
"tasty_cookie=strawberry"
);
assert_eq!(
req.context
.get::<&str, u8>("this-is-a-test-context")
.unwrap()
.unwrap(),
42
);
let context = req.context.clone();
let body_bytes = router::body::into_bytes(req.router_request.into_body())
.await
.unwrap();
let body: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
assert_eq!(
body["query"].as_str().unwrap(),
"query Long {\n me {\n name\n}\n}"
);
responder.send_response(
router::Response::builder()
.data(json!({ "test": 1234_u32 }))
.context(context)
.build()
.unwrap(),
);
});
let mock_router_service = router_mock;
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let req = req.http_request;
let deserialized_request: Externalizable<Value> =
serde_json::from_slice(&router::body::into_bytes(req.into_body()).await.unwrap())
.unwrap();
assert_eq!(EXTERNALIZABLE_VERSION, deserialized_request.version);
assert_eq!(
PipelineStep::RouterRequest.to_string(),
deserialized_request.stage
);
let input = json!(
{
"version": 1,
"stage": "RouterRequest",
"control": "continue",
"id": "1b19c05fdafc521016df33148ad63c1b",
"headers": {
"cookie": [
"tasty_cookie=strawberry"
],
"content-type": [
"application/json"
],
"host": [
"127.0.0.1:4000"
],
"apollo-federation-include-trace": [
"ftv1"
],
"apollographql-client-name": [
"manual"
],
"accept": [
"*/*"
],
"user-agent": [
"curl/7.79.1"
],
"content-length": [
"46"
]
},
"body": "{\"query\":\"query Long {\\n me {\\n name\\n}\\n}\"}",
"context": {
"entries": {
"accepts-json": false,
"accepts-wildcard": true,
"accepts-multipart": false,
"this-is-a-test-context": 42
}
},
"sdl": "the sdl shouldnt change"
});
let response = http::Response::builder()
.body(router::body::from_bytes(
serde_json::to_string(&input).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
true,
);
let request = supergraph::Request::canned_builder().build().unwrap();
service.oneshot(request.try_into().unwrap()).await.unwrap();
crate::plugin::test::await_mock_driver(http_driver).await;
crate::plugin::test::await_mock_driver(router_driver).await;
}
#[tokio::test]
async fn external_plugin_router_request_with_selective_context() {
let router_stage = RouterStage {
request: RouterRequestConf {
condition: Default::default(),
headers: true,
context: ContextConf::Selective(Arc::new(
["this-is-a-test-context".to_string()].into(),
)),
body: true,
sdl: true,
path: true,
method: true,
url: None,
},
response: Default::default(),
};
let (router_mock, mut router_handle) =
tower_test::mock::pair::<router::Request, router::Response>();
let router_driver = tokio::spawn(async move {
let (req, responder) = router_handle.next_request().await.unwrap();
assert_eq!(
req.router_request.headers().get("cookie").unwrap(),
"tasty_cookie=strawberry"
);
assert_eq!(
req.context
.get::<&str, u8>("this-is-a-test-context")
.unwrap()
.unwrap(),
42
);
let context = req.context.clone();
let body_bytes = router::body::into_bytes(req.router_request.into_body())
.await
.unwrap();
let body: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
assert_eq!(
body["query"].as_str().unwrap(),
"query Long {\n me {\n name\n}\n}"
);
responder.send_response(
router::Response::builder()
.data(json!({ "test": 1234_u32 }))
.context(context)
.build()
.unwrap(),
);
});
let mock_router_service = router_mock;
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let req = req.http_request;
let deserialized_request: Externalizable<Value> =
serde_json::from_slice(&router::body::into_bytes(req.into_body()).await.unwrap())
.unwrap();
assert_eq!(
deserialized_request
.context
.as_ref()
.unwrap()
.get::<&str, u8>("this-is-a-test-context")
.unwrap()
.unwrap(),
42
);
assert!(
deserialized_request
.context
.as_ref()
.unwrap()
.get::<&str, String>("not_passed")
.ok()
.flatten()
.is_none()
);
assert_eq!(EXTERNALIZABLE_VERSION, deserialized_request.version);
assert_eq!(
PipelineStep::RouterRequest.to_string(),
deserialized_request.stage
);
let input = json!(
{
"version": 1,
"stage": "RouterRequest",
"control": "continue",
"id": "1b19c05fdafc521016df33148ad63c1b",
"headers": {
"cookie": [
"tasty_cookie=strawberry"
],
"content-type": [
"application/json"
],
"host": [
"127.0.0.1:4000"
],
"apollo-federation-include-trace": [
"ftv1"
],
"apollographql-client-name": [
"manual"
],
"accept": [
"*/*"
],
"user-agent": [
"curl/7.79.1"
],
"content-length": [
"46"
]
},
"body": "{\"query\":\"query Long {\\n me {\\n name\\n}\\n}\"}",
"context": {
"entries": {
"accepts-json": false,
"accepts-wildcard": true,
"accepts-multipart": false,
"this-is-a-test-context": 42
}
},
"sdl": "the sdl shouldnt change"
});
let response = http::Response::builder()
.body(router::body::from_bytes(
serde_json::to_string(&input).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
true,
);
let request = supergraph::Request::canned_builder().build().unwrap();
request
.context
.insert("not_passed", "OK".to_string())
.unwrap();
request
.context
.insert("this-is-a-test-context", 42)
.unwrap();
let res = service.oneshot(request.try_into().unwrap()).await.unwrap();
assert!(
res.context
.get::<&str, String>("not_passed")
.ok()
.flatten()
.is_some()
);
crate::plugin::test::await_mock_driver(http_driver).await;
crate::plugin::test::await_mock_driver(router_driver).await;
}
#[tokio::test]
async fn external_plugin_router_request_with_condition() {
let router_stage = RouterStage {
request: RouterRequestConf {
condition: Condition::Eq([
SelectorOrValue::Selector(RouterSelector::RequestMethod {
request_method: true,
}),
SelectorOrValue::Value("GET".to_string().into()),
])
.into(),
headers: true,
context: ContextConf::All,
body: true,
sdl: true,
path: true,
method: true,
url: None,
},
response: Default::default(),
};
let (router_mock, mut router_handle) =
tower_test::mock::pair::<supergraph::Request, supergraph::Response>();
let router_driver = tokio::spawn(async move {
let (req, responder) = router_handle.next_request().await.unwrap();
assert!(
req.context
.get::<&str, u8>("this-is-a-test-context")
.ok()
.flatten()
.is_none()
);
responder.send_response(
supergraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.context(req.context)
.build()
.unwrap(),
);
});
let mock_router_service = router::service::from_supergraph_mock(router_mock).await;
let (mock_http_client, http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
true,
);
let request = supergraph::Request::canned_builder().build().unwrap();
service.oneshot(request.try_into().unwrap()).await.unwrap();
crate::plugin::test::assert_no_mock_calls(http_handle).await;
crate::plugin::test::await_mock_driver(router_driver).await;
}
#[tokio::test]
async fn external_plugin_router_request_http_get() {
let router_stage = RouterStage {
request: RouterRequestConf {
condition: Default::default(),
headers: true,
context: ContextConf::All,
body: true,
sdl: true,
path: true,
method: true,
url: None,
},
response: Default::default(),
};
let (router_mock, mut router_handle) =
tower_test::mock::pair::<router::Request, router::Response>();
let router_driver = tokio::spawn(async move {
let (req, responder) = router_handle.next_request().await.unwrap();
assert_eq!(
req.router_request.headers().get("cookie").unwrap(),
"tasty_cookie=strawberry"
);
assert_eq!(req.router_request.method(), Method::GET);
assert_eq!(
req.context
.get::<&str, u8>("this-is-a-test-context")
.unwrap()
.unwrap(),
42
);
let context = req.context.clone();
let body_bytes = router::body::into_bytes(req.router_request.into_body())
.await
.unwrap();
let body: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
assert_eq!(
body["query"].as_str().unwrap(),
"query Long {\n me {\n name\n}\n}"
);
responder.send_response(
router::Response::builder()
.data(json!({ "test": 1234_u32 }))
.context(context)
.build()
.unwrap(),
);
});
let mock_router_service = router_mock;
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let req = req.http_request;
let deserialized_request: Externalizable<Value> =
serde_json::from_slice(&router::body::into_bytes(req.into_body()).await.unwrap())
.unwrap();
assert_eq!(EXTERNALIZABLE_VERSION, deserialized_request.version);
assert_eq!(
PipelineStep::RouterRequest.to_string(),
deserialized_request.stage
);
let input = json!(
{
"version": 1,
"stage": "RouterRequest",
"control": "continue",
"id": "1b19c05fdafc521016df33148ad63c1b",
"uri": "/this/is/a/new/uri",
"method": "POST",
"headers": {
"cookie": [
"tasty_cookie=strawberry"
],
"content-type": [
"application/json"
],
"host": [
"127.0.0.1:4000"
],
"apollo-federation-include-trace": [
"ftv1"
],
"apollographql-client-name": [
"manual"
],
"accept": [
"*/*"
],
"user-agent": [
"curl/7.79.1"
],
"content-length": [
"46"
]
},
"body": "{\"query\":\"query Long {\\n me {\\n name\\n}\\n}\"}",
"context": {
"entries": {
"accepts-json": false,
"accepts-wildcard": true,
"accepts-multipart": false,
"this-is-a-test-context": 42
}
},
"sdl": "the sdl shouldnt change"
});
let response = http::Response::builder()
.body(router::body::from_bytes(
serde_json::to_string(&input).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
true,
);
let request = supergraph::Request::fake_builder()
.method(Method::GET)
.build()
.unwrap();
service.oneshot(request.try_into().unwrap()).await.unwrap();
crate::plugin::test::await_mock_driver(http_driver).await;
crate::plugin::test::await_mock_driver(router_driver).await;
}
#[tokio::test]
async fn external_plugin_router_request_controlflow_break() {
let router_stage = RouterStage {
request: RouterRequestConf {
condition: Default::default(),
headers: true,
context: ContextConf::All,
body: true,
sdl: true,
path: true,
method: true,
url: None,
},
response: Default::default(),
};
let (mock_router_service, handle) =
tower_test::mock::pair::<router::Request, router::Response>();
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let req = req.http_request;
let deserialized_request: Externalizable<Value> =
serde_json::from_slice(&router::body::into_bytes(req.into_body()).await.unwrap())
.unwrap();
assert_eq!(EXTERNALIZABLE_VERSION, deserialized_request.version);
assert_eq!(
PipelineStep::RouterRequest.to_string(),
deserialized_request.stage
);
let input = json!(
{
"version": 1,
"stage": "RouterRequest",
"control": {
"break": 200
},
"id": "1b19c05fdafc521016df33148ad63c1b",
"body": "{
\"errors\": [{ \"message\": \"my error message\" }]
}",
"context": {
"entries": {
"testKey": true
}
},
"headers": {
"aheader": ["a value"]
}
}
);
let response = http::Response::builder()
.body(router::body::from_bytes(
serde_json::to_string(&input).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
true,
);
let request = supergraph::Request::canned_builder().build().unwrap();
let crate::services::router::Response { response, context } =
service.oneshot(request.try_into().unwrap()).await.unwrap();
assert!(context.get::<_, bool>("testKey").unwrap().unwrap());
let value = response.headers().get("aheader").unwrap();
assert_eq!("a value", value);
let actual_response = serde_json::from_slice::<Value>(
&router::body::into_bytes(response.into_body())
.await
.unwrap(),
)
.unwrap();
assert_eq!(
json!({
"errors": [{
"message": "my error message"
}]
}),
actual_response
);
crate::plugin::test::assert_no_mock_calls(handle).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_router_request_controlflow_break_with_message_string() {
let router_stage = RouterStage {
request: RouterRequestConf {
condition: Default::default(),
headers: true,
context: ContextConf::All,
body: true,
sdl: true,
path: true,
method: true,
url: None,
},
response: Default::default(),
};
let (mock_router_service, handle) =
tower_test::mock::pair::<router::Request, router::Response>();
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let req = req.http_request;
let deserialized_request: Externalizable<Value> =
serde_json::from_slice(&router::body::into_bytes(req.into_body()).await.unwrap())
.unwrap();
assert_eq!(EXTERNALIZABLE_VERSION, deserialized_request.version);
assert_eq!(
PipelineStep::RouterRequest.to_string(),
deserialized_request.stage
);
let input = json!(
{
"version": 1,
"stage": "RouterRequest",
"control": {
"break": 401
},
"id": "1b19c05fdafc521016df33148ad63c1b",
"body": "this is a test error",
}
);
let response = http::Response::builder()
.body(router::body::from_bytes(
serde_json::to_string(&input).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
true,
);
let request = supergraph::Request::canned_builder().build().unwrap();
let response = service
.oneshot(request.try_into().unwrap())
.await
.unwrap()
.response;
assert_eq!(response.status(), http::StatusCode::UNAUTHORIZED);
let actual_response = serde_json::from_slice::<Value>(
&router::body::into_bytes(response.into_body())
.await
.unwrap(),
)
.unwrap();
assert_eq!(
json!({
"errors": [{
"message": "this is a test error",
"extensions": {
"code": "ERROR"
}
}]
}),
actual_response
);
crate::plugin::test::assert_no_mock_calls(handle).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_router_response() {
let router_stage = RouterStage {
response: RouterResponseConf {
condition: Default::default(),
headers: true,
context: ContextConf::All,
body: true,
sdl: true,
status_code: false,
url: None,
},
request: Default::default(),
};
let (router_mock, mut router_handle) =
tower_test::mock::pair::<supergraph::Request, supergraph::Response>();
let router_driver = tokio::spawn(async move {
let (req, responder) = router_handle.next_request().await.unwrap();
responder.send_response(
supergraph::Response::builder()
.data(json!("{ \"test\": 1234_u32 }"))
.context(req.context)
.build()
.unwrap(),
);
});
let mock_router_service = router::service::from_supergraph_mock(router_mock).await;
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let res = req.http_request;
let deserialized_response: Externalizable<Value> =
serde_json::from_slice(&router::body::into_bytes(res.into_body()).await.unwrap())
.unwrap();
assert_eq!(EXTERNALIZABLE_VERSION, deserialized_response.version);
assert_eq!(
PipelineStep::RouterResponse.to_string(),
deserialized_response.stage
);
assert_eq!(
json!("{\"data\":\"{ \\\"test\\\": 1234_u32 }\"}"),
deserialized_response.body.unwrap()
);
let input = json!(
{
"version": 1,
"stage": "RouterResponse",
"control": {
"break": 400
},
"id": "1b19c05fdafc521016df33148ad63c1b",
"headers": {
"cookie": [
"tasty_cookie=strawberry"
],
"content-type": [
"application/json"
],
"host": [
"127.0.0.1:4000"
],
"apollo-federation-include-trace": [
"ftv1"
],
"apollographql-client-name": [
"manual"
],
"accept": [
"*/*"
],
"user-agent": [
"curl/7.79.1"
],
"content-length": [
"46"
]
},
"body": "{
\"data\": { \"test\": 42 }
}",
"context": {
"entries": {
"accepts-json": false,
"accepts-wildcard": true,
"accepts-multipart": false,
"this-is-a-test-context": 42
}
},
"sdl": "the sdl shouldnt change"
});
let response = http::Response::builder()
.body(router::body::from_bytes(
serde_json::to_string(&input).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
true,
);
let request = supergraph::Request::canned_builder().build().unwrap();
let res = service.oneshot(request.try_into().unwrap()).await.unwrap();
assert_eq!(res.response.status(), StatusCode::BAD_REQUEST);
assert_eq!(
res.response.headers().get("cookie").unwrap(),
"tasty_cookie=strawberry"
);
assert_eq!(
res.context
.get::<&str, u8>("this-is-a-test-context")
.unwrap()
.unwrap(),
42
);
assert_eq!(
json!({ "data": { "test": 42_u32 } }),
serde_json::from_slice::<Value>(
&router::body::into_bytes(res.response.into_body())
.await
.unwrap()
)
.unwrap()
);
crate::plugin::test::await_mock_driver(http_driver).await;
crate::plugin::test::await_mock_driver(router_driver).await;
}
#[tokio::test]
async fn external_plugin_router_response_validation_disabled_custom() {
let router_stage = RouterStage {
response: RouterResponseConf {
body: true,
..Default::default()
},
..Default::default()
};
let (router_mock, mut router_handle) =
tower_test::mock::pair::<router::Request, router::Response>();
let router_driver = tokio::spawn(async move {
let (req, responder) = router_handle.next_request().await.unwrap();
responder.send_response(
router::Response::builder()
.data(json!({"test": 42}))
.context(req.context)
.build()
.unwrap(),
);
});
let mock_router_service = router_mock;
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let response_body = json!({
"version": 1,
"stage": "RouterResponse",
"control": "continue",
"body": "{\"data\": {\"test\": \"modified_by_coprocessor\"}}"
});
let response = http::Response::builder()
.status(200)
.body(router::body::from_bytes(
serde_json::to_string(&response_body).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service_stack = router_stage
.as_service(
mock_http_client,
mock_router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
false, )
.boxed();
let request = router::Request::fake_builder().build().unwrap();
let res = service_stack.oneshot(request).await.unwrap();
assert_eq!(res.response.status(), 200);
let body_bytes = router::body::into_bytes(res.response.into_body())
.await
.unwrap();
let body: Value = serde_json::from_slice(&body_bytes).unwrap();
assert_eq!(body["data"]["test"], "modified_by_coprocessor");
crate::plugin::test::await_mock_driver(http_driver).await;
crate::plugin::test::await_mock_driver(router_driver).await;
}
#[tokio::test]
async fn external_plugin_router_response_validation_enabled_valid() {
let (http_client, http_driver) = create_mock_http_client_router_response_valid_response();
let (router_service, mut router_handle) = create_mock_router_service_for_validation_test();
let router_driver = tokio::spawn(async move {
let (req, responder) = router_handle.next_request().await.unwrap();
responder.send_response(
router::Response::builder()
.data(json!({"test": 42}))
.context(req.context)
.build()
.unwrap(),
);
});
let service_stack = create_router_stage_for_response_validation_test()
.as_service(
http_client,
router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
true, )
.boxed();
let request = router::Request::fake_builder().build().unwrap();
let res = service_stack.oneshot(request).await.unwrap();
assert_eq!(res.response.status(), 200);
let body_bytes = router::body::into_bytes(res.response.into_body())
.await
.unwrap();
let body: Value = serde_json::from_slice(&body_bytes).unwrap();
assert_eq!(body["data"]["test"], "valid_response");
crate::plugin::test::await_mock_driver(http_driver).await;
crate::plugin::test::await_mock_driver(router_driver).await;
}
#[tokio::test]
async fn external_plugin_router_response_validation_enabled_empty() {
let (http_client, http_driver) = create_mock_http_client_router_response_empty_response();
let (router_service, mut router_handle) = create_mock_router_service_for_validation_test();
let router_driver = tokio::spawn(async move {
let (req, responder) = router_handle.next_request().await.unwrap();
responder.send_response(
router::Response::builder()
.data(json!({"test": 42}))
.context(req.context)
.build()
.unwrap(),
);
});
let service_stack = create_router_stage_for_response_validation_test()
.as_service(
http_client,
router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
true, )
.boxed();
let request = router::Request::fake_builder().build().unwrap();
let res = service_stack.oneshot(request).await.unwrap();
assert_eq!(res.response.status(), 200);
let body_bytes = router::body::into_bytes(res.response.into_body())
.await
.unwrap();
let body: Value = serde_json::from_slice(&body_bytes).unwrap();
assert!(body.as_object().unwrap().is_empty());
crate::plugin::test::await_mock_driver(http_driver).await;
crate::plugin::test::await_mock_driver(router_driver).await;
}
#[tokio::test]
async fn external_plugin_router_response_validation_enabled_invalid() {
let (http_client, http_driver) = create_mock_http_client_router_response_invalid_response();
let (router_service, mut router_handle) = create_mock_router_service_for_validation_test();
let router_driver = tokio::spawn(async move {
let (req, responder) = router_handle.next_request().await.unwrap();
responder.send_response(
router::Response::builder()
.data(json!({"test": 42}))
.context(req.context)
.build()
.unwrap(),
);
});
let service_stack = create_router_stage_for_response_validation_test()
.as_service(
http_client,
router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
true, )
.boxed();
let request = router::Request::fake_builder().build().unwrap();
let res = service_stack.oneshot(request).await.unwrap();
assert_eq!(res.response.status(), 200);
let body_bytes = router::body::into_bytes(res.response.into_body())
.await
.unwrap();
let body: Value = serde_json::from_slice(&body_bytes).unwrap();
assert_eq!(body["errors"], "this should be an array not a string");
crate::plugin::test::await_mock_driver(http_driver).await;
crate::plugin::test::await_mock_driver(router_driver).await;
}
#[tokio::test]
async fn external_plugin_router_response_validation_disabled_valid() {
let (http_client, http_driver) = create_mock_http_client_router_response_valid_response();
let (router_service, mut router_handle) = create_mock_router_service_for_validation_test();
let router_driver = tokio::spawn(async move {
let (req, responder) = router_handle.next_request().await.unwrap();
responder.send_response(
router::Response::builder()
.data(json!({"test": 42}))
.context(req.context)
.build()
.unwrap(),
);
});
let service_stack = create_router_stage_for_response_validation_test()
.as_service(
http_client,
router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
false, )
.boxed();
let request = router::Request::fake_builder().build().unwrap();
let res = service_stack.oneshot(request).await.unwrap();
assert_eq!(res.response.status(), 200);
let body_bytes = router::body::into_bytes(res.response.into_body())
.await
.unwrap();
let body: Value = serde_json::from_slice(&body_bytes).unwrap();
assert_eq!(body["data"]["test"], "valid_response");
crate::plugin::test::await_mock_driver(http_driver).await;
crate::plugin::test::await_mock_driver(router_driver).await;
}
#[tokio::test]
async fn external_plugin_router_response_validation_disabled_empty() {
let (http_client, http_driver) = create_mock_http_client_router_response_empty_response();
let (router_service, mut router_handle) = create_mock_router_service_for_validation_test();
let router_driver = tokio::spawn(async move {
let (req, responder) = router_handle.next_request().await.unwrap();
responder.send_response(
router::Response::builder()
.data(json!({"test": 42}))
.context(req.context)
.build()
.unwrap(),
);
});
let service_stack = create_router_stage_for_response_validation_test()
.as_service(
http_client,
router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
false, )
.boxed();
let request = router::Request::fake_builder().build().unwrap();
let res = service_stack.oneshot(request).await.unwrap();
assert_eq!(res.response.status(), 200);
let body_bytes = router::body::into_bytes(res.response.into_body())
.await
.unwrap();
let body: Value = serde_json::from_slice(&body_bytes).unwrap();
assert!(body.as_object().unwrap().is_empty());
crate::plugin::test::await_mock_driver(http_driver).await;
crate::plugin::test::await_mock_driver(router_driver).await;
}
#[tokio::test]
async fn external_plugin_router_response_validation_disabled_invalid() {
let (http_client, http_driver) = create_mock_http_client_router_response_invalid_response();
let (router_service, mut router_handle) = create_mock_router_service_for_validation_test();
let router_driver = tokio::spawn(async move {
let (req, responder) = router_handle.next_request().await.unwrap();
responder.send_response(
router::Response::builder()
.data(json!({"test": 42}))
.context(req.context)
.build()
.unwrap(),
);
});
let service_stack = create_router_stage_for_response_validation_test()
.as_service(
http_client,
router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
false, )
.boxed();
let request = router::Request::fake_builder().build().unwrap();
let res = service_stack.oneshot(request).await.unwrap();
assert_eq!(res.response.status(), 200);
let body_bytes = router::body::into_bytes(res.response.into_body())
.await
.unwrap();
let body: Value = serde_json::from_slice(&body_bytes).unwrap();
assert_eq!(body["errors"], "this should be an array not a string");
crate::plugin::test::await_mock_driver(http_driver).await;
crate::plugin::test::await_mock_driver(router_driver).await;
}
fn create_router_stage_for_request_validation_test() -> RouterStage {
RouterStage {
request: RouterRequestConf {
body: true,
..Default::default()
},
..Default::default()
}
}
fn create_router_stage_for_request_with_false_condition() -> RouterStage {
RouterStage {
request: RouterRequestConf {
condition: Some(Condition::False),
headers: true,
context: ContextConf::All,
body: true,
sdl: true,
path: false,
method: false,
url: None,
},
response: Default::default(),
}
}
fn create_router_stage_for_response_with_false_condition() -> RouterStage {
RouterStage {
request: Default::default(),
response: RouterResponseConf {
condition: Condition::False,
headers: true,
context: ContextConf::All,
body: true,
sdl: true,
status_code: false,
url: None,
},
}
}
fn create_mock_router_service_for_validation_test() -> (
tower_test::mock::Mock<router::Request, router::Response>,
tower_test::mock::Handle<router::Request, router::Response>,
) {
tower_test::mock::pair::<router::Request, router::Response>()
}
fn create_mock_router_service() -> (
tower_test::mock::Mock<router::Request, router::Response>,
tokio::task::JoinHandle<()>,
) {
let (mock, mut handle) = tower_test::mock::pair::<router::Request, router::Response>();
let driver = tokio::spawn(async move {
while let Some((req, responder)) = handle.next_request().await {
responder.send_response(
router::Response::builder()
.data(json!({ "test": 1234_u32 }))
.errors(Vec::new())
.extensions(Object::new())
.context(req.context)
.build()
.unwrap(),
);
}
});
(mock, driver)
}
fn create_mock_http_client_router_request_valid_response() -> (
tower_test::mock::Mock<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>,
tokio::task::JoinHandle<()>,
) {
let (mock, mut handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let driver = tokio::spawn(async move {
let (req, responder) = handle.next_request().await.unwrap();
let context = req.context.clone();
let response_body = json!({
"version": 1,
"stage": "RouterRequest",
"control": "continue",
"body": "{\"data\": {\"test\": \"valid_response\"}}"
});
let response = http::Response::builder()
.status(200)
.body(router::body::from_bytes(
serde_json::to_string(&response_body).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
(mock, driver)
}
fn create_mock_http_client_router_response_valid_response() -> (
tower_test::mock::Mock<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>,
tokio::task::JoinHandle<()>,
) {
let (mock, mut handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let driver = tokio::spawn(async move {
let (req, responder) = handle.next_request().await.unwrap();
let context = req.context.clone();
let response_body = json!({
"version": 1,
"stage": "RouterResponse",
"control": "continue",
"body": "{\"data\": {\"test\": \"valid_response\"}}"
});
let response = http::Response::builder()
.status(200)
.body(router::body::from_bytes(
serde_json::to_string(&response_body).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
(mock, driver)
}
fn create_mock_http_client_router_response_invalid_response() -> (
tower_test::mock::Mock<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>,
tokio::task::JoinHandle<()>,
) {
let (mock, mut handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let driver = tokio::spawn(async move {
let (req, responder) = handle.next_request().await.unwrap();
let context = req.context.clone();
let response_body = json!({
"version": 1,
"stage": "RouterResponse",
"control": "continue",
"body": "{\"errors\": \"this should be an array not a string\"}"
});
let response = http::Response::builder()
.status(200)
.body(router::body::from_bytes(
serde_json::to_string(&response_body).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
(mock, driver)
}
fn create_mock_http_client_empty_router_response() -> (
tower_test::mock::Mock<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>,
tokio::task::JoinHandle<()>,
) {
let (mock, mut handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let driver = tokio::spawn(async move {
let (req, responder) = handle.next_request().await.unwrap();
let context = req.context.clone();
let response_body = json!({
"version": 1,
"stage": "RouterRequest",
"control": {
"break": 400
},
"body": "{}"
});
let response = http::Response::builder()
.status(200)
.body(router::body::from_bytes(
serde_json::to_string(&response_body).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
(mock, driver)
}
fn create_router_stage_for_response_validation_test() -> RouterStage {
RouterStage {
request: Default::default(),
response: RouterResponseConf {
condition: Default::default(),
headers: true,
context: ContextConf::All,
body: true,
sdl: true,
status_code: false,
url: None,
},
}
}
fn create_mock_http_client_router_response_empty_response() -> (
tower_test::mock::Mock<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>,
tokio::task::JoinHandle<()>,
) {
let (mock, mut handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let driver = tokio::spawn(async move {
let (req, responder) = handle.next_request().await.unwrap();
let context = req.context.clone();
let response_body = json!({
"version": 1,
"stage": "RouterResponse",
"control": "continue",
"body": "{}"
});
let response = http::Response::builder()
.status(200)
.body(router::body::from_bytes(
serde_json::to_string(&response_body).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
(mock, driver)
}
#[tokio::test]
async fn external_plugin_router_request_validation_disabled_empty() {
let (http_client, http_driver) = create_mock_http_client_empty_router_response();
let (router_service, router_handle) = create_mock_router_service_for_validation_test();
let service_stack = create_router_stage_for_request_validation_test()
.as_service(
http_client,
router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
false, )
.boxed();
let request = router::Request::fake_builder().build().unwrap();
let res = service_stack.oneshot(request).await.unwrap();
assert_eq!(res.response.status(), 400);
let body_bytes = router::body::into_bytes(res.response.into_body())
.await
.unwrap();
let body: Value = serde_json::from_slice(&body_bytes).unwrap();
assert!(
body.as_object().unwrap().is_empty()
|| body.get("data").is_some()
|| body.get("errors").is_some()
);
crate::plugin::test::await_mock_driver(http_driver).await;
crate::plugin::test::assert_no_mock_calls(router_handle).await;
}
#[tokio::test]
async fn external_plugin_router_request_validation_enabled_empty() {
let (http_client, http_driver) = create_mock_http_client_empty_router_response();
let (router_service, router_handle) = create_mock_router_service_for_validation_test();
let service_stack = create_router_stage_for_request_validation_test()
.as_service(
http_client,
router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
true, )
.boxed();
let request = router::Request::fake_builder().build().unwrap();
let res = service_stack.oneshot(request).await.unwrap();
assert_eq!(res.response.status(), 400);
let body_bytes = router::body::into_bytes(res.response.into_body())
.await
.unwrap();
let body: Value = serde_json::from_slice(&body_bytes).unwrap();
assert!(body.get("errors").is_some());
let errors = body["errors"].as_array().unwrap();
assert!(
errors[0]["message"]
.as_str()
.unwrap()
.contains("couldn't deserialize coprocessor output body")
);
crate::plugin::test::await_mock_driver(http_driver).await;
crate::plugin::test::assert_no_mock_calls(router_handle).await;
}
#[test]
fn it_externalizes_headers() {
let mut expected = HashMap::new();
expected.insert(
"content-type".to_string(),
vec![APPLICATION_JSON.essence_str().to_string()],
);
expected.insert(
"accept".to_string(),
vec![
APPLICATION_JSON.essence_str().to_string(),
TEXT_HTML.essence_str().to_string(),
],
);
let mut external_form = HeaderMap::new();
external_form.insert(
CONTENT_TYPE,
HeaderValue::from_static(APPLICATION_JSON.essence_str()),
);
external_form.insert(
ACCEPT,
HeaderValue::from_static(APPLICATION_JSON.essence_str()),
);
external_form.append(ACCEPT, HeaderValue::from_static(TEXT_HTML.essence_str()));
let actual = externalize_header_map(&external_form);
assert_eq!(expected, actual);
}
#[test]
fn it_internalizes_headers() {
let mut expected = HeaderMap::new();
expected.insert(
ACCEPT,
HeaderValue::from_static(APPLICATION_JSON.essence_str()),
);
expected.append(ACCEPT, HeaderValue::from_static(TEXT_HTML.essence_str()));
let mut external_form = HashMap::new();
external_form.insert(
"accept".to_string(),
vec![
APPLICATION_JSON.essence_str().to_string(),
TEXT_HTML.essence_str().to_string(),
],
);
external_form.insert("content-length".to_string(), vec!["1024".to_string()]);
let actual = internalize_header_map(external_form).expect("internalized header map");
assert_eq!(expected, actual);
}
#[test]
fn test_handle_graphql_response_validation_enabled() {
let original = graphql::Response::builder()
.data(json!({"test": "original"}))
.build();
let valid_response = json!({
"data": {"test": "modified"}
});
let result = handle_graphql_response(
original.clone(),
Some(valid_response),
true,
true,
&BodyConf::All(true),
)
.unwrap();
assert_eq!(result.data, Some(json!({"test": "modified"})));
let invalid_response = json!({
"invalid": "structure"
});
let result = handle_graphql_response(
original.clone(),
Some(invalid_response),
true,
true,
&BodyConf::All(true),
);
assert!(result.is_err());
}
#[test]
fn test_handle_graphql_response_validation_disabled() {
let original = graphql::Response::builder()
.data(json!({"test": "original"}))
.build();
let valid_response = json!({
"data": {"test": "modified"}
});
let result = handle_graphql_response(
original.clone(),
Some(valid_response),
false,
true,
&BodyConf::All(true),
)
.unwrap();
assert_eq!(result.data, Some(json!({"test": "modified"})));
let invalid_response = json!({
"errors": "this should be an array not a string"
});
let result = handle_graphql_response(
original.clone(),
Some(invalid_response),
false,
true,
&BodyConf::All(true),
)
.unwrap();
assert_eq!(result.data, Some(json!({"test": "original"})));
}
#[test]
fn test_handle_graphql_response_validation_disabled_empty_response() {
let original = graphql::Response::builder()
.data(json!({"test": "original"}))
.build();
let empty_response = json!({});
let result = handle_graphql_response(
original.clone(),
Some(empty_response),
false,
true,
&BodyConf::All(true),
)
.unwrap();
assert_eq!(result.data, None);
assert_eq!(result.errors.len(), 0);
}
#[test]
fn test_handle_graphql_response_validation_enabled_empty_response() {
let original = graphql::Response::builder()
.data(json!({"test": "original"}))
.build();
let empty_response = json!({});
let result = handle_graphql_response(
original.clone(),
Some(empty_response),
true,
true,
&BodyConf::All(true),
);
assert!(result.is_err());
}
fn create_subgraph_stage_for_validation_test() -> SubgraphStage {
SubgraphStage {
request: Default::default(),
response: SubgraphResponseConf {
condition: Condition::True,
headers: true,
context: ContextConf::All,
body: BodyConf::All(true),
service_name: false,
status_code: false,
subgraph_request_id: false,
url: None,
},
}
}
fn create_mock_subgraph_service() -> (
tower_test::mock::Mock<subgraph::Request, subgraph::Response>,
tokio::task::JoinHandle<()>,
) {
let (mock, mut handle) = tower_test::mock::pair::<subgraph::Request, subgraph::Response>();
let driver = tokio::spawn(async move {
while let Some((req, responder)) = handle.next_request().await {
responder.send_response(
subgraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.errors(Vec::new())
.extensions(Object::new())
.subgraph_name("coprocessorMockSubgraph")
.context(req.context)
.id(req.id)
.build(),
);
}
});
(mock, driver)
}
fn create_subgraph_stage_for_request_validation_test() -> SubgraphStage {
SubgraphStage {
request: SubgraphRequestConf {
condition: Condition::True,
headers: true,
context: ContextConf::All,
body: true,
uri: true,
method: true,
service_name: true,
subgraph_request_id: true,
url: None,
},
response: Default::default(),
}
}
fn create_subgraph_stage_for_request_with_false_condition() -> SubgraphStage {
SubgraphStage {
request: SubgraphRequestConf {
condition: Condition::False,
headers: true,
context: ContextConf::All,
body: true,
uri: true,
method: true,
service_name: true,
subgraph_request_id: true,
url: None,
},
response: Default::default(),
}
}
fn create_subgraph_stage_for_response_with_false_condition() -> SubgraphStage {
SubgraphStage {
request: Default::default(),
response: SubgraphResponseConf {
condition: Condition::False,
headers: true,
context: ContextConf::All,
body: BodyConf::All(true),
service_name: false,
status_code: false,
subgraph_request_id: false,
url: None,
},
}
}
fn create_mock_http_client_subgraph_request_valid_response() -> (
tower_test::mock::Mock<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>,
tokio::task::JoinHandle<()>,
) {
let (mock, mut handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let driver = tokio::spawn(async move {
let (req, responder) = handle.next_request().await.unwrap();
let context = req.context.clone();
let response_body = json!({
"version": 1,
"stage": "SubgraphRequest",
"control": {
"break": 400
},
"body": {
"data": {"test": "valid_response"}
}
});
let response = http::Response::builder()
.status(200)
.body(router::body::from_bytes(
serde_json::to_string(&response_body).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
(mock, driver)
}
fn create_mock_http_client_subgraph_request_empty_response() -> (
tower_test::mock::Mock<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>,
tokio::task::JoinHandle<()>,
) {
let (mock, mut handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let driver = tokio::spawn(async move {
let (req, responder) = handle.next_request().await.unwrap();
let context = req.context.clone();
let response_body = json!({
"version": 1,
"stage": "SubgraphRequest",
"control": {
"break": 400
},
"body": {}
});
let response = http::Response::builder()
.status(200)
.body(router::body::from_bytes(
serde_json::to_string(&response_body).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
(mock, driver)
}
fn create_mock_http_client_subgraph_request_invalid_response() -> (
tower_test::mock::Mock<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>,
tokio::task::JoinHandle<()>,
) {
let (mock, mut handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let driver = tokio::spawn(async move {
let (req, responder) = handle.next_request().await.unwrap();
let context = req.context.clone();
let response_body = json!({
"version": 1,
"stage": "SubgraphRequest",
"control": {
"break": 400
},
"body": {
"errors": "this should be an array not a string"
}
});
let response = http::Response::builder()
.status(200)
.body(router::body::from_bytes(
serde_json::to_string(&response_body).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
(mock, driver)
}
fn create_mock_http_client_subgraph_response_valid_response() -> (
tower_test::mock::Mock<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>,
tokio::task::JoinHandle<()>,
) {
let (mock, mut handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let driver = tokio::spawn(async move {
let (req, responder) = handle.next_request().await.unwrap();
let context = req.context.clone();
let response_body = json!({
"version": 1,
"stage": "SubgraphResponse",
"control": "continue",
"body": {
"data": {"test": "valid_response"}
}
});
let response = http::Response::builder()
.body(router::body::from_bytes(
serde_json::to_string(&response_body).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
(mock, driver)
}
fn create_mock_http_client_subgraph_response_empty_response() -> (
tower_test::mock::Mock<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>,
tokio::task::JoinHandle<()>,
) {
let (mock, mut handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let driver = tokio::spawn(async move {
let (req, responder) = handle.next_request().await.unwrap();
let context = req.context.clone();
let response_body = json!({
"version": 1,
"stage": "SubgraphResponse",
"control": "continue",
"body": {}
});
let response = http::Response::builder()
.body(router::body::from_bytes(
serde_json::to_string(&response_body).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
(mock, driver)
}
fn create_mock_http_client_invalid_subgraph_response() -> (
tower_test::mock::Mock<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>,
tokio::task::JoinHandle<()>,
) {
let (mock, mut handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let driver = tokio::spawn(async move {
let (req, responder) = handle.next_request().await.unwrap();
let context = req.context.clone();
let response_body = json!({
"version": 1,
"stage": "SubgraphResponse",
"control": "continue",
"body": {
"errors": "this should be an array not a string"
}
});
let response = http::Response::builder()
.body(router::body::from_bytes(
serde_json::to_string(&response_body).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
(mock, driver)
}
fn create_mock_http_client_hard_error() -> (
tower_test::mock::Mock<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>,
tokio::task::JoinHandle<()>,
) {
let (mock, mut handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let driver = tokio::spawn(async move {
while let Some((_req, responder)) = handle.next_request().await {
responder.send_error("hard error from mock http client");
}
});
(mock, driver)
}
#[tokio::test]
async fn external_plugin_subgraph_response_validation_disabled_invalid() {
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_client, http_driver) = create_mock_http_client_invalid_subgraph_response();
let service = create_subgraph_stage_for_validation_test().as_service(
http_client,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
false, );
let request = subgraph::Request::fake_builder().build();
let res = service.oneshot(request).await.unwrap();
assert_eq!(
&json!({ "test": 1234_u32 }),
res.response.body().data.as_ref().unwrap()
);
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_request_validation_enabled_valid() {
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_client, http_driver) = create_mock_http_client_subgraph_request_valid_response();
let service = create_subgraph_stage_for_request_validation_test().as_service(
http_client,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
true, );
let request = subgraph::Request::fake_builder().build();
let res = service.oneshot(request).await.unwrap();
assert_eq!(res.response.status(), 400);
assert_eq!(
&json!({"test": "valid_response"}),
res.response.body().data.as_ref().unwrap()
);
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_request_validation_enabled_empty() {
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_client, http_driver) = create_mock_http_client_subgraph_request_empty_response();
let service = create_subgraph_stage_for_request_validation_test().as_service(
http_client,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
true, );
let request = subgraph::Request::fake_builder().build();
let res = service.oneshot(request).await.unwrap();
assert_eq!(res.response.status(), 400);
assert!(!res.response.body().errors.is_empty());
assert!(
res.response.body().errors[0]
.message
.contains("couldn't deserialize coprocessor output body")
);
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_request_validation_enabled_invalid() {
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_client, http_driver) =
create_mock_http_client_subgraph_request_invalid_response();
let service = create_subgraph_stage_for_request_validation_test().as_service(
http_client,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
true, );
let request = subgraph::Request::fake_builder().build();
let res = service.oneshot(request).await.unwrap();
assert_eq!(res.response.status(), 400);
assert!(!res.response.body().errors.is_empty());
assert!(
res.response.body().errors[0]
.message
.contains("couldn't deserialize coprocessor output body")
);
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_request_validation_disabled_valid() {
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_client, http_driver) = create_mock_http_client_subgraph_request_valid_response();
let service = create_subgraph_stage_for_request_validation_test().as_service(
http_client,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
false, );
let request = subgraph::Request::fake_builder().build();
let res = service.oneshot(request).await.unwrap();
assert_eq!(res.response.status(), 400);
assert_eq!(
&json!({"test": "valid_response"}),
res.response.body().data.as_ref().unwrap()
);
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_request_validation_disabled_empty() {
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_client, http_driver) = create_mock_http_client_subgraph_request_empty_response();
let service = create_subgraph_stage_for_request_validation_test().as_service(
http_client,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
false, );
let request = subgraph::Request::fake_builder().build();
let res = service.oneshot(request).await.unwrap();
assert_eq!(res.response.status(), 400);
assert_eq!(res.response.body().data, None);
assert_eq!(res.response.body().errors.len(), 0);
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_request_validation_disabled_invalid() {
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_client, http_driver) =
create_mock_http_client_subgraph_request_invalid_response();
let service = create_subgraph_stage_for_request_validation_test().as_service(
http_client,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
false, );
let request = subgraph::Request::fake_builder().build();
let res = service.oneshot(request).await.unwrap();
assert_eq!(res.response.status(), 400);
assert!(res.response.body().data.is_some() || !res.response.body().errors.is_empty());
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_response_validation_enabled_valid() {
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_client, http_driver) = create_mock_http_client_subgraph_response_valid_response();
let service = create_subgraph_stage_for_validation_test().as_service(
http_client,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
true, );
let request = subgraph::Request::fake_builder().build();
let res = service.oneshot(request).await.unwrap();
assert_eq!(
&json!({"test": "valid_response"}),
res.response.body().data.as_ref().unwrap()
);
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_response_validation_enabled_empty() {
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_client, http_driver) = create_mock_http_client_subgraph_response_empty_response();
let service = create_subgraph_stage_for_validation_test().as_service(
http_client,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
true, );
let request = subgraph::Request::fake_builder().build();
let result = service.oneshot(request).await;
assert!(result.is_err());
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_response_validation_enabled_invalid() {
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_client, http_driver) = create_mock_http_client_invalid_subgraph_response();
let service = create_subgraph_stage_for_validation_test().as_service(
http_client,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
true, );
let request = subgraph::Request::fake_builder().build();
let result = service.oneshot(request).await;
assert!(result.is_err());
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_response_validation_disabled_valid() {
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_client, http_driver) = create_mock_http_client_subgraph_response_valid_response();
let service = create_subgraph_stage_for_validation_test().as_service(
http_client,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
false, );
let request = subgraph::Request::fake_builder().build();
let res = service.oneshot(request).await.unwrap();
assert_eq!(
&json!({"test": "valid_response"}),
res.response.body().data.as_ref().unwrap()
);
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_response_validation_disabled_empty() {
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_client, http_driver) = create_mock_http_client_subgraph_response_empty_response();
let service = create_subgraph_stage_for_validation_test().as_service(
http_client,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
false, );
let request = subgraph::Request::fake_builder().build();
let res = service.oneshot(request).await.unwrap();
assert_eq!(res.response.body().data, None);
assert_eq!(res.response.body().errors.len(), 0);
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_response_selective_errors_only() {
let subgraph_stage = SubgraphStage {
request: Default::default(),
response: SubgraphResponseConf {
condition: Default::default(),
body: BodyConf::Selective(BodyFieldsConf {
data: false,
errors: true,
extensions: false,
}),
..Default::default()
},
};
let (mock_subgraph_service, mut handle_subgraph) =
tower_test::mock::pair::<subgraph::Request, subgraph::Response>();
let subgraph_driver = tokio::spawn(async move {
let (req, responder) = handle_subgraph.next_request().await.unwrap();
use crate::graphql::Error;
responder.send_response(
subgraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.error(
Error::builder()
.message("test error")
.extension_code("TEST_ERROR")
.build(),
)
.extensions(Object::from_iter(vec![(
"ext_key".into(),
json!("ext_value"),
)]))
.context(req.context)
.id(req.id)
.subgraph_name("test_subgraph".to_string())
.build(),
);
});
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let (_, body) = req.http_request.into_parts();
let body: Value =
serde_json::from_slice(&router::body::into_bytes(body).await.unwrap()).unwrap();
assert!(body.get("body").is_some());
let response_body = body.get("body").unwrap();
assert!(response_body.get("errors").is_some());
assert!(response_body.get("data").is_none());
assert!(response_body.get("extensions").is_none());
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "SubgraphResponse",
"body": {
"errors": [{ "message": "modified error" }]
}
}"#,
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
true,
);
let request = subgraph::Request::fake_builder().build();
let response = service.oneshot(request).await.unwrap();
assert_eq!(response.response.body().errors[0].message, "modified error");
assert_eq!(
json!({ "test": 1234_u32 }),
*response.response.body().data.as_ref().unwrap()
);
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_response_selective_data_and_extensions() {
let subgraph_stage = SubgraphStage {
request: Default::default(),
response: SubgraphResponseConf {
condition: Default::default(),
body: BodyConf::Selective(BodyFieldsConf {
data: true,
errors: false,
extensions: true,
}),
..Default::default()
},
};
let (mock_subgraph_service, mut handle_subgraph) =
tower_test::mock::pair::<subgraph::Request, subgraph::Response>();
let subgraph_driver = tokio::spawn(async move {
let (req, responder) = handle_subgraph.next_request().await.unwrap();
use crate::graphql::Error;
responder.send_response(
subgraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.error(
Error::builder()
.message("test error")
.extension_code("TEST_ERROR")
.build(),
)
.extensions(Object::from_iter(vec![(
"ext_key".into(),
json!("ext_value"),
)]))
.context(req.context)
.id(req.id)
.subgraph_name("test_subgraph".to_string())
.build(),
);
});
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let (_, body) = req.http_request.into_parts();
let body: Value =
serde_json::from_slice(&router::body::into_bytes(body).await.unwrap()).unwrap();
assert!(body.get("body").is_some());
let response_body = body.get("body").unwrap();
assert!(response_body.get("data").is_some());
assert!(response_body.get("extensions").is_some());
assert!(response_body.get("errors").is_none());
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "SubgraphResponse",
"body": {
"data": { "test": 5678 },
"extensions": { "ext_key": "modified_value" }
}
}"#,
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
true,
);
let request = subgraph::Request::fake_builder().build();
let response = service.oneshot(request).await.unwrap();
assert_eq!(
json!({ "test": 5678_u32 }),
*response.response.body().data.as_ref().unwrap()
);
assert_eq!(
json!("modified_value"),
*response.response.body().extensions.get("ext_key").unwrap()
);
assert_eq!(response.response.body().errors[0].message, "test error");
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_response_selective_nothing() {
let subgraph_stage = SubgraphStage {
request: Default::default(),
response: SubgraphResponseConf {
condition: Default::default(),
body: BodyConf::Selective(BodyFieldsConf {
data: false,
errors: false,
extensions: false,
}),
..Default::default()
},
};
let (mock_subgraph_service, mut handle_subgraph) =
tower_test::mock::pair::<subgraph::Request, subgraph::Response>();
let subgraph_driver = tokio::spawn(async move {
let (req, responder) = handle_subgraph.next_request().await.unwrap();
responder.send_response(
subgraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.extensions(Object::new())
.context(req.context)
.id(req.id)
.subgraph_name("test_subgraph".to_string())
.build(),
);
});
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let (_, body) = req.http_request.into_parts();
let body: Value =
serde_json::from_slice(&router::body::into_bytes(body).await.unwrap()).unwrap();
assert!(body.get("body").is_none());
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "SubgraphResponse"
}"#,
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
true,
);
let request = subgraph::Request::fake_builder().build();
let response = service.oneshot(request).await.unwrap();
assert_eq!(
json!({ "test": 1234_u32 }),
*response.response.body().data.as_ref().unwrap()
);
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_response_selective_errors_with_empty_errors() {
let subgraph_stage = SubgraphStage {
request: Default::default(),
response: SubgraphResponseConf {
condition: Default::default(),
body: BodyConf::Selective(BodyFieldsConf {
data: false,
errors: true,
extensions: false,
}),
..Default::default()
},
};
let (mock_subgraph_service, mut handle_subgraph) =
tower_test::mock::pair::<subgraph::Request, subgraph::Response>();
let subgraph_driver = tokio::spawn(async move {
let (req, responder) = handle_subgraph.next_request().await.unwrap();
responder.send_response(
subgraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.errors(Vec::new())
.extensions(Object::new())
.context(req.context)
.id(req.id)
.subgraph_name("test_subgraph".to_string())
.build(),
);
});
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let (_, body) = req.http_request.into_parts();
let body: Value =
serde_json::from_slice(&router::body::into_bytes(body).await.unwrap()).unwrap();
assert!(body.get("body").is_some());
let response_body = body.get("body").unwrap();
assert!(
response_body.get("errors").is_some(),
"errors field should be present"
);
let errors = response_body.get("errors").unwrap();
assert!(errors.is_array(), "errors should be an array");
assert_eq!(
errors.as_array().unwrap().len(),
0,
"errors array should be empty"
);
assert!(response_body.get("data").is_none());
assert!(response_body.get("extensions").is_none());
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "SubgraphResponse"
}"#,
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
true,
);
let request = subgraph::Request::fake_builder().build();
let response = service.oneshot(request).await.unwrap();
assert_eq!(
json!({ "test": 1234_u32 }),
*response.response.body().data.as_ref().unwrap()
);
assert!(response.response.body().errors.is_empty());
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn external_plugin_subgraph_response_selective_extensions_only() {
let subgraph_stage = SubgraphStage {
request: Default::default(),
response: SubgraphResponseConf {
condition: Default::default(),
body: BodyConf::Selective(BodyFieldsConf {
data: false,
errors: false,
extensions: true,
}),
..Default::default()
},
};
let (mock_subgraph_service, mut handle_subgraph) =
tower_test::mock::pair::<subgraph::Request, subgraph::Response>();
let subgraph_driver = tokio::spawn(async move {
let (req, responder) = handle_subgraph.next_request().await.unwrap();
responder.send_response(
subgraph::Response::builder()
.data(json!({ "test": 5678_u32 }))
.errors(Vec::new())
.extensions(Object::from_iter(vec![(
"trace_id".into(),
json!("abc123"),
)]))
.context(req.context)
.id(req.id)
.subgraph_name("test_subgraph".to_string())
.build(),
);
});
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let (_, body) = req.http_request.into_parts();
let body: Value =
serde_json::from_slice(&router::body::into_bytes(body).await.unwrap()).unwrap();
assert!(body.get("body").is_some());
let response_body = body.get("body").unwrap();
assert!(
response_body.get("extensions").is_some(),
"extensions should be present"
);
assert!(
response_body.get("data").is_none(),
"data should not be sent"
);
assert!(
response_body.get("errors").is_none(),
"errors should not be sent"
);
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "SubgraphResponse",
"body": {
"extensions": { "trace_id": "abc123", "processor": "modified" }
}
}"#,
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed_clone(),
"http://test".to_string(),
"my_subgraph_service_name".to_string(),
false, );
let request = subgraph::Request::fake_builder().build();
let response = service.oneshot(request).await.unwrap();
assert_eq!(
json!({ "test": 5678_u32 }),
*response.response.body().data.as_ref().unwrap()
);
assert_eq!(
response.response.body().extensions.get("trace_id"),
Some(&json!("abc123"))
);
assert_eq!(
response.response.body().extensions.get("processor"),
Some(&json!("modified"))
);
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
fn valid_response() -> crate::graphql::Response {
crate::graphql::Response::builder()
.data(json!({"field": "value"}))
.build()
}
fn valid_response_with_errors() -> crate::graphql::Response {
use crate::graphql::Error;
crate::graphql::Response::builder()
.errors(vec![
Error::builder()
.message("error")
.extension_code("TEST")
.build(),
])
.build()
}
fn invalid_response() -> crate::graphql::Response {
crate::graphql::Response::builder().build() }
fn valid_copro_body() -> Value {
json!({"data": {"field": "new_value"}})
}
fn invalid_copro_body() -> Value {
json!({}) }
#[test]
fn test_minimal_graphql_validation() {
assert!(is_graphql_response_minimally_valid(&valid_response()));
assert!(is_graphql_response_minimally_valid(
&valid_response_with_errors()
));
assert!(!is_graphql_response_minimally_valid(&invalid_response()));
}
#[test]
fn test_was_incoming_payload_valid() {
assert!(was_incoming_payload_valid(
&valid_response(),
&BodyConf::All(false)
));
assert!(was_incoming_payload_valid(
&invalid_response(),
&BodyConf::All(false)
));
assert!(was_incoming_payload_valid(
&valid_response(),
&BodyConf::All(true)
));
assert!(!was_incoming_payload_valid(
&invalid_response(),
&BodyConf::All(true)
));
assert!(was_incoming_payload_valid(
&valid_response(),
&BodyConf::Selective(BodyFieldsConf {
data: false,
errors: true,
extensions: false,
})
));
assert!(!was_incoming_payload_valid(
&invalid_response(),
&BodyConf::Selective(BodyFieldsConf {
data: false,
errors: true,
extensions: false,
})
));
assert!(was_incoming_payload_valid(
&invalid_response(),
&BodyConf::Selective(BodyFieldsConf {
data: false,
errors: false,
extensions: false,
})
));
assert!(was_incoming_payload_valid(
&invalid_response(),
&BodyConf::Selective(BodyFieldsConf {
data: false,
errors: false,
extensions: true,
})
));
}
#[test]
fn test_filter_graphql_response_body() {
use crate::plugins::coprocessor::BodyFieldsConf;
use crate::plugins::coprocessor::filter_graphql_response_body;
let response = valid_response();
assert!(filter_graphql_response_body(&response, &BodyConf::All(false)).is_none());
let result = filter_graphql_response_body(&response, &BodyConf::All(true));
assert!(result.is_some());
let body = result.unwrap();
assert!(body.get("data").is_some());
let response_with_errors = graphql::Response::builder()
.data(serde_json_bytes::json!({"test": "data"}))
.errors(vec![
graphql::Error::builder().message("test error").build(),
])
.extensions(serde_json_bytes::Map::from_iter([(
"ext_key".into(),
serde_json_bytes::json!("ext_value"),
)]))
.build();
let result = filter_graphql_response_body(
&response_with_errors,
&BodyConf::Selective(BodyFieldsConf {
data: false,
errors: true,
extensions: false,
}),
);
assert!(result.is_some());
let body = result.unwrap();
assert!(body.get("data").is_none(), "data should not be included");
assert!(body.get("errors").is_some(), "errors should be included");
assert!(
body.get("extensions").is_none(),
"extensions should not be included"
);
let result = filter_graphql_response_body(
&response_with_errors,
&BodyConf::Selective(BodyFieldsConf {
data: true,
errors: false,
extensions: false,
}),
);
assert!(result.is_some());
let body = result.unwrap();
assert!(body.get("data").is_some(), "data should be included");
assert!(
body.get("errors").is_none(),
"errors should not be included"
);
let result = filter_graphql_response_body(
&response_with_errors,
&BodyConf::Selective(BodyFieldsConf {
data: true,
errors: true,
extensions: true,
}),
);
assert!(result.is_some());
let body = result.unwrap();
assert!(body.get("data").is_some());
assert!(body.get("errors").is_some());
assert!(body.get("extensions").is_some());
let result = filter_graphql_response_body(
&response_with_errors,
&BodyConf::Selective(BodyFieldsConf {
data: false,
errors: false,
extensions: false,
}),
);
assert!(result.is_none());
}
#[test]
fn test_filter_graphql_response_body_includes_empty_fields() {
use crate::plugins::coprocessor::BodyFieldsConf;
use crate::plugins::coprocessor::filter_graphql_response_body;
let response_data_only = graphql::Response::builder()
.data(serde_json_bytes::json!({"test": "data"}))
.build();
let result = filter_graphql_response_body(
&response_data_only,
&BodyConf::Selective(BodyFieldsConf {
data: false,
errors: true,
extensions: false,
}),
);
assert!(result.is_some());
let body = result.unwrap();
assert!(
body.get("errors").is_some(),
"errors field should be included even when empty"
);
assert_eq!(
body.get("errors").unwrap().as_array().unwrap().len(),
0,
"errors should be empty array"
);
assert!(body.get("data").is_none(), "data should not be included");
let result = filter_graphql_response_body(
&response_data_only,
&BodyConf::Selective(BodyFieldsConf {
data: false,
errors: false,
extensions: true,
}),
);
assert!(result.is_some());
let body = result.unwrap();
assert!(
body.get("extensions").is_some(),
"extensions field should be included even when empty"
);
assert_eq!(
body.get("extensions").unwrap().as_object().unwrap().len(),
0,
"extensions should be empty object"
);
let response_no_data = graphql::Response::builder()
.errors(vec![
graphql::Error::builder().message("test error").build(),
])
.build();
let result = filter_graphql_response_body(
&response_no_data,
&BodyConf::Selective(BodyFieldsConf {
data: true,
errors: false,
extensions: false,
}),
);
assert!(result.is_some());
let body = result.unwrap();
assert!(
body.get("data").is_some(),
"data field should be included even when null"
);
assert!(body.get("data").unwrap().is_null(), "data should be null");
assert!(
body.get("errors").is_none(),
"errors should not be included"
);
}
#[test]
fn test_body_conf_should_send_data_or_errors() {
use crate::plugins::coprocessor::BodyFieldsConf;
assert!(BodyConf::All(true).should_send_data_or_errors());
assert!(!BodyConf::All(false).should_send_data_or_errors());
let extensions_only = BodyConf::Selective(BodyFieldsConf {
data: false,
errors: false,
extensions: true,
});
assert!(!extensions_only.should_send_data_or_errors());
let errors_only = BodyConf::Selective(BodyFieldsConf {
data: false,
errors: true,
extensions: false,
});
assert!(errors_only.should_send_data_or_errors());
let data_only = BodyConf::Selective(BodyFieldsConf {
data: true,
errors: false,
extensions: false,
});
assert!(data_only.should_send_data_or_errors());
let data_and_extensions = BodyConf::Selective(BodyFieldsConf {
data: true,
errors: false,
extensions: true,
});
assert!(data_and_extensions.should_send_data_or_errors());
}
#[test]
fn test_conditional_validation_logic() {
assert!(
handle_graphql_response(
invalid_response(),
Some(invalid_copro_body()),
true,
false,
&BodyConf::All(true)
)
.is_ok()
);
assert!(
handle_graphql_response(
valid_response(),
Some(invalid_copro_body()),
true,
true,
&BodyConf::All(true)
)
.is_err()
);
assert!(
handle_graphql_response(
valid_response(),
Some(valid_copro_body()),
true,
true,
&BodyConf::All(true)
)
.is_ok()
);
assert!(
handle_graphql_response(
valid_response(),
Some(invalid_copro_body()),
false,
true,
&BodyConf::All(true)
)
.is_ok()
);
}
#[test]
fn test_selective_body_field_merging() {
use crate::plugins::coprocessor::BodyFieldsConf;
let original = graphql::Response::builder()
.data(json!({"original": "data"}))
.error(Error::builder().message("original error").build())
.extension("original_ext", json!("original_value"))
.build();
let selective_data = BodyConf::Selective(BodyFieldsConf {
data: true,
errors: false,
extensions: false,
});
let copro_response = json!({
"data": {"modified": "data"},
"errors": [], "extensions": {} });
let result = handle_graphql_response(
original.clone(),
Some(copro_response),
false,
true,
&selective_data,
)
.unwrap();
assert_eq!(result.data, Some(json!({"modified": "data"})));
assert_eq!(result.errors.len(), 1);
assert_eq!(result.errors[0].message, "original error");
assert_eq!(
result.extensions.get("original_ext"),
Some(&json!("original_value"))
);
let selective_errors = BodyConf::Selective(BodyFieldsConf {
data: false,
errors: true,
extensions: false,
});
let copro_response = json!({
"data": null, "errors": [{"message": "modified error"}]
});
let result = handle_graphql_response(
original.clone(),
Some(copro_response),
false,
true,
&selective_errors,
)
.unwrap();
assert_eq!(result.data, Some(json!({"original": "data"})));
assert_eq!(result.errors.len(), 1);
assert_eq!(result.errors[0].message, "modified error");
assert_eq!(
result.extensions.get("original_ext"),
Some(&json!("original_value"))
);
let selective_extensions = BodyConf::Selective(BodyFieldsConf {
data: false,
errors: false,
extensions: true,
});
let copro_response = json!({
"extensions": {"modified_ext": "modified_value"}
});
let result = handle_graphql_response(
original.clone(),
Some(copro_response),
false,
true,
&selective_extensions,
)
.unwrap();
assert_eq!(result.data, Some(json!({"original": "data"})));
assert_eq!(result.errors.len(), 1);
assert_eq!(result.errors[0].message, "original error");
assert_eq!(
result.extensions.get("modified_ext"),
Some(&json!("modified_value"))
);
assert_eq!(result.extensions.get("original_ext"), None);
let selective_data_ext = BodyConf::Selective(BodyFieldsConf {
data: true,
errors: false,
extensions: true,
});
let copro_response = json!({
"data": {"modified": "data"},
"extensions": {"modified_ext": "modified_value"}
});
let result = handle_graphql_response(
original.clone(),
Some(copro_response),
false,
true,
&selective_data_ext,
)
.unwrap();
assert_eq!(result.data, Some(json!({"modified": "data"})));
assert_eq!(result.errors.len(), 1);
assert_eq!(result.errors[0].message, "original error");
assert_eq!(
result.extensions.get("modified_ext"),
Some(&json!("modified_value"))
);
}
#[test]
fn test_update_context_from_coprocessor_deletes_missing_keys() {
use std::collections::HashSet;
use crate::Context;
use crate::plugins::coprocessor::update_context_from_coprocessor;
let target_context = Context::new();
target_context.insert("k1", "v1".to_string()).unwrap();
target_context.insert("k2", "v2".to_string()).unwrap();
target_context.insert("k3", "v3".to_string()).unwrap();
let keys_sent: HashSet<String> = ["k1", "k2", "k3"].into_iter().map(String::from).collect();
let returned_context = Context::new();
returned_context
.insert("k1", "v1_updated".to_string())
.unwrap();
returned_context.insert("k3", "v3".to_string()).unwrap();
update_context_from_coprocessor(&target_context, returned_context, &keys_sent).unwrap();
assert_eq!(
target_context.get_json_value("k1"),
Some(serde_json_bytes::json!("v1_updated"))
);
assert!(!target_context.contains_key("k2"));
assert_eq!(
target_context.get_json_value("k3"),
Some(serde_json_bytes::json!("v3"))
);
}
#[test]
fn test_update_context_from_coprocessor_adds_new_keys() {
use std::collections::HashSet;
use crate::Context;
use crate::plugins::coprocessor::update_context_from_coprocessor;
let target_context = Context::new();
target_context.insert("k1", "v1".to_string()).unwrap();
let keys_sent: HashSet<String> = ["k1"].into_iter().map(String::from).collect();
let returned_context = Context::new();
returned_context
.insert("k1", "v1_updated".to_string())
.unwrap();
returned_context.insert("k2", "v2_new".to_string()).unwrap();
update_context_from_coprocessor(&target_context, returned_context, &keys_sent).unwrap();
assert_eq!(
target_context.get_json_value("k1"),
Some(serde_json_bytes::json!("v1_updated"))
);
assert_eq!(
target_context.get_json_value("k2"),
Some(serde_json_bytes::json!("v2_new"))
);
}
#[test]
fn test_update_context_from_coprocessor_preserves_keys_not_sent() {
use std::collections::HashSet;
use crate::Context;
use crate::plugins::coprocessor::update_context_from_coprocessor;
let target_context = Context::new();
target_context.insert("k1", "v1".to_string()).unwrap();
target_context
.insert("key_not_sent", "preserved_value".to_string())
.unwrap();
let returned_context = Context::new();
let keys_sent: HashSet<String> = ["k1"].into_iter().map(String::from).collect();
update_context_from_coprocessor(&target_context, returned_context, &keys_sent).unwrap();
assert!(!target_context.contains_key("k1"));
assert_eq!(
target_context.get_json_value("key_not_sent"),
Some(serde_json_bytes::json!("preserved_value"))
);
}
#[test]
fn test_update_context_from_coprocessor_preserves_concurrently_added_keys() {
use std::collections::HashSet;
use crate::Context;
use crate::plugins::coprocessor::update_context_from_coprocessor;
let target_context = Context::new();
target_context.insert("k1", "v1".to_string()).unwrap();
target_context.insert("k2", "v2".to_string()).unwrap();
target_context
.insert("k3", "concurrent_value".to_string())
.unwrap();
let keys_sent: HashSet<String> = ["k1", "k2"].into_iter().map(String::from).collect();
let returned_context = Context::new();
returned_context.insert("k1", "v1".to_string()).unwrap();
update_context_from_coprocessor(&target_context, returned_context, &keys_sent).unwrap();
assert!(target_context.contains_key("k1"));
assert!(!target_context.contains_key("k2")); assert!(target_context.contains_key("k3")); }
#[test]
fn test_sibling_subgraph_response_does_not_delete_other_subgraphs_keys() {
use std::collections::HashSet;
use crate::Context;
use crate::plugins::coprocessor::update_context_from_coprocessor;
let shared_context = Context::new();
shared_context
.insert("base_key", "base".to_string())
.unwrap();
shared_context
.insert("accounts_request_start", 1234i64)
.unwrap();
shared_context
.insert("book_request_start", 5678i64)
.unwrap();
let keys_sent: HashSet<String> = ["base_key", "book_request_start"]
.into_iter()
.map(String::from)
.collect();
let returned_context = Context::new();
returned_context
.insert("base_key", "base".to_string())
.unwrap();
returned_context
.insert("book_request_start", 5678i64)
.unwrap();
returned_context
.insert("book_request_end", 5700i64)
.unwrap();
update_context_from_coprocessor(&shared_context, returned_context, &keys_sent).unwrap();
assert!(shared_context.contains_key("accounts_request_start"));
assert!(shared_context.contains_key("book_request_start"));
assert!(shared_context.contains_key("book_request_end"));
assert!(shared_context.contains_key("base_key"));
}
#[tokio::test]
async fn subgraph_request_metric_incremented_when_condition_true() {
async {
for _ in 0..2 {
let _stage = create_subgraph_stage_for_request_validation_test();
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_client, http_driver) =
create_mock_http_client_subgraph_request_valid_response();
let _service = _stage.as_service(
http_client,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_service".to_string(),
false,
);
let _request = subgraph::Request::fake_builder().build();
let _response = _service.oneshot(_request).await;
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
assert_coprocessor_operations_metrics(&[(
PipelineStep::SubgraphRequest,
2,
Some(true),
)]);
}
.with_metrics()
.await;
}
#[tokio::test]
async fn subgraph_response_metric_incremented_when_condition_true() {
async {
for _ in 0..3 {
let _stage = create_subgraph_stage_for_validation_test();
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_client, http_driver) =
create_mock_http_client_subgraph_response_valid_response();
let _service = _stage.as_service(
http_client,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_service".to_string(),
false,
);
let _request = subgraph::Request::fake_builder().build();
let _response = _service.oneshot(_request).await;
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
assert_coprocessor_operations_metrics(&[(
PipelineStep::SubgraphResponse,
3,
Some(true),
)]);
}
.with_metrics()
.await;
}
#[tokio::test]
async fn subgraph_request_metric_not_incremented_when_condition_false() {
async {
for _ in 0..2 {
let _stage = create_subgraph_stage_for_request_with_false_condition();
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_mock, http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let _service = _stage.as_service(
http_mock,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_service".to_string(),
false,
);
let _request = subgraph::Request::fake_builder().build();
let _response = _service.oneshot(_request).await;
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::assert_no_mock_calls(http_handle).await;
}
assert_coprocessor_operations_metrics(&[]);
}
.with_metrics()
.await;
}
#[tokio::test]
async fn subgraph_response_metric_not_incremented_when_condition_false() {
async {
for _ in 0..3 {
let _stage = create_subgraph_stage_for_response_with_false_condition();
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_mock, http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let _service = _stage.as_service(
http_mock,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_service".to_string(),
false,
);
let _request = subgraph::Request::fake_builder().build();
let _response = _service.oneshot(_request).await;
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::assert_no_mock_calls(http_handle).await;
}
assert_coprocessor_operations_metrics(&[]);
}
.with_metrics()
.await;
}
#[tokio::test]
async fn both_subgraph_stages_metric_incremented_when_conditions_true() {
async {
for _ in 0..2 {
let _stage = create_subgraph_stage_for_request_validation_test();
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_client, http_driver) =
create_mock_http_client_subgraph_request_valid_response();
let _service = _stage.as_service(
http_client,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_service".to_string(),
false,
);
let _request = subgraph::Request::fake_builder().build();
let _response = _service.oneshot(_request).await;
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
for _ in 0..3 {
let _stage = create_subgraph_stage_for_validation_test();
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_client, http_driver) =
create_mock_http_client_subgraph_response_valid_response();
let _service = _stage.as_service(
http_client,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_service".to_string(),
false,
);
let _request = subgraph::Request::fake_builder().build();
let _response = _service.oneshot(_request).await;
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
assert_coprocessor_operations_metrics(&[
(PipelineStep::SubgraphRequest, 2, Some(true)),
(PipelineStep::SubgraphResponse, 3, Some(true)),
]);
}
.with_metrics()
.await;
}
#[tokio::test]
async fn subgraph_request_metric_incremented_for_errored_stage_processing() {
async {
for _ in 0..2 {
let _stage = create_subgraph_stage_for_request_validation_test();
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_client, http_driver) = create_mock_http_client_hard_error();
let _service = _stage.as_service(
http_client,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_service".to_string(),
true,
);
let _request = subgraph::Request::fake_builder().build();
let _response = _service.oneshot(_request).await;
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
assert_coprocessor_operations_metrics(&[(
PipelineStep::SubgraphRequest,
2,
Some(false),
)]);
}
.with_metrics()
.await;
}
#[tokio::test]
async fn subgraph_response_metric_incremented_for_errored_stage_processing() {
async {
for _ in 0..3 {
let _stage = create_subgraph_stage_for_validation_test();
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_client, http_driver) = create_mock_http_client_hard_error();
let _service = _stage.as_service(
http_client,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_service".to_string(),
true,
);
let _request = subgraph::Request::fake_builder().build();
let _response = _service.oneshot(_request).await;
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
assert_coprocessor_operations_metrics(&[(
PipelineStep::SubgraphResponse,
3,
Some(false),
)]);
}
.with_metrics()
.await;
}
#[tokio::test]
async fn both_subgraph_stages_metric_incremented_for_errored_stages_processing() {
async {
for _ in 0..1 {
let _stage = create_subgraph_stage_for_request_validation_test();
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_client, http_driver) = create_mock_http_client_hard_error();
let _service = _stage.as_service(
http_client,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_service".to_string(),
true,
);
let _request = subgraph::Request::fake_builder().build();
let _response = _service.oneshot(_request).await;
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
for _ in 0..2 {
let _stage = create_subgraph_stage_for_validation_test();
let (subgraph_mock, subgraph_driver) = create_mock_subgraph_service();
let (http_client, http_driver) = create_mock_http_client_hard_error();
let _service = _stage.as_service(
http_client,
subgraph_mock.boxed_clone(),
"http://test".to_string(),
"my_service".to_string(),
true,
);
let _request = subgraph::Request::fake_builder().build();
let _response = _service.oneshot(_request).await;
crate::plugin::test::await_mock_driver(subgraph_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
assert_coprocessor_operations_metrics(&[
(PipelineStep::SubgraphRequest, 1, Some(false)),
(PipelineStep::SubgraphResponse, 2, Some(false)),
]);
}
.with_metrics()
.await;
}
#[tokio::test]
async fn router_request_metric_incremented_when_condition_true() {
async {
for _ in 0..3 {
let router_stage = create_router_stage_for_request_validation_test();
let (http_client, http_driver) =
create_mock_http_client_router_request_valid_response();
let (router_mock, router_driver) = create_mock_router_service();
let service_stack = router_stage
.as_service(
http_client,
router_mock.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
false,
)
.boxed();
let request = router::Request::fake_builder().build().unwrap();
let _ = service_stack.oneshot(request).await.unwrap();
crate::plugin::test::await_mock_driver(router_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
assert_coprocessor_operations_metrics(&[(PipelineStep::RouterRequest, 3, Some(true))]);
}
.with_metrics()
.await;
}
#[tokio::test]
async fn router_response_metric_incremented_when_condition_true() {
async {
for _ in 0..2 {
let router_stage = create_router_stage_for_response_validation_test();
let (http_client, http_driver) =
create_mock_http_client_router_response_valid_response();
let (router_mock, router_driver) = create_mock_router_service();
let service_stack = router_stage
.as_service(
http_client,
router_mock.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
false,
)
.boxed();
let request = router::Request::fake_builder().build().unwrap();
let _ = service_stack.oneshot(request).await.unwrap();
crate::plugin::test::await_mock_driver(router_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
assert_coprocessor_operations_metrics(&[(PipelineStep::RouterResponse, 2, Some(true))]);
}
.with_metrics()
.await;
}
#[tokio::test]
async fn router_request_metric_not_incremented_when_condition_false() {
async {
for _ in 0..2 {
let router_stage = create_router_stage_for_request_with_false_condition();
let (http_mock, http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let (router_mock, router_driver) = create_mock_router_service();
let service_stack = router_stage
.as_service(
http_mock,
router_mock.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
false,
)
.boxed();
let request = router::Request::fake_builder().build().unwrap();
let _ = service_stack.oneshot(request).await.unwrap();
crate::plugin::test::await_mock_driver(router_driver).await;
crate::plugin::test::assert_no_mock_calls(http_handle).await;
}
assert_coprocessor_operations_metrics(&[]);
}
.with_metrics()
.await;
}
#[tokio::test]
async fn router_response_metric_not_incremented_when_condition_false() {
async {
for _ in 0..1 {
let router_stage = create_router_stage_for_response_with_false_condition();
let (http_mock, http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let (router_mock, router_driver) = create_mock_router_service();
let service_stack = router_stage
.as_service(
http_mock,
router_mock.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
false,
)
.boxed();
let request = router::Request::fake_builder().build().unwrap();
let _ = service_stack.oneshot(request).await.unwrap();
crate::plugin::test::await_mock_driver(router_driver).await;
crate::plugin::test::assert_no_mock_calls(http_handle).await;
}
assert_coprocessor_operations_metrics(&[]);
}
.with_metrics()
.await;
}
#[tokio::test]
async fn both_router_stages_metric_incremented_when_conditions_true() {
async {
for _ in 0..2 {
let router_stage = create_router_stage_for_request_validation_test();
let (http_client, http_driver) =
create_mock_http_client_router_request_valid_response();
let (router_mock, router_driver) = create_mock_router_service();
let service_stack = router_stage
.as_service(
http_client,
router_mock.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
false,
)
.boxed();
let request = router::Request::fake_builder().build().unwrap();
let _ = service_stack.oneshot(request).await.unwrap();
crate::plugin::test::await_mock_driver(router_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
for _ in 0..4 {
let router_stage = create_router_stage_for_response_validation_test();
let (http_client, http_driver) =
create_mock_http_client_router_response_valid_response();
let (router_mock, router_driver) = create_mock_router_service();
let service_stack = router_stage
.as_service(
http_client,
router_mock.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
false,
)
.boxed();
let request = router::Request::fake_builder().build().unwrap();
let _ = service_stack.oneshot(request).await.unwrap();
crate::plugin::test::await_mock_driver(router_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
assert_coprocessor_operations_metrics(&[
(PipelineStep::RouterRequest, 2, Some(true)),
(PipelineStep::RouterResponse, 4, Some(true)),
]);
}
.with_metrics()
.await;
}
#[tokio::test]
async fn router_request_metric_incremented_for_errored_stage_processing() {
async {
for _ in 0..2 {
let router_stage = create_router_stage_for_request_validation_test();
let (http_client, http_driver) = create_mock_http_client_hard_error();
let (router_mock, router_driver) = create_mock_router_service();
let service_stack = router_stage
.as_service(
http_client,
router_mock.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
false,
)
.boxed();
let request = router::Request::fake_builder().build().unwrap();
let _ = service_stack.oneshot(request).await;
crate::plugin::test::await_mock_driver(router_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
assert_coprocessor_operations_metrics(&[(PipelineStep::RouterRequest, 2, Some(false))]);
}
.with_metrics()
.await;
}
#[tokio::test]
async fn router_response_metric_incremented_for_errored_stage_processing() {
async {
for _ in 0..4 {
let router_stage = create_router_stage_for_response_validation_test();
let (http_client, http_driver) = create_mock_http_client_hard_error();
let (router_mock, router_driver) = create_mock_router_service();
let service_stack = router_stage
.as_service(
http_client,
router_mock.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
false,
)
.boxed();
let request = router::Request::fake_builder().build().unwrap();
let _ = service_stack.oneshot(request).await;
crate::plugin::test::await_mock_driver(router_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
assert_coprocessor_operations_metrics(&[(
PipelineStep::RouterResponse,
4,
Some(false),
)]);
}
.with_metrics()
.await;
}
#[tokio::test]
async fn router_response_deferred_chunk_metric_incremented_when_on_graphql_error_matches() {
async {
use futures::StreamExt as _;
use crate::graphql::Error as GraphQLError;
use crate::plugins::telemetry::config::AttributeValue;
let router_stage = RouterStage {
request: Default::default(),
response: RouterResponseConf {
condition: Condition::Eq([
SelectorOrValue::Selector(RouterSelector::OnGraphQLError {
on_graphql_error: true,
}),
SelectorOrValue::Value(AttributeValue::Bool(true)),
]),
body: true,
..Default::default()
},
};
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let response_body = json!({
"version": 1,
"stage": "RouterResponse",
"control": "continue",
});
let response = http::Response::builder()
.status(200)
.body(router::body::from_bytes(
serde_json::to_string(&response_body).unwrap(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let (mock_router_service, mut handle_router) =
tower_test::mock::pair::<router::Request, router::Response>();
let router_driver = tokio::spawn(async move {
let (req, responder) = handle_router.next_request().await.unwrap();
let ctx = req.context.clone();
let graphql_chunks = vec![
Response::builder()
.data(serde_json_bytes::json!({"hello": "world"}))
.build(),
Response::builder()
.errors(vec![
GraphQLError::builder().message("deferred error").build(),
])
.build(),
];
let sg_response = supergraph::Response::new_from_response(
http::Response::new(futures::stream::iter(graphql_chunks).boxed()),
ctx.clone(),
);
let bytes_stream = sg_response.response.into_body().map(|graphql_resp| {
Ok::<bytes::Bytes, tower::BoxError>(
serde_json::to_vec(&graphql_resp)
.expect("graphql::Response serializes without error")
.into(),
)
});
let body = router::body::from_result_stream(bytes_stream);
responder.send_response(
router::Response::http_response_builder()
.response(http::Response::new(body))
.context(ctx)
.build()
.unwrap(),
);
});
let service_stack = router_stage
.as_service(
mock_http_client,
mock_router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
false,
)
.boxed();
let request = router::Request::fake_builder().build().unwrap();
let response = service_stack.oneshot(request).await.unwrap();
let _ = router::body::into_bytes(response.response.into_body()).await;
assert_coprocessor_operations_metrics(&[(PipelineStep::RouterResponse, 1, Some(true))]);
crate::plugin::test::await_mock_driver(router_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
.with_metrics()
.await;
}
#[tokio::test]
async fn both_router_stages_metric_incremented_for_errored_stages_processing() {
async {
for _ in 0..3 {
let router_stage = create_router_stage_for_request_validation_test();
let (http_client, http_driver) = create_mock_http_client_hard_error();
let (router_mock, router_driver) = create_mock_router_service();
let service_stack = router_stage
.as_service(
http_client,
router_mock.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
false,
)
.boxed();
let request = router::Request::fake_builder().build().unwrap();
let _ = service_stack.oneshot(request).await;
crate::plugin::test::await_mock_driver(router_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
for _ in 0..2 {
let router_stage = create_router_stage_for_response_validation_test();
let (http_client, http_driver) = create_mock_http_client_hard_error();
let (router_mock, router_driver) = create_mock_router_service();
let service_stack = router_stage
.as_service(
http_client,
router_mock.boxed_clone(),
"http://test".to_string(),
Arc::new("".to_string()),
false,
)
.boxed();
let request = router::Request::fake_builder().build().unwrap();
let _ = service_stack.oneshot(request).await;
crate::plugin::test::await_mock_driver(router_driver).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
assert_coprocessor_operations_metrics(&[
(PipelineStep::RouterRequest, 3, Some(false)),
(PipelineStep::RouterResponse, 2, Some(false)),
]);
}
.with_metrics()
.await;
}
#[cfg(unix)]
#[tokio::test]
async fn load_plugin_with_unix_socket_url() {
let config = serde_json::json!({
"coprocessor": {
"url": "unix:///tmp/coprocessor.sock"
}
});
let _test_harness = crate::TestHarness::builder()
.configuration_json(config)
.unwrap()
.build_router()
.await
.unwrap();
}
#[cfg(unix)]
#[tokio::test]
async fn load_plugin_with_unix_socket_and_h2c_http2only() {
let config = serde_json::json!({
"coprocessor": {
"url": "unix:///tmp/coprocessor.sock",
"client": {
"experimental_http2": "http2only"
}
}
});
let _test_harness = crate::TestHarness::builder()
.configuration_json(config)
.unwrap()
.build_router()
.await
.unwrap();
}
#[cfg(unix)]
#[tokio::test]
async fn load_plugin_with_unix_socket_and_h2c_enable() {
let config = serde_json::json!({
"coprocessor": {
"url": "unix:///tmp/coprocessor.sock",
"client": {
"experimental_http2": "enable"
}
}
});
let _test_harness = crate::TestHarness::builder()
.configuration_json(config)
.unwrap()
.build_router()
.await
.unwrap();
}
#[cfg(unix)]
#[tokio::test]
async fn load_plugin_with_unix_socket_and_h2c_disable() {
let config = serde_json::json!({
"coprocessor": {
"url": "unix:///tmp/coprocessor.sock",
"client": {
"experimental_http2": "disable"
}
}
});
let _test_harness = crate::TestHarness::builder()
.configuration_json(config)
.unwrap()
.build_router()
.await
.unwrap();
}
#[tokio::test]
async fn test_coprocessor_http_url_configuration() {
let config = serde_json::json!({
"coprocessor": {
"url": "http://localhost:8081"
}
});
let _test_harness = crate::TestHarness::builder()
.configuration_json(config)
.unwrap()
.build_router()
.await
.unwrap();
}
#[tokio::test]
async fn test_coprocessor_https_url_configuration() {
let config = serde_json::json!({
"coprocessor": {
"url": "https://example.com:8443/coprocessor"
}
});
let _test_harness = crate::TestHarness::builder()
.configuration_json(config)
.unwrap()
.build_router()
.await
.unwrap();
}
#[test]
fn test_url_scheme_detection() {
let test_cases = vec![
("http://localhost:8081", false),
("https://example.com:443/path", false),
("unix:///tmp/socket.sock", true),
("unix:///var/run/app/coprocessor.sock", true),
("ftp://example.com", false), ];
for (url, should_be_unix) in test_cases {
let is_unix = url.starts_with("unix://");
assert_eq!(
is_unix, should_be_unix,
"URL '{}' unix detection failed",
url
);
}
}
#[tokio::test]
async fn test_backwards_compatibility_with_existing_configs() {
let legacy_http_configs = vec![
serde_json::json!({
"coprocessor": {
"url": "http://coprocessor:8080"
}
}),
serde_json::json!({
"coprocessor": {
"url": "https://external-coprocessor.company.com/graphql",
"timeout": "10s"
}
}),
serde_json::json!({
"coprocessor": {
"url": "http://127.0.0.1:3001/webhook",
"router": {
"request": {
"context": "all",
"headers": true
}
}
}
}),
];
for config in legacy_http_configs {
let test_harness = crate::TestHarness::builder()
.configuration_json(config)
.unwrap()
.build_router()
.await;
assert!(
test_harness.is_ok(),
"Legacy HTTP configuration should load successfully"
);
}
}
#[tokio::test]
async fn test_selective_body_field_configuration() {
let selective_configs = vec![
serde_json::json!({
"coprocessor": {
"url": "http://coprocessor:8080",
"supergraph": {
"response": {
"body": true
}
}
}
}),
serde_json::json!({
"coprocessor": {
"url": "http://coprocessor:8080",
"supergraph": {
"response": {
"body": false
}
}
}
}),
serde_json::json!({
"coprocessor": {
"url": "http://coprocessor:8080",
"supergraph": {
"response": {
"body": {
"data": false,
"errors": true,
"extensions": false
}
}
}
}
}),
serde_json::json!({
"coprocessor": {
"url": "http://coprocessor:8080",
"supergraph": {
"response": {
"body": {
"data": true,
"errors": true,
"extensions": true
}
}
}
}
}),
serde_json::json!({
"coprocessor": {
"url": "http://coprocessor:8080",
"execution": {
"response": {
"body": {
"errors": true
}
}
}
}
}),
];
for config in selective_configs {
let test_harness = crate::TestHarness::builder()
.configuration_json(config.clone())
.unwrap()
.build_router()
.await;
assert!(
test_harness.is_ok(),
"Selective body configuration should load successfully: {:?}",
config
);
}
}
#[tokio::test]
async fn test_empty_unix_socket_path_rejected() {
let config = serde_json::json!({
"coprocessor": {
"url": "unix://"
}
});
let result = crate::TestHarness::builder()
.configuration_json(config)
.unwrap()
.build_router()
.await;
assert!(result.is_err(), "Empty Unix socket path should be rejected");
let err = result.unwrap_err().to_string();
assert!(
err.contains("must include a path"),
"Error should mention missing path: {err}"
);
}
#[tokio::test]
async fn test_relative_unix_socket_path_rejected() {
let config = serde_json::json!({
"coprocessor": {
"url": "unix://relative/path.sock"
}
});
let result = crate::TestHarness::builder()
.configuration_json(config)
.unwrap()
.build_router()
.await;
assert!(
result.is_err(),
"Relative Unix socket path should be rejected"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains("should be absolute"),
"Error should mention absolute path requirement: {err}"
);
}
#[tokio::test]
async fn test_invalid_http_url_rejected() {
let config = serde_json::json!({
"coprocessor": {
"url": "not a valid url"
}
});
let result = crate::TestHarness::builder()
.configuration_json(config)
.unwrap()
.build_router()
.await;
assert!(result.is_err(), "Invalid HTTP URL should be rejected");
}
#[tokio::test]
async fn test_stage_specific_empty_unix_socket_path_rejected() {
let config = serde_json::json!({
"coprocessor": {
"url": "http://localhost:8080",
"router": {
"request": {
"url": "unix://",
"headers": true
}
}
}
});
let result = crate::TestHarness::builder()
.configuration_json(config)
.unwrap()
.build_router()
.await;
assert!(
result.is_err(),
"Empty Unix socket path in stage config should be rejected"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains("router.request.url"),
"Error should mention the specific config path: {err}"
);
}
#[tokio::test]
async fn test_unix_socket_with_valid_path_query_accepted() {
let config = serde_json::json!({
"coprocessor": {
"url": "unix:///tmp/coprocessor.sock?path=/api/v1"
}
});
let result = crate::TestHarness::builder()
.configuration_json(config)
.unwrap()
.build_router()
.await;
assert!(
result.is_ok(),
"Unix socket with ?path= should be accepted, got: {}",
result.unwrap_err()
);
}
#[tokio::test]
async fn test_unix_socket_stage_override_with_valid_path_query_accepted() {
let config = serde_json::json!({
"coprocessor": {
"url": "http://localhost:8080",
"router": {
"request": {
"url": "unix:///tmp/router.sock?path=/hook",
"headers": true
}
}
}
});
let result = crate::TestHarness::builder()
.configuration_json(config)
.unwrap()
.build_router()
.await;
assert!(
result.is_ok(),
"Stage-specific Unix socket with ?path= should be accepted, got: {}",
result.unwrap_err()
);
}
#[rstest::rstest]
#[case::unix_with_path("unix:///tmp/coprocessor.sock?path=/api/v1")]
#[case::unix_without_query("unix:///tmp/coprocessor.sock")]
#[case::unix_unknown_query_param_warns("unix:///tmp/coprocessor.sock?foo=bar")]
#[case::unix_empty_query_string_warns("unix:///tmp/coprocessor.sock?")]
#[case::http_url("http://localhost:8080/path")]
fn test_validate_coprocessor_url_accepted(#[case] url: &str) {
assert!(
crate::plugins::coprocessor::validate_coprocessor_url(url, "coprocessor.url").is_ok(),
"URL should be accepted: {url}"
);
}
#[rstest::rstest]
#[case::empty_path("unix://", "must include a path")]
#[case::relative_path("unix://relative/path.sock", "should be absolute")]
#[case::invalid_http("not a valid url", "invalid URL")]
fn test_validate_coprocessor_url_rejected(#[case] url: &str, #[case] expected_err: &str) {
let result = crate::plugins::coprocessor::validate_coprocessor_url(url, "coprocessor.url");
assert!(result.is_err(), "URL should be rejected: {url}");
assert!(
result.unwrap_err().to_string().contains(expected_err),
"Error for '{url}' should contain '{expected_err}'"
);
}
#[cfg(test)]
mod connector_tests {
use std::str::FromStr;
use std::sync::Arc;
use std::sync::Mutex;
use apollo_compiler::name;
use apollo_federation::connectors::ConnectId;
use apollo_federation::connectors::ConnectSpec;
use apollo_federation::connectors::Connector;
use apollo_federation::connectors::HttpJsonTransport;
use apollo_federation::connectors::JSONSelection;
use apollo_federation::connectors::SourceName;
use apollo_federation::connectors::StringTemplate;
use apollo_federation::connectors::runtime::http_json_transport::HttpRequest as ConnectorsHttpRequest;
use apollo_federation::connectors::runtime::http_json_transport::TransportRequest;
use apollo_federation::connectors::runtime::key::ResponseKey;
use apollo_federation::connectors::runtime::responses::MappedResponse;
use tower::BoxError;
use tower::ServiceExt;
use crate::metrics::FutureMetricsExt;
use crate::plugins::coprocessor::ContextConf;
use crate::plugins::coprocessor::connector::ConnectorRequestConf;
use crate::plugins::coprocessor::connector::ConnectorResponseConf;
use crate::plugins::coprocessor::connector::ConnectorStage;
use crate::plugins::coprocessor::test::assert_coprocessor_operations_metrics;
use crate::plugins::telemetry::config_new::conditions::Condition;
use crate::services::PipelineStep;
use crate::services::connector::request_service;
use crate::services::http::HttpRequest;
use crate::services::http::HttpResponse;
use crate::services::router;
fn create_test_connector() -> Arc<Connector> {
Arc::new(Connector {
id: ConnectId::new(
"subgraph".into(),
Some(SourceName::cast("source")),
name!(Query),
name!(users),
None,
0,
),
transport: Some(HttpJsonTransport {
source_template: None,
connect_template: StringTemplate::from_str("/test").unwrap(),
..Default::default()
}),
selection: JSONSelection::empty(),
config: None,
max_requests: None,
entity_resolver: None,
spec: ConnectSpec::V0_1,
schema_subtypes_map: Default::default(),
batch_settings: None,
request_headers: Default::default(),
response_headers: Default::default(),
request_variable_keys: Default::default(),
response_variable_keys: Default::default(),
error_settings: Default::default(),
label: "label".into(),
})
}
fn create_test_response_key() -> ResponseKey {
ResponseKey::RootField {
name: "hello".to_string(),
inputs: Default::default(),
selection: Arc::new(JSONSelection::parse("$.data").unwrap()),
}
}
fn create_test_connector_request() -> request_service::Request {
let http_request = http::Request::builder()
.uri("http://original-connector-uri/api")
.method(http::Method::POST)
.header("content-type", "application/json")
.body(r#"{"query":"test"}"#.to_string())
.unwrap();
let transport_request = TransportRequest::Http(Box::new(ConnectorsHttpRequest {
inner: http_request,
debug: Default::default(),
}));
request_service::Request {
context: crate::Context::default(),
connector: create_test_connector(),
transport_request,
key: create_test_response_key(),
mapping_problems: vec![],
supergraph_request: Default::default(),
operation: Default::default(),
}
}
#[tokio::test]
async fn should_apply_modified_body_when_coprocessor_returns_new_body() {
let connector_stage = ConnectorStage {
request: ConnectorRequestConf {
headers: true,
body: true,
uri: true,
..Default::default()
},
response: Default::default(),
};
let mock_connector_service = crate::plugin::test::MockConnector::builder()
.with_json(
serde_json::json!(r#"{"modified":"body"}"#),
serde_json::json!("test_result"),
)
.build();
let (mock_http_client, mut http_handle) =
tower_test::mock::pair::<HttpRequest, HttpResponse>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "ConnectorRequest",
"control": "continue",
"body": "{\"modified\":\"body\"}"
}"#,
))
.unwrap();
responder.send_response(HttpResponse {
http_response: response,
context,
});
});
let service = connector_stage.as_service(
mock_http_client,
mock_connector_service.boxed_clone(),
"http://test".to_string(),
"my_connector_source".to_string(),
);
let request = create_test_connector_request();
let response = service.oneshot(request).await.unwrap();
assert!(response.transport_result.is_ok());
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn should_send_json_body_as_parsed_json_to_coprocessor() {
let connector_stage = ConnectorStage {
request: ConnectorRequestConf {
body: true,
..Default::default()
},
response: Default::default(),
};
let mock_connector_service = crate::plugin::test::MockConnector::new(
[(r#"{"query":"test"}"#.to_string(), "ok".to_string())].into(),
);
let (mock_http_client, mut http_handle) =
tower_test::mock::pair::<HttpRequest, HttpResponse>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let body = router::body::into_bytes(req.http_request.into_body())
.await
.unwrap();
let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(
payload["body"].is_object(),
"expected body to be a JSON object, got: {}",
payload["body"]
);
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "ConnectorRequest",
"control": "continue"
}"#,
))
.unwrap();
responder.send_response(HttpResponse {
http_response: response,
context,
});
});
let service = connector_stage.as_service(
mock_http_client,
mock_connector_service.boxed_clone(),
"http://test".to_string(),
"my_connector_source".to_string(),
);
let request = create_test_connector_request();
let response = service.oneshot(request).await.unwrap();
assert!(response.transport_result.is_ok());
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn should_send_non_json_body_as_string_to_coprocessor() {
let connector_stage = ConnectorStage {
request: ConnectorRequestConf {
body: true,
..Default::default()
},
response: Default::default(),
};
let mock_connector_service = crate::plugin::test::MockConnector::new(
[("plain text body".to_string(), "ok".to_string())].into(),
);
let (mock_http_client, mut http_handle) =
tower_test::mock::pair::<HttpRequest, HttpResponse>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let body = router::body::into_bytes(req.http_request.into_body())
.await
.unwrap();
let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(
payload["body"].is_string(),
"expected body to be a JSON string, got: {}",
payload["body"]
);
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "ConnectorRequest",
"control": "continue"
}"#,
))
.unwrap();
responder.send_response(HttpResponse {
http_response: response,
context,
});
});
let service = connector_stage.as_service(
mock_http_client,
mock_connector_service.boxed_clone(),
"http://test".to_string(),
"my_connector_source".to_string(),
);
let http_request = http::Request::builder()
.uri("http://original-connector-uri/api")
.method(http::Method::POST)
.header("content-type", "text/plain")
.body("plain text body".to_string())
.unwrap();
let transport_request = TransportRequest::Http(Box::new(ConnectorsHttpRequest {
inner: http_request,
debug: Default::default(),
}));
let request = request_service::Request {
context: crate::Context::default(),
connector: create_test_connector(),
transport_request,
key: create_test_response_key(),
mapping_problems: vec![],
supergraph_request: Default::default(),
operation: Default::default(),
};
let response = service.oneshot(request).await.unwrap();
assert!(response.transport_result.is_ok());
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn should_return_transport_error_when_coprocessor_breaks() {
let connector_stage = ConnectorStage {
request: ConnectorRequestConf {
body: true,
..Default::default()
},
response: Default::default(),
};
let mock_connector_service =
crate::plugin::test::MockConnector::new(Default::default());
let (mock_http_client, mut http_handle) =
tower_test::mock::pair::<HttpRequest, HttpResponse>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "ConnectorRequest",
"control": { "break": 400 },
"body": "Request blocked by coprocessor"
}"#,
))
.unwrap();
responder.send_response(HttpResponse {
http_response: response,
context,
});
});
let service = connector_stage.as_service(
mock_http_client,
mock_connector_service.boxed_clone(),
"http://test".to_string(),
"my_connector_source".to_string(),
);
let request = create_test_connector_request();
let response = service.oneshot(request).await.unwrap();
assert!(response.transport_result.is_err());
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn should_apply_modified_headers_and_uri_when_coprocessor_returns_them() {
let connector_stage = ConnectorStage {
request: ConnectorRequestConf {
headers: true,
uri: true,
..Default::default()
},
response: Default::default(),
};
let captured_uri = Arc::new(Mutex::new(String::new()));
let captured_headers = Arc::new(Mutex::new(Vec::<(String, String)>::new()));
let captured_uri_clone = captured_uri.clone();
let captured_headers_clone = captured_headers.clone();
let inner_service = tower::service_fn(move |req: request_service::Request| {
let captured_uri = captured_uri_clone.clone();
let captured_headers = captured_headers_clone.clone();
async move {
let TransportRequest::Http(ref http_req) = req.transport_request else {
panic!("expected Http transport request");
};
*captured_uri.lock().unwrap() = http_req.inner.uri().to_string();
*captured_headers.lock().unwrap() = http_req
.inner
.headers()
.iter()
.map(|(k, v)| (k.to_string(), v.to_str().unwrap().to_string()))
.collect();
let response = request_service::Response::test_new(
req.context.clone(),
req.key,
Default::default(),
serde_json_bytes::json!("ok"),
None,
);
Ok(response)
}
});
let (mock_http_client, mut http_handle) =
tower_test::mock::pair::<HttpRequest, HttpResponse>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "ConnectorRequest",
"control": "continue",
"headers": {
"content-type": ["application/json"],
"x-new-header": ["new-value"]
},
"uri": "http://new-connector-uri/api"
}"#,
))
.unwrap();
responder.send_response(HttpResponse {
http_response: response,
context,
});
});
let service = connector_stage.as_service(
mock_http_client,
inner_service.boxed_clone(),
"http://test".to_string(),
"my_connector_source".to_string(),
);
let request = create_test_connector_request();
service.oneshot(request).await.unwrap();
assert_eq!(
*captured_uri.lock().unwrap(),
"http://new-connector-uri/api"
);
assert!(
captured_headers
.lock()
.unwrap()
.contains(&("x-new-header".to_string(), "new-value".to_string()))
);
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn should_update_context_when_coprocessor_returns_context_entries() {
let connector_stage = ConnectorStage {
request: ConnectorRequestConf {
context: ContextConf::All,
body: true,
..Default::default()
},
response: Default::default(),
};
let mock_connector_service = crate::plugin::test::MockConnector::new(
[(r#"{"query":"test"}"#.to_string(), "ok".to_string())].into(),
);
let (mock_http_client, mut http_handle) =
tower_test::mock::pair::<HttpRequest, HttpResponse>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "ConnectorRequest",
"control": "continue",
"context": {
"entries": {
"test-key": "test-value"
}
}
}"#,
))
.unwrap();
responder.send_response(HttpResponse {
http_response: response,
context,
});
});
let service = connector_stage.as_service(
mock_http_client,
mock_connector_service.boxed_clone(),
"http://test".to_string(),
"my_connector_source".to_string(),
);
let request = create_test_connector_request();
let context = request.context.clone();
service.oneshot(request).await.unwrap();
assert_eq!(
context.get_json_value("test-key"),
Some(serde_json_bytes::Value::String("test-value".into()))
);
crate::plugin::test::await_mock_driver(http_driver).await;
}
#[tokio::test]
async fn should_increment_request_metric_when_condition_is_true() {
async {
for _ in 0..2 {
let connector_stage = ConnectorStage {
request: ConnectorRequestConf {
body: true,
..Default::default()
},
response: Default::default(),
};
let mock_connector_service = crate::plugin::test::MockConnector::new(
[(r#"{"query":"test"}"#.to_string(), "ok".to_string())].into(),
);
let (http_client, mut http_handle) =
tower_test::mock::pair::<HttpRequest, HttpResponse>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "ConnectorRequest",
"control": "continue"
}"#,
))
.unwrap();
responder.send_response(HttpResponse {
http_response: response,
context,
});
});
let service = connector_stage.as_service(
http_client,
mock_connector_service.boxed_clone(),
"http://test".to_string(),
"my_connector_source".to_string(),
);
let request = create_test_connector_request();
let _response = service.oneshot(request).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
assert_coprocessor_operations_metrics(&[(
PipelineStep::ConnectorRequest,
2,
Some(true),
)]);
}
.with_metrics()
.await;
}
#[tokio::test]
async fn should_not_increment_request_metric_when_condition_is_false() {
async {
for _ in 0..2 {
let connector_stage = ConnectorStage {
request: ConnectorRequestConf {
condition: Condition::False,
body: true,
..Default::default()
},
response: Default::default(),
};
let mock_connector_service = crate::plugin::test::MockConnector::new(
[(r#"{"query":"test"}"#.to_string(), "ok".to_string())].into(),
);
let (http_mock, http_handle) =
tower_test::mock::pair::<HttpRequest, HttpResponse>();
let service = connector_stage.as_service(
http_mock,
mock_connector_service.boxed_clone(),
"http://test".to_string(),
"my_connector_source".to_string(),
);
let request = create_test_connector_request();
let _response = service.oneshot(request).await;
crate::plugin::test::assert_no_mock_calls(http_handle).await;
}
assert_coprocessor_operations_metrics(&[]);
}
.with_metrics()
.await;
}
#[tokio::test]
async fn should_return_successful_response_when_response_coprocessor_continues() {
let connector_stage = ConnectorStage {
request: Default::default(),
response: ConnectorResponseConf {
headers: true,
status_code: true,
body: true,
..Default::default()
},
};
let mock_connector_service = crate::plugin::test::MockConnector::new(
[(r#"{"query":"test"}"#.to_string(), "ok".to_string())].into(),
);
let (mock_http_client, mut http_handle) =
tower_test::mock::pair::<HttpRequest, HttpResponse>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "ConnectorResponse"
}"#,
))
.unwrap();
responder.send_response(HttpResponse {
http_response: response,
context,
});
});
let service = connector_stage.as_service(
mock_http_client,
mock_connector_service.boxed_clone(),
"http://test".to_string(),
"my_connector_source".to_string(),
);
let request = create_test_connector_request();
let response = service.oneshot(request).await.unwrap();
crate::plugin::test::await_mock_driver(http_driver).await;
assert!(response.transport_result.is_ok());
}
#[tokio::test]
async fn should_increment_response_metric_when_condition_is_true() {
async {
for _ in 0..3 {
let connector_stage = ConnectorStage {
request: Default::default(),
response: ConnectorResponseConf {
body: true,
..Default::default()
},
};
let mock_connector_service = crate::plugin::test::MockConnector::new(
[(r#"{"query":"test"}"#.to_string(), "ok".to_string())].into(),
);
let (mock_http_client, mut http_handle) =
tower_test::mock::pair::<HttpRequest, HttpResponse>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "ConnectorResponse"
}"#,
))
.unwrap();
responder.send_response(HttpResponse {
http_response: response,
context,
});
});
let service = connector_stage.as_service(
mock_http_client,
mock_connector_service.boxed_clone(),
"http://test".to_string(),
"my_connector_source".to_string(),
);
let request = create_test_connector_request();
let _response = service.oneshot(request).await;
crate::plugin::test::await_mock_driver(http_driver).await;
}
assert_coprocessor_operations_metrics(&[(
PipelineStep::ConnectorResponse,
3,
Some(true),
)]);
}
.with_metrics()
.await;
}
#[tokio::test]
async fn should_use_structured_error_when_coprocessor_breaks_with_errors_object() {
let connector_stage = ConnectorStage {
request: ConnectorRequestConf {
body: true,
..Default::default()
},
response: Default::default(),
};
let mock_connector_service =
crate::plugin::test::MockConnector::new(Default::default());
let (mock_http_client, mut http_handle) =
tower_test::mock::pair::<HttpRequest, HttpResponse>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "ConnectorRequest",
"control": { "break": 401 },
"body": {
"errors": [
{
"message": "Not authenticated.",
"extensions": {
"code": "ERR_UNAUTHENTICATED"
}
}
]
}
}"#,
))
.unwrap();
responder.send_response(HttpResponse {
http_response: response,
context,
});
});
let service = connector_stage.as_service(
mock_http_client,
mock_connector_service.boxed_clone(),
"http://test".to_string(),
"my_connector_source".to_string(),
);
let request = create_test_connector_request();
let response = service.oneshot(request).await.unwrap();
crate::plugin::test::await_mock_driver(http_driver).await;
assert!(response.transport_result.is_err());
match &response.mapped_response {
MappedResponse::Error { error, .. } => {
assert_eq!(error.message, "Not authenticated.");
assert_eq!(error.code(), "ERR_UNAUTHENTICATED");
}
_ => panic!("Expected MappedResponse::Error"),
}
}
#[tokio::test]
async fn should_use_string_error_when_coprocessor_breaks_with_string_body() {
let connector_stage = ConnectorStage {
request: ConnectorRequestConf {
body: true,
..Default::default()
},
response: Default::default(),
};
let mock_connector_service =
crate::plugin::test::MockConnector::new(Default::default());
let (mock_http_client, mut http_handle) =
tower_test::mock::pair::<HttpRequest, HttpResponse>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "ConnectorRequest",
"control": { "break": 400 },
"body": "Request blocked"
}"#,
))
.unwrap();
responder.send_response(HttpResponse {
http_response: response,
context,
});
});
let service = connector_stage.as_service(
mock_http_client,
mock_connector_service.boxed_clone(),
"http://test".to_string(),
"my_connector_source".to_string(),
);
let request = create_test_connector_request();
let response = service.oneshot(request).await.unwrap();
crate::plugin::test::await_mock_driver(http_driver).await;
assert!(response.transport_result.is_err());
match &response.mapped_response {
MappedResponse::Error { error, .. } => {
assert_eq!(error.message, "Request blocked");
assert_eq!(error.code(), "ERROR");
}
_ => panic!("Expected MappedResponse::Error"),
}
}
#[tokio::test]
async fn should_pass_extra_extensions_from_structured_error() {
let connector_stage = ConnectorStage {
request: ConnectorRequestConf {
body: true,
..Default::default()
},
response: Default::default(),
};
let mock_connector_service =
crate::plugin::test::MockConnector::new(Default::default());
let (mock_http_client, mut http_handle) =
tower_test::mock::pair::<HttpRequest, HttpResponse>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "ConnectorRequest",
"control": { "break": 429 },
"body": {
"errors": [
{
"message": "Rate limited",
"extensions": {
"code": "RATE_LIMITED",
"retryAfter": 30
}
}
]
}
}"#,
))
.unwrap();
responder.send_response(HttpResponse {
http_response: response,
context,
});
});
let service = connector_stage.as_service(
mock_http_client,
mock_connector_service.boxed_clone(),
"http://test".to_string(),
"my_connector_source".to_string(),
);
let request = create_test_connector_request();
let response = service.oneshot(request).await.unwrap();
crate::plugin::test::await_mock_driver(http_driver).await;
assert!(response.transport_result.is_err());
match &response.mapped_response {
MappedResponse::Error { error, .. } => {
assert_eq!(error.message, "Rate limited");
assert_eq!(error.code(), "RATE_LIMITED");
assert_eq!(
error.extensions.get("retryAfter"),
Some(&serde_json_bytes::Value::Number(30.into()))
);
}
_ => panic!("Expected MappedResponse::Error"),
}
}
#[tokio::test]
async fn should_send_context_and_id_in_response_stage() {
let connector_stage = ConnectorStage {
request: Default::default(),
response: ConnectorResponseConf {
context: ContextConf::All,
body: true,
..Default::default()
},
};
let mock_connector_service = crate::plugin::test::MockConnector::new(
[(r#"{"query":"test"}"#.to_string(), "ok".to_string())].into(),
);
let (mock_http_client, mut http_handle) =
tower_test::mock::pair::<HttpRequest, HttpResponse>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let body = router::body::into_bytes(req.http_request.into_body())
.await
.unwrap();
let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(
!payload["id"].as_str().unwrap_or("").is_empty(),
"id should not be empty in response stage"
);
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "ConnectorResponse"
}"#,
))
.unwrap();
responder.send_response(HttpResponse {
http_response: response,
context,
});
});
let service = connector_stage.as_service(
mock_http_client,
mock_connector_service.boxed_clone(),
"http://test".to_string(),
"my_connector_source".to_string(),
);
let request = create_test_connector_request();
let response = service.oneshot(request).await.unwrap();
crate::plugin::test::await_mock_driver(http_driver).await;
assert!(response.transport_result.is_ok());
}
#[tokio::test]
async fn should_update_context_in_response_stage() {
let connector_stage = ConnectorStage {
request: Default::default(),
response: ConnectorResponseConf {
context: ContextConf::All,
body: true,
..Default::default()
},
};
let mock_connector_service = crate::plugin::test::MockConnector::new(
[(r#"{"query":"test"}"#.to_string(), "ok".to_string())].into(),
);
let (mock_http_client, mut http_handle) =
tower_test::mock::pair::<HttpRequest, HttpResponse>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "ConnectorResponse",
"context": {
"entries": {
"response-key": "response-value"
}
}
}"#,
))
.unwrap();
responder.send_response(HttpResponse {
http_response: response,
context,
});
});
let service = connector_stage.as_service(
mock_http_client,
mock_connector_service.boxed_clone(),
"http://test".to_string(),
"my_connector_source".to_string(),
);
let request = create_test_connector_request();
let context = request.context.clone();
service.oneshot(request).await.unwrap();
crate::plugin::test::await_mock_driver(http_driver).await;
assert_eq!(
context.get_json_value("response-key"),
Some(serde_json_bytes::Value::String("response-value".into()))
);
}
#[tokio::test]
async fn should_apply_body_modification_for_data_response() {
let connector_stage = ConnectorStage {
request: Default::default(),
response: ConnectorResponseConf {
body: true,
..Default::default()
},
};
let mock_connector_service = crate::plugin::test::MockConnector::new(
[(r#"{"query":"test"}"#.to_string(), "ok".to_string())].into(),
);
let (mock_http_client, mut http_handle) =
tower_test::mock::pair::<HttpRequest, HttpResponse>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "ConnectorResponse",
"body": {"modified": "data"}
}"#,
))
.unwrap();
responder.send_response(HttpResponse {
http_response: response,
context,
});
});
let service = connector_stage.as_service(
mock_http_client,
mock_connector_service.boxed_clone(),
"http://test".to_string(),
"my_connector_source".to_string(),
);
let request = create_test_connector_request();
let response = service.oneshot(request).await.unwrap();
crate::plugin::test::await_mock_driver(http_driver).await;
match &response.mapped_response {
MappedResponse::Data { data, .. } => {
assert_eq!(data, &serde_json_bytes::json!({"modified": "data"}));
}
_ => panic!("Expected MappedResponse::Data"),
}
}
fn create_error_connector_service() -> tower::util::BoxCloneService<
request_service::Request,
request_service::Response,
BoxError,
> {
tower::service_fn(|req: request_service::Request| async move {
let subgraph_name = req.connector.id.subgraph_name.to_string();
Ok(request_service::Response {
context: req.context,
subgraph_name,
transport_result: Err(
apollo_federation::connectors::runtime::errors::Error::TransportFailure(
"original error".to_string(),
),
),
mapped_response: MappedResponse::Error {
error: apollo_federation::connectors::runtime::errors::RuntimeError::new(
"Original error message",
&create_test_response_key(),
),
key: create_test_response_key(),
problems: Vec::new(),
},
})
})
.boxed_clone()
}
#[tokio::test]
async fn should_apply_error_message_modification_for_error_response() {
let connector_stage = ConnectorStage {
request: Default::default(),
response: ConnectorResponseConf {
body: true,
..Default::default()
},
};
let (mock_http_client, mut http_handle) =
tower_test::mock::pair::<HttpRequest, HttpResponse>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "ConnectorResponse",
"body": {
"errors": [{"message": "Modified error message"}]
}
}"#,
))
.unwrap();
responder.send_response(HttpResponse {
http_response: response,
context,
});
});
let service = connector_stage.as_service(
mock_http_client,
create_error_connector_service(),
"http://test".to_string(),
"my_connector_source".to_string(),
);
let request = create_test_connector_request();
let response = service.oneshot(request).await.unwrap();
crate::plugin::test::await_mock_driver(http_driver).await;
match &response.mapped_response {
MappedResponse::Error { error, .. } => {
assert_eq!(error.message, "Modified error message");
}
_ => panic!("Expected MappedResponse::Error"),
}
}
#[tokio::test]
async fn should_apply_error_code_modification_for_error_response() {
let connector_stage = ConnectorStage {
request: Default::default(),
response: ConnectorResponseConf {
body: true,
..Default::default()
},
};
let (mock_http_client, mut http_handle) =
tower_test::mock::pair::<HttpRequest, HttpResponse>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let response = http::Response::builder()
.body(router::body::from_bytes(
r#"{
"version": 1,
"stage": "ConnectorResponse",
"body": {
"errors": [{
"message": "Not authorized",
"extensions": {
"code": "ERR_UNAUTHORIZED"
}
}]
}
}"#,
))
.unwrap();
responder.send_response(HttpResponse {
http_response: response,
context,
});
});
let service = connector_stage.as_service(
mock_http_client,
create_error_connector_service(),
"http://test".to_string(),
"my_connector_source".to_string(),
);
let request = create_test_connector_request();
let response = service.oneshot(request).await.unwrap();
crate::plugin::test::await_mock_driver(http_driver).await;
match &response.mapped_response {
MappedResponse::Error { error, .. } => {
assert_eq!(error.code(), "ERR_UNAUTHORIZED");
}
_ => panic!("Expected MappedResponse::Error"),
}
}
}
mod header_masking_tests {
use super::*;
#[tokio::test]
async fn router_request_headers_sent_unmasked_to_coprocessor() {
let router_stage = RouterStage {
request: RouterRequestConf {
condition: Default::default(),
headers: true,
context: ContextConf::All,
body: false,
sdl: false,
path: false,
method: false,
url: None,
},
response: Default::default(),
};
let (router_mock, mut router_handle) =
tower_test::mock::pair::<router::Request, router::Response>();
let router_driver = tokio::spawn(async move {
let (req, responder) = router_handle.next_request().await.unwrap();
responder.send_response(
router::Response::builder()
.data(json!({ "test": 1234_u32 }))
.context(req.context)
.build()
.unwrap(),
);
});
let mock_router_service = router_mock;
let received_headers = Arc::new(std::sync::Mutex::new(None));
let received_headers_clone = received_headers.clone();
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let body_bytes = router::body::into_bytes(req.http_request.into_body())
.await
.unwrap();
let body: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
if let Some(headers) = body.get("headers") {
*received_headers_clone.lock().unwrap() = Some(headers.clone());
}
let response = http::Response::builder()
.body(router::body::from_bytes(
serde_json::json!({
"version": 1,
"stage": "RouterRequest",
"control": "continue",
})
.to_string(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let inner = Arc::new(
crate::services::header_masking::HeaderMaskingRules::from_config(
&crate::configuration::header_masking_config::HeaderMaskingConfig {
enabled: true,
sensitive_headers: vec!["authorization".to_string(), "cookie".to_string()],
replace_defaults: false,
},
),
);
let masking_rules =
Arc::new(crate::services::header_masking::MaskingRulesMap::new_test(
inner,
Default::default(),
));
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("schema".to_string()),
false,
);
let request = router::Request::fake_builder()
.header("authorization", "Bearer secret-token-12345") .header("cookie", "session=my-session-id")
.header("user-agent", "test-agent")
.build()
.unwrap();
request
.context
.extensions()
.with_lock(|lock| lock.insert(masking_rules));
let _response = service.oneshot(request).await.unwrap();
crate::plugin::test::await_mock_driver(http_driver).await;
{
let headers = received_headers.lock().unwrap();
assert!(headers.is_some(), "Headers should be sent to coprocessor");
let headers_obj = headers.as_ref().unwrap().as_object().unwrap();
assert_eq!(
headers_obj
.get("authorization")
.unwrap()
.as_array()
.unwrap()[0]
.as_str()
.unwrap(),
"Bearer secret-token-12345", "Authorization header should be sent unmasked to coprocessor"
);
assert_eq!(
headers_obj.get("cookie").unwrap().as_array().unwrap()[0]
.as_str()
.unwrap(),
"session=my-session-id",
"Cookie header should be sent unmasked to coprocessor"
);
assert_eq!(
headers_obj.get("user-agent").unwrap().as_array().unwrap()[0]
.as_str()
.unwrap(),
"test-agent"
);
}
crate::plugin::test::await_mock_driver(router_driver).await;
}
#[tokio::test]
async fn subgraph_request_headers_sent_unmasked_to_coprocessor() {
let subgraph_stage = SubgraphStage {
request: SubgraphRequestConf {
condition: Default::default(),
headers: true,
context: ContextConf::All,
body: false,
uri: false,
method: false,
service_name: false,
subgraph_request_id: false,
url: None,
},
response: Default::default(),
};
let (mock_subgraph_service, mut handle_subgraph) =
tower_test::mock::pair::<subgraph::Request, subgraph::Response>();
let subgraph_driver = tokio::spawn(async move {
let (req, responder) = handle_subgraph.next_request().await.unwrap();
responder.send_response(
subgraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.errors(Vec::new())
.extensions(Object::new())
.context(req.context)
.subgraph_name("test_subgraph".to_string())
.build(),
);
});
let received_headers = Arc::new(std::sync::Mutex::new(None));
let received_headers_clone = received_headers.clone();
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let body_bytes = router::body::into_bytes(req.http_request.into_body())
.await
.unwrap();
let body: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
if let Some(headers) = body.get("headers") {
*received_headers_clone.lock().unwrap() = Some(headers.clone());
}
let response = http::Response::builder()
.body(router::body::from_bytes(
serde_json::json!({
"version": 1,
"stage": "SubgraphRequest",
"control": "continue",
})
.to_string(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let inner = Arc::new(
crate::services::header_masking::HeaderMaskingRules::from_config(
&crate::configuration::header_masking_config::HeaderMaskingConfig {
enabled: true,
sensitive_headers: vec!["x-api-key".to_string()],
replace_defaults: false,
},
),
);
let masking_rules =
Arc::new(crate::services::header_masking::MaskingRulesMap::new_test(
inner,
Default::default(),
));
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed_clone(),
"http://test".to_string(),
"test_subgraph".to_string(),
false,
);
let request = subgraph::Request::fake_builder()
.subgraph_request(
http::Request::builder()
.header("x-api-key", "secret-api-key-67890") .header("content-type", "application/json")
.body(graphql::Request::fake_builder().query("{ test }").build())
.unwrap(),
)
.build();
request
.context
.extensions()
.with_lock(|lock| lock.insert(masking_rules));
let _response = service.oneshot(request).await.unwrap();
crate::plugin::test::await_mock_driver(http_driver).await;
crate::plugin::test::await_mock_driver(subgraph_driver).await;
{
let headers = received_headers.lock().unwrap();
assert!(headers.is_some());
let headers_obj = headers.as_ref().unwrap().as_object().unwrap();
assert_eq!(
headers_obj.get("x-api-key").unwrap().as_array().unwrap()[0]
.as_str()
.unwrap(),
"secret-api-key-67890", "API key should be sent unmasked to coprocessor"
);
}
}
#[tokio::test]
async fn headers_sent_when_masking_disabled() {
let router_stage = RouterStage {
request: RouterRequestConf {
condition: Default::default(),
headers: true,
context: ContextConf::All,
body: false,
sdl: false,
path: false,
method: false,
url: None,
},
response: Default::default(),
};
let (router_mock, mut router_handle) =
tower_test::mock::pair::<router::Request, router::Response>();
let router_driver = tokio::spawn(async move {
let (req, responder) = router_handle.next_request().await.unwrap();
responder.send_response(
router::Response::builder()
.data(json!({ "test": 1234_u32 }))
.context(req.context)
.build()
.unwrap(),
);
});
let mock_router_service = router_mock;
let received_headers = Arc::new(std::sync::Mutex::new(None));
let received_headers_clone = received_headers.clone();
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let body_bytes = router::body::into_bytes(req.http_request.into_body())
.await
.unwrap();
let body: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
if let Some(headers) = body.get("headers") {
*received_headers_clone.lock().unwrap() = Some(headers.clone());
}
let response = http::Response::builder()
.body(router::body::from_bytes(
serde_json::json!({
"version": 1,
"stage": "RouterRequest",
"control": "continue",
})
.to_string(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("schema".to_string()),
false,
);
let request = router::Request::fake_builder()
.header("authorization", "Bearer token") .build()
.unwrap();
let _response = service.oneshot(request).await.unwrap();
crate::plugin::test::await_mock_driver(http_driver).await;
{
let headers = received_headers.lock().unwrap();
assert!(headers.is_some());
let headers_obj = headers.as_ref().unwrap().as_object().unwrap();
assert_eq!(
headers_obj
.get("authorization")
.unwrap()
.as_array()
.unwrap()[0]
.as_str()
.unwrap(),
"Bearer token"
);
}
crate::plugin::test::await_mock_driver(router_driver).await;
}
#[tokio::test]
async fn multiple_headers_selective_masking() {
let router_stage = RouterStage {
request: RouterRequestConf {
condition: Default::default(),
headers: true,
context: ContextConf::All,
body: false,
sdl: false,
path: false,
method: false,
url: None,
},
response: Default::default(),
};
let (router_mock, mut router_handle) =
tower_test::mock::pair::<router::Request, router::Response>();
let router_driver = tokio::spawn(async move {
let (req, responder) = router_handle.next_request().await.unwrap();
responder.send_response(
router::Response::builder()
.data(json!({ "test": 1234_u32 }))
.context(req.context)
.build()
.unwrap(),
);
});
let mock_router_service = router_mock;
let received_headers = Arc::new(std::sync::Mutex::new(None));
let received_headers_clone = received_headers.clone();
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let body_bytes = router::body::into_bytes(req.http_request.into_body())
.await
.unwrap();
let body: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
if let Some(headers) = body.get("headers") {
*received_headers_clone.lock().unwrap() = Some(headers.clone());
}
let response = http::Response::builder()
.body(router::body::from_bytes(
serde_json::json!({
"version": 1,
"stage": "RouterRequest",
"control": "continue",
})
.to_string(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let inner = Arc::new(
crate::services::header_masking::HeaderMaskingRules::from_config(
&crate::configuration::header_masking_config::HeaderMaskingConfig {
enabled: true,
sensitive_headers: vec!["authorization".to_string(), "cookie".to_string()],
replace_defaults: false,
},
),
);
let masking_rules =
Arc::new(crate::services::header_masking::MaskingRulesMap::new_test(
inner,
Default::default(),
));
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("schema".to_string()),
false,
);
let request = router::Request::fake_builder()
.header("authorization", "Bearer secret") .header("cookie", "session=secret") .header("user-agent", "test-agent")
.header("x-custom-header", "custom-value")
.build()
.unwrap();
request
.context
.extensions()
.with_lock(|lock| lock.insert(masking_rules));
let _response = service.oneshot(request).await.unwrap();
crate::plugin::test::await_mock_driver(http_driver).await;
{
let headers = received_headers.lock().unwrap();
let headers_obj = headers.as_ref().unwrap().as_object().unwrap();
assert_eq!(
headers_obj
.get("authorization")
.unwrap()
.as_array()
.unwrap()[0]
.as_str()
.unwrap(),
"Bearer secret" );
assert_eq!(
headers_obj.get("cookie").unwrap().as_array().unwrap()[0]
.as_str()
.unwrap(),
"session=secret" );
assert_eq!(
headers_obj.get("user-agent").unwrap().as_array().unwrap()[0]
.as_str()
.unwrap(),
"test-agent"
);
assert_eq!(
headers_obj
.get("x-custom-header")
.unwrap()
.as_array()
.unwrap()[0]
.as_str()
.unwrap(),
"custom-value"
);
}
crate::plugin::test::await_mock_driver(router_driver).await;
}
#[tokio::test]
async fn custom_sensitive_headers_list() {
let router_stage = RouterStage {
request: RouterRequestConf {
condition: Default::default(),
headers: true,
context: ContextConf::All,
body: false,
sdl: false,
path: false,
method: false,
url: None,
},
response: Default::default(),
};
let (router_mock, mut router_handle) =
tower_test::mock::pair::<router::Request, router::Response>();
let router_driver = tokio::spawn(async move {
let (req, responder) = router_handle.next_request().await.unwrap();
responder.send_response(
router::Response::builder()
.data(json!({ "test": 1234_u32 }))
.context(req.context)
.build()
.unwrap(),
);
});
let mock_router_service = router_mock;
let received_headers = Arc::new(std::sync::Mutex::new(None));
let received_headers_clone = received_headers.clone();
let (mock_http_client, mut http_handle) = tower_test::mock::pair::<
crate::services::http::HttpRequest,
crate::services::http::HttpResponse,
>();
let http_driver = tokio::spawn(async move {
let (req, responder) = http_handle.next_request().await.unwrap();
let context = req.context.clone();
let body_bytes = router::body::into_bytes(req.http_request.into_body())
.await
.unwrap();
let body: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
if let Some(headers) = body.get("headers") {
*received_headers_clone.lock().unwrap() = Some(headers.clone());
}
let response = http::Response::builder()
.body(router::body::from_bytes(
serde_json::json!({
"version": 1,
"stage": "RouterRequest",
"control": "continue",
})
.to_string(),
))
.unwrap();
responder.send_response(crate::services::http::HttpResponse {
http_response: response,
context,
});
});
let inner = Arc::new(
crate::services::header_masking::HeaderMaskingRules::from_config(
&crate::configuration::header_masking_config::HeaderMaskingConfig {
enabled: true,
sensitive_headers: vec![
"x-internal-token".to_string(),
"x-secret-key".to_string(),
],
replace_defaults: false,
},
),
);
let masking_rules =
Arc::new(crate::services::header_masking::MaskingRulesMap::new_test(
inner,
Default::default(),
));
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed_clone(),
"http://test".to_string(),
Arc::new("schema".to_string()),
false,
);
let request = router::Request::fake_builder()
.header("x-internal-token", "internal-secret") .header("x-secret-key", "secret-key-value") .header("authorization", "Bearer public-token") .build()
.unwrap();
request
.context
.extensions()
.with_lock(|lock| lock.insert(masking_rules));
let _response = service.oneshot(request).await.unwrap();
crate::plugin::test::await_mock_driver(http_driver).await;
{
let headers = received_headers.lock().unwrap();
let headers_obj = headers.as_ref().unwrap().as_object().unwrap();
assert_eq!(
headers_obj
.get("x-internal-token")
.unwrap()
.as_array()
.unwrap()[0]
.as_str()
.unwrap(),
"internal-secret" );
assert_eq!(
headers_obj.get("x-secret-key").unwrap().as_array().unwrap()[0]
.as_str()
.unwrap(),
"secret-key-value" );
assert_eq!(
headers_obj
.get("authorization")
.unwrap()
.as_array()
.unwrap()[0]
.as_str()
.unwrap(),
"Bearer public-token"
);
}
crate::plugin::test::await_mock_driver(router_driver).await;
}
}
}