diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs index 23e3fcf4b7e..b806ccb8a5c 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs @@ -16,6 +16,7 @@ public static class LogEntryCodes public const string FieldArgumentTypesNotMergeable = "FIELD_ARGUMENT_TYPES_NOT_MERGEABLE"; public const string FieldWithMissingRequiredArgument = "FIELD_WITH_MISSING_REQUIRED_ARGUMENT"; public const string InputFieldDefaultMismatch = "INPUT_FIELD_DEFAULT_MISMATCH"; + public const string InputFieldReferencesInaccessibleType = "INPUT_FIELD_REFERENCES_INACCESSIBLE_TYPE"; public const string InputFieldTypesNotMergeable = "INPUT_FIELD_TYPES_NOT_MERGEABLE"; public const string InputWithMissingRequiredFields = "INPUT_WITH_MISSING_REQUIRED_FIELDS"; public const string InterfaceFieldNoImplementation = "INTERFACE_FIELD_NO_IMPLEMENTATION"; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs index ba3ad608671..a4bfbbf6ab7 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs @@ -310,6 +310,27 @@ public static LogEntry InputFieldDefaultMismatch( schemaA); } + public static LogEntry InputFieldReferencesInaccessibleType( + MutableInputFieldDefinition field, + string typeName, + string referenceTypeName, + MutableSchemaDefinition schema) + { + var coordinate = new SchemaCoordinate(typeName, field.Name); + + return new LogEntry( + string.Format( + LogEntryHelper_InputFieldReferencesInaccessibleType, + field.Name, + typeName, + referenceTypeName), + LogEntryCodes.InputFieldReferencesInaccessibleType, + LogSeverity.Error, + coordinate, + field, + schema); + } + public static LogEntry InputFieldTypesNotMergeable( MutableInputFieldDefinition field, string typeName, diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRule.cs new file mode 100644 index 00000000000..99f52afa8db --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRule.cs @@ -0,0 +1,40 @@ +using HotChocolate.Fusion.Events; +using HotChocolate.Fusion.Events.Contracts; +using HotChocolate.Fusion.Extensions; +using HotChocolate.Types; +using static HotChocolate.Fusion.Logging.LogEntryHelper; + +namespace HotChocolate.Fusion.PostMergeValidationRules; + +/// +/// In a composed schema, a field within an input type must only reference types that are exposed. +/// This requirement guarantees that public types do not reference inaccessible structures +/// which are intended for internal use. +/// +/// +/// Specification +/// +internal sealed class InputFieldReferencesInaccessibleTypeRule : IEventHandler +{ + public void Handle(InputFieldEvent @event, CompositionContext context) + { + var (field, type, schema) = @event; + + if (field.HasFusionInaccessibleDirective()) + { + return; + } + + var fieldType = field.Type.AsTypeDefinition(); + + if (fieldType.HasFusionInaccessibleDirective()) + { + context.Log.Write( + InputFieldReferencesInaccessibleType( + field, + type.Name, + fieldType.Name, + schema)); + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs index 1ed9e0d0ecd..2b5ce908e07 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs @@ -572,6 +572,15 @@ internal static string LogEntryHelper_InputFieldDefaultMismatch { } } + /// + /// Looks up a localized string similar to The merged input field '{0}' in type '{1}' cannot reference the inaccessible type '{2}'.. + /// + internal static string LogEntryHelper_InputFieldReferencesInaccessibleType { + get { + return ResourceManager.GetString("LogEntryHelper_InputFieldReferencesInaccessibleType", resourceCulture); + } + } + /// /// Looks up a localized string similar to The input field '{0}' has a different type shape in schema '{1}' than it does in schema '{2}'.. /// diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx index c3c4b3431fb..85c0f6cb7ca 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx @@ -189,6 +189,9 @@ The default value '{0}' of input field '{1}' in schema '{2}' differs from the default value of '{3}' in schema '{4}'. + + The merged input field '{0}' in type '{1}' cannot reference the inaccessible type '{2}'. + The input field '{0}' has a different type shape in schema '{1}' than it does in schema '{2}'. diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs index 115593fe8fd..ecfa629147b 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs @@ -126,6 +126,7 @@ public CompositionResult Compose() new EmptyMergedInterfaceTypeRule(), new EmptyMergedObjectTypeRule(), new EmptyMergedUnionTypeRule(), + new InputFieldReferencesInaccessibleTypeRule(), new InterfaceFieldNoImplementationRule(), new NonNullInputFieldIsInaccessibleRule(), new NoQueriesRule(), diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRuleTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRuleTests.cs new file mode 100644 index 00000000000..8b2c883004c --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRuleTests.cs @@ -0,0 +1,133 @@ +using System.Collections.Immutable; +using HotChocolate.Fusion.Logging; + +namespace HotChocolate.Fusion.PostMergeValidationRules; + +public sealed class InputFieldReferencesInaccessibleTypeRuleTests : CompositionTestBase +{ + private static readonly object s_rule = new InputFieldReferencesInaccessibleTypeRule(); + private static readonly ImmutableArray s_rules = [s_rule]; + private readonly CompositionLog _log = new(); + + [Theory] + [MemberData(nameof(ValidExamplesData))] + public void Examples_Valid(string[] sdl) + { + // arrange + var schemas = CreateSchemaDefinitions(sdl); + var merger = new SourceSchemaMerger(schemas); + var mergeResult = merger.Merge(); + var validator = new PostMergeValidator(mergeResult.Value, s_rules, schemas, _log); + + // act + var result = validator.Validate(); + + // assert + Assert.True(result.IsSuccess); + Assert.True(_log.IsEmpty); + } + + [Theory] + [MemberData(nameof(InvalidExamplesData))] + public void Examples_Invalid(string[] sdl, string[] errorMessages) + { + // arrange + var schemas = CreateSchemaDefinitions(sdl); + var merger = new SourceSchemaMerger(schemas); + var mergeResult = merger.Merge(); + var validator = new PostMergeValidator(mergeResult.Value, s_rules, schemas, _log); + + // act + var result = validator.Validate(); + + // assert + Assert.True(result.IsFailure); + Assert.Equal(errorMessages, _log.Select(e => e.Message).ToArray()); + Assert.True(_log.All(e => e.Code == "INPUT_FIELD_REFERENCES_INACCESSIBLE_TYPE")); + Assert.True(_log.All(e => e.Severity == LogSeverity.Error)); + } + + public static TheoryData ValidExamplesData() + { + return new TheoryData + { + // A valid case where a public input field references another public input type. + { + [ + """ + # Schema A + input Input1 { + field1: String! + field2: Input2 + } + + input Input2 { + field3: String + } + """, + """ + # Schema B + input Input2 { + field3: String + } + """ + ] + }, + // Another valid case is where the field is not exposed in the composed schema. + { + [ + """ + # Schema A + input Input1 { + field1: String! + field2: Input2 @inaccessible + } + + input Input2 { + field3: String + } + """, + """ + # Schema B + input Input2 @inaccessible { + field3: String + } + """ + ] + } + }; + } + + public static TheoryData InvalidExamplesData() + { + return new TheoryData + { + // An invalid case is when an input field references an inaccessible type. + { + [ + """ + # Schema A + input Input1 { + field1: String! + field2: Input2! + } + + input Input2 { + field3: String + } + """, + """ + # Schema B + input Input2 @inaccessible { + field3: String + } + """ + ], + [ + "The merged input field 'field2' in type 'Input1' cannot reference the " + + "inaccessible type 'Input2'." + ] + } + }; + } +}