Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fusion] Added post-merge validation rule "InputFieldReferencesInaccessibleTypeRule" #7992

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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 <c>inaccessible</c> structures
/// which are intended for internal use.
/// </summary>
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Input-Fields-cannot-reference-inaccessible-type">
/// Specification
/// </seealso>
internal sealed class InputFieldReferencesInaccessibleTypeRule : IEventHandler<InputFieldEvent>
{
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));
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@
<data name="LogEntryHelper_InputFieldDefaultMismatch" xml:space="preserve">
<value>The default value '{0}' of input field '{1}' in schema '{2}' differs from the default value of '{3}' in schema '{4}'.</value>
</data>
<data name="LogEntryHelper_InputFieldReferencesInaccessibleType" xml:space="preserve">
<value>The merged input field '{0}' in type '{1}' cannot reference the inaccessible type '{2}'.</value>
</data>
<data name="LogEntryHelper_InputFieldTypesNotMergeable" xml:space="preserve">
<value>The input field '{0}' has a different type shape in schema '{1}' than it does in schema '{2}'.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ public CompositionResult<MutableSchemaDefinition> Compose()
new EmptyMergedInterfaceTypeRule(),
new EmptyMergedObjectTypeRule(),
new EmptyMergedUnionTypeRule(),
new InputFieldReferencesInaccessibleTypeRule(),
new InterfaceFieldNoImplementationRule(),
new NonNullInputFieldIsInaccessibleRule(),
new NoQueriesRule(),
Expand Down
Original file line number Diff line number Diff line change
@@ -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<object> 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<string[]> ValidExamplesData()
{
return new TheoryData<string[]>
{
// 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<string[], string[]> InvalidExamplesData()
{
return new TheoryData<string[], string[]>
{
// 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'."
]
}
};
}
}
Loading