use std::collections::HashMap;
use std::sync::Arc;
use apollo_compiler::Schema;
use bytes::Bytes;
use fred::error::RedisErrorKind;
use fred::mocks::MockCommand;
use fred::mocks::Mocks;
use fred::prelude::RedisError;
use fred::prelude::RedisValue;
use http::HeaderValue;
use http::header::CACHE_CONTROL;
use parking_lot::Mutex;
use serde_json_bytes::ByteString;
use tower::ServiceExt;
use super::entity::EntityCache;
use crate::Context;
use crate::MockedSubgraphs;
use crate::TestHarness;
use crate::cache::redis::RedisCacheStorage;
use crate::plugin::test::MockSubgraph;
use crate::plugin::test::MockSubgraphService;
use crate::plugins::cache::entity::CONTEXT_CACHE_KEYS;
use crate::plugins::cache::entity::CacheKeyContext;
use crate::plugins::cache::entity::CacheKeysContext;
use crate::plugins::cache::entity::Subgraph;
use crate::plugins::cache::entity::hash_representation;
use crate::services::subgraph;
use crate::services::supergraph;
const SCHEMA: &str = include_str!("../../testdata/orga_supergraph.graphql");
const SCHEMA_REQUIRES: &str = include_str!("../../testdata/supergraph.graphql");
const SCHEMA_NESTED_KEYS: &str = include_str!("../../testdata/supergraph_nested_fields.graphql");
#[derive(Debug)]
pub(crate) struct MockStore {
map: Arc<Mutex<HashMap<Bytes, Bytes>>>,
}
impl MockStore {
pub(crate) fn new() -> MockStore {
MockStore {
map: Arc::new(Mutex::new(HashMap::new())),
}
}
}
impl Mocks for MockStore {
fn process_command(&self, command: MockCommand) -> Result<RedisValue, RedisError> {
println!("mock received redis command: {command:?}");
match &*command.cmd {
"GET" => {
if let Some(RedisValue::Bytes(b)) = command.args.first() {
if let Some(bytes) = self.map.lock().get(b) {
println!("-> returning {:?}", std::str::from_utf8(bytes));
return Ok(RedisValue::Bytes(bytes.clone()));
}
}
}
"MGET" => {
let mut result: Vec<RedisValue> = Vec::new();
let mut args_it = command.args.iter();
while let Some(RedisValue::Bytes(key)) = args_it.next() {
if let Some(bytes) = self.map.lock().get(key) {
result.push(RedisValue::Bytes(bytes.clone()));
} else {
result.push(RedisValue::Null);
}
}
return Ok(RedisValue::Array(result));
}
"SET" => {
if let (Some(RedisValue::Bytes(key)), Some(RedisValue::Bytes(value))) =
(command.args.first(), command.args.get(1))
{
self.map.lock().insert(key.clone(), value.clone());
return Ok(RedisValue::Null);
}
}
"MSET" => {
let mut args_it = command.args.iter();
while let (Some(RedisValue::Bytes(key)), Some(RedisValue::Bytes(value))) =
(args_it.next(), args_it.next())
{
self.map.lock().insert(key.clone(), value.clone());
}
return Ok(RedisValue::Null);
}
_ => {
panic!("unrecoginzed command: {command:?}")
}
}
Err(RedisError::new(RedisErrorKind::NotFound, "mock not found"))
}
}
#[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 redis_cache = RedisCacheStorage::from_mocks(Arc::new(MockStore::new()))
.await
.unwrap();
let map = [
(
"user".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true,
ttl: None,
..Default::default()
},
),
(
"orga".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true,
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let entity_cache = EntityCache::with_mocks(redis_cache.clone(), map, valid_schema.clone())
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({
"include_subgraph_errors": { "all": true },
"experimental_mock_subgraphs": subgraphs,
}))
.unwrap()
.schema(SCHEMA)
.extra_plugin(entity_cache)
.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_keys: CacheKeysContext = response.context.get(CONTEXT_CACHE_KEYS).unwrap().unwrap();
let mut cache_keys: Vec<CacheKeyContext> = cache_keys.into_values().flatten().collect();
cache_keys.sort();
insta::assert_json_snapshot!(cache_keys);
let mut entity_key = serde_json_bytes::Map::new();
entity_key.insert(
ByteString::from("id"),
serde_json_bytes::Value::String(ByteString::from("1")),
);
let hashed_entity_key = hash_representation(&entity_key);
let prefix_key =
format!("version:1.0:subgraph:orga:type:Organization:entity:{hashed_entity_key}");
assert!(
cache_keys
.iter()
.any(|cache_key| cache_key.key.starts_with(&prefix_key))
);
insta::assert_debug_snapshot!(response.response.headers().get(CACHE_CONTROL));
let response = response.next_response().await.unwrap();
insta::assert_json_snapshot!(response);
let entity_cache =
EntityCache::with_mocks(redis_cache.clone(), HashMap::new(), valid_schema.clone())
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
.unwrap()
.schema(SCHEMA)
.extra_plugin(entity_cache)
.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();
insta::assert_debug_snapshot!(response.response.headers().get(CACHE_CONTROL));
let cache_keys: CacheKeysContext = response.context.get(CONTEXT_CACHE_KEYS).unwrap().unwrap();
let mut cache_keys: Vec<CacheKeyContext> = cache_keys.into_values().flatten().collect();
cache_keys.sort();
insta::assert_json_snapshot!(cache_keys);
let response = response.next_response().await.unwrap();
insta::assert_json_snapshot!(response);
}
#[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,
"price": 150,
"upc": "1",
"__typename": "Product"
}
]
}}},
serde_json::json!{{"data": {
"_entities": [{
"shippingEstimate": 15
}]
}}}
).with_header(CACHE_CONTROL, HeaderValue::from_static("public")).build())
].into_iter().collect());
let redis_cache = RedisCacheStorage::from_mocks(Arc::new(MockStore::new()))
.await
.unwrap();
let map = [
(
"products".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true,
ttl: None,
..Default::default()
},
),
(
"inventory".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true,
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let entity_cache = EntityCache::with_mocks(redis_cache.clone(), map, valid_schema.clone())
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
.unwrap()
.schema(SCHEMA_REQUIRES)
.extra_plugin(entity_cache)
.extra_plugin(subgraphs)
.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_keys: CacheKeysContext = response.context.get(CONTEXT_CACHE_KEYS).unwrap().unwrap();
let mut cache_keys: Vec<CacheKeyContext> = cache_keys.into_values().flatten().collect();
cache_keys.sort();
let mut entity_key = serde_json_bytes::Map::new();
entity_key.insert(
ByteString::from("upc"),
serde_json_bytes::Value::String(ByteString::from("1")),
);
let hashed_entity_key = hash_representation(&entity_key);
let prefix_key =
format!("version:1.0:subgraph:inventory:type:Product:entity:{hashed_entity_key}");
assert!(
cache_keys
.iter()
.any(|cache_key| cache_key.key.starts_with(&prefix_key))
);
insta::assert_json_snapshot!(cache_keys);
insta::assert_debug_snapshot!(response.response.headers().get(CACHE_CONTROL));
let response = response.next_response().await.unwrap();
insta::assert_json_snapshot!(response);
let entity_cache =
EntityCache::with_mocks(redis_cache.clone(), HashMap::new(), valid_schema.clone())
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
.unwrap()
.schema(SCHEMA_REQUIRES)
.extra_plugin(entity_cache)
.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();
insta::assert_debug_snapshot!(response.response.headers().get(CACHE_CONTROL));
let cache_keys: CacheKeysContext = response.context.get(CONTEXT_CACHE_KEYS).unwrap().unwrap();
let mut cache_keys: Vec<CacheKeyContext> = cache_keys.into_values().flatten().collect();
cache_keys.sort();
insta::assert_json_snapshot!(cache_keys);
let response = response.next_response().await.unwrap();
insta::assert_json_snapshot!(response);
}
#[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 redis_cache = RedisCacheStorage::from_mocks(Arc::new(MockStore::new()))
.await
.unwrap();
let map = [
(
"products".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true,
ttl: None,
..Default::default()
},
),
(
"users".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true,
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let entity_cache = EntityCache::with_mocks(redis_cache.clone(), 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_NESTED_KEYS)
.extra_plugin(entity_cache)
.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_keys: CacheKeysContext = response.context.get(CONTEXT_CACHE_KEYS).unwrap().unwrap();
let mut cache_keys: Vec<CacheKeyContext> = cache_keys.into_values().flatten().collect();
cache_keys.sort();
let mut entity_key = serde_json_bytes::Map::new();
entity_key.insert(
ByteString::from("email"),
serde_json_bytes::Value::String(ByteString::from("test@test.com")),
);
entity_key.insert(
ByteString::from("country"),
serde_json_bytes::json!({"a": "France"}),
);
let hashed_entity_key = hash_representation(&entity_key);
let prefix_key = format!("version:1.0:subgraph:users:type:User:entity:{hashed_entity_key}");
assert!(
cache_keys
.iter()
.any(|cache_key| cache_key.key.starts_with(&prefix_key))
);
insta::assert_json_snapshot!(cache_keys);
insta::assert_debug_snapshot!(response.response.headers().get(CACHE_CONTROL));
let response = response.next_response().await.unwrap();
insta::assert_json_snapshot!(response);
let entity_cache =
EntityCache::with_mocks(redis_cache.clone(), HashMap::new(), 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_NESTED_KEYS)
.extra_plugin(entity_cache)
.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();
insta::assert_debug_snapshot!(response.response.headers().get(CACHE_CONTROL));
let cache_keys: CacheKeysContext = response.context.get(CONTEXT_CACHE_KEYS).unwrap().unwrap();
let mut cache_keys: Vec<CacheKeyContext> = cache_keys.into_values().flatten().collect();
cache_keys.sort();
insta::assert_json_snapshot!(cache_keys);
let response = response.next_response().await.unwrap();
insta::assert_json_snapshot!(response);
}
#[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 = MockedSubgraphs([
("user", MockSubgraph::builder().with_json(
serde_json::json!{{"query":"{currentUser{activeOrganization{__typename id}}}"}},
serde_json::json!{{"data": {"currentUser": { "activeOrganization": {
"__typename": "Organization",
"id": "1"
} }}}}
).build()),
("orga", MockSubgraph::builder().with_json(
serde_json::json!{{
"query": "query($representations:[_Any!]!){_entities(representations:$representations){... on Organization{creatorUser{__typename id}}}}",
"variables": {
"representations": [
{
"id": "1",
"__typename": "Organization",
}
]
}}},
serde_json::json!{{"data": {
"_entities": [{
"creatorUser": {
"__typename": "User",
"id": 2
}
}]
}}}
).build())
].into_iter().collect());
let redis_cache = RedisCacheStorage::from_mocks(Arc::new(MockStore::new()))
.await
.unwrap();
let entity_cache =
EntityCache::with_mocks(redis_cache.clone(), HashMap::new(), valid_schema.clone())
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
.unwrap()
.schema(SCHEMA)
.extra_plugin(entity_cache)
.extra_plugin(subgraphs)
.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();
insta::assert_debug_snapshot!(response.response.headers().get(CACHE_CONTROL));
let response = response.next_response().await.unwrap();
insta::assert_json_snapshot!(response);
let entity_cache =
EntityCache::with_mocks(redis_cache.clone(), HashMap::new(), valid_schema.clone())
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
.unwrap()
.schema(SCHEMA)
.extra_plugin(entity_cache)
.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();
insta::assert_debug_snapshot!(response.response.headers().get(CACHE_CONTROL));
let response = response.next_response().await.unwrap();
insta::assert_json_snapshot!(response);
}
#[tokio::test]
async fn private() {
let query = "query { currentUser { activeOrganization { id creatorUser { __typename id } } } }";
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{activeOrganization{__typename id}}}"}},
serde_json::json!{{"data": {"currentUser": { "activeOrganization": {
"__typename": "Organization",
"id": "1"
} }}}}
).with_header(CACHE_CONTROL, HeaderValue::from_static("private"))
.build()),
("orga", MockSubgraph::builder().with_json(
serde_json::json!{{
"query": "query($representations:[_Any!]!){_entities(representations:$representations){... on Organization{creatorUser{__typename id}}}}",
"variables": {
"representations": [
{
"id": "1",
"__typename": "Organization",
}
]
}}},
serde_json::json!{{"data": {
"_entities": [{
"creatorUser": {
"__typename": "User",
"id": 2
}
}]
}}}
).with_header(CACHE_CONTROL, HeaderValue::from_static("private")).build())
].into_iter().collect());
let redis_cache = RedisCacheStorage::from_mocks(Arc::new(MockStore::new()))
.await
.unwrap();
let map = [
(
"user".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true,
ttl: None,
..Default::default()
},
),
(
"orga".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true,
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let entity_cache = EntityCache::with_mocks(redis_cache.clone(), map, valid_schema.clone())
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
.unwrap()
.schema(SCHEMA)
.extra_plugin(entity_cache.clone())
.extra_plugin(subgraphs)
.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)
.build()
.unwrap();
let mut response = service.clone().oneshot(request).await.unwrap();
let cache_keys: CacheKeysContext = response.context.get(CONTEXT_CACHE_KEYS).unwrap().unwrap();
let mut cache_keys: Vec<CacheKeyContext> = cache_keys.into_values().flatten().collect();
cache_keys.sort();
insta::assert_json_snapshot!(cache_keys);
let response = response.next_response().await.unwrap();
insta::assert_json_snapshot!(response);
println!("\nNOW WITHOUT SUBGRAPHS\n");
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
.unwrap()
.schema(SCHEMA)
.extra_plugin(entity_cache)
.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)
.build()
.unwrap();
let mut response = service.clone().oneshot(request).await.unwrap();
let cache_keys: CacheKeysContext = response.context.get(CONTEXT_CACHE_KEYS).unwrap().unwrap();
let mut cache_keys: Vec<CacheKeyContext> = cache_keys.into_values().flatten().collect();
cache_keys.sort();
insta::assert_json_snapshot!(cache_keys);
let response = response.next_response().await.unwrap();
insta::assert_json_snapshot!(response);
println!("\nNOW WITH DIFFERENT SUB\n");
let context = Context::new();
context.insert_json_value("sub", "5678".into());
let request = supergraph::Request::fake_builder()
.query(query)
.context(context)
.build()
.unwrap();
let mut response = service.clone().oneshot(request).await.unwrap();
assert!(
response
.context
.get::<_, CacheKeysContext>(CONTEXT_CACHE_KEYS)
.ok()
.flatten()
.is_none()
);
insta::assert_json_snapshot!(cache_keys);
let response = response.next_response().await.unwrap();
insta::assert_json_snapshot!(response);
}
#[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 redis_cache = RedisCacheStorage::from_mocks(Arc::new(MockStore::new()))
.await
.unwrap();
let map = [
(
"user".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true,
ttl: None,
..Default::default()
},
),
(
"orga".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true,
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let entity_cache = EntityCache::with_mocks(redis_cache.clone(), map, valid_schema.clone())
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
.unwrap()
.schema(SCHEMA)
.extra_plugin(entity_cache)
.extra_plugin(subgraphs)
.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_keys: CacheKeysContext = response.context.get(CONTEXT_CACHE_KEYS).unwrap().unwrap();
let mut cache_keys: Vec<CacheKeyContext> = cache_keys.into_values().flatten().collect();
cache_keys.sort();
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 response = response.next_response().await.unwrap();
insta::assert_json_snapshot!(response);
let entity_cache =
EntityCache::with_mocks(redis_cache.clone(), HashMap::new(), valid_schema.clone())
.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(),
)]
.into_iter()
.collect(),
);
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
.unwrap()
.schema(SCHEMA)
.extra_plugin(entity_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())
.build()
.unwrap();
let mut response = service.oneshot(request).await.unwrap();
let cache_keys: CacheKeysContext = response.context.get(CONTEXT_CACHE_KEYS).unwrap().unwrap();
let mut cache_keys: Vec<CacheKeyContext> = cache_keys.into_values().flatten().collect();
cache_keys.sort();
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 response = response.next_response().await.unwrap();
insta::assert_json_snapshot!(response);
}
#[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 redis_cache = RedisCacheStorage::from_mocks(Arc::new(MockStore::new()))
.await
.unwrap();
let map = [
(
"user".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true,
ttl: None,
..Default::default()
},
),
(
"orga".to_string(),
Subgraph {
redis: None,
private_id: Some("sub".to_string()),
enabled: true,
ttl: None,
..Default::default()
},
),
]
.into_iter()
.collect();
let entity_cache = EntityCache::with_mocks(redis_cache.clone(), map, valid_schema.clone())
.await
.unwrap();
let service = TestHarness::builder()
.configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
.unwrap()
.schema(SCHEMA)
.extra_plugin(entity_cache)
.extra_plugin(subgraphs)
.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);
let entity_cache =
EntityCache::with_mocks(redis_cache.clone(), HashMap::new(), valid_schema.clone())
.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_plugin(entity_cache)
.extra_plugin(subgraphs)
.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);
}