Skip to main content

subgraph_mock/handle/
graphql.rs

1use crate::{
2    handle::ByteResponse,
3    state::{Config, FederatedSchema, State},
4};
5use anyhow::anyhow;
6use apollo_compiler::schema::UnionType;
7use apollo_compiler::{
8    ExecutableDocument, Name, Node, Schema,
9    ast::OperationType,
10    executable::{Field, Selection, SelectionSet},
11    request::coerce_variable_values,
12    response::JsonMap,
13    schema::ExtendedType,
14    validation::{Valid, WithErrors},
15};
16use cached::proc_macro::cached;
17use http_body_util::{BodyExt, Empty, Full};
18use hyper::{
19    HeaderMap, Response, StatusCode,
20    body::Bytes,
21    header::{HeaderName, HeaderValue},
22};
23use ordered_float::OrderedFloat;
24use rand::{RngExt, rngs::ThreadRng, seq::IteratorRandom};
25use serde::{Deserialize, Deserializer, Serialize};
26use serde_json_bytes::{
27    ByteString, Map, Value, json,
28    serde_json::{self, Number},
29};
30use std::{
31    collections::{BTreeMap, HashMap, HashSet},
32    hash::{DefaultHasher, Hash, Hasher},
33    mem,
34    ops::RangeInclusive,
35    sync::Arc,
36};
37use tracing::{debug, error, trace};
38
39pub async fn handle(
40    body_bytes: Vec<u8>,
41    subgraph_name: Option<&str>,
42    state: Arc<State>,
43) -> anyhow::Result<ByteResponse> {
44    let req: GraphQLRequest = match serde_json::from_slice(&body_bytes) {
45        Ok(req) => req,
46        Err(err) => {
47            error!(%err, "received invalid graphql request");
48            let mut resp = Response::new(
49                Full::new(err.to_string().into_bytes().into())
50                    .map_err(|never| match never {})
51                    .boxed(),
52            );
53            *resp.status_mut() = StatusCode::BAD_REQUEST;
54
55            return Ok(resp);
56        }
57    };
58
59    let config = state.config.read().await;
60    let schema = state.schema.read().await;
61    let rgen_cfg = subgraph_name
62        .and_then(|name| config.subgraph_overrides.response_generation.get(name))
63        .unwrap_or_else(|| &config.response_generation);
64
65    // Since the response gen config and schema can be reloaded, they need to be included in the cache hash
66    // alongside the query itself. This does mean that hot reloads will balloon memory over time since the old
67    // values aren't invalidated. If we find this to actually be a practical problem in test scenarios that
68    // demand a high cardinality of config/schema setups, we can set up more intelligent caching with invalidation.
69    let mut hasher = DefaultHasher::new();
70    req.query.hash(&mut hasher);
71    rgen_cfg.hash(&mut hasher);
72    schema.hash(&mut hasher);
73    let cache_hash = hasher.finish();
74
75    if let Some((numerator, denominator)) = rgen_cfg.http_error_ratio {
76        let mut rng = rand::rng();
77        if rng.random_ratio(numerator, denominator) {
78            return Response::builder()
79                .status(rng.random_range(500..=504))
80                .body(Empty::new().map_err(|never| match never {}).boxed())
81                .map_err(|err| err.into());
82        }
83    }
84
85    let (bytes, status_code) = if subgraph_name
86        .and_then(|name| config.subgraph_overrides.cache_responses.get(name).copied())
87        .unwrap_or_else(|| config.cache_responses)
88    {
89        into_response_bytes_and_status_code(rgen_cfg, req, &schema, cache_hash).await
90    } else {
91        into_response_bytes_and_status_code_no_cache(rgen_cfg, req, &schema, cache_hash).await
92    };
93
94    let mut resp = Response::new(Full::new(bytes).map_err(|never| match never {}).boxed());
95    *resp.status_mut() = status_code;
96
97    let headers = resp.headers_mut();
98    add_headers(&config, rgen_cfg, subgraph_name, headers);
99
100    Ok(resp)
101}
102
103#[derive(Debug, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase")]
105pub struct GraphQLRequest {
106    pub query: String,
107    pub operation_name: Option<String>,
108    #[serde(default)]
109    #[serde(deserialize_with = "null_or_missing_as_default")]
110    pub variables: JsonMap,
111}
112
113/// Allows a field to be either null *or* not present in a request. Some GraphQL implementations
114/// specifically set variables to null rather than omitting them or providing an empty struct.
115fn null_or_missing_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
116where
117    D: Deserializer<'de>,
118    T: Default + Deserialize<'de>,
119{
120    Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
121}
122
123fn add_headers(
124    config: &Config,
125    rgen_cfg: &ResponseGenerationConfig,
126    subgraph_name: Option<&str>,
127    headers: &mut HeaderMap,
128) {
129    let mut rng = rand::rng();
130
131    // HeaderMap is a multimap and yields Some(HeaderName) only for the first element of each multimap.
132    // We have to track the last one we saw and treat that as the key for all subsequent None values as such.
133    // Based on that contract, the first iteration will *always* yield a value so we can safely just initialize
134    // this to a dummy value and trust that it will get overwritten instead of using an Option.
135    let mut last_header_name: HeaderName = HeaderName::from_static("unused");
136    let mut last_ratio: Option<Ratio> = None;
137
138    for (header_name, header_value) in subgraph_name
139        .and_then(|name| config.subgraph_overrides.headers.get(name).cloned())
140        .unwrap_or_else(|| config.headers.clone())
141        .into_iter()
142    {
143        if let Some(name) = header_name {
144            last_ratio = rgen_cfg.header_ratio.get(name.as_str()).copied();
145            last_header_name = name;
146        }
147
148        let should_insert = last_ratio
149            .is_none_or(|(numerator, denominator)| rng.random_ratio(numerator, denominator));
150
151        if should_insert {
152            headers.insert(&last_header_name, header_value);
153        }
154    }
155
156    headers.insert("Content-Type", HeaderValue::from_static("application/json"));
157}
158
159#[cached(result = true, key = "u64", convert = "{_cache_hash}")]
160fn parse_and_validate(
161    req: &GraphQLRequest,
162    schema: &Valid<Schema>,
163    _cache_hash: u64,
164) -> Result<Valid<ExecutableDocument>, WithErrors<ExecutableDocument>> {
165    let op_name = req.operation_name.as_deref().unwrap_or("unknown");
166
167    ExecutableDocument::parse_and_validate(schema, &req.query, op_name)
168}
169
170#[tracing::instrument(skip(req, schema))]
171#[cached(key = "u64", convert = "{cache_hash}")]
172async fn into_response_bytes_and_status_code(
173    cfg: &ResponseGenerationConfig,
174    req: GraphQLRequest,
175    schema: &FederatedSchema,
176    cache_hash: u64,
177) -> (Bytes, StatusCode) {
178    debug!(%cache_hash, req.operation_name, "handling graphql request");
179    trace!(variables=?req.variables, "request variables");
180
181    let doc = match parse_and_validate(&req, schema, cache_hash) {
182        Ok(doc) => doc,
183        Err(err) => {
184            let errs: Vec<_> = err.errors.iter().map(|d| d.to_json()).collect();
185            error!(?errs, query=%req.query, "invalid graphql query");
186            let bytes = serde_json::to_vec(&json!({ "data": Value::Null, "errors": errs }))
187                .unwrap_or_default();
188            return (bytes.into(), StatusCode::BAD_REQUEST);
189        }
190    };
191
192    let op = doc.operations.iter().next().unwrap();
193    let op_name = op.name.as_ref().map(|name| name.as_str());
194
195    debug!(
196        ?op_name,
197        type=%op.operation_type,
198        n_selections = op.selection_set.selections.len(),
199        "processing operation"
200    );
201
202    let resp = match op.operation_type {
203        OperationType::Query => {
204            match generate_response(cfg, op_name, &doc, schema, &req.variables) {
205                Ok(resp) => resp,
206                Err(err) => {
207                    error!(%err, "unable to generate response");
208                    return (
209                        Bytes::from("unable to generate response"),
210                        StatusCode::INTERNAL_SERVER_ERROR,
211                    );
212                }
213            }
214        }
215
216        // Not currently supporting mutations or subscriptions
217        op_type => {
218            error!("received {op_type} request: not implemented");
219            return (
220                Bytes::from("not implemented"),
221                StatusCode::INTERNAL_SERVER_ERROR,
222            );
223        }
224    };
225
226    match serde_json::to_vec(&resp) {
227        Ok(bytes) => (bytes.into(), StatusCode::OK),
228        Err(err) => {
229            error!(%err, "unable to serialize response");
230            (
231                Bytes::from(err.to_string().into_bytes()),
232                StatusCode::INTERNAL_SERVER_ERROR,
233            )
234        }
235    }
236}
237
238fn generate_response(
239    cfg: &ResponseGenerationConfig,
240    op_name: Option<&str>,
241    doc: &Valid<ExecutableDocument>,
242    schema: &FederatedSchema,
243    variables: &JsonMap,
244) -> anyhow::Result<Value> {
245    let op = match doc.operations.get(op_name) {
246        Ok(op) => op,
247        Err(_) => return Ok(json!({ "data": null })),
248    };
249    let mut rng = rand::rng();
250
251    if let Some((numerator, denominator)) = cfg.graphql_errors.request_error_ratio
252        && rng.random_ratio(numerator, denominator)
253    {
254        return Ok(json!({ "data": null, "errors": [{ "message": "Request error simulated" }]}));
255    }
256
257    // Short-circuit introspection responses if a request is *only* introspection. This does mean that requests
258    // that combine both introspection and non-introspection fields in their query will get random data for
259    // the introspection fields. For our use-cases we only need correct introspection data if that is the only
260    // data being requested, but if we want to make this fully spec-compliant in the future we will need to merge
261    // the result of `partial_execute` with the random data generated on every query (which would be costlier).
262    if op.is_introspection(doc) {
263        return apollo_compiler::introspection::partial_execute(
264            schema,
265            &schema.implementers_map(),
266            doc,
267            op,
268            &coerce_variable_values(schema, op, variables)
269                .map_err(|err| anyhow!("{}", err.message()))?,
270        )
271        .map_err(|err| anyhow!("{}", err.message()))
272        .and_then(|result| serde_json_bytes::to_value(result).map_err(|err| anyhow!("{}", err)));
273    }
274
275    let mut data =
276        ResponseBuilder::new(&mut rng, doc, schema, cfg).selection_set(&op.selection_set)?;
277
278    // Select a random number of top-level fields to "fail" if we are going to have field errors. For the sake of
279    // simplicity and performance, we won't traverse deeper into the response object.
280    if let Some((numerator, denominator)) = cfg.graphql_errors.field_error_ratio
281        && rng.random_ratio(numerator, denominator)
282    {
283        let drop_count = rng.random_range(1..=data.len());
284        let sampled_keys = data.keys().cloned().sample(&mut rng, drop_count);
285        let to_drop: HashSet<ByteString> = HashSet::from_iter(sampled_keys);
286
287        data.retain(|key, _| !to_drop.contains(key));
288
289        let errors: Vec<Value> = to_drop
290            .into_iter()
291            .map(|key| {
292                json!({
293                    "message": "Field error simulated",
294                    "path": [key]
295                })
296            })
297            .collect();
298
299        Ok(json!({
300            "data": data,
301            "errors": errors,
302        }))
303    } else {
304        Ok(json!({ "data": data }))
305    }
306}
307
308pub type Ratio = (u32, u32);
309
310#[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)]
311pub struct GraphQLErrorConfig {
312    /// The ratio of GraphQL requests that should be responded to with a request error and no data.
313    ///
314    /// Defaults to no requests containing errors.
315    pub request_error_ratio: Option<Ratio>,
316    /// The ratio of GraphQL requests that should include field-level errors and partial data.
317    /// Note that if both this field and the request error ratio are set, this ratio will be applicable
318    /// to the subset of requests that do not have request errors.
319    ///
320    /// For example, if you have a `request_error_ratio` of `[1,3]`, and a `field_error_ratio` of `[1,4]`,
321    /// then only 1 in 6 of your total requests will contain field errors.
322    ///
323    /// Defaults to no requests containing errors.
324    pub field_error_ratio: Option<Ratio>,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
328pub struct ResponseGenerationConfig {
329    #[serde(default = "default_scalar_config")]
330    pub scalars: BTreeMap<String, ScalarGenerator>,
331    #[serde(default = "default_array_size")]
332    pub array: ArraySize,
333    #[serde(default = "default_null_ratio")]
334    pub null_ratio: Option<Ratio>,
335    #[serde(default)]
336    pub header_ratio: BTreeMap<String, (u32, u32)>,
337    #[serde(default)]
338    pub http_error_ratio: Option<Ratio>,
339    #[serde(default)]
340    pub graphql_errors: GraphQLErrorConfig,
341}
342
343impl ResponseGenerationConfig {
344    /// Merges the default scalar config with the provided config, allowing users to specify a partial set of scalar
345    /// generators while inheriting the default configuration for those they do not specify.
346    pub(crate) fn merge_default_scalars(&mut self) {
347        let default = default_scalar_config();
348        let provided = mem::replace(&mut self.scalars, default);
349        self.scalars.extend(provided);
350    }
351}
352
353impl Default for ResponseGenerationConfig {
354    fn default() -> Self {
355        Self {
356            scalars: default_scalar_config(),
357            array: default_array_size(),
358            null_ratio: default_null_ratio(),
359            header_ratio: BTreeMap::new(),
360            graphql_errors: GraphQLErrorConfig::default(),
361            http_error_ratio: None,
362        }
363    }
364}
365
366fn default_scalar_config() -> BTreeMap<String, ScalarGenerator> {
367    [
368        ("Boolean".into(), ScalarGenerator::Bool),
369        ("Int".into(), ScalarGenerator::Int { min: 0, max: 100 }),
370        ("ID".into(), ScalarGenerator::Int { min: 0, max: 100 }),
371        (
372            "Float".into(),
373            ScalarGenerator::Float {
374                min: OrderedFloat(-1.0),
375                max: OrderedFloat(1.0),
376            },
377        ),
378        (
379            "String".into(),
380            ScalarGenerator::String {
381                min_len: 1,
382                max_len: 10,
383            },
384        ),
385    ]
386    .into_iter()
387    .collect()
388}
389
390fn default_array_size() -> ArraySize {
391    ArraySize {
392        min_length: 0,
393        max_length: 10,
394    }
395}
396
397fn default_null_ratio() -> Option<Ratio> {
398    Some((1, 2))
399}
400
401#[derive(Debug, Clone, Copy, Serialize, Deserialize, Hash)]
402#[serde(tag = "type", rename_all = "lowercase")]
403pub enum ScalarGenerator {
404    Bool,
405    Float {
406        min: OrderedFloat<f64>,
407        max: OrderedFloat<f64>,
408    },
409    Int {
410        min: i32,
411        max: i32,
412    },
413    String {
414        min_len: usize,
415        max_len: usize,
416    },
417}
418
419impl Default for ScalarGenerator {
420    fn default() -> Self {
421        Self::DEFAULT
422    }
423}
424
425impl ScalarGenerator {
426    const DEFAULT: Self = Self::String {
427        min_len: 1,
428        max_len: 10,
429    };
430
431    fn generate(&self, rng: &mut ThreadRng) -> anyhow::Result<Value> {
432        let val = match *self {
433            Self::Bool => Value::Bool(rng.random_bool(0.5)),
434            Self::Int { min, max } => Value::Number(rng.random_range(min..=max).into()),
435
436            Self::Float { min, max } => Value::Number(
437                Number::from_f64(rng.random_range(*min..=*max)).expect("expected finite float"),
438            ),
439
440            // The default Arbitrary impl for String has a random length so we build based on
441            // characters instead
442            Self::String { min_len, max_len } => {
443                let len = rng.random_range(min_len..=max_len);
444                // Allow for some multibyte chars. May still need to realloc
445                let mut chars = Vec::with_capacity(len * 2);
446                for _ in 0..len {
447                    chars.push(rng.sample(rand::distr::Alphanumeric) as char);
448                }
449
450                Value::String(ByteString::from(chars.into_iter().collect::<String>()))
451            }
452        };
453
454        Ok(val)
455    }
456}
457
458#[derive(Debug, Clone, Copy, Serialize, Deserialize, Hash)]
459pub struct ArraySize {
460    pub min_length: usize,
461    pub max_length: usize,
462}
463
464impl ArraySize {
465    fn range(&self) -> RangeInclusive<usize> {
466        self.min_length..=self.max_length
467    }
468}
469
470struct ResponseBuilder<'a, 'doc, 'schema> {
471    rng: &'a mut ThreadRng,
472    doc: &'doc Valid<ExecutableDocument>,
473    schema: &'schema FederatedSchema,
474    cfg: &'a ResponseGenerationConfig,
475}
476
477impl<'a, 'doc, 'schema> ResponseBuilder<'a, 'doc, 'schema> {
478    fn new(
479        rng: &'a mut ThreadRng,
480        doc: &'doc Valid<ExecutableDocument>,
481        schema: &'schema FederatedSchema,
482        cfg: &'a ResponseGenerationConfig,
483    ) -> Self {
484        Self {
485            rng,
486            doc,
487            schema,
488            cfg,
489        }
490    }
491
492    fn selection_set(
493        &mut self,
494        selection_set: &SelectionSet,
495    ) -> anyhow::Result<Map<ByteString, Value>> {
496        let grouped_fields = self.collect_fields(selection_set)?;
497        let mut result = Map::new();
498
499        for (key, fields) in grouped_fields {
500            // The first occurrence of a field is representative for metadata that is defined by the schema
501            let meta_field = fields[0];
502
503            let val = if meta_field.name == "__typename" {
504                let selection_type = if let Some(union_schema_ty) = self
505                    .schema
506                    .types
507                    .get(&selection_set.ty)
508                    .and_then(|t| t.as_union())
509                {
510                    // pick a specific member of the union, rather than using the union name
511                    self.arbitrary_union_member(union_schema_ty)?.to_string()
512                } else {
513                    selection_set.ty.to_string()
514                };
515                Value::String(ByteString::from(selection_type))
516            } else if meta_field.name == "_service" {
517                let mut service_obj = Map::new();
518                service_obj.insert("sdl".to_string(), Value::String(self.schema.sdl().into()));
519                Value::Object(service_obj)
520            } else if !meta_field.ty().is_non_null() && self.should_be_null() {
521                Value::Null
522            } else {
523                let is_selection_set = !meta_field.selection_set.is_empty();
524                let is_array = meta_field.ty().is_list();
525
526                if is_selection_set {
527                    let mut selections = Vec::new();
528                    for field in fields {
529                        selections.extend_from_slice(&field.selection_set.selections);
530                    }
531                    let full_selection_set = SelectionSet {
532                        ty: meta_field.selection_set.ty.clone(),
533                        selections,
534                    };
535
536                    if is_array {
537                        Value::Array(self.array_selection_set(&full_selection_set)?)
538                    } else {
539                        Value::Object(self.selection_set(&full_selection_set)?)
540                    }
541                } else {
542                    match is_array {
543                        false => self.leaf_field(meta_field.ty().inner_named_type())?,
544                        true => self.array_leaf_field(meta_field.ty().inner_named_type())?,
545                    }
546                }
547            };
548
549            result.insert(key, val);
550        }
551
552        Ok(result)
553    }
554
555    fn collect_fields(
556        &self,
557        selection_set: &'doc SelectionSet,
558    ) -> anyhow::Result<HashMap<String, Vec<&'doc Node<Field>>>> {
559        let mut collected_fields: HashMap<String, Vec<&Node<Field>>> = HashMap::new();
560
561        for selection in &selection_set.selections {
562            match selection {
563                Selection::Field(field) => {
564                    let key = field.alias.as_ref().unwrap_or(&field.name).to_string();
565                    collected_fields.entry(key).or_default().push(field);
566                }
567                Selection::FragmentSpread(fragment) => {
568                    if let Some(fragment_def) = self.doc.fragments.get(&fragment.fragment_name) {
569                        for (key, mut fields) in self.collect_fields(&fragment_def.selection_set)? {
570                            collected_fields.entry(key).or_default().append(&mut fields);
571                        }
572                    }
573                }
574                Selection::InlineFragment(inline_fragment) => {
575                    // NB: ignore inline fragment type conditions; if we add extra fields, the router
576                    // can filter them out for us
577                    for (key, mut fields) in self.collect_fields(&inline_fragment.selection_set)? {
578                        collected_fields.entry(key).or_default().append(&mut fields);
579                    }
580                }
581            }
582        }
583
584        Ok(collected_fields)
585    }
586
587    fn leaf_field(&mut self, type_name: &Name) -> anyhow::Result<Value> {
588        match self.schema.types.get(type_name).unwrap() {
589            ExtendedType::Enum(enum_ty) => {
590                let enum_value = enum_ty
591                    .values
592                    .values()
593                    .choose(self.rng)
594                    .ok_or(anyhow!("empty enum: {type_name}"))?;
595
596                Ok(Value::String(ByteString::from(
597                    enum_value.value.to_string(),
598                )))
599            }
600
601            ExtendedType::Scalar(scalar) => self
602                .cfg
603                .scalars
604                .get(scalar.name.as_str())
605                .unwrap_or(&ScalarGenerator::DEFAULT)
606                .generate(self.rng),
607
608            _ => unreachable!("A field with an empty selection set must be a scalar or enum type"),
609        }
610    }
611
612    fn arbitrary_union_member(&mut self, union_type: &UnionType) -> anyhow::Result<Name> {
613        let num_values = union_type.members.len();
614        let index = self.rng.random_range(0..num_values);
615        Ok(union_type
616            .members
617            .get_index(index)
618            .ok_or(anyhow!("Missing value"))?
619            .name
620            .clone())
621    }
622
623    fn arbitrary_array_len(&mut self) -> anyhow::Result<usize> {
624        Ok(self.rng.random_range(self.cfg.array.range()))
625    }
626
627    fn array_selection_set(&mut self, selection_set: &SelectionSet) -> anyhow::Result<Vec<Value>> {
628        let num_values = self.arbitrary_array_len()?;
629        let mut values = Vec::with_capacity(num_values);
630        for _ in 0..num_values {
631            values.push(Value::Object(self.selection_set(selection_set)?));
632        }
633
634        Ok(values)
635    }
636
637    fn array_leaf_field(&mut self, type_name: &Name) -> anyhow::Result<Value> {
638        let num_values = self.arbitrary_array_len()?;
639        let mut values = Vec::with_capacity(num_values);
640        for _ in 0..num_values {
641            values.push(self.leaf_field(type_name)?);
642        }
643
644        Ok(Value::Array(values))
645    }
646
647    fn should_be_null(&mut self) -> bool {
648        if let Some((numerator, denominator)) = self.cfg.null_ratio {
649            self.rng.random_ratio(numerator, denominator)
650        } else {
651            false
652        }
653    }
654}
655
656#[cfg(test)]
657mod tests {
658    use super::*;
659
660    #[test]
661    fn introspection_short_circuits() -> anyhow::Result<()> {
662        let supergraph = include_str!("../../tests/data/schema.graphql");
663        let schema = FederatedSchema::parse_string(supergraph, "../../tests/data/schema.graphql")?;
664
665        let query = r#"
666            query {
667                __schema {
668                    queryType {
669                        name
670                    }
671                    types {
672                        name
673                        kind
674                    }
675                }
676            }
677        "#;
678
679        let doc = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
680        let cfg = ResponseGenerationConfig::default();
681        let result = generate_response(&cfg, None, &doc, &schema, &JsonMap::new())?;
682
683        assert!(result.get("data").is_some());
684        let data = result.get("data").unwrap();
685        assert!(data.get("__schema").is_some());
686        // No other random data is included
687        assert!(data.as_object().unwrap().len() == 1);
688
689        let schema_obj = data.get("__schema").unwrap();
690        assert!(schema_obj.get("queryType").is_some());
691
692        let query_type = schema_obj.get("queryType").unwrap();
693        assert_eq!(query_type.get("name").unwrap().as_str().unwrap(), "Query");
694
695        let types = schema_obj.get("types").unwrap().as_array().unwrap();
696        assert!(!types.is_empty());
697
698        let type_names: Vec<&str> = types
699            .iter()
700            .filter_map(|t| t.get("name")?.as_str())
701            .collect();
702        assert!(type_names.contains(&"Query"));
703        assert!(type_names.contains(&"User"));
704        assert!(type_names.contains(&"Post"));
705
706        Ok(())
707    }
708
709    #[test]
710    fn service_introspection_uses_raw_schema() -> anyhow::Result<()> {
711        let supergraph = include_str!("../../tests/data/schema.graphql");
712        let schema = FederatedSchema::parse_string(supergraph, "../../tests/data/schema.graphql")?;
713
714        let query = r#"
715            query {
716                _service {
717                    sdl
718                }
719            }
720        "#;
721
722        let doc = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
723        let cfg = ResponseGenerationConfig::default();
724        let result = generate_response(&cfg, None, &doc, &schema, &JsonMap::new())?;
725
726        assert!(result.get("data").is_some());
727        let data = result.get("data").unwrap();
728        assert!(data.get("_service").is_some());
729
730        let schema_obj = data.get("_service").unwrap();
731        assert!(schema_obj.get("sdl").is_some());
732
733        let sdl = schema_obj.get("sdl").unwrap().as_str().unwrap();
734        assert_eq!(supergraph, sdl);
735
736        Ok(())
737    }
738}