diff --git a/src/FairyBread.Tests/FairyBread.Tests.csproj b/src/FairyBread.Tests/FairyBread.Tests.csproj index fabd8c7..458972e 100644 --- a/src/FairyBread.Tests/FairyBread.Tests.csproj +++ b/src/FairyBread.Tests/FairyBread.Tests.csproj @@ -23,4 +23,182 @@ + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + MutationConventionsTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + GeneralTests.cs + + + diff --git a/src/FairyBread.Tests/MutationConventionsTests.Doesnt_Call_Field_Resolver_If_Invalid.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Doesnt_Call_Field_Resolver_If_Invalid.verified.txt new file mode 100644 index 0000000..9ed713d --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Doesnt_Call_Field_Resolver_If_Invalid.verified.txt @@ -0,0 +1,32 @@ +{ + Errors: [ + { + Message: 'Some Integer' must be equal to '1'., + Code: FairyBread_ValidationError, + Path: { + Name: someResolver, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: foo, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: EqualValidator, + errorMessage: 'Some Integer' must be equal to '1'., + formattedMessagePlaceholderValues: { + ComparisonProperty: , + ComparisonValue: 1, + PropertyName: Some Integer, + PropertyValue: -1 + }, + propertyName: SomeInteger, + severity: Error, + validatorName: FooInputDtoValidator + } + } + ] +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Ignores_Null_Argument_Value.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Ignores_Null_Argument_Value.verified.txt new file mode 100644 index 0000000..764366a --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Ignores_Null_Argument_Value.verified.txt @@ -0,0 +1,5 @@ +{ + Data: { + read: SomeInteger: 1, SomeString: hello; + } +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Multi_TopLevelFields_And_MultiRuns_Works.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Multi_TopLevelFields_And_MultiRuns_Works.verified.txt new file mode 100644 index 0000000..a7a8c13 --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Multi_TopLevelFields_And_MultiRuns_Works.verified.txt @@ -0,0 +1,98 @@ +{ + result1: { + Errors: [ + { + Message: 'Some Integer' must be equal to '1'., + Code: FairyBread_ValidationError, + Path: { + Name: read, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: foo, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: EqualValidator, + errorMessage: 'Some Integer' must be equal to '1'., + formattedMessagePlaceholderValues: { + ComparisonProperty: , + ComparisonValue: 1, + PropertyName: Some Integer, + PropertyValue: -1 + }, + propertyName: SomeInteger, + severity: Error, + validatorName: FooInputDtoValidator + } + } + ] + }, + result2: { + Errors: [ + { + Message: 'Some Integer' must be equal to '1'., + Code: FairyBread_ValidationError, + Path: { + Name: read, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: foo, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: EqualValidator, + errorMessage: 'Some Integer' must be equal to '1'., + formattedMessagePlaceholderValues: { + ComparisonProperty: , + ComparisonValue: 1, + PropertyName: Some Integer, + PropertyValue: -1 + }, + propertyName: SomeInteger, + severity: Error, + validatorName: FooInputDtoValidator + } + } + ] + }, + result3: { + Errors: [ + { + Message: 'Some Integer' must be equal to '1'., + Code: FairyBread_ValidationError, + Path: { + Name: read, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: foo, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: EqualValidator, + errorMessage: 'Some Integer' must be equal to '1'., + formattedMessagePlaceholderValues: { + ComparisonProperty: , + ComparisonValue: 1, + PropertyName: Some Integer, + PropertyValue: -1 + }, + propertyName: SomeInteger, + severity: Error, + validatorName: FooInputDtoValidator + } + } + ] + } +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Mutation_Works_caseData=1.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Mutation_Works_caseData=1.verified.txt new file mode 100644 index 0000000..f24d515 --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Mutation_Works_caseData=1.verified.txt @@ -0,0 +1,5 @@ +{ + Data: { + write: SomeInteger: 1, SomeString: hello; EmailAddress: ben@lol.com + } +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Mutation_Works_caseData=2.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Mutation_Works_caseData=2.verified.txt new file mode 100644 index 0000000..8bdd86f --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Mutation_Works_caseData=2.verified.txt @@ -0,0 +1,32 @@ +{ + Errors: [ + { + Message: 'Some Integer' must be equal to '1'., + Code: FairyBread_ValidationError, + Path: { + Name: write, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: foo, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: EqualValidator, + errorMessage: 'Some Integer' must be equal to '1'., + formattedMessagePlaceholderValues: { + ComparisonProperty: , + ComparisonValue: 1, + PropertyName: Some Integer, + PropertyValue: -1 + }, + propertyName: SomeInteger, + severity: Error, + validatorName: FooInputDtoValidator + } + } + ] +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Mutation_Works_caseData=3.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Mutation_Works_caseData=3.verified.txt new file mode 100644 index 0000000..27e452c --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Mutation_Works_caseData=3.verified.txt @@ -0,0 +1,30 @@ +{ + Errors: [ + { + Message: The specified condition was not met for 'Email Address'., + Code: FairyBread_ValidationError, + Path: { + Name: write, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: bar, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: AsyncPredicateValidator, + errorMessage: The specified condition was not met for 'Email Address'., + formattedMessagePlaceholderValues: { + PropertyName: Email Address, + PropertyValue: -1 + }, + propertyName: EmailAddress, + severity: Error, + validatorName: BarInputDtoAsyncValidator + } + } + ] +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Mutation_Works_caseData=4.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Mutation_Works_caseData=4.verified.txt new file mode 100644 index 0000000..b863843 --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Mutation_Works_caseData=4.verified.txt @@ -0,0 +1,86 @@ +{ + Errors: [ + { + Message: 'Some Integer' must be equal to '1'., + Code: FairyBread_ValidationError, + Path: { + Name: write, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: foo, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: EqualValidator, + errorMessage: 'Some Integer' must be equal to '1'., + formattedMessagePlaceholderValues: { + ComparisonProperty: , + ComparisonValue: 1, + PropertyName: Some Integer, + PropertyValue: -1 + }, + propertyName: SomeInteger, + severity: Error, + validatorName: FooInputDtoValidator + } + }, + { + Message: 'Some String' must be equal to 'hello'., + Code: FairyBread_ValidationError, + Path: { + Name: write, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: foo, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: EqualValidator, + errorMessage: 'Some String' must be equal to 'hello'., + formattedMessagePlaceholderValues: { + ComparisonProperty: , + ComparisonValue: hello, + PropertyName: Some String, + PropertyValue: -1 + }, + propertyName: SomeString, + severity: Error, + validatorName: FooInputDtoValidator + } + }, + { + Message: The specified condition was not met for 'Email Address'., + Code: FairyBread_ValidationError, + Path: { + Name: write, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: bar, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: AsyncPredicateValidator, + errorMessage: The specified condition was not met for 'Email Address'., + formattedMessagePlaceholderValues: { + PropertyName: Email Address, + PropertyValue: -1 + }, + propertyName: EmailAddress, + severity: Error, + validatorName: BarInputDtoAsyncValidator + } + } + ] +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Query_Array_Works_caseData=1.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Query_Array_Works_caseData=1.verified.txt new file mode 100644 index 0000000..524ebb1 --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Query_Array_Works_caseData=1.verified.txt @@ -0,0 +1,5 @@ +{ + Data: { + readWithArrayArg: SomeInteger: 1, SomeString: hello + } +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Query_Array_Works_caseData=2.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Query_Array_Works_caseData=2.verified.txt new file mode 100644 index 0000000..524ebb1 --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Query_Array_Works_caseData=2.verified.txt @@ -0,0 +1,5 @@ +{ + Data: { + readWithArrayArg: SomeInteger: 1, SomeString: hello + } +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Query_Array_Works_caseData=3.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Query_Array_Works_caseData=3.verified.txt new file mode 100644 index 0000000..8835b7c --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Query_Array_Works_caseData=3.verified.txt @@ -0,0 +1,5 @@ +{ + Data: { + readWithArrayArg: SomeInteger: 1, SomeString: hello, SomeInteger: 1, SomeString: hello + } +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Query_Array_Works_caseData=4.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Query_Array_Works_caseData=4.verified.txt new file mode 100644 index 0000000..84a6415 --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Query_Array_Works_caseData=4.verified.txt @@ -0,0 +1,33 @@ +{ + Errors: [ + { + Message: 'Some Integer' must be equal to '1'., + Code: FairyBread_ValidationError, + Path: { + Name: readWithArrayArg, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: foos, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: EqualValidator, + errorMessage: 'Some Integer' must be equal to '1'., + formattedMessagePlaceholderValues: { + CollectionIndex: 0, + ComparisonProperty: , + ComparisonValue: 1, + PropertyName: Some Integer, + PropertyValue: -1 + }, + propertyName: x[0].SomeInteger, + severity: Error, + validatorName: ArrayOfFooInputDtoValidator + } + } + ] +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Query_Array_Works_caseData=5.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Query_Array_Works_caseData=5.verified.txt new file mode 100644 index 0000000..84a6415 --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Query_Array_Works_caseData=5.verified.txt @@ -0,0 +1,33 @@ +{ + Errors: [ + { + Message: 'Some Integer' must be equal to '1'., + Code: FairyBread_ValidationError, + Path: { + Name: readWithArrayArg, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: foos, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: EqualValidator, + errorMessage: 'Some Integer' must be equal to '1'., + formattedMessagePlaceholderValues: { + CollectionIndex: 0, + ComparisonProperty: , + ComparisonValue: 1, + PropertyName: Some Integer, + PropertyValue: -1 + }, + propertyName: x[0].SomeInteger, + severity: Error, + validatorName: ArrayOfFooInputDtoValidator + } + } + ] +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Query_Array_Works_caseData=6.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Query_Array_Works_caseData=6.verified.txt new file mode 100644 index 0000000..2c45fc2 --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Query_Array_Works_caseData=6.verified.txt @@ -0,0 +1,62 @@ +{ + Errors: [ + { + Message: 'Some Integer' must be equal to '1'., + Code: FairyBread_ValidationError, + Path: { + Name: readWithArrayArg, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: foos, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: EqualValidator, + errorMessage: 'Some Integer' must be equal to '1'., + formattedMessagePlaceholderValues: { + CollectionIndex: 0, + ComparisonProperty: , + ComparisonValue: 1, + PropertyName: Some Integer, + PropertyValue: -1 + }, + propertyName: x[0].SomeInteger, + severity: Error, + validatorName: ArrayOfFooInputDtoValidator + } + }, + { + Message: 'Some Integer' must be equal to '1'., + Code: FairyBread_ValidationError, + Path: { + Name: readWithArrayArg, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: foos, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: EqualValidator, + errorMessage: 'Some Integer' must be equal to '1'., + formattedMessagePlaceholderValues: { + CollectionIndex: 1, + ComparisonProperty: , + ComparisonValue: 1, + PropertyName: Some Integer, + PropertyValue: -1 + }, + propertyName: x[1].SomeInteger, + severity: Error, + validatorName: ArrayOfFooInputDtoValidator + } + } + ] +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Query_List_Works_caseData=1.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Query_List_Works_caseData=1.verified.txt new file mode 100644 index 0000000..e6a0e98 --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Query_List_Works_caseData=1.verified.txt @@ -0,0 +1,5 @@ +{ + Data: { + readWithListArg: SomeInteger: 1, SomeString: hello + } +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Query_List_Works_caseData=2.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Query_List_Works_caseData=2.verified.txt new file mode 100644 index 0000000..e6a0e98 --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Query_List_Works_caseData=2.verified.txt @@ -0,0 +1,5 @@ +{ + Data: { + readWithListArg: SomeInteger: 1, SomeString: hello + } +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Query_List_Works_caseData=3.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Query_List_Works_caseData=3.verified.txt new file mode 100644 index 0000000..8442d9f --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Query_List_Works_caseData=3.verified.txt @@ -0,0 +1,5 @@ +{ + Data: { + readWithListArg: SomeInteger: 1, SomeString: hello, SomeInteger: 1, SomeString: hello + } +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Query_List_Works_caseData=4.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Query_List_Works_caseData=4.verified.txt new file mode 100644 index 0000000..62952f3 --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Query_List_Works_caseData=4.verified.txt @@ -0,0 +1,33 @@ +{ + Errors: [ + { + Message: 'Some Integer' must be equal to '1'., + Code: FairyBread_ValidationError, + Path: { + Name: readWithListArg, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: foos, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: EqualValidator, + errorMessage: 'Some Integer' must be equal to '1'., + formattedMessagePlaceholderValues: { + CollectionIndex: 0, + ComparisonProperty: , + ComparisonValue: 1, + PropertyName: Some Integer, + PropertyValue: -1 + }, + propertyName: x[0].SomeInteger, + severity: Error, + validatorName: ListOfFooInputDtoValidator + } + } + ] +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Query_List_Works_caseData=5.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Query_List_Works_caseData=5.verified.txt new file mode 100644 index 0000000..62952f3 --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Query_List_Works_caseData=5.verified.txt @@ -0,0 +1,33 @@ +{ + Errors: [ + { + Message: 'Some Integer' must be equal to '1'., + Code: FairyBread_ValidationError, + Path: { + Name: readWithListArg, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: foos, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: EqualValidator, + errorMessage: 'Some Integer' must be equal to '1'., + formattedMessagePlaceholderValues: { + CollectionIndex: 0, + ComparisonProperty: , + ComparisonValue: 1, + PropertyName: Some Integer, + PropertyValue: -1 + }, + propertyName: x[0].SomeInteger, + severity: Error, + validatorName: ListOfFooInputDtoValidator + } + } + ] +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Query_List_Works_caseData=6.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Query_List_Works_caseData=6.verified.txt new file mode 100644 index 0000000..26113d1 --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Query_List_Works_caseData=6.verified.txt @@ -0,0 +1,62 @@ +{ + Errors: [ + { + Message: 'Some Integer' must be equal to '1'., + Code: FairyBread_ValidationError, + Path: { + Name: readWithListArg, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: foos, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: EqualValidator, + errorMessage: 'Some Integer' must be equal to '1'., + formattedMessagePlaceholderValues: { + CollectionIndex: 0, + ComparisonProperty: , + ComparisonValue: 1, + PropertyName: Some Integer, + PropertyValue: -1 + }, + propertyName: x[0].SomeInteger, + severity: Error, + validatorName: ListOfFooInputDtoValidator + } + }, + { + Message: 'Some Integer' must be equal to '1'., + Code: FairyBread_ValidationError, + Path: { + Name: readWithListArg, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: foos, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: EqualValidator, + errorMessage: 'Some Integer' must be equal to '1'., + formattedMessagePlaceholderValues: { + CollectionIndex: 1, + ComparisonProperty: , + ComparisonValue: 1, + PropertyName: Some Integer, + PropertyValue: -1 + }, + propertyName: x[1].SomeInteger, + severity: Error, + validatorName: ListOfFooInputDtoValidator + } + } + ] +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Query_Works_caseData=1.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Query_Works_caseData=1.verified.txt new file mode 100644 index 0000000..0392acb --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Query_Works_caseData=1.verified.txt @@ -0,0 +1,5 @@ +{ + Data: { + read: SomeInteger: 1, SomeString: hello; EmailAddress: ben@lol.com + } +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Query_Works_caseData=2.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Query_Works_caseData=2.verified.txt new file mode 100644 index 0000000..fe07897 --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Query_Works_caseData=2.verified.txt @@ -0,0 +1,32 @@ +{ + Errors: [ + { + Message: 'Some Integer' must be equal to '1'., + Code: FairyBread_ValidationError, + Path: { + Name: read, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: foo, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: EqualValidator, + errorMessage: 'Some Integer' must be equal to '1'., + formattedMessagePlaceholderValues: { + ComparisonProperty: , + ComparisonValue: 1, + PropertyName: Some Integer, + PropertyValue: -1 + }, + propertyName: SomeInteger, + severity: Error, + validatorName: FooInputDtoValidator + } + } + ] +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Query_Works_caseData=3.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Query_Works_caseData=3.verified.txt new file mode 100644 index 0000000..0069536 --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Query_Works_caseData=3.verified.txt @@ -0,0 +1,30 @@ +{ + Errors: [ + { + Message: The specified condition was not met for 'Email Address'., + Code: FairyBread_ValidationError, + Path: { + Name: read, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: bar, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: AsyncPredicateValidator, + errorMessage: The specified condition was not met for 'Email Address'., + formattedMessagePlaceholderValues: { + PropertyName: Email Address, + PropertyValue: -1 + }, + propertyName: EmailAddress, + severity: Error, + validatorName: BarInputDtoAsyncValidator + } + } + ] +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Query_Works_caseData=4.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Query_Works_caseData=4.verified.txt new file mode 100644 index 0000000..e62fefd --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Query_Works_caseData=4.verified.txt @@ -0,0 +1,86 @@ +{ + Errors: [ + { + Message: 'Some Integer' must be equal to '1'., + Code: FairyBread_ValidationError, + Path: { + Name: read, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: foo, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: EqualValidator, + errorMessage: 'Some Integer' must be equal to '1'., + formattedMessagePlaceholderValues: { + ComparisonProperty: , + ComparisonValue: 1, + PropertyName: Some Integer, + PropertyValue: -1 + }, + propertyName: SomeInteger, + severity: Error, + validatorName: FooInputDtoValidator + } + }, + { + Message: 'Some String' must be equal to 'hello'., + Code: FairyBread_ValidationError, + Path: { + Name: read, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: foo, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: EqualValidator, + errorMessage: 'Some String' must be equal to 'hello'., + formattedMessagePlaceholderValues: { + ComparisonProperty: , + ComparisonValue: hello, + PropertyName: Some String, + PropertyValue: -1 + }, + propertyName: SomeString, + severity: Error, + validatorName: FooInputDtoValidator + } + }, + { + Message: The specified condition was not met for 'Email Address'., + Code: FairyBread_ValidationError, + Path: { + Name: read, + Parent: { + IsRoot: true + }, + Length: 1, + IsRoot: false + }, + Extensions: { + argumentName: bar, + attemptedValue: -1, + code: FairyBread_ValidationError, + errorCode: AsyncPredicateValidator, + errorMessage: The specified condition was not met for 'Email Address'., + formattedMessagePlaceholderValues: { + PropertyName: Email Address, + PropertyValue: -1 + }, + propertyName: EmailAddress, + severity: Error, + validatorName: BarInputDtoAsyncValidator + } + } + ] +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.SchemaWorks.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.SchemaWorks.verified.txt new file mode 100644 index 0000000..ed63812 --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.SchemaWorks.verified.txt @@ -0,0 +1,26 @@ +schema { + query: Query + mutation: Mutation +} + +type Mutation { + write(foo: FooInputDtoInput! bar: BarInputDtoInput!): String! +} + +type Query { + read(foo: FooInputDtoInput! bar: BarInputDtoInput): String! + readWithArrayArg(foos: [FooInputDtoInput!]!): String! + readWithListArg(foos: [FooInputDtoInput!]!): String! + someResolver(foo: FooInputDtoInput! bar: BarInputDtoInput): String! + intResolver(count: Int!): String! + nullableIntResolver(count: Int): String! +} + +input BarInputDtoInput { + emailAddress: String! +} + +input FooInputDtoInput { + someInteger: Int! + someString: String! +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.Should_Respect_ShouldValidateArgument_Option.verified.txt b/src/FairyBread.Tests/MutationConventionsTests.Should_Respect_ShouldValidateArgument_Option.verified.txt new file mode 100644 index 0000000..59ea752 --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.Should_Respect_ShouldValidateArgument_Option.verified.txt @@ -0,0 +1,5 @@ +{ + Data: { + write: SomeInteger: -1, SomeString: hello; EmailAddress: ben@lol.com + } +} \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationConventionsTests.cs b/src/FairyBread.Tests/MutationConventionsTests.cs new file mode 100644 index 0000000..d1f76c9 --- /dev/null +++ b/src/FairyBread.Tests/MutationConventionsTests.cs @@ -0,0 +1,209 @@ +namespace FairyBread.Tests; + +[UsesVerify] +public class MutationsConventionTests +{ + static MutationsConventionTests() + { + VerifierSettings.NameForParameter(_ => _.CaseId); + } + + private static async Task GetRequestExecutorAsync( + Action? configureOptions = null, + Action? configureServices = null, + bool registerValidators = true) + { + var services = new ServiceCollection(); + configureServices?.Invoke(services); + + if (registerValidators) + { + services.AddValidator(); + services.AddValidator(); + services.AddValidator>(); + services.AddValidator(); + services.AddValidator(); + services.AddValidator(); + } + + var builder = services + .AddGraphQL() + .AddQueryType() + .AddMutationType() + .AddMutationConventions() + .AddFairyBread(options => + { + configureOptions?.Invoke(options); + }); + + return await builder + .BuildRequestExecutorAsync(); + } + + [Fact] + public async Task SchemaWorks() + { + // Arrange + var executor = await GetRequestExecutorAsync(); + + // Act + var result = executor.Schema.ToString(); + + // Assert + await Verifier.Verify(result); + } + + [Theory] + [MemberData(nameof(Cases))] + public async Task Mutation_Works(CaseData caseData) + { + // Arrange + var executor = await GetRequestExecutorAsync(); + + var query = $$""" + mutation { + write(foo: {{caseData.FooInput}}, bar: {{caseData.BarInput}}) { + string + errors { + __typename + } + } + } + """; + + // Act + var result = await executor.ExecuteAsync(query); + + // Assert + var verifySettings = new VerifySettings(); + verifySettings.UseParameters(caseData); + await Verifier.Verify(result, verifySettings); + } + + public static IEnumerable Cases() + { + var caseId = 1; + yield return new object[] + { + // Happy days + new CaseData(caseId++, @"{ someInteger: 1, someString: ""hello"" }", @"{ emailAddress: ""ben@lol.com"" }") + }; + yield return new object[] + { + // Sync error + new CaseData(caseId++, @"{ someInteger: -1, someString: ""hello"" }", @"{ emailAddress: ""ben@lol.com"" }") + }; + yield return new object[] + { + // Async error + new CaseData(caseId++, @"{ someInteger: 1, someString: ""hello"" }", @"{ emailAddress: ""-1"" }") + }; + yield return new object[] + { + // Multiple sync errors and async error + new CaseData(caseId++, @"{ someInteger: -1, someString: ""-1"" }", @"{ emailAddress: ""-1"" }") + }; + } + + public class CaseData + { + public string CaseId { get; set; } + public string FooInput { get; set; } + public string BarInput { get; set; } + + public CaseData(int caseId, string fooInput, string barInput) + { + CaseId = caseId.ToString(); + FooInput = fooInput; + BarInput = barInput; + } + } + +#pragma warning disable CA1822 // Mark members as static + public class Query + { + public string Meh => "meh"; + } + + public class Mutation + { + public string Write(FooInputDto foo, BarInputDto bar) => $"{foo}; {bar}"; + } + + public class FooInputDto + { + public int SomeInteger { get; set; } + + public string SomeString { get; set; } = ""; + + public override string ToString() => + $"SomeInteger: {SomeInteger}, " + + $"SomeString: {SomeString}"; + } + + public class FooInputDtoValidator : AbstractValidator + { + public FooInputDtoValidator() + { + RuleFor(x => x.SomeInteger).Equal(1); + RuleFor(x => x.SomeString).Equal("hello"); + } + } + + public class ArrayOfFooInputDtoValidator : AbstractValidator + { + public ArrayOfFooInputDtoValidator() + { + RuleForEach(x => x).SetValidator(new FooInputDtoValidator()); + } + } + + public class ListOfFooInputDtoValidator : AbstractValidator> + { + public ListOfFooInputDtoValidator() + { + RuleForEach(x => x).SetValidator(new FooInputDtoValidator()); + } + } + + public class BarInputDto + { + public string EmailAddress { get; set; } = ""; + + public override string ToString() + => $"EmailAddress: {EmailAddress}"; + } + + public abstract class BarInputDtoValidatorBase : AbstractValidator + { + public BarInputDtoValidatorBase() + { + RuleFor(x => x.EmailAddress).NotNull(); + } + } + + public class BarInputDtoValidator : BarInputDtoValidatorBase + { + + } + + public class BarInputDtoAsyncValidator : AbstractValidator + { + public BarInputDtoAsyncValidator() + { + RuleFor(x => x.EmailAddress) + // TODO: Cancellation unit test + .MustAsync((val, _) => Task.FromResult(val == "ben@lol.com")); + } + } + + public class NullableIntValidator : AbstractValidator + { + public NullableIntValidator() + { + RuleFor(x => x) + //.Null() + .GreaterThan(0).When(x => x is not null); + } + } +} diff --git a/src/FairyBread.Tests/MutationsConventionTests.Mutation_Works_caseData=1.verified.txt b/src/FairyBread.Tests/MutationsConventionTests.Mutation_Works_caseData=1.verified.txt new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/src/FairyBread.Tests/MutationsConventionTests.Mutation_Works_caseData=1.verified.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationsConventionTests.Mutation_Works_caseData=2.verified.txt b/src/FairyBread.Tests/MutationsConventionTests.Mutation_Works_caseData=2.verified.txt new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/src/FairyBread.Tests/MutationsConventionTests.Mutation_Works_caseData=2.verified.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/FairyBread.Tests/MutationsConventionTests.SchemaWorks.verified.txt b/src/FairyBread.Tests/MutationsConventionTests.SchemaWorks.verified.txt new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/src/FairyBread.Tests/MutationsConventionTests.SchemaWorks.verified.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/FairyBread/DefaultFairyBreadOptions.cs b/src/FairyBread/DefaultFairyBreadOptions.cs index db61137..2a52d35 100644 --- a/src/FairyBread/DefaultFairyBreadOptions.cs +++ b/src/FairyBread/DefaultFairyBreadOptions.cs @@ -3,9 +3,18 @@ public class DefaultFairyBreadOptions : IFairyBreadOptions { /// - public virtual bool ThrowIfNoValidatorsFound { get; set; } = true; + public virtual bool ThrowIfNoValidatorsFound { get; set; } + = true; /// - public Func ShouldValidateArgument { get; set; } + public virtual Func ShouldValidateArgument { get; set; } = (o, f, a) => true; -} \ No newline at end of file + + /// + public virtual bool UseMutationConventions { get; set; } + = true; + + /// + public virtual Type ValidationExceptionType { get; set; } + = typeof(DefaultValidationException); +} diff --git a/src/FairyBread/DefaultValidationErrorsHandler.cs b/src/FairyBread/DefaultValidationErrorsHandler.cs index 101b78e..0c82296 100644 --- a/src/FairyBread/DefaultValidationErrorsHandler.cs +++ b/src/FairyBread/DefaultValidationErrorsHandler.cs @@ -6,11 +6,27 @@ public virtual void Handle( IMiddlewareContext context, IEnumerable invalidResults) { + if (context.ObjectType.Fields.Any()) + { + // Throw and let the mutation convention's error middleware + // pick this up and do what it wants with it. + // TODO: Consider if I can short-circuit their middleware + // or just give it something they handle rather than doing the whole + // exception hoop jump + throw new DefaultValidationException( + context, + invalidResults); + } + foreach (var invalidResult in invalidResults) { foreach (var failure in invalidResult.Result.Errors) { - var errorBuilder = CreateErrorBuilder(context, invalidResult.ArgumentName, invalidResult.Validator, failure); + var errorBuilder = CreateErrorBuilder( + context, + invalidResult.ArgumentName, + invalidResult.Validator, + failure); var error = errorBuilder.Build(); context.ReportError(error); } @@ -33,7 +49,9 @@ protected virtual IErrorBuilder CreateErrorBuilder( .SetExtension("errorMessage", failure.ErrorMessage) .SetExtension("attemptedValue", failure.AttemptedValue) .SetExtension("severity", failure.Severity) - .SetExtension("formattedMessagePlaceholderValues", failure.FormattedMessagePlaceholderValues); + .SetExtension( + "formattedMessagePlaceholderValues", + failure.FormattedMessagePlaceholderValues); if (!string.IsNullOrWhiteSpace(failure.PropertyName)) { diff --git a/src/FairyBread/DefaultValidationException.cs b/src/FairyBread/DefaultValidationException.cs new file mode 100644 index 0000000..71c48f1 --- /dev/null +++ b/src/FairyBread/DefaultValidationException.cs @@ -0,0 +1,34 @@ +namespace FairyBread; + +[GraphQLName("ValidationError")] +public class DefaultValidationError +{ + private DefaultValidationError( + DefaultValidationException exception) + { + Message = "A validation error occurred"; + // TODO: Complete a nice rich implementation. + } + + public static DefaultValidationError CreateErrorFrom( + DefaultValidationException ex) + { + return new(ex); + } + + public string Message { get; } +} + +public class DefaultValidationException : Exception +{ + public DefaultValidationException( + IMiddlewareContext context, + IEnumerable invalidResults) + { + Context = context; + InvalidResults = invalidResults; + } + + public IMiddlewareContext Context { get; } + public IEnumerable InvalidResults { get; } +} diff --git a/src/FairyBread/FairyBread.csproj b/src/FairyBread/FairyBread.csproj index 3f07c42..8e45db6 100644 --- a/src/FairyBread/FairyBread.csproj +++ b/src/FairyBread/FairyBread.csproj @@ -8,6 +8,7 @@ + \ No newline at end of file diff --git a/src/FairyBread/IFairyBreadOptions.cs b/src/FairyBread/IFairyBreadOptions.cs index 69978cd..0d333fd 100644 --- a/src/FairyBread/IFairyBreadOptions.cs +++ b/src/FairyBread/IFairyBreadOptions.cs @@ -15,4 +15,8 @@ public interface IFairyBreadOptions /// The default implementation always returns true. /// Func ShouldValidateArgument { get; set; } + + bool UseMutationConventions { get; set; } + + Type ValidationExceptionType { get; set; } } diff --git a/src/FairyBread/ValidationMiddlewareInjector.cs b/src/FairyBread/ValidationMiddlewareInjector.cs index e302118..43dd083 100644 --- a/src/FairyBread/ValidationMiddlewareInjector.cs +++ b/src/FairyBread/ValidationMiddlewareInjector.cs @@ -2,10 +2,11 @@ internal class ValidationMiddlewareInjector : TypeInterceptor { + private const string _middlewareKey = "FairyBread.ValidationMiddleware"; private FieldMiddlewareDefinition? _validationFieldMiddlewareDef; - public override void OnBeforeCompleteType( - ITypeCompletionContext completionContext, + public override void OnBeforeRegisterDependencies( + ITypeDiscoveryContext context, DefinitionBase definition) { if (definition is not ObjectTypeDefinition objTypeDef) @@ -13,9 +14,9 @@ public override void OnBeforeCompleteType( return; } - var options = completionContext.Services + var options = context.Services .GetRequiredService(); - var validatorRegistry = completionContext.Services + var validatorRegistry = context.Services .GetRequiredService(); foreach (var fieldDef in objTypeDef.Fields) @@ -68,13 +69,26 @@ public override void OnBeforeCompleteType( if (needsValidationMiddleware) { - if (_validationFieldMiddlewareDef is null) - { - _validationFieldMiddlewareDef = new FieldMiddlewareDefinition( - FieldClassMiddlewareFactory.Create()); - } + _validationFieldMiddlewareDef ??= new FieldMiddlewareDefinition( + FieldClassMiddlewareFactory.Create(), + key: _middlewareKey); fieldDef.MiddlewareDefinitions.Insert(0, _validationFieldMiddlewareDef); + + // If the middleware contains the mutation convention's errors middleware + // then this field is using mutation conventions and we can add our error + // type to this mutation field + // We also add a context data flag so that we know how to handle any validation + // errors later + if (fieldDef.MiddlewareDefinitions + .Any(md => md.Key == WellKnownMiddleware.MutationErrors)) + { + fieldDef.ContextData[WellKnownContextData.UsesMutationConvention] = true; + + fieldDef.AddErrorType( + context.DescriptorContext, + typeof(OutOfMemoryException)); + } } } } @@ -231,3 +245,23 @@ private static List DetermineValidatorsForArg( return null; } } + +//// Copied from HC source +//internal static class MutationContextDataKeys +//{ +// public const string Options = "HotChocolate.Types.Mutations.Options"; +// public const string Fields = "HotChocolate.Types.Mutations.Fields"; +//} +//namespace HotChocolate; + +///// +///// Provides keys that identify well-known middleware components. +///// +//public static class WellKnownMiddleware +//{ +// /// +// /// This key identifies the mutation convention middleware. +// /// +// public const string MutationErrors = "HotChocolate.Types.Mutations.Errors"; +//} + diff --git a/src/FairyBread/WellKnownContextData.cs b/src/FairyBread/WellKnownContextData.cs index 05eb5b7..959190a 100644 --- a/src/FairyBread/WellKnownContextData.cs +++ b/src/FairyBread/WellKnownContextData.cs @@ -2,17 +2,20 @@ internal static class WellKnownContextData { - public const string Prefix = "FairyBread"; + public const string Prefix = "FairyBread."; public const string DontValidate = - Prefix + ".DontValidate"; + Prefix + "DontValidate"; public const string DontValidateImplicitly = - Prefix + ".DontValidateImplicitly"; + Prefix + "DontValidateImplicitly"; public const string ExplicitValidatorTypes = - Prefix + ".ExplicitValidatorTypes"; + Prefix + "ExplicitValidatorTypes"; public const string ValidatorDescriptors = - Prefix + ".Validators"; -} \ No newline at end of file + Prefix + "Validators"; + + public const string UsesMutationConvention = + Prefix + "UsesMutationConvention"; +}