diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 270ba79b7..5a969a99b 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -13,7 +13,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi ### BC Breaks - Upgraded [`chrono-tz` crate] integration to [0.10 version](https://github.com/chronotope/chrono-tz/releases/tag/v0.10.0). ([#1252], [#1284]) -- Bumped up [MSRV] to 1.85. ([#1272], [todo]) +- Bumped up [MSRV] to 1.85. ([#1272], [1b1fc618]) - Corrected compliance with newer [graphql-scalars.dev] specs: ([#1275], [#1277]) - Switched `LocalDateTime` scalars to `yyyy-MM-ddTHH:mm:ss` format in types: - `chrono::NaiveDateTime`. @@ -48,6 +48,10 @@ All user visible changes to `juniper` crate will be documented in this file. Thi - Upgraded [GraphiQL] to [4.0.2 version](https://github.com/graphql/graphiql/blob/graphiql%404.0.2/packages/graphiql/CHANGELOG.md#402). ([#1316]) +### Fixed + +- Incorrect error propagation inside fragments. ([#1318], [#1287]) + [#1252]: /../../pull/1252 [#1270]: /../../issues/1270 [#1271]: /../../pull/1271 @@ -57,9 +61,11 @@ All user visible changes to `juniper` crate will be documented in this file. Thi [#1278]: /../../pull/1278 [#1281]: /../../pull/1281 [#1284]: /../../pull/1284 +[#1287]: /../../issues/1287 [#1311]: /../../pull/1311 [#1316]: /../../pull/1316 -[todo]: /../../commit/todo +[#1318]: /../../pull/1318 +[1b1fc618]: /../../commit/1b1fc61879ffdd640d741e187dc20678bf7ab295 diff --git a/juniper/src/executor_tests/executor.rs b/juniper/src/executor_tests/executor.rs index c0f1094d9..3c04e7398 100644 --- a/juniper/src/executor_tests/executor.rs +++ b/juniper/src/executor_tests/executor.rs @@ -738,259 +738,958 @@ mod propagates_errors_to_nullable_fields { } } - #[tokio::test] - async fn nullable_first_level() { - let schema = RootNode::new( + fn schema() -> RootNode<'static, Schema, EmptyMutation, EmptySubscription> { + RootNode::new( Schema, EmptyMutation::<()>::new(), EmptySubscription::<()>::new(), - ); + ) + } + + #[tokio::test] + async fn nullable_first_level() { + // language=GraphQL let doc = r"{ inner { nullableErrorField } }"; - let vars = graphql_vars! {}; - let (result, errs) = crate::execute(doc, None, &schema, &vars, &()) - .await - .expect("Execution failed"); + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!( + result, + graphql_value!({"inner": {"nullableErrorField": null}}), + ); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(10, 0, 10), + &["inner", "nullableErrorField"], + FieldError::new("Error for nullableErrorField", graphql_value!(null)), + )], + ); + } + } - println!("Result: {result:#?}"); + #[tokio::test] + async fn nullable_first_level_in_fragment() { + // language=GraphQL + let doc = r" + { inner { ...Frag } } + fragment Frag on Inner { nullableErrorField } + "; - assert_eq!( - result, - graphql_value!({"inner": {"nullableErrorField": null}}), - ); + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!( + result, + graphql_value!({"inner": {"nullableErrorField": null}}), + ); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(72, 2, 37), + &["inner", "nullableErrorField"], + FieldError::new("Error for nullableErrorField", graphql_value!(null)), + )], + ); + } + } - assert_eq!( - errs, - vec![ExecutionError::new( - SourcePosition::new(10, 0, 10), - &["inner", "nullableErrorField"], - FieldError::new("Error for nullableErrorField", graphql_value!(null)), - )], - ); + #[tokio::test] + async fn nullable_first_level_in_inline_fragment() { + // language=GraphQL + let doc = r"{ inner { ...{ nullableErrorField } } }"; + + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!( + result, + graphql_value!({"inner": {"nullableErrorField": null}}), + ); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(15, 0, 15), + &["inner", "nullableErrorField"], + FieldError::new("Error for nullableErrorField", graphql_value!(null)), + )], + ); + } + } + + #[tokio::test] + async fn nullable_first_level_in_inline_typed_fragment() { + // language=GraphQL + let doc = r"{ inner { ...on Inner { nullableErrorField } } }"; + + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!( + result, + graphql_value!({"inner": {"nullableErrorField": null}}), + ); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(24, 0, 24), + &["inner", "nullableErrorField"], + FieldError::new("Error for nullableErrorField", graphql_value!(null)), + )], + ); + } } #[tokio::test] async fn non_nullable_first_level() { - let schema = RootNode::new( - Schema, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); + // language=GraphQL let doc = r"{ inner { nonNullableErrorField } }"; - let vars = graphql_vars! {}; - let (result, errs) = crate::execute(doc, None, &schema, &vars, &()) - .await - .expect("Execution failed"); + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!(result, graphql_value!(null)); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(10, 0, 10), + &["inner", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + )], + ); + } + } - println!("Result: {result:#?}"); + #[tokio::test] + async fn non_nullable_first_level_in_fragment() { + // language=GraphQL + let doc = r" + { inner { ...Frag } } + fragment Frag on Inner { nonNullableErrorField } + "; - assert_eq!(result, graphql_value!(null)); + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!(result, graphql_value!(null)); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(72, 2, 37), + &["inner", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + )], + ); + } + } - assert_eq!( - errs, - vec![ExecutionError::new( - SourcePosition::new(10, 0, 10), - &["inner", "nonNullableErrorField"], - FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), - )], - ); + #[tokio::test] + async fn non_nullable_first_level_in_inline_fragment() { + // language=GraphQL + let doc = r"{ inner { ...{ nonNullableErrorField } } }"; + + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!(result, graphql_value!(null)); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(15, 0, 15), + &["inner", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + )], + ); + } + } + + #[tokio::test] + async fn non_nullable_first_level_in_inline_typed_fragment() { + // language=GraphQL + let doc = r"{ inner { ...on Inner { nonNullableErrorField } } }"; + + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!(result, graphql_value!(null)); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(24, 0, 24), + &["inner", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + )], + ); + } } #[tokio::test] async fn custom_error_first_level() { - let schema = RootNode::new( - Schema, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); + // language=GraphQL let doc = r"{ inner { customErrorField } }"; - let vars = graphql_vars! {}; - let (result, errs) = crate::execute(doc, None, &schema, &vars, &()) - .await - .expect("Execution failed"); + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!(result, graphql_value!(null)); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(10, 0, 10), + &["inner", "customErrorField"], + FieldError::new("Not Found", graphql_value!({"type": "NOT_FOUND"})), + )], + ); + } + } - println!("Result: {result:#?}"); + #[tokio::test] + async fn nullable_nested_level() { + // language=GraphQL + let doc = r"{ inner { nullableField { nonNullableErrorField } } }"; - assert_eq!(result, graphql_value!(null)); + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!(result, graphql_value!({"inner": {"nullableField": null}}),); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(26, 0, 26), + &["inner", "nullableField", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + )], + ); + } + } - assert_eq!( - errs, - vec![ExecutionError::new( - SourcePosition::new(10, 0, 10), - &["inner", "customErrorField"], - FieldError::new("Not Found", graphql_value!({"type": "NOT_FOUND"})), - )], - ); + #[tokio::test] + async fn nullable_nested_level_in_fragment() { + // language=GraphQL + let doc = r" + { inner { nullableField { ...Frag } } } + fragment Frag on Inner { nonNullableErrorField } + "; + + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!(result, graphql_value!({"inner": {"nullableField": null}}),); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(90, 2, 37), + &["inner", "nullableField", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + )], + ); + } } #[tokio::test] - async fn nullable_nested_level() { - let schema = RootNode::new( - Schema, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); - let doc = r"{ inner { nullableField { nonNullableErrorField } } }"; - let vars = graphql_vars! {}; + async fn nullable_nested_level_in_inline_fragment() { + // language=GraphQL + let doc = r"{ inner { nullableField { ...{ nonNullableErrorField } } } }"; + + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!(result, graphql_value!({"inner": {"nullableField": null}}),); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(31, 0, 31), + &["inner", "nullableField", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + )], + ); + } + } - let (result, errs) = crate::execute(doc, None, &schema, &vars, &()) - .await - .expect("Execution failed"); + #[tokio::test] + async fn nullable_nested_level_in_inline_typed_fragment() { + // language=GraphQL + let doc = r"{ inner { nullableField { ...on Inner { nonNullableErrorField } } } }"; + + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!(result, graphql_value!({"inner": {"nullableField": null}}),); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(40, 0, 40), + &["inner", "nullableField", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + )], + ); + } + } - println!("Result: {result:#?}"); + #[tokio::test] + async fn nullable_in_fragment_nested_level() { + // language=GraphQL + let doc = r" + { inner { ...Frag } } + fragment Frag on Inner { nullableField { nonNullableErrorField } } + "; - assert_eq!(result, graphql_value!({"inner": {"nullableField": null}}),); + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!(result, graphql_value!({"inner": {"nullableField": null}}),); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(88, 2, 53), + &["inner", "nullableField", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + )], + ); + } + } - assert_eq!( - errs, - vec![ExecutionError::new( - SourcePosition::new(26, 0, 26), - &["inner", "nullableField", "nonNullableErrorField"], - FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), - )], - ); + #[tokio::test] + async fn nullable_in_inline_fragment_nested_level() { + // language=GraphQL + let doc = r"{ inner { ...{ nullableField { nonNullableErrorField } } } }"; + + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!(result, graphql_value!({"inner": {"nullableField": null}}),); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(31, 0, 31), + &["inner", "nullableField", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + )], + ); + } + } + + #[tokio::test] + async fn nullable_in_inline_typed_fragment_nested_level() { + // language=GraphQL + let doc = r"{ inner { ...on Inner { nullableField { nonNullableErrorField } } } }"; + + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!(result, graphql_value!({"inner": {"nullableField": null}}),); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(40, 0, 40), + &["inner", "nullableField", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + )], + ); + } } #[tokio::test] async fn non_nullable_nested_level() { - let schema = RootNode::new( - Schema, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); + // language=GraphQL let doc = r"{ inner { nonNullableField { nonNullableErrorField } } }"; - let vars = graphql_vars! {}; - let (result, errs) = crate::execute(doc, None, &schema, &vars, &()) - .await - .expect("Execution failed"); + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!(result, graphql_value!(null)); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(29, 0, 29), + &["inner", "nonNullableField", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + )], + ); + } + } - println!("Result: {result:#?}"); + #[tokio::test] + async fn non_nullable_nested_level_in_fragment() { + // language=GraphQL + let doc = r" + { inner { nonNullableField { ...Frag } } } + fragment Frag on Inner { nonNullableErrorField } + "; - assert_eq!(result, graphql_value!(null)); + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!(result, graphql_value!(null)); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(93, 2, 37), + &["inner", "nonNullableField", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + )], + ); + } + } - assert_eq!( - errs, - vec![ExecutionError::new( - SourcePosition::new(29, 0, 29), - &["inner", "nonNullableField", "nonNullableErrorField"], - FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), - )], - ); + #[tokio::test] + async fn non_nullable_nested_level_in_inline_fragment() { + // language=GraphQL + let doc = r"{ inner { nonNullableField { ...{ nonNullableErrorField } } } }"; + + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!(result, graphql_value!(null)); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(34, 0, 34), + &["inner", "nonNullableField", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + )], + ); + } } #[tokio::test] - async fn nullable_innermost() { - let schema = RootNode::new( - Schema, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); - let doc = r"{ inner { nonNullableField { nullableErrorField } } }"; - let vars = graphql_vars! {}; + async fn non_nullable_nested_level_in_inline_typed_fragment() { + // language=GraphQL + let doc = r"{ inner { nonNullableField { ...on Inner { nonNullableErrorField } } } }"; + + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!(result, graphql_value!(null)); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(43, 0, 43), + &["inner", "nonNullableField", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + )], + ); + } + } - let (result, errs) = crate::execute(doc, None, &schema, &vars, &()) - .await - .expect("Execution failed"); + #[tokio::test] + async fn non_nullable_in_fragment_nested_level() { + // language=GraphQL + let doc = r" + { inner { ...Frag } } + fragment Frag on Inner { nonNullableField { nonNullableErrorField } } + "; - println!("Result: {result:#?}"); + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!(result, graphql_value!(null)); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(91, 2, 56), + &["inner", "nonNullableField", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + )], + ); + } + } - assert_eq!( - result, - graphql_value!({"inner": {"nonNullableField": {"nullableErrorField": null}}}), - ); + #[tokio::test] + async fn non_nullable_in_inline_fragment_nested_level() { + // language=GraphQL + let doc = r"{ inner { ...{ nonNullableField { nonNullableErrorField } } } }"; + + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!(result, graphql_value!(null)); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(34, 0, 34), + &["inner", "nonNullableField", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + )], + ); + } + } - assert_eq!( - errs, - vec![ExecutionError::new( - SourcePosition::new(29, 0, 29), - &["inner", "nonNullableField", "nullableErrorField"], - FieldError::new("Error for nullableErrorField", graphql_value!(null)), - )], - ); + #[tokio::test] + async fn non_nullable_in_inline_typed_fragment_nested_level() { + // language=GraphQL + let doc = r"{ inner { ...on Inner { nonNullableField { nonNullableErrorField } } } }"; + + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!(result, graphql_value!(null)); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(43, 0, 43), + &["inner", "nonNullableField", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + )], + ); + } } #[tokio::test] - async fn non_null_list() { - let schema = RootNode::new( - Schema, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); - let doc = r"{ inners { nonNullableErrorField } }"; - let vars = graphql_vars! {}; + async fn nullable_innermost() { + // language=GraphQL + let doc = r"{ inner { nonNullableField { nullableErrorField } } }"; - let (result, errs) = crate::execute(doc, None, &schema, &vars, &()) - .await - .expect("Execution failed"); + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!( + result, + graphql_value!({"inner": {"nonNullableField": {"nullableErrorField": null}}}), + ); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(29, 0, 29), + &["inner", "nonNullableField", "nullableErrorField"], + FieldError::new("Error for nullableErrorField", graphql_value!(null)), + )], + ); + } + } - println!("Result: {result:#?}"); + #[tokio::test] + async fn nullable_innermost_in_fragment() { + // language=GraphQL + let doc = r" + { inner { ...Frag } } + fragment Frag on Inner { nonNullableField { nullableErrorField } } + "; - assert_eq!(result, graphql_value!(null)); + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!( + result, + graphql_value!({"inner": {"nonNullableField": {"nullableErrorField": null}}}), + ); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(91, 2, 56), + &["inner", "nonNullableField", "nullableErrorField"], + FieldError::new("Error for nullableErrorField", graphql_value!(null)), + )], + ); + } + } - assert_eq!( - errs, - vec![ExecutionError::new( - SourcePosition::new(11, 0, 11), - &["inners", "nonNullableErrorField"], - FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), - )], - ); + #[tokio::test] + async fn nullable_innermost_in_inline_fragment() { + // language=GraphQL + let doc = r"{ inner { ...{ nonNullableField { nullableErrorField } } } }"; + + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!( + result, + graphql_value!({"inner": {"nonNullableField": {"nullableErrorField": null}}}), + ); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(34, 0, 34), + &["inner", "nonNullableField", "nullableErrorField"], + FieldError::new("Error for nullableErrorField", graphql_value!(null)), + )], + ); + } } #[tokio::test] - async fn non_null_list_of_nullable() { - let schema = RootNode::new( - Schema, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); - let doc = r"{ nullableInners { nonNullableErrorField } }"; - let vars = graphql_vars! {}; + async fn nullable_innermost_in_inline_typed_fragment() { + // language=GraphQL + let doc = r"{ inner { ...on Inner { nonNullableField { nullableErrorField } } } }"; + + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!( + result, + graphql_value!({"inner": {"nonNullableField": {"nullableErrorField": null}}}), + ); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(43, 0, 43), + &["inner", "nonNullableField", "nullableErrorField"], + FieldError::new("Error for nullableErrorField", graphql_value!(null)), + )], + ); + } + } - let (result, errs) = crate::execute(doc, None, &schema, &vars, &()) - .await - .expect("Execution failed"); + #[tokio::test] + async fn nullable_in_fragment_innermost() { + // language=GraphQL + let doc = r" + { inner { nonNullableField { ...Frag } } } + fragment Frag on Inner { nullableErrorField } + "; - println!("Result: {result:#?}"); + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!( + result, + graphql_value!({"inner": {"nonNullableField": {"nullableErrorField": null}}}), + ); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(93, 2, 37), + &["inner", "nonNullableField", "nullableErrorField"], + FieldError::new("Error for nullableErrorField", graphql_value!(null)), + )], + ); + } + } - assert_eq!( - result, - graphql_value!({"nullableInners": [null, null, null, null, null]}), - ); + #[tokio::test] + async fn nullable_in_inline_fragment_innermost() { + // language=GraphQL + let doc = r"{ inner { nonNullableField { ...{ nullableErrorField } } } }"; + + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!( + result, + graphql_value!({"inner": {"nonNullableField": {"nullableErrorField": null}}}), + ); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(34, 0, 34), + &["inner", "nonNullableField", "nullableErrorField"], + FieldError::new("Error for nullableErrorField", graphql_value!(null)), + )], + ); + } + } - assert_eq!( - errs, - vec![ - ExecutionError::new( - SourcePosition::new(19, 0, 19), - &["nullableInners", "nonNullableErrorField"], - FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), - ), - ExecutionError::new( - SourcePosition::new(19, 0, 19), - &["nullableInners", "nonNullableErrorField"], - FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), - ), - ExecutionError::new( - SourcePosition::new(19, 0, 19), - &["nullableInners", "nonNullableErrorField"], - FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), - ), - ExecutionError::new( - SourcePosition::new(19, 0, 19), - &["nullableInners", "nonNullableErrorField"], - FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), - ), - ExecutionError::new( - SourcePosition::new(19, 0, 19), - &["nullableInners", "nonNullableErrorField"], + #[tokio::test] + async fn nullable_in_inline_typed_fragment_innermost() { + // language=GraphQL + let doc = r"{ inner { nonNullableField { ...on Inner { nullableErrorField } } } }"; + + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!( + result, + graphql_value!({"inner": {"nonNullableField": {"nullableErrorField": null}}}), + ); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(43, 0, 43), + &["inner", "nonNullableField", "nullableErrorField"], + FieldError::new("Error for nullableErrorField", graphql_value!(null)), + )], + ); + } + } + + #[tokio::test] + async fn non_null_list() { + // language=GraphQL + let doc = r"{ inners { nonNullableErrorField } }"; + + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!(result, graphql_value!(null)); + + assert_eq!( + errs, + vec![ExecutionError::new( + SourcePosition::new(11, 0, 11), + &["inners", "nonNullableErrorField"], FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), - ), - ], - ); + )], + ); + } + } + + #[tokio::test] + async fn non_null_list_of_nullable() { + // language=GraphQL + let doc = r"{ nullableInners { nonNullableErrorField } }"; + + for (result, errs) in [ + crate::execute(doc, None, &schema(), &graphql_vars! {}, &()) + .await + .expect("async execution failed"), + crate::execute_sync(doc, None, &schema(), &graphql_vars! {}, &()) + .expect("sync execution failed"), + ] { + println!("Result: {result:#?}"); + + assert_eq!( + result, + graphql_value!({"nullableInners": [null, null, null, null, null]}), + ); + + assert_eq!( + errs, + vec![ + ExecutionError::new( + SourcePosition::new(19, 0, 19), + &["nullableInners", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + ), + ExecutionError::new( + SourcePosition::new(19, 0, 19), + &["nullableInners", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + ), + ExecutionError::new( + SourcePosition::new(19, 0, 19), + &["nullableInners", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + ), + ExecutionError::new( + SourcePosition::new(19, 0, 19), + &["nullableInners", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + ), + ExecutionError::new( + SourcePosition::new(19, 0, 19), + &["nullableInners", "nonNullableErrorField"], + FieldError::new("Error for nonNullableErrorField", graphql_value!(null)), + ), + ], + ); + } } } diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index 859c18497..15d9f3a53 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -9,6 +9,11 @@ extern crate core; extern crate self as juniper; +#[cfg(test)] +mod for_benches_only { + use bencher as _; +} + use std::fmt; // These are required by the code generated via the `juniper_codegen` macros. diff --git a/juniper/src/types/async_await.rs b/juniper/src/types/async_await.rs index 9e6e4a2ce..2595b34cd 100644 --- a/juniper/src/types/async_await.rs +++ b/juniper/src/types/async_await.rs @@ -159,11 +159,13 @@ where )) } +#[derive(Debug)] struct AsyncField { name: String, value: Option>, } +#[derive(Debug)] enum AsyncValue { Field(AsyncField), Nested(Value), @@ -184,16 +186,17 @@ where use futures::stream::{FuturesOrdered, StreamExt as _}; #[enum_derive(Future)] - enum AsyncValueFuture { - Field(A), - FragmentSpread(B), - InlineFragment1(C), - InlineFragment2(D), + enum AsyncValueFuture { + Field1(F1), + Field2(F2), + FragmentSpread(FS), + InlineFragment1(IF1), + InlineFragment2(IF2), } let mut object = Object::with_capacity(selection_set.len()); - let mut async_values = FuturesOrdered::>::new(); + let mut async_values = FuturesOrdered::>::new(); let meta_type = executor .schema() @@ -258,7 +261,7 @@ where let is_non_null = meta_field.field_type.is_non_null(); let response_name = response_name.to_string(); - async_values.push_back(AsyncValueFuture::Field(async move { + async_values.push_back(AsyncValueFuture::Field1(async move { // TODO: implement custom future type instead of // two-level boxing. let res = instance @@ -327,8 +330,23 @@ where })), )); } - } else if let Err(e) = sub_result { - sub_exec.push_error_at(e, span.start); + } else { + if let Err(e) = sub_result { + sub_exec.push_error_at(e, span.start); + } + // NOTE: Executing a fragment cannot really result in anything other + // than `Value::Object`, because it represents a set of fields. + // So, if an error happens or a `Value::Null` is returned, it's an + // indication that the fragment execution failed somewhere and, + // because of non-`null` types involved, its error should be + // propagated to the parent field, which is done here by returning + // a `Value::Null`. + async_values.push_back(AsyncValueFuture::Field2(future::ready( + AsyncValue::Field(AsyncField { + name: String::new(), // doesn't matter here + value: None, + }), + ))); } } } @@ -371,8 +389,23 @@ where })), )); } - } else if let Err(e) = sub_result { - sub_exec.push_error_at(e, span.start); + } else { + if let Err(e) = sub_result { + sub_exec.push_error_at(e, span.start); + } + // NOTE: Executing a fragment cannot really result in anything other + // than `Value::Object`, because it represents a set of fields. + // So, if an error happens or a `Value::Null` is returned, it's an + // indication that the fragment execution failed somewhere and, + // because of non-`null` types involved, its error should be + // propagated to the parent field, which is done here by returning + // a `Value::Null`. + async_values.push_back(AsyncValueFuture::Field2(future::ready( + AsyncValue::Field(AsyncField { + name: String::new(), // doesn't matter here + value: None, + }), + ))); } } } else { diff --git a/juniper/src/types/base.rs b/juniper/src/types/base.rs index fd720a8c2..5e5f668cf 100644 --- a/juniper/src/types/base.rs +++ b/juniper/src/types/base.rs @@ -537,8 +537,18 @@ where for (k, v) in object { merge_key_into(result, &k, v); } - } else if let Err(e) = sub_result { - sub_exec.push_error_at(e, span.start); + } else { + if let Err(e) = sub_result { + sub_exec.push_error_at(e, span.start); + } + // NOTE: Executing a fragment cannot really result in anything other + // than `Value::Object`, because it represents a set of fields. + // So, if an error happens or a `Value::Null` is returned, it's an + // indication that the fragment execution failed somewhere and, + // because of non-`null` types involved, its error should be + // propagated to the parent field, which is done here by returning + // a `false`. + return false; } } } @@ -573,8 +583,18 @@ where for (k, v) in object { merge_key_into(result, &k, v); } - } else if let Err(e) = sub_result { - sub_exec.push_error_at(e, span.start); + } else { + if let Err(e) = sub_result { + sub_exec.push_error_at(e, span.start); + } + // NOTE: Executing a fragment cannot really result in anything other + // than `Value::Object`, because it represents a set of fields. + // So, if an error happens or a `Value::Null` is returned, it's an + // indication that the fragment execution failed somewhere and, + // because of non-`null` types involved, its error should be + // propagated to the parent field, which is done here by returning + // a `false`. + return false; } } } else if !resolve_selection_set_into( diff --git a/juniper/src/validation/test_harness.rs b/juniper/src/validation/test_harness.rs index 822b59da6..577fe6c65 100644 --- a/juniper/src/validation/test_harness.rs +++ b/juniper/src/validation/test_harness.rs @@ -62,6 +62,7 @@ enum FurColor { Spotted, } +#[expect(dead_code, reason = "GraphQL schema testing")] #[derive(Debug)] struct ComplexInput { required_field: bool, diff --git a/tests/integration/tests/issue_1287.rs b/tests/integration/tests/issue_1287.rs new file mode 100644 index 000000000..c5e0d3e91 --- /dev/null +++ b/tests/integration/tests/issue_1287.rs @@ -0,0 +1,90 @@ +//! Checks that error is propagated from a fragment the same way as without it. +//! See [#1287](https://github.com/graphql-rust/juniper/issues/1287) for details. + +use juniper::{EmptyMutation, EmptySubscription, Variables, graphql_object}; + +struct MyObject; + +#[graphql_object] +impl MyObject { + fn erroring_field() -> Result { + Err("This field always errors") + } +} + +struct Query; + +#[graphql_object] +impl Query { + fn my_object() -> MyObject { + MyObject + } + + fn just_a_field() -> i32 { + 3 + } +} + +type Schema = juniper::RootNode<'static, Query, EmptyMutation, EmptySubscription>; + +#[tokio::test] +async fn error_propagates_same_way() { + // language=GraphQL + let without_fragment = r"{ + myObject { erroringField } + justAField + }"; + // language=GraphQL + let with_fragment = r" + query { + myObject { + ...MyObjectFragment + } + justAField + } + + fragment MyObjectFragment on MyObject { + erroringField + } + "; + + let (expected, _) = juniper::execute( + without_fragment, + None, + &Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()), + &Variables::new(), + &(), + ) + .await + .unwrap(); + let (actual, _) = juniper::execute( + with_fragment, + None, + &Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()), + &Variables::new(), + &(), + ) + .await + .unwrap(); + + assert_eq!(actual, expected, "async execution mismatch"); + + let (expected, _) = juniper::execute_sync( + without_fragment, + None, + &Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()), + &Variables::new(), + &(), + ) + .unwrap(); + let (actual, _) = juniper::execute_sync( + with_fragment, + None, + &Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()), + &Variables::new(), + &(), + ) + .unwrap(); + + assert_eq!(actual, expected, "sync execution mismatch"); +}