use std::str::FromStr;
use std::sync::Arc;
use apollo_compiler::response::JsonMap;
use http::header::CONTENT_TYPE;
use itertools::EitherOrBoth;
use itertools::Itertools;
use mime::APPLICATION_JSON;
use mockall::mock;
use mockall::predicate::eq;
use req_asserts::Matcher;
use serde_json::Value;
use serde_json_bytes::json;
use tower::ServiceExt;
use tracing_core::Event;
use tracing_core::Metadata;
use tracing_core::span::Attributes;
use tracing_core::span::Id;
use tracing_core::span::Record;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::http::HeaderName;
use wiremock::http::HeaderValue;
use wiremock::matchers::body_json;
use wiremock::matchers::method;
use wiremock::matchers::path;
use crate::Configuration;
use crate::json_ext::ValueExt;
use crate::metrics::FutureMetricsExt;
use crate::plugins::connectors::tests::req_asserts::Plan;
use crate::plugins::telemetry::consts::CONNECT_SPAN_NAME;
use crate::plugins::telemetry::consts::OTEL_STATUS_CODE;
use crate::router_factory::RouterSuperServiceFactory;
use crate::router_factory::YamlRouterFactory;
use crate::services::new_service::ServiceFactory;
use crate::services::router::Request;
use crate::services::supergraph;
use crate::uplink::license_enforcement::LicenseState;
mod connect_on_type;
mod content_type;
mod error_handling;
mod mock_api;
mod progressive_override;
mod query_plan;
mod quickstart;
mod req_asserts;
mod url_properties;
mod variables;
const STEEL_THREAD_SCHEMA: &str = include_str!("../testdata/steelthread.graphql");
const MUTATION_SCHEMA: &str = include_str!("../testdata/mutation.graphql");
const NULLABILITY_SCHEMA: &str = include_str!("../testdata/nullability.graphql");
const SELECTION_SCHEMA: &str = include_str!("../testdata/selection.graphql");
const NO_SOURCES_SCHEMA: &str = include_str!("../testdata/connector-without-source.graphql");
const QUICKSTART_SCHEMA: &str = include_str!("../testdata/quickstart.graphql");
const INTERFACE_OBJECT_SCHEMA: &str = include_str!("../testdata/interface-object.graphql");
const VARIABLES_SCHEMA: &str = include_str!("../testdata/variables.graphql");
#[tokio::test]
async fn value_from_config() {
let mock_server = MockServer::start().await;
mock_api::user_1().mount(&mock_server).await;
let response = execute(
STEEL_THREAD_SCHEMA,
&mock_server.uri(),
"query { me { id name username} }",
Default::default(),
Some(json!({
"connectors": {
"sources": {
"connectors.json": {
"$config": {
"id": 1,
}
}
}
}
})),
|_| {},
None,
)
.await;
insta::assert_json_snapshot!(response, @r###"
{
"data": {
"me": {
"id": 1,
"name": "Leanne Graham",
"username": "Bret"
}
}
}
"###);
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![Matcher::new().method("GET").path("/users/1")],
);
}
#[tokio::test]
async fn max_requests() {
let mock_server = MockServer::start().await;
mock_api::users().mount(&mock_server).await;
mock_api::user_1().mount(&mock_server).await;
mock_api::user_2().mount(&mock_server).await;
let response = execute(
STEEL_THREAD_SCHEMA,
&mock_server.uri(),
"query { users { id name username } }",
Default::default(),
Some(json!({
"connectors": {
"max_requests_per_operation_per_source": 2
}
})),
|_| {},
Some(LicenseState::Licensed {
limits: Default::default(),
}),
)
.await;
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"users": [
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret"
},
{
"id": 2,
"name": "Ervin Howell",
"username": null
}
]
},
"errors": [
{
"message": "Request limit exceeded",
"path": [
"users",
1
],
"extensions": {
"code": "REQUEST_LIMIT_EXCEEDED",
"service": "connectors",
"connector": {
"coordinate": "connectors:Query.user[0]"
}
}
}
]
}
"#);
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![
Matcher::new().method("GET").path("/users"),
Matcher::new().method("GET").path("/users/1"),
],
);
}
#[tokio::test]
async fn source_max_requests() {
let mock_server = MockServer::start().await;
mock_api::users().mount(&mock_server).await;
mock_api::user_1().mount(&mock_server).await;
mock_api::user_2().mount(&mock_server).await;
let response = execute(
STEEL_THREAD_SCHEMA,
&mock_server.uri(),
"query { users { id name username } }",
Default::default(),
Some(json!({
"connectors": {
"subgraphs": {
"connectors": {
"sources": {
"json": {
"max_requests_per_operation": 2,
}
}
}
}
}
})),
|_| {},
Some(LicenseState::Licensed {
limits: Default::default(),
}),
)
.await;
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"users": [
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret"
},
{
"id": 2,
"name": "Ervin Howell",
"username": null
}
]
},
"errors": [
{
"message": "Request limit exceeded",
"path": [
"users",
1
],
"extensions": {
"code": "REQUEST_LIMIT_EXCEEDED",
"service": "connectors",
"connector": {
"coordinate": "connectors:Query.user[0]"
}
}
}
]
}
"#);
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![
Matcher::new().method("GET").path("/users"),
Matcher::new().method("GET").path("/users/1"),
],
);
}
#[tokio::test]
async fn test_root_field_plus_entity() {
let mock_server = MockServer::start().await;
mock_api::users().mount(&mock_server).await;
mock_api::user_1().mount(&mock_server).await;
mock_api::user_2().mount(&mock_server).await;
let response = execute(
STEEL_THREAD_SCHEMA,
&mock_server.uri(),
"query { users { __typename id name username } }",
Default::default(),
None,
|_| {},
None,
)
.await;
insta::assert_json_snapshot!(response, @r###"
{
"data": {
"users": [
{
"__typename": "User",
"id": 1,
"name": "Leanne Graham",
"username": "Bret"
},
{
"__typename": "User",
"id": 2,
"name": "Ervin Howell",
"username": "Antonette"
}
]
}
}
"###);
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![
Matcher::new().method("GET").path("/users"),
Matcher::new().method("GET").path("/users/1"),
Matcher::new().method("GET").path("/users/2"),
],
);
}
#[tokio::test]
async fn test_root_field_plus_entity_plus_requires() {
let mock_server = MockServer::start().await;
mock_api::users().mount(&mock_server).await;
mock_api::user_1().mount(&mock_server).await;
mock_api::user_2().mount(&mock_server).await;
Mock::given(method("POST"))
.and(path("/graphql"))
.and(body_json(json!({
"query": "query($representations: [_Any!]!) { _entities(representations: $representations) { ... on User { c } } }",
"variables": {"representations":[{"__typename":"User","id":1},{"__typename":"User","id":2}]}
})))
.respond_with(
ResponseTemplate::new(200)
.insert_header(CONTENT_TYPE, APPLICATION_JSON.essence_str())
.set_body_json(json!({
"data": {
"_entities": [{
"__typename": "User",
"c": "1",
}, {
"__typename": "User",
"c": "2",
}]
}
})),
).mount(&mock_server).await;
let response = execute(
STEEL_THREAD_SCHEMA,
&mock_server.uri(),
"query { users { __typename id name username d } }",
Default::default(),
None,
|_| {},
None,
)
.await;
insta::assert_json_snapshot!(response, @r###"
{
"data": {
"users": [
{
"__typename": "User",
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"d": "1-770-736-8031 x56442"
},
{
"__typename": "User",
"id": 2,
"name": "Ervin Howell",
"username": "Antonette",
"d": "1-770-736-8031 x56442"
}
]
}
}
"###);
let plan = Plan::Sequence(vec![
Plan::Fetch(Matcher::new().method("GET").path("/users")),
Plan::Parallel(vec![
Matcher::new().method("GET").path("/users/1"),
Matcher::new().method("GET").path("/users/2"),
Matcher::new().method("POST").path("/graphql"),
]),
Plan::Parallel(vec![
Matcher::new().method("GET").path("/users/1"),
Matcher::new().method("GET").path("/users/2"),
]),
]);
plan.assert_matches(&mock_server.received_requests().await.unwrap())
}
/// Tests that a connector can vend an entity reference like `user: { id: userId }`
#[tokio::test]
async fn test_entity_references() {
let mock_server = MockServer::start().await;
mock_api::posts().mount(&mock_server).await;
mock_api::user_1().mount(&mock_server).await;
mock_api::user_2().mount(&mock_server).await;
let response = execute(
STEEL_THREAD_SCHEMA,
&mock_server.uri(),
"query { posts { title user { name } } }",
Default::default(),
None,
|_| {},
None,
)
.await;
insta::assert_json_snapshot!(response, @r###"
{
"data": {
"posts": [
{
"title": "Post 1",
"user": {
"name": "Leanne Graham"
}
},
{
"title": "Post 2",
"user": {
"name": "Ervin Howell"
}
}
]
}
}
"###);
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![
Matcher::new().method("GET").path("/posts"),
Matcher::new().method("GET").path("/users/1"),
Matcher::new().method("GET").path("/users/2"),
],
);
}
#[tokio::test]
async fn basic_errors() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/users"))
.respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
"error": "not found"
})))
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/posts"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(serde_json::json!([{ "id": "1", "userId": "1" }])),
)
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/users/1"))
.respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({"error": "bad"})))
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/users/1/nicknames"))
.respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({"error": "bad"})))
.mount(&mock_server)
.await;
let response = execute(
STEEL_THREAD_SCHEMA,
&mock_server.uri(),
"{ users { id } posts { id user { name nickname } } }",
Default::default(),
None,
|_| {},
None,
)
.await;
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"users": null,
"posts": [
{
"id": "1",
"user": {
"name": null,
"nickname": null
}
}
]
},
"errors": [
{
"message": "Request failed",
"path": [
"users"
],
"extensions": {
"code": "CONNECTOR_FETCH",
"service": "connectors",
"connector": {
"coordinate": "connectors:Query.users[0]"
},
"http": {
"status": 404
}
}
},
{
"message": "Request failed",
"path": [
"posts",
0,
"user"
],
"extensions": {
"code": "CONNECTOR_FETCH",
"service": "connectors",
"connector": {
"coordinate": "connectors:Query.user[0]"
},
"http": {
"status": 400
}
}
},
{
"message": "Request failed",
"path": [
"posts",
0,
"user",
"nickname"
],
"extensions": {
"code": "CONNECTOR_FETCH",
"service": "connectors",
"connector": {
"coordinate": "connectors:User.nickname[0]"
},
"http": {
"status": 400
}
}
}
]
}
"#);
}
#[tokio::test]
async fn basic_connection_errors() {
let response = execute(
STEEL_THREAD_SCHEMA,
"http://localhost:9999",
"{ users { id } }",
Default::default(),
None,
|_| {},
None,
)
.await;
assert_eq!(response.get("data").unwrap(), &Value::Null);
assert_eq!(response.get("errors").unwrap().as_array().unwrap().len(), 1);
let err = response
.get("errors")
.unwrap()
.as_array()
.unwrap()
.first()
.unwrap();
// Different OSes have different codes at the end of the message so we have to assert on the parts separately
let msg = err.get("message").unwrap().as_str().unwrap();
assert!(
msg.starts_with(
"Connector error: HTTP fetch failed from 'connectors.json': tcp connect error"
),
"got message: {msg}"
);
assert_eq!(err.get("path").unwrap(), &serde_json::json!(["users"]));
assert_eq!(
err.get("extensions").unwrap(),
&serde_json::json!({
"service": "connectors",
"connector": {
"coordinate": "connectors:Query.users[0]"
},
"code": "HTTP_CLIENT_ERROR"
})
);
}
#[tokio::test]
async fn test_headers() {
let mock_server = MockServer::start().await;
mock_api::users().mount(&mock_server).await;
execute(
STEEL_THREAD_SCHEMA,
&mock_server.uri(),
"query { users { id } }",
Default::default(),
Some(json!({
"connectors": {
"subgraphs": {
"connectors": {
"$config": {
"source": {
"val": "val-from-config-source"
},
"connect": {
"val": "val-from-config-connect"
},
}
}
}
}
})),
|request| {
let headers = request.router_request.headers_mut();
headers.insert("x-rename-source", "renamed-by-source".parse().unwrap());
headers.insert("x-rename-connect", "renamed-by-connect".parse().unwrap());
headers.insert("x-forward", "forwarded".parse().unwrap());
headers.append("x-forward", "forwarded-again".parse().unwrap());
request
.context
.insert("val", String::from("val-from-request-context"))
.unwrap();
},
None,
)
.await;
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![
Matcher::new()
.method("GET")
.header(
HeaderName::from_str("x-forward").unwrap(),
HeaderValue::from_str("forwarded").unwrap(),
)
.header(
HeaderName::from_str("x-forward").unwrap(),
HeaderValue::from_str("forwarded-again").unwrap(),
)
.header(
HeaderName::from_str("x-new-name").unwrap(),
HeaderValue::from_str("renamed-by-connect").unwrap(),
)
.header(
HeaderName::from_str("x-insert").unwrap(),
HeaderValue::from_str("inserted").unwrap(),
)
.header(
HeaderName::from_str("x-insert-multi-value").unwrap(),
HeaderValue::from_str("first,second").unwrap(),
)
.header(
HeaderName::from_str("x-config-variable-source").unwrap(),
HeaderValue::from_str("before val-from-config-source after").unwrap(),
)
.header(
HeaderName::from_str("x-config-variable-connect").unwrap(),
HeaderValue::from_str("before val-from-config-connect after").unwrap(),
)
.header(
HeaderName::from_str("x-context-value-source").unwrap(),
HeaderValue::from_str("before val-from-request-context after").unwrap(),
)
.header(
HeaderName::from_str("x-context-value-connect").unwrap(),
HeaderValue::from_str("before val-from-request-context after").unwrap(),
)
.path("/users"),
],
);
}
#[tokio::test]
async fn test_override_headers_with_config() {
let mock_server = MockServer::start().await;
mock_api::users().mount(&mock_server).await;
execute(
STEEL_THREAD_SCHEMA,
&mock_server.uri(),
"query { users { id } }",
Default::default(),
Some(json!({
"connectors": {
"subgraphs": {
"connectors": {
"$config": {
"source": {
"val": "val-from-config-source"
},
"connect": {
"val": "val-from-config-connect"
},
}
}
}
},
"headers": {
"connector": {
"all": {
"request": [
// This is additive to the existing forwarding rule
{
"propagate": {
"named": "x-forward-2",
"rename": "x-forward"
}
},
// This is an override
{
"insert": {
"name": "x-insert",
"value": "inserted-by-config"
}
},
// This is an override
{
"insert": {
"name": "x-insert-multi-value",
"value": "third,fourth"
}
}
]
}
}
}
})),
|request| {
let headers = request.router_request.headers_mut();
headers.insert("x-rename-source", "renamed-by-source".parse().unwrap());
headers.insert("x-rename-connect", "renamed-by-connect".parse().unwrap());
headers.insert("x-forward", "forwarded".parse().unwrap());
headers.insert("x-forward-2", "forwarded-by-config".parse().unwrap());
headers.append("x-forward", "forwarded-again".parse().unwrap());
request
.context
.insert("val", String::from("val-from-request-context"))
.unwrap();
},
None,
)
.await;
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![
Matcher::new()
.method("GET")
.header(
HeaderName::from_str("x-forward").unwrap(),
HeaderValue::from_str("forwarded-by-config").unwrap(),
)
.header(
HeaderName::from_str("x-insert").unwrap(),
HeaderValue::from_str("inserted-by-config").unwrap(),
)
.header(
HeaderName::from_str("x-insert-multi-value").unwrap(),
HeaderValue::from_str("third,fourth").unwrap(),
)
.path("/users"),
],
);
}
#[tokio::test]
async fn should_only_send_named_header_once_when_both_config_and_schema_propagate_header() {
let mock_server = MockServer::start().await;
mock_api::users().mount(&mock_server).await;
execute(
STEEL_THREAD_SCHEMA,
&mock_server.uri(),
"query { users { id } }",
Default::default(),
Some(json!({
"connectors": {
"subgraphs": {
"connectors": {
"$config": {
"source": {
"val": "val-from-config-source"
},
"connect": {
"val": "val-from-config-connect"
},
}
}
}
},
"headers": {
"connector": {
"all": {
"request": [
{
"propagate": {
"named": "x-forward",
}
},
]
}
}
}
})),
|request| {
let headers = request.router_request.headers_mut();
headers.append("x-forward", "forwarded".parse().unwrap());
request
.context
.insert("val", String::from("val-from-request-context"))
.unwrap();
},
None,
)
.await;
let received_requests = &mock_server.received_requests().await.unwrap();
assert!(
!received_requests
.iter()
.any(|r| r.headers.get_all("x-forward").iter().count() > 1),
"There should only be one instance of x-forward since the yaml config is overriding the sdl"
);
req_asserts::matches(
received_requests,
vec![
Matcher::new()
.method("GET")
.header(
HeaderName::from_str("x-forward").unwrap(),
HeaderValue::from_str("forwarded").unwrap(),
)
.header(
HeaderName::from_str("x-insert").unwrap(),
HeaderValue::from_str("inserted").unwrap(),
)
.header(
HeaderName::from_str("x-insert-multi-value").unwrap(),
HeaderValue::from_str("first,second").unwrap(),
)
.path("/users"),
],
);
}
#[tokio::test]
async fn should_only_send_matching_header_once_when_both_config_and_schema_propagate_header() {
let mock_server = MockServer::start().await;
mock_api::users().mount(&mock_server).await;
execute(
STEEL_THREAD_SCHEMA,
&mock_server.uri(),
"query { users { id } }",
Default::default(),
Some(json!({
"connectors": {
"subgraphs": {
"connectors": {
"$config": {
"source": {
"val": "val-from-config-source"
},
"connect": {
"val": "val-from-config-connect"
},
}
}
}
},
"headers": {
"connector": {
"all": {
"request": [
{
"propagate": {
"matching": ".+?forward",
}
},
]
}
}
}
})),
|request| {
let headers = request.router_request.headers_mut();
headers.append("x-forward", "forwarded".parse().unwrap());
headers.append("y-forward", "also-forwarded".parse().unwrap());
request
.context
.insert("val", String::from("val-from-request-context"))
.unwrap();
},
None,
)
.await;
let received_requests = &mock_server.received_requests().await.unwrap();
assert!(
!received_requests
.iter()
.any(|r| r.headers.get_all("x-forward").iter().count() > 1),
"There should only be one instance of x-forward since the yaml config is overriding the sdl"
);
req_asserts::matches(
received_requests,
vec![
Matcher::new()
.method("GET")
.header(
HeaderName::from_str("x-forward").unwrap(),
HeaderValue::from_str("forwarded").unwrap(),
)
.header(
HeaderName::from_str("y-forward").unwrap(),
HeaderValue::from_str("also-forwarded").unwrap(),
)
.header(
HeaderName::from_str("x-insert").unwrap(),
HeaderValue::from_str("inserted").unwrap(),
)
.header(
HeaderName::from_str("x-insert-multi-value").unwrap(),
HeaderValue::from_str("first,second").unwrap(),
)
.path("/users"),
],
);
}
#[tokio::test]
async fn should_remove_header_when_sdl_has_insert_and_yaml_has_remove() {
let mock_server = MockServer::start().await;
mock_api::users().mount(&mock_server).await;
execute(
STEEL_THREAD_SCHEMA,
&mock_server.uri(),
"query { users { id } }",
Default::default(),
Some(json!({
"connectors": {
"subgraphs": {
"connectors": {
"$config": {
"source": {
"val": "val-from-config-source"
},
"connect": {
"val": "val-from-config-connect"
},
}
}
}
},
"headers": {
"connector": {
"all": {
"request": [
{
"remove": {
"named": "x-insert",
}
},
]
}
}
}
})),
|request| {
request
.context
.insert("val", String::from("val-from-request-context"))
.unwrap();
},
None,
)
.await;
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![
Matcher::new()
.method("GET")
.header(
HeaderName::from_str("x-insert-multi-value").unwrap(),
HeaderValue::from_str("first,second").unwrap(),
)
.path("/users"),
],
);
}
#[tokio::test]
async fn test_args_and_this_in_header() {
let mock_server = MockServer::start().await;
mock_api::user_2().mount(&mock_server).await;
mock_api::user_2_nicknames().mount(&mock_server).await;
execute(
STEEL_THREAD_SCHEMA,
&mock_server.uri(),
"query { user(id: 2){ id nickname } }",
Default::default(),
None,
|_| {},
None,
)
.await;
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![
Matcher::new()
.method("GET")
.header(
HeaderName::from_str("x-from-args").unwrap(),
HeaderValue::from_str("before 2 after").unwrap(),
)
.path("/users/2"),
Matcher::new()
.method("GET")
.header(
HeaderName::from_str("x-from-this").unwrap(),
HeaderValue::from_str("before 2 after").unwrap(),
)
.path("/users/2/nicknames"),
],
);
}
mock! {
Subscriber {}
impl tracing_core::Subscriber for Subscriber {
fn enabled<'a>(&self, metadata: &Metadata<'a>) -> bool;
fn new_span<'a>(&self, span: &Attributes<'a>) -> Id;
fn record<'a>(&self, span: &Id, values: &Record<'a>);
fn record_follows_from(&self, span: &Id, follows: &Id);
fn event_enabled<'a>(&self, event: &Event<'a>) -> bool;
fn event<'a>(&self, event: &Event<'a>);
fn enter(&self, span: &Id);
fn exit(&self, span: &Id);
}
}
#[tokio::test]
async fn test_tracing_connect_span() {
let mut mock_subscriber = MockSubscriber::new();
mock_subscriber.expect_event_enabled().returning(|_| false);
mock_subscriber.expect_record().returning(|_, _| {});
mock_subscriber
.expect_enabled()
.returning(|metadata| metadata.name() == CONNECT_SPAN_NAME);
mock_subscriber.expect_new_span().returning(|attributes| {
if attributes.metadata().name() == CONNECT_SPAN_NAME {
assert!(attributes.fields().field("apollo.connector.type").is_some());
assert!(
attributes
.fields()
.field("apollo.connector.detail")
.is_some()
);
assert!(
attributes
.fields()
.field("apollo.connector.coordinate")
.is_some()
);
assert!(
attributes
.fields()
.field("apollo.connector.selection")
.is_some()
);
assert!(
attributes
.fields()
.field("apollo.connector.source.name")
.is_some()
);
assert!(
attributes
.fields()
.field("apollo.connector.source.detail")
.is_some()
);
assert!(attributes.fields().field(OTEL_STATUS_CODE).is_some());
Id::from_u64(1)
} else {
panic!("unexpected span: {}", attributes.metadata().name());
}
});
mock_subscriber
.expect_enter()
.with(eq(Id::from_u64(1)))
.returning(|_| {});
mock_subscriber
.expect_exit()
.with(eq(Id::from_u64(1)))
.returning(|_| {});
let _guard = tracing::subscriber::set_default(mock_subscriber);
let mock_server = MockServer::start().await;
mock_api::users().mount(&mock_server).await;
execute(
STEEL_THREAD_SCHEMA,
&mock_server.uri(),
"query { users { id } }",
Default::default(),
None,
|_| {},
None,
)
.await;
}
#[tokio::test]
async fn test_operation_counter() {
async {
let mock_server = MockServer::start().await;
mock_api::users().mount(&mock_server).await;
execute(
STEEL_THREAD_SCHEMA,
&mock_server.uri(),
"query { users { id name username } }",
Default::default(),
None,
|_| {},
None,
)
.await;
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![
Matcher::new().method("GET").path("/users"),
Matcher::new().method("GET").path("/users/1"),
Matcher::new().method("GET").path("/users/2"),
],
);
assert_counter!(
"apollo.router.operations.connectors",
3,
connector.type = "http",
subgraph.name = "connectors"
);
}
.with_metrics()
.await;
}
#[tokio::test]
async fn test_mutation() {
let mock_server = MockServer::start().await;
mock_api::create_user().mount(&mock_server).await;
let response = execute(
MUTATION_SCHEMA,
&mock_server.uri(),
"mutation CreateUser($name: String!) {
createUser(name: $name) {
success
user {
id
name
}
}
}",
serde_json_bytes::json!({ "name": "New User" })
.as_object()
.unwrap()
.clone(),
None,
|_| {},
None,
)
.await;
insta::assert_json_snapshot!(response, @r###"
{
"data": {
"createUser": {
"success": true,
"user": {
"id": 3,
"name": "New User"
}
}
}
}
"###);
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![
Matcher::new()
.method("POST")
.body(serde_json::json!({ "username": "New User" }))
.path("/user"),
],
);
}
#[tokio::test]
async fn test_mutation_empty_body() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/user"))
.respond_with(ResponseTemplate::new(200))
.mount(&mock_server)
.await;
let response = execute(
MUTATION_SCHEMA,
&mock_server.uri(),
"mutation CreateUser($name: String!) {
createUser(name: $name) {
success
}
}",
serde_json_bytes::json!({ "name": "New User" })
.as_object()
.unwrap()
.clone(),
None,
|_| {},
None,
)
.await;
insta::assert_json_snapshot!(response, @r###"
{
"data": {
"createUser": {
"success": true
}
}
}
"###);
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![
Matcher::new()
.method("POST")
.body(serde_json::json!({ "username": "New User" }))
.path("/user"),
],
);
}
#[tokio::test]
async fn test_selection_set() {
let mock_server = MockServer::start().await;
mock_api::commits().mount(&mock_server).await;
let response = execute(
SELECTION_SCHEMA,
&mock_server.uri(),
"query Commits($owner: String!, $repo: String!, $skipInlineFragment: Boolean!,
$skipNamedFragment: Boolean!, $skipField: Boolean!) {
commits(owner: $owner, repo: $repo) {
__typename
commit {
__typename
from_path_alias: name_from_path
...CommitDetails @skip(if: $skipNamedFragment)
}
}
}
fragment CommitDetails on CommitDetail {
by {
__typename
user: name @skip(if: $skipField)
name
...on CommitAuthor @skip(if: $skipInlineFragment) {
address: email
owner
}
owner_not_fragment: owner
}
}",
serde_json_bytes::json!({
"owner": "foo",
"repo": "bar",
"skipField": false,
"skipInlineFragment": false,
"skipNamedFragment": false
})
.as_object()
.unwrap()
.clone(),
None,
|_| {},
None,
)
.await;
insta::assert_json_snapshot!(response, @r###"
{
"data": {
"commits": [
{
"__typename": "Commit",
"commit": {
"__typename": "CommitDetail",
"from_path_alias": "Foo Bar",
"by": {
"__typename": "CommitAuthor",
"user": "Foo Bar",
"name": "Foo Bar",
"address": "noone@nowhere",
"owner": "foo",
"owner_not_fragment": "foo"
}
}
}
]
}
}
"###);
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![
Matcher::new()
// Testing multiline headers
.header(
HeaderName::from_static("x-multiline"),
HeaderValue::from_static("multi line header"),
)
.method("GET")
.path("/repos/foo/bar/commits"),
],
);
}
#[tokio::test]
async fn test_nullability() {
let mock_server = MockServer::start().await;
mock_api::user_1_with_pet().mount(&mock_server).await;
let response = execute(
NULLABILITY_SCHEMA,
&mock_server.uri(),
"query { user(id: 1) { id name occupation address { zip } pet { species } } }",
Default::default(),
None,
|_| {},
None,
)
.await;
insta::assert_json_snapshot!(response, @r###"
{
"data": {
"user": {
"id": 1,
"name": "Leanne Graham",
"occupation": null,
"address": null,
"pet": {
"species": null
}
}
}
}
"###);
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![Matcher::new().method("GET").path("/users/1")],
);
}
#[tokio::test]
async fn test_default_argument_values() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/default-args"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!("hello")))
.mount(&mock_server)
.await;
let response = execute(
NULLABILITY_SCHEMA,
&mock_server.uri(),
"query { defaultArgs }",
Default::default(),
None,
|_| {},
None,
)
.await;
insta::assert_json_snapshot!(response, @r###"
{
"data": {
"defaultArgs": "hello"
}
}
"###);
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![
Matcher::new()
.method("POST")
.path("/default-args")
.body(serde_json::json!({
"str": "default",
"int": 42,
"float": 1.23,
"bool": true,
"arr": ["default"],
})),
],
);
}
#[tokio::test]
async fn test_default_argument_overrides() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/default-args"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!("hello")))
.mount(&mock_server)
.await;
let response = execute(
NULLABILITY_SCHEMA,
&mock_server.uri(),
"query { defaultArgs(str: \"hi\" int: 108 float: 9.87 bool: false arr: [\"hi again\"]) }",
Default::default(),
None,
|_| {},
None,
)
.await;
insta::assert_json_snapshot!(response, @r###"
{
"data": {
"defaultArgs": "hello"
}
}
"###);
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![
Matcher::new()
.method("POST")
.path("/default-args")
.body(serde_json::json!({
"str": "hi",
"int": 108,
"float": 9.87,
"bool": false,
"arr": ["hi again"],
})),
],
);
}
#[tokio::test]
async fn test_form_encoding() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/posts"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ "id": 1 })))
.mount(&mock_server)
.await;
let uri = mock_server.uri();
let response = execute(
include_str!("../testdata/form-encoding.graphql"),
&uri,
"mutation {
post(
input: {
int: 1
str: \"s\"
bool: true
id: \"id\"
intArr: [1, 2]
strArr: [\"a\", \"b\"]
boolArr: [true, false]
idArr: [\"id1\", \"id2\"]
obj: {
a: 1
b: \"b\"
c: true
nested: {
d: 1
e: \"e\"
f: true
}
}
objArr: [
{
a: 1
b: \"b\"
c: true
nested: {
d: 1
e: \"e\"
f: true
}
},
{
a: 2
b: \"bb\"
c: false
nested: {
d: 1
e: \"e\"
f: true
}
}
]
}
)
{ id }
}",
Default::default(),
None,
|_| {},
None,
)
.await;
insta::assert_json_snapshot!(response, @r###"
{
"data": {
"post": {
"id": 1
}
}
}
"###);
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![Matcher::new().method("POST").path("/posts")],
);
let reqs = mock_server.received_requests().await.unwrap();
let body = String::from_utf8_lossy(&reqs[0].body).to_string();
assert_eq!(
body,
"int=1&str=s&bool=true&id=id&intArr%5B0%5D=1&intArr%5B1%5D=2&strArr%5B0%5D=a&strArr%5B1%5D=b&boolArr%5B0%5D=true&boolArr%5B1%5D=false&idArr%5B0%5D=id1&idArr%5B1%5D=id2&obj%5Ba%5D=1&obj%5Bb%5D=b&obj%5Bc%5D=true&obj%5Bnested%5D%5Bd%5D=1&obj%5Bnested%5D%5Be%5D=e&obj%5Bnested%5D%5Bf%5D=true&objArr%5B0%5D%5Ba%5D=1&objArr%5B0%5D%5Bb%5D=b&objArr%5B0%5D%5Bc%5D=true&objArr%5B0%5D%5Bnested%5D%5Bd%5D=1&objArr%5B0%5D%5Bnested%5D%5Be%5D=e&objArr%5B0%5D%5Bnested%5D%5Bf%5D=true&objArr%5B1%5D%5Ba%5D=2&objArr%5B1%5D%5Bb%5D=bb&objArr%5B1%5D%5Bc%5D=false&objArr%5B1%5D%5Bnested%5D%5Bd%5D=1&objArr%5B1%5D%5Bnested%5D%5Be%5D=e&objArr%5B1%5D%5Bnested%5D%5Bf%5D=true"
);
}
#[tokio::test]
async fn test_no_source() {
let mock_server = MockServer::start().await;
mock_api::user_1().mount(&mock_server).await;
let uri = mock_server.uri();
let response = execute(
&NO_SOURCES_SCHEMA.replace("http://localhost", &uri),
&uri,
"query { user(id: 1) { id name }}",
Default::default(),
None,
|_| {},
None,
)
.await;
insta::assert_json_snapshot!(response, @r###"
{
"data": {
"user": {
"id": 1,
"name": "Leanne Graham"
}
}
}
"###);
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![Matcher::new().method("GET").path("/users/1")],
);
}
#[tokio::test]
async fn error_not_redacted() {
let mock_server = MockServer::start().await;
mock_api::users_error().mount(&mock_server).await;
let response = execute(
STEEL_THREAD_SCHEMA,
&mock_server.uri(),
"query { users { id name username } }",
Default::default(),
Some(json!({
"include_subgraph_errors": {
"subgraphs": {
"connectors": true
}
}
})),
|_| {},
None,
)
.await;
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"users": null
},
"errors": [
{
"message": "Request failed",
"path": [
"users"
],
"extensions": {
"code": "CONNECTOR_FETCH",
"service": "connectors",
"connector": {
"coordinate": "connectors:Query.users[0]"
},
"http": {
"status": 404
}
}
}
]
}
"#);
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![Matcher::new().method("GET").path("/users")],
);
}
#[tokio::test]
async fn error_redacted() {
let mock_server = MockServer::start().await;
mock_api::users_error().mount(&mock_server).await;
let response = execute(
STEEL_THREAD_SCHEMA,
&mock_server.uri(),
"query { users { id name username } }",
Default::default(),
Some(json!({
"include_subgraph_errors": {
"subgraphs": {
"connectors": false
}
}
})),
|_| {},
None,
)
.await;
insta::assert_json_snapshot!(response, @r###"
{
"data": {
"users": null
},
"errors": [
{
"message": "Subgraph errors redacted",
"path": [
"users"
]
}
]
}
"###);
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![Matcher::new().method("GET").path("/users")],
);
}
#[tokio::test]
async fn test_interface_object() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/itfs"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(serde_json::json!([{ "id": 1, "c": 10 }, { "id": 2, "c": 11 }])),
)
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/itfs/1"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(serde_json::json!({ "id": 1, "c": 10, "d": 20 })),
)
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/itfs/2"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(serde_json::json!({ "id": 1, "c": 11, "d": 21 })),
)
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/itfs/1/e"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!("e1")))
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/itfs/2/e"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!("e2")))
.mount(&mock_server)
.await;
Mock::given(method("POST"))
.and(path("/graphql"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"data": {
"_entities": [{
"__typename": "T1",
"a": "a"
}, {
"__typename": "T2",
"b": "b"
}]
}
})))
.mount(&mock_server)
.await;
let response = execute(
INTERFACE_OBJECT_SCHEMA,
&mock_server.uri(),
"query { itfs { __typename id c d e ... on T1 { a } ... on T2 { b } } }",
Default::default(),
None,
|_| {},
None,
)
.await;
insta::assert_json_snapshot!(response, @r###"
{
"data": {
"itfs": [
{
"__typename": "T1",
"id": 1,
"c": 10,
"d": 20,
"e": "e1",
"a": "a"
},
{
"__typename": "T2",
"id": 2,
"c": 11,
"d": 21,
"e": "e2",
"b": "b"
}
]
}
}
"###);
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![
Matcher::new().method("GET").path("/itfs"),
Matcher::new().method("GET").path("/itfs/1/e"),
Matcher::new().method("GET").path("/itfs/2/e"),
Matcher::new().method("GET").path("/itfs/1"),
Matcher::new().method("GET").path("/itfs/2"),
Matcher::new()
.method("POST")
.path("/graphql")
.body(serde_json::json!({
"query": r#"query($representations: [_Any!]!) { _entities(representations: $representations) { ... on Itf { __typename ... on T1 { a } ... on T2 { b } } } }"#,
"variables": {
"representations": [
{ "__typename": "Itf", "id": 1 },
{ "__typename": "Itf", "id": 2 }
]
}
})),
],
);
}
#[tokio::test]
async fn test_sources_in_context() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/coprocessor"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"control": "continue",
"version": 1,
"stage": "ExecutionRequest"
})))
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/posts"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
{ "userId": 1, "id": 1, "title": "title", "body": "body" },
{ "userId": 1, "id": 2, "title": "title", "body": "body" }]
)))
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/users/1"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": 1,
"name": "Leanne Graham",
"username": "Bret"
})))
.mount(&mock_server)
.await;
let uri = mock_server.uri();
let _ = execute(
&QUICKSTART_SCHEMA.replace("https://jsonplaceholder.typicode.com", &mock_server.uri()),
&uri,
"query Posts { posts { id body title author { name username } } }",
Default::default(),
Some(json!({
"connectors": {
"expose_sources_in_context": true
},
"coprocessor": {
"url": format!("{}/coprocessor", mock_server.uri()),
"execution": {
"request": {
"context": true
}
}
}
})),
|_| {},
Some(LicenseState::Licensed {
limits: Default::default(),
}),
)
.await;
let requests = &mock_server.received_requests().await.unwrap();
let coprocessor_request = requests.first().unwrap();
let body = coprocessor_request
.body_json::<serde_json_bytes::Value>()
.unwrap();
pretty_assertions::assert_eq!(
body.get("context")
.unwrap()
.as_object()
.unwrap()
.get("entries")
.unwrap()
.as_object()
.unwrap()
.get("apollo_connectors::sources_in_query_plan")
.unwrap(),
&serde_json_bytes::json!([
{ "subgraph_name": "connectors", "source_name": "jsonPlaceholder" }
])
);
}
#[tokio::test]
async fn test_variables() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/coprocessor"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"control": "continue",
"version": 1,
"stage": "SupergraphRequest",
"context": {
"entries": {
"value": "B"
}
}
})))
.mount(&mock_server)
.await;
Mock::given(method("POST"))
.and(path("/f"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(serde_json::json!({}))
.insert_header("value", "myothercoolheader"),
)
.mount(&mock_server)
.await;
let uri = mock_server.uri();
let response = execute(
&VARIABLES_SCHEMA.replace("http://localhost:4001/", &mock_server.uri()),
&uri,
"{ f(arg: \"arg\") { arg context config sibling status extra request response f(arg: \"arg\") { arg context config sibling status } } }",
Default::default(),
Some(json!({
"connectors": {
"subgraphs": {
"connectors": {
"$config": {
"value": "C"
}
}
}
},
"coprocessor": {
"url": format!("{}/coprocessor", mock_server.uri()),
"supergraph": {
"request": {
"context": true
}
}
}
})),
|request| {
let headers = request.router_request.headers_mut();
headers.insert("value", "coolheader".parse().unwrap());
},
Some(LicenseState::Licensed {
limits: Default::default(),
}),
)
.await;
insta::assert_json_snapshot!(response, @r###"
{
"data": {
"f": {
"arg": "arg",
"context": "B",
"config": "C",
"sibling": "D",
"status": 200,
"extra": {
"arg": "arg",
"context": "B",
"config": "C",
"status": 200
},
"request": "coolheader",
"response": "myothercoolheader",
"f": {
"arg": "arg",
"context": "B",
"config": "C",
"sibling": "D",
"status": 200
}
}
}
}
"###);
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![
Matcher::new().method("POST").path("/coprocessor"),
Matcher::new()
.method("POST")
.path("/f")
.query("arg=rg&context=B&config=C&header=coolheader")
.header(HeaderName::from_static("x-source-context"), "B".try_into().unwrap())
.header(HeaderName::from_static("x-source-config"), "C".try_into().unwrap())
.header(HeaderName::from_static("x-connect-arg"), "g".try_into().unwrap())
.header(HeaderName::from_static("x-connect-context"), "B".try_into().unwrap())
.header(HeaderName::from_static("x-connect-config"), "C".try_into().unwrap())
.body(serde_json::json!({ "arg": "arg", "context": "B", "config": "C", "request": "coolheader" }))
,
Matcher::new()
.method("POST")
.path("/f")
.query("arg=g&context=B&config=C&sibling=D")
.header(HeaderName::from_static("x-source-context"), "B".try_into().unwrap())
.header(HeaderName::from_static("x-source-config"), "C".try_into().unwrap())
.header(HeaderName::from_static("x-connect-arg"), "a".try_into().unwrap())
.header(HeaderName::from_static("x-connect-context"), "B".try_into().unwrap())
.header(HeaderName::from_static("x-connect-config"), "C".try_into().unwrap())
.header(HeaderName::from_static("x-connect-sibling"), "D".try_into().unwrap())
.body(serde_json::json!({ "arg": "arg", "context": "B", "config": "C", "sibling": "D" }))
,
],
);
}
#[tokio::test]
async fn should_support_using_variable_in_nested_input_argument() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/complexInputType"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!("Hello world!")))
.mount(&mock_server)
.await;
let uri = mock_server.uri();
let mut variables: JsonMap = serde_json_bytes::Map::new();
variables.insert(
serde_json_bytes::ByteString::from("query"),
serde_json_bytes::Value::from("kim"),
);
let response = execute(
&VARIABLES_SCHEMA.replace("http://localhost:4001/", &mock_server.uri()),
&uri,
"query Query ($query: String){ complexInputType(filters: { inSpace: true, search: $query }) }",
variables,
None,
|_|{},
None
)
.await;
insta::assert_json_snapshot!(response, @r###"
{
"data": {
"complexInputType": "Hello world!"
}
}
"###);
req_asserts::matches(
&mock_server.received_requests().await.unwrap(),
vec![
Matcher::new()
.method("GET")
.path("/complexInputType")
.query("inSpace=true&search=kim"),
],
);
}
#[tokio::test]
async fn doesnt_error_when_using_undefined_arguments() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/complexInputType"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!("Hello world!")))
.mount(&mock_server)
.await;
let uri = mock_server.uri();
let mut variables: JsonMap = serde_json_bytes::Map::new();
variables.insert(
serde_json_bytes::ByteString::from("query_not_named_right"),
serde_json_bytes::Value::from("kim"),
);
let response = execute(
&VARIABLES_SCHEMA.replace("http://localhost:4001/", &mock_server.uri()),
&uri,
"query Query ($query: String){ complexInputType(filters: { inSpace: true, search: $query }) }",
variables,
None,
|_|{},
None,
)
.await;
insta::assert_json_snapshot!(response, @r###"
{
"data": {
"complexInputType": "Hello world!"
}
}
"###);
}
mod quickstart_tests {
use http::Uri;
use super::*;
use crate::test_harness::http_snapshot::SnapshotServer;
const SNAPSHOT_DIR: &str = "./src/plugins/connectors/testdata/quickstart_api_snapshots/";
macro_rules! map {
($($tt:tt)*) => {
serde_json_bytes::json!($($tt)*).as_object().unwrap().clone()
};
}
async fn execute(
query: &str,
variables: JsonMap,
snapshot_file_name: &str,
) -> serde_json::Value {
let snapshot_path = [SNAPSHOT_DIR, snapshot_file_name, ".json"].concat();
let server = SnapshotServer::spawn(
snapshot_path,
Uri::from_str("https://jsonPlaceholder.typicode.com/").unwrap(),
true,
false,
Some(vec![CONTENT_TYPE.to_string()]),
None,
)
.await;
super::execute(
&QUICKSTART_SCHEMA.replace("https://jsonplaceholder.typicode.com", &server.uri()),
&server.uri(),
query,
variables,
None,
|_| {},
None,
)
.await
}
#[tokio::test]
async fn query_1() {
let query = r#"
query Posts {
posts {
id
body
title
}
}
"#;
let response = execute(query, Default::default(), "query_1").await;
insta::assert_json_snapshot!(response, @r###"
{
"data": {
"posts": [
{
"id": 1,
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto",
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit"
},
{
"id": 2,
"body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla",
"title": "qui est esse"
}
]
}
}
"###);
}
#[tokio::test]
async fn query_2() {
let query = r#"
query Post($postId: ID!) {
post(id: $postId) {
id
title
body
}
}
"#;
let response = execute(query, map!({ "postId": "1" }), "query_2").await;
insta::assert_json_snapshot!(response, @r###"
{
"data": {
"post": {
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
}
}
"###);
}
#[tokio::test]
async fn query_3() {
let query = r#"
query PostWithAuthor($postId: ID!) {
post(id: $postId) {
id
title
body
author {
id
name
}
}
}
"#;
let response = execute(query, map!({ "postId": "1" }), "query_3").await;
insta::assert_json_snapshot!(response, @r###"
{
"data": {
"post": {
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto",
"author": {
"id": 1,
"name": "Leanne Graham"
}
}
}
}
"###);
}
#[tokio::test]
async fn query_4() {
let query = r#"
query PostsForUser($userId: ID!) {
user(id: $userId) {
id
name
posts {
id
title
author {
id
name
}
}
}
}
"#;
let response = execute(query, map!({ "userId": "1" }), "query_4").await;
insta::assert_json_snapshot!(response, @r###"
{
"data": {
"user": {
"id": 1,
"name": "Leanne Graham",
"posts": [
{
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"author": {
"id": 1,
"name": "Leanne Graham"
}
},
{
"id": 2,
"title": "qui est esse",
"author": {
"id": 1,
"name": "Leanne Graham"
}
}
]
}
}
}
"###);
}
}
async fn execute(
schema: &str,
uri: &str,
query: &str,
variables: JsonMap,
config: Option<serde_json_bytes::Value>,
mut request_mutator: impl FnMut(&mut Request),
license: Option<LicenseState>,
) -> serde_json::Value {
let connector_uri = format!("{uri}/");
let subgraph_uri = format!("{uri}/graphql");
// we cannot use Testharness because the subgraph connectors are actually extracted in YamlRouterFactory
let mut factory = YamlRouterFactory;
let common_config = json!({
"include_subgraph_errors": { "all": true },
"override_subgraph_url": {"graphql": subgraph_uri},
"connectors": {
"sources": {
"connectors.json": {
"override_url": connector_uri
}
}
}
});
let config = if let Some(mut config) = config {
config.deep_merge(common_config);
config
} else {
common_config
};
let config: Configuration = serde_json_bytes::from_value(config).unwrap();
let router_creator = factory
.create(
false,
Arc::new(config.clone()),
Arc::new(crate::spec::Schema::parse(schema, &config).unwrap()),
None,
None,
Arc::new(license.unwrap_or_default()),
)
.await
.unwrap();
let service = router_creator.create();
let mut request = supergraph::Request::fake_builder()
.query(query)
.variables(variables)
.header("x-client-header", "client-header-value")
.build()
.unwrap()
.try_into()
.unwrap();
request_mutator(&mut request);
let response = service
.oneshot(request)
.await
.unwrap()
.next_response()
.await
.unwrap()
.unwrap();
serde_json::from_slice(&response).unwrap()
}