#![cfg(test)]
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use apollo_compiler::Schema;
use futures::StreamExt;
use http::HeaderName;
use http::HeaderValue;
use http::header::CACHE_CONTROL;
use tokio_stream::wrappers::IntervalStream;
use tower::Service;
use tower::ServiceExt;
use uuid::Uuid;
use super::plugin::CacheSubgraph;
use super::plugin::ResponseCache;
use crate::Context;
use crate::MockedSubgraphs;
use crate::TestHarness;
use crate::configuration::subgraph::SubgraphConfiguration;
use crate::graphql;
use crate::metrics::FutureMetricsExt;
use crate::plugin::test::MockSubgraph;
use crate::plugin::test::MockSubgraphService;
use crate::plugins::response_cache::debugger::CacheKeysContext;
use crate::plugins::response_cache::invalidation::InvalidationRequest;
use crate::plugins::response_cache::invalidation_endpoint::SubgraphInvalidationConfig;
use crate::plugins::response_cache::metrics::CacheMetricContextKey;
use crate::plugins::response_cache::plugin::CACHE_DEBUG_HEADER_NAME;
use crate::plugins::response_cache::plugin::CONTEXT_CACHE_KEY;
use crate::plugins::response_cache::plugin::INVALIDATION_SHARED_KEY;
use crate::plugins::response_cache::plugin::Subgraph;
use crate::plugins::response_cache::storage::CacheStorage;
use crate::plugins::response_cache::storage::redis::Config;
use crate::plugins::response_cache::storage::redis::Storage;
use crate::services::subgraph;
use crate::services::supergraph;
const SCHEMA: &str = include_str!("../../testdata/orga_supergraph_cache_key.graphql");
const SCHEMA_CACHE_TAG: &str =
include_str!("../../testdata/orga_supergraph_cache_key_cache_tag.graphql");
const SCHEMA_REQUIRES: &str = include_str!("../../testdata/supergraph_cache_key.graphql");
const SCHEMA_NESTED_KEYS: &str =
include_str!("../../testdata/supergraph_nested_fields_cache_key.graphql");
async fn wait_for_cache(storage: &Storage, keys: Vec<String>) {
if keys.is_empty() {
return;
}
let keys_strs: Vec<&str> = keys.iter().map(|s| s.as_str()).collect();
let mut interval_stream =
IntervalStream::new(tokio::time::interval(Duration::from_millis(100))).take(50);
while interval_stream.next().await.is_some() {
if let Ok(values) = storage.fetch_multiple(&keys_strs, "").await
&& values.iter().all(Option::is_some)
{
return;
}
}
panic!("insert not complete");
}
pub(super) fn create_subgraph_conf(
subgraphs: HashMap<String, Subgraph>,
) -> SubgraphConfiguration<Subgraph> {
SubgraphConfiguration {
all: Subgraph {
invalidation: Some(SubgraphInvalidationConfig {
enabled: true,
shared_key: INVALIDATION_SHARED_KEY.to_string(),
}),
..Default::default()
},
subgraphs,
}
}
fn expected_cached_keys(cache_keys_context: &CacheKeysContext) -> Vec<String> {
cache_keys_context
.iter()
.filter(|context| context.cache_control.should_store())
.map(|context| context.key.clone())
.collect()
}
fn get_cache_keys_context(response: &supergraph::Response) -> Option<CacheKeysContext> {
let mut cache_keys: CacheKeysContext = response
.context
.get(super::plugin::CONTEXT_DEBUG_CACHE_KEYS)
.ok()??;
cache_keys.iter_mut().for_each(|ck| {
ck.invalidation_keys.sort();
ck.cache_control.set_created(0);
});
cache_keys.sort_by(|a, b| a.invalidation_keys.cmp(&b.invalidation_keys));
Some(cache_keys)
}
fn get_cache_control_header(response: &supergraph::Response) -> Option<Vec<String>> {
Some(
response
.response
.headers()
.get(CACHE_CONTROL)?
.to_str()
.ok()?
.split(',')
.map(ToString::to_string)
.collect(),
)
}
fn cache_control_contains_no_store(cache_control_header: &[String]) -> bool {
cache_control_header.iter().any(|h| h == "no-store")
}
fn cache_control_contains_public(cache_control_header: &[String]) -> bool {
cache_control_header.iter().any(|h| h == "public")
}
fn cache_control_contains_private(cache_control_header: &[String]) -> bool {
cache_control_header.iter().any(|h| h == "private")
}
fn cache_control_contains_max_age(cache_control_header: &[String]) -> bool {
cache_control_header
.iter()
.any(|h| h.starts_with("max-age="))
}
fn remove_debug_extensions_key(response: &mut graphql::Response) -> bool {
response
.extensions
.remove(super::plugin::CACHE_DEBUG_EXTENSIONS_KEY)
.is_some()
}
#[tokio::test]
async fn insert() {
let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
let query = "query { currentUser { activeOrganization { id creatorUser { __typename id } } } }";
let subgraphs = serde_json::json!({
"user": {
"query": {
"currentUser": {
"activeOrganization": {
"__typename": "Organization",
"id": "1",
}
}
},
"headers": {"cache-control": "public"},
},
"orga": {
"entities": [
{
"__typename": "Organization",
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
],
"headers": {"cache-control": "public"},
},
});
let (drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false, &Uuid::new_v4().to_string()), drop_rx)
.await
.unwrap();
let subgraphs_conf = create_subgraph_conf(
[
(
"user".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
(
"orga".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect(),
);
let response_cache = ResponseCache::for_test(
storage.clone(),
subgraphs_conf,
valid_schema.clone(),
true,
drop_tx,
)
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({
"include_subgraph_errors": { "all": true },
"experimental_mock_subgraphs": subgraphs,
}))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::with_settings!({
description => "Make sure everything is in status 'new' and we have all the entities and root fields"
}, {
insta::assert_json_snapshot!(cache_keys);
});
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_max_age(&cache_control_header));
assert!(cache_control_contains_public(&cache_control_header));
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
wait_for_cache(&storage, expected_cached_keys(&cache_keys)).await;
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_max_age(&cache_control_header));
assert!(cache_control_contains_public(&cache_control_header));
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::with_settings!({
description => "Make sure everything is in status 'cached' and we have all the entities and root fields"
}, {
insta::assert_json_snapshot!(cache_keys);
});
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
}
#[tokio::test]
async fn insert_with_custom_key() {
let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
let query = "query { currentUser { activeOrganization { id creatorUser { __typename id } } } }";
let subgraphs = serde_json::json!({
"user": {
"query": {
"currentUser": {
"activeOrganization": {
"__typename": "Organization",
"id": "1",
}
}
},
"headers": {"cache-control": "public"},
},
"orga": {
"entities": [
{
"__typename": "Organization",
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
],
"headers": {"cache-control": "public"},
},
});
let (drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false, &Uuid::new_v4().to_string()), drop_rx)
.await
.unwrap();
let map = [
(
"user".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
(
"orga".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let subgraphs_conf = create_subgraph_conf(map);
let response_cache = ResponseCache::for_test(
storage.clone(),
subgraphs_conf,
valid_schema.clone(),
true,
drop_tx,
)
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({
"include_subgraph_errors": { "all": true },
"experimental_mock_subgraphs": subgraphs,
}))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let context = Context::new();
context.insert_json_value(
CONTEXT_CACHE_KEY,
serde_json_bytes::json!({
"all": {
"locale": "be"
},
"subgraphs": {
"user": {
"foo": "bar"
}
}
}),
);
let request = supergraph::Request::fake_builder()
.query(query)
.context(context.clone())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::with_settings!({
description => "Make sure everything is with source 'subgraph' and we have all the entities and root fields"
}, {
insta::assert_json_snapshot!(cache_keys);
});
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_max_age(&cache_control_header));
assert!(cache_control_contains_public(&cache_control_header));
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
wait_for_cache(&storage, expected_cached_keys(&cache_keys)).await;
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs, }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_max_age(&cache_control_header));
assert!(cache_control_contains_public(&cache_control_header));
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::with_settings!({
description => "Make sure everything is with source 'subgraph' because we didn't pass the context and we have all the entities and root fields"
}, {
insta::assert_json_snapshot!(cache_keys);
});
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
}
#[tokio::test]
async fn already_expired_cache_control() {
let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
let query = "query { currentUser { activeOrganization { id creatorUser { __typename id } } } }";
let subgraphs = serde_json::json!({
"user": {
"query": {
"currentUser": {
"activeOrganization": {
"__typename": "Organization",
"id": "1",
}
}
},
"headers": {"cache-control": "public", "age": "5"},
},
"orga": {
"entities": [
{
"__typename": "Organization",
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
],
"headers": {"cache-control": "public", "age": "1000000"},
},
});
let (drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false, &Uuid::new_v4().to_string()), drop_rx)
.await
.unwrap();
let map = [
(
"user".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
(
"orga".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let subgraphs_conf = create_subgraph_conf(map);
let response_cache = ResponseCache::for_test(
storage.clone(),
subgraphs_conf,
valid_schema.clone(),
true,
drop_tx,
)
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({
"include_subgraph_errors": { "all": true },
"experimental_mock_subgraphs": subgraphs,
}))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::with_settings!({
description => "Make sure everything is in status 'new' and we have all the entities and root fields"
}, {
insta::assert_json_snapshot!(cache_keys);
});
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_public(&cache_control_header));
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
wait_for_cache(&storage, expected_cached_keys(&cache_keys)).await;
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::with_settings!({
description => "Make sure only root field query is in status 'cached' and entities are not cached"
}, {
insta::assert_json_snapshot!(cache_keys);
});
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
}
#[tokio::test]
async fn insert_without_debug_header() {
let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
let query = "query { currentUser { activeOrganization { id creatorUser { __typename id } } } }";
let subgraphs = serde_json::json!({
"user": {
"query": {
"currentUser": {
"activeOrganization": {
"__typename": "Organization",
"id": "1",
}
}
},
"headers": {"cache-control": "public"},
},
"orga": {
"entities": [
{
"__typename": "Organization",
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
],
"headers": {"cache-control": "public"},
},
});
let (drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false, &Uuid::new_v4().to_string()), drop_rx)
.await
.unwrap();
let map = [
(
"user".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
(
"orga".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let subgraphs_conf = create_subgraph_conf(map);
let response_cache = ResponseCache::for_test(
storage.clone(),
subgraphs_conf,
valid_schema.clone(),
true,
drop_tx,
)
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({
"include_subgraph_errors": { "all": true },
"experimental_mock_subgraphs": subgraphs,
}))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
assert!(get_cache_keys_context(&response).is_none());
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_max_age(&cache_control_header));
assert!(cache_control_contains_public(&cache_control_header));
let mut response = response.next_response().await.unwrap();
assert!(!remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_max_age(&cache_control_header));
assert!(cache_control_contains_public(&cache_control_header));
assert!(get_cache_keys_context(&response).is_none());
let mut response = response.next_response().await.unwrap();
assert!(!remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
}
#[tokio::test]
async fn insert_with_requires() {
let valid_schema =
Arc::new(Schema::parse_and_validate(SCHEMA_REQUIRES, "test.graphql").unwrap());
let query = "query { topProducts { name shippingEstimate price } }";
let subgraphs = MockedSubgraphs([
("products", MockSubgraph::builder().with_json(
serde_json::json! {{"query":"{ topProducts { __typename upc name price weight } }"}},
serde_json::json! {{"data": {"topProducts": [{
"__typename": "Product",
"upc": "1",
"name": "Test",
"price": 150,
"weight": 5
}]}}},
).with_header(CACHE_CONTROL, HeaderValue::from_static("public")).build()),
("inventory", MockSubgraph::builder().with_json(
serde_json::json! {{
"query": "query($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { shippingEstimate } } }",
"variables": {
"representations": [
{
"weight": 5,
"upc": "1",
"price": 150,
"__typename": "Product"
}
]
}}},
serde_json::json! {{"data": {
"_entities": [{
"shippingEstimate": 15
}]
}}},
).with_header(CACHE_CONTROL, HeaderValue::from_static("public")).build())
].into_iter().collect());
let (drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false, &Uuid::new_v4().to_string()), drop_rx)
.await
.unwrap();
let map: HashMap<String, Subgraph> = [
(
"products".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
(
"inventory".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let subgraphs_conf = create_subgraph_conf(map);
let response_cache = ResponseCache::for_test(
storage.clone(),
subgraphs_conf,
valid_schema.clone(),
true,
drop_tx,
)
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
.unwrap()
.schema(SCHEMA_REQUIRES)
.extra_private_plugin(response_cache.clone())
.extra_plugin(subgraphs.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::with_settings!({
description => "Make sure everything is in status 'new' and we have all the entities and root fields"
}, {
insta::assert_json_snapshot!(cache_keys);
});
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_max_age(&cache_control_header));
assert!(cache_control_contains_public(&cache_control_header));
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"topProducts": [
{
"name": "Test",
"shippingEstimate": 15,
"price": 150
}
]
}
}
"#);
wait_for_cache(&storage, expected_cached_keys(&cache_keys)).await;
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
.unwrap()
.schema(SCHEMA_REQUIRES)
.extra_private_plugin(response_cache)
.extra_plugin(subgraphs.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::with_settings!({
description => "Make sure everything is in status 'cached' and we have all the entities and root fields"
}, {
insta::assert_json_snapshot!(cache_keys);
});
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_max_age(&cache_control_header));
assert!(cache_control_contains_public(&cache_control_header));
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"topProducts": [
{
"name": "Test",
"shippingEstimate": 15,
"price": 150
}
]
}
}
"#);
}
#[tokio::test]
async fn insert_with_nested_field_set() {
let valid_schema =
Arc::new(Schema::parse_and_validate(SCHEMA_NESTED_KEYS, "test.graphql").unwrap());
let query = "query { allProducts { name createdBy { name country { a } } } }";
let subgraphs = serde_json::json!({
"products": {
"query": {"allProducts": [{
"id": "1",
"name": "Test",
"sku": "150",
"createdBy": { "__typename": "User", "email": "test@test.com", "country": {"a": "France"} }
}]},
"headers": {"cache-control": "public"},
},
"users": {
"entities": [{
"__typename": "User",
"email": "test@test.com",
"name": "test",
"country": {
"a": "France"
}
}],
"headers": {"cache-control": "public"},
}
});
let (drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false, &Uuid::new_v4().to_string()), drop_rx)
.await
.unwrap();
let map = [
(
"products".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
(
"users".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let subgraphs_conf = create_subgraph_conf(map);
let response_cache = ResponseCache::for_test(
storage.clone(),
subgraphs_conf,
valid_schema.clone(),
true,
drop_tx,
)
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() }))
.unwrap()
.schema(SCHEMA_NESTED_KEYS)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::with_settings!({
description => "Make sure everything is in status 'new' and we have all the entities and root fields"
}, {
insta::assert_json_snapshot!(cache_keys);
});
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_max_age(&cache_control_header));
assert!(cache_control_contains_public(&cache_control_header));
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"allProducts": [
{
"name": "Test",
"createdBy": {
"name": "test",
"country": {
"a": "France"
}
}
}
]
}
}
"#);
wait_for_cache(&storage, expected_cached_keys(&cache_keys)).await;
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() }))
.unwrap()
.schema(SCHEMA_NESTED_KEYS)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_max_age(&cache_control_header));
assert!(cache_control_contains_public(&cache_control_header));
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::with_settings!({
description => "Make sure everything is in status 'cached' and we have all the entities and root fields"
}, {
insta::assert_json_snapshot!(cache_keys);
});
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"allProducts": [
{
"name": "Test",
"createdBy": {
"name": "test",
"country": {
"a": "France"
}
}
}
]
}
}
"#);
}
#[tokio::test]
async fn no_cache_control() {
let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
let query = "query { currentUser { activeOrganization { id creatorUser { __typename id } } } }";
let subgraphs = serde_json::json!({
"user": {
"query": {
"currentUser": {
"activeOrganization": {
"__typename": "Organization",
"id": "1",
}
}
}
},
"orga": {
"entities": [
{
"__typename": "Organization",
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
]
},
});
let (drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false, &Uuid::new_v4().to_string()), drop_rx)
.await
.unwrap();
let response_cache = ResponseCache::for_test(
storage.clone(),
Default::default(),
valid_schema.clone(),
false,
drop_tx,
)
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_no_store(&cache_control_header));
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_no_store(&cache_control_header));
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
}
#[tokio::test]
async fn no_store_from_request() {
let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
let query = "query { currentUser { activeOrganization { id creatorUser { __typename id } } } }";
let subgraphs = serde_json::json!({
"user": {
"query": {
"currentUser": {
"activeOrganization": {
"__typename": "Organization",
"id": "1",
}
}
}
},
"orga": {
"entities": [
{
"__typename": "Organization",
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
]
},
});
let (drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false, &Uuid::new_v4().to_string()), drop_rx)
.await
.unwrap();
let response_cache = ResponseCache::for_test(
storage.clone(),
Default::default(),
valid_schema.clone(),
false,
drop_tx,
)
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone(), "headers": {
"all": {
"request": [{
"propagate": {
"named": "cache-control"
}
}]
}
} }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.header(CACHE_CONTROL, HeaderValue::from_static("no-store"))
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_no_store(&cache_control_header));
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
let invalidations_by_subgraph = storage
.invalidate(
vec![
"user".to_string(),
"organization".to_string(),
"currentUser".to_string(),
],
vec!["orga".to_string(), "user".to_string()],
"test_bulk_invalidation",
)
.await
.unwrap();
assert_eq!(invalidations_by_subgraph.into_values().sum::<u64>(), 0);
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone(), "headers": {
"all": {
"request": [{
"propagate": {
"named": "cache-control"
}
}]
}
} }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.header(CACHE_CONTROL, HeaderValue::from_static("no-store"))
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_no_store(&cache_control_header));
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
let invalidations_by_subgraph = storage
.invalidate(
vec![
"user".to_string(),
"organization".to_string(),
"currentUser".to_string(),
],
vec!["orga".to_string(), "user".to_string()],
"test_bulk_invalidate",
)
.await
.unwrap();
assert_eq!(invalidations_by_subgraph.into_values().sum::<u64>(), 0);
}
#[tokio::test]
async fn no_cache_from_request() {
let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
let query = "query { currentUser { activeOrganization { id creatorUser { __typename id } } } }";
let subgraphs = serde_json::json!({
"user": {
"query": {
"currentUser": {
"activeOrganization": {
"__typename": "Organization",
"id": "1",
}
}
}
},
"orga": {
"entities": [
{
"__typename": "Organization",
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
]
},
});
let (drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false, &Uuid::new_v4().to_string()), drop_rx)
.await
.unwrap();
let response_cache = ResponseCache::for_test(
storage.clone(),
Default::default(),
valid_schema.clone(),
false,
drop_tx,
)
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone(), "headers": {
"all": {
"request": [{
"propagate": {
"named": "cache-control"
}
}]
}
} }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let response = response.next_response().await.unwrap();
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone(), "headers": {
"all": {
"request": [{
"propagate": {
"named": "cache-control"
}
}]
}
} }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let no_cache_context = Context::new();
let request = supergraph::Request::fake_builder()
.query(query)
.context(no_cache_context.clone())
.header(CACHE_CONTROL, HeaderValue::from_static("no-cache"))
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let response = response.next_response().await.unwrap();
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
let orga_metric = no_cache_context
.get::<_, CacheSubgraph>(CacheMetricContextKey::new("orga".to_string()))
.ok()
.flatten();
assert!(
orga_metric.is_none(),
"no-cache requests should not record cache hit/miss metrics"
);
}
#[tokio::test]
async fn private_only() {
async {
let query = "query { currentUser { activeOrganization { id creatorUser { __typename id } } } }";
let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
let subgraphs = serde_json::json!({
"user": {
"query": {
"currentUser": {
"activeOrganization": {
"__typename": "Organization",
"id": "1",
}
}
},
"headers": {"cache-control": "private"},
},
"orga": {
"entities": [
{
"__typename": "Organization",
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
],
"headers": {"cache-control": "private"},
},
});
let (drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false,"private_only"), drop_rx)
.await
.unwrap();
let map = [
(
"user".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
(
"orga".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let subgraphs_conf = create_subgraph_conf(map);
let response_cache =
ResponseCache::for_test(storage.clone(), subgraphs_conf, valid_schema.clone(), true, drop_tx)
.await
.unwrap();
let mut service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let context = Context::new();
context.insert_json_value("sub", "1234".into());
let request = supergraph::Request::fake_builder()
.query(query)
.context(context)
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.ready().await.unwrap().call(request).await.unwrap();
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys);
assert_gauge!("apollo.router.response_cache.private_queries.lru.size", 1);
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
let mut service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let context = Context::new();
context.insert_json_value("sub", "1234".into());
let request = supergraph::Request::fake_builder()
.query(query)
.context(context)
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.ready().await.unwrap().call(request).await.unwrap();
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_private(&cache_control_header));
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys);
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
let context = Context::new();
context.insert_json_value("sub", "5678".into());
let request = supergraph::Request::fake_builder()
.query(query)
.context(context)
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.ready().await.unwrap().call(request).await.unwrap();
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_private(&cache_control_header));
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys);
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
}.with_metrics().await;
}
#[tokio::test]
async fn private_and_public() {
let query = "query { currentUser { activeOrganization { id creatorUser { __typename id } } } orga(id: \"2\") { name } }";
let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
let subgraphs = serde_json::json!({
"user": {
"query": {
"currentUser": {
"activeOrganization": {
"__typename": "Organization",
"id": "1",
}
}
},
"headers": {"cache-control": "public"},
},
"orga": {
"query": {
"orga": {
"__typename": "Organization",
"id": "2",
"name": "test_orga"
}
},
"entities": [
{
"__typename": "Organization",
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
],
"headers": {"cache-control": "private"},
},
});
let (drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false, &Uuid::new_v4().to_string()), drop_rx)
.await
.unwrap();
let map = [
(
"user".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
(
"orga".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let subgraphs_conf = create_subgraph_conf(map);
let response_cache = ResponseCache::for_test(
storage.clone(),
subgraphs_conf,
valid_schema.clone(),
true,
drop_tx,
)
.await
.unwrap();
let mut service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let context = Context::new();
context.insert_json_value("sub", "1234".into());
let request = supergraph::Request::fake_builder()
.query(query)
.context(context)
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.ready().await.unwrap().call(request).await.unwrap();
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys);
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
},
"orga": {
"name": "test_orga"
}
}
}
"#);
let mut service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let context = Context::new();
context.insert_json_value("sub", "1234".into());
let request = supergraph::Request::fake_builder()
.query(query)
.context(context)
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.ready().await.unwrap().call(request).await.unwrap();
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_private(&cache_control_header));
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys);
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
},
"orga": {
"name": "test_orga"
}
}
}
"#);
let context = Context::new();
context.insert_json_value("sub", "5678".into());
let request = supergraph::Request::fake_builder()
.query(query)
.context(context)
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.ready().await.unwrap().call(request).await.unwrap();
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_private(&cache_control_header));
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys);
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
},
"orga": {
"name": "test_orga"
}
}
}
"#);
}
#[tokio::test]
async fn polymorphic_private_and_public() {
async {
let query = "query { currentUser { activeOrganization { id creatorUser { __typename id } } } orga(id: \"2\") { name } }";
let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
let subgraphs = serde_json::json!({
"user": {
"query": {
"currentUser": {
"activeOrganization": {
"__typename": "Organization",
"id": "1",
}
}
},
"headers": {"cache-control": "public"},
},
"orga": {
"query": {
"orga": {
"__typename": "Organization",
"id": "2",
"name": "test_orga"
}
},
"entities": [
{
"__typename": "Organization",
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
],
"headers": {"cache-control": "private"},
},
});
let (drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false,"polymorphic_private_and_public"), drop_rx)
.await
.unwrap();
let map = [
(
"user".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
(
"orga".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let subgraphs_conf = create_subgraph_conf(map);
let response_cache =
ResponseCache::for_test(storage.clone(), subgraphs_conf, valid_schema.clone(), true, drop_tx)
.await
.unwrap();
let mut service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let context = Context::new();
context.insert_json_value("sub", "1234".into());
let request = supergraph::Request::fake_builder()
.query(query)
.context(context)
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.ready().await.unwrap().call(request).await.unwrap();
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::with_settings!({
description => "Make sure everything is in status 'new' and we have all the entities and root fields"
}, {
insta::assert_json_snapshot!(cache_keys);
});
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
},
"orga": {
"name": "test_orga"
}
}
}
"#);
let subgraphs_public = serde_json::json!({
"user": {
"query": {
"currentUser": {
"activeOrganization": {
"__typename": "Organization",
"id": "1",
}
}
},
"headers": {"cache-control": "public"},
},
"orga": {
"query": {
"orga": {
"__typename": "Organization",
"id": "2",
"name": "test_orga_public"
}
},
"entities": [
{
"__typename": "Organization",
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 3
}
}
],
"headers": {"cache-control": "public"},
},
});
let mut service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs_public.clone() }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let context = Context::new();
let request = supergraph::Request::fake_builder()
.query(query)
.context(context)
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.ready().await.unwrap().call(request).await.unwrap();
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_public(&cache_control_header));
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys);
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 3
}
}
},
"orga": {
"name": "test_orga_public"
}
}
}
"#);
let mut service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let context = Context::new();
context.insert_json_value("sub", "1234".into());
let request = supergraph::Request::fake_builder()
.query(query)
.context(context)
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.ready().await.unwrap().call(request).await.unwrap();
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_private(&cache_control_header));
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys);
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
},
"orga": {
"name": "test_orga"
}
}
}
"#);
let mut service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs_public.clone() }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let context = Context::new();
let request = supergraph::Request::fake_builder()
.query(query)
.context(context)
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.ready().await.unwrap().call(request).await.unwrap();
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_public(&cache_control_header));
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys);
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 3
}
}
},
"orga": {
"name": "test_orga_public"
}
}
}
"#);
assert_gauge!("apollo.router.response_cache.private_queries.lru.size", 1);
let context = Context::new();
context.insert_json_value("sub", "1234".into());
let request = supergraph::Request::fake_builder()
.query(query)
.context(context)
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.ready().await.unwrap().call(request).await.unwrap();
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_private(&cache_control_header));
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys);
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
},
"orga": {
"name": "test_orga"
}
}
}
"#);
assert_gauge!("apollo.router.response_cache.private_queries.lru.size", 1);
let mut service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let context = Context::new();
let request = supergraph::Request::fake_builder()
.query(query)
.context(context)
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.ready().await.unwrap().call(request).await.unwrap();
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_public(&cache_control_header));
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys);
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 3
}
}
},
"orga": {
"name": "test_orga_public"
}
}
}
"#);
assert_gauge!("apollo.router.response_cache.private_queries.lru.size", 1);
}.with_metrics().await;
}
#[tokio::test]
async fn private_without_private_id() {
async {
let query = "query { currentUser { activeOrganization { id creatorUser { __typename id } } } }";
let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
let subgraphs = serde_json::json!({
"user": {
"query": {
"currentUser": {
"activeOrganization": {
"__typename": "Organization",
"id": "1",
}
}
},
"headers": {"cache-control": "private"},
},
"orga": {
"entities": [
{
"__typename": "Organization",
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
],
"headers": {"cache-control": "private"},
},
});
let (drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false,"private_without_private_id"), drop_rx)
.await
.unwrap();
let map = [
(
"user".to_string(),
Subgraph {
redis: None,
enabled: true.into(),
ttl: None,
..Default::default()
},
),
(
"orga".to_string(),
Subgraph {
redis: None,
enabled: true.into(),
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let subgraphs_conf = create_subgraph_conf(map);
let response_cache =
ResponseCache::for_test(storage.clone(), subgraphs_conf, valid_schema.clone(), true, drop_tx)
.await
.unwrap();
let mut service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let context = Context::new();
let request = supergraph::Request::fake_builder()
.query(query)
.context(context)
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.ready().await.unwrap().call(request).await.unwrap();
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_private(&cache_control_header));
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys);
assert_gauge!("apollo.router.response_cache.private_queries.lru.size", 1);
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
let mut service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let context = Context::new();
let request = supergraph::Request::fake_builder()
.query(query)
.context(context)
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.ready().await.unwrap().call(request).await.unwrap();
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_private(&cache_control_header));
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys);
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
}.with_metrics().await;
}
#[tokio::test]
async fn no_data() {
let query = "query { currentUser { allOrganizations { id name } } }";
let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
let subgraphs = MockedSubgraphs([
("user", MockSubgraph::builder().with_json(
serde_json::json! {{"query":"{currentUser{allOrganizations{__typename id}}}"}},
serde_json::json! {{"data": {"currentUser": { "allOrganizations": [
{
"__typename": "Organization",
"id": "1"
},
{
"__typename": "Organization",
"id": "3"
}
] }}}},
).with_header(CACHE_CONTROL, HeaderValue::from_static("no-store")).build()),
("orga", MockSubgraph::builder().with_json(
serde_json::json! {{
"query": "query($representations:[_Any!]!){_entities(representations:$representations){...on Organization{name}}}",
"variables": {
"representations": [
{
"id": "1",
"__typename": "Organization",
},
{
"id": "3",
"__typename": "Organization",
}
]
}}},
serde_json::json! {{
"data": {
"_entities": [{
"name": "Organization 1",
},
{
"name": "Organization 3"
}]
}
}},
).with_header(CACHE_CONTROL, HeaderValue::from_static("public, max-age=3600")).build())
].into_iter().collect());
let (drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false, &Uuid::new_v4().to_string()), drop_rx)
.await
.unwrap();
let map = [
(
"user".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
(
"orga".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let subgraphs_conf = create_subgraph_conf(map);
let response_cache = ResponseCache::for_test(
storage.clone(),
subgraphs_conf,
valid_schema.clone(),
true,
drop_tx,
)
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.extra_plugin(subgraphs)
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys, {
"[].cache_control" => insta::dynamic_redaction(|value, _path| {
let cache_control = value.as_str().unwrap().to_string();
assert!(cache_control.contains("max-age="));
assert!(cache_control.contains("public"));
"[REDACTED]"
})
});
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"allOrganizations": [
{
"id": "1",
"name": "Organization 1"
},
{
"id": "3",
"name": "Organization 3"
}
]
}
}
}
"#);
let subgraphs = MockedSubgraphs(
[(
"user",
MockSubgraph::builder()
.with_json(
serde_json::json! {{"query":"{currentUser{allOrganizations{__typename id}}}"}},
serde_json::json! {{"data": {"currentUser": { "allOrganizations": [
{
"__typename": "Organization",
"id": "1"
},
{
"__typename": "Organization",
"id": "2"
},
{
"__typename": "Organization",
"id": "3"
}
] }}}},
)
.with_header(CACHE_CONTROL, HeaderValue::from_static("no-store"))
.build(),
)]
.into_iter()
.collect(),
);
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache)
.subgraph_hook(|name, service| {
if name == "orga" {
let mut subgraph = MockSubgraphService::new();
subgraph
.expect_call()
.times(1)
.returning(move |_req: subgraph::Request| Err("orga not found".into()));
subgraph.boxed()
} else {
service
}
})
.extra_plugin(subgraphs)
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys);
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"allOrganizations": [
{
"id": "1",
"name": "Organization 1"
},
{
"id": "2",
"name": null
},
{
"id": "3",
"name": "Organization 3"
}
]
}
},
"errors": [
{
"message": "HTTP fetch failed from 'orga': orga not found",
"path": [
"currentUser",
"allOrganizations",
1
],
"extensions": {
"code": "SUBREQUEST_HTTP_ERROR",
"service": "orga",
"reason": "orga not found"
}
}
]
}
"#);
}
#[tokio::test]
async fn missing_entities() {
let query = "query { currentUser { allOrganizations { id name } } }";
let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
let subgraphs = MockedSubgraphs([
("user", MockSubgraph::builder().with_json(
serde_json::json! {{"query":"{currentUser{allOrganizations{__typename id}}}"}},
serde_json::json! {{"data": {"currentUser": { "allOrganizations": [
{
"__typename": "Organization",
"id": "1"
},
{
"__typename": "Organization",
"id": "2"
}
] }}}},
).with_header(CACHE_CONTROL, HeaderValue::from_static("no-store")).build()),
("orga", MockSubgraph::builder().with_json(
serde_json::json! {{
"query": "query($representations:[_Any!]!){_entities(representations:$representations){...on Organization{name}}}",
"variables": {
"representations": [
{
"id": "1",
"__typename": "Organization",
},
{
"id": "2",
"__typename": "Organization",
}
]
}}},
serde_json::json! {{
"data": {
"_entities": [
{
"name": "Organization 1",
},
{
"name": "Organization 2"
}
]
}
}},
).with_header(CACHE_CONTROL, HeaderValue::from_static("public, max-age=3600")).build())
].into_iter().collect());
let map = [
(
"user".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
(
"orga".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let subgraphs_conf = create_subgraph_conf(map);
let namespace = Uuid::new_v4().to_string();
let (drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false, &namespace), drop_rx)
.await
.unwrap();
let response_cache = ResponseCache::for_test(
storage.clone(),
subgraphs_conf,
valid_schema.clone(),
true,
drop_tx,
)
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.extra_plugin(subgraphs)
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response);
let (drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false, &namespace), drop_rx)
.await
.unwrap();
let response_cache = ResponseCache::for_test(
storage.clone(),
Default::default(),
valid_schema.clone(),
false,
drop_tx,
)
.await
.unwrap();
let subgraphs = MockedSubgraphs([
("user", MockSubgraph::builder().with_json(
serde_json::json! {{"query":"{currentUser{allOrganizations{__typename id}}}"}},
serde_json::json! {{"data": {"currentUser": { "allOrganizations": [
{
"__typename": "Organization",
"id": "1"
},
{
"__typename": "Organization",
"id": "2"
},
{
"__typename": "Organization",
"id": "3"
}
] }}}},
).with_header(CACHE_CONTROL, HeaderValue::from_static("no-store")).build()),
("orga", MockSubgraph::builder().with_json(
serde_json::json! {{
"query": "query($representations:[_Any!]!){_entities(representations:$representations){...on Organization{name}}}",
"variables": {
"representations": [
{
"id": "3",
"__typename": "Organization",
}
]
}}},
serde_json::json! {{
"data": null,
"errors": [{
"message": "Organization not found",
}]
}},
).with_header(CACHE_CONTROL, HeaderValue::from_static("public, max-age=3600")).build())
].into_iter().collect());
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache)
.extra_plugin(subgraphs)
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response);
}
#[tokio::test(flavor = "multi_thread")]
async fn invalidate_by_cache_tag() {
async move {
let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
let query = "query { currentUser { activeOrganization { id creatorUser { __typename id } } } }";
let subgraphs = serde_json::json!({
"user": {
"query": {
"currentUser": {
"activeOrganization": {
"__typename": "Organization",
"id": "1",
}
}
},
"headers": {"cache-control": "public"},
},
"orga": {
"entities": [
{
"__typename": "Organization",
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
],
"headers": {"cache-control": "public"},
},
});
let (drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false,"test_invalidate_by_cache_tag"), drop_rx)
.await
.unwrap();
let map = [
(
"user".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
(
"orga".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let subgraphs_conf = create_subgraph_conf(map);
let response_cache =
ResponseCache::for_test(storage.clone(), subgraphs_conf, valid_schema.clone(), true, drop_tx)
.await
.unwrap();
let invalidation = response_cache.invalidation.clone();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys);
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_max_age(&cache_control_header));
assert!(cache_control_contains_public(&cache_control_header));
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
assert_histogram_sum!("apollo.router.operations.response_cache.fetch.entity", 1u64, "subgraph.name" = "orga");
wait_for_cache(&storage, expected_cached_keys(&cache_keys)).await;
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.clone().oneshot(request).await.unwrap();
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys);
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_max_age(&cache_control_header));
assert!(cache_control_contains_public(&cache_control_header));
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
assert_histogram_sum!("apollo.router.operations.response_cache.fetch.entity", 2u64, "subgraph.name" = "orga");
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
let res = invalidation
.invalidate(vec![InvalidationRequest::CacheTag {
subgraphs: vec!["orga".to_string()].into_iter().collect(),
cache_tag: String::from("organization-1"),
}])
.await
.unwrap();
assert_eq!(res, 1);
assert_counter!("apollo.router.operations.response_cache.invalidation.entry", 1u64, "subgraph.name" = "orga", "kind" = "cache_tag", "cache.tag" = "organization-1");
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache)
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.clone().oneshot(request).await.unwrap();
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys);
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_max_age(&cache_control_header));
assert!(cache_control_contains_public(&cache_control_header));
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
assert_histogram_sum!("apollo.router.operations.response_cache.fetch.entity", 3u64, "subgraph.name" = "orga");
}.with_metrics().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn complex_cache_tag() {
async move {
let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA_CACHE_TAG, "test.graphql").unwrap());
let query = "query { currentUser { activeOrganization { ... on Organization { id creatorUser { __typename id } } } } }";
let subgraphs = serde_json::json!({
"user": {
"query": {
"currentUser": {
"activeOrganization": {
"__typename": "Organization",
"id": "1",
}
}
},
"headers": {"cache-control": "public"},
},
"orga": {
"entities": [
{
"__typename": "Organization",
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
],
"headers": {"cache-control": "public"},
},
});
let (drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false,"test_complex_cache_tag"), drop_rx)
.await
.unwrap();
let map = [
(
"user".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
(
"orga".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let subgraphs_conf = create_subgraph_conf(map);
let response_cache =
ResponseCache::for_test(storage.clone(), subgraphs_conf, valid_schema.clone(), true, drop_tx)
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys);
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_max_age(&cache_control_header));
assert!(cache_control_contains_public(&cache_control_header));
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
}.with_metrics().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn invalidate_by_type() {
async move {
let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
let query = "query { currentUser { activeOrganization { id creatorUser { __typename id } } } }";
let subgraphs = serde_json::json!({
"user": {
"query": {
"currentUser": {
"activeOrganization": {
"__typename": "Organization",
"id": "1",
}
}
},
"headers": {"cache-control": "public"},
},
"orga": {
"entities": [
{
"__typename": "Organization",
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
],
"headers": {"cache-control": "public"},
},
});
let (drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false,"test_invalidate_by_subgraph"), drop_rx)
.await
.unwrap();
let map = [
(
"user".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
(
"orga".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let subgraphs_conf = create_subgraph_conf(map);
let response_cache =
ResponseCache::for_test(storage.clone(), subgraphs_conf, valid_schema.clone(), true, drop_tx)
.await
.unwrap();
let invalidation = response_cache.invalidation.clone();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys);
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_max_age(&cache_control_header));
assert!(cache_control_contains_public(&cache_control_header));
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
wait_for_cache(&storage, expected_cached_keys(&cache_keys)).await;
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.clone().oneshot(request).await.unwrap();
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys);
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_max_age(&cache_control_header));
assert!(cache_control_contains_public(&cache_control_header));
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
let res = invalidation
.invalidate(vec![InvalidationRequest::Type { subgraph: "orga".to_string(), r#type: "Organization".to_string() }])
.await
.unwrap();
assert_eq!(res, 1);
assert_counter!("apollo.router.operations.response_cache.invalidation.entry", 1u64, "subgraph.name" = "orga", "graphql.type" = "Organization", "kind" = "type");
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() }))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache)
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.clone().oneshot(request).await.unwrap();
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::assert_json_snapshot!(cache_keys);
let cache_control_header = get_cache_control_header(&response).expect("missing header");
assert!(cache_control_contains_max_age(&cache_control_header));
assert!(cache_control_contains_public(&cache_control_header));
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
}.with_metrics().await;
}
#[tokio::test]
async fn failure_mode() {
async {
let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
let query =
"query { currentUser { activeOrganization { id creatorUser { __typename id } } } }";
let subgraphs = serde_json::json!({
"user": {
"query": {
"currentUser": {
"activeOrganization": {
"__typename": "Organization",
"id": "1",
}
}
},
"headers": {"cache-control": "public"},
},
"orga": {
"entities": [
{
"__typename": "Organization",
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
],
"headers": {"cache-control": "public"},
},
});
let map = [
(
"user".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
(
"orga".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let response_cache =
ResponseCache::without_storage_for_failure_mode(map, valid_schema.clone())
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({
"include_subgraph_errors": { "all": true },
"experimental_mock_subgraphs": subgraphs.clone(),
}))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let response = response.next_response().await.unwrap();
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
assert_counter!(
"apollo.router.operations.response_cache.fetch.error",
1,
"subgraph.name" = "orga",
"code" = "NO_STORAGE"
);
assert_counter!(
"apollo.router.operations.response_cache.fetch.error",
1,
"subgraph.name" = "user",
"code" = "NO_STORAGE"
);
let service = TestHarness::builder()
.configuration_json(
serde_json::json!({"include_subgraph_errors": { "all": true },
"experimental_mock_subgraphs": subgraphs.clone(),
}),
)
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let response = response.next_response().await.unwrap();
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
assert_counter!(
"apollo.router.operations.response_cache.fetch.error",
2,
"subgraph.name" = "orga",
"code" = "NO_STORAGE"
);
assert_counter!(
"apollo.router.operations.response_cache.fetch.error",
2,
"subgraph.name" = "user",
"code" = "NO_STORAGE"
);
}
.with_metrics()
.await;
}
#[tokio::test]
async fn failure_mode_reconnect() {
async {
let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
let query =
"query { currentUser { activeOrganization { id creatorUser { __typename id } } } }";
let subgraphs = serde_json::json!({
"user": {
"query": {
"currentUser": {
"activeOrganization": {
"__typename": "Organization",
"id": "1",
}
}
},
"headers": {"cache-control": "public"},
},
"orga": {
"entities": [
{
"__typename": "Organization",
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
],
"headers": {"cache-control": "public"},
},
});
let map = [
(
"user".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
(
"orga".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true.into(),
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let (_drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false,"failure_mode_reconnect"), drop_rx)
.await
.unwrap();
storage.truncate_namespace().await.unwrap();
let response_cache =
ResponseCache::without_storage_for_failure_mode(map, valid_schema.clone())
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({
"include_subgraph_errors": { "all": true },
"experimental_mock_subgraphs": subgraphs.clone(),
}))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let response = response.next_response().await.unwrap();
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
assert_counter!(
"apollo.router.operations.response_cache.fetch.error",
1,
"subgraph.name" = "orga",
"code" = "NO_STORAGE"
);
assert_counter!(
"apollo.router.operations.response_cache.fetch.error",
1,
"subgraph.name" = "user",
"code" = "NO_STORAGE"
);
let service = TestHarness::builder()
.configuration_json(
serde_json::json!({"include_subgraph_errors": { "all": true },
"experimental_mock_subgraphs": subgraphs.clone(),
}),
)
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
response_cache
.storage.replace_storage(storage).expect("must be able to replace");
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::with_settings!({
description => "Make sure everything is in status 'new' and we have all the entities and root fields"
}, {
insta::assert_json_snapshot!(cache_keys);
});
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
assert_counter!(
"apollo.router.operations.response_cache.fetch.error",
1,
"subgraph.name" = "orga",
"code" = "NO_STORAGE"
);
assert_counter!(
"apollo.router.operations.response_cache.fetch.error",
1,
"subgraph.name" = "user",
"code" = "NO_STORAGE"
);
let service = TestHarness::builder()
.configuration_json(
serde_json::json!({"include_subgraph_errors": { "all": true },
"experimental_mock_subgraphs": subgraphs.clone(),
}),
)
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.header(
HeaderName::from_static(CACHE_DEBUG_HEADER_NAME),
HeaderValue::from_static("true"),
)
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_keys = get_cache_keys_context(&response).expect("missing cache keys");
insta::with_settings!({
description => "Make sure everything is in status 'cached' and we have all the entities and root fields"
}, {
insta::assert_json_snapshot!(cache_keys);
});
let mut response = response.next_response().await.unwrap();
assert!(remove_debug_extensions_key(&mut response));
insta::assert_json_snapshot!(response, @r#"
{
"data": {
"currentUser": {
"activeOrganization": {
"id": "1",
"creatorUser": {
"__typename": "User",
"id": 2
}
}
}
}
}
"#);
assert_counter!(
"apollo.router.operations.response_cache.fetch.error",
1,
"subgraph.name" = "orga",
"code" = "NO_STORAGE"
);
assert_counter!(
"apollo.router.operations.response_cache.fetch.error",
1,
"subgraph.name" = "user",
"code" = "NO_STORAGE"
);
}
.with_metrics()
.await;
}
#[tokio::test(flavor = "multi_thread")]
async fn no_store_on_subgraph_timeout() {
let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
let query = "query { currentUser { activeOrganization { id creatorUser { __typename id } } } }";
let subgraphs = serde_json::json!({
"user": {
"query": {
"currentUser": {
"activeOrganization": {
"__typename": "Organization",
"id": "1",
}
}
},
"headers": {"cache-control": "max-age=1800, public"},
},
"orga": {
"entities": [],
},
});
let (drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false, &Uuid::new_v4().to_string()), drop_rx)
.await
.unwrap();
let subgraphs_conf = create_subgraph_conf(HashMap::from([
("user".to_string(), Subgraph::default()),
("orga".to_string(), Subgraph::default()),
]));
let response_cache = ResponseCache::for_test(
storage.clone(),
subgraphs_conf,
valid_schema.clone(),
true,
drop_tx,
)
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({
"include_subgraph_errors": { "all": true },
"experimental_mock_subgraphs": subgraphs,
"traffic_shaping": {
"subgraphs": {
"orga": {
"timeout": "1ms"
}
}
}
}))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.subgraph_hook(|name, service| {
if name == "orga" {
tower::service_fn(|_req: subgraph::Request| async move {
tokio::time::sleep(Duration::from_millis(500)).await;
Err::<subgraph::Response, tower::BoxError>("orga sleep exceeded".into())
})
.boxed()
} else {
service
}
})
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_control_header =
get_cache_control_header(&response).expect("missing cache-control header");
assert!(
cache_control_contains_no_store(&cache_control_header),
"expected Cache-Control: no-store when a subgraph times out, got: {:?}",
cache_control_header
);
assert!(
!cache_control_contains_public(&cache_control_header),
"Cache-Control must not contain 'public' when a subgraph timed out, got: {:?}",
cache_control_header
);
assert!(
!cache_control_contains_max_age(&cache_control_header),
"Cache-Control must not contain max-age when a subgraph timed out, got: {:?}",
cache_control_header
);
let body = response.next_response().await.unwrap();
assert!(
!body.errors.is_empty(),
"expected errors in response body due to subgraph timeout"
);
}
#[tokio::test]
async fn no_store_on_partial_subgraph_failure() {
let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
let query = "query { currentUser { activeOrganization { id creatorUser { __typename id } } } }";
let subgraphs = serde_json::json!({
"user": {
"query": {
"currentUser": {
"activeOrganization": {
"__typename": "Organization",
"id": "1",
}
}
},
"headers": {"cache-control": "max-age=1800, public"},
},
});
let (drop_tx, drop_rx) = tokio::sync::broadcast::channel(2);
let storage = Storage::new(&Config::test(false, &Uuid::new_v4().to_string()), drop_rx)
.await
.unwrap();
let subgraphs_conf = create_subgraph_conf(HashMap::from([
("user".to_string(), Subgraph::default()),
("orga".to_string(), Subgraph::default()),
]));
let response_cache = ResponseCache::for_test(
storage.clone(),
subgraphs_conf,
valid_schema.clone(),
true,
drop_tx,
)
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({
"include_subgraph_errors": { "all": true },
"experimental_mock_subgraphs": subgraphs,
}))
.unwrap()
.schema(SCHEMA)
.extra_private_plugin(response_cache.clone())
.build_supergraph()
.await
.unwrap();
let request = supergraph::Request::fake_builder()
.query(query)
.context(Context::new())
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_control_header =
get_cache_control_header(&response).expect("missing cache-control header");
assert!(
cache_control_contains_no_store(&cache_control_header),
"expected Cache-Control: no-store on partial failure, got: {:?}",
cache_control_header
);
assert!(
!cache_control_contains_public(&cache_control_header),
"Cache-Control must not contain 'public' when a subgraph failed, got: {:?}",
cache_control_header
);
assert!(
!cache_control_contains_max_age(&cache_control_header),
"Cache-Control must not contain max-age when a subgraph failed, got: {:?}",
cache_control_header
);
let body = response.next_response().await.unwrap();
assert!(
!body.errors.is_empty(),
"expected errors in response body due to failing subgraph"
);
}