#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::sync::Arc;
use futures::future::BoxFuture;
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 router::body::RouterBody;
use serde_json_bytes::json;
use services::subgraph::SubgraphRequestId;
use tower::BoxError;
use tower::ServiceExt;
use super::super::*;
use crate::json_ext::Object;
use crate::json_ext::Value;
use crate::plugin::test::MockInternalHttpClientService;
use crate::plugin::test::MockRouterService;
use crate::plugin::test::MockSubgraphService;
use crate::plugin::test::MockSupergraphService;
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::was_incoming_payload_valid;
use crate::plugins::telemetry::config_new::conditions::SelectorOrValue;
use crate::services::external::EXTERNALIZABLE_VERSION;
use crate::services::external::Externalizable;
use crate::services::external::PipelineStep;
use crate::services::router::body::get_body_bytes;
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 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)
.unwrap()
.build_router()
.await
.is_err()
);
}
#[tokio::test]
async fn external_plugin_with_stages_wont_load_without_graph_ref() {
let config = serde_json::json!({
"coprocessor": {
"url": "http://127.0.0.1:8081",
"stages": {
"subgraph": {
"request": {
"uri": true
}
}
},
}
});
assert!(
crate::TestHarness::builder()
.configuration_json(config)
.unwrap()
.build_router()
.await
.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: true,
body: true,
sdl: true,
path: false,
method: false,
},
response: Default::default(),
};
let mock_router_service = MockRouterService::new();
let mock_http_client = mock_with_callback(move |_: http::Request<RouterBody>| {
Box::pin(async {
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"
});
Ok(http::Response::builder()
.body(RouterBody::from(serde_json::to_string(&input).unwrap()))
.unwrap())
})
});
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed(),
"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()
);
}
#[tokio::test]
async fn coprocessor_returning_the_wrong_stage_should_fail() {
let router_stage = RouterStage {
request: RouterRequestConf {
condition: Default::default(),
headers: true,
context: true,
body: true,
sdl: true,
path: false,
method: false,
},
response: Default::default(),
};
let mock_router_service = MockRouterService::new();
let mock_http_client = mock_with_callback(move |_: http::Request<RouterBody>| {
Box::pin(async {
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"
});
Ok(http::Response::builder()
.body(RouterBody::from(serde_json::to_string(&input).unwrap()))
.unwrap())
})
});
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed(),
"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()
);
}
#[tokio::test]
async fn coprocessor_missing_request_control_should_fail() {
let router_stage = RouterStage {
request: RouterRequestConf {
condition: Default::default(),
headers: true,
context: true,
body: true,
sdl: true,
path: false,
method: false,
},
response: Default::default(),
};
let mock_router_service = MockRouterService::new();
let mock_http_client = mock_with_callback(move |_: http::Request<RouterBody>| {
Box::pin(async {
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"
});
Ok(http::Response::builder()
.body(RouterBody::from(serde_json::to_string(&input).unwrap()))
.unwrap())
})
});
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed(),
"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()
);
}
#[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 = MockSubgraphService::new();
let mock_http_client = mock_with_callback(move |_: http::Request<RouterBody>| {
Box::pin(async {
Ok(http::Response::builder()
.body(RouterBody::from(
r#"{
"version": 1,
"stage": "SubgraphRequest",
"control": {
"break": 200
},
"id": "3a67e2dd75e8777804e4a8f42b971df7",
"body": {
"errors": [{
"body": "Errors need a message, this will fail to deserialize"
}]
}
}"#,
))
.unwrap())
})
});
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed(),
"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()
);
}
#[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 mut mock_subgraph_service = MockSubgraphService::new();
mock_subgraph_service
.expect_call()
.returning(|req: subgraph::Request| {
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");
Ok(subgraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.errors(Vec::new())
.extensions(Object::new())
.context(req.context)
.id(req.id)
.build())
});
let mock_http_client = mock_with_callback(move |req: http::Request<RouterBody>| {
Box::pin(async {
let deserialized_request: Externalizable<Value> =
serde_json::from_slice(&hyper::body::to_bytes(req.into_body()).await.unwrap())
.unwrap();
assert_eq!(
deserialized_request.subgraph_request_id.as_deref(),
Some("5678")
);
Ok(http::Response::builder()
.body(RouterBody::from(
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())
})
});
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed(),
"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()
);
}
#[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()),
])
.into(),
body: true,
..Default::default()
},
response: Default::default(),
};
let mut mock_subgraph_service = MockSubgraphService::new();
mock_subgraph_service
.expect_call()
.returning(|req: subgraph::Request| {
assert_eq!("/", req.subgraph_request.uri().to_string());
Ok(subgraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.errors(Vec::new())
.extensions(Object::new())
.context(req.context)
.build())
});
let mock_http_client = mock_with_callback(move |_: http::Request<RouterBody>| {
Box::pin(async {
Ok(http::Response::builder()
.body(RouterBody::from(
r#"{
"version": 1,
"stage": "SubgraphRequest",
"control": "continue",
"body": {
"query": "query Long {\n me {\n name\n}\n}"
},
"context": {
},
"serviceName": "service name shouldn't change",
"uri": "http://thisurihaschanged"
}"#,
))
.unwrap())
})
});
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed(),
"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()
);
}
#[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 = MockSubgraphService::new();
let mock_http_client = mock_with_callback(move |_: http::Request<RouterBody>| {
Box::pin(async {
Ok(http::Response::builder()
.body(RouterBody::from(
r#"{
"version": 1,
"stage": "SubgraphRequest",
"control": {
"break": 200
},
"body": {
"errors": [{ "message": "my error message" }]
},
"context": {
"entries": {
"testKey": true
}
},
"headers": {
"aheader": ["a value"]
}
}"#,
))
.unwrap())
})
});
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed(),
"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()
);
}
#[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 = MockSubgraphService::new();
let mock_http_client = mock_with_callback(move |_: http::Request<RouterBody>| {
Box::pin(async {
Ok(http::Response::builder()
.body(RouterBody::from(
r#"{
"version": 1,
"stage": "SubgraphRequest",
"control": {
"break": 200
},
"body": "my error message"
}"#,
))
.unwrap())
})
});
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed(),
"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_eq!(
actual_response,
serde_json_bytes::from_value(json!({
"errors": [{
"message": "my error message",
"extensions": {
"code": "ERROR"
}
}]
}))
.unwrap(),
);
}
#[tokio::test]
async fn external_plugin_subgraph_response() {
let subgraph_stage = SubgraphStage {
request: Default::default(),
response: SubgraphResponseConf {
condition: Default::default(),
body: true,
subgraph_request_id: true,
..Default::default()
},
};
let mut mock_subgraph_service = MockSubgraphService::new();
mock_subgraph_service
.expect_call()
.returning(|req: subgraph::Request| {
assert_eq!(&*req.id, "5678");
Ok(subgraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.errors(Vec::new())
.extensions(Object::new())
.context(req.context)
.id(req.id)
.build())
});
let mock_http_client = mock_with_callback(move |r: http::Request<RouterBody>| {
Box::pin(async move {
let (_, body) = r.into_parts();
let body: Value = serde_json::from_slice(&body.to_bytes().await.unwrap()).unwrap();
let subgraph_id = body.get("subgraphRequestId").unwrap();
assert_eq!(subgraph_id.as_str(), Some("5678"));
Ok(http::Response::builder()
.body(RouterBody::from(
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())
})
});
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed(),
"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()
);
}
#[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"),
redact: None,
default: None,
})
.into(),
body: true,
..Default::default()
},
};
let mut mock_subgraph_service = MockSubgraphService::new();
mock_subgraph_service
.expect_call()
.returning(|req: subgraph::Request| {
req.context
.insert("context_value", "content".to_string())
.unwrap();
Ok(subgraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.errors(Vec::new())
.extensions(Object::new())
.context(req.context)
.build())
});
let mock_http_client = mock_with_callback(move |_: http::Request<RouterBody>| {
Box::pin(async {
Ok(http::Response::builder()
.body(RouterBody::from(
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())
})
});
let service = subgraph_stage.as_service(
mock_http_client,
mock_subgraph_service.boxed(),
"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()
);
}
#[tokio::test]
async fn external_plugin_supergraph_response() {
let supergraph_stage = SupergraphStage {
request: Default::default(),
response: SupergraphResponseConf {
condition: Default::default(),
headers: false,
context: false,
body: true,
status_code: false,
sdl: false,
},
};
let mut mock_supergraph_service = MockSupergraphService::new();
mock_supergraph_service
.expect_call()
.returning(|req: supergraph::Request| {
Ok(supergraph::Response::new_from_graphql_response(
graphql::Response::builder()
.data(Value::Null)
.subscribed(true)
.build(),
req.context,
))
});
let mock_http_client = mock_with_deferred_callback(move |_: http::Request<RouterBody>| {
Box::pin(async {
Ok(http::Response::builder()
.body(RouterBody::from(
r#"{
"version": 1,
"stage": "SupergraphResponse",
"body": {
"data": null
}
}"#,
))
.unwrap())
})
});
let service = supergraph_stage.as_service(
mock_http_client,
mock_supergraph_service.boxed(),
"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));
}
#[tokio::test]
async fn external_plugin_router_request() {
let router_stage = RouterStage {
request: RouterRequestConf {
condition: Default::default(),
headers: true,
context: true,
body: true,
sdl: true,
path: true,
method: true,
},
response: Default::default(),
};
let mock_router_service = router::service::from_supergraph_mock_callback(move |req| {
assert_eq!(
req.supergraph_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!(
"query Long {\n me {\n name\n}\n}",
req.supergraph_request.into_body().query.unwrap()
);
Ok(supergraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.context(req.context)
.build()
.unwrap())
})
.await;
let mock_http_client = mock_with_callback(move |req: http::Request<RouterBody>| {
Box::pin(async {
let deserialized_request: Externalizable<Value> =
serde_json::from_slice(&hyper::body::to_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"
});
Ok(http::Response::builder()
.body(RouterBody::from(serde_json::to_string(&input).unwrap()))
.unwrap())
})
});
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed(),
"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();
}
#[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: true,
body: true,
sdl: true,
path: true,
method: true,
},
response: Default::default(),
};
let mock_router_service = router::service::from_supergraph_mock_callback(move |req| {
assert!(
req.context
.get::<&str, u8>("this-is-a-test-context")
.ok()
.flatten()
.is_none()
);
Ok(supergraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.context(req.context)
.build()
.unwrap())
})
.await;
let mock_http_client = mock_with_callback(move |req: http::Request<RouterBody>| {
Box::pin(async {
let deserialized_request: Externalizable<Value> =
serde_json::from_slice(&hyper::body::to_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"
});
Ok(http::Response::builder()
.body(RouterBody::from(serde_json::to_string(&input).unwrap()))
.unwrap())
})
});
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed(),
"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();
}
#[tokio::test]
async fn external_plugin_router_request_http_get() {
let router_stage = RouterStage {
request: RouterRequestConf {
condition: Default::default(),
headers: true,
context: true,
body: true,
sdl: true,
path: true,
method: true,
},
response: Default::default(),
};
let mock_router_service = router::service::from_supergraph_mock_callback(move |req| {
assert_eq!(
req.supergraph_request.headers().get("cookie").unwrap(),
"tasty_cookie=strawberry"
);
assert_eq!(req.supergraph_request.method(), Method::GET);
assert_eq!(req.supergraph_request.uri(), "/");
assert_eq!(
req.context
.get::<&str, u8>("this-is-a-test-context")
.unwrap()
.unwrap(),
42
);
assert_eq!(
"query Long {\n me {\n name\n}\n}",
req.supergraph_request.into_body().query.unwrap()
);
Ok(supergraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.context(req.context)
.build()
.unwrap())
})
.await;
let mock_http_client = mock_with_callback(move |req: http::Request<RouterBody>| {
Box::pin(async {
let deserialized_request: Externalizable<Value> =
serde_json::from_slice(&hyper::body::to_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"
});
Ok(http::Response::builder()
.body(RouterBody::from(serde_json::to_string(&input).unwrap()))
.unwrap())
})
});
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed(),
"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();
}
#[tokio::test]
async fn external_plugin_router_request_controlflow_break() {
let router_stage = RouterStage {
request: RouterRequestConf {
condition: Default::default(),
headers: true,
context: true,
body: true,
sdl: true,
path: true,
method: true,
},
response: Default::default(),
};
let mock_router_service = MockRouterService::new();
let mock_http_client = mock_with_callback(move |req: http::Request<RouterBody>| {
Box::pin(async {
let deserialized_request: Externalizable<Value> =
serde_json::from_slice(&get_body_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"]
}
}
);
Ok(http::Response::builder()
.body(RouterBody::from(serde_json::to_string(&input).unwrap()))
.unwrap())
})
});
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed(),
"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>(
&hyper::body::to_bytes(response.into_body()).await.unwrap(),
)
.unwrap();
assert_eq!(
json!({
"errors": [{
"message": "my error message"
}]
}),
actual_response
);
}
#[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: true,
body: true,
sdl: true,
path: true,
method: true,
},
response: Default::default(),
};
let mock_router_service = MockRouterService::new();
let mock_http_client = mock_with_callback(move |req: http::Request<RouterBody>| {
Box::pin(async {
let deserialized_request: Externalizable<Value> =
serde_json::from_slice(&hyper::body::to_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",
}
);
Ok(http::Response::builder()
.body(RouterBody::from(serde_json::to_string(&input).unwrap()))
.unwrap())
})
});
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed(),
"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>(
&hyper::body::to_bytes(response.into_body()).await.unwrap(),
)
.unwrap();
assert_eq!(
json!({
"errors": [{
"message": "this is a test error",
"extensions": {
"code": "ERROR"
}
}]
}),
actual_response
);
}
#[tokio::test]
async fn external_plugin_router_response() {
let router_stage = RouterStage {
response: RouterResponseConf {
condition: Default::default(),
headers: true,
context: true,
body: true,
sdl: true,
status_code: false,
},
request: Default::default(),
};
let mock_router_service = router::service::from_supergraph_mock_callback(move |req| {
Ok(supergraph::Response::builder()
.data(json!("{ \"test\": 1234_u32 }"))
.context(req.context)
.build()
.unwrap())
})
.await;
let mock_http_client =
mock_with_deferred_callback(move |res: http::Request<RouterBody>| {
Box::pin(async {
let deserialized_response: Externalizable<Value> = serde_json::from_slice(
&hyper::body::to_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"
});
Ok(http::Response::builder()
.body(RouterBody::from(serde_json::to_string(&input).unwrap()))
.unwrap())
})
});
let service = router_stage.as_service(
mock_http_client,
mock_router_service.boxed(),
"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>(
&get_body_bytes(res.response.into_body()).await.unwrap()
)
.unwrap()
);
}
#[tokio::test]
async fn external_plugin_router_response_validation_disabled_custom() {
let router_stage = RouterStage {
response: RouterResponseConf {
body: true,
..Default::default()
},
..Default::default()
};
let mock_router_service = router::service::from_supergraph_mock_callback(move |req| {
Ok(supergraph::Response::builder()
.data(json!({"test": 42}))
.context(req.context)
.build()
.unwrap())
})
.await;
let mock_http_client = mock_with_deferred_callback(move |_: http::Request<RouterBody>| {
Box::pin(async {
let response = json!({
"version": 1,
"stage": "RouterResponse",
"control": "continue",
"body": "{\"data\": {\"test\": \"modified_by_coprocessor\"}}"
});
Ok(http::Response::builder()
.status(200)
.body(RouterBody::from(serde_json::to_string(&response).unwrap()))
.unwrap())
})
});
let service_stack = router_stage
.as_service(
mock_http_client,
mock_router_service.boxed(),
"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 = get_body_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");
}
#[tokio::test]
async fn external_plugin_router_response_validation_enabled_valid() {
let service_stack = create_router_stage_for_response_validation_test()
.as_service(
create_mock_http_client_router_response_valid_response(),
create_mock_router_service_for_validation_test().await,
"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 = get_body_bytes(res.response.into_body()).await.unwrap();
let body: Value = serde_json::from_slice(&body_bytes).unwrap();
assert_eq!(body["data"]["test"], "valid_response");
}
#[tokio::test]
async fn external_plugin_router_response_validation_enabled_empty() {
let service_stack = create_router_stage_for_response_validation_test()
.as_service(
create_mock_http_client_router_response_empty_response(),
create_mock_router_service_for_validation_test().await,
"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 = get_body_bytes(res.response.into_body()).await.unwrap();
let body: Value = serde_json::from_slice(&body_bytes).unwrap();
assert!(body.as_object().unwrap().is_empty());
}
#[tokio::test]
async fn external_plugin_router_response_validation_enabled_invalid() {
let service_stack = create_router_stage_for_response_validation_test()
.as_service(
create_mock_http_client_router_response_invalid_response(),
create_mock_router_service_for_validation_test().await,
"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 = get_body_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");
}
#[tokio::test]
async fn external_plugin_router_response_validation_disabled_valid() {
let service_stack = create_router_stage_for_response_validation_test()
.as_service(
create_mock_http_client_router_response_valid_response(),
create_mock_router_service_for_validation_test().await,
"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 = get_body_bytes(res.response.into_body()).await.unwrap();
let body: Value = serde_json::from_slice(&body_bytes).unwrap();
assert_eq!(body["data"]["test"], "valid_response");
}
#[tokio::test]
async fn external_plugin_router_response_validation_disabled_empty() {
let service_stack = create_router_stage_for_response_validation_test()
.as_service(
create_mock_http_client_router_response_empty_response(),
create_mock_router_service_for_validation_test().await,
"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 = get_body_bytes(res.response.into_body()).await.unwrap();
let body: Value = serde_json::from_slice(&body_bytes).unwrap();
assert!(body.as_object().unwrap().is_empty());
}
#[tokio::test]
async fn external_plugin_router_response_validation_disabled_invalid() {
let service_stack = create_router_stage_for_response_validation_test()
.as_service(
create_mock_http_client_router_response_invalid_response(),
create_mock_router_service_for_validation_test().await,
"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 = get_body_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");
}
fn create_router_stage_for_validation_test() -> RouterStage {
RouterStage {
request: RouterRequestConf {
body: true,
..Default::default()
},
..Default::default()
}
}
async fn create_mock_router_service_for_validation_test() -> router::BoxService {
router::service::from_supergraph_mock_callback(move |req| {
Ok(supergraph::Response::builder()
.data(json!({"test": 42}))
.context(req.context)
.build()
.unwrap())
})
.await
.boxed()
}
fn create_mock_http_client_empty_router_response() -> MockInternalHttpClientService {
mock_with_callback(move |_: http::Request<RouterBody>| {
Box::pin(async {
let response = json!({
"version": 1,
"stage": "RouterRequest",
"control": {
"break": 400
},
"body": "{}"
});
Ok(http::Response::builder()
.status(200)
.body(RouterBody::from(serde_json::to_string(&response).unwrap()))
.unwrap())
})
})
}
fn create_router_stage_for_response_validation_test() -> RouterStage {
RouterStage {
request: Default::default(),
response: RouterResponseConf {
condition: Default::default(),
headers: true,
context: true,
body: true,
sdl: true,
status_code: false,
},
}
}
fn create_mock_http_client_router_response_valid_response() -> MockInternalHttpClientService {
mock_with_deferred_callback(move |_: http::Request<RouterBody>| {
Box::pin(async {
let response = json!({
"version": 1,
"stage": "RouterResponse",
"control": "continue",
"body": "{\"data\": {\"test\": \"valid_response\"}}"
});
Ok(http::Response::builder()
.status(200)
.body(RouterBody::from(serde_json::to_string(&response).unwrap()))
.unwrap())
})
})
}
fn create_mock_http_client_router_response_empty_response() -> MockInternalHttpClientService {
mock_with_deferred_callback(move |_: http::Request<RouterBody>| {
Box::pin(async {
let response = json!({
"version": 1,
"stage": "RouterResponse",
"control": "continue",
"body": "{}"
});
Ok(http::Response::builder()
.status(200)
.body(RouterBody::from(serde_json::to_string(&response).unwrap()))
.unwrap())
})
})
}
fn create_mock_http_client_router_response_invalid_response() -> MockInternalHttpClientService {
mock_with_deferred_callback(move |_: http::Request<RouterBody>| {
Box::pin(async {
let response = json!({
"version": 1,
"stage": "RouterResponse",
"control": "continue",
"body": "{\"errors\": \"this should be an array not a string\"}"
});
Ok(http::Response::builder()
.status(200)
.body(RouterBody::from(serde_json::to_string(&response).unwrap()))
.unwrap())
})
})
}
#[tokio::test]
async fn external_plugin_router_request_validation_disabled_empty() {
let service_stack = create_router_stage_for_validation_test()
.as_service(
create_mock_http_client_empty_router_response(),
create_mock_router_service_for_validation_test().await,
"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 = get_body_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()
);
}
#[tokio::test]
async fn external_plugin_router_request_validation_enabled_empty() {
let service_stack = create_router_stage_for_validation_test()
.as_service(
create_mock_http_client_empty_router_response(),
create_mock_router_service_for_validation_test().await,
"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 = get_body_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")
);
}
#[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).expect("externalized header map");
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).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);
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).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).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).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);
assert!(result.is_err());
}
fn create_subgraph_stage_for_validation_test() -> SubgraphStage {
SubgraphStage {
request: Default::default(),
response: SubgraphResponseConf {
condition: None,
headers: true,
context: true,
body: true,
service_name: false,
status_code: false,
subgraph_request_id: false,
},
}
}
fn create_mock_subgraph_service() -> MockSubgraphService {
let mut mock_subgraph_service = MockSubgraphService::new();
mock_subgraph_service
.expect_call()
.returning(|req: subgraph::Request| {
Ok(subgraph::Response::builder()
.data(json!({ "test": 1234_u32 }))
.errors(Vec::new())
.extensions(Object::new())
.context(req.context)
.id(req.id)
.build())
});
mock_subgraph_service
}
fn create_subgraph_stage_for_request_validation_test() -> SubgraphStage {
SubgraphStage {
request: SubgraphRequestConf {
condition: None,
headers: true,
context: true,
body: true,
uri: true,
method: true,
service_name: true,
subgraph_request_id: true,
},
response: Default::default(),
}
}
fn create_mock_http_client_subgraph_request_valid_response() -> MockInternalHttpClientService {
mock_with_callback(move |_: http::Request<RouterBody>| {
Box::pin(async {
let response = json!({
"version": 1,
"stage": "SubgraphRequest",
"control": {
"break": 400
},
"body": {
"data": {"test": "valid_response"}
}
});
Ok(http::Response::builder()
.status(200)
.body(RouterBody::from(serde_json::to_string(&response).unwrap()))
.unwrap())
})
})
}
fn create_mock_http_client_subgraph_request_empty_response() -> MockInternalHttpClientService {
mock_with_callback(move |_: http::Request<RouterBody>| {
Box::pin(async {
let response = json!({
"version": 1,
"stage": "SubgraphRequest",
"control": {
"break": 400
},
"body": {}
});
Ok(http::Response::builder()
.status(200)
.body(RouterBody::from(serde_json::to_string(&response).unwrap()))
.unwrap())
})
})
}
fn create_mock_http_client_subgraph_request_invalid_response() -> MockInternalHttpClientService
{
mock_with_callback(move |_: http::Request<RouterBody>| {
Box::pin(async {
let response = json!({
"version": 1,
"stage": "SubgraphRequest",
"control": {
"break": 400
},
"body": {
"errors": "this should be an array not a string"
}
});
Ok(http::Response::builder()
.status(200)
.body(RouterBody::from(serde_json::to_string(&response).unwrap()))
.unwrap())
})
})
}
fn create_mock_http_client_subgraph_response_valid_response() -> MockInternalHttpClientService {
mock_with_callback(move |_: http::Request<RouterBody>| {
Box::pin(async {
let input = json!({
"version": 1,
"stage": "SubgraphResponse",
"control": "continue",
"body": {
"data": {"test": "valid_response"}
}
});
Ok(http::Response::builder()
.body(RouterBody::from(serde_json::to_string(&input).unwrap()))
.unwrap())
})
})
}
fn create_mock_http_client_subgraph_response_empty_response() -> MockInternalHttpClientService {
mock_with_callback(move |_: http::Request<RouterBody>| {
Box::pin(async {
let input = json!({
"version": 1,
"stage": "SubgraphResponse",
"control": "continue",
"body": {}
});
Ok(http::Response::builder()
.body(RouterBody::from(serde_json::to_string(&input).unwrap()))
.unwrap())
})
})
}
fn create_mock_http_client_invalid_subgraph_response() -> MockInternalHttpClientService {
mock_with_callback(move |_: http::Request<RouterBody>| {
Box::pin(async {
let input = json!({
"version": 1,
"stage": "SubgraphResponse",
"control": "continue",
"body": {
"errors": "this should be an array not a string"
}
});
Ok(http::Response::builder()
.body(RouterBody::from(serde_json::to_string(&input).unwrap()))
.unwrap())
})
})
}
#[tokio::test]
async fn external_plugin_subgraph_response_validation_disabled_invalid() {
let service = create_subgraph_stage_for_validation_test().as_service(
create_mock_http_client_invalid_subgraph_response(),
create_mock_subgraph_service().boxed(),
"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()
);
}
#[tokio::test]
async fn external_plugin_subgraph_request_validation_enabled_valid() {
let service = create_subgraph_stage_for_request_validation_test().as_service(
create_mock_http_client_subgraph_request_valid_response(),
create_mock_subgraph_service().boxed(),
"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()
);
}
#[tokio::test]
async fn external_plugin_subgraph_request_validation_enabled_empty() {
let service = create_subgraph_stage_for_request_validation_test().as_service(
create_mock_http_client_subgraph_request_empty_response(),
create_mock_subgraph_service().boxed(),
"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")
);
}
#[tokio::test]
async fn external_plugin_subgraph_request_validation_enabled_invalid() {
let service = create_subgraph_stage_for_request_validation_test().as_service(
create_mock_http_client_subgraph_request_invalid_response(),
create_mock_subgraph_service().boxed(),
"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")
);
}
#[tokio::test]
async fn external_plugin_subgraph_request_validation_disabled_valid() {
let service = create_subgraph_stage_for_request_validation_test().as_service(
create_mock_http_client_subgraph_request_valid_response(),
create_mock_subgraph_service().boxed(),
"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()
);
}
#[tokio::test]
async fn external_plugin_subgraph_request_validation_disabled_empty() {
let service = create_subgraph_stage_for_request_validation_test().as_service(
create_mock_http_client_subgraph_request_empty_response(),
create_mock_subgraph_service().boxed(),
"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);
}
#[tokio::test]
async fn external_plugin_subgraph_request_validation_disabled_invalid() {
let service = create_subgraph_stage_for_request_validation_test().as_service(
create_mock_http_client_subgraph_request_invalid_response(),
create_mock_subgraph_service().boxed(),
"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());
}
#[tokio::test]
async fn external_plugin_subgraph_response_validation_enabled_valid() {
let service = create_subgraph_stage_for_validation_test().as_service(
create_mock_http_client_subgraph_response_valid_response(),
create_mock_subgraph_service().boxed(),
"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()
);
}
#[tokio::test]
async fn external_plugin_subgraph_response_validation_enabled_empty() {
let service = create_subgraph_stage_for_validation_test().as_service(
create_mock_http_client_subgraph_response_empty_response(),
create_mock_subgraph_service().boxed(),
"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());
}
#[tokio::test]
async fn external_plugin_subgraph_response_validation_enabled_invalid() {
let service = create_subgraph_stage_for_validation_test().as_service(
create_mock_http_client_invalid_subgraph_response(),
create_mock_subgraph_service().boxed(),
"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());
}
#[tokio::test]
async fn external_plugin_subgraph_response_validation_disabled_valid() {
let service = create_subgraph_stage_for_validation_test().as_service(
create_mock_http_client_subgraph_response_valid_response(),
create_mock_subgraph_service().boxed(),
"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()
);
}
#[tokio::test]
async fn external_plugin_subgraph_response_validation_disabled_empty() {
let service = create_subgraph_stage_for_validation_test().as_service(
create_mock_http_client_subgraph_response_empty_response(),
create_mock_subgraph_service().boxed(),
"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);
}
#[allow(clippy::type_complexity)]
fn mock_with_callback(
callback: fn(
http::Request<RouterBody>,
) -> BoxFuture<'static, Result<http::Response<RouterBody>, BoxError>>,
) -> MockInternalHttpClientService {
let mut mock_http_client = MockInternalHttpClientService::new();
mock_http_client.expect_clone().returning(move || {
let mut mock_http_client = MockInternalHttpClientService::new();
mock_http_client.expect_clone().returning(move || {
let mut mock_http_client = MockInternalHttpClientService::new();
mock_http_client.expect_call().returning(callback);
mock_http_client
});
mock_http_client
});
mock_http_client
}
#[allow(clippy::type_complexity)]
fn mock_with_deferred_callback(
callback: fn(
http::Request<RouterBody>,
) -> BoxFuture<'static, Result<http::Response<RouterBody>, BoxError>>,
) -> MockInternalHttpClientService {
let mut mock_http_client = MockInternalHttpClientService::new();
mock_http_client.expect_clone().returning(move || {
let mut mock_http_client = MockInternalHttpClientService::new();
mock_http_client.expect_clone().returning(move || {
let mut mock_http_client = MockInternalHttpClientService::new();
mock_http_client.expect_clone().returning(move || {
let mut mock_http_client = MockInternalHttpClientService::new();
mock_http_client.expect_call().returning(callback);
mock_http_client
});
mock_http_client
});
mock_http_client
});
mock_http_client
}
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(), false));
assert!(was_incoming_payload_valid(&invalid_response(), false));
assert!(was_incoming_payload_valid(&valid_response(), true));
assert!(!was_incoming_payload_valid(&invalid_response(), true));
}
#[test]
fn test_conditional_validation_logic() {
assert!(
handle_graphql_response(invalid_response(), Some(invalid_copro_body()), true, false)
.is_ok()
);
assert!(
handle_graphql_response(valid_response(), Some(invalid_copro_body()), true, true)
.is_err()
);
assert!(
handle_graphql_response(valid_response(), Some(valid_copro_body()), true, true).is_ok()
);
assert!(
handle_graphql_response(valid_response(), Some(invalid_copro_body()), false, true)
.is_ok()
);
}
}