From 630f60b86ca33b61d9bccb979114cbbe279fd2f9 Mon Sep 17 00:00:00 2001 From: rb090 Date: Fri, 26 Sep 2025 18:08:47 +0200 Subject: [PATCH 1/7] Accept all valid Float value inputs on coerce_variable_value Coerce JSON ints to finite f64 in compliance with GraphQL spec --- .../src/resolvers/input_coercion.rs | 6 ++- crates/apollo-compiler/tests/introspection.rs | 41 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/crates/apollo-compiler/src/resolvers/input_coercion.rs b/crates/apollo-compiler/src/resolvers/input_coercion.rs index 5f8e4065..e546e966 100644 --- a/crates/apollo-compiler/src/resolvers/input_coercion.rs +++ b/crates/apollo-compiler/src/resolvers/input_coercion.rs @@ -119,8 +119,10 @@ fn coerce_variable_value( } } "Float" => { - // https://spec.graphql.org/October2021/#sec-Float.Input-Coercion - if value.is_f64() { + // https://spec.graphql.org/September2025/#sec-Float.Input-Coercion + // Accept any JSON number (`int` or `float`) that coerces to a finite f64, + // rejecting special values (NaN, +∞, -∞) as required by the GraphQL spec. + if value.as_f64().is_some_and(f64::is_finite) { return Ok(value.clone()); } } diff --git a/crates/apollo-compiler/tests/introspection.rs b/crates/apollo-compiler/tests/introspection.rs index 60c6447d..9c51d7f4 100644 --- a/crates/apollo-compiler/tests/introspection.rs +++ b/crates/apollo-compiler/tests/introspection.rs @@ -321,3 +321,44 @@ fn mixed() { }"#]] .assert_eq(&response); } + +#[test] +fn test_graphql_float_variable_coercion() { + // Small schema with a Float in the input object + let sdl = r#" + type Car { id: ID! kilometers: Float! } + input CarInput { kilometers: Float! } + type Query { getCarById(id: ID!): Car } + type Mutation { insertACar(car: CarInput!): Car! + } + "#; + + let parsed_schema = Schema::parse_and_validate(sdl, "sdl").unwrap(); + + let executable_mutation = ExecutableDocument::parse_and_validate( + &parsed_schema, + "mutation MyCarInsertMutation($car: CarInput!){ insertACar(car:$car) { id kilometers } }", + "MyCarInsertMutation", + ).unwrap(); + let operation = executable_mutation.operations.get(Some("MyCarInsertMutation")).unwrap(); + + let kilometers_value = 3000; + + // Provide an integer for a Float field + let input_variables = serde_json_bytes::json!({ "car": { "kilometers": kilometers_value } }); + let map = match input_variables { + serde_json_bytes::Value::Object(m) => m, + _ => unreachable!(), + }; + + // Coerce and validate. + let coerced = coerce_variable_values(&parsed_schema, operation, &map).unwrap(); + let vars_for_exec = coerced.into_inner(); + + // ---- Assertions ---- + let car = vars_for_exec + .get("car") + .and_then(|value| value.as_object()) + .expect("coerced `car` object"); + assert_eq!(car.get("kilometers").unwrap(), kilometers_value, "kilometers should be present and a valid amount."); +} From a06f2d680d456c5e139e4a7faf2515d4ebba0b5f Mon Sep 17 00:00:00 2001 From: rb090 Date: Mon, 29 Sep 2025 12:24:52 +0200 Subject: [PATCH 2/7] Fix formatting issues --- crates/apollo-compiler/tests/introspection.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/apollo-compiler/tests/introspection.rs b/crates/apollo-compiler/tests/introspection.rs index 9c51d7f4..2f20f961 100644 --- a/crates/apollo-compiler/tests/introspection.rs +++ b/crates/apollo-compiler/tests/introspection.rs @@ -339,8 +339,13 @@ fn test_graphql_float_variable_coercion() { &parsed_schema, "mutation MyCarInsertMutation($car: CarInput!){ insertACar(car:$car) { id kilometers } }", "MyCarInsertMutation", - ).unwrap(); - let operation = executable_mutation.operations.get(Some("MyCarInsertMutation")).unwrap(); + ) + .unwrap(); + + let operation = executable_mutation + .operations + .get(Some("MyCarInsertMutation")) + .unwrap(); let kilometers_value = 3000; @@ -360,5 +365,9 @@ fn test_graphql_float_variable_coercion() { .get("car") .and_then(|value| value.as_object()) .expect("coerced `car` object"); - assert_eq!(car.get("kilometers").unwrap(), kilometers_value, "kilometers should be present and a valid amount."); + assert_eq!( + car.get("kilometers").unwrap(), + kilometers_value, + "kilometers should be present and a valid amount." + ); } From 892891ab59e96a00b760ce48cf4a96dd16520df5 Mon Sep 17 00:00:00 2001 From: rb090 Date: Mon, 29 Sep 2025 16:17:19 +0200 Subject: [PATCH 3/7] Move test_graphql_float_variable_coercion to separate test file --- .../apollo-compiler/tests/input_coercion.rs | 50 +++++++++++++++++++ crates/apollo-compiler/tests/introspection.rs | 50 ------------------- 2 files changed, 50 insertions(+), 50 deletions(-) create mode 100644 crates/apollo-compiler/tests/input_coercion.rs diff --git a/crates/apollo-compiler/tests/input_coercion.rs b/crates/apollo-compiler/tests/input_coercion.rs new file mode 100644 index 00000000..e419bc31 --- /dev/null +++ b/crates/apollo-compiler/tests/input_coercion.rs @@ -0,0 +1,50 @@ + +#[test] +fn test_graphql_float_variable_coercion() { + // Small schema with a Float in the input object + let sdl = r#" + type Car { id: ID! kilometers: Float! } + input CarInput { kilometers: Float! } + type Query { getCarById(id: ID!): Car } + type Mutation { insertACar(car: CarInput!): Car! + } + "#; + + let parsed_schema = Schema::parse_and_validate(sdl, "sdl").unwrap(); + + let executable_mutation = ExecutableDocument::parse_and_validate( + &parsed_schema, + "mutation MyCarInsertMutation($car: CarInput!){ insertACar(car:$car) { id kilometers } }", + "MyCarInsertMutation", + ) + .unwrap(); + + let operation = executable_mutation + .operations + .get(Some("MyCarInsertMutation")) + .unwrap(); + + let kilometers_value = 3000; + + // Provide an integer for a Float field + let input_variables = serde_json_bytes::json!({ "car": { "kilometers": kilometers_value } }); + let map = match input_variables { + serde_json_bytes::Value::Object(m) => m, + _ => unreachable!(), + }; + + // Coerce and validate. + let coerced = coerce_variable_values(&parsed_schema, operation, &map).unwrap(); + let vars_for_exec = coerced.into_inner(); + + // ---- Assertions ---- + let car = vars_for_exec + .get("car") + .and_then(|value| value.as_object()) + .expect("coerced `car` object"); + assert_eq!( + car.get("kilometers").unwrap(), + kilometers_value, + "kilometers should be present and a valid amount." + ); +} \ No newline at end of file diff --git a/crates/apollo-compiler/tests/introspection.rs b/crates/apollo-compiler/tests/introspection.rs index 2f20f961..60c6447d 100644 --- a/crates/apollo-compiler/tests/introspection.rs +++ b/crates/apollo-compiler/tests/introspection.rs @@ -321,53 +321,3 @@ fn mixed() { }"#]] .assert_eq(&response); } - -#[test] -fn test_graphql_float_variable_coercion() { - // Small schema with a Float in the input object - let sdl = r#" - type Car { id: ID! kilometers: Float! } - input CarInput { kilometers: Float! } - type Query { getCarById(id: ID!): Car } - type Mutation { insertACar(car: CarInput!): Car! - } - "#; - - let parsed_schema = Schema::parse_and_validate(sdl, "sdl").unwrap(); - - let executable_mutation = ExecutableDocument::parse_and_validate( - &parsed_schema, - "mutation MyCarInsertMutation($car: CarInput!){ insertACar(car:$car) { id kilometers } }", - "MyCarInsertMutation", - ) - .unwrap(); - - let operation = executable_mutation - .operations - .get(Some("MyCarInsertMutation")) - .unwrap(); - - let kilometers_value = 3000; - - // Provide an integer for a Float field - let input_variables = serde_json_bytes::json!({ "car": { "kilometers": kilometers_value } }); - let map = match input_variables { - serde_json_bytes::Value::Object(m) => m, - _ => unreachable!(), - }; - - // Coerce and validate. - let coerced = coerce_variable_values(&parsed_schema, operation, &map).unwrap(); - let vars_for_exec = coerced.into_inner(); - - // ---- Assertions ---- - let car = vars_for_exec - .get("car") - .and_then(|value| value.as_object()) - .expect("coerced `car` object"); - assert_eq!( - car.get("kilometers").unwrap(), - kilometers_value, - "kilometers should be present and a valid amount." - ); -} From c5f4bac4123eb047a89ac523c865b0de1987cf4b Mon Sep 17 00:00:00 2001 From: rb090 Date: Tue, 7 Oct 2025 12:36:45 +0200 Subject: [PATCH 4/7] Move input_coercion to test/validation Add it to mod.rs --- .../tests/{ => validation}/input_coercion.rs | 7 +++++-- crates/apollo-compiler/tests/validation/mod.rs | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) rename crates/apollo-compiler/tests/{ => validation}/input_coercion.rs (91%) diff --git a/crates/apollo-compiler/tests/input_coercion.rs b/crates/apollo-compiler/tests/validation/input_coercion.rs similarity index 91% rename from crates/apollo-compiler/tests/input_coercion.rs rename to crates/apollo-compiler/tests/validation/input_coercion.rs index e419bc31..d66cdb51 100644 --- a/crates/apollo-compiler/tests/input_coercion.rs +++ b/crates/apollo-compiler/tests/validation/input_coercion.rs @@ -1,3 +1,6 @@ +use apollo_compiler::request::coerce_variable_values; +use apollo_compiler::ExecutableDocument; +use apollo_compiler::Schema; #[test] fn test_graphql_float_variable_coercion() { @@ -17,7 +20,7 @@ fn test_graphql_float_variable_coercion() { "mutation MyCarInsertMutation($car: CarInput!){ insertACar(car:$car) { id kilometers } }", "MyCarInsertMutation", ) - .unwrap(); + .unwrap(); let operation = executable_mutation .operations @@ -47,4 +50,4 @@ fn test_graphql_float_variable_coercion() { kilometers_value, "kilometers should be present and a valid amount." ); -} \ No newline at end of file +} diff --git a/crates/apollo-compiler/tests/validation/mod.rs b/crates/apollo-compiler/tests/validation/mod.rs index e5b5ee18..ffbb7c84 100644 --- a/crates/apollo-compiler/tests/validation/mod.rs +++ b/crates/apollo-compiler/tests/validation/mod.rs @@ -1,5 +1,6 @@ mod field_merging; mod ignore_builtin_redefinition; +mod input_coercion; mod interface; mod object; mod operation; From a62c087f14e35836ee22fe37f7ca6bba581f080d Mon Sep 17 00:00:00 2001 From: rb090 Date: Tue, 7 Oct 2025 12:41:35 +0200 Subject: [PATCH 5/7] Remove newline before bracket for mutation declaration in sdl in unit test --- crates/apollo-compiler/tests/validation/input_coercion.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/apollo-compiler/tests/validation/input_coercion.rs b/crates/apollo-compiler/tests/validation/input_coercion.rs index d66cdb51..83f32310 100644 --- a/crates/apollo-compiler/tests/validation/input_coercion.rs +++ b/crates/apollo-compiler/tests/validation/input_coercion.rs @@ -9,8 +9,7 @@ fn test_graphql_float_variable_coercion() { type Car { id: ID! kilometers: Float! } input CarInput { kilometers: Float! } type Query { getCarById(id: ID!): Car } - type Mutation { insertACar(car: CarInput!): Car! - } + type Mutation { insertACar(car: CarInput!): Car! } "#; let parsed_schema = Schema::parse_and_validate(sdl, "sdl").unwrap(); From a4c357ef05b9bce4fe549b2860df55c623946a95 Mon Sep 17 00:00:00 2001 From: rb090 Date: Tue, 7 Oct 2025 15:39:51 +0200 Subject: [PATCH 6/7] Move input_coercion test back to apollo-compiler/tests Add module declaration to tests/main.rs --- crates/apollo-compiler/tests/{validation => }/input_coercion.rs | 0 crates/apollo-compiler/tests/main.rs | 1 + crates/apollo-compiler/tests/validation/mod.rs | 1 - 3 files changed, 1 insertion(+), 1 deletion(-) rename crates/apollo-compiler/tests/{validation => }/input_coercion.rs (100%) diff --git a/crates/apollo-compiler/tests/validation/input_coercion.rs b/crates/apollo-compiler/tests/input_coercion.rs similarity index 100% rename from crates/apollo-compiler/tests/validation/input_coercion.rs rename to crates/apollo-compiler/tests/input_coercion.rs diff --git a/crates/apollo-compiler/tests/main.rs b/crates/apollo-compiler/tests/main.rs index d18397fe..31514990 100644 --- a/crates/apollo-compiler/tests/main.rs +++ b/crates/apollo-compiler/tests/main.rs @@ -2,6 +2,7 @@ mod executable; mod extensions; mod field_set; mod field_type; +mod input_coercion; mod introspection; mod introspection_max_depth; mod locations; diff --git a/crates/apollo-compiler/tests/validation/mod.rs b/crates/apollo-compiler/tests/validation/mod.rs index ffbb7c84..e5b5ee18 100644 --- a/crates/apollo-compiler/tests/validation/mod.rs +++ b/crates/apollo-compiler/tests/validation/mod.rs @@ -1,6 +1,5 @@ mod field_merging; mod ignore_builtin_redefinition; -mod input_coercion; mod interface; mod object; mod operation; From 23d67b8e731eb502ac101f87d45c00514e5a982d Mon Sep 17 00:00:00 2001 From: rb090 Date: Tue, 7 Oct 2025 15:44:20 +0200 Subject: [PATCH 7/7] Fix value range validation in input_coercion Extend unit tests to cover failing Float coercion cases --- .../src/resolvers/input_coercion.rs | 8 +- .../apollo-compiler/tests/input_coercion.rs | 147 +++++++++++++++--- 2 files changed, 128 insertions(+), 27 deletions(-) diff --git a/crates/apollo-compiler/src/resolvers/input_coercion.rs b/crates/apollo-compiler/src/resolvers/input_coercion.rs index e546e966..9e025556 100644 --- a/crates/apollo-compiler/src/resolvers/input_coercion.rs +++ b/crates/apollo-compiler/src/resolvers/input_coercion.rs @@ -121,8 +121,12 @@ fn coerce_variable_value( "Float" => { // https://spec.graphql.org/September2025/#sec-Float.Input-Coercion // Accept any JSON number (`int` or `float`) that coerces to a finite f64, - // rejecting special values (NaN, +∞, -∞) as required by the GraphQL spec. - if value.as_f64().is_some_and(f64::is_finite) { + // rejecting special values (NaN, +∞, -∞) and values that exceed the representable + // numeric range of a 64-bit floating-point number, as required by the GraphQL spec. + if value + .as_f64() + .is_some_and(|num| num.is_finite() && num.abs() <= (i64::MAX as f64)) + { return Ok(value.clone()); } } diff --git a/crates/apollo-compiler/tests/input_coercion.rs b/crates/apollo-compiler/tests/input_coercion.rs index 83f32310..b9724c3a 100644 --- a/crates/apollo-compiler/tests/input_coercion.rs +++ b/crates/apollo-compiler/tests/input_coercion.rs @@ -1,52 +1,149 @@ use apollo_compiler::request::coerce_variable_values; use apollo_compiler::ExecutableDocument; use apollo_compiler::Schema; +use serde_json_bytes::ByteString; +use serde_json_bytes::Map; +use serde_json_bytes::Value; -#[test] -fn test_graphql_float_variable_coercion() { - // Small schema with a Float in the input object +/// +/// Builds and coerces a GraphQL mutation variable map for testing Float coercion behavior. +/// +/// Helper function, used in unit tests to verify how GraphQL variable coercion behaves when provided +/// with different numeric types or extreme values. +/// +/// It defines a minimal GraphQL schema with a `Car` type and a corresponding `CarInput` that +/// contains two `Float!` fields: `range` and `totalKilometers`. +/// The function then constructs a mutation using these input types and attempts to coerce the provided +/// values into the correct GraphQL variable format. +/// +/// # Type Parameters +/// * `R` – The type of the `range` argument, must implement [`Into`]. +/// * `K` – The type of the `total_kilometers` argument, must implement [`Into`]. +/// +/// # Arguments +/// * `range` – A numeric value (or convertible type) representing the range attribute of a car. +/// * `total_kilometers` – A numeric value (or convertible type) representing the total kilometers of a car. +/// +/// # Returns +/// A [`Result`] containing the coerced [`Map`] representing the +/// GraphQL variable map if coercion succeeds. +/// If coercion fails (for example, due to exceeding `f64` limits or type mismatches), +/// an error message is returned as a [`String`]. +fn build_and_coerce_test_mutation_variables( + range: R, + total_kilometers: K, +) -> Result, String> +where + R: Into, + K: Into, +{ + // Example schema with Float fields that will coerce ints to floats where needed. let sdl = r#" - type Car { id: ID! kilometers: Float! } - input CarInput { kilometers: Float! } + type Car { id: ID! range: Float! totalKilometers: Float! } + input CarInput { range: Float! totalKilometers: Float! } type Query { getCarById(id: ID!): Car } type Mutation { insertACar(car: CarInput!): Car! } "#; - let parsed_schema = Schema::parse_and_validate(sdl, "sdl").unwrap(); + let parsed_schema = Schema::parse_and_validate(sdl, "sdl").map_err(|e| format!("{e:?}"))?; + // Prepare a mutation that uses the variables passed in when this function is called. let executable_mutation = ExecutableDocument::parse_and_validate( &parsed_schema, - "mutation MyCarInsertMutation($car: CarInput!){ insertACar(car:$car) { id kilometers } }", - "MyCarInsertMutation", + "mutation InsertCarMutation($car: CarInput!){ insertACar(car:$car) { id range totalKilometers } }", + "InsertCarMutation", ) - .unwrap(); + .map_err(|e| format!("{e:?}"))?; let operation = executable_mutation .operations - .get(Some("MyCarInsertMutation")) - .unwrap(); + .get(Some("InsertCarMutation")) + .map_err(|e| format!("{e:?}"))?; - let kilometers_value = 3000; + // Build the GraphQL variables JSON, converting both values to `f64` to match the mutation’s `Float!` fields. + let input_variables = serde_json_bytes::json!({ + "car": { + "range": range.into(), + "totalKilometers": total_kilometers.into() + } + }); - // Provide an integer for a Float field - let input_variables = serde_json_bytes::json!({ "car": { "kilometers": kilometers_value } }); + // Extract the map for coercion. let map = match input_variables { - serde_json_bytes::Value::Object(m) => m, - _ => unreachable!(), + Value::Object(m) => m, + _ => return Err("variables JSON must be an object".into()), }; - // Coerce and validate. - let coerced = coerce_variable_values(&parsed_schema, operation, &map).unwrap(); + // Attempt coercion, return an error if it fails! + let coerced = + coerce_variable_values(&parsed_schema, operation, &map).map_err(|e| format!("{e:?}"))?; let vars_for_exec = coerced.into_inner(); - // ---- Assertions ---- - let car = vars_for_exec + // Return the inner `car` object. + vars_for_exec .get("car") - .and_then(|value| value.as_object()) - .expect("coerced `car` object"); + .and_then(Value::as_object) + .cloned() + .ok_or_else(|| "coerced `car` object missing".to_string()) +} + +#[test] +fn test_graphql_float_variable_coercion_with_expected_float_and_int() { + let car = build_and_coerce_test_mutation_variables(344.678_f64, 50_000_i32).unwrap(); + + let range = car + .get("range") + .and_then(Value::as_f64) + .expect("range as f64"); + let total_km = car + .get("totalKilometers") + .and_then(Value::as_f64) + .expect("totalKilometers as f64"); + assert_eq!( - car.get("kilometers").unwrap(), - kilometers_value, - "kilometers should be present and a valid amount." + 344.678_f64, range, + "Expected `range` to be correctly coerced into Float." + ); + assert_eq!( + 50_000_f64, total_km, + "Expected `totalKilometers` to be correctly coerced into Float." + ); +} + +#[test] +fn test_graphql_failing_coercion_because_greater_i64_max() { + // Use a very large integer value to simulate a value that exceeds the precision range of a 64-bit floating point number. + // When cast to f64, this value will lose precision and trigger coercion issues in GraphQL variable validation for `Float! fields. + let range: f64 = "170141183460469231731687303715884105727" + .parse::() + .expect("invalid float"); + + // Provide a normal, expected value for the 2nd field. + let total_kilometers = 50_000; + + let car = build_and_coerce_test_mutation_variables(range, total_kilometers); + + assert!( + car.is_err(), + "Expected coercion to fail for given 'range' and 'total_kilometers' params." + ); +} + +#[test] +fn test_graphql_failing_coercion_because_infinity_value() { + // Define a floating-point value for the `range` field. + // This represents a typical valid input value within normal f64 precision limits. + let range = 433.777_f64; + + // An extremely large floating-point value which is beyond the maximum representable f64 range + // and overflows to `f64::INFINITY`. + // This should trigger coercion validation errors when used in GraphQL input variable. + let total_kilometers = "1e1000".parse::().expect("invalid float"); + + let car = build_and_coerce_test_mutation_variables(range, total_kilometers); + + assert!( + car.is_err(), + "Expected coercion to fail for given 'range' and 'total_kilometers' params." ); }