diff --git a/src/graphql_tests.rs b/src/graphql_tests.rs index 6729022..68aab2e 100644 --- a/src/graphql_tests.rs +++ b/src/graphql_tests.rs @@ -3,9 +3,15 @@ use std::{fs, sync::LazyLock}; use anyhow::{anyhow, Result}; use diff_logger::DiffLogger; use reqwest::Method; +use serde::{de::DeserializeOwned, Serialize}; +use serde_json::Value; use tracing::{error, info}; -use crate::request::{MOCK_API_CLIENT, REFERENCE_GRAPHQL_CLIENT, TESTED_GRAPHQL_CLIENT}; +use crate::{ + query_info::Schema, + request::{MOCK_API_CLIENT, REFERENCE_GRAPHQL_CLIENT, TESTED_GRAPHQL_CLIENT}, + type_info::Root, +}; use super::ROOT_DIR; @@ -65,3 +71,99 @@ pub async fn run_graphql_tests() -> Result<()> { Ok(()) } + +fn query_builder(type_name: &str) -> String { + format!( + r#" + {{ + __type(name: "{}") {{ + name + kind + fields {{ + name + args {{ + name + }} + }} + }} + }} + "#, + type_name + ) +} + +fn compare( + actual: Value, + expected: Value, + error_message: &str, +) -> Result<()> { + // in order to have sorting. + let actual: T = serde_json::from_value(actual)?; + let expected: T = serde_json::from_value(expected)?; + + // with value we can compare with diff logger + let actual_value = serde_json::to_value(actual)?; + let expected_value = serde_json::to_value(expected)?; + + let differ = DiffLogger::new(); + + let difference = differ.diff(&actual_value, &expected_value); + if !difference.is_empty() { + error!(error_message); + println!("{}", difference); + return Err(anyhow!(error_message.to_owned())); + } + + Ok(()) +} + +pub async fn run_introspection_query() -> Result<()> { + info!("Run graphql introspection tests"); + let query_info = include_str!("./query_info.graphql"); + + // check the root query is same or not. + let actual_value = TESTED_GRAPHQL_CLIENT.request(&query_info).await?; + let expected_value = REFERENCE_GRAPHQL_CLIENT.request(&query_info).await?; + let actual: Schema = serde_json::from_value(actual_value.clone())?; + let expected: Schema = serde_json::from_value(expected_value.clone())?; + + let _ = compare::( + actual_value, + expected_value, + "Query Operation type mismatch", + )?; + + for (actual, expected) in actual + .data + .schema + .query_type + .fields + .iter() + .zip(expected.data.schema.query_type.fields.iter()) + { + let actual_op_type = actual.field_type.get_name(); + let expected_op_type = expected.field_type.get_name(); + + if actual_op_type.is_none() != expected_op_type.is_none() { + error!("Output type mismatch for field: {:?}", actual.name); + return Err(anyhow!("Output type mismatch for field: {:?}", actual.name)); + } + + let actual_op_type = actual_op_type.unwrap(); + let expected_op_type = expected_op_type.unwrap(); + + let actual_op_type_query = query_builder(&actual_op_type); + let expected_op_type_query = query_builder(&expected_op_type); + + let actual = TESTED_GRAPHQL_CLIENT.request(&actual_op_type_query).await?; + let expected = REFERENCE_GRAPHQL_CLIENT + .request(&expected_op_type_query) + .await?; + + let _ = compare::(actual, expected, "Type Defination mismatch")?; + } + + info!("Execution of graphql schema validation finished"); + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index f6d859e..77a5797 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,5 +4,8 @@ mod graphql_tests; pub mod project; mod request; mod utils; +mod introspection; +mod query_info; +mod type_info; pub const ROOT_DIR: &str = env!("CARGO_MANIFEST_DIR"); diff --git a/src/project.rs b/src/project.rs index ce91a7d..5edf288 100644 --- a/src/project.rs +++ b/src/project.rs @@ -9,7 +9,7 @@ use tracing::{error, info, instrument}; use crate::{ benchmarks::run_benchmarks, command::{Command, CommandInstance}, - graphql_tests::run_graphql_tests, + graphql_tests::{run_graphql_tests, run_introspection_query}, request::{REFERENCE_GRAPHQL_CLIENT, TESTED_GRAPHQL_CLIENT}, utils::env_default, ROOT_DIR, @@ -53,6 +53,8 @@ impl Project { let reference_server = self.run_reference_server().await?; let server = self.run_server().await?; + run_introspection_query().await?; + run_graphql_tests().await?; run_benchmarks(&Path::new(ROOT_DIR).join(format!("results/{}", self.name()))).await?; run_graphql_tests().await?; diff --git a/src/query_info.graphql b/src/query_info.graphql new file mode 100644 index 0000000..3c760f1 --- /dev/null +++ b/src/query_info.graphql @@ -0,0 +1,41 @@ +query { + __schema { + queryType { + fields { + name + type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + args { + name + type { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + defaultValue + } + } + } + } +} diff --git a/src/query_info.rs b/src/query_info.rs new file mode 100644 index 0000000..27a7181 --- /dev/null +++ b/src/query_info.rs @@ -0,0 +1,127 @@ +use serde::{Deserialize, Serialize, Serializer, Deserializer}; +use serde::ser::SerializeStruct; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Schema { + pub data: SchemaData, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct SchemaData { + #[serde(rename = "__schema")] + pub schema: SchemaType, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SchemaType { + pub query_type: QueryType, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct QueryType { + pub fields: Vec, +} + +impl Serialize for QueryType { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut fields = self.fields.clone(); + fields.sort_by(|a, b| a.name.cmp(&b.name)); + let mut state = serializer.serialize_struct("queryType", 1)?; + state.serialize_field("fields", &fields)?; + state.end() + } +} + +impl<'de> Deserialize<'de> for QueryType { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + struct QueryTypeHelper { + fields: Vec, + } + + let helper = QueryTypeHelper::deserialize(deserializer)?; + let mut fields = helper.fields; + fields.sort_by(|a, b| a.name.cmp(&b.name)); + Ok(QueryType { fields }) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Field { + pub name: String, + pub field_type: FieldType, + pub args: Vec, +} + +impl Serialize for Field { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut args = self.args.clone(); + args.sort_by(|a, b| a.name.cmp(&b.name)); + let mut state = serializer.serialize_struct("field", 3)?; + state.serialize_field("name", &self.name)?; + state.serialize_field("type", &self.field_type)?; + state.serialize_field("args", &args)?; + state.end() + } +} + +impl<'de> Deserialize<'de> for Field { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + struct FieldHelper { + name: String, + #[serde(rename = "type")] + field_type: FieldType, + args: Vec, + } + + let helper = FieldHelper::deserialize(deserializer)?; + let mut args = helper.args; + args.sort_by(|a, b| a.name.cmp(&b.name)); + Ok(Field { + name: helper.name, + field_type: helper.field_type, + args, + }) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct Argument { + pub name: String, + #[serde(rename = "type")] + pub arg_type: FieldType, + pub default_value: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct FieldType { + pub kind: String, + pub name: Option, + pub of_type: Option>, +} + + +impl FieldType { + pub fn get_name(&self) -> Option { + match &self.name { + Some(name) if !name.is_empty() => Some(name.clone()), + _ => self.of_type.as_ref().and_then(|t| t.get_name()), + } + } +} \ No newline at end of file diff --git a/src/type_info.rs b/src/type_info.rs new file mode 100644 index 0000000..71db580 --- /dev/null +++ b/src/type_info.rs @@ -0,0 +1,105 @@ +use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer}; + +#[derive(Debug, Clone, PartialEq)] +pub struct Field { + name: String, + args: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Type { + name: String, + kind: String, + fields: Vec, +} + + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct Data { + #[serde(rename = "__type")] + type_info: Type, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct Root { + data: Data, +} + +// Custom serialization for Field +impl Serialize for Field { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("Field", 2)?; + state.serialize_field("name", &self.name)?; + let mut sorted_args = self.args.clone(); + sorted_args.sort(); + state.serialize_field("args", &sorted_args)?; + state.end() + } +} + +// Custom deserialization for Field +impl<'de> Deserialize<'de> for Field { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + struct FieldHelper { + name: String, + args: Vec, + } + + let helper = FieldHelper::deserialize(deserializer)?; + let mut args = helper.args; + args.sort(); + + Ok(Field { + name: helper.name, + args, + }) + } +} + +// Custom serialization for Type +impl Serialize for Type { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("Type", 3)?; + state.serialize_field("name", &self.name)?; + state.serialize_field("kind", &self.kind)?; + let mut sorted_fields = self.fields.clone(); + sorted_fields.sort_by(|a, b| a.name.cmp(&b.name)); + state.serialize_field("fields", &sorted_fields)?; + state.end() + } +} + +// Custom deserialization for Type +impl<'de> Deserialize<'de> for Type { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + struct TypeHelper { + name: String, + kind: String, + fields: Vec, + } + + let helper = TypeHelper::deserialize(deserializer)?; + let mut fields = helper.fields; + fields.sort_by(|a, b| a.name.cmp(&b.name)); + + Ok(Type { + name: helper.name, + kind: helper.kind, + fields, + }) + } +}