From ba1c48cbfbaf9a207a94081eca2b1681ad4abecf Mon Sep 17 00:00:00 2001 From: jcamiel Date: Sun, 29 Jan 2023 18:51:04 +0100 Subject: [PATCH] Fix GraphQL query with variables to HTTP body request --- integration/tests_ok/graphql.curl | 4 +- integration/tests_ok/graphql.py | 2 +- packages/hurl/src/runner/body.rs | 2 +- packages/hurl/src/runner/json.rs | 89 +++++++++++----- packages/hurl/src/runner/multiline.rs | 142 +++++++++++++++++++++++++- packages/hurl/src/runner/response.rs | 2 +- 6 files changed, 205 insertions(+), 36 deletions(-) diff --git a/integration/tests_ok/graphql.curl b/integration/tests_ok/graphql.curl index 0c8f6b1979c..6bf69e7c90a 100644 --- a/integration/tests_ok/graphql.curl +++ b/integration/tests_ok/graphql.curl @@ -1,5 +1,5 @@ curl -H 'Content-Type: application/json' --data '{"query":"{\n allFilms {\n films {\n title\n director\n releaseDate\n }\n }\n}"}' 'http://localhost:8000/graphql' curl -H 'Content-Type: application/json' --data '{"query":"{\n allFilms {\n films {\n title\n director\n releaseDate\n }\n }\n}"}' 'http://localhost:8000/graphql' curl -H 'Content-Type: application/json' --data '{"query":"query Query {\n allFilms {\n films {\n title\n director\n releaseDate\n }\n }\n}"}' 'http://localhost:8000/graphql' -curl -H 'Content-Type: application/json' --data '{"query":"query Person($id: ID!) {\n person(id: $id) {\n name\n }\n}","variables":"{\n \"id\": \"cGVvcGxlOjQ=\"\n}"}' 'http://localhost:8000/graphql' -curl -H 'Content-Type: application/json' --data '{"query":"query Person($id: ID!) {\n person(id: $id) {\n name\n }\n}","variables":"{\n \"id\": \"cGVvcGxlOjQ=\"\n}"}' 'http://localhost:8000/graphql' +curl -H 'Content-Type: application/json' --data '{"query":"query Person($id: ID!) {\n person(id: $id) {\n name\n }\n}","variables":{"id":"cGVvcGxlOjQ="}}' 'http://localhost:8000/graphql' +curl -H 'Content-Type: application/json' --data '{"query":"query Person($id: ID!) {\n person(id: $id) {\n name\n }\n}","variables":{"id":"cGVvcGxlOjQ="}}' 'http://localhost:8000/graphql' diff --git a/integration/tests_ok/graphql.py b/integration/tests_ok/graphql.py index 32eec1d9c1b..18e192ced17 100644 --- a/integration/tests_ok/graphql.py +++ b/integration/tests_ok/graphql.py @@ -9,7 +9,7 @@ def graphql(): responses = { r'{"query":"{\n allFilms {\n films {\n title\n director\n releaseDate\n }\n }\n}"}': r'{"data":{"allFilms":{"films":[{"title":"A New Hope","director":"George Lucas","releaseDate":"1977-05-25"},{"title":"The Empire Strikes Back","director":"Irvin Kershner","releaseDate":"1980-05-17"},{"title":"Return of the Jedi","director":"Richard Marquand","releaseDate":"1983-05-25"},{"title":"The Phantom Menace","director":"George Lucas","releaseDate":"1999-05-19"},{"title":"Attack of the Clones","director":"George Lucas","releaseDate":"2002-05-16"},{"title":"Revenge of the Sith","director":"George Lucas","releaseDate":"2005-05-19"}]}}}', r'{"query":"query Query {\n allFilms {\n films {\n title\n director\n releaseDate\n }\n }\n}"}': r'{"data":{"allFilms":{"films":[{"title":"A New Hope","director":"George Lucas","releaseDate":"1977-05-25"},{"title":"The Empire Strikes Back","director":"Irvin Kershner","releaseDate":"1980-05-17"},{"title":"Return of the Jedi","director":"Richard Marquand","releaseDate":"1983-05-25"},{"title":"The Phantom Menace","director":"George Lucas","releaseDate":"1999-05-19"},{"title":"Attack of the Clones","director":"George Lucas","releaseDate":"2002-05-16"},{"title":"Revenge of the Sith","director":"George Lucas","releaseDate":"2005-05-19"}]}}}', - r'{"query":"query Person($id: ID!) {\n person(id: $id) {\n name\n }\n}","variables":"{\n \"id\": \"cGVvcGxlOjQ=\"\n}"}': r'{"data":{"person":{"name":"Darth Vader"}}}', + r'{"query":"query Person($id: ID!) {\n person(id: $id) {\n name\n }\n}","variables":{"id":"cGVvcGxlOjQ="}}': r'{"data":{"person":{"name":"Darth Vader"}}}', } data = responses[body_in] resp = make_response(data) diff --git a/packages/hurl/src/runner/body.rs b/packages/hurl/src/runner/body.rs index 9a71ababbc9..b8cfea11601 100644 --- a/packages/hurl/src/runner/body.rs +++ b/packages/hurl/src/runner/body.rs @@ -53,7 +53,7 @@ pub fn eval_bytes( } Bytes::Xml(value) => Ok(http::Body::Text(value.clone())), Bytes::Json(value) => { - let value = eval_json_value(value, variables)?; + let value = eval_json_value(value, variables, true)?; Ok(http::Body::Text(value)) } Bytes::Base64(Base64 { value, .. }) => Ok(http::Body::Binary(value.clone())), diff --git a/packages/hurl/src/runner/json.rs b/packages/hurl/src/runner/json.rs index b480164d5a1..a0128413215 100644 --- a/packages/hurl/src/runner/json.rs +++ b/packages/hurl/src/runner/json.rs @@ -24,9 +24,13 @@ use super::core::{Error, RunnerError}; use super::value::Value; use crate::runner::template::eval_expression; +/// Evaluates a JSON value to a string given a set of `variables`. +/// If `keep_whitespace` is true, whitespace is preserved from the JSonValue, otherwise +/// it is trimmed. pub fn eval_json_value( json_value: &JsonValue, variables: &HashMap, + keep_whitespace: bool, ) -> Result { match json_value { JsonValue::Null {} => Ok("null".to_string()), @@ -39,18 +43,26 @@ pub fn eval_json_value( JsonValue::List { space0, elements } => { let mut elems_string = vec![]; for element in elements { - let s = eval_json_list_element(element, variables)?; + let s = eval_json_list_element(element, variables, keep_whitespace)?; elems_string.push(s); } - Ok(format!("[{}{}]", space0, elems_string.join(","))) + if keep_whitespace { + Ok(format!("[{}{}]", space0, elems_string.join(","))) + } else { + Ok(format!("[{}]", elems_string.join(","))) + } } JsonValue::Object { space0, elements } => { let mut elems_string = vec![]; for element in elements { - let s = eval_json_object_element(element, variables)?; + let s = eval_json_object_element(element, variables, keep_whitespace)?; elems_string.push(s); } - Ok(format!("{{{}{}}}", space0, elems_string.join(","))) + if keep_whitespace { + Ok(format!("{{{}{}}}", space0, elems_string.join(","))) + } else { + Ok(format!("{{{}}}", elems_string.join(","))) + } } JsonValue::Expression(exp) => { let s = eval_expression(exp, variables)?; @@ -79,38 +91,47 @@ pub fn eval_json_value( } } -pub fn eval_json_list_element( +/// Evaluates a JSON list to a string given a set of `variables`. +/// If `keep_whitespace` is true, whitespace is preserved from the JSonValue, otherwise +/// it is trimmed. +fn eval_json_list_element( element: &JsonListElement, variables: &HashMap, + keep_whitespace: bool, ) -> Result { - let s = eval_json_value(&element.value, variables)?; - Ok(format!("{}{}{}", element.space0, s, element.space1)) + let s = eval_json_value(&element.value, variables, keep_whitespace)?; + if keep_whitespace { + Ok(format!("{}{}{}", element.space0, s, element.space1)) + } else { + Ok(s) + } } -pub fn eval_json_object_element( +/// Renders a JSON object to a string given a set of `variables`. +/// If `keep_whitespace` is true, whitespace is preserved from the JSonValue, otherwise +/// it is trimmed. +fn eval_json_object_element( element: &JsonObjectElement, variables: &HashMap, + keep_whitespace: bool, ) -> Result { - let value = eval_json_value(&element.value, variables)?; - Ok(format!( - "{}\"{}\"{}:{}{}{}", - element.space0, element.name, element.space1, element.space2, value, element.space3 - )) + let value = eval_json_value(&element.value, variables, keep_whitespace)?; + if keep_whitespace { + Ok(format!( + "{}\"{}\"{}:{}{}{}", + element.space0, element.name, element.space1, element.space2, value, element.space3 + )) + } else { + Ok(format!("\"{}\":{}", element.name, value)) + } } -/// Eval a JSON template to a valid JSON string -/// The variable are replaced by their value and encoded into JSON -/// -/// # Arguments -/// -/// * `template` - An Hurl Template -/// * `variables` - A map of input variables +/// Evaluates a JSON template to a string given a set of `variables` /// /// # Example /// /// The template "Hello {{quote}}" with variable quote=" /// will be evaluated to the JSON String "Hello \"" -/// pub fn eval_json_template( template: &Template, variables: &HashMap, @@ -227,19 +248,19 @@ mod tests { let mut variables = HashMap::new(); variables.insert("name".to_string(), Value::String("Bob".to_string())); assert_eq!( - eval_json_value(&JsonValue::Null {}, &variables).unwrap(), + eval_json_value(&JsonValue::Null {}, &variables, true).unwrap(), "null".to_string() ); assert_eq!( - eval_json_value(&JsonValue::Number("3.14".to_string()), &variables).unwrap(), + eval_json_value(&JsonValue::Number("3.14".to_string()), &variables, true).unwrap(), "3.14".to_string() ); assert_eq!( - eval_json_value(&JsonValue::Boolean(false), &variables).unwrap(), + eval_json_value(&JsonValue::Boolean(false), &variables, true).unwrap(), "false".to_string() ); assert_eq!( - eval_json_value(&json_hello_world_value(), &variables).unwrap(), + eval_json_value(&json_hello_world_value(), &variables, true).unwrap(), "\"Hello\\u0020Bob!\"".to_string() ); } @@ -247,7 +268,7 @@ mod tests { #[test] fn test_error() { let variables = HashMap::new(); - let error = eval_json_value(&json_hello_world_value(), &variables) + let error = eval_json_value(&json_hello_world_value(), &variables, true) .err() .unwrap(); assert_eq!(error.source_info, SourceInfo::new(1, 15, 1, 19)); @@ -270,6 +291,7 @@ mod tests { elements: vec![], }, &variables, + true, ) .unwrap(), "[]".to_string() @@ -298,6 +320,7 @@ mod tests { ], }, &variables, + true ) .unwrap(), "[1, -2, 3.0]".to_string() @@ -329,6 +352,7 @@ mod tests { ], }, &variables, + true ) .unwrap(), "[\"Hi\", \"Hello\\u0020Bob!\"]".to_string() @@ -345,12 +369,13 @@ mod tests { elements: vec![], }, &variables, + true ) .unwrap(), "{}".to_string() ); assert_eq!( - eval_json_value(&json_person_value(), &variables).unwrap(), + eval_json_value(&json_person_value(), &variables, true).unwrap(), r#"{ "firstName": "John" }"# @@ -372,6 +397,7 @@ mod tests { source_info: SourceInfo::new(1, 1, 1, 1), }), &variables, + true ) .unwrap(), "\"\\n\"".to_string() @@ -439,4 +465,13 @@ mod tests { assert_eq!(encode_json_string("\""), "\\\""); assert_eq!(encode_json_string("\\"), "\\\\"); } + + #[test] + fn test_not_preserving_spaces() { + let variables = HashMap::new(); + assert_eq!( + eval_json_value(&json_person_value(), &variables, true).unwrap(), + r#"{"firstName":"John"}"#.to_string() + ); + } } diff --git a/packages/hurl/src/runner/multiline.rs b/packages/hurl/src/runner/multiline.rs index 9e7a5a1be86..2b43ef75053 100644 --- a/packages/hurl/src/runner/multiline.rs +++ b/packages/hurl/src/runner/multiline.rs @@ -22,6 +22,7 @@ use hurl_core::ast::{MultilineString, Text}; use serde_json::json; use std::collections::HashMap; +/// Renders to string a multiline body, given a set of variables. pub fn eval_multiline( multiline: &MultilineString, variables: &HashMap, @@ -40,13 +41,146 @@ pub fn eval_multiline( MultilineString::GraphQl(graphql) => { let query = eval_template(&graphql.value, variables)?; let body = match &graphql.variables { - None => json!({ "query": query.trim()}), + None => json!({ "query": query.trim()}).to_string(), Some(vars) => { - let s = eval_json_value(&vars.value, variables)?; - json!({ "query": query.trim(), "variables": s}) + let s = eval_json_value(&vars.value, variables, false)?; + let query = json!(query.trim()); + format!(r#"{{"query":{query},"variables":{s}}}"#) } }; - Ok(body.to_string()) + Ok(body) } } } + +#[cfg(test)] +mod tests { + use crate::runner::multiline::eval_multiline; + use hurl_core::ast::JsonValue; + use hurl_core::ast::{ + GraphQl, GraphQlVariables, JsonObjectElement, MultilineString, SourceInfo, Template, + TemplateElement, Whitespace, + }; + use std::collections::HashMap; + + fn whitespace() -> Whitespace { + Whitespace { + value: String::from(" "), + source_info: SourceInfo::new(0, 0, 0, 0), + } + } + + fn newline() -> Whitespace { + Whitespace { + value: String::from("\n"), + source_info: SourceInfo::new(0, 0, 0, 0), + } + } + + fn empty_source_info() -> SourceInfo { + SourceInfo::new(0, 0, 0, 0) + } + + #[test] + fn eval_graphql_multiline_simple() { + let query = r#"{ + human(id: "1000") { + name + height(unit: FOOT) + } +}"#; + let variables = HashMap::new(); + let multiline = MultilineString::GraphQl(GraphQl { + space: whitespace(), + newline: newline(), + value: Template { + delimiter: None, + elements: vec![TemplateElement::String { + value: query.to_string(), + encoded: query.to_string(), + }], + source_info: empty_source_info(), + }, + variables: None, + }); + let body = eval_multiline(&multiline, &variables).unwrap(); + assert_eq!( + body, + r#"{"query":"{\n human(id: \"1000\") {\n name\n height(unit: FOOT)\n }\n}"}"# + .to_string() + ) + } + + #[test] + fn eval_graphql_multiline_with_graphql_variables() { + let query = r#"{ + human(id: "1000") { + name + height(unit: FOOT) + } +}"#; + let hurl_variables = HashMap::new(); + let graphql_variables = GraphQlVariables { + space: whitespace(), + value: JsonValue::Object { + space0: "".to_string(), + elements: vec![ + JsonObjectElement { + space0: "".to_string(), + name: Template { + delimiter: Some('"'), + elements: vec![TemplateElement::String { + value: "episode".to_string(), + encoded: "episode".to_string(), + }], + source_info: empty_source_info(), + }, + space1: "".to_string(), + space2: "".to_string(), + value: JsonValue::String(Template { + delimiter: Some('"'), + elements: vec![TemplateElement::String { + value: "JEDI".to_string(), + encoded: "JEDI".to_string(), + }], + source_info: empty_source_info(), + }), + space3: "".to_string(), + }, + JsonObjectElement { + space0: "".to_string(), + name: Template { + delimiter: Some('"'), + elements: vec![TemplateElement::String { + value: "withFriends".to_string(), + encoded: "withFriends".to_string(), + }], + source_info: empty_source_info(), + }, + space1: "".to_string(), + space2: "".to_string(), + value: JsonValue::Boolean(false), + space3: "".to_string(), + }, + ], + }, + whitespace: whitespace(), + }; + let multiline = MultilineString::GraphQl(GraphQl { + space: whitespace(), + newline: newline(), + value: Template { + delimiter: None, + elements: vec![TemplateElement::String { + value: query.to_string(), + encoded: query.to_string(), + }], + source_info: empty_source_info(), + }, + variables: Some(graphql_variables), + }); + + let body = eval_multiline(&multiline, &hurl_variables).unwrap(); + assert_eq!(body, r#"{"query":"{\n human(id: \"1000\") {\n name\n height(unit: FOOT)\n }\n}","variables":{"episode":"JEDI","withFriends":false}}"#.to_string()) + } +} diff --git a/packages/hurl/src/runner/response.rs b/packages/hurl/src/runner/response.rs index 7d2d9b84b1b..d0b6ddff210 100644 --- a/packages/hurl/src/runner/response.rs +++ b/packages/hurl/src/runner/response.rs @@ -146,7 +146,7 @@ fn eval_implicit_body_asserts( ) -> AssertResult { match &spec_body.value { Bytes::Json(value) => { - let expected = match eval_json_value(value, variables) { + let expected = match eval_json_value(value, variables, true) { Ok(s) => Ok(Value::String(s)), Err(e) => Err(e), };