From b080bc6018dc6303e76ad113a308352a0b8fdb85 Mon Sep 17 00:00:00 2001 From: Avery Harnish Date: Mon, 19 Jul 2021 18:22:40 -0500 Subject: [PATCH] chore: test structured output --- .../src/operations/graph/publish/mod.rs | 4 +- .../src/operations/graph/publish/runner.rs | 64 ++- .../src/operations/graph/publish/types.rs | 90 ++- .../src/operations/subgraph/list/types.rs | 4 + .../src/operations/subgraph/publish/types.rs | 6 + .../rover-client/src/shared/check_response.rs | 1 + .../rover-client/src/shared/fetch_response.rs | 1 + src/cli.rs | 4 +- src/command/config/auth.rs | 2 +- src/command/config/clear.rs | 2 +- src/command/config/delete.rs | 2 +- src/command/config/whoami.rs | 2 +- src/command/docs/open.rs | 2 +- src/command/docs/shortlinks.rs | 6 +- src/command/explain.rs | 2 +- src/command/info.rs | 2 +- src/command/install/mod.rs | 2 +- src/command/output.rs | 538 ++++++++++++++++-- src/command/subgraph/delete.rs | 4 +- src/command/update/check.rs | 2 +- 20 files changed, 645 insertions(+), 95 deletions(-) diff --git a/crates/rover-client/src/operations/graph/publish/mod.rs b/crates/rover-client/src/operations/graph/publish/mod.rs index 4d4a2c1848..e4fe3abb22 100644 --- a/crates/rover-client/src/operations/graph/publish/mod.rs +++ b/crates/rover-client/src/operations/graph/publish/mod.rs @@ -2,4 +2,6 @@ mod runner; mod types; pub use runner::run; -pub use types::{GraphPublishInput, GraphPublishResponse}; +pub use types::{ + ChangeSummary, FieldChanges, GraphPublishInput, GraphPublishResponse, TypeChanges, +}; diff --git a/crates/rover-client/src/operations/graph/publish/runner.rs b/crates/rover-client/src/operations/graph/publish/runner.rs index c46032d204..eafc988b9f 100644 --- a/crates/rover-client/src/operations/graph/publish/runner.rs +++ b/crates/rover-client/src/operations/graph/publish/runner.rs @@ -1,4 +1,5 @@ use crate::blocking::StudioClient; +use crate::operations::graph::publish::types::{ChangeSummary, FieldChanges, TypeChanges}; use crate::operations::graph::publish::{GraphPublishInput, GraphPublishResponse}; use crate::shared::GraphRef; use crate::RoverClientError; @@ -75,9 +76,20 @@ fn build_response( // which very well may have changes. For this, we'll just look at the code // first and handle the response as if there was `None` for the diff let change_summary = if publish_response.code == "NO_CHANGES" { - build_change_summary(None) + ChangeSummary::none() } else { - build_change_summary(publish_response.tag.unwrap().diff_to_previous) + let diff = publish_response + .tag + .ok_or_else(|| RoverClientError::MalformedResponse { + null_field: "service.upload_schema.tag".to_string(), + })? + .diff_to_previous; + + if let Some(diff) = diff { + build_change_summary(diff) + } else { + ChangeSummary::none() + } }; Ok(GraphPublishResponse { @@ -86,25 +98,21 @@ fn build_response( }) } -type ChangeDiff = graph_publish_mutation::GraphPublishMutationServiceUploadSchemaTagDiffToPrevious; +type QueryChangeDiff = + graph_publish_mutation::GraphPublishMutationServiceUploadSchemaTagDiffToPrevious; -/// builds a string-representation of the diff between two schemas -/// e.g. ` [Fields: +2 -1 △0, Types: +4 -0 △7]` or `[No Changes]` -fn build_change_summary(diff: Option) -> String { - match diff { - None => "[No Changes]".to_string(), - Some(diff) => { - let changes = diff.change_summary; - let fields = format!( - "Fields: +{} -{} △ {}", - changes.field.additions, changes.field.removals, changes.field.edits - ); - let types = format!( - "Types: +{} -{} △ {}", - changes.type_.additions, changes.type_.removals, changes.type_.edits - ); - format!("[{}, {}]", fields, types) - } +fn build_change_summary(diff: QueryChangeDiff) -> ChangeSummary { + ChangeSummary { + field_changes: FieldChanges { + additions: diff.change_summary.field.additions as u64, + removals: diff.change_summary.field.removals as u64, + edits: diff.change_summary.field.edits as u64, + }, + type_changes: TypeChanges { + additions: diff.change_summary.type_.additions as u64, + removals: diff.change_summary.type_.removals as u64, + edits: diff.change_summary.type_.edits as u64, + }, } } @@ -200,7 +208,7 @@ mod tests { output.unwrap(), GraphPublishResponse { schema_hash: "123456".to_string(), - change_summary: "[No Changes]".to_string(), + change_summary: ChangeSummary::none(), } ); } @@ -251,14 +259,20 @@ mod tests { } } }); - let diff_to_previous: ChangeDiff = serde_json::from_value(json_diff).unwrap(); - let output = build_change_summary(Some(diff_to_previous)); - assert_eq!(output, "[Fields: +3 -1 △ 0, Types: +4 -0 △ 2]".to_string()) + let diff_to_previous: QueryChangeDiff = serde_json::from_value(json_diff).unwrap(); + let output = build_change_summary(diff_to_previous); + assert_eq!( + output.to_string(), + "[Fields: +3 -1 △ 0, Types: +4 -0 △ 2]".to_string() + ) } #[test] fn build_change_summary_works_with_no_changes() { - assert_eq!(build_change_summary(None), "[No Changes]".to_string()) + assert_eq!( + ChangeSummary::none().to_string(), + "[No Changes]".to_string() + ) } fn mock_graph_ref() -> GraphRef { diff --git a/crates/rover-client/src/operations/graph/publish/types.rs b/crates/rover-client/src/operations/graph/publish/types.rs index 5bfd698ba7..05ff59c1d7 100644 --- a/crates/rover-client/src/operations/graph/publish/types.rs +++ b/crates/rover-client/src/operations/graph/publish/types.rs @@ -3,6 +3,8 @@ use crate::shared::{GitContext, GraphRef}; use serde::Serialize; +use std::fmt; + #[derive(Clone, Debug, PartialEq)] pub struct GraphPublishInput { pub graph_ref: GraphRef, @@ -38,5 +40,91 @@ impl From for GraphPublishContextInput { #[derive(Clone, Serialize, Debug, PartialEq)] pub struct GraphPublishResponse { pub schema_hash: String, - pub change_summary: String, + #[serde(flatten)] + pub change_summary: ChangeSummary, +} + +#[derive(Clone, Serialize, Debug, PartialEq)] +pub struct ChangeSummary { + pub field_changes: FieldChanges, + pub type_changes: TypeChanges, +} + +impl ChangeSummary { + pub(crate) fn none() -> ChangeSummary { + ChangeSummary { + field_changes: FieldChanges::none(), + type_changes: TypeChanges::none(), + } + } +} + +impl fmt::Display for ChangeSummary { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.field_changes.additions == 0 + && self.field_changes.removals == 0 + && self.field_changes.edits == 0 + && self.type_changes.additions == 0 + && self.type_changes.removals == 0 + && self.type_changes.edits == 0 + { + write!(f, "[No Changes]") + } else { + write!(f, "[{}, {}]", &self.field_changes, &self.type_changes) + } + } +} + +#[derive(Clone, Serialize, Debug, PartialEq)] +pub struct FieldChanges { + pub additions: u64, + pub removals: u64, + pub edits: u64, +} + +impl FieldChanges { + pub(crate) fn none() -> FieldChanges { + FieldChanges { + additions: 0, + removals: 0, + edits: 0, + } + } +} + +impl fmt::Display for FieldChanges { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Fields: +{} -{} △ {}", + &self.additions, &self.removals, &self.edits + ) + } +} + +#[derive(Clone, Serialize, Debug, PartialEq)] +pub struct TypeChanges { + pub additions: u64, + pub removals: u64, + pub edits: u64, +} + +impl TypeChanges { + pub(crate) fn none() -> TypeChanges { + TypeChanges { + additions: 0, + removals: 0, + edits: 0, + } + } +} + +impl fmt::Display for TypeChanges { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Types: +{} -{} △ {}", + &self.additions, &self.removals, &self.edits + ) + } } diff --git a/crates/rover-client/src/operations/subgraph/list/types.rs b/crates/rover-client/src/operations/subgraph/list/types.rs index 882051241f..8e18dd9f27 100644 --- a/crates/rover-client/src/operations/subgraph/list/types.rs +++ b/crates/rover-client/src/operations/subgraph/list/types.rs @@ -26,7 +26,11 @@ impl From for QueryVariables { #[derive(Clone, Serialize, PartialEq, Debug)] pub struct SubgraphListResponse { pub subgraphs: Vec, + + #[serde(skip_serializing)] pub root_url: String, + + #[serde(skip_serializing)] pub graph_ref: GraphRef, } diff --git a/crates/rover-client/src/operations/subgraph/publish/types.rs b/crates/rover-client/src/operations/subgraph/publish/types.rs index 95aa5fb107..d735e306ab 100644 --- a/crates/rover-client/src/operations/subgraph/publish/types.rs +++ b/crates/rover-client/src/operations/subgraph/publish/types.rs @@ -24,8 +24,14 @@ pub struct SubgraphPublishInput { #[derive(Debug, Clone, Serialize, PartialEq)] pub struct SubgraphPublishResponse { pub schema_hash: Option, + + // we skip serializing this field as it is merged with "success" + // at the top level + #[serde(skip_serializing)] pub supergraph_was_updated: bool, + pub subgraph_was_created: bool, + #[serde(flatten)] pub composition_errors: CompositionErrors, } diff --git a/crates/rover-client/src/shared/check_response.rs b/crates/rover-client/src/shared/check_response.rs index 8ff525c972..651aaf8a48 100644 --- a/crates/rover-client/src/shared/check_response.rs +++ b/crates/rover-client/src/shared/check_response.rs @@ -17,6 +17,7 @@ pub struct CheckResponse { pub target_url: Option, pub operation_check_count: u64, pub changes: Vec, + #[serde(skip_serializing)] pub result: ChangeSeverity, pub failure_count: u64, } diff --git a/crates/rover-client/src/shared/fetch_response.rs b/crates/rover-client/src/shared/fetch_response.rs index 0c2500caaf..b5ddc80bb9 100644 --- a/crates/rover-client/src/shared/fetch_response.rs +++ b/crates/rover-client/src/shared/fetch_response.rs @@ -8,6 +8,7 @@ pub struct FetchResponse { #[derive(Debug, Clone, Serialize, PartialEq)] pub struct Sdl { pub contents: String, + #[serde(skip_serializing)] pub r#type: SdlType, } diff --git a/src/cli.rs b/src/cli.rs index c614e7fbcb..9b05543697 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -115,7 +115,7 @@ impl Rover { match rover_output { Ok(output) => { if self.json { - println!("{}", JsonOutput::success(output)); + println!("{}", JsonOutput::from(output)); } else { output.print(); } @@ -123,7 +123,7 @@ impl Rover { } Err(error) => { if self.json { - println!("{}", JsonOutput::error(error)); + println!("{}", JsonOutput::from(error)); } else { tracing::debug!(?error); eprint!("{}", error); diff --git a/src/command/config/auth.rs b/src/command/config/auth.rs index efaacda889..90951bcec3 100644 --- a/src/command/config/auth.rs +++ b/src/command/config/auth.rs @@ -32,7 +32,7 @@ impl Auth { Profile::get_credential(&self.profile_name, &config).map(|_| { eprintln!("Successfully saved API key."); })?; - Ok(RoverOutput::None) + Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/config/clear.rs b/src/command/config/clear.rs index dfcda7b77b..5ebc9f214b 100644 --- a/src/command/config/clear.rs +++ b/src/command/config/clear.rs @@ -16,6 +16,6 @@ impl Clear { pub fn run(&self, config: config::Config) -> Result { config.clear()?; eprintln!("Successfully cleared all configuration."); - Ok(RoverOutput::None) + Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/config/delete.rs b/src/command/config/delete.rs index 0df08911a1..e282ae0bb7 100644 --- a/src/command/config/delete.rs +++ b/src/command/config/delete.rs @@ -23,6 +23,6 @@ impl Delete { pub fn run(&self, config: config::Config) -> Result { config::Profile::delete(&self.name, &config)?; eprintln!("Successfully deleted profile \"{}\"", &self.name); - Ok(RoverOutput::None) + Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/config/whoami.rs b/src/command/config/whoami.rs index a5dad81e84..c0d7f7d357 100644 --- a/src/command/config/whoami.rs +++ b/src/command/config/whoami.rs @@ -80,6 +80,6 @@ impl WhoAmI { eprintln!("{}", message); - Ok(RoverOutput::None) + Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/docs/open.rs b/src/command/docs/open.rs index f446fbca43..8f39136d3c 100644 --- a/src/command/docs/open.rs +++ b/src/command/docs/open.rs @@ -40,6 +40,6 @@ impl Open { Ok(()) }?; - Ok(RoverOutput::None) + Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/docs/shortlinks.rs b/src/command/docs/shortlinks.rs index 18833e7587..6471824c00 100644 --- a/src/command/docs/shortlinks.rs +++ b/src/command/docs/shortlinks.rs @@ -1,9 +1,9 @@ pub const URL_BASE: &str = "https://go.apollo.dev/r"; -use std::collections::HashMap; +use std::collections::BTreeMap; -pub fn get_shortlinks_with_description() -> HashMap<&'static str, &'static str> { - let mut links = HashMap::new(); +pub fn get_shortlinks_with_description() -> BTreeMap<&'static str, &'static str> { + let mut links = BTreeMap::new(); links.insert("docs", "Rover's Documentation Homepage"); links.insert("api-keys", "Understanding Apollo's API Keys"); links.insert("contributing", "Contributing to Rover"); diff --git a/src/command/explain.rs b/src/command/explain.rs index f09d9b6455..4de0510d7d 100644 --- a/src/command/explain.rs +++ b/src/command/explain.rs @@ -14,6 +14,6 @@ pub struct Explain { impl Explain { pub fn run(&self) -> Result { let explanation = &self.code.explain(); - Ok(RoverOutput::Markdown(explanation.clone())) + Ok(RoverOutput::ErrorExplanation(explanation.clone())) } } diff --git a/src/command/info.rs b/src/command/info.rs index 47bbdc605b..0f719690c7 100644 --- a/src/command/info.rs +++ b/src/command/info.rs @@ -28,6 +28,6 @@ impl Info { PKG_VERSION, location, os, shell ); - Ok(RoverOutput::None) + Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/install/mod.rs b/src/command/install/mod.rs index 60a779df72..293a4b9419 100644 --- a/src/command/install/mod.rs +++ b/src/command/install/mod.rs @@ -68,7 +68,7 @@ impl Install { } else { eprintln!("{} was not installed. To override the existing installation, you can pass the `--force` flag to the installer.", &binary_name); } - Ok(RoverOutput::None) + Ok(RoverOutput::EmptySuccess) } else { Err(anyhow!("Failed to get the current executable's path.").into()) } diff --git a/src/command/output.rs b/src/command/output.rs index 8762cbe6f0..a9615a2a1a 100644 --- a/src/command/output.rs +++ b/src/command/output.rs @@ -1,5 +1,5 @@ -use std::fmt::Debug; -use std::{collections::HashMap, fmt::Display}; +use std::collections::BTreeMap; +use std::fmt::{self, Debug, Display}; use crate::error::RoverError; use crate::utils::table::{self, cell, row}; @@ -13,7 +13,7 @@ use crossterm::style::Attribute::Underlined; use rover_client::operations::graph::publish::GraphPublishResponse; use rover_client::operations::subgraph::list::SubgraphListResponse; use rover_client::operations::subgraph::publish::SubgraphPublishResponse; -use rover_client::shared::{CheckResponse, FetchResponse, GraphRef, SdlType}; +use rover_client::shared::{ChangeSeverity, CheckResponse, FetchResponse, GraphRef, SdlType}; use serde::Serialize; use serde_json::{json, Value}; use termimad::MadSkin; @@ -24,11 +24,11 @@ use termimad::MadSkin; /// in this enum, and its print logic should be handled in `RoverOutput::print` /// /// Not all commands will output machine readable information, and those should -/// return `Ok(RoverOutput::None)`. If a new command is added and it needs to +/// return `Ok(RoverOutput::EmptySuccess)`. If a new command is added and it needs to /// return something that is not described well in this enum, it should be added. #[derive(Clone, PartialEq, Debug)] pub enum RoverOutput { - DocsList(HashMap<&'static str, &'static str>), + DocsList(BTreeMap<&'static str, &'static str>), FetchResponse(FetchResponse), CoreSchema(String), SubgraphList(SubgraphListResponse), @@ -42,11 +42,10 @@ pub enum RoverOutput { subgraph: String, publish_response: SubgraphPublishResponse, }, - VariantList(Vec), Profiles(Vec), Introspection(String), - Markdown(String), - None, + ErrorExplanation(String), + EmptySuccess, } impl RoverOutput { @@ -162,12 +161,6 @@ impl RoverOutput { print_descriptor("Check Result"); print_content(check_response.get_table()); } - RoverOutput::VariantList(variants) => { - print_descriptor("Variants"); - for variant in variants { - println!("{}", variant); - } - } RoverOutput::Profiles(profiles) => { if profiles.is_empty() { eprintln!("No profiles found."); @@ -183,14 +176,14 @@ impl RoverOutput { print_descriptor("Introspection Response"); print_content(&introspection_response); } - RoverOutput::Markdown(markdown_string) => { + RoverOutput::ErrorExplanation(explanation) => { // underline bolded md let mut skin = MadSkin::default(); skin.bold.add_attr(Underlined); - println!("{}", skin.inline(&markdown_string)); + println!("{}", skin.inline(&explanation)); } - RoverOutput::None => (), + RoverOutput::EmptySuccess => (), } } @@ -218,44 +211,89 @@ impl RoverOutput { } => json!(publish_response), RoverOutput::SubgraphList(list_response) => json!(list_response), RoverOutput::CheckResponse(check_response) => json!(check_response), - RoverOutput::VariantList(variants) => json!({ "variants": variants }), RoverOutput::Profiles(profiles) => json!({ "profiles": profiles }), RoverOutput::Introspection(introspection_response) => { json!({ "introspection_response": introspection_response }) } - RoverOutput::Markdown(markdown_string) => json!({ "markdown": markdown_string }), - RoverOutput::None => json!(null), + RoverOutput::ErrorExplanation(explanation) => { + json!({ "explanation": explanation }) + } + RoverOutput::EmptySuccess => json!(null), } } } +fn print_descriptor(descriptor: impl Display) { + if atty::is(Stream::Stdout) { + eprintln!("{}: \n", Style::new().bold().paint(descriptor.to_string())); + } +} +fn print_one_line_descriptor(descriptor: impl Display) { + if atty::is(Stream::Stdout) { + eprint!("{}: ", Style::new().bold().paint(descriptor.to_string())); + } +} + +/// if the user is outputting to a terminal, we want there to be a terminating +/// newline, but we don't want that newline to leak into output that's piped +/// to a file, like from a `graph fetch` +fn print_content(content: impl Display) { + if atty::is(Stream::Stdout) { + println!("{}", content) + } else { + print!("{}", content) + } +} + #[derive(Debug, Clone, Serialize)] pub(crate) struct JsonOutput { - pub(crate) data: JsonData, - pub(crate) error: Value, + data: JsonData, + error: Value, } -impl JsonOutput { - pub(crate) fn success(output: RoverOutput) -> Value { - let json_output = JsonOutput { - data: JsonData { - inner: output.get_internal_json(), - success: true, - }, - error: json!(null), - }; - json!(json_output) +impl fmt::Display for JsonOutput { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", json!(self)) } +} - pub(crate) fn error(error: RoverError) -> Value { - let json_output = JsonOutput { +impl From for JsonOutput { + fn from(error: RoverError) -> Self { + JsonOutput { data: JsonData { inner: json!(null), success: false, }, error: json!(error), - }; - json!(json_output) + } + } +} + +impl From for JsonOutput { + fn from(output: RoverOutput) -> Self { + JsonOutput { + data: JsonData { + inner: output.get_internal_json(), + success: { + if let RoverOutput::CheckResponse(check_response) = output { + match check_response.result { + ChangeSeverity::PASS => true, + ChangeSeverity::FAIL => false, + } + } else if let RoverOutput::SubgraphPublishResponse { + graph_ref: _, + subgraph: _, + publish_response, + } = output + { + publish_response.supergraph_was_updated + } else { + true + } + }, + }, + error: json!(null), + } } } @@ -266,24 +304,420 @@ pub(crate) struct JsonData { pub(crate) success: bool, } -fn print_descriptor(descriptor: impl Display) { - if atty::is(Stream::Stdout) { - eprintln!("{}: \n", Style::new().bold().paint(descriptor.to_string())); +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use chrono::{DateTime, Local, Utc}; + use rover_client::{ + operations::{ + graph::publish::{ChangeSummary, FieldChanges, TypeChanges}, + subgraph::list::{SubgraphInfo, SubgraphUpdatedAt}, + }, + shared::{CompositionError, CompositionErrors, SchemaChange, Sdl}, + }; + + use super::*; + + #[test] + fn docs_list_json() { + let mut mock_shortlinks = BTreeMap::new(); + mock_shortlinks.insert("slug_one", "description_one"); + mock_shortlinks.insert("slug_two", "description_two"); + let actual_json: JsonOutput = RoverOutput::DocsList(mock_shortlinks).into(); + let expected_json = json!( + { + "data": { + "shortlinks": [ + { + "slug": "slug_one", + "description": "description_one" + }, + { + "slug": "slug_two", + "description": "description_two" + } + ], + "success": true + }, + "error": null + }); + assert_eq!(expected_json.to_string(), actual_json.to_string()); } -} -fn print_one_line_descriptor(descriptor: impl Display) { - if atty::is(Stream::Stdout) { - eprint!("{}: ", Style::new().bold().paint(descriptor.to_string())); + + #[test] + fn fetch_response_json() { + let mock_fetch_response = FetchResponse { + sdl: Sdl { + contents: "sdl contents".to_string(), + r#type: SdlType::Subgraph, + }, + }; + let actual_json: JsonOutput = RoverOutput::FetchResponse(mock_fetch_response).into(); + let expected_json = json!({ + "data": { + "sdl": { + "contents": "sdl contents", + }, + "success": true + }, + "error": null + }); + assert_eq!(expected_json.to_string(), actual_json.to_string()); } -} -/// if the user is outputting to a terminal, we want there to be a terminating -/// newline, but we don't want that newline to leak into output that's piped -/// to a file, like from a `graph fetch` -fn print_content(content: impl Display) { - if atty::is(Stream::Stdout) { - println!("{}", content) - } else { - print!("{}", content) + #[test] + fn core_schema_json() { + let mock_core_schema = "core schema contents".to_string(); + let actual_json: JsonOutput = RoverOutput::CoreSchema(mock_core_schema).into(); + let expected_json = json!( + { + "data": { + "core_schema": "core schema contents", + "success": true + }, + "error": null + }); + assert_eq!(expected_json.to_string(), actual_json.to_string()); + } + + #[test] + fn subgraph_list_json() { + let now_utc: DateTime = Utc::now(); + let now_local: DateTime = now_utc.into(); + let mock_subgraph_list_response = SubgraphListResponse { + subgraphs: vec![ + SubgraphInfo { + name: "subgraph one".to_string(), + url: Some("http://localhost:4001".to_string()), + updated_at: SubgraphUpdatedAt { + local: Some(now_local), + utc: Some(now_utc), + }, + }, + SubgraphInfo { + name: "subgraph two".to_string(), + url: None, + updated_at: SubgraphUpdatedAt { + local: None, + utc: None, + }, + }, + ], + root_url: "https://studio.apollographql.com/".to_string(), + graph_ref: GraphRef { + name: "graph".to_string(), + variant: "current".to_string(), + }, + }; + let actual_json: JsonOutput = RoverOutput::SubgraphList(mock_subgraph_list_response).into(); + let expected_json = json!({ + "data": { + "subgraphs": [ + { + "name": "subgraph one", + "url": "http://localhost:4001", + "updated_at": { + "local": now_local, + "utc": now_utc + } + }, + { + "name": "subgraph two", + "url": null, + "updated_at": { + "local": null, + "utc": null + } + } + ], + "success": true + }, + "error": null + }); + assert_eq!(expected_json.to_string(), actual_json.to_string()); + } + + #[test] + fn check_success_response_json() { + let mock_check_response = CheckResponse { + target_url: Some("https://studio.apollographql.com/graph/my-graph/composition/big-hash?variant=current".to_string()), + operation_check_count: 10, + changes: vec![SchemaChange { + code: "SOMETHING_HAPPENED".to_string(), + description: "beeg yoshi".to_string(), + severity: ChangeSeverity::PASS, + }, + SchemaChange { + code: "WOW".to_string(), + description: "that was so cool".to_string(), + severity: ChangeSeverity::PASS, + }], + result: ChangeSeverity::PASS, + failure_count: 0, + }; + let actual_json: JsonOutput = RoverOutput::CheckResponse(mock_check_response).into(); + let expected_json = json!( + { + "data": { + "target_url": "https://studio.apollographql.com/graph/my-graph/composition/big-hash?variant=current", + "operation_check_count": 10, + "changes": [ + { + "code": "SOMETHING_HAPPENED", + "description": "beeg yoshi", + "severity": "PASS" + }, + { + "code": "WOW", + "description": "that was so cool", + "severity": "PASS" + }, + ], + "failure_count": 0, + "success": true, + }, + "error": null + }); + assert_eq!(expected_json.to_string(), actual_json.to_string()); + } + + #[test] + fn check_failure_response_json() { + let mock_check_response = CheckResponse { + target_url: Some("https://studio.apollographql.com/graph/my-graph/composition/big-hash?variant=current".to_string()), + operation_check_count: 10, + changes: vec![SchemaChange { + code: "SOMETHING_HAPPENED".to_string(), + description: "beeg yoshi".to_string(), + severity: ChangeSeverity::FAIL, + }, + SchemaChange { + code: "WOW".to_string(), + description: "that was so cool".to_string(), + severity: ChangeSeverity::FAIL, + }], + result: ChangeSeverity::FAIL, + failure_count: 2, + }; + let actual_json: JsonOutput = RoverOutput::CheckResponse(mock_check_response).into(); + let expected_json = json!({ + "data": { + "target_url": "https://studio.apollographql.com/graph/my-graph/composition/big-hash?variant=current", + "operation_check_count": 10, + "changes": [ + { + "code": "SOMETHING_HAPPENED", + "description": "beeg yoshi", + "severity": "FAIL" + }, + { + "code": "WOW", + "description": "that was so cool", + "severity": "FAIL" + }, + ], + "failure_count": 2, + "success": false, + }, + "error": null + }); + assert_eq!(expected_json.to_string(), actual_json.to_string()); + } + + #[test] + fn graph_publish_response_json() { + let mock_publish_response = GraphPublishResponse { + schema_hash: "123456".to_string(), + change_summary: ChangeSummary { + field_changes: FieldChanges { + additions: 2, + removals: 1, + edits: 0, + }, + type_changes: TypeChanges { + additions: 4, + removals: 0, + edits: 7, + }, + }, + }; + let actual_json: JsonOutput = RoverOutput::GraphPublishResponse { + graph_ref: GraphRef { + name: "graph".to_string(), + variant: "variant".to_string(), + }, + publish_response: mock_publish_response, + } + .into(); + let expected_json = json!( + { + "data": { + "schema_hash": "123456", + "field_changes": { + "additions": 2, + "removals": 1, + "edits": 0 + }, + "type_changes": { + "additions": 4, + "removals": 0, + "edits": 7 + }, + "success": true + }, + "error": null + }); + assert_eq!(expected_json.to_string(), actual_json.to_string()); + } + + #[test] + fn subgraph_publish_success_response_json() { + let mock_publish_response = SubgraphPublishResponse { + schema_hash: Some("123456".to_string()), + + composition_errors: CompositionErrors { + composition_errors: vec![], + }, + supergraph_was_updated: true, + subgraph_was_created: true, + }; + let actual_json: JsonOutput = RoverOutput::SubgraphPublishResponse { + graph_ref: GraphRef { + name: "graph".to_string(), + variant: "variant".to_string(), + }, + subgraph: "subgraph".to_string(), + publish_response: mock_publish_response, + } + .into(); + let expected_json = json!( + { + "data": { + "schema_hash": "123456", + "subgraph_was_created": true, + "composition_errors": [], + "success": true + }, + "error": null + }); + assert_eq!(expected_json.to_string(), actual_json.to_string()); + } + + #[test] + fn subgraph_publish_failure_response_json() { + let mock_publish_response = SubgraphPublishResponse { + schema_hash: None, + + composition_errors: CompositionErrors { + composition_errors: vec![ + CompositionError { + message: "[Accounts] -> Things went really wrong".to_string(), + code: Some("AN_ERROR_CODE".to_string()), + }, + CompositionError { + message: "[Films] -> Something else also went wrong".to_string(), + code: None, + }, + ], + }, + supergraph_was_updated: false, + subgraph_was_created: false, + }; + let actual_json: JsonOutput = RoverOutput::SubgraphPublishResponse { + graph_ref: GraphRef { + name: "graph".to_string(), + variant: "variant".to_string(), + }, + subgraph: "subgraph".to_string(), + publish_response: mock_publish_response, + } + .into(); + let expected_json = json!({ + "data": { + "schema_hash": null, + "subgraph_was_created": false, + "composition_errors": [ + { + "message": "[Accounts] -> Things went really wrong", + "code": "AN_ERROR_CODE" + }, + { + "message": "[Films] -> Something else also went wrong", + "code": null + } + ], + "success": false + }, + "error": null + }); + assert_eq!(expected_json.to_string(), actual_json.to_string()); + } + + #[test] + fn profiles_json() { + let mock_profiles = vec!["default".to_string(), "staging".to_string()]; + let actual_json: JsonOutput = RoverOutput::Profiles(mock_profiles).into(); + let expected_json = json!({ + "data": { + "profiles": [ + "default", + "staging" + ], + "success": true + }, + "error": null + }); + assert_eq!(expected_json.to_string(), actual_json.to_string()); + } + + #[test] + fn introspection_json() { + let actual_json: JsonOutput = RoverOutput::Introspection( + "i cant believe its not a real introspection response".to_string(), + ) + .into(); + let expected_json = json!({ + "data": { + "introspection_response": "i cant believe its not a real introspection response", + "success": true + }, + "error": null + }); + assert_eq!(expected_json.to_string(), actual_json.to_string()); + } + + #[test] + fn error_explanation_json() { + let actual_json: JsonOutput = RoverOutput::ErrorExplanation( + "this error occurs when stuff is real complicated... I wouldn't worry about it" + .to_string(), + ) + .into(); + let expected_json = json!( + { + "data": { + "explanation_markdown": "this error occurs when stuff is real complicated... I wouldn't worry about it", + "success": true + }, + "error": null + } + + ); + assert_eq!(expected_json.to_string(), actual_json.to_string()); + } + + #[test] + fn empty_success_json() { + let actual_json: JsonOutput = RoverOutput::EmptySuccess.into(); + let expected_json = json!( + { + "data": { + "success": true + }, + "error": null + } + ); + assert_eq!(expected_json.to_string(), actual_json.to_string()); } } diff --git a/src/command/subgraph/delete.rs b/src/command/subgraph/delete.rs index 3e28648fe1..dcd0fc49b0 100644 --- a/src/command/subgraph/delete.rs +++ b/src/command/subgraph/delete.rs @@ -65,7 +65,7 @@ impl Delete { // I chose not to error here, since this is a perfectly valid path if !confirm_delete()? { eprintln!("Delete cancelled by user"); - return Ok(RoverOutput::None); + return Ok(RoverOutput::EmptySuccess); } } @@ -79,7 +79,7 @@ impl Delete { )?; handle_response(delete_response, &self.subgraph, &graph_ref); - Ok(RoverOutput::None) + Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/update/check.rs b/src/command/update/check.rs index b380968555..3b73ab4ad2 100644 --- a/src/command/update/check.rs +++ b/src/command/update/check.rs @@ -15,6 +15,6 @@ pub struct Check { impl Check { pub fn run(&self, config: config::Config, client: Client) -> Result { version::check_for_update(config, true, client)?; - Ok(RoverOutput::None) + Ok(RoverOutput::EmptySuccess) } }