From c5951665b98e9dcf3b917a3c8883a5add063831f Mon Sep 17 00:00:00 2001 From: Brian George Date: Fri, 19 Jul 2024 10:30:26 -0400 Subject: [PATCH] Add tests for supergraph/resolve_config.rs --- Cargo.lock | 1 + Cargo.toml | 1 + src/command/supergraph/resolve_config.rs | 430 ++++++++++++++++++++++- 3 files changed, 427 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a91ca0c9a..fb5bf8377 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4389,6 +4389,7 @@ dependencies = [ "heck 0.5.0", "houston", "httpmock", + "indoc", "interprocess", "lazy_static", "lazycell", diff --git a/Cargo.toml b/Cargo.toml index 6d4610c44..0dab6ebcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -195,6 +195,7 @@ assert_cmd = { workspace = true } assert_fs = { workspace = true } assert-json-diff = { workspace = true } httpmock = { workspace = true } +indoc = { workspace = true } predicates = { workspace = true } reqwest = { workspace = true, features = ["blocking", "native-tls-vendored"] } rstest = { workspace = true } diff --git a/src/command/supergraph/resolve_config.rs b/src/command/supergraph/resolve_config.rs index 3aa660755..e6c77e067 100644 --- a/src/command/supergraph/resolve_config.rs +++ b/src/command/supergraph/resolve_config.rs @@ -271,18 +271,438 @@ pub(crate) fn resolve_supergraph_yaml( #[cfg(test)] mod test_expand_supergraph_yaml { - use apollo_federation_types::config::FederationVersion; + use std::io::Write; + + use anyhow::Result; + use apollo_federation_types::config::{FederationVersion, SchemaSource, SubgraphConfig}; + use camino::Utf8PathBuf; + use houston::Config; + use httpmock::MockServer; + use indoc::indoc; + use rstest::{fixture, rstest}; + use serde_json::json; + use speculoos::prelude::*; + + use crate::{ + options::ProfileOpt, + utils::{ + client::{ClientBuilder, StudioClientConfig}, + parsers::FileDescriptorType, + }, + }; + + const INTROSPECTION_SDL: &str = r#"directive @key(fields: _FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE + +directive @requires(fields: _FieldSet!) on FIELD_DEFINITION + +directive @provides(fields: _FieldSet!) on FIELD_DEFINITION + +directive @external(reason: String) on OBJECT | FIELD_DEFINITION + +directive @tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +directive @extends on OBJECT | INTERFACE + +type Query {\n test: String!\n _service: _Service!\n} + +scalar _FieldSet + +scalar _Any + +type _Service {\n sdl: String\n}"#; + + #[fixture] + fn schema() -> String { + indoc! {r#" + type Query { + test: String! + } + "# + } + .to_string() + } + + #[fixture] + fn profile_opt() -> ProfileOpt { + ProfileOpt { + profile_name: "default".to_string(), + } + } + + #[fixture] + #[once] + fn home_dir() -> Utf8PathBuf { + tempfile::tempdir() + .unwrap() + .path() + .to_path_buf() + .try_into() + .unwrap() + } + + #[fixture] + #[once] + fn api_key() -> String { + uuid::Uuid::new_v4().as_simple().to_string() + } + + #[fixture] + fn config(home_dir: &Utf8PathBuf, api_key: &String) -> Config { + Config::new(Some(home_dir), Some(api_key.to_string())).unwrap() + } + + #[fixture] + fn studio_client_config(config: Config) -> StudioClientConfig { + StudioClientConfig::new(None, config, false, ClientBuilder::default()) + } #[test] fn test_supergraph_yaml_int_version() { - let yaml = r#" -federation_version: 1 -subgraphs: -"#; + let yaml = indoc! {r#" + federation_version: 1 + subgraphs: +"# + }; let config = super::expand_supergraph_yaml(yaml).unwrap(); assert_eq!( config.get_federation_version(), Some(FederationVersion::LatestFedOne) ); } + + #[rstest] + fn test_subgraph_file_resolution( + schema: String, + profile_opt: ProfileOpt, + studio_client_config: StudioClientConfig, + ) -> Result<()> { + let mut schema_path = tempfile::NamedTempFile::new()?; + schema_path + .as_file_mut() + .write_all(&schema.clone().into_bytes())?; + let supergraph_config = format!( + indoc! {r#" + federation_version: 2 + subgraphs: + products: + routing_url: http://localhost:8000/ + schema: + file: {} +"# + }, + schema_path.path().to_str().unwrap() + ); + + let mut supergraph_config_path = tempfile::NamedTempFile::new()?; + supergraph_config_path + .as_file_mut() + .write_all(&supergraph_config.into_bytes())?; + + let unresolved_supergraph_config = + FileDescriptorType::File(supergraph_config_path.path().to_path_buf().try_into()?); + + let resolved_config = super::resolve_supergraph_yaml( + &unresolved_supergraph_config, + studio_client_config, + &profile_opt, + ); + + assert_that!(resolved_config).is_ok(); + let resolved_config = resolved_config.unwrap(); + + let subgraphs = resolved_config.into_iter().collect::>(); + assert_that!(subgraphs).has_length(1); + let subgraph = &subgraphs[0]; + assert_that!(subgraph).is_equal_to(&( + "products".to_string(), + SubgraphConfig { + routing_url: Some("http://localhost:8000/".to_string()), + schema: SchemaSource::Sdl { + sdl: schema.to_string(), + }, + }, + )); + + Ok(()) + } + + #[rstest] + fn test_subgraph_introspection_resolution( + profile_opt: ProfileOpt, + studio_client_config: StudioClientConfig, + ) -> Result<()> { + let server = MockServer::start(); + + let mock = server.mock(|when, then| { + let body = json!({ + "data": { + "_service": { + "sdl": INTROSPECTION_SDL + } + } + }); + when.method(httpmock::Method::POST).path("/"); + then.status(200) + .header("content-type", "application/json") + .json_body(body); + }); + + let supergraph_config = format!( + indoc! {r#" + federation_version: 2 + subgraphs: + products: + routing_url: {} + schema: + subgraph_url: {} +"# + }, + server.base_url(), + server.base_url() + ); + + let mut supergraph_config_path = tempfile::NamedTempFile::new()?; + supergraph_config_path + .as_file_mut() + .write_all(&supergraph_config.into_bytes())?; + + let unresolved_supergraph_config = + FileDescriptorType::File(supergraph_config_path.path().to_path_buf().try_into()?); + + let resolved_config = super::resolve_supergraph_yaml( + &unresolved_supergraph_config, + studio_client_config, + &profile_opt, + ); + + mock.assert_hits(1); + + assert_that!(resolved_config).is_ok(); + let resolved_config = resolved_config.unwrap(); + + let subgraphs = resolved_config.into_iter().collect::>(); + assert_that!(subgraphs).has_length(1); + let subgraph = &subgraphs[0]; + assert_that!(subgraph).is_equal_to(&( + "products".to_string(), + SubgraphConfig { + routing_url: Some(server.base_url()), + schema: SchemaSource::Sdl { + sdl: INTROSPECTION_SDL.to_string(), + }, + }, + )); + + Ok(()) + } + + #[rstest] + fn test_subgraph_studio_resolution(profile_opt: ProfileOpt, config: Config) -> Result<()> { + let graph_id = "testgraph"; + let variant = "current"; + let graphref = format!("{}@{}", graph_id, variant); + let server = MockServer::start(); + + let subgraph_fetch_mock = server.mock(|when, then| { + let body = json!({ + "data": { + "variant": { + "__typename": "GraphVariant", + "subgraph": { + "url": server.base_url(), + "activePartialSchema": { + "sdl": INTROSPECTION_SDL + } + }, + "subgraphs": [ + { + "name": "products" + } + ] + } + } + }); + when.method(httpmock::Method::POST) + .path("/") + .json_body_obj(&json!({ + "query": indoc!{ + r#" + query SubgraphFetchQuery($graph_ref: ID!, $subgraph_name: ID!) { + variant(ref: $graph_ref) { + __typename + ... on GraphVariant { + subgraph(name: $subgraph_name) { + url, + activePartialSchema { + sdl + } + } + subgraphs { + name + } + } + } + } + "# + }, + "variables": { + "graph_ref": graphref, + "subgraph_name": "products" + }, + "operationName": "SubgraphFetchQuery" + })); + then.status(200) + .header("content-type", "application/json") + .json_body(body); + }); + + let is_federated_mock = server.mock(|when, then| { + let body = json!({ + "data": { + "graph": { + "variant": { + "subgraphs": [ + { + "name": "products" + } + ] + } + } + } + }); + when.method(httpmock::Method::POST) + .path("/") + .json_body_obj(&json!({ + "query": indoc!{ + r#" + query IsFederatedGraph($graph_id: ID!, $variant: String!) { + graph(id: $graph_id) { + variant(name: $variant) { + subgraphs { + name + } + } + } + } + "# + }, + "variables": { + "graph_id": graph_id, + "variant": variant + }, + "operationName": "IsFederatedGraph" + })); + then.status(200) + .header("content-type", "application/json") + .json_body(body); + }); + + let supergraph_config = format!( + indoc! {r#" + federation_version: 2 + subgraphs: + products: + schema: + graphref: {} + subgraph: products +"# + }, + graphref + ); + + let studio_client_config = StudioClientConfig::new( + Some(server.base_url()), + config, + false, + ClientBuilder::default(), + ); + + let mut supergraph_config_path = tempfile::NamedTempFile::new()?; + supergraph_config_path + .as_file_mut() + .write_all(&supergraph_config.into_bytes())?; + + let unresolved_supergraph_config = + FileDescriptorType::File(supergraph_config_path.path().to_path_buf().try_into()?); + + let resolved_config = super::resolve_supergraph_yaml( + &unresolved_supergraph_config, + studio_client_config, + &profile_opt, + ); + + assert_that!(resolved_config).is_ok(); + let resolved_config = resolved_config.unwrap(); + + is_federated_mock.assert_hits(1); + subgraph_fetch_mock.assert_hits(1); + + let subgraphs = resolved_config.into_iter().collect::>(); + assert_that!(subgraphs).has_length(1); + let subgraph = &subgraphs[0]; + assert_that!(subgraph).is_equal_to(&( + "products".to_string(), + SubgraphConfig { + routing_url: Some(server.base_url()), + schema: SchemaSource::Sdl { + sdl: INTROSPECTION_SDL.to_string(), + }, + }, + )); + + Ok(()) + } + + #[rstest] + fn test_subgraph_sdl_resolution( + schema: String, + profile_opt: ProfileOpt, + studio_client_config: StudioClientConfig, + ) -> Result<()> { + let supergraph_config = format!( + indoc! { + r#" + federation_version: 2 + subgraphs: + products: + routing_url: http://localhost:8000/ + schema: + sdl: "{}" + "# + }, + schema.escape_default() + ); + + let mut supergraph_config_path = tempfile::NamedTempFile::new()?; + supergraph_config_path + .as_file_mut() + .write_all(&supergraph_config.into_bytes())?; + + let unresolved_supergraph_config = + FileDescriptorType::File(supergraph_config_path.path().to_path_buf().try_into()?); + + let resolved_config = super::resolve_supergraph_yaml( + &unresolved_supergraph_config, + studio_client_config, + &profile_opt, + ); + + assert_that!(resolved_config).is_ok(); + let resolved_config = resolved_config.unwrap(); + + let subgraphs = resolved_config.into_iter().collect::>(); + assert_that!(subgraphs).has_length(1); + let subgraph = &subgraphs[0]; + assert_that!(subgraph).is_equal_to(&( + "products".to_string(), + SubgraphConfig { + routing_url: Some("http://localhost:8000/".to_string()), + schema: SchemaSource::Sdl { + sdl: schema.to_string(), + }, + }, + )); + + Ok(()) + } }