diff --git a/OData/src/CommonAssemblyInfo.cs b/OData/src/CommonAssemblyInfo.cs index 2ffa5766..7fe86dac 100644 --- a/OData/src/CommonAssemblyInfo.cs +++ b/OData/src/CommonAssemblyInfo.cs @@ -25,8 +25,8 @@ using System.Runtime.InteropServices; #if ASPNETODATA #if !BUILD_GENERATED_VERSION -[assembly: AssemblyVersion("5.3.0.0")] // ASPNETODATA -[assembly: AssemblyFileVersion("5.3.0.0")] // ASPNETODATA +[assembly: AssemblyVersion("5.3.1.0")] // ASPNETODATA +[assembly: AssemblyFileVersion("5.3.1.0")] // ASPNETODATA #endif [assembly: AssemblyProduct("Microsoft ASP.NET Web API OData")] #endif \ No newline at end of file diff --git a/OData/src/System.Web.Http.OData/OData/Query/Expressions/FilterBinder.cs b/OData/src/System.Web.Http.OData/OData/Query/Expressions/FilterBinder.cs index b922e652..1f7a8d94 100644 --- a/OData/src/System.Web.Http.OData/OData/Query/Expressions/FilterBinder.cs +++ b/OData/src/System.Web.Http.OData/OData/Query/Expressions/FilterBinder.cs @@ -153,6 +153,9 @@ namespace System.Web.Http.OData.Query.Expressions case QueryNodeKind.EntityCollectionCast: return BindEntityCollectionCastNode(node as EntityCollectionCastNode); + case QueryNodeKind.CollectionFunctionCall: + case QueryNodeKind.EntityCollectionFunctionCall: + // Unused or have unknown uses. default: throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, node.Kind, typeof(FilterBinder).Name); } @@ -198,6 +201,11 @@ namespace System.Web.Http.OData.Query.Expressions case QueryNodeKind.SingleEntityCast: return BindSingleEntityCastNode(node as SingleEntityCastNode); + case QueryNodeKind.NamedFunctionParameter: + case QueryNodeKind.SingleValueOpenPropertyAccess: + // Unused or have unknown uses. + case QueryNodeKind.SingleEntityFunctionCall: + // Used for some 'cast' calls but not supported here or in FilterQueryValidator. default: throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, node.Kind, typeof(FilterBinder).Name); } diff --git a/OData/src/System.Web.Http.OData/OData/Query/Validators/FilterQueryValidator.cs b/OData/src/System.Web.Http.OData/OData/Query/Validators/FilterQueryValidator.cs index 2dc45794..2e052b18 100644 --- a/OData/src/System.Web.Http.OData/OData/Query/Validators/FilterQueryValidator.cs +++ b/OData/src/System.Web.Http.OData/OData/Query/Validators/FilterQueryValidator.cs @@ -70,6 +70,7 @@ namespace System.Web.Http.OData.Query.Validators throw Error.ArgumentNull("settings"); } + ValidateFunction("all", settings); EnterLambda(settings); try @@ -105,6 +106,7 @@ namespace System.Web.Http.OData.Query.Validators throw Error.ArgumentNull("settings"); } + ValidateFunction("any", settings); EnterLambda(settings); try @@ -255,7 +257,7 @@ namespace System.Web.Http.OData.Query.Validators throw Error.ArgumentNull("settings"); } - // no default validation logic here + // No default validation logic here. } /// @@ -279,7 +281,7 @@ namespace System.Web.Http.OData.Query.Validators throw Error.ArgumentNull("settings"); } - // no default validation logic here + // Validate child nodes but not the ConvertNode itself. ValidateQueryNode(convertNode.Source, settings); } @@ -300,7 +302,7 @@ namespace System.Web.Http.OData.Query.Validators throw Error.ArgumentNull("settings"); } - // no default validation logic here + // No default validation logic here. // recursion if (sourceNode != null) @@ -330,7 +332,7 @@ namespace System.Web.Http.OData.Query.Validators throw Error.ArgumentNull("settings"); } - // no default validation logic here + // No default validation logic here. } /// @@ -354,7 +356,7 @@ namespace System.Web.Http.OData.Query.Validators throw Error.ArgumentNull("settings"); } - // no default validation logic here + // No default validation logic here. ValidateQueryNode(propertyAccessNode.Source, settings); } @@ -379,7 +381,7 @@ namespace System.Web.Http.OData.Query.Validators throw Error.ArgumentNull("settings"); } - // no default validation logic here + // No default validation logic here. ValidateQueryNode(propertyAccessNode.Source, settings); } @@ -434,6 +436,9 @@ namespace System.Web.Http.OData.Query.Validators throw new ODataException(Error.Format(SRResources.NotAllowedLogicalOperator, unaryOperatorNode.OperatorKind, "AllowedLogicalOperators")); } break; + + default: + throw Error.NotSupported(SRResources.UnaryNodeValidationNotSupported, unaryOperatorNode.OperatorKind, typeof(FilterQueryValidator).Name); } } @@ -547,6 +552,12 @@ namespace System.Web.Http.OData.Query.Validators case QueryNodeKind.EntityCollectionCast: ValidateEntityCollectionCastNode(node as EntityCollectionCastNode, settings); break; + + case QueryNodeKind.CollectionFunctionCall: + case QueryNodeKind.EntityCollectionFunctionCall: + // Unused or have unknown uses. + default: + throw Error.NotSupported(SRResources.QueryNodeValidationNotSupported, node.Kind, typeof(FilterQueryValidator).Name); } } @@ -607,6 +618,14 @@ namespace System.Web.Http.OData.Query.Validators case QueryNodeKind.All: ValidateAllNode(node as AllNode, settings); break; + + case QueryNodeKind.NamedFunctionParameter: + case QueryNodeKind.SingleValueOpenPropertyAccess: + // Unused or have unknown uses. + case QueryNodeKind.SingleEntityFunctionCall: + // Used for some 'cast' calls but not supported here or in FilterBinder. + default: + throw Error.NotSupported(SRResources.QueryNodeValidationNotSupported, node.Kind, typeof(FilterQueryValidator).Name); } } @@ -663,7 +682,7 @@ namespace System.Web.Http.OData.Query.Validators case ClrCanonicalFunctions.IndexofFunctionName: result = AllowedFunctions.IndexOf; break; - case "IsOf": + case "isof": result = AllowedFunctions.IsOf; break; case ClrCanonicalFunctions.LengthFunctionName: diff --git a/OData/src/System.Web.Http.OData/Properties/SRResources.Designer.cs b/OData/src/System.Web.Http.OData/Properties/SRResources.Designer.cs index a8c21221..85638fbd 100644 --- a/OData/src/System.Web.Http.OData/Properties/SRResources.Designer.cs +++ b/OData/src/System.Web.Http.OData/Properties/SRResources.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.34011 +// Runtime Version:4.0.30319.35317 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -1383,6 +1383,15 @@ namespace System.Web.Http.OData.Properties { } } + /// + /// Looks up a localized string similar to Validating OData QueryNode of kind {0} is not supported by {1}.. + /// + internal static string QueryNodeValidationNotSupported { + get { + return ResourceManager.GetString("QueryNodeValidationNotSupported", resourceCulture); + } + } + /// /// Looks up a localized string similar to The query parameter '{0}' is not supported.. /// @@ -1626,6 +1635,15 @@ namespace System.Web.Http.OData.Properties { } } + /// + /// Looks up a localized string similar to Validating OData UnaryOperatorNode of kind {0} is not supported by {1}.. + /// + internal static string UnaryNodeValidationNotSupported { + get { + return ResourceManager.GetString("UnaryNodeValidationNotSupported", resourceCulture); + } + } + /// /// Looks up a localized string similar to The element type '{0}' of the given collection type '{1}' is not of the type '{2}'.. /// diff --git a/OData/src/System.Web.Http.OData/Properties/SRResources.resx b/OData/src/System.Web.Http.OData/Properties/SRResources.resx index 4eec4a98..3dcf1c29 100644 --- a/OData/src/System.Web.Http.OData/Properties/SRResources.resx +++ b/OData/src/System.Web.Http.OData/Properties/SRResources.resx @@ -201,6 +201,12 @@ Binding OData QueryNode of kind {0} is not supported by {1}. + + Validating OData QueryNode of kind {0} is not supported by {1}. + + + Validating OData UnaryOperatorNode of kind {0} is not supported by {1}. + Duplicate property named '{0}' is not supported in '$orderby'. diff --git a/OData/src/System.Web.OData/OData/Query/Expressions/FilterBinder.cs b/OData/src/System.Web.OData/OData/Query/Expressions/FilterBinder.cs index daf89a42..55538669 100644 --- a/OData/src/System.Web.OData/OData/Query/Expressions/FilterBinder.cs +++ b/OData/src/System.Web.OData/OData/Query/Expressions/FilterBinder.cs @@ -159,6 +159,11 @@ namespace System.Web.OData.Query.Expressions case QueryNodeKind.EntityCollectionCast: return BindEntityCollectionCastNode(node as EntityCollectionCastNode); + case QueryNodeKind.CollectionFunctionCall: + case QueryNodeKind.EntityCollectionFunctionCall: + case QueryNodeKind.CollectionOpenPropertyAccess: + case QueryNodeKind.CollectionPropertyCast: + // Unused or have unknown uses. default: throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, node.Kind, typeof(FilterBinder).Name); } @@ -207,6 +212,14 @@ namespace System.Web.OData.Query.Expressions case QueryNodeKind.SingleEntityFunctionCall: return BindSingleEntityFunctionCallNode(node as SingleEntityFunctionCallNode); + case QueryNodeKind.NamedFunctionParameter: + case QueryNodeKind.SingleValueOpenPropertyAccess: + case QueryNodeKind.ParameterAlias: + case QueryNodeKind.EntitySet: + case QueryNodeKind.KeyLookup: + case QueryNodeKind.SearchTerm: + case QueryNodeKind.SingleValueCast: + // Unused or have unknown uses. default: throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, node.Kind, typeof(FilterBinder).Name); } diff --git a/OData/src/System.Web.OData/OData/Query/Validators/FilterQueryValidator.cs b/OData/src/System.Web.OData/OData/Query/Validators/FilterQueryValidator.cs index e7d3ff7f..fe06170d 100644 --- a/OData/src/System.Web.OData/OData/Query/Validators/FilterQueryValidator.cs +++ b/OData/src/System.Web.OData/OData/Query/Validators/FilterQueryValidator.cs @@ -75,6 +75,7 @@ namespace System.Web.OData.Query.Validators throw Error.ArgumentNull("settings"); } + ValidateFunction("all", settings); EnterLambda(settings); try @@ -110,6 +111,7 @@ namespace System.Web.OData.Query.Validators throw Error.ArgumentNull("settings"); } + ValidateFunction("any", settings); EnterLambda(settings); try @@ -261,7 +263,7 @@ namespace System.Web.OData.Query.Validators throw Error.ArgumentNull("settings"); } - // no default validation logic here + // No default validation logic here. } /// @@ -285,7 +287,7 @@ namespace System.Web.OData.Query.Validators throw Error.ArgumentNull("settings"); } - // no default validation logic here + // Validate child nodes but not the ConvertNode itself. ValidateQueryNode(convertNode.Source, settings); } @@ -312,8 +314,6 @@ namespace System.Web.OData.Query.Validators throw new ODataException(Error.Format(SRResources.NotFilterablePropertyUsedInFilter, navigationProperty.Name)); } - // no default validation logic here - // recursion if (sourceNode != null) { @@ -342,7 +342,7 @@ namespace System.Web.OData.Query.Validators throw Error.ArgumentNull("settings"); } - // no default validation logic here + // No default validation logic here. } /// @@ -366,14 +366,13 @@ namespace System.Web.OData.Query.Validators throw Error.ArgumentNull("settings"); } - // Check whether the property is not filterable + // Check whether the property is filterable. IEdmProperty property = propertyAccessNode.Property; if (EdmLibHelpers.IsNotFilterable(property, _model)) { throw new ODataException(Error.Format(SRResources.NotFilterablePropertyUsedInFilter, property.Name)); } - // no default validation logic here ValidateQueryNode(propertyAccessNode.Source, settings); } @@ -398,7 +397,13 @@ namespace System.Web.OData.Query.Validators throw Error.ArgumentNull("settings"); } - // no default validation logic here + // Check whether the property is filterable. + IEdmProperty property = propertyAccessNode.Property; + if (EdmLibHelpers.IsNotFilterable(property, _model)) + { + throw new ODataException(Error.Format(SRResources.NotFilterablePropertyUsedInFilter, property.Name)); + } + ValidateQueryNode(propertyAccessNode.Source, settings); } @@ -431,6 +436,35 @@ namespace System.Web.OData.Query.Validators } } + /// + /// Override this method to validate single entity function calls, such as 'cast'. + /// + /// The node to validate. + /// The settings to use while validating. + /// + /// This method is intended to be called from method overrides in subclasses. This method also supports unit + /// testing scenarios and is not intended to be called from user code. Call the Validate method to validate a + /// instance. + /// + public virtual void ValidateSingleEntityFunctionCallNode(SingleEntityFunctionCallNode node, ODataValidationSettings settings) + { + if (node == null) + { + throw Error.ArgumentNull("node"); + } + + if (settings == null) + { + throw Error.ArgumentNull("settings"); + } + + ValidateFunction(node.Name, settings); + foreach (QueryNode argumentNode in node.Parameters) + { + ValidateQueryNode(argumentNode, settings); + } + } + /// /// Override this method to validate the Not operator. /// @@ -453,6 +487,9 @@ namespace System.Web.OData.Query.Validators throw new ODataException(Error.Format(SRResources.NotAllowedLogicalOperator, unaryOperatorNode.OperatorKind, "AllowedLogicalOperators")); } break; + + default: + throw Error.NotSupported(SRResources.UnaryNodeValidationNotSupported, unaryOperatorNode.OperatorKind, typeof(FilterQueryValidator).Name); } } @@ -566,6 +603,14 @@ namespace System.Web.OData.Query.Validators case QueryNodeKind.EntityCollectionCast: ValidateEntityCollectionCastNode(node as EntityCollectionCastNode, settings); break; + + case QueryNodeKind.CollectionFunctionCall: + case QueryNodeKind.EntityCollectionFunctionCall: + case QueryNodeKind.CollectionOpenPropertyAccess: + case QueryNodeKind.CollectionPropertyCast: + // Unused or have unknown uses. + default: + throw Error.NotSupported(SRResources.QueryNodeValidationNotSupported, node.Kind, typeof(FilterQueryValidator).Name); } } @@ -610,6 +655,10 @@ namespace System.Web.OData.Query.Validators ValidateSingleValueFunctionCallNode(node as SingleValueFunctionCallNode, settings); break; + case QueryNodeKind.SingleEntityFunctionCall: + ValidateSingleEntityFunctionCallNode((SingleEntityFunctionCallNode)node, settings); + break; + case QueryNodeKind.SingleNavigationNode: SingleNavigationNode navigationNode = node as SingleNavigationNode; ValidateNavigationPropertyNode(navigationNode.Source, navigationNode.NavigationProperty, settings); @@ -626,6 +675,17 @@ namespace System.Web.OData.Query.Validators case QueryNodeKind.All: ValidateAllNode(node as AllNode, settings); break; + + case QueryNodeKind.NamedFunctionParameter: + case QueryNodeKind.SingleValueOpenPropertyAccess: + case QueryNodeKind.ParameterAlias: + case QueryNodeKind.EntitySet: + case QueryNodeKind.KeyLookup: + case QueryNodeKind.SearchTerm: + case QueryNodeKind.SingleValueCast: + // Unused or have unknown uses. + default: + throw Error.NotSupported(SRResources.QueryNodeValidationNotSupported, node.Kind, typeof(FilterQueryValidator).Name); } } @@ -685,7 +745,7 @@ namespace System.Web.OData.Query.Validators case ClrCanonicalFunctions.IndexofFunctionName: result = AllowedFunctions.IndexOf; break; - case "IsOf": + case "isof": result = AllowedFunctions.IsOf; break; case ClrCanonicalFunctions.LengthFunctionName: diff --git a/OData/src/System.Web.OData/Properties/SRResources.Designer.cs b/OData/src/System.Web.OData/Properties/SRResources.Designer.cs index f4d005e3..a51d9dd7 100644 --- a/OData/src/System.Web.OData/Properties/SRResources.Designer.cs +++ b/OData/src/System.Web.OData/Properties/SRResources.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.34014 +// Runtime Version:4.0.30319.35317 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -1563,6 +1563,15 @@ namespace System.Web.OData.Properties { } } + /// + /// Looks up a localized string similar to Validating OData QueryNode of kind {0} is not supported by {1}.. + /// + internal static string QueryNodeValidationNotSupported { + get { + return ResourceManager.GetString("QueryNodeValidationNotSupported", resourceCulture); + } + } + /// /// Looks up a localized string similar to The query parameter '{0}' is not supported.. /// @@ -1869,6 +1878,15 @@ namespace System.Web.OData.Properties { } } + /// + /// Looks up a localized string similar to Validating OData UnaryOperatorNode of kind {0} is not supported by {1}.. + /// + internal static string UnaryNodeValidationNotSupported { + get { + return ResourceManager.GetString("UnaryNodeValidationNotSupported", resourceCulture); + } + } + /// /// Looks up a localized string similar to The element type '{0}' of the given collection type '{1}' is not of the type '{2}'.. /// diff --git a/OData/src/System.Web.OData/Properties/SRResources.resx b/OData/src/System.Web.OData/Properties/SRResources.resx index 207be994..6229cb95 100644 --- a/OData/src/System.Web.OData/Properties/SRResources.resx +++ b/OData/src/System.Web.OData/Properties/SRResources.resx @@ -198,6 +198,12 @@ Binding OData QueryNode of kind {0} is not supported by {1}. + + Validating OData QueryNode of kind {0} is not supported by {1}. + + + Validating OData UnaryOperatorNode of kind {0} is not supported by {1}. + Duplicate property named '{0}' is not supported in '$orderby'. diff --git a/OData/test/System.Web.Http.OData.Test/OData/EnableQueryTest.cs b/OData/test/System.Web.Http.OData.Test/OData/EnableQueryTest.cs new file mode 100644 index 00000000..352ee8ff --- /dev/null +++ b/OData/test/System.Web.Http.OData.Test/OData/EnableQueryTest.cs @@ -0,0 +1,678 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Web.Http.OData.Builder; +using System.Web.Http.OData.Extensions; +using System.Web.Http.OData.Query; +using Microsoft.Data.Edm; +using Microsoft.TestCommon; + +namespace System.Web.Http.OData.Test +{ + public class EnableQueryTests + { + // Other not allowed query options like $orderby, $top, etc. + public static TheoryDataSet OtherQueryOptionsTestData + { + get + { + return new TheoryDataSet + { + {"?$orderby=Id", "OrderBy"}, + {"?$top=5", "Top"}, + {"?$skip=10", "Skip"}, + {"?$inlinecount=allpages", "Count"}, + {"?$select=Id", "Select"}, + {"?$expand=Orders", "Expand"}, + }; + } + } + + // Other unsupported query options + public static TheoryDataSet OtherUnsupportedQueryOptionsTestData + { + get + { + return new TheoryDataSet + { + {"?$format=json", "Format"}, + {"?$skiptoken=5", "SkipToken"}, + }; + } + } + + public static TheoryDataSet LogicalOperatorsTestData + { + get + { + return new TheoryDataSet + { + // And and or operators + {"?$filter=Adult or false", "'Or'"}, + {"?$filter=true and Adult", "'And'"}, + + // Logical operators with simple property + {"?$filter=Id ne 5", "'NotEqual'"}, + {"?$filter=Id gt 5", "'GreaterThan'"}, + {"?$filter=Id ge 5", "'GreaterThanOrEqual'"}, + {"?$filter=Id lt 5", "'LessThan'"}, + {"?$filter=Id le 5", "'LessThanOrEqual'"}, + + // Logical operators with property in a complex type property + {"?$filter=Address/ZipCode ne 5", "'NotEqual'"}, + {"?$filter=Address/ZipCode gt 5", "'GreaterThan'"}, + {"?$filter=Address/ZipCode ge 5", "'GreaterThanOrEqual'"}, + {"?$filter=Address/ZipCode lt 5", "'LessThan'"}, + {"?$filter=Address/ZipCode le 5", "'LessThanOrEqual'"}, + + // Logical operators with property in a single valued navigation property + {"?$filter=Category/Id ne 5", "'NotEqual'"}, + {"?$filter=Category/Id gt 5", "'GreaterThan'"}, + {"?$filter=Category/Id ge 5", "'GreaterThanOrEqual'"}, + {"?$filter=Category/Id lt 5", "'LessThan'"}, + {"?$filter=Category/Id le 5", "'LessThanOrEqual'"}, + + // Logical operators with property in a derived type in a single valued navigation property + {"?$filter=Category/System.Web.Http.OData.Test.PremiumEnableQueryCategory/PremiumLevel ne 5", "NotEqual'"}, + {"?$filter=Category/System.Web.Http.OData.Test.PremiumEnableQueryCategory/PremiumLevel gt 5", "GreaterThan'"}, + {"?$filter=Category/System.Web.Http.OData.Test.PremiumEnableQueryCategory/PremiumLevel ge 5", "GreaterThanOrEqual'"}, + {"?$filter=Category/System.Web.Http.OData.Test.PremiumEnableQueryCategory/PremiumLevel lt 5", "LessThan'"}, + {"?$filter=Category/System.Web.Http.OData.Test.PremiumEnableQueryCategory/PremiumLevel le 5", "LessThanOrEqual'"}, + + // not operator + {"?$filter=not Adult", "'Not'"}, + }; + } + } + + public static TheoryDataSet EqualsOperatorTestData + { + get + { + return new TheoryDataSet + { + {"?$filter=Id eq 5", "Equal"}, + {"?$filter=Address/ZipCode eq 5", "Equal"}, + {"?$filter=Category/Id eq 5", "Equal"}, + {"?$filter=Category/System.Web.Http.OData.Test.PremiumEnableQueryCategory/PremiumLevel eq 5", "Equal"}, + }; + } + } + + public static TheoryDataSet ArithmeticOperatorsTestData + { + get + { + return new TheoryDataSet + { + // Arithmetic operators with simple property + {"?$filter=1 eq (3 add Id)", "Add"}, + {"?$filter=1 eq (3 sub Id)", "Subtract"}, + {"?$filter=1 eq (1 mul Id)", "Multiply"}, + {"?$filter=1 eq (Id div 1)", "Divide"}, + {"?$filter=1 eq (Id mod 1)", "Modulo"}, + + // Arithmetic operators with property in a complex type property + {"?$filter=1 eq (3 add Address/ZipCode)", "Add"}, + {"?$filter=1 eq (3 sub Address/ZipCode)", "Subtract"}, + {"?$filter=1 eq (1 mul Address/ZipCode)", "Multiply"}, + {"?$filter=1 eq (Address/ZipCode div 1)", "Divide"}, + {"?$filter=1 eq (Address/ZipCode mod 1)", "Modulo"}, + + // Arithmetic operators with property in a single valued navigation property + {"?$filter=1 eq (3 add Category/Id)", "Add"}, + {"?$filter=1 eq (3 sub Category/Id)", "Subtract"}, + {"?$filter=1 eq (1 mul Category/Id)", "Multiply"}, + {"?$filter=1 eq (Category/Id div 1)", "Divide"}, + {"?$filter=1 eq (Category/Id mod 1)", "Modulo"}, + + // Arithmetic operators with property in a derived type in a single valued navigation property + {"?$filter=1 eq (3 add Category/System.Web.Http.OData.Test.PremiumEnableQueryCategory/PremiumLevel)", "Add"}, + {"?$filter=1 eq (3 sub Category/System.Web.Http.OData.Test.PremiumEnableQueryCategory/PremiumLevel)", "Subtract"}, + {"?$filter=1 eq (1 mul Category/System.Web.Http.OData.Test.PremiumEnableQueryCategory/PremiumLevel)", "Multiply"}, + {"?$filter=1 eq (Category/System.Web.Http.OData.Test.PremiumEnableQueryCategory/PremiumLevel div 1)", "Divide"}, + {"?$filter=1 eq (Category/System.Web.Http.OData.Test.PremiumEnableQueryCategory/PremiumLevel mod 1)", "Modulo"}, + }; + } + } + + public static TheoryDataSet AnyAndAllFunctionsTestData + { + get + { + return new TheoryDataSet + { + // Primitive collection property + {"?$filter=Points/any()", "any"}, + {"?$filter=Points/any(p: p eq 1)", "any"}, + {"?$filter=Points/all(p: p eq 1)", "all"}, + + // Complex type collection property + {"?$filter=Addresses/any()", "any"}, + {"?$filter=Addresses/any(a: a/ZipCode eq 1)", "any"}, + {"?$filter=Addresses/all(a: a/ZipCode eq 1)", "all"}, + + // Collection navigation property + {"?$filter=Orders/any()", "any"}, + {"?$filter=Orders/any(o: o/Id eq 1)", "any"}, + {"?$filter=Orders/all(o: o/Id eq 1)", "all"}, + + // Collection navigation property with casts + {"?$filter=Orders/any(o: o/System.Web.Http.OData.Test.DiscountedEnableQueryOrder/Discount eq 1)", "any"}, + {"?$filter=Orders/all(o: o/System.Web.Http.OData.Test.DiscountedEnableQueryOrder/Discount eq 1)", "all"}, + {"?$filter=Orders/System.Web.Http.OData.Test.DiscountedEnableQueryOrder/any()", "any"}, + {"?$filter=Orders/System.Web.Http.OData.Test.DiscountedEnableQueryOrder/any(o: o/Discount eq 1)", "any"}, + {"?$filter=Orders/System.Web.Http.OData.Test.DiscountedEnableQueryOrder/all(o: o/Discount eq 1)", "all"}, + }; + + } + } + + public static TheoryDataSet CastFunctionTestData + { + get + { + return new TheoryDataSet + { + // Entity type casts + {"?$filter=cast(Category,'System.Web.Http.OData.Test.PremiumEnableQueryCategory') eq null", "cast"}, + {"?$filter=cast('System.Web.Http.OData.Test.PremiumEnableQueryCustomer') eq null", "cast"}, + }; + } + } + + public static TheoryDataSet IsOfFunctionTestData + { + get + { + return new TheoryDataSet + { + // Entity type casts + {"?$filter=isof(Category,'System.Web.Http.OData.Test.PremiumEnableQueryCategory')", "isof"}, + {"?$filter=isof('System.Web.Http.OData.Test.PremiumEnableQueryCustomer')", "isof"}, + }; + } + } + + public static TheoryDataSet StringFunctionsTestData + { + get + { + return new TheoryDataSet + { + {"?$filter=startswith(Name, 'Customer')", "startswith"}, + {"?$filter=endswith(Name, 'Customer')", "endswith"}, + {"?$filter=substringof(Name, 'Customer')", "substringof"}, + {"?$filter=length(Name) eq 1", "length"}, + {"?$filter=indexof(Name, 'Customer') eq 1", "indexof"}, + {"?$filter=concat('Customer', Name) eq 'Customer'", "concat"}, + {"?$filter=substring(Name, 3) eq 'Customer'", "substring"}, + {"?$filter=substring(Name, 3, 3) eq 'Customer'", "substring"}, + {"?$filter=tolower(Name) eq 'customer'", "tolower"}, + {"?$filter=toupper(Name) eq 'CUSTOMER'", "toupper"}, + {"?$filter=trim(Name) eq 'Customer'", "trim"}, + }; + } + } + + public static TheoryDataSet MathFunctionsTestData + { + get + { + return new TheoryDataSet + { + {"?$filter=round(Id) eq 1", "round"}, + {"?$filter=floor(Id) eq 1", "floor"}, + {"?$filter=ceiling(Id) eq 1", "ceiling"}, + }; + } + } + + public static TheoryDataSet SupportedDateTimeFunctionsTestData + { + get + { + return new TheoryDataSet + { + {"?$filter=year(AbsoluteBirthDate) eq 1987", "year"}, + {"?$filter=month(AbsoluteBirthDate) eq 1987", "month"}, + {"?$filter=day(AbsoluteBirthDate) eq 1987", "day"}, + {"?$filter=hour(AbsoluteBirthDate) eq 1987", "hour"}, + {"?$filter=minute(AbsoluteBirthDate) eq 1987", "minute"}, + {"?$filter=second(AbsoluteBirthDate) eq 1987", "second"}, + }; + } + } + + // These represent time functions that we validate but for which we don't support + // end to end. + public static TheoryDataSet UnsupportedDateTimeFunctionsTestData + { + get + { + return new TheoryDataSet + { + {"?$filter=years(Time) eq 1987", "years"}, + {"?$filter=months(Time) eq 1987", "months"}, + {"?$filter=days(Time) eq 1987", "days"}, + {"?$filter=hours(Time) eq 1987", "hours"}, + {"?$filter=minutes(Time) eq 1987", "minutes"}, + {"?$filter=seconds(Time) eq 1987", "seconds"}, + }; + } + } + + // Other limitations like MaxSkip, MaxTop, AllowedOrderByProperties, etc. + public static TheoryDataSet NumericQueryLimitationsTestData + { + get + { + return new TheoryDataSet + { + {"?$orderby=Name desc, Id asc", "$orderby"}, + {"?$skip=20", "Skip"}, + {"?$top=20", "Top"}, + {"?$expand=Orders/OrderLines", "$expand"}, + {"?$filter=Orders/any(o: o/OrderLines/all(ol: ol/Id gt 0))", "MaxAnyAllExpressionDepth"}, + {"?$filter=Orders/any(o: o/Total gt 0) and Id eq 5", "MaxNodeCount"}, + }; + } + } + + [Theory] + [PropertyData("LogicalOperatorsTestData")] + [PropertyData("ArithmeticOperatorsTestData")] + [PropertyData("StringFunctionsTestData")] + [PropertyData("MathFunctionsTestData")] + [PropertyData("SupportedDateTimeFunctionsTestData")] + [PropertyData("UnsupportedDateTimeFunctionsTestData")] + [PropertyData("AnyAndAllFunctionsTestData")] + [PropertyData("OtherQueryOptionsTestData")] + [PropertyData("OtherUnsupportedQueryOptionsTestData")] + public void EnableQuery_Blocks_NotAllowedQueries(string queryString, string expectedElement) + { + // Arrange + string url = "http://localhost/odata/OnlyFilterAndEqualsAllowedCustomers"; + HttpServer server = CreateServer("OnlyFilterAndEqualsAllowedCustomers"); + HttpClient client = new HttpClient(server); + + // Act + HttpResponseMessage response = client.GetAsync(url + queryString).Result; + string errorMessage = response.Content.ReadAsStringAsync().Result; + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Contains("not allowed", errorMessage); + Assert.Contains(expectedElement, errorMessage); + } + + [Theory] + [PropertyData("LogicalOperatorsTestData")] + [PropertyData("ArithmeticOperatorsTestData")] + [PropertyData("StringFunctionsTestData")] + [PropertyData("MathFunctionsTestData")] + [PropertyData("SupportedDateTimeFunctionsTestData")] + [PropertyData("UnsupportedDateTimeFunctionsTestData")] + [PropertyData("AnyAndAllFunctionsTestData")] + public void EnableQuery_BlocksFilter_WhenNotAllowed(string queryString, string unused) + { + // Arrange + string url = "http://localhost/odata/FilterDisabledCustomers"; + HttpServer server = CreateServer("FilterDisabledCustomers"); + HttpClient client = new HttpClient(server); + + // Act + HttpResponseMessage response = client.GetAsync(url + queryString).Result; + string errorMessage = response.Content.ReadAsStringAsync().Result; + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Contains("not allowed", errorMessage); + Assert.Contains("Filter", errorMessage); + } + + [Theory] + [PropertyData("OtherUnsupportedQueryOptionsTestData")] + public void EnableQuery_ReturnsBadRequest_ForUnsupportedQueryOptions(string queryString, string expectedElement) + { + // Arrange + string url = "http://localhost/odata/EverythingAllowedCustomers"; + HttpServer server = CreateServer("EverythingAllowedCustomers"); + HttpClient client = new HttpClient(server); + + // Act + HttpResponseMessage response = client.GetAsync(url + queryString).Result; + string errorMessage = response.Content.ReadAsStringAsync().Result; + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Contains("not allowed", errorMessage); + Assert.Contains(expectedElement, errorMessage); + } + + // We check equals separately because we need to use it in the rest of the + // tests to produce valid filter expressions in other cases, so we need to + // enable it in those tests and this test only makes sure it covers the case + // when everything is disabled + [Theory] + [PropertyData("EqualsOperatorTestData")] + public void EnableQuery_BlocksEquals_WhenNotAllowed(string queryString, string expectedElement) + { + // Arrange + string url = "http://localhost/odata/OnlyFilterAllowedCustomers"; + HttpServer server = CreateServer("OnlyFilterAllowedCustomers"); + HttpClient client = new HttpClient(server); + + // Act + HttpResponseMessage response = client.GetAsync(url + queryString).Result; + string errorMessage = response.Content.ReadAsStringAsync().Result; + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Contains("not allowed", errorMessage); + Assert.Contains(expectedElement, errorMessage); + } + + [Theory] + [PropertyData("LogicalOperatorsTestData")] + [PropertyData("ArithmeticOperatorsTestData")] + [PropertyData("EqualsOperatorTestData")] + [PropertyData("OtherQueryOptionsTestData")] + [PropertyData("StringFunctionsTestData")] + [PropertyData("MathFunctionsTestData")] + [PropertyData("SupportedDateTimeFunctionsTestData")] + [PropertyData("AnyAndAllFunctionsTestData")] + public void EnableQuery_DoesNotBlockQueries_WhenEverythingIsAllowed(string queryString, string unused) + { + // Arrange + string url = "http://localhost/odata/EverythingAllowedCustomers"; + HttpServer server = CreateServer("EverythingAllowedCustomers"); + HttpClient client = new HttpClient(server); + + // Act + HttpResponseMessage response = client.GetAsync(url + queryString).Result; + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Theory] + [PropertyData("UnsupportedDateTimeFunctionsTestData")] + public void EnableQuery_ReturnsBadRequest_ForUnsupportedFunctions(string queryString, string expectedElement) + { + // Arrange + string url = "http://localhost/odata/EverythingAllowedCustomers"; + HttpServer server = CreateServer("EverythingAllowedCustomers"); + HttpClient client = new HttpClient(server); + + // Act + HttpResponseMessage response = client.GetAsync(url + queryString).Result; + string errorMessage = response.Content.ReadAsStringAsync().Result; + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Contains("unknown function", errorMessage); + Assert.Contains(expectedElement, errorMessage); + } + + [Theory] + [PropertyData("IsOfFunctionTestData")] + [PropertyData("CastFunctionTestData")] + public void EnableQuery_ReturnsInternalServerError_ForIsOfAndCastFunctions(string queryString, string unused) + { + // Arrange + string url = "http://localhost/odata/EverythingAllowedCustomers"; + HttpServer server = CreateServer("EverythingAllowedCustomers"); + HttpClient client = new HttpClient(server); + + // Act + HttpResponseMessage response = client.GetAsync(url + queryString).Result; + string errorMessage = response.Content.ReadAsStringAsync().Result; + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Contains("An error has occurred", errorMessage); + } + + [Theory] + [PropertyData("NumericQueryLimitationsTestData")] + public void EnableQuery_BlocksQueries_WithOtherLimitations(string queryString, string expectedElement) + { + // Arrange + string url = "http://localhost/odata/OtherLimitationsCustomers"; + HttpServer server = CreateServer("OtherLimitationsCustomers"); + HttpClient client = new HttpClient(server); + + // Act + HttpResponseMessage response = client.GetAsync(url + queryString).Result; + string errorMessage = response.Content.ReadAsStringAsync().Result; + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Contains(expectedElement, errorMessage); + } + + // This controller limits any operation except for $filter and the eq operator + // in order to validate that all limitations work. + public class OnlyFilterAndEqualsAllowedCustomersController : ODataController + { + private static readonly IQueryable _customers; + + [EnableQuery( + AllowedFunctions = AllowedFunctions.None, + AllowedLogicalOperators = AllowedLogicalOperators.Equal, + AllowedQueryOptions = AllowedQueryOptions.Filter, + AllowedArithmeticOperators = AllowedArithmeticOperators.None)] + public IQueryable Get() + { + return _customers; + } + + static OnlyFilterAndEqualsAllowedCustomersController() + { + _customers = CreateCustomers().AsQueryable(); + } + } + + // This controller exposes an action that limits everything except for + // filtering in order to verify that limiting the eq operator works. + // We didn't limit the eq operator in other queries as we need it to + // create valid filter queries using other limited elements and we + // want the query to fail because of limitations imposed on other + // elements rather than eq. + public class OnlyFilterAllowedCustomersController : ODataController + { + private static readonly IQueryable _customers; + + [EnableQuery( + AllowedFunctions = AllowedFunctions.None, + AllowedLogicalOperators = AllowedLogicalOperators.None, + AllowedQueryOptions = AllowedQueryOptions.Filter, + AllowedArithmeticOperators = AllowedArithmeticOperators.None)] + public IQueryable Get() + { + return _customers; + } + + static OnlyFilterAllowedCustomersController() + { + _customers = CreateCustomers().AsQueryable(); + } + } + + // This controller disables all the query options ($filter amongst them) + public class FilterDisabledCustomersController : ODataController + { + private static readonly IQueryable _customers; + + [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.None)] + public IQueryable Get() + { + return _customers; + } + + static FilterDisabledCustomersController() + { + _customers = CreateCustomers().AsQueryable(); + } + } + + // This controller doesn't limit anything specific to ensure that the + // requests succeed if they aren't limited. + public class EverythingAllowedCustomersController : ODataController + { + private static readonly IQueryable _customers; + + [EnableQuery] + public IQueryable Get() + { + return _customers; + } + + static EverythingAllowedCustomersController() + { + _customers = CreateCustomers().AsQueryable(); + } + } + + // This controller exposes an action that has limitations on aspects + // other than AllowedFunctions, AllowedLogicalOperators, etc. + public class OtherLimitationsCustomersController : ODataController + { + private static readonly IQueryable _customers; + + [EnableQuery(MaxNodeCount = 5, + MaxExpansionDepth = 1, + MaxAnyAllExpressionDepth = 1, + MaxSkip = 5, + MaxTop = 5, + MaxOrderByNodeCount = 1)] + public IQueryable Get() + { + return _customers; + } + + static OtherLimitationsCustomersController() + { + _customers = CreateCustomers().AsQueryable(); + } + } + + private static HttpServer CreateServer(string customersEntitySet) + { + HttpConfiguration configuration = new HttpConfiguration(); + + ODataModelBuilder builder = new ODataConventionModelBuilder(); + + builder.EntitySet(customersEntitySet); + builder.Entity(); + + builder.EntitySet("EnableQueryCategories"); + builder.Entity(); + + builder.EntitySet("EnableQueryOrders"); + builder.Entity(); + + builder.EntitySet("EnableQueryOrderLines"); + + builder.ComplexType(); + + IEdmModel model = builder.GetEdmModel(); + + configuration.Routes.MapODataServiceRoute("odata", "odata", model); + + return new HttpServer(configuration); + } + + // We need to create the data as we need the queries to succeed in one scenario. + private static IEnumerable CreateCustomers() + { + PremiumEnableQueryCustomer customer = new PremiumEnableQueryCustomer(); + + customer.Id = 1; + customer.Name = "Customer 1"; + customer.Points = Enumerable.Range(1, 10).ToList(); + customer.Address = new EnableQueryAddress { ZipCode = 1 }; + customer.Addresses = Enumerable.Range(1, 10).Select(j => new EnableQueryAddress { ZipCode = j }).ToList(); + + customer.Category = new PremiumEnableQueryCategory + { + Id = 1, + PremiumLevel = 1, + }; + + customer.Orders = Enumerable.Range(1, 10).Select(j => new DiscountedEnableQueryOrder + { + Id = j, + Total = j, + Discount = j, + }).ToList(); + + yield return customer; + } + + public class EnableQueryCustomer + { + public int Id { get; set; } + + public string Name { get; set; } + + public EnableQueryCategory Category { get; set; } + + public ICollection Orders { get; set; } + + public ICollection Points { get; set; } + + public ICollection Addresses { get; set; } + + public EnableQueryAddress Address { get; set; } + + public DateTimeOffset AbsoluteBirthDate { get; set; } + + public TimeSpan Time { get; set; } + + public bool Adult { get; set; } + } + + public class PremiumEnableQueryCustomer : EnableQueryCustomer + { + } + + public class EnableQueryOrder + { + public int Id { get; set; } + + public double Total { get; set; } + + public ICollection OrderLines { get; set; } + } + + public class EnableQueryOrderLine + { + public int Id { get; set; } + } + + public class DiscountedEnableQueryOrder : EnableQueryOrder + { + public double Discount { get; set; } + } + + public class EnableQueryCategory + { + public int Id { get; set; } + } + + public class PremiumEnableQueryCategory : EnableQueryCategory + { + public int PremiumLevel { get; set; } + } + + public class EnableQueryAddress + { + public int ZipCode { get; set; } + } + } +} diff --git a/OData/test/System.Web.Http.OData.Test/OData/Query/Expressions/DataModel.cs b/OData/test/System.Web.Http.OData.Test/OData/Query/Expressions/DataModel.cs index da8a7228..9638612c 100644 --- a/OData/test/System.Web.Http.OData.Test/OData/Query/Expressions/DataModel.cs +++ b/OData/test/System.Web.Http.OData.Test/OData/Query/Expressions/DataModel.cs @@ -16,6 +16,7 @@ namespace System.Web.Http.OData.Query.Expressions public int CategoryID { get; set; } public string QuantityPerUnit { get; set; } public decimal? UnitPrice { get; set; } + public double? Weight { get; set; } public short? UnitsInStock { get; set; } public short? UnitsOnOrder { get; set; } diff --git a/OData/test/System.Web.Http.OData.Test/OData/Query/Expressions/FilterBinderTests.cs b/OData/test/System.Web.Http.OData.Test/OData/Query/Expressions/FilterBinderTests.cs index 760f8c82..12c8e5ac 100644 --- a/OData/test/System.Web.Http.OData.Test/OData/Query/Expressions/FilterBinderTests.cs +++ b/OData/test/System.Web.Http.OData.Test/OData/Query/Expressions/FilterBinderTests.cs @@ -384,7 +384,7 @@ namespace System.Web.Http.OData.Query.Expressions var filters = VerifyQueryDeserialization(filter); var result = RunFilter(filters.WithoutNullPropagation, new DataTypes { StringProp = value }); - Assert.Equal(result, expectedResult); + Assert.Equal(expectedResult, result); } // Issue: 477 @@ -1380,6 +1380,649 @@ namespace System.Web.Http.OData.Query.Expressions #endregion + #region cast in query option + + [Theory] + [InlineData("cast(NoSuchProperty,Edm.Int32) ne null", + "Could not find a property named 'NoSuchProperty' on type 'System.Web.Http.OData.Query.Expressions.DataTypes'.")] + public void Cast_UndefinedSource_ThrowsODataException(string filter, string errorMessage) + { + // Arrange & Act & Assert + Assert.Throws(() => Bind(filter), errorMessage); + } + + public static TheoryDataSet CastToUnquotedPrimitiveType + { + get + { + return new TheoryDataSet + { + { "cast(Edm.Binary) eq null", "Edm.Binary" }, + { "cast(Edm.Boolean) eq null", "Edm.Boolean" }, + { "cast(Edm.Byte) eq null", "Edm.Byte" }, + { "cast(Edm.DateTime) eq null", "Edm.DateTime" }, + { "cast(Edm.DateTimeOffset) eq null", "Edm.DateTimeOffset" }, + { "cast(Edm.Decimal) eq null", "Edm.Decimal" }, + { "cast(Edm.Double) eq null", "Edm.Double" }, + { "cast(Edm.Guid) eq null", "Edm.Guid" }, + { "cast(Edm.Int16) eq null", "Edm.Int16" }, + { "cast(Edm.Int32) eq null", "Edm.Int32" }, + { "cast(Edm.Int64) eq null", "Edm.Int64" }, + { "cast(Edm.SByte) eq null", "Edm.SByte" }, + { "cast(Edm.Single) eq null", "Edm.Single" }, + { "cast(Edm.Stream) eq null", "Edm.Stream" }, + { "cast(Edm.String) eq null", "Edm.String" }, + { "cast(Edm.Time) eq null", "Edm.Time" }, + { "cast(Edm.Unknown) eq null", "Edm.Unknown" }, + + { "cast(null,Edm.Binary) eq null", "Edm.Binary" }, + { "cast(null,Edm.Boolean) eq null", "Edm.Boolean" }, + { "cast(null,Edm.Byte) eq null", "Edm.Byte" }, + { "cast(null,Edm.DateTime) eq null", "Edm.DateTime" }, + { "cast(null,Edm.DateTimeOffset) eq null", "Edm.DateTimeOffset" }, + { "cast(null,Edm.Decimal) eq null", "Edm.Decimal" }, + { "cast(null,Edm.Double) eq null", "Edm.Double" }, + { "cast(null,Edm.Guid) eq null", "Edm.Guid" }, + { "cast(null,Edm.Int16) eq null", "Edm.Int16" }, + { "cast(null,Edm.Int32) eq null", "Edm.Int32" }, + { "cast(null,Edm.Int64) eq null", "Edm.Int64" }, + { "cast(null,Edm.SByte) eq null", "Edm.SByte" }, + { "cast(null,Edm.Single) eq null", "Edm.Single" }, + { "cast(null,Edm.Stream) eq null", "Edm.Stream" }, + { "cast(null,Edm.String) eq null", "Edm.String" }, + { "cast(null,Edm.Time) eq null", "Edm.Time" }, + { "cast(null,Edm.Unknown) eq null", "Edm.Unknown" }, + + { "cast(binary'4F64617461',Edm.Binary) eq null", "Edm.Binary" }, + { "cast(false,Edm.Boolean) eq binary'4F64617461'", "Edm.Boolean" }, + { "cast(23,Edm.Byte) eq 23", "Edm.Byte" }, + { "cast(datetime'2001-01-01T12:00:00.000',Edm.DateTime) eq datetime'2001-01-01T12:00:00.000'", "Edm.DateTime" }, + { "cast(datetimeoffset'2001-01-01T12:00:00.000+08:00',Edm.DateTimeOffset) eq datetimeoffset'2001-01-01T12:00:00.000+08:00'", "Edm.DateTimeOffset" }, + { "cast(23,Edm.Decimal) eq 23", "Edm.Decimal" }, + { "cast(23,Edm.Double) eq 23", "Edm.Double" }, + { "cast(guid'00000000-0000-0000-0000-000000000000',Edm.Guid) eq guid'00000000-0000-0000-0000-000000000000'", "Edm.Guid" }, + { "cast(23,Edm.Int16) eq 23", "Edm.Int16" }, + { "cast(23,Edm.Int32) eq 23", "Edm.Int32" }, + { "cast(23,Edm.Int64) eq 23", "Edm.Int64" }, + { "cast(23,Edm.SByte) eq 23", "Edm.SByte" }, + { "cast(23,Edm.Single) eq 23", "Edm.Single" }, + { "cast('hello',Edm.Stream) eq null", "Edm.Stream" }, + { "cast('hello',Edm.String) eq 'hello'", "Edm.String" }, + { "cast(time'PT12H',Edm.Time) eq time'PT12H'", "Edm.Time" }, + { "cast('',Edm.Unknown) eq null", "Edm.Unknown" }, + + { "cast('OData',Edm.Binary) eq binary'4F64617461'", "Edm.Binary" }, + { "cast('false',Edm.Boolean) eq false", "Edm.Boolean" }, + { "cast('23',Edm.Byte) eq 23", "Edm.Byte" }, + { "cast('2001-01-01T12:00:00.000',Edm.DateTime) eq datetime'2001-01-01T12:00:00.000'", "Edm.DateTime" }, + { "cast('2001-01-01T12:00:00.000+08:00',Edm.DateTimeOffset) eq datetimeoffset'2001-01-01T12:00:00.000+08:00'", "Edm.DateTimeOffset" }, + { "cast('23',Edm.Decimal) eq 23", "Edm.Decimal" }, + { "cast('23',Edm.Double) eq 23", "Edm.Double" }, + { "cast('00000000-0000-0000-0000-000000000000',Edm.Guid) eq guid'00000000-0000-0000-0000-000000000000'", "Edm.Guid" }, + { "cast('23',Edm.Int16) eq 23", "Edm.Int16" }, + { "cast('23',Edm.Int32) eq 23", "Edm.Int32" }, + { "cast('23',Edm.Int64) eq 23", "Edm.Int64" }, + { "cast('23',Edm.SByte) eq 23", "Edm.SByte" }, + { "cast('23',Edm.Single) eq 23", "Edm.Single" }, + { "cast(23,Edm.String) eq '23'", "Edm.String" }, + { "cast('PT12H',Edm.Time) eq time'PT12H'", "Edm.Time" }, + + { "cast(ByteArrayProp,Edm.Binary) eq null", "Edm.Binary" }, + { "cast(DateTimeProp,Edm.DateTime) eq null", "Edm.DateTime" }, + { "cast(DateTimeOffsetProp,Edm.DateTimeOffset) eq null", "Edm.DateTimeOffset" }, + { "cast(DecimalProp,Edm.Decimal) eq null", "Edm.Decimal" }, + { "cast(DoubleProp,Edm.Double) eq null", "Edm.Double" }, + { "cast(GuidProp,Edm.Guid) eq null", "Edm.Guid" }, + { "cast(NullableShortProp,Edm.Int16) eq null", "Edm.Int16" }, + { "cast(IntProp,Edm.Int32) eq null", "Edm.Int32" }, + { "cast(LongProp,Edm.Int64) eq null", "Edm.Int64" }, + { "cast(FloatProp,Edm.Single) eq null", "Edm.Single" }, + { "cast(StringProp,Edm.String) eq null", "Edm.String" }, + }; + } + } + + [Theory] + [PropertyData("CastToUnquotedPrimitiveType")] + public void CastToUnquotedPrimitiveType_ThrowsODataException(string filter, string typeName) + { + // Arrange + var expectedMessage = string.Format( + "The child type '{0}' in a cast was not an entity type. Casts can only be performed on entity types.", + typeName); + + // Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + public static TheoryDataSet CastToQuotedPrimitiveType + { + get + { + return new TheoryDataSet + { + { "cast('Edm.Binary') eq null" }, + { "cast('Edm.Boolean') eq null" }, + { "cast('Edm.Byte') eq null" }, + { "cast('Edm.DateTime') eq null" }, + { "cast('Edm.DateTimeOffset') eq null" }, + { "cast('Edm.Decimal') eq null" }, + { "cast('Edm.Double') eq null" }, + { "cast('Edm.Guid') eq null" }, + { "cast('Edm.Int16') eq null" }, + { "cast('Edm.Int32') eq null" }, + { "cast('Edm.Int64') eq null" }, + { "cast('Edm.SByte') eq null" }, + { "cast('Edm.Single') eq null" }, + { "cast('Edm.String') eq null" }, + { "cast('Edm.Time') eq null" }, + + { "cast(null,'Edm.Binary') eq null" }, + { "cast(null,'Edm.Boolean') eq null" }, + { "cast(null,'Edm.Byte') eq null" }, + { "cast(null,'Edm.DateTime') eq null" }, + { "cast(null,'Edm.DateTimeOffset') eq null" }, + { "cast(null,'Edm.Decimal') eq null" }, + { "cast(null,'Edm.Double') eq null" }, + { "cast(null,'Edm.Guid') eq null" }, + { "cast(null,'Edm.Int16') eq null" }, + { "cast(null,'Edm.Int32') eq null" }, + { "cast(null,'Edm.Int64') eq null" }, + { "cast(null,'Edm.SByte') eq null" }, + { "cast(null,'Edm.Single') eq null" }, + { "cast(null,'Edm.String') eq null" }, + { "cast(null,'Edm.Time') eq null" }, + + { "cast(binary'4F64617461','Edm.Binary') eq binary'4F64617461'" }, + { "cast(false,'Edm.Boolean') eq false" }, + { "cast(23,'Edm.Byte') eq 23" }, + { "cast(datetime'2001-01-01T12:00:00.000','Edm.DateTime') eq datetime'2001-01-01T12:00:00.000'" }, + { "cast(datetimeoffset'2001-01-01T12:00:00.000+08:00','Edm.DateTimeOffset') eq datetimeoffset'2001-01-01T12:00:00.000+08:00'" }, + { "cast(23,'Edm.Decimal') eq 23" }, + { "cast(23,'Edm.Double') eq 23" }, + { "cast(guid'00000000-0000-0000-0000-000000000000','Edm.Guid') eq guid'00000000-0000-0000-0000-000000000000'" }, + { "cast(23,'Edm.Int16') eq 23" }, + { "cast(23,'Edm.Int32') eq 23" }, + { "cast(23,'Edm.Int64') eq 23" }, + { "cast(23,'Edm.SByte') eq 23" }, + { "cast(23,'Edm.Single') eq 23" }, + { "cast('hello','Edm.String') eq 'hello'" }, + { "cast(time'PT12H','Edm.Time') eq time'PT12H'" }, + + { "cast('OData','Edm.Binary') eq binary'4F64617461'" }, + { "cast('false','Edm.Boolean') eq false" }, + { "cast('23','Edm.Byte') eq 23" }, + { "cast('2001-01-01T12:00:00.000','Edm.DateTime') eq datetime'2001-01-01T12:00:00.000'" }, + { "cast('2001-01-01T12:00:00.000+08:00','Edm.DateTimeOffset') eq datetimeoffset'2001-01-01T12:00:00.000+08:00'" }, + { "cast('23','Edm.Decimal') eq 23" }, + { "cast('23','Edm.Double') eq 23" }, + { "cast('00000000-0000-0000-0000-000000000000','Edm.Guid') eq guid'00000000-0000-0000-0000-000000000000'" }, + { "cast('23','Edm.Int16') eq 23" }, + { "cast('23','Edm.Int32') eq 23" }, + { "cast('23','Edm.Int64') eq 23" }, + { "cast('23','Edm.SByte') eq 23" }, + { "cast('23','Edm.Single') eq 23" }, + { "cast(23,'Edm.String') eq '23'" }, + { "cast('PT12H','Edm.Time') eq time'PT12H'" }, + + { "cast(ByteArrayProp,'Edm.Binary') eq null" }, + { "cast(DateTimeProp,'Edm.DateTime') eq datetime'2001-01-01T12:00:00.000'" }, + { "cast(DateTimeOffsetProp,'Edm.DateTimeOffset') eq datetimeoffset'2001-01-01T12:00:00.000+08:00'" }, + { "cast(DecimalProp,'Edm.Decimal') eq 23" }, + { "cast(DoubleProp,'Edm.Double') eq 23" }, + { "cast(GuidProp,'Edm.Guid') eq guid'0EFDAECF-A9F0-42F3-A384-1295917AF95E'" }, + { "cast(NullableShortProp,'Edm.Int16') eq 23" }, + { "cast(IntProp,'Edm.Int32') eq 23" }, + { "cast(LongProp,'Edm.Int64') eq 23" }, + { "cast(FloatProp,'Edm.Single') eq 23" }, + { "cast(StringProp,'Edm.String') eq 'hello'" }, + { "cast(TimeSpanProp,'Edm.Time') eq time'PT23H'" }, + }; + } + } + + // Exception messages here is misleading since issue is actually quoting the type name. + [Theory] + [PropertyData("CastToQuotedPrimitiveType")] + public void CastToQuotedPrimitiveType_ThrowsNotImplemented(string filter) + { + // Arrange + var expectedMessage = "Unknown function 'cast'."; + + // Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + public static TheoryDataSet CastToUnquotedComplexType + { + get + { + return new TheoryDataSet + { + { "cast(System.Web.Http.OData.Query.Expressions.Address) eq null" }, + { "cast(null, System.Web.Http.OData.Query.Expressions.Address) eq null" }, + { "cast('', System.Web.Http.OData.Query.Expressions.Address) eq null" }, + { "cast(SupplierAddress, System.Web.Http.OData.Query.Expressions.Address) eq null" }, + }; + } + } + + [Theory] + [PropertyData("CastToUnquotedComplexType")] + public void CastToUnquotedComplexType_ThrowsODataException(string filter) + { + // Arrange + var expectedMessage = + "The child type 'System.Web.Http.OData.Query.Expressions.Address' in a cast was not an entity type. " + + "Casts can only be performed on entity types."; + + // Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + public static TheoryDataSet CastToQuotedComplexType + { + get + { + return new TheoryDataSet + { + { "cast('System.Web.Http.OData.Query.Expressions.Address') eq null" }, + { "cast(null, 'System.Web.Http.OData.Query.Expressions.Address') eq null" }, + { "cast('', 'System.Web.Http.OData.Query.Expressions.Address') eq null" }, + { "cast(SupplierAddress, 'System.Web.Http.OData.Query.Expressions.Address') eq null" }, + }; + } + } + + [Theory] + [PropertyData("CastToQuotedComplexType")] + public void CastToQuotedComplexType_ThrowsNotImplemented(string filter) + { + // Arrange + var expectedMessage = "Unknown function 'cast'."; + + // Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + public static TheoryDataSet CastToUnquotedEntityType + { + get + { + return new TheoryDataSet + { + { + "cast(System.Web.Http.OData.Query.Expressions.DerivedProduct)/DerivedProductName eq null", + "Cast or IsOf Function must have a type in its arguments." + }, + { + "cast(null, System.Web.Http.OData.Query.Expressions.DerivedCategory)/DerivedCategoryName eq null", + "Encountered invalid type cast. " + + "'System.Web.Http.OData.Query.Expressions.DerivedCategory' is not assignable from 'System.Web.Http.OData.Query.Expressions.Product'." + }, + { + "cast(Category, System.Web.Http.OData.Query.Expressions.DerivedCategory)/DerivedCategoryName eq null", + "Encountered invalid type cast. " + + "'System.Web.Http.OData.Query.Expressions.DerivedCategory' is not assignable from 'System.Web.Http.OData.Query.Expressions.Product'." + }, + }; + } + } + + [Theory] + [PropertyData("CastToUnquotedEntityType")] + public void CastToUnquotedEntityType_ThrowsODataException(string filter, string expectedMessage) + { + // Arrange & Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + [Theory] + [InlineData("cast('System.Web.Http.OData.Query.Expressions.DerivedProduct')/DerivedProductName eq null")] + [InlineData("cast(Category,'System.Web.Http.OData.Query.Expressions.DerivedCategory')/DerivedCategoryName eq null")] + [InlineData("cast(Category, 'System.Web.Http.OData.Query.Expressions.DerivedCategory')/DerivedCategoryName eq null")] + public void CastToQuotedEntityType_ThrowsNotSupported(string filter) + { + // Arrange + var expectedMessage = "Binding OData QueryNode of kind SingleEntityFunctionCall is not supported by FilterBinder."; + + // Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + [Theory] + [InlineData("cast(null,'System.Web.Http.OData.Query.Expressions.DerivedCategory')/DerivedCategoryName eq null")] + [InlineData("cast(null, 'System.Web.Http.OData.Query.Expressions.DerivedCategory')/DerivedCategoryName eq null")] + public void CastNullToQuotedEntityType_ThrowsNotImplemented(string filter) + { + // Arrange + var expectedMessage = "Unknown function 'cast'."; + + // Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + #endregion + + #region 'isof' in query option + + [Theory] + [InlineData("isof(NoSuchProperty,Edm.Int32)", + "Could not find a property named 'NoSuchProperty' on type 'System.Web.Http.OData.Query.Expressions.DataTypes'.")] + public void IsOfUndefinedSource_ThrowsODataException(string filter, string errorMessage) + { + // Arrange & Act & Assert + Assert.Throws(() => Bind(filter), errorMessage); + } + + public static TheoryDataSet IsOfUnquotedPrimitiveType + { + get + { + return new TheoryDataSet + { + { "isof(Edm.Binary)", "Edm.Binary" }, + { "isof(Edm.Boolean)", "Edm.Boolean" }, + { "isof(Edm.Byte)", "Edm.Byte" }, + { "isof(Edm.DateTime)", "Edm.DateTime" }, + { "isof(Edm.DateTimeOffset)", "Edm.DateTimeOffset" }, + { "isof(Edm.Decimal)", "Edm.Decimal" }, + { "isof(Edm.Double)", "Edm.Double" }, + { "isof(Edm.Guid)", "Edm.Guid" }, + { "isof(Edm.Int16)", "Edm.Int16" }, + { "isof(Edm.Int32)", "Edm.Int32" }, + { "isof(Edm.Int64)", "Edm.Int64" }, + { "isof(Edm.SByte)", "Edm.SByte" }, + { "isof(Edm.Single)", "Edm.Single" }, + { "isof(Edm.Stream)", "Edm.Stream" }, + { "isof(Edm.String)", "Edm.String" }, + { "isof(Edm.Time)", "Edm.Time" }, + { "isof(Edm.Unknown)", "Edm.Unknown" }, + + { "isof(null,Edm.Binary)", "Edm.Binary" }, + { "isof(null,Edm.Boolean)", "Edm.Boolean" }, + { "isof(null,Edm.Byte)", "Edm.Byte" }, + { "isof(null,Edm.DateTime)", "Edm.DateTime" }, + { "isof(null,Edm.DateTimeOffset)", "Edm.DateTimeOffset" }, + { "isof(null,Edm.Decimal)", "Edm.Decimal" }, + { "isof(null,Edm.Double)", "Edm.Double" }, + { "isof(null,Edm.Guid)", "Edm.Guid" }, + { "isof(null,Edm.Int16)", "Edm.Int16" }, + { "isof(null,Edm.Int32)", "Edm.Int32" }, + { "isof(null,Edm.Int64)", "Edm.Int64" }, + { "isof(null,Edm.SByte)", "Edm.SByte" }, + { "isof(null,Edm.Single)", "Edm.Single" }, + { "isof(null,Edm.Stream)", "Edm.Stream" }, + { "isof(null,Edm.String)", "Edm.String" }, + { "isof(null,Edm.Time)", "Edm.Time" }, + { "isof(null,Edm.Unknown)", "Edm.Unknown" }, + + { "isof(binary'4F64617461',Edm.Binary)", "Edm.Binary" }, + { "isof(false,Edm.Boolean)", "Edm.Boolean" }, + { "isof(23,Edm.Byte)", "Edm.Byte" }, + { "isof(datetime'2001-01-01T12:00:00.000',Edm.DateTime)", "Edm.DateTime" }, + { "isof(datetimeoffset'2001-01-01T12:00:00.000+08:00',Edm.DateTimeOffset)", "Edm.DateTimeOffset" }, + { "isof(23,Edm.Decimal)", "Edm.Decimal" }, + { "isof(23,Edm.Double)", "Edm.Double" }, + { "isof(guid'00000000-0000-0000-0000-000000000000',Edm.Guid)", "Edm.Guid" }, + { "isof(23,Edm.Int16)", "Edm.Int16" }, + { "isof(23,Edm.Int32)", "Edm.Int32" }, + { "isof(23,Edm.Int64)", "Edm.Int64" }, + { "isof(23,Edm.SByte)", "Edm.SByte" }, + { "isof(23,Edm.Single)", "Edm.Single" }, + { "isof('hello',Edm.Stream)", "Edm.Stream" }, + { "isof('hello',Edm.String)", "Edm.String" }, + { "isof(time'PT12H',Edm.Time)", "Edm.Time" }, + { "isof('',Edm.Unknown)", "Edm.Unknown" }, + + { "isof('OData',Edm.Binary)", "Edm.Binary" }, + { "isof('false',Edm.Boolean)", "Edm.Boolean" }, + { "isof('23',Edm.Byte)", "Edm.Byte" }, + { "isof('2001-01-01T12:00:00.000',Edm.DateTime)", "Edm.DateTime" }, + { "isof('2001-01-01T12:00:00.000+08:00',Edm.DateTimeOffset)", "Edm.DateTimeOffset" }, + { "isof('23',Edm.Decimal)", "Edm.Decimal" }, + { "isof('23',Edm.Double)", "Edm.Double" }, + { "isof('00000000-0000-0000-0000-000000000000',Edm.Guid)", "Edm.Guid" }, + { "isof('23',Edm.Int16)", "Edm.Int16" }, + { "isof('23',Edm.Int32)", "Edm.Int32" }, + { "isof('23',Edm.Int64)", "Edm.Int64" }, + { "isof('23',Edm.SByte)", "Edm.SByte" }, + { "isof('23',Edm.Single)", "Edm.Single" }, + { "isof(23,Edm.String)", "Edm.String" }, + { "isof('PT12H',Edm.Time)", "Edm.Time" }, + + { "isof(ByteArrayProp,Edm.Binary)", "Edm.Binary" }, + { "isof(DateTimeProp,Edm.DateTime)", "Edm.DateTime" }, + { "isof(DateTimeOffsetProp,Edm.DateTimeOffset)", "Edm.DateTimeOffset" }, + { "isof(DecimalProp,Edm.Decimal)", "Edm.Decimal" }, + { "isof(DoubleProp,Edm.Double)", "Edm.Double" }, + { "isof(GuidProp,Edm.Guid)", "Edm.Guid" }, + { "isof(NullableShortProp,Edm.Int16)", "Edm.Int16" }, + { "isof(IntProp,Edm.Int32)", "Edm.Int32" }, + { "isof(LongProp,Edm.Int64)", "Edm.Int64" }, + { "isof(FloatProp,Edm.Single)", "Edm.Single" }, + { "isof(StringProp,Edm.String)", "Edm.String" }, + { "isof(TimeSpanProp,Edm.Time)", "Edm.Time" }, + { "isof(IntProp,Edm.Unknown)", "Edm.Unknown" }, + }; + } + } + + [Theory] + [PropertyData("IsOfUnquotedPrimitiveType")] + public void IsOfUnquotedPrimitiveType_ThrowsODataException(string filter, string typeName) + { + // Arrange + var expectedMessage = string.Format( + "The child type '{0}' in a cast was not an entity type. Casts can only be performed on entity types.", + typeName); + + // Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + public static TheoryDataSet IsOfQuotedPrimitiveType + { + get + { + return new TheoryDataSet + { + { "isof('Edm.Binary')" }, + { "isof('Edm.Boolean')" }, + { "isof('Edm.Byte')" }, + { "isof('Edm.DateTime')" }, + { "isof('Edm.DateTimeOffset')" }, + { "isof('Edm.Decimal')" }, + { "isof('Edm.Double')" }, + { "isof('Edm.Guid')" }, + { "isof('Edm.Int16')" }, + { "isof('Edm.Int32')" }, + { "isof('Edm.Int64')" }, + { "isof('Edm.SByte')" }, + { "isof('Edm.Single')" }, + { "isof('Edm.String')" }, + { "isof('Edm.Time')" }, + + { "isof(null,'Edm.Binary')" }, + { "isof(null,'Edm.Boolean')" }, + { "isof(null,'Edm.Byte')" }, + { "isof(null,'Edm.DateTime')" }, + { "isof(null,'Edm.DateTimeOffset')" }, + { "isof(null,'Edm.Decimal')" }, + { "isof(null,'Edm.Double')" }, + { "isof(null,'Edm.Guid')" }, + { "isof(null,'Edm.Int16')" }, + { "isof(null,'Edm.Int32')" }, + { "isof(null,'Edm.Int64')" }, + { "isof(null,'Edm.SByte')" }, + { "isof(null,'Edm.Single')" }, + { "isof(null,'Edm.Stream')" }, + { "isof(null,'Edm.String')" }, + { "isof(null,'Edm.Time')" }, + + { "isof(binary'4F64617461','Edm.Binary')" }, + { "isof(false,'Edm.Boolean')" }, + { "isof(23,'Edm.Byte')" }, + { "isof(datetime'2001-01-01T12:00:00.000+08:00','Edm.DateTime')" }, + { "isof(datetimeoffset'2001-01-01T12:00:00.000+08:00','Edm.DateTimeOffset')" }, + { "isof(23,'Edm.Decimal')" }, + { "isof(23,'Edm.Double')" }, + { "isof(guid'00000000-0000-0000-0000-000000000000','Edm.Guid')" }, + { "isof(23,'Edm.Int16')" }, + { "isof(23,'Edm.Int32')" }, + { "isof(23,'Edm.Int64')" }, + { "isof(23,'Edm.SByte')" }, + { "isof(23,'Edm.Single')" }, + { "isof('hello','Edm.Stream')" }, + { "isof('hello','Edm.String')" }, + { "isof(time'PT12H','Edm.Time')" }, + + { "isof('OData','Edm.Binary')" }, + { "isof('false','Edm.Boolean')" }, + { "isof('23','Edm.Byte')" }, + { "isof('2001-01-01T12:00:00.000+08:00','Edm.DateTime')" }, + { "isof('2001-01-01T12:00:00.000+08:00','Edm.DateTimeOffset')" }, + { "isof('23','Edm.Decimal')" }, + { "isof('23','Edm.Double')" }, + { "isof('00000000-0000-0000-0000-000000000000','Edm.Guid')" }, + { "isof('23','Edm.Int16')" }, + { "isof('23','Edm.Int32')" }, + { "isof('23','Edm.Int64')" }, + { "isof('23','Edm.SByte')" }, + { "isof('23','Edm.Single')" }, + { "isof(23,'Edm.String')" }, + { "isof('PT12H','Edm.Time')" }, + + { "isof(ByteArrayProp,'Edm.Binary')" }, + { "isof(DateTimeProp,'Edm.DateTime')" }, + { "isof(DateTimeOffsetProp,'Edm.DateTimeOffset')" }, + { "isof(DecimalProp,'Edm.Decimal')" }, + { "isof(DoubleProp,'Edm.Double')" }, + { "isof(GuidProp,'Edm.Guid')" }, + { "isof(NullableShortProp,'Edm.Int16')" }, + { "isof(IntProp,'Edm.Int32')" }, + { "isof(LongProp,'Edm.Int64')" }, + { "isof(FloatProp,'Edm.Single')" }, + { "isof(StringProp,'Edm.String')" }, + { "isof(TimeSpanProp,'Edm.Time')" }, + }; + } + } + + [Theory] + [PropertyData("IsOfQuotedPrimitiveType")] + public void IsOfQuotedPrimitiveType_ThrowsNotImplemented(string filter) + { + // Arrange + var expectedMessage = "Unknown function 'isof'."; + + // Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + public static TheoryDataSet IsOfUnquotedComplexType + { + get + { + return new TheoryDataSet + { + { "isof(System.Web.Http.OData.Query.Expressions.Address)" }, + { "isof(null,System.Web.Http.OData.Query.Expressions.Address)" }, + { "isof(null, System.Web.Http.OData.Query.Expressions.Address)" }, + { "isof(SupplierAddress,System.Web.Http.OData.Query.Expressions.Address)" }, + { "isof(SupplierAddress, System.Web.Http.OData.Query.Expressions.Address)" }, + }; + } + } + + [Theory] + [PropertyData("IsOfUnquotedComplexType")] + public void IsOfUnquotedComplexType_ThrowsODataException(string filter) + { + // Arrange + var expectedMessage = + "The child type 'System.Web.Http.OData.Query.Expressions.Address' in a cast was not an entity type. " + + "Casts can only be performed on entity types."; + + // Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + public static TheoryDataSet IsOfUnquotedEntityType + { + get + { + return new TheoryDataSet + { + { + "isof(System.Web.Http.OData.Query.Expressions.DerivedProduct)", + "Cast or IsOf Function must have a type in its arguments." + }, + { + "isof(null,System.Web.Http.OData.Query.Expressions.DerivedCategory)", + "Encountered invalid type cast. " + + "'System.Web.Http.OData.Query.Expressions.DerivedCategory' is not assignable from 'System.Web.Http.OData.Query.Expressions.Product'." + }, + { + "isof(null, System.Web.Http.OData.Query.Expressions.DerivedCategory)", + "Encountered invalid type cast. " + + "'System.Web.Http.OData.Query.Expressions.DerivedCategory' is not assignable from 'System.Web.Http.OData.Query.Expressions.Product'." + }, + { + "isof(Category,System.Web.Http.OData.Query.Expressions.DerivedCategory)", + "Encountered invalid type cast. " + + "'System.Web.Http.OData.Query.Expressions.DerivedCategory' is not assignable from 'System.Web.Http.OData.Query.Expressions.Product'." + }, + { + "isof(Category, System.Web.Http.OData.Query.Expressions.DerivedCategory)", + "Encountered invalid type cast. " + + "'System.Web.Http.OData.Query.Expressions.DerivedCategory' is not assignable from 'System.Web.Http.OData.Query.Expressions.Product'." + }, + }; + } + } + + [Theory] + [PropertyData("IsOfUnquotedEntityType")] + public void IsOfUnquotedEntityType_ThrowsODataException(string filter, string expectedMessage) + { + // Arrange & Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + public static TheoryDataSet IsOfQuotedNonPrimitiveType + { + get + { + return new TheoryDataSet + { + { "isof('System.Web.Http.OData.Query.Expressions.Address')" }, + { "isof('System.Web.Http.OData.Query.Expressions.DerivedProduct')" }, + { "isof(null,'System.Web.Http.OData.Query.Expressions.Address')" }, + { "isof(null, 'System.Web.Http.OData.Query.Expressions.Address')" }, + { "isof(null,'System.Web.Http.OData.Query.Expressions.DerivedCategory')" }, + { "isof(null, 'System.Web.Http.OData.Query.Expressions.DerivedCategory')" }, + { "isof(SupplierAddress,'System.Web.Http.OData.Query.Expressions.Address')" }, + { "isof(SupplierAddress, 'System.Web.Http.OData.Query.Expressions.Address')" }, + { "isof(Category,'System.Web.Http.OData.Query.Expressions.DerivedCategory')" }, + { "isof(Category, 'System.Web.Http.OData.Query.Expressions.DerivedCategory')" }, + }; + } + } + + [Theory] + [PropertyData("IsOfQuotedNonPrimitiveType")] + public void IsOfQuotedNonPrimitiveType_ThrowsNotImplemented(string filter) + { + // Arrange + var expectedMessage = "Unknown function 'isof'."; + + // Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + #endregion + [Theory] [InlineData("UShortProp eq 12", "$it => (Convert($it.UShortProp) == 12)")] [InlineData("ULongProp eq 12L", "$it => (Convert($it.ULongProp) == 12)")] @@ -1542,7 +2185,7 @@ namespace System.Web.Http.OData.Query.Expressions } else { - Assert.Equal(RunFilter(filterWithNullPropagation, product), expectedValue.WithNullPropagation); + Assert.Equal(expectedValue.WithNullPropagation, RunFilter(filterWithNullPropagation, product)); } var filterWithoutNullPropagation = filters.WithoutNullPropagation as Expression>; @@ -1552,7 +2195,7 @@ namespace System.Web.Http.OData.Query.Expressions } else { - Assert.Equal(RunFilter(filterWithoutNullPropagation, product), expectedValue.WithoutNullPropagation); + Assert.Equal(expectedValue.WithoutNullPropagation, RunFilter(filterWithoutNullPropagation, product)); } } @@ -1631,6 +2274,12 @@ namespace System.Web.Http.OData.Query.Expressions { ODataModelBuilder model = new ODataConventionModelBuilder(); model.EntitySet("Products"); + if (key == typeof(Product)) + { + model.Entity().DerivesFrom(); + model.Entity().DerivesFrom(); + } + value = _modelCache[key] = model.GetEdmModel(); } return value; diff --git a/OData/test/System.Web.Http.OData.Test/OData/Query/Validators/FilterQueryValidatorTest.cs b/OData/test/System.Web.Http.OData.Test/OData/Query/Validators/FilterQueryValidatorTest.cs index 2ed599d1..662eb49d 100644 --- a/OData/test/System.Web.Http.OData.Test/OData/Query/Validators/FilterQueryValidatorTest.cs +++ b/OData/test/System.Web.Http.OData.Test/OData/Query/Validators/FilterQueryValidatorTest.cs @@ -48,11 +48,384 @@ namespace System.Web.Http.OData.Query.Validators } } + public static TheoryDataSet ArithmeticOperators + { + get + { + return new TheoryDataSet + { + { AllowedArithmeticOperators.Add, "UnitPrice add 0 eq 23", "Add" }, + { AllowedArithmeticOperators.Divide, "UnitPrice div 23 eq 1", "Divide" }, + { AllowedArithmeticOperators.Modulo, "UnitPrice mod 23 eq 0", "Modulo" }, + { AllowedArithmeticOperators.Multiply, "UnitPrice mul 1 eq 23", "Multiply" }, + { AllowedArithmeticOperators.Subtract, "UnitPrice sub 0 eq 23", "Subtract" }, + }; + } + } + + public static TheoryDataSet ArithmeticOperators_CheckArguments + { + get + { + return new TheoryDataSet + { + { "day(DiscontinuedDate) add 0 eq 23" }, + { "day(DiscontinuedDate) div 23 eq 1" }, + { "day(DiscontinuedDate) mod 23 eq 0" }, + { "day(DiscontinuedDate) mul 1 eq 23" }, + { "day(DiscontinuedDate) sub 0 eq 23" }, + { "0 add day(DiscontinuedDate) eq 23" }, + { "23 div day(DiscontinuedDate) eq 1" }, + { "23 mod day(DiscontinuedDate) eq 0" }, + { "1 mul day(DiscontinuedDate) eq 23" }, + { "0 sub day(DiscontinuedDate) eq -23" }, + }; + } + } + + public static TheoryDataSet DateTimeFunctions + { + get + { + return new TheoryDataSet + { + { AllowedFunctions.Day, "day(null) eq 20", "day" }, + { AllowedFunctions.Day, "day(DiscontinuedDate) eq 20", "day" }, + { AllowedFunctions.Hour, "hour(null) eq 10", "hour" }, + { AllowedFunctions.Hour, "hour(DiscontinuedDate) eq 10", "hour" }, + { AllowedFunctions.Minute, "minute(null) eq 20", "minute" }, + { AllowedFunctions.Minute, "minute(DiscontinuedDate) eq 20", "minute" }, + { AllowedFunctions.Month, "month(null) eq 10", "month" }, + { AllowedFunctions.Month, "month(DiscontinuedDate) eq 10", "month" }, + { AllowedFunctions.Second, "second(null) eq 20", "second" }, + { AllowedFunctions.Second, "second(DiscontinuedDate) eq 20", "second" }, + { AllowedFunctions.Year, "year(null) eq 2000", "year" }, + { AllowedFunctions.Year, "year(DiscontinuedDate) eq 2000", "year" }, + }; + } + } + + // Some code remains supporting these TimeSpan functions e.g. in ClrCanonicalFunctions. + public static TheoryDataSet DateTimeFunctions_Unsupported + { + get + { + return new TheoryDataSet + { + { AllowedFunctions.Days, "days(DiscontinuedSince) eq 6", "days" }, + { AllowedFunctions.Hours, "hours(DiscontinuedSince) eq 6", "hours" }, + { AllowedFunctions.Minutes, "minutes(DiscontinuedSince) eq 6", "minutes" }, + { AllowedFunctions.Months, "months(DiscontinuedSince) eq 6", "months" }, + { AllowedFunctions.Seconds, "seconds(DiscontinuedSince) eq 6", "seconds" }, + { AllowedFunctions.Years, "years(DiscontinuedSince) eq 6", "years" }, + }; + } + } + + public static TheoryDataSet MathFunctions + { + get + { + return new TheoryDataSet + { + { AllowedFunctions.Ceiling, "ceiling(null) eq 0", "ceiling" }, + { AllowedFunctions.Ceiling, "ceiling(Weight) eq 0", "ceiling" }, + { AllowedFunctions.Floor, "floor(null) eq 0", "floor" }, + { AllowedFunctions.Floor, "floor(Weight) eq 0", "floor" }, + { AllowedFunctions.Round, "round(null) eq 0", "round" }, + { AllowedFunctions.Round, "round(Weight) eq 0", "round" }, + }; + } + } + + public static TheoryDataSet OtherFunctions + { + get + { + return new TheoryDataSet + { + { AllowedFunctions.All, "AlternateIDs/all(t : null eq 1)", "all" }, + { AllowedFunctions.All, "AlternateIDs/all(t : t eq 1)", "all" }, + { AllowedFunctions.All, "AlternateAddresses/all(t : null eq 'Redmond')", "all" }, + { AllowedFunctions.All, "AlternateAddresses/all(t : t/City eq 'Redmond')", "all" }, + { AllowedFunctions.All, "Category/QueryableProducts/all(t : null eq 'Name')", "all" }, + { AllowedFunctions.All, "Category/QueryableProducts/all(t : t/ProductName eq 'Name')", "all" }, + { AllowedFunctions.All, "Category/EnumerableProducts/all(t : null eq 'Name')", "all" }, + { AllowedFunctions.All, "Category/EnumerableProducts/all(t : t/ProductName eq 'Name')", "all" }, + + { AllowedFunctions.Any, "AlternateIDs/any()", "any" }, + { AllowedFunctions.Any, "AlternateIDs/any(t : null eq 1)", "any" }, + { AllowedFunctions.Any, "AlternateIDs/any(t : t eq 1)", "any" }, + { AllowedFunctions.Any, "AlternateAddresses/any()", "any" }, + { AllowedFunctions.Any, "AlternateAddresses/any(t : null eq 'Redmond')", "any" }, + { AllowedFunctions.Any, "AlternateAddresses/any(t : t/City eq 'Redmond')", "any" }, + { AllowedFunctions.Any, "Category/QueryableProducts/any()", "any" }, + { AllowedFunctions.Any, "Category/QueryableProducts/any(t : null eq 'Name')", "any" }, + { AllowedFunctions.Any, "Category/QueryableProducts/any(t : t/ProductName eq 'Name')", "any" }, + { AllowedFunctions.Any, "Category/EnumerableProducts/any()", "any" }, + { AllowedFunctions.Any, "Category/EnumerableProducts/any(t : null eq 'Name')", "any" }, + { AllowedFunctions.Any, "Category/EnumerableProducts/any(t : t/ProductName eq 'Name')", "any" }, + + { AllowedFunctions.Cast, "cast('Edm.Int64') eq 0", "cast" }, + { AllowedFunctions.Cast, "cast('Edm.String') eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast('System.Web.Http.OData.Query.Expressions.Address')/City eq 'Redmond'", "cast" }, + { AllowedFunctions.Cast, "cast(null,'Edm.Int64') eq 0", "cast" }, + { AllowedFunctions.Cast, "cast(null, 'Edm.Int64') eq 0", "cast" }, + { AllowedFunctions.Cast, "cast(null,'Edm.String') eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(null, 'Edm.String') eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(null,'System.Web.Http.OData.Query.Expressions.Address')/City eq 'Redmond'", "cast" }, + { AllowedFunctions.Cast, "cast(null, 'System.Web.Http.OData.Query.Expressions.Address')/City eq 'Redmond'", "cast" }, + { AllowedFunctions.Cast, "cast(null,'System.Web.Http.OData.Query.Expressions.DerivedCategory')/DerivedCategoryName eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(null, 'System.Web.Http.OData.Query.Expressions.DerivedCategory')/DerivedCategoryName eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(CategoryID,'Edm.Int64') eq 0", "cast" }, + { AllowedFunctions.Cast, "cast(CategoryID, 'Edm.Int64') eq 0", "cast" }, + { AllowedFunctions.Cast, "cast(ReorderLevel,'Edm.String') eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(ReorderLevel, 'Edm.String') eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(SupplierAddress,'System.Web.Http.OData.Query.Expressions.Address')/City eq 'Redmond'", "cast" }, + { AllowedFunctions.Cast, "cast(SupplierAddress, 'System.Web.Http.OData.Query.Expressions.Address')/City eq 'Redmond'", "cast" }, + + { AllowedFunctions.IsOf, "isof('Edm.Int64')", "isof" }, + { AllowedFunctions.IsOf, "isof('Edm.String')", "isof" }, + { AllowedFunctions.IsOf, "isof('System.Web.Http.OData.Query.Expressions.Address')", "isof" }, + { AllowedFunctions.IsOf, "isof('System.Web.Http.OData.Query.Expressions.DerivedProduct')", "isof" }, + { AllowedFunctions.IsOf, "isof(null,'Edm.Int64')", "isof" }, + { AllowedFunctions.IsOf, "isof(null, 'Edm.Int64')", "isof" }, + { AllowedFunctions.IsOf, "isof(null,'Edm.String')", "isof" }, + { AllowedFunctions.IsOf, "isof(null, 'Edm.String')", "isof" }, + { AllowedFunctions.IsOf, "isof(null,'System.Web.Http.OData.Query.Expressions.Address')", "isof" }, + { AllowedFunctions.IsOf, "isof(null, 'System.Web.Http.OData.Query.Expressions.Address')", "isof" }, + { AllowedFunctions.IsOf, "isof(null,'System.Web.Http.OData.Query.Expressions.DerivedCategory')", "isof" }, + { AllowedFunctions.IsOf, "isof(null, 'System.Web.Http.OData.Query.Expressions.DerivedCategory')", "isof" }, + { AllowedFunctions.IsOf, "isof(CategoryID,'Edm.Int64')", "isof" }, + { AllowedFunctions.IsOf, "isof(CategoryID, 'Edm.Int64')", "isof" }, + { AllowedFunctions.IsOf, "isof(ReorderLevel,'Edm.String')", "isof" }, + { AllowedFunctions.IsOf, "isof(ReorderLevel, 'Edm.String')", "isof" }, + { AllowedFunctions.IsOf, "isof(SupplierAddress,'System.Web.Http.OData.Query.Expressions.Address')", "isof" }, + { AllowedFunctions.IsOf, "isof(SupplierAddress, 'System.Web.Http.OData.Query.Expressions.Address')", "isof" }, + { AllowedFunctions.IsOf, "isof(Category,'System.Web.Http.OData.Query.Expressions.DerivedCategory')", "isof" }, + { AllowedFunctions.IsOf, "isof(Category, 'System.Web.Http.OData.Query.Expressions.DerivedCategory')", "isof" }, + }; + } + } + + public static TheoryDataSet OtherFunctions_SomeSingleParameterCasts + { + get + { + return new TheoryDataSet + { + // Single-parameter casts without quotes around the type name. + { AllowedFunctions.Cast, "cast(System.Web.Http.OData.Query.Expressions.DerivedProduct)/DerivedProductName eq 'Name'", "cast" }, + { AllowedFunctions.IsOf, "isof(System.Web.Http.OData.Query.Expressions.DerivedProduct)", "isof" }, + }; + } + } + + public static TheoryDataSet OtherFunctions_SomeTwoParameterCasts + { + get + { + return new TheoryDataSet + { + // Two-parameter casts without quotes around the type name. + { AllowedFunctions.Cast, "cast(null,System.Web.Http.OData.Query.Expressions.DerivedCategory)/DerivedCategoryName eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(null, System.Web.Http.OData.Query.Expressions.DerivedCategory)/DerivedCategoryName eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(Category,System.Web.Http.OData.Query.Expressions.DerivedCategory)/DerivedCategoryName eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(Category, System.Web.Http.OData.Query.Expressions.DerivedCategory)/DerivedCategoryName eq 'Name'", "cast" }, + + { AllowedFunctions.IsOf, "isof(null,System.Web.Http.OData.Query.Expressions.DerivedCategory)", "isof" }, + { AllowedFunctions.IsOf, "isof(null, System.Web.Http.OData.Query.Expressions.DerivedCategory)", "isof" }, + { AllowedFunctions.IsOf, "isof(Category,System.Web.Http.OData.Query.Expressions.DerivedCategory)", "isof" }, + { AllowedFunctions.IsOf, "isof(Category, System.Web.Http.OData.Query.Expressions.DerivedCategory)", "isof" }, + }; + } + } + public static TheoryDataSet OtherFunctions_UnsupportedTargetType + { + get + { + return new TheoryDataSet + { + { "cast(Edm.Int64) eq 0", "Edm.Int64" }, + { "cast(System.Web.Http.OData.Query.Expressions.Address)/City eq 'Redmond'", typeof(Address).FullName }, + { "cast(null,Edm.Int64) eq 0", "Edm.Int64" }, + { "cast(null, Edm.Int64) eq 0", "Edm.Int64" }, + { "cast(null,System.Web.Http.OData.Query.Expressions.Address)/City eq 'Redmond'", typeof(Address).FullName }, + { "cast(null, System.Web.Http.OData.Query.Expressions.Address)/City eq 'Redmond'", typeof(Address).FullName }, + { "cast(CategoryID,Edm.Int64) eq 0", "Edm.Int64" }, + { "cast(CategoryID, Edm.Int64) eq 0", "Edm.Int64" }, + { "cast(SupplierAddress,System.Web.Http.OData.Query.Expressions.Address)/City eq 'Redmond'", typeof(Address).FullName }, + { "cast(SupplierAddress, System.Web.Http.OData.Query.Expressions.Address)/City eq 'Redmond'", typeof(Address).FullName }, + + { "isof(Edm.Int64)", "Edm.Int64" }, + { "isof(System.Web.Http.OData.Query.Expressions.Address)", typeof(Address).FullName }, + { "isof(null,Edm.Int64)", "Edm.Int64" }, + { "isof(null, Edm.Int64)", "Edm.Int64" }, + { "isof(null,System.Web.Http.OData.Query.Expressions.Address)", typeof(Address).FullName }, + { "isof(null, System.Web.Http.OData.Query.Expressions.Address)", typeof(Address).FullName }, + { "isof(CategoryID,Edm.Int64)", "Edm.Int64" }, + { "isof(CategoryID, Edm.Int64)", "Edm.Int64" }, + { "isof(SupplierAddress,System.Web.Http.OData.Query.Expressions.Address)", typeof(Address).FullName }, + { "isof(SupplierAddress, System.Web.Http.OData.Query.Expressions.Address)", typeof(Address).FullName }, + }; + } + } + + public static TheoryDataSet OtherFunctions_Unsupported + { + get + { + return new TheoryDataSet + { + { AllowedFunctions.Cast, "cast('System.Web.Http.OData.Query.Expressions.DerivedProduct')/DerivedProductName eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(Category,'System.Web.Http.OData.Query.Expressions.DerivedCategory')/DerivedCategoryName eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(Category, 'System.Web.Http.OData.Query.Expressions.DerivedCategory')/DerivedCategoryName eq 'Name'", "cast" }, + }; + } + } + + public static TheoryDataSet StringFunctions + { + get + { + return new TheoryDataSet + { + { AllowedFunctions.Concat, "concat(null,'Name') eq 'Name'", "concat" }, + { AllowedFunctions.Concat, "concat(null, 'Name') eq 'Name'", "concat" }, + { AllowedFunctions.Concat, "concat(ProductName,'Name') eq 'Name'", "concat" }, + { AllowedFunctions.Concat, "concat(ProductName, 'Name') eq 'Name'", "concat" }, + { AllowedFunctions.EndsWith, "endswith(null,'Name')", "endswith" }, + { AllowedFunctions.EndsWith, "endswith(null, 'Name')", "endswith" }, + { AllowedFunctions.EndsWith, "endswith(ProductName,'Name')", "endswith" }, + { AllowedFunctions.EndsWith, "endswith(ProductName, 'Name')", "endswith" }, + { AllowedFunctions.IndexOf, "indexof(null,'Name') eq 1", "indexof" }, + { AllowedFunctions.IndexOf, "indexof(null, 'Name') eq 1", "indexof" }, + { AllowedFunctions.IndexOf, "indexof(ProductName,'Name') eq 1", "indexof" }, + { AllowedFunctions.IndexOf, "indexof(ProductName, 'Name') eq 1", "indexof" }, + { AllowedFunctions.Length, "length(null) eq 6", "length" }, + { AllowedFunctions.Length, "length(ProductName) eq 6", "length" }, + { AllowedFunctions.StartsWith, "startswith(null,'Name')", "startswith" }, + { AllowedFunctions.StartsWith, "startswith(null, 'Name')", "startswith" }, + { AllowedFunctions.StartsWith, "startswith(ProductName,'Name')", "startswith" }, + { AllowedFunctions.StartsWith, "startswith(ProductName, 'Name')", "startswith" }, + { AllowedFunctions.Substring, "substring(null,1) eq 'Name'", "substring" }, + { AllowedFunctions.Substring, "substring(null, 1) eq 'Name'", "substring" }, + { AllowedFunctions.Substring, "substring(ProductName,1) eq 'Name'", "substring" }, + { AllowedFunctions.Substring, "substring(ProductName, 1) eq 'Name'", "substring" }, + { AllowedFunctions.Substring, "substring(null,1,2) eq 'Name'", "substring" }, + { AllowedFunctions.Substring, "substring(null, 1, 2) eq 'Name'", "substring" }, + { AllowedFunctions.Substring, "substring(ProductName,1,2) eq 'Name'", "substring" }, + { AllowedFunctions.Substring, "substring(ProductName, 1, 2) eq 'Name'", "substring" }, + { AllowedFunctions.SubstringOf, "substringof(null,'Name')", "substringof" }, + { AllowedFunctions.SubstringOf, "substringof(null, 'Name')", "substringof" }, + { AllowedFunctions.SubstringOf, "substringof(ProductName,'Name')", "substringof" }, + { AllowedFunctions.SubstringOf, "substringof(ProductName, 'Name')", "substringof" }, + { AllowedFunctions.ToLower, "tolower(null) eq 'Name'", "tolower" }, + { AllowedFunctions.ToLower, "tolower(ProductName) eq 'Name'", "tolower" }, + { AllowedFunctions.ToUpper, "toupper(null) eq 'Name'", "toupper" }, + { AllowedFunctions.ToUpper, "toupper(ProductName) eq 'Name'", "toupper" }, + { AllowedFunctions.Trim, "trim(null) eq 'Name'", "trim" }, + { AllowedFunctions.Trim, "trim(ProductName) eq 'Name'", "trim" }, + }; + } + } + + public static TheoryDataSet Functions_CheckArguments + { + get + { + return new TheoryDataSet + { + // Not testing that DateTime functions validate their argument. + // No supported expressions except property value return a DateTime value. + + { AllowedFunctions.Ceiling, AllowedFunctions.IndexOf, "ceiling(indexof(ProductName, 'Name')) eq 0", "indexof" }, + { AllowedFunctions.Floor, AllowedFunctions.IndexOf, "floor(indexof(ProductName, 'Name')) eq 0", "indexof" }, + { AllowedFunctions.Round, AllowedFunctions.IndexOf, "round(indexof(ProductName, 'Name')) eq 0", "indexof" }, + + // Not testing that all() / any() validate their source. + // No supported expressions except property value return a collection. + { AllowedFunctions.All, AllowedFunctions.IndexOf, "AlternateAddresses/all(t : indexof(t/City, 'Name') eq 3)", "indexof" }, + { AllowedFunctions.Any, AllowedFunctions.IndexOf, "AlternateAddresses/any(t : indexof(t/City, 'Name') eq 3)", "indexof" }, + + { AllowedFunctions.Cast, AllowedFunctions.IndexOf, "cast(indexof(ProductName, 'Name'), 'Edm.Int64') eq 0", "indexof" }, + { AllowedFunctions.IsOf, AllowedFunctions.IndexOf, "isof(indexof(ProductName, 'Name'), 'Edm.Int64')", "indexof" }, + + { AllowedFunctions.Concat, AllowedFunctions.Substring, "concat(substring(ProductName, 1), 'Name') eq 'Name'", "substring" }, + { AllowedFunctions.Concat, AllowedFunctions.Substring, "concat(ProductName, substring(ProductName, 1)) eq 'Name'", "substring" }, + { AllowedFunctions.EndsWith, AllowedFunctions.Substring, "endswith(substring(ProductName, 1), 'Name')", "substring" }, + { AllowedFunctions.EndsWith, AllowedFunctions.Substring, "endswith(ProductName, substring(ProductName, 1))", "substring" }, + { AllowedFunctions.IndexOf, AllowedFunctions.Substring, "indexof(substring(ProductName, 1), 'Name') eq 1", "substring" }, + { AllowedFunctions.IndexOf, AllowedFunctions.Substring, "indexof(ProductName, substring(ProductName, 1)) eq 1", "substring" }, + { AllowedFunctions.Length, AllowedFunctions.Substring, "length(substring(ProductName, 1)) eq 6", "substring" }, + { AllowedFunctions.StartsWith, AllowedFunctions.Substring, "startswith(substring(ProductName, 1), 'Name')", "substring" }, + { AllowedFunctions.StartsWith, AllowedFunctions.Substring, "startswith(ProductName, substring(ProductName, 1))", "substring" }, + { AllowedFunctions.Substring, AllowedFunctions.Concat, "substring(concat(ProductName, 'Name'), 1) eq 'Name'", "concat" }, + { AllowedFunctions.Substring, AllowedFunctions.IndexOf, "substring(ProductName, indexof(ProductName, 'Name')) eq 'Name'", "indexof" }, + { AllowedFunctions.Substring, AllowedFunctions.Concat, "substring(concat(ProductName, 'Name'), 1, 2) eq 'Name'", "concat" }, + { AllowedFunctions.Substring, AllowedFunctions.IndexOf, "substring(ProductName, indexof(ProductName, 'Name'), 2) eq 'Name'", "indexof" }, + { AllowedFunctions.Substring, AllowedFunctions.IndexOf, "substring(ProductName, 1, indexof(ProductName, 'Name')) eq 'Name'", "indexof" }, + { AllowedFunctions.SubstringOf, AllowedFunctions.Substring, "substringof(substring(ProductName, 1), 'Name')", "substring" }, + { AllowedFunctions.SubstringOf, AllowedFunctions.Substring, "substringof(ProductName, substring(ProductName, 1))", "substring" }, + { AllowedFunctions.ToLower, AllowedFunctions.Substring, "tolower(substring(ProductName, 1)) eq 'Name'", "substring" }, + { AllowedFunctions.ToUpper, AllowedFunctions.Substring, "toupper(substring(ProductName, 1)) eq 'Name'", "substring" }, + { AllowedFunctions.Trim, AllowedFunctions.Substring, "trim(substring(ProductName, 1)) eq 'Name'", "substring" }, + }; + } + } + + public static TheoryDataSet LogicalOperators + { + get + { + return new TheoryDataSet + { + { AllowedLogicalOperators.And, "Discontinued and AlternateIDs/any()", "And" }, + { AllowedLogicalOperators.Equal, "UnitPrice add 0 eq UnitPrice", "Equal" }, + { AllowedLogicalOperators.GreaterThan, "UnitPrice add 1 gt UnitPrice", "GreaterThan" }, + { AllowedLogicalOperators.GreaterThanOrEqual, "UnitPrice add 0 ge UnitPrice", "GreaterThanOrEqual" }, + { AllowedLogicalOperators.LessThan, "UnitPrice add -1 lt UnitPrice", "LessThan" }, + { AllowedLogicalOperators.LessThanOrEqual, "UnitPrice add 0 le UnitPrice", "LessThanOrEqual" }, + { AllowedLogicalOperators.Not, "not Discontinued", "Not" }, + { AllowedLogicalOperators.NotEqual, "UnitPrice add 1 ne UnitPrice", "NotEqual" }, + { AllowedLogicalOperators.Or, "Discontinued or AlternateIDs/any()", "Or" }, + }; + } + } + + public static TheoryDataSet LogicalOperators_CheckArguments + { + get + { + return new TheoryDataSet + { + { "(UnitPrice add 0 eq UnitPrice) and AlternateIDs/any()" }, + { "UnitPrice add 0 eq UnitPrice" }, + { "UnitPrice add 1 gt UnitPrice" }, + { "UnitPrice add 0 ge UnitPrice" }, + { "UnitPrice add -1 lt UnitPrice" }, + { "UnitPrice add 0 le UnitPrice" }, + { "not (UnitPrice add 0 eq UnitPrice)" }, + { "UnitPrice add 1 ne UnitPrice" }, + { "(UnitPrice add 0 eq UnitPrice) or AlternateIDs/any()" }, + + { "Discontinued and (UnitPrice add 0 eq UnitPrice)" }, + { "UnitPrice eq UnitPrice add 0" }, + { "UnitPrice gt UnitPrice add -1" }, + { "UnitPrice ge UnitPrice add 0" }, + { "UnitPrice lt UnitPrice add 1" }, + { "UnitPrice le UnitPrice add 0" }, + { "UnitPrice ne UnitPrice add 1" }, + { "Discontinued or (UnitPrice add 0 eq UnitPrice)" }, + }; + } + } + public FilterQueryValidatorTest() { _validator = new MyFilterValidator(); _context = ValidationTestHelper.CreateCustomerContext(); - _productContext = ValidationTestHelper.CreateProductContext(); + _productContext = ValidationTestHelper.CreateDerivedProductsContext(); } [Fact] @@ -69,45 +442,6 @@ namespace System.Web.Http.OData.Query.Validators _validator.Validate(new FilterQueryOption("Name eq 'abc'", _context), null)); } - [Fact] - public void ValidateThrowsIfSubStringIsNotAllowed() - { - Assert.DoesNotThrow(() => - _validator.Validate(new FilterQueryOption("substring(Name,8,1) eq '7'", _context), - new ODataValidationSettings() { AllowedFunctions = AllowedFunctions.Substring })); - - Assert.Throws(() => - _validator.Validate(new FilterQueryOption("substring(Name,8,1) eq '7'", _context), - new ODataValidationSettings() { AllowedFunctions = AllowedFunctions.AllMathFunctions }), - "Function 'substring' is not allowed. To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings."); - } - - [Fact] - public void ValidateThrowsIfNotIsNotAllowed() - { - Assert.DoesNotThrow(() => - _validator.Validate(new FilterQueryOption("not (Name eq 'David')", _context), - new ODataValidationSettings() { AllowedLogicalOperators = AllowedLogicalOperators.Not | AllowedLogicalOperators.Equal })); - - Assert.Throws(() => - _validator.Validate(new FilterQueryOption("not (Name eq 'David')", _context), - new ODataValidationSettings() { AllowedLogicalOperators = AllowedLogicalOperators.Equal }), - "Logical operator 'Not' is not allowed. To allow it, set the 'AllowedLogicalOperators' property on EnableQueryAttribute or QueryValidationSettings."); - } - - [Fact] - public void ValidateThrowsIfModIsNotAllowed() - { - Assert.DoesNotThrow(() => - _validator.Validate(new FilterQueryOption("Id mod 2 eq 0", _context), - new ODataValidationSettings() { AllowedArithmeticOperators = AllowedArithmeticOperators.All })); - - Assert.Throws(() => - _validator.Validate(new FilterQueryOption("Id mod 2 eq 0", _context), - new ODataValidationSettings() { AllowedArithmeticOperators = AllowedArithmeticOperators.Add }), - "Arithmetic operator 'Modulo' is not allowed. To allow it, set the 'AllowedArithmeticOperators' property on EnableQueryAttribute or QueryValidationSettings."); - } - // want to test if all the virtual methods are being invoked correctly [Fact] public void ValidateVisitAll() @@ -224,55 +558,597 @@ namespace System.Web.Http.OData.Query.Validators } [Fact] - public void AllowedArithmeticOperators_ThrowsOnNotAllowedOperators() + public void ArithmeticOperatorsDataSet_CoversAllValues() { // Arrange - ODataValidationSettings settings = new ODataValidationSettings + // Get all values in the AllowedArithmeticOperators enum. + var values = new HashSet( + Enum.GetValues(typeof(AllowedArithmeticOperators)).Cast()); + var groupValues = new[] { - AllowedArithmeticOperators = AllowedArithmeticOperators.All & ~AllowedArithmeticOperators.Modulo + AllowedArithmeticOperators.All, + AllowedArithmeticOperators.None, }; - FilterQueryOption option = new FilterQueryOption("ProductID mod 2 eq 0", _productContext); + // Act + // Remove the group items. + foreach (var allowed in groupValues) + { + values.Remove(allowed); + } + + // Remove the individual items. + foreach (var allowed in ArithmeticOperators.Select(item => (AllowedArithmeticOperators)(item[0]))) + { + values.Remove(allowed); + } + + // Assert + // Should have nothing left. + Assert.Empty(values); + } + + [Theory] + [PropertyData("ArithmeticOperators")] + public void AllowedArithmeticOperators_SucceedIfAllowed(AllowedArithmeticOperators allow, string query, string unused) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedArithmeticOperators = allow, + }; + var option = new FilterQueryOption(query, _productContext); // Act & Assert - Assert.Throws( - () => _validator.Validate(option, settings), - "Arithmetic operator 'Modulo' is not allowed. To allow it, set the 'AllowedArithmeticOperators' property on EnableQueryAttribute or QueryValidationSettings."); + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [PropertyData("ArithmeticOperators")] + public void AllowedArithmeticOperators_ThrowIfNotAllowed(AllowedArithmeticOperators exclude, string query, string operatorName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedArithmeticOperators = AllowedArithmeticOperators.All & ~exclude, + }; + var expectedMessage = string.Format( + "Arithmetic operator '{0}' is not allowed. " + + "To allow it, set the 'AllowedArithmeticOperators' property on EnableQueryAttribute or QueryValidationSettings.", + operatorName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("ArithmeticOperators")] + public void AllowedArithmeticOperators_ThrowIfNoneAllowed(AllowedArithmeticOperators unused, string query, string operatorName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedArithmeticOperators = AllowedArithmeticOperators.None, + }; + var expectedMessage = string.Format( + "Arithmetic operator '{0}' is not allowed. " + + "To allow it, set the 'AllowedArithmeticOperators' property on EnableQueryAttribute or QueryValidationSettings.", + operatorName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("ArithmeticOperators_CheckArguments")] + public void ArithmeticOperators_CheckArguments_SucceedIfAllowed(string query) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.Day, + }; + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [PropertyData("ArithmeticOperators_CheckArguments")] + public void ArithmeticOperators_CheckArguments_ThrowIfNotAllowed(string query) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllFunctions & ~AllowedFunctions.Day, + }; + var expectedMessage = string.Format( + "Function 'day' is not allowed. " + + "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings."); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); } [Fact] - public void AllowedFunctions_ThrowsOnNotAllowedFunctions() + public void AllowedFunctionDataSets_CoverAllValues() { // Arrange - ODataValidationSettings settings = new ODataValidationSettings + // Get all values in the AllowedFunctions enum. + var values = new HashSet(Enum.GetValues(typeof(AllowedFunctions)).Cast()); + + var groupValues = new[] { - AllowedFunctions = AllowedFunctions.All & ~AllowedFunctions.Length + AllowedFunctions.None, + AllowedFunctions.AllFunctions, + AllowedFunctions.AllDateTimeFunctions, + AllowedFunctions.AllMathFunctions, + AllowedFunctions.AllStringFunctions }; - FilterQueryOption option = new FilterQueryOption("length(ProductName) eq 6", _productContext); + // No need to include OtherFunctions_* here since they cover enum values also in OtherFunctions. + var dataSets = DateTimeFunctions + .Concat(DateTimeFunctions_Unsupported) + .Concat(MathFunctions) + .Concat(OtherFunctions) + .Concat(StringFunctions); + + // Act + // Remove the group items. + foreach (var allowed in groupValues) + { + values.Remove(allowed); + } + + // Remove the individual items. + foreach (var allowed in dataSets.Select(item => (AllowedFunctions)(item[0]))) + { + values.Remove(allowed); + } + + // Assert + // Should have nothing left. + Assert.Empty(values); + } + + [Theory] + [PropertyData("DateTimeFunctions")] + [PropertyData("MathFunctions")] + [PropertyData("OtherFunctions")] + [PropertyData("StringFunctions")] + public void AllowedFunctions_SucceedIfAllowed(AllowedFunctions allow, string query, string unused) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = allow, + }; + var option = new FilterQueryOption(query, _productContext); // Act & Assert - Assert.Throws( - () => _validator.Validate(option, settings), - "Function 'length' is not allowed. To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings."); + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + [Theory] + [PropertyData("DateTimeFunctions")] + [PropertyData("MathFunctions")] + [PropertyData("OtherFunctions")] + [PropertyData("StringFunctions")] + public void AllowedFunctions_ThrowIfNotAllowed(AllowedFunctions exclude, string query, string functionName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllFunctions & ~exclude, + }; + var expectedMessage = string.Format( + "Function '{0}' is not allowed. " + + "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", + functionName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("DateTimeFunctions")] + [PropertyData("MathFunctions")] + [PropertyData("OtherFunctions")] + [PropertyData("StringFunctions")] + public void AllowedFunctions_ThrowIfNoneAllowed(AllowedFunctions unused, string query, string functionName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.None, + }; + var expectedMessage = string.Format( + "Function '{0}' is not allowed. " + + "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", + functionName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("DateTimeFunctions")] + public void DateTimeFunctions_SucceedIfGroupAllowed(AllowedFunctions unused, string query, string unusedName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllDateTimeFunctions, + }; + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [PropertyData("DateTimeFunctions")] + public void DateTimeFunctions_ThrowIfGroupNotAllowed(AllowedFunctions unused, string query, string functionName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllFunctions & ~AllowedFunctions.AllDateTimeFunctions, + }; + var expectedMessage = string.Format( + "Function '{0}' is not allowed. " + + "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", + functionName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("MathFunctions")] + public void MathFunctions_SucceedIfGroupAllowed(AllowedFunctions unused, string query, string unusedName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllMathFunctions, + }; + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [PropertyData("MathFunctions")] + public void MathFunctions_ThrowIfGroupNotAllowed(AllowedFunctions unused, string query, string functionName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllFunctions & ~AllowedFunctions.AllMathFunctions, + }; + var expectedMessage = string.Format( + "Function '{0}' is not allowed. " + + "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", + functionName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("StringFunctions")] + public void StringFunctions_SucceedIfGroupAllowed(AllowedFunctions unused, string query, string unusedName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllStringFunctions, + }; + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [PropertyData("StringFunctions")] + public void StringFunctions_ThrowIfGroupNotAllowed(AllowedFunctions unused, string query, string functionName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllFunctions & ~AllowedFunctions.AllStringFunctions, + }; + var expectedMessage = string.Format( + "Function '{0}' is not allowed. " + + "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", + functionName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("DateTimeFunctions_Unsupported")] + public void DateTimeFunctions_Unsupported_ThrowODataException(AllowedFunctions unused, string query, string functionName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.None, + }; + var expectedMessage = string.Format( + "An unknown function with name '{0}' was found. " + + "This may also be a key lookup on a navigation property, which is not allowed.", + functionName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("OtherFunctions_UnsupportedTargetType")] + public void OtherFunctions_UnsupportedTargetType_ThrowODataException(string query, string targetType) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.None, + }; + var expectedMessage = string.Format( + "The child type '{0}' in a cast was not an entity type. Casts can only be performed on entity types.", + targetType); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("OtherFunctions_Unsupported")] + public void OtherFunctions_Unsupported_ThrowNotSupported(AllowedFunctions unused, string query, string unusedName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.None, + }; + var expectedMessage = + "Validating OData QueryNode of kind SingleEntityFunctionCall is not supported by FilterQueryValidator."; + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("OtherFunctions_SomeSingleParameterCasts")] + public void OtherFunctions_SomeSingleParameterCasts_ThrowODataException(AllowedFunctions unused, string query, string unusedName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.None, + }; + var expectedMessage = "Cast or IsOf Function must have a type in its arguments."; + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("OtherFunctions_SomeTwoParameterCasts")] + public void OtherFunctions_SomeTwoParameterCasts_ThrowODataException(AllowedFunctions unused, string query, string unusedName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.None, + }; + var expectedMessage = string.Format( + "Encountered invalid type cast. '{0}' is not assignable from '{1}'.", + typeof(DerivedCategory).FullName, + typeof(Product).FullName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("Functions_CheckArguments")] + public void Functions_CheckArguments_SucceedIfAllowed(AllowedFunctions outer, AllowedFunctions inner, string query, string unused) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = outer | inner, + }; + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [PropertyData("Functions_CheckArguments")] + public void Functions_CheckArguments_ThrowIfNotAllowed(AllowedFunctions outer, AllowedFunctions inner, string query, string functionName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = outer, + }; + var expectedMessage = string.Format( + "Function '{0}' is not allowed. " + + "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", + functionName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); } [Fact] - public void AllowedLogicalOperators_ThrowsOnNotAllowedOperators() + public void LogicalOperatorsDataSet_CoversAllValues() { // Arrange - ODataValidationSettings settings = new ODataValidationSettings + // Get all values in the AllowedLogicalOperators enum. + var values = new HashSet( + Enum.GetValues(typeof(AllowedLogicalOperators)).Cast()); + var groupValues = new[] { - AllowedLogicalOperators = AllowedLogicalOperators.All & ~AllowedLogicalOperators.NotEqual + AllowedLogicalOperators.All, + AllowedLogicalOperators.None, }; - FilterQueryOption option = new FilterQueryOption("length(ProductName) ne 6", _productContext); + // Act + // Remove the group items. + foreach (var allowed in groupValues) + { + values.Remove(allowed); + } + + // Remove the individual items. + foreach (var allowed in LogicalOperators.Select(item => (AllowedLogicalOperators)(item[0]))) + { + values.Remove(allowed); + } + + // Assert + // Should have nothing left. + Assert.Empty(values); + } + + [Theory] + [PropertyData("LogicalOperators")] + public void AllowedLogicalOperators_SucceedIfAllowed(AllowedLogicalOperators allow, string query, string unused) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedLogicalOperators = allow, + }; + var option = new FilterQueryOption(query, _productContext); // Act & Assert - Assert.Throws( - () => _validator.Validate(option, settings), - "Logical operator 'NotEqual' is not allowed. To allow it, set the 'AllowedLogicalOperators' property on EnableQueryAttribute or QueryValidationSettings."); + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [PropertyData("LogicalOperators")] + public void AllowedLogicalOperators_ThrowIfNotAllowed(AllowedLogicalOperators exclude, string query, string operatorName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedLogicalOperators = AllowedLogicalOperators.All & ~exclude, + }; + var expectedMessage = string.Format( + "Logical operator '{0}' is not allowed. " + + "To allow it, set the 'AllowedLogicalOperators' property on EnableQueryAttribute or QueryValidationSettings.", + operatorName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("LogicalOperators")] + public void AllowedLogicalOperators_ThrowIfNoneAllowed(AllowedLogicalOperators unused, string query, string operatorName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedLogicalOperators = AllowedLogicalOperators.None, + }; + var expectedMessage = string.Format( + "Logical operator '{0}' is not allowed. " + + "To allow it, set the 'AllowedLogicalOperators' property on EnableQueryAttribute or QueryValidationSettings.", + operatorName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("LogicalOperators_CheckArguments")] + public void LogicalOperators_CheckArguments_SucceedIfAllowed(string query) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedArithmeticOperators = AllowedArithmeticOperators.Add, + }; + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [PropertyData("LogicalOperators_CheckArguments")] + public void LogicalOperators_CheckArguments_ThrowIfNotAllowed(string query) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedArithmeticOperators = AllowedArithmeticOperators.All & ~AllowedArithmeticOperators.Add, + }; + var expectedMessage = string.Format( + "Arithmetic operator 'Add' is not allowed. " + + "To allow it, set the 'AllowedArithmeticOperators' property on EnableQueryAttribute or QueryValidationSettings."); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Fact] + public void ArithmeticNegation_SucceedsIfLogicalNotIsAllowed() + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedLogicalOperators = AllowedLogicalOperators.LessThan | AllowedLogicalOperators.Not, + }; + var option = new FilterQueryOption("-UnitPrice lt 0", _productContext); + + // Act & Assert + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + // Note Negate is _not_ a logical operator. + [Fact] + public void ArithmeticNegation_ThrowsIfLogicalNotIsNotAllowed() + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedLogicalOperators = AllowedLogicalOperators.LessThan, + }; + var expectedMessage = string.Format( + "Logical operator 'Negate' is not allowed. " + + "To allow it, set the 'AllowedLogicalOperators' property on EnableQueryAttribute or QueryValidationSettings."); + var option = new FilterQueryOption("-UnitPrice lt 0", _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); } [Fact] diff --git a/OData/test/System.Web.Http.OData.Test/OData/Query/Validators/ODataQueryValidatorTest.cs b/OData/test/System.Web.Http.OData.Test/OData/Query/Validators/ODataQueryValidatorTest.cs index e1e67358..d1d7bc68 100644 --- a/OData/test/System.Web.Http.OData.Test/OData/Query/Validators/ODataQueryValidatorTest.cs +++ b/OData/test/System.Web.Http.OData.Test/OData/Query/Validators/ODataQueryValidatorTest.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +using System.Collections.Generic; +using System.Linq; using System.Net.Http; using Microsoft.Data.OData; using Microsoft.TestCommon; @@ -18,6 +20,35 @@ namespace System.Web.Http.OData.Query.Validators _context = ValidationTestHelper.CreateCustomerContext(); } + public static TheoryDataSet SupportedQueryOptions + { + get + { + return new TheoryDataSet + { + { AllowedQueryOptions.Expand, "$expand=Contacts", "Expand" }, + { AllowedQueryOptions.Filter, "$filter=Name eq 'Name'", "Filter" }, + { AllowedQueryOptions.InlineCount, "$inlinecount=allpages", "InlineCount" }, + { AllowedQueryOptions.OrderBy, "$orderby=Name", "OrderBy" }, + { AllowedQueryOptions.Select, "$select=Name", "Select" }, + { AllowedQueryOptions.Skip, "$skip=5", "Skip" }, + { AllowedQueryOptions.Top, "$top=10", "Top" }, + }; + } + } + + public static TheoryDataSet UnsupportedQueryOptions + { + get + { + return new TheoryDataSet + { + { AllowedQueryOptions.Format, "$format=json", "Format" }, + { AllowedQueryOptions.SkipToken, "$skiptoken=__skip__", "SkipToken" }, + }; + } + } + [Fact] public void ValidateThrowsOnNullOption() { @@ -32,64 +63,171 @@ namespace System.Web.Http.OData.Query.Validators _validator.Validate(new ODataQueryOptions(_context, new HttpRequestMessage()), null)); } - [Theory] - [InlineData("filter", "Name eq 'abc'", AllowedQueryOptions.Filter)] - [InlineData("orderby", "Name", AllowedQueryOptions.OrderBy)] - [InlineData("skip", "5", AllowedQueryOptions.Skip)] - [InlineData("top", "5", AllowedQueryOptions.Top)] - [InlineData("inlinecount", "none", AllowedQueryOptions.InlineCount)] - [InlineData("select", "Name", AllowedQueryOptions.Select)] - [InlineData("expand", "Contacts", AllowedQueryOptions.Expand)] - [InlineData("format", "json", AllowedQueryOptions.Format)] - [InlineData("skiptoken", "token", AllowedQueryOptions.SkipToken)] - public void Validate_Throws_ForDisallowedQueryOptions(string queryOptionName, string queryValue, AllowedQueryOptions queryOption) + [Fact] + public void QueryOptionDataSets_CoverAllValues() { // Arrange - HttpRequestMessage message = new HttpRequestMessage( - HttpMethod.Get, - new Uri("http://localhost/?$" + queryOptionName + "=" + queryValue) - ); - ODataQueryOptions option = new ODataQueryOptions(_context, message); - ODataValidationSettings settings = new ODataValidationSettings() - { - AllowedQueryOptions = AllowedQueryOptions.All & ~queryOption - }; + // Get all values in the AllowedQueryOptions enum. + var values = new HashSet( + Enum.GetValues(typeof(AllowedQueryOptions)).Cast()); - // Act & Assert - var exception = Assert.Throws(() => _validator.Validate(option, settings)); - Assert.Equal( - "Query option '" + queryOptionName + "' is not allowed. To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings.", - exception.Message, - StringComparer.OrdinalIgnoreCase); + var groupValues = new[] + { + AllowedQueryOptions.All, + AllowedQueryOptions.None, + AllowedQueryOptions.Supported, + }; + var dataSets = SupportedQueryOptions.Concat(UnsupportedQueryOptions); + + // Act + // Remove the group items. + foreach (var allowed in groupValues) + { + values.Remove(allowed); + } + + // Remove the individual items. + foreach (var allowed in dataSets.Select(item => (AllowedQueryOptions)(item[0]))) + { + values.Remove(allowed); + } + + // Assert + // Should have nothing left. + Assert.Empty(values); } [Theory] - [InlineData("filter", "Name eq 'abc'", AllowedQueryOptions.Filter)] - [InlineData("orderby", "Name", AllowedQueryOptions.OrderBy)] - [InlineData("skip", "5", AllowedQueryOptions.Skip)] - [InlineData("top", "5", AllowedQueryOptions.Top)] - [InlineData("inlinecount", "none", AllowedQueryOptions.InlineCount)] - [InlineData("select", "Name", AllowedQueryOptions.Select)] - [InlineData("expand", "Contacts", AllowedQueryOptions.Expand)] - [InlineData("format", "json", AllowedQueryOptions.Format)] - [InlineData("skiptoken", "token", AllowedQueryOptions.SkipToken)] - public void Validate_DoesNotThrow_ForAllowedQueryOptions(string queryOptionName, string queryValue, AllowedQueryOptions queryOption) + [PropertyData("SupportedQueryOptions")] + [PropertyData("UnsupportedQueryOptions")] + public void AllowedQueryOptions_SucceedIfAllowed(AllowedQueryOptions allow, string query, string unused) { // Arrange - HttpRequestMessage message = new HttpRequestMessage( - HttpMethod.Get, - new Uri("http://localhost/?$" + queryOptionName + "=" + queryValue) - ); + HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get, new Uri("http://localhost/?$" + query)); ODataQueryOptions option = new ODataQueryOptions(_context, message); ODataValidationSettings settings = new ODataValidationSettings() { - AllowedQueryOptions = queryOption + AllowedQueryOptions = allow, }; // Act & Assert Assert.DoesNotThrow(() => _validator.Validate(option, settings)); } + [Theory] + [PropertyData("SupportedQueryOptions")] + [PropertyData("UnsupportedQueryOptions")] + public void AllowedQueryOptions_ThrowIfNotAllowed(AllowedQueryOptions exclude, string query, string optionName) + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, new Uri("http://localhost/?" + query)); + var option = new ODataQueryOptions(_context, message); + var expectedMessage = string.Format( + "Query option '{0}' is not allowed. " + + "To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings.", + optionName); + var settings = new ODataValidationSettings() + { + AllowedQueryOptions = AllowedQueryOptions.All & ~exclude, + }; + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("SupportedQueryOptions")] + [PropertyData("UnsupportedQueryOptions")] + public void AllowedQueryOptions_ThrowIfNoneAllowed(AllowedQueryOptions unused, string query, string optionName) + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, new Uri("http://localhost/?" + query)); + var option = new ODataQueryOptions(_context, message); + var expectedMessage = string.Format( + "Query option '{0}' is not allowed. " + + "To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings.", + optionName); + var settings = new ODataValidationSettings() + { + AllowedQueryOptions = AllowedQueryOptions.None, + }; + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("SupportedQueryOptions")] + public void SupportedQueryOptions_SucceedIfGroupAllowed(AllowedQueryOptions unused, string query, string unusedName) + { + // Arrange + HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get, new Uri("http://localhost/?$" + query)); + ODataQueryOptions option = new ODataQueryOptions(_context, message); + ODataValidationSettings settings = new ODataValidationSettings() + { + AllowedQueryOptions = AllowedQueryOptions.Supported, + }; + + // Act & Assert + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [PropertyData("SupportedQueryOptions")] + public void SupportedQueryOptions_ThrowIfGroupNotAllowed(AllowedQueryOptions unused, string query, string optionName) + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, new Uri("http://localhost/?" + query)); + var option = new ODataQueryOptions(_context, message); + var expectedMessage = string.Format( + "Query option '{0}' is not allowed. " + + "To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings.", + optionName); + var settings = new ODataValidationSettings() + { + AllowedQueryOptions = AllowedQueryOptions.All & ~AllowedQueryOptions.Supported, + }; + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("UnsupportedQueryOptions")] + public void UnsupportedQueryOptions_SucceedIfGroupAllowed(AllowedQueryOptions unused, string query, string unusedName) + { + // Arrange + HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get, new Uri("http://localhost/?$" + query)); + ODataQueryOptions option = new ODataQueryOptions(_context, message); + ODataValidationSettings settings = new ODataValidationSettings() + { + AllowedQueryOptions = AllowedQueryOptions.All & ~AllowedQueryOptions.Supported, + }; + + // Act & Assert + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [PropertyData("UnsupportedQueryOptions")] + public void UnsupportedQueryOptions_ThrowIfGroupNotAllowed(AllowedQueryOptions unused, string query, string optionName) + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, new Uri("http://localhost/?" + query)); + var option = new ODataQueryOptions(_context, message); + var expectedMessage = string.Format( + "Query option '{0}' is not allowed. " + + "To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings.", + optionName); + var settings = new ODataValidationSettings() + { + AllowedQueryOptions = AllowedQueryOptions.Supported, + }; + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + [Fact] public void Validate_ValidatesSelectExpandQueryOption_IfItIsNotNull() { diff --git a/OData/test/System.Web.Http.OData.Test/OData/Query/Validators/ValidationTestHelper.cs b/OData/test/System.Web.Http.OData.Test/OData/Query/Validators/ValidationTestHelper.cs index 76cd368b..90f184fc 100644 --- a/OData/test/System.Web.Http.OData.Test/OData/Query/Validators/ValidationTestHelper.cs +++ b/OData/test/System.Web.Http.OData.Test/OData/Query/Validators/ValidationTestHelper.cs @@ -20,6 +20,11 @@ namespace System.Web.Http.OData.Query.Validators return new ODataQueryContext(GetProductsModel(), typeof(Product)); } + internal static ODataQueryContext CreateDerivedProductsContext() + { + return new ODataQueryContext(GetDerivedProductsModel(), typeof(Product)); + } + private static IEdmModel GetCustomersModel() { HttpConfiguration configuration = new HttpConfiguration(); @@ -31,12 +36,27 @@ namespace System.Web.Http.OData.Query.Validators } private static IEdmModel GetProductsModel() + { + var builder = GetProductsBuilder(); + return builder.GetEdmModel(); + } + + private static IEdmModel GetDerivedProductsModel() + { + var builder = GetProductsBuilder(); + builder.EntitySet("Product"); + builder.Entity().DerivesFrom(); + builder.Entity().DerivesFrom(); + return builder.GetEdmModel(); + } + + private static ODataConventionModelBuilder GetProductsBuilder() { HttpConfiguration configuration = new HttpConfiguration(); configuration.Services.Replace(typeof(IAssembliesResolver), new TestAssemblyResolver(typeof(Product))); ODataConventionModelBuilder builder = new ODataConventionModelBuilder(configuration); builder.EntitySet("Product"); - return builder.GetEdmModel(); + return builder; } } } diff --git a/OData/test/System.Web.Http.OData.Test/System.Web.Http.OData.Test.csproj b/OData/test/System.Web.Http.OData.Test/System.Web.Http.OData.Test.csproj index cc111531..75f2ec87 100644 --- a/OData/test/System.Web.Http.OData.Test/System.Web.Http.OData.Test.csproj +++ b/OData/test/System.Web.Http.OData.Test/System.Web.Http.OData.Test.csproj @@ -103,6 +103,7 @@ + diff --git a/OData/test/System.Web.OData.Test/OData/EnableQueryTests.cs b/OData/test/System.Web.OData.Test/OData/EnableQueryTests.cs new file mode 100644 index 00000000..79aebc28 --- /dev/null +++ b/OData/test/System.Web.OData.Test/OData/EnableQueryTests.cs @@ -0,0 +1,696 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Web.Http; +using System.Web.Http.Dispatcher; +using System.Web.OData.Builder; +using System.Web.OData.Extensions; +using System.Web.OData.Query; +using System.Web.OData.TestCommon; +using Microsoft.OData.Edm; +using Microsoft.TestCommon; + +namespace System.Web.OData.Test +{ + public class EnableQueryTests + { + // Other not allowed query options like $orderby, $top, etc. + public static TheoryDataSet OtherQueryOptionsTestData + { + get + { + return new TheoryDataSet + { + {"?$orderby=Id", "OrderBy"}, + {"?$top=5", "Top"}, + {"?$skip=10", "Skip"}, + {"?$count=true", "Count"}, + {"?$select=Id", "Select"}, + {"?$expand=Orders", "Expand"}, + }; + } + } + + // Other unsupported query options + public static TheoryDataSet OtherUnsupportedQueryOptionsTestData + { + get + { + return new TheoryDataSet + { + {"?$skiptoken=5", "SkipToken"}, + }; + } + } + + public static TheoryDataSet LogicalOperatorsTestData + { + get + { + return new TheoryDataSet + { + // And and or operators + {"?$filter=Adult or false", "'Or'"}, + {"?$filter=true and Adult", "'And'"}, + + // Logical operators with simple property + {"?$filter=Id ne 5", "'NotEqual'"}, + {"?$filter=Id gt 5", "'GreaterThan'"}, + {"?$filter=Id ge 5", "'GreaterThanOrEqual'"}, + {"?$filter=Id lt 5", "'LessThan'"}, + {"?$filter=Id le 5", "'LessThanOrEqual'"}, + + // Logical operators with property in a complex type property + {"?$filter=Address/ZipCode ne 5", "'NotEqual'"}, + {"?$filter=Address/ZipCode gt 5", "'GreaterThan'"}, + {"?$filter=Address/ZipCode ge 5", "'GreaterThanOrEqual'"}, + {"?$filter=Address/ZipCode lt 5", "'LessThan'"}, + {"?$filter=Address/ZipCode le 5", "'LessThanOrEqual'"}, + + // Logical operators with property in a single valued navigation property + {"?$filter=Category/Id ne 5", "'NotEqual'"}, + {"?$filter=Category/Id gt 5", "'GreaterThan'"}, + {"?$filter=Category/Id ge 5", "'GreaterThanOrEqual'"}, + {"?$filter=Category/Id lt 5", "'LessThan'"}, + {"?$filter=Category/Id le 5", "'LessThanOrEqual'"}, + + // Logical operators with property in a derived type in a single valued navigation property + {"?$filter=Category/System.Web.OData.Test.PremiumEnableQueryCategory/PremiumLevel ne 5", "NotEqual'"}, + {"?$filter=Category/System.Web.OData.Test.PremiumEnableQueryCategory/PremiumLevel gt 5", "GreaterThan'"}, + {"?$filter=Category/System.Web.OData.Test.PremiumEnableQueryCategory/PremiumLevel ge 5", "GreaterThanOrEqual'"}, + {"?$filter=Category/System.Web.OData.Test.PremiumEnableQueryCategory/PremiumLevel lt 5", "LessThan'"}, + {"?$filter=Category/System.Web.OData.Test.PremiumEnableQueryCategory/PremiumLevel le 5", "LessThanOrEqual'"}, + + // not operator + {"?$filter=not Adult", "'Not'"}, + }; + } + } + + public static TheoryDataSet EqualsOperatorTestData + { + get + { + return new TheoryDataSet + { + {"?$filter=Id eq 5", "Equal"}, + {"?$filter=Address/ZipCode eq 5", "Equal"}, + {"?$filter=Category/Id eq 5", "Equal"}, + {"?$filter=Category/System.Web.OData.Test.PremiumEnableQueryCategory/PremiumLevel eq 5", "Equal"}, + }; + } + } + + public static TheoryDataSet ArithmeticOperatorsTestData + { + get + { + return new TheoryDataSet + { + // Arithmetic operators with simple property + {"?$filter=1 eq (3 add Id)", "Add"}, + {"?$filter=1 eq (3 sub Id)", "Subtract"}, + {"?$filter=1 eq (1 mul Id)", "Multiply"}, + {"?$filter=1 eq (Id div 1)", "Divide"}, + {"?$filter=1 eq (Id mod 1)", "Modulo"}, + + // Arithmetic operators with property in a complex type property + {"?$filter=1 eq (3 add Address/ZipCode)", "Add"}, + {"?$filter=1 eq (3 sub Address/ZipCode)", "Subtract"}, + {"?$filter=1 eq (1 mul Address/ZipCode)", "Multiply"}, + {"?$filter=1 eq (Address/ZipCode div 1)", "Divide"}, + {"?$filter=1 eq (Address/ZipCode mod 1)", "Modulo"}, + + // Arithmetic operators with property in a single valued navigation property + {"?$filter=1 eq (3 add Category/Id)", "Add"}, + {"?$filter=1 eq (3 sub Category/Id)", "Subtract"}, + {"?$filter=1 eq (1 mul Category/Id)", "Multiply"}, + {"?$filter=1 eq (Category/Id div 1)", "Divide"}, + {"?$filter=1 eq (Category/Id mod 1)", "Modulo"}, + + // Arithmetic operators with property in a derived type in a single valued navigation property + {"?$filter=1 eq (3 add Category/System.Web.OData.Test.PremiumEnableQueryCategory/PremiumLevel)", "Add"}, + {"?$filter=1 eq (3 sub Category/System.Web.OData.Test.PremiumEnableQueryCategory/PremiumLevel)", "Subtract"}, + {"?$filter=1 eq (1 mul Category/System.Web.OData.Test.PremiumEnableQueryCategory/PremiumLevel)", "Multiply"}, + {"?$filter=1 eq (Category/System.Web.OData.Test.PremiumEnableQueryCategory/PremiumLevel div 1)", "Divide"}, + {"?$filter=1 eq (Category/System.Web.OData.Test.PremiumEnableQueryCategory/PremiumLevel mod 1)", "Modulo"}, + }; + } + } + + public static TheoryDataSet AnyAndAllFunctionsTestData + { + get + { + return new TheoryDataSet + { + // Primitive collection property + {"?$filter=Points/any()", "any"}, + {"?$filter=Points/any(p: p eq 1)", "any"}, + {"?$filter=Points/all(p: p eq 1)", "all"}, + + // Complex type collection property + {"?$filter=Addresses/any()", "any"}, + {"?$filter=Addresses/any(a: a/ZipCode eq 1)", "any"}, + {"?$filter=Addresses/all(a: a/ZipCode eq 1)", "all"}, + + // Collection navigation property + {"?$filter=Orders/any()", "any"}, + {"?$filter=Orders/any(o: o/Id eq 1)", "any"}, + {"?$filter=Orders/all(o: o/Id eq 1)", "all"}, + + // Collection navigation property with casts + {"?$filter=Orders/any(o: o/System.Web.OData.Test.DiscountedEnableQueryOrder/Discount eq 1)", "any"}, + {"?$filter=Orders/all(o: o/System.Web.OData.Test.DiscountedEnableQueryOrder/Discount eq 1)", "all"}, + {"?$filter=Orders/System.Web.OData.Test.DiscountedEnableQueryOrder/any()", "any"}, + {"?$filter=Orders/System.Web.OData.Test.DiscountedEnableQueryOrder/any(o: o/Discount eq 1)", "any"}, + {"?$filter=Orders/System.Web.OData.Test.DiscountedEnableQueryOrder/all(o: o/Discount eq 1)", "all"}, + }; + + } + } + + public static TheoryDataSet CastFunctionTestData + { + get + { + return new TheoryDataSet + { + // Entity type casts + {"?$filter=cast(Category,'System.Web.OData.Test.PremiumEnableQueryCategory') eq null", "cast"}, + {"?$filter=cast(Id, Edm.Double) eq 2", "cast"}, + {"?$filter=cast(Id, 'Edm.Double') eq 2", "cast"}, + {"?$filter=cast('System.Web.OData.Test.PremiumEnableQueryCustomer') eq null", "cast"}, + }; + } + } + + public static TheoryDataSet IsOfFunctionTestData + { + get + { + return new TheoryDataSet + { + // Entity type casts + {"?$filter=isof(Category,'System.Web.OData.Test.PremiumEnableQueryCategory')", "isof"}, + {"?$filter=isof('System.Web.OData.Test.PremiumEnableQueryCustomer')", "isof"}, + }; + } + } + + public static TheoryDataSet StringFunctionsTestData + { + get + { + return new TheoryDataSet + { + {"?$filter=startswith(Name, 'Customer')", "startswith"}, + {"?$filter=endswith(Name, 'Customer')", "endswith"}, + {"?$filter=contains(Name, 'Customer')", "contains"}, + {"?$filter=length(Name) eq 1", "length"}, + {"?$filter=indexof(Name, 'Customer') eq 1", "indexof"}, + {"?$filter=concat('Customer', Name) eq 'Customer'", "concat"}, + {"?$filter=substring(Name, 3) eq 'Customer'", "substring"}, + {"?$filter=substring(Name, 3, 3) eq 'Customer'", "substring"}, + {"?$filter=tolower(Name) eq 'customer'", "tolower"}, + {"?$filter=toupper(Name) eq 'CUSTOMER'", "toupper"}, + {"?$filter=trim(Name) eq 'Customer'", "trim"}, + }; + } + } + + public static TheoryDataSet MathFunctionsTestData + { + get + { + return new TheoryDataSet + { + {"?$filter=round(Id) eq 1", "round"}, + {"?$filter=floor(Id) eq 1", "floor"}, + {"?$filter=ceiling(Id) eq 1", "ceiling"}, + }; + } + } + + public static TheoryDataSet SupportedDateTimeFunctionsTestData + { + get + { + return new TheoryDataSet + { + {"?$filter=year(AbsoluteBirthDate) eq 1987", "year"}, + {"?$filter=month(AbsoluteBirthDate) eq 1987", "month"}, + {"?$filter=day(AbsoluteBirthDate) eq 1987", "day"}, + {"?$filter=hour(AbsoluteBirthDate) eq 1987", "hour"}, + {"?$filter=minute(AbsoluteBirthDate) eq 1987", "minute"}, + {"?$filter=second(AbsoluteBirthDate) eq 1987", "second"}, + }; + } + } + + // These represent time functions that we validate but for which we don't support + // end to end. + public static TheoryDataSet UnsupportedDateTimeFunctionsTestData + { + get + { + return new TheoryDataSet + { + {"?$filter=years(Time) eq 1987", "years"}, + {"?$filter=months(Time) eq 1987", "months"}, + {"?$filter=days(Time) eq 1987", "days"}, + {"?$filter=hours(Time) eq 1987", "hours"}, + {"?$filter=minutes(Time) eq 1987", "minutes"}, + {"?$filter=seconds(Time) eq 1987", "seconds"}, + }; + } + } + + // Other limitations like MaxSkip, MaxTop, AllowedOrderByProperties, etc. + public static TheoryDataSet NumericQueryLimitationsTestData + { + get + { + return new TheoryDataSet + { + {"?$orderby=Name desc, Id asc", "$orderby"}, + {"?$skip=20", "Skip"}, + {"?$top=20", "Top"}, + {"?$expand=Orders($expand=OrderLines)", "$expand"}, + {"?$filter=Orders/any(o: o/OrderLines/all(ol: ol/Id gt 0))", "MaxAnyAllExpressionDepth"}, + {"?$filter=Orders/any(o: o/Total gt 0) and Id eq 5", "MaxNodeCount"}, + }; + } + } + + [Theory] + [PropertyData("LogicalOperatorsTestData")] + [PropertyData("ArithmeticOperatorsTestData")] + [PropertyData("StringFunctionsTestData")] + [PropertyData("MathFunctionsTestData")] + [PropertyData("SupportedDateTimeFunctionsTestData")] + [PropertyData("UnsupportedDateTimeFunctionsTestData")] + [PropertyData("AnyAndAllFunctionsTestData")] + [PropertyData("OtherQueryOptionsTestData")] + [PropertyData("OtherUnsupportedQueryOptionsTestData")] + public void EnableQuery_Blocks_NotAllowedQueries(string queryString, string expectedElement) + { + // Arrange + string url = "http://localhost/odata/OnlyFilterAndEqualsAllowedCustomers"; + HttpServer server = CreateServer("OnlyFilterAndEqualsAllowedCustomers"); + HttpClient client = new HttpClient(server); + + // Act + HttpResponseMessage response = client.GetAsync(url + queryString).Result; + string errorMessage = response.Content.ReadAsStringAsync().Result; + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Contains("not allowed", errorMessage); + Assert.Contains(expectedElement, errorMessage); + } + + [Theory] + [PropertyData("LogicalOperatorsTestData")] + [PropertyData("ArithmeticOperatorsTestData")] + [PropertyData("StringFunctionsTestData")] + [PropertyData("MathFunctionsTestData")] + [PropertyData("SupportedDateTimeFunctionsTestData")] + [PropertyData("UnsupportedDateTimeFunctionsTestData")] + [PropertyData("AnyAndAllFunctionsTestData")] + public void EnableQuery_BlocksFilter_WhenNotAllowed(string queryString, string unused) + { + // Arrange + string url = "http://localhost/odata/FilterDisabledCustomers"; + HttpServer server = CreateServer("FilterDisabledCustomers"); + HttpClient client = new HttpClient(server); + + // Act + HttpResponseMessage response = client.GetAsync(url + queryString).Result; + string errorMessage = response.Content.ReadAsStringAsync().Result; + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Contains("not allowed", errorMessage); + Assert.Contains("Filter", errorMessage); + } + + [Theory] + [PropertyData("OtherUnsupportedQueryOptionsTestData")] + public void EnableQuery_ReturnsBadRequest_ForUnsupportedQueryOptions(string queryString, string expectedElement) + { + // Arrange + string url = "http://localhost/odata/EverythingAllowedCustomers"; + HttpServer server = CreateServer("EverythingAllowedCustomers"); + HttpClient client = new HttpClient(server); + + // Act + HttpResponseMessage response = client.GetAsync(url + queryString).Result; + string errorMessage = response.Content.ReadAsStringAsync().Result; + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Contains("not allowed", errorMessage); + Assert.Contains(expectedElement, errorMessage); + } + + // We check equals separately because we need to use it in the rest of the + // tests to produce valid filter expressions in other cases, so we need to + // enable it in those tests and this test only makes sure it covers the case + // when everything is disabled + [Theory] + [PropertyData("EqualsOperatorTestData")] + public void EnableQuery_BlocksEquals_WhenNotAllowed(string queryString, string expectedElement) + { + // Arrange + string url = "http://localhost/odata/OnlyFilterAllowedCustomers"; + HttpServer server = CreateServer("OnlyFilterAllowedCustomers"); + HttpClient client = new HttpClient(server); + + // Act + HttpResponseMessage response = client.GetAsync(url + queryString).Result; + string errorMessage = response.Content.ReadAsStringAsync().Result; + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Contains("not allowed", errorMessage); + Assert.Contains(expectedElement, errorMessage); + } + + [Theory] + [PropertyData("LogicalOperatorsTestData")] + [PropertyData("ArithmeticOperatorsTestData")] + [PropertyData("EqualsOperatorTestData")] + [PropertyData("OtherQueryOptionsTestData")] + [PropertyData("StringFunctionsTestData")] + [PropertyData("MathFunctionsTestData")] + [PropertyData("SupportedDateTimeFunctionsTestData")] + [PropertyData("AnyAndAllFunctionsTestData")] + [PropertyData("CastFunctionTestData")] + public void EnableQuery_DoesNotBlockQueries_WhenEverythingIsAllowed(string queryString, string unused) + { + // Arrange + string url = "http://localhost/odata/EverythingAllowedCustomers"; + HttpServer server = CreateServer("EverythingAllowedCustomers"); + HttpClient client = new HttpClient(server); + + // Act + HttpResponseMessage response = client.GetAsync(url + queryString).Result; + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Theory] + [PropertyData("UnsupportedDateTimeFunctionsTestData")] + public void EnableQuery_ReturnsBadRequest_ForUnsupportedFunctions(string queryString, string expectedElement) + { + // Arrange + string url = "http://localhost/odata/EverythingAllowedCustomers"; + HttpServer server = CreateServer("EverythingAllowedCustomers"); + HttpClient client = new HttpClient(server); + + // Act + HttpResponseMessage response = client.GetAsync(url + queryString).Result; + string errorMessage = response.Content.ReadAsStringAsync().Result; + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Contains("unknown function", errorMessage); + Assert.Contains(expectedElement, errorMessage); + } + + [Theory] + [PropertyData("IsOfFunctionTestData")] + public void EnableQuery_ReturnsBadRequest_ForIsOf(string queryString, string expectedElement) + { + // Arrange + string url = "http://localhost/odata/EverythingAllowedCustomers"; + HttpServer server = CreateServer("EverythingAllowedCustomers"); + HttpClient client = new HttpClient(server); + + // Act + HttpResponseMessage response = client.GetAsync(url + queryString).Result; + string errorMessage = response.Content.ReadAsStringAsync().Result; + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Contains("Unknown function", errorMessage); + Assert.Contains(expectedElement, errorMessage); + } + + [Theory] + [PropertyData("NumericQueryLimitationsTestData")] + public void EnableQuery_BlocksQueries_WithOtherLimitations(string queryString, string expectedElement) + { + // Arrange + string url = "http://localhost/odata/OtherLimitationsCustomers"; + HttpServer server = CreateServer("OtherLimitationsCustomers"); + HttpClient client = new HttpClient(server); + + // Act + HttpResponseMessage response = client.GetAsync(url + queryString).Result; + string errorMessage = response.Content.ReadAsStringAsync().Result; + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Contains(expectedElement, errorMessage); + } + + // This controller limits any operation except for $filter and the eq operator + // in order to validate that all limitations work. + public class OnlyFilterAndEqualsAllowedCustomersController : ODataController + { + private static readonly IQueryable _customers; + + [EnableQuery( + AllowedFunctions = AllowedFunctions.None, + AllowedLogicalOperators = AllowedLogicalOperators.Equal, + AllowedQueryOptions = AllowedQueryOptions.Filter, + AllowedArithmeticOperators = AllowedArithmeticOperators.None)] + public IQueryable Get() + { + return _customers; + } + + static OnlyFilterAndEqualsAllowedCustomersController() + { + _customers = CreateCustomers().AsQueryable(); + } + } + + // This controller exposes an action that limits everything except for + // filtering in order to verify that limiting the eq operator works. + // We didn't limit the eq operator in other queries as we need it to + // create valid filter queries using other limited elements and we + // want the query to fail because of limitations imposed on other + // elements rather than eq. + public class OnlyFilterAllowedCustomersController : ODataController + { + private static readonly IQueryable _customers; + + [EnableQuery( + AllowedFunctions = AllowedFunctions.None, + AllowedLogicalOperators = AllowedLogicalOperators.None, + AllowedQueryOptions = AllowedQueryOptions.Filter, + AllowedArithmeticOperators = AllowedArithmeticOperators.None)] + public IQueryable Get() + { + return _customers; + } + + static OnlyFilterAllowedCustomersController() + { + _customers = CreateCustomers().AsQueryable(); + } + } + + // This controller disables all the query options ($filter amongst them) + public class FilterDisabledCustomersController : ODataController + { + private static readonly IQueryable _customers; + + [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.None)] + public IQueryable Get() + { + return _customers; + } + + static FilterDisabledCustomersController() + { + _customers = CreateCustomers().AsQueryable(); + } + } + + // This controller doesn't limit anything specific to ensure that the + // requests succeed if they aren't limited. + public class EverythingAllowedCustomersController : ODataController + { + private static readonly IQueryable _customers; + + [EnableQuery] + public IQueryable Get() + { + return _customers; + } + + static EverythingAllowedCustomersController() + { + _customers = CreateCustomers().AsQueryable(); + } + } + + // This controller exposes an action that has limitations on aspects + // other than AllowedFunctions, AllowedLogicalOperators, etc. + public class OtherLimitationsCustomersController : ODataController + { + private static readonly IQueryable _customers; + + [EnableQuery(MaxNodeCount = 5, + MaxExpansionDepth = 1, + MaxAnyAllExpressionDepth = 1, + MaxSkip = 5, + MaxTop = 5, + MaxOrderByNodeCount = 1)] + public IQueryable Get() + { + return _customers; + } + + static OtherLimitationsCustomersController() + { + _customers = CreateCustomers().AsQueryable(); + } + } + + private static HttpServer CreateServer(string customersEntitySet) + { + HttpConfiguration configuration = new HttpConfiguration(); + + // We need to do this to avoid controllers with incorrect attribute + // routing configuration in this assembly that cause an exception to + // be thrown at runtime. With this, we restrict the test to the following + // set of controllers. + configuration.Services.Replace( + typeof(IAssembliesResolver), + new TestAssemblyResolver( + typeof(OnlyFilterAllowedCustomersController), + typeof(OnlyFilterAndEqualsAllowedCustomersController), + typeof(FilterDisabledCustomersController), + typeof(EverythingAllowedCustomersController), + typeof(OtherLimitationsCustomersController))); + + ODataModelBuilder builder = new ODataConventionModelBuilder(); + + builder.EntitySet(customersEntitySet); + builder.EntityType(); + + builder.EntitySet("EnableQueryCategories"); + builder.EntityType(); + + builder.EntitySet("EnableQueryOrders"); + builder.EntityType(); + + builder.EntitySet("EnableQueryOrderLines"); + + builder.ComplexType(); + + IEdmModel model = builder.GetEdmModel(); + + configuration.MapODataServiceRoute("odata", "odata", model); + + return new HttpServer(configuration); + } + + // We need to create the data as we need the queries to succeed in one scenario. + private static IEnumerable CreateCustomers() + { + PremiumEnableQueryCustomer customer = new PremiumEnableQueryCustomer(); + + customer.Id = 1; + customer.Name = "Customer 1"; + customer.Points = Enumerable.Range(1, 10).ToList(); + customer.Address = new EnableQueryAddress { ZipCode = 1 }; + customer.Addresses = Enumerable.Range(1, 10).Select(j => new EnableQueryAddress { ZipCode = j }).ToList(); + + customer.Category = new PremiumEnableQueryCategory + { + Id = 1, + PremiumLevel = 1, + }; + + customer.Orders = Enumerable.Range(1, 10).Select(j => new DiscountedEnableQueryOrder + { + Id = j, + Total = j, + Discount = j, + }).ToList(); + + yield return customer; + } + + public class EnableQueryCustomer + { + public int Id { get; set; } + + public string Name { get; set; } + + public EnableQueryCategory Category { get; set; } + + public ICollection Orders { get; set; } + + public ICollection Points { get; set; } + + public ICollection Addresses { get; set; } + + public EnableQueryAddress Address { get; set; } + + public DateTimeOffset AbsoluteBirthDate { get; set; } + + public TimeSpan Time { get; set; } + + public bool Adult { get; set; } + } + + public class PremiumEnableQueryCustomer : EnableQueryCustomer + { + } + + public class EnableQueryOrder + { + public int Id { get; set; } + + public double Total { get; set; } + + public ICollection OrderLines { get; set; } + } + + public class EnableQueryOrderLine + { + public int Id { get; set; } + } + + public class DiscountedEnableQueryOrder : EnableQueryOrder + { + public double Discount { get; set; } + } + + public class EnableQueryCategory + { + public int Id { get; set; } + } + + public class PremiumEnableQueryCategory : EnableQueryCategory + { + public int PremiumLevel { get; set; } + } + + public class EnableQueryAddress + { + public int ZipCode { get; set; } + } + } +} diff --git a/OData/test/System.Web.OData.Test/OData/Query/Expressions/DataModel.cs b/OData/test/System.Web.OData.Test/OData/Query/Expressions/DataModel.cs index 3c6a5416..a447656a 100644 --- a/OData/test/System.Web.OData.Test/OData/Query/Expressions/DataModel.cs +++ b/OData/test/System.Web.OData.Test/OData/Query/Expressions/DataModel.cs @@ -26,18 +26,24 @@ namespace System.Web.OData.Query.Expressions public bool? Discontinued { get; set; } public DateTimeOffset? DiscontinuedDate { get; set; } public DateTimeOffset NonNullableDiscontinuedDate { get; set; } + [NotFilterable] + public DateTimeOffset NotFilterableDiscontinuedDate { get; set; } public DateTimeOffset DiscontinuedOffset { get; set; } public TimeSpan DiscontinuedSince { get; set; } public ushort? UnsignedReorderLevel { get; set; } + public SimpleEnum Ranking { get; set; } + public Category Category { get; set; } public Address SupplierAddress { get; set; } public int[] AlternateIDs { get; set; } public Address[] AlternateAddresses { get; set; } + [NotFilterable] + public Address[] NotFilterableAlternateAddresses { get; set; } } public class Category diff --git a/OData/test/System.Web.OData.Test/OData/Query/Expressions/FilterBinderTests.cs b/OData/test/System.Web.OData.Test/OData/Query/Expressions/FilterBinderTests.cs index b7afa930..779f8cf2 100644 --- a/OData/test/System.Web.OData.Test/OData/Query/Expressions/FilterBinderTests.cs +++ b/OData/test/System.Web.OData.Test/OData/Query/Expressions/FilterBinderTests.cs @@ -6,7 +6,6 @@ using System.Globalization; using System.Linq; using System.Linq.Expressions; using System.Reflection; -using System.Web.Http; using System.Web.Http.Dispatcher; using System.Web.OData.Builder; using System.Xml.Linq; @@ -425,7 +424,7 @@ namespace System.Web.OData.Query.Expressions var filters = VerifyQueryDeserialization(filter); var result = RunFilter(filters.WithoutNullPropagation, new DataTypes { StringProp = value }); - Assert.Equal(result, expectedResult); + Assert.Equal(expectedResult, result); } // Issue: 477 @@ -1559,6 +1558,7 @@ namespace System.Web.OData.Query.Expressions [InlineData("cast('123',Microsoft.TestCommon.Types.SimpleEnum) ne null", "$it => (Convert(123) != null)")] public void CastMethod_Succeeds(string filter, string expectedResult) { + // Arrange & Act & Assert VerifyQueryDeserialization( filter, expectedResult, @@ -1566,23 +1566,113 @@ namespace System.Web.OData.Query.Expressions } [Theory] - [InlineData("cast(NoSuchProperty,Edm.Int32) ne null", "Could not find a property named 'NoSuchProperty' on type 'System.Web.OData.Query.Expressions.DataTypes'.")] - [InlineData("cast(null,Edm.Unknown) ne null", "The child type 'Edm.Unknown' in a cast was not an entity type. Casts can only be performed on entity types.")] - public void CastFails_UndefinedSourceOrTarget_Throws(string filter, string errorMessage) + [InlineData("cast(NoSuchProperty,Edm.Int32) ne null", + "Could not find a property named 'NoSuchProperty' on type 'System.Web.OData.Query.Expressions.DataTypes'.")] + public void Cast_UndefinedSource_ThrowsODataException(string filter, string errorMessage) { + // Arrange & Act & Assert Assert.Throws(() => Bind(filter), errorMessage); } + public static TheoryDataSet CastToUnquotedUndefinedTarget + { + get + { + return new TheoryDataSet + { + { "cast(Edm.DateTime) eq null", "Edm.DateTime" }, + { "cast(Edm.Unknown) eq null", "Edm.Unknown" }, + { "cast(null,Edm.DateTime) eq null", "Edm.DateTime" }, + { "cast(null,Edm.Unknown) eq null", "Edm.Unknown" }, + { "cast('2001-01-01T12:00:00.000',Edm.DateTime) eq null", "Edm.DateTime" }, + { "cast('2001-01-01T12:00:00.000',Edm.Unknown) eq null", "Edm.Unknown" }, + { "cast(DateTimeProp,Edm.DateTime) eq null", "Edm.DateTime" }, + { "cast(DateTimeProp,Edm.Unknown) eq null", "Edm.Unknown" }, + }; + } + } + + // Exception messages here and in CastQuotedUndefinedTarget_ThrowsODataException should be consistent. + // Worse, this message is incorrect -- casts can be performed on most types but _not_ entity types. [Theory] + [PropertyData("CastToUnquotedUndefinedTarget")] + public void CastToUnquotedUndefinedTarget_ThrowsODataException(string filter, string typeName) + { + // Arrange + var expectedMessage = string.Format( + "The child type '{0}' in a cast was not an entity type. Casts can only be performed on entity types.", + typeName); + + // Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + public static TheoryDataSet CastToQuotedUndefinedTarget + { + get + { + return new TheoryDataSet + { + { "cast('Edm.DateTime') eq null" }, + { "cast('Edm.Unknown') eq null" }, + { "cast(null,'Edm.DateTime') eq null" }, + { "cast(null,'Edm.Unknown') eq null" }, + { "cast('2001-01-01T12:00:00.000','Edm.DateTime') eq null" }, + { "cast('','Edm.Unknown') eq null" }, + { "cast(DateTimeProp,'Edm.DateTime') eq null" }, + { "cast(IntProp,'Edm.Unknown') eq null" }, + }; + } + } + + [Theory] + [PropertyData("CastToQuotedUndefinedTarget")] + public void CastToQuotedUndefinedTarget_ThrowsODataException(string filter) + { + // Arrange + var expectedMessage = "Cast or IsOf Function must have a type in its arguments."; + + // Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + [Theory] + [InlineData("cast(Microsoft.TestCommon.Types.SimpleEnum) ne null")] + [InlineData("cast(Microsoft.TestCommon.Types.FlagsEnum) ne null")] + [InlineData("cast(0,Microsoft.TestCommon.Types.SimpleEnum) ne null")] + [InlineData("cast(0,Microsoft.TestCommon.Types.FlagsEnum) ne null")] + [InlineData("cast(Microsoft.TestCommon.Types.SimpleEnum'0',Microsoft.TestCommon.Types.SimpleEnum) ne null")] + [InlineData("cast(Microsoft.TestCommon.Types.FlagsEnum'0',Microsoft.TestCommon.Types.FlagsEnum) ne null")] [InlineData("cast(SimpleEnumProp,Microsoft.TestCommon.Types.SimpleEnum) ne null")] [InlineData("cast(FlagsEnumProp,Microsoft.TestCommon.Types.FlagsEnum) ne null")] [InlineData("cast(NullableSimpleEnumProp,Microsoft.TestCommon.Types.SimpleEnum) ne null")] [InlineData("cast(IntProp,Microsoft.TestCommon.Types.SimpleEnum) ne null")] [InlineData("cast(DateTimeOffsetProp,Microsoft.TestCommon.Types.SimpleEnum) ne null")] + [InlineData("cast(Microsoft.TestCommon.Types.SimpleEnum'1',Edm.Int32) eq 1")] + [InlineData("cast(Microsoft.TestCommon.Types.FlagsEnum'1',Edm.Int32) eq 1")] + [InlineData("cast(SimpleEnumProp,Edm.Int32) eq 123")] [InlineData("cast(FlagsEnumProp,Edm.Int32) eq 123")] [InlineData("cast(NullableSimpleEnumProp,Edm.Guid) ne null")] - public void CastFails_UnsupportedSourceOrTargetForEnumCast_Throws(string filter) + + [InlineData("cast('Microsoft.TestCommon.Types.SimpleEnum') ne null")] + [InlineData("cast('Microsoft.TestCommon.Types.FlagsEnum') ne null")] + [InlineData("cast(0,'Microsoft.TestCommon.Types.SimpleEnum') ne null")] + [InlineData("cast(0,'Microsoft.TestCommon.Types.FlagsEnum') ne null")] + [InlineData("cast(Microsoft.TestCommon.Types.SimpleEnum'0','Microsoft.TestCommon.Types.SimpleEnum') ne null")] + [InlineData("cast(Microsoft.TestCommon.Types.FlagsEnum'0','Microsoft.TestCommon.Types.FlagsEnum') ne null")] + [InlineData("cast(SimpleEnumProp,'Microsoft.TestCommon.Types.SimpleEnum') ne null")] + [InlineData("cast(FlagsEnumProp,'Microsoft.TestCommon.Types.FlagsEnum') ne null")] + [InlineData("cast(NullableSimpleEnumProp,'Microsoft.TestCommon.Types.SimpleEnum') ne null")] + [InlineData("cast(IntProp,'Microsoft.TestCommon.Types.SimpleEnum') ne null")] + [InlineData("cast(DateTimeOffsetProp,'Microsoft.TestCommon.Types.SimpleEnum') ne null")] + [InlineData("cast(Microsoft.TestCommon.Types.SimpleEnum'1','Edm.Int32') eq 1")] + [InlineData("cast(Microsoft.TestCommon.Types.FlagsEnum'1','Edm.Int32') eq 1")] + [InlineData("cast(SimpleEnumProp,'Edm.Int32') eq 123")] + [InlineData("cast(FlagsEnumProp,'Edm.Int32') eq 123")] + [InlineData("cast(NullableSimpleEnumProp,'Edm.Guid') ne null")] + public void Cast_UnsupportedSourceOrTargetForEnumCast_Throws(string filter) { + // Arrange & Act & Assert // TODO : 1824 Should not throw exception for invalid enum cast in query option. Assert.Throws(() => Bind(filter), "Enumeration type value can only be casted to or from string."); } @@ -1601,18 +1691,23 @@ namespace System.Web.OData.Query.Expressions [InlineData("cast(ComplexProp,Edm.String) eq null")] [InlineData("cast(StringProp,Microsoft.TestCommon.Types.SimpleEnum) eq null")] [InlineData("cast(StringProp,Microsoft.TestCommon.Types.FlagsEnum) eq null")] - public void CastFails_UnsupportedTarget_ReturnsNull(string filter) + public void Cast_UnsupportedTarget_ReturnsNull(string filter) { + // Arrange & Act & Assert VerifyQueryDeserialization(filter, "$it => (null == null)"); } + // See OtherFunctions_SomeTwoParameterCasts_ThrowODataException and OtherFunctions_SomeSingleParameterCasts_ThrowODataException + // in FilterQueryValidatorTest. ODL's ODataQueryOptionParser and FunctionCallBinder call the code throwing these exceptions. [Theory] [InlineData("cast(null,System.Web.OData.Query.Expressions.Address) ne null", - "Encountered invalid type cast. 'System.Web.OData.Query.Expressions.Address' is not assignable from 'System.Web.OData.Query.Expressions.DataTypes'.")] + "Encountered invalid type cast. " + + "'System.Web.OData.Query.Expressions.Address' is not assignable from 'System.Web.OData.Query.Expressions.DataTypes'.")] [InlineData("cast(null,System.Web.OData.Query.Expressions.DataTypes) ne null", "Cast or IsOf Function must have a type in its arguments.")] - public void CastFails_NonPrimitiveTarget_Throws(string filter, string expectErrorMessage) + public void Cast_NonPrimitiveTarget_ThrowsODataException(string filter, string expectErrorMessage) { + // Arrange & Act & Assert // TODO : 1827 Should not throw when the target type of cast is not primitive or enumeration type. Assert.Throws(() => Bind(filter), expectErrorMessage); } @@ -1665,6 +1760,600 @@ namespace System.Web.OData.Query.Expressions Assert.Equal("Microsoft.TestCommon.Types.SimpleEnum", ((ConstantNode)castNode.Parameters.Last()).Value); } + public static TheoryDataSet CastToQuotedPrimitiveType + { + get + { + return new TheoryDataSet + { + { "cast('Edm.Binary') eq null" }, + { "cast('Edm.Boolean') eq null" }, + { "cast('Edm.Byte') eq null" }, + { "cast('Edm.DateTimeOffset') eq null" }, + { "cast('Edm.Decimal') eq null" }, + { "cast('Edm.Double') eq null" }, + { "cast('Edm.Duration') eq null" }, + { "cast('Edm.Guid') eq null" }, + { "cast('Edm.Int16') eq null" }, + { "cast('Edm.Int32') eq null" }, + { "cast('Edm.Int64') eq null" }, + { "cast('Edm.SByte') eq null" }, + { "cast('Edm.Single') eq null" }, + { "cast('Edm.String') eq null" }, + + { "cast(null,'Edm.Binary') eq null" }, + { "cast(null,'Edm.Boolean') eq null" }, + { "cast(null,'Edm.Byte') eq null" }, + { "cast(null,'Edm.DateTimeOffset') eq null" }, + { "cast(null,'Edm.Decimal') eq null" }, + { "cast(null,'Edm.Double') eq null" }, + { "cast(null,'Edm.Duration') eq null" }, + { "cast(null,'Edm.Guid') eq null" }, + { "cast(null,'Edm.Int16') eq null" }, + { "cast(null,'Edm.Int32') eq null" }, + { "cast(null,'Edm.Int64') eq null" }, + { "cast(null,'Edm.SByte') eq null" }, + { "cast(null,'Edm.Single') eq null" }, + { "cast(null,'Edm.String') eq null" }, + + { "cast(binary'T0RhdGE=','Edm.Binary') eq binary'T0RhdGE='" }, + { "cast(false,'Edm.Boolean') eq false" }, + { "cast(23,'Edm.Byte') eq 23" }, + { "cast(2001-01-01T12:00:00.000+08:00,'Edm.DateTimeOffset') eq 2001-01-01T12:00:00.000+08:00" }, + { "cast(23,'Edm.Decimal') eq 23" }, + { "cast(23,'Edm.Double') eq 23" }, + { "cast(duration'PT12H','Edm.Duration') eq duration'PT12H'" }, + { "cast(00000000-0000-0000-0000-000000000000,'Edm.Guid') eq 00000000-0000-0000-0000-000000000000" }, + { "cast(23,'Edm.Int16') eq 23" }, + { "cast(23,'Edm.Int32') eq 23" }, + { "cast(23,'Edm.Int64') eq 23" }, + { "cast(23,'Edm.SByte') eq 23" }, + { "cast(23,'Edm.Single') eq 23" }, + { "cast('hello','Edm.String') eq 'hello'" }, + + { "cast(ByteArrayProp,'Edm.Binary') eq null" }, + { "cast(BoolProp,'Edm.Boolean') eq true" }, + { "cast(DateTimeOffsetProp,'Edm.DateTimeOffset') eq 2001-01-01T12:00:00.000+08:00" }, + { "cast(DecimalProp,'Edm.Decimal') eq 23" }, + { "cast(DoubleProp,'Edm.Double') eq 23" }, + { "cast(TimeSpanProp,'Edm.Duration') eq duration'PT23H'" }, + { "cast(GuidProp,'Edm.Guid') eq 0EFDAECF-A9F0-42F3-A384-1295917AF95E" }, + { "cast(NullableShortProp,'Edm.Int16') eq 23" }, + { "cast(IntProp,'Edm.Int32') eq 23" }, + { "cast(LongProp,'Edm.Int64') eq 23" }, + { "cast(FloatProp,'Edm.Single') eq 23" }, + { "cast(StringProp,'Edm.String') eq 'hello'" }, + }; + } + } + + [Theory] + [PropertyData("CastToQuotedPrimitiveType")] + public void CastToQuotedPrimitiveType_Succeeds(string filter) + { + // Arrange + var model = new DataTypes + { + BoolProp = true, + DateTimeOffsetProp = DateTimeOffset.Parse("2001-01-01T12:00:00.000+08:00"), + DecimalProp = 23, + DoubleProp = 23, + GuidProp = Guid.Parse("0EFDAECF-A9F0-42F3-A384-1295917AF95E"), + NullableShortProp = 23, + IntProp = 23, + LongProp = 23, + FloatProp = 23, + StringProp = "hello", + TimeSpanProp = TimeSpan.FromHours(23), + }; + + // Act & Assert + var filters = VerifyQueryDeserialization( + filter, + expectedResult: NotTesting, + expectedResultWithNullPropagation: NotTesting); + RunFilters(filters, model, expectedValue: new { WithNullPropagation = true, WithoutNullPropagation = true }); + } + + public static TheoryDataSet CastToUnquotedComplexType + { + get + { + return new TheoryDataSet + { + { "cast(System.Web.OData.Query.Expressions.Address) eq null" }, + { "cast(null, System.Web.OData.Query.Expressions.Address) eq null" }, + { "cast('', System.Web.OData.Query.Expressions.Address) eq null" }, + { "cast(SupplierAddress, System.Web.OData.Query.Expressions.Address) eq null" }, + }; + } + } + + [Theory] + [PropertyData("CastToUnquotedComplexType")] + public void CastToUnquotedComplexType_ThrowsODataException(string filter) + { + // Arrange + var expectedMessage = + "Encountered invalid type cast. " + + "'System.Web.OData.Query.Expressions.Address' is not assignable from 'System.Web.OData.Query.Expressions.Product'."; + + // Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + public static TheoryDataSet CastToQuotedComplexType + { + get + { + return new TheoryDataSet + { + { "cast('System.Web.OData.Query.Expressions.Address') eq null" }, + { "cast(null, 'System.Web.OData.Query.Expressions.Address') eq null" }, + { "cast('', 'System.Web.OData.Query.Expressions.Address') eq null" }, + { "cast(SupplierAddress, 'System.Web.OData.Query.Expressions.Address') ne null" }, + }; + } + } + + [Theory] + [PropertyData("CastToQuotedComplexType")] + public void CastToQuotedComplexType_Succeeds(string filter) + { + // Arrange + var model = new Product + { + SupplierAddress = new Address { City = "Redmond", }, + }; + + // Act & Assert + var filters = VerifyQueryDeserialization( + filter, + expectedResult: NotTesting, + expectedResultWithNullPropagation: NotTesting); + RunFilters(filters, model, expectedValue: new { WithNullPropagation = true, WithoutNullPropagation = true }); + } + + public static TheoryDataSet CastToUnquotedEntityType + { + get + { + return new TheoryDataSet + { + { + "cast(System.Web.OData.Query.Expressions.DerivedProduct)/DerivedProductName eq null", + "Cast or IsOf Function must have a type in its arguments." + }, + { + "cast(null, System.Web.OData.Query.Expressions.DerivedCategory)/DerivedCategoryName eq null", + "Encountered invalid type cast. " + + "'System.Web.OData.Query.Expressions.DerivedCategory' is not assignable from 'System.Web.OData.Query.Expressions.Product'." + }, + { + "cast(Category, System.Web.OData.Query.Expressions.DerivedCategory)/DerivedCategoryName eq null", + "Encountered invalid type cast. " + + "'System.Web.OData.Query.Expressions.DerivedCategory' is not assignable from 'System.Web.OData.Query.Expressions.Product'." + }, + }; + } + } + + [Theory] + [PropertyData("CastToUnquotedEntityType")] + public void CastToUnquotedEntityType_ThrowsODataException(string filter, string expectedMessage) + { + // Arrange & Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + // Demonstrates a bug in FilterBinder. + [Theory] + [InlineData("cast('System.Web.OData.Query.Expressions.DerivedProduct')/DerivedProductName eq null", "DerivedProductName")] + [InlineData("cast(Category,'System.Web.OData.Query.Expressions.DerivedCategory')/DerivedCategoryName eq null", "DerivedCategoryName")] + [InlineData("cast(Category, 'System.Web.OData.Query.Expressions.DerivedCategory')/DerivedCategoryName eq null", "DerivedCategoryName")] + public void CastToQuotedEntityType_ThrowsArgumentException(string filter, string propertyName) + { + // Arrange + var expectedMessage = string.Format( + "Instance property '{0}' is not defined for type '{1}'", + propertyName, + typeof(object).FullName); + + // Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + [Theory] + [InlineData("cast(null,'System.Web.OData.Query.Expressions.DerivedCategory')/DerivedCategoryName eq null")] + [InlineData("cast(null, 'System.Web.OData.Query.Expressions.DerivedCategory')/DerivedCategoryName eq null")] + public void CastNullToQuotedEntityType_ThrowsArgumentException(string filter) + { + // Arrange + var expectedMessage = + "An instance of SingleValueFunctionCallNode can only be created with a primitive, complex or enum type. " + + "For functions returning a single entity, use SingleEntityFunctionCallNode instead."; + + // Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + #endregion + + #region 'isof' in query option + + [Theory] + [InlineData("isof(NoSuchProperty,Edm.Int32)", + "Could not find a property named 'NoSuchProperty' on type 'System.Web.OData.Query.Expressions.DataTypes'.")] + public void IsOfUndefinedSource_ThrowsODataException(string filter, string errorMessage) + { + // Arrange & Act & Assert + Assert.Throws(() => Bind(filter), errorMessage); + } + + public static TheoryDataSet IsOfUndefinedTarget + { + get + { + return new TheoryDataSet + { + { "isof(Edm.DateTime)", "Edm.DateTime" }, + { "isof(Edm.Unknown)", "Edm.Unknown" }, + { "isof(null,Edm.DateTime)", "Edm.DateTime" }, + { "isof(null,Edm.Unknown)", "Edm.Unknown" }, + { "isof('2001-01-01T12:00:00.000',Edm.DateTime)", "Edm.DateTime" }, + { "isof('',Edm.Unknown)", "Edm.Unknown" }, + { "isof(DateTimeProp,Edm.DateTime)", "Edm.DateTime" }, + { "isof(IntProp,Edm.Unknown)", "Edm.Unknown" }, + }; + } + } + + // Exception messages here and in IsOfQuotedUndefinedTarget_ThrowsODataException should be consistent. + // Worse, this message is incorrect -- casts can be performed on most types but _not_ entity types and + // isof can't be performed. + [Theory] + [PropertyData("IsOfUndefinedTarget")] + public void IsOfUndefinedTarget_ThrowsODataException(string filter, string typeName) + { + // Arrange + var expectedMessage = string.Format( + "The child type '{0}' in a cast was not an entity type. Casts can only be performed on entity types.", + typeName); + + // Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + public static TheoryDataSet IsOfQuotedUndefinedTarget + { + get + { + return new TheoryDataSet + { + { "isof('Edm.DateTime')" }, + { "isof('Edm.Unknown')" }, + { "isof(null,'Edm.DateTime')" }, + { "isof(null,'Edm.Unknown')" }, + { "isof('2001-01-01T12:00:00.000','Edm.DateTime')" }, + { "isof('','Edm.Unknown')" }, + { "isof(DateTimeProp,'Edm.DateTime')" }, + { "isof(IntProp,'Edm.Unknown')" }, + }; + } + } + + [Theory] + [PropertyData("IsOfQuotedUndefinedTarget")] + public void IsOfQuotedUndefinedTarget_ThrowsODataException(string filter) + { + // Arrange + var expectedMessage = "Cast or IsOf Function must have a type in its arguments."; + + // Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + public static TheoryDataSet IsOfPrimitiveType + { + get + { + return new TheoryDataSet + { + { "isof(Edm.Binary)" }, + { "isof(Edm.Boolean)" }, + { "isof(Edm.Byte)" }, + { "isof(Edm.DateTimeOffset)" }, + { "isof(Edm.Decimal)" }, + { "isof(Edm.Double)" }, + { "isof(Edm.Duration)" }, + { "isof(Edm.Guid)" }, + { "isof(Edm.Int16)" }, + { "isof(Edm.Int32)" }, + { "isof(Edm.Int64)" }, + { "isof(Edm.SByte)" }, + { "isof(Edm.Single)" }, + { "isof(Edm.Stream)" }, + { "isof(Edm.String)" }, + { "isof(Microsoft.TestCommon.Types.SimpleEnum)" }, + { "isof(Microsoft.TestCommon.Types.FlagsEnum)" }, + + { "isof(null,Edm.Binary)" }, + { "isof(null,Edm.Boolean)" }, + { "isof(null,Edm.Byte)" }, + { "isof(null,Edm.DateTimeOffset)" }, + { "isof(null,Edm.Decimal)" }, + { "isof(null,Edm.Double)" }, + { "isof(null,Edm.Duration)" }, + { "isof(null,Edm.Guid)" }, + { "isof(null,Edm.Int16)" }, + { "isof(null,Edm.Int32)" }, + { "isof(null,Edm.Int64)" }, + { "isof(null,Edm.SByte)" }, + { "isof(null,Edm.Single)" }, + { "isof(null,Edm.Stream)" }, + { "isof(null,Edm.String)" }, + { "isof(null,Microsoft.TestCommon.Types.SimpleEnum)" }, + { "isof(null,Microsoft.TestCommon.Types.FlagsEnum)" }, + + { "isof(binary'T0RhdGE=',Edm.Binary)" }, + { "isof(false,Edm.Boolean)" }, + { "isof(23,Edm.Byte)" }, + { "isof(2001-01-01T12:00:00.000+08:00,Edm.DateTimeOffset)" }, + { "isof(23,Edm.Decimal)" }, + { "isof(23,Edm.Double)" }, + { "isof(duration'PT12H',Edm.Duration)" }, + { "isof(00000000-0000-0000-0000-000000000000,Edm.Guid)" }, + { "isof(23,Edm.Int16)" }, + { "isof(23,Edm.Int32)" }, + { "isof(23,Edm.Int64)" }, + { "isof(23,Edm.SByte)" }, + { "isof(23,Edm.Single)" }, + { "isof('hello',Edm.Stream)" }, + { "isof('hello',Edm.String)" }, + { "isof(0,Microsoft.TestCommon.Types.SimpleEnum)" }, + { "isof(Microsoft.TestCommon.Types.SimpleEnum'0',Microsoft.TestCommon.Types.SimpleEnum)" }, + { "isof(0,Microsoft.TestCommon.Types.FlagsEnum)" }, + { "isof(Microsoft.TestCommon.Types.FlagsEnum'0',Microsoft.TestCommon.Types.FlagsEnum)" }, + + { "isof('OData',Edm.Binary)" }, + { "isof('false',Edm.Boolean)" }, + { "isof('23',Edm.Byte)" }, + { "isof('2001-01-01T12:00:00.000+08:00',Edm.DateTimeOffset)" }, + { "isof('23',Edm.Decimal)" }, + { "isof('23',Edm.Double)" }, + { "isof('PT12H',Edm.Duration)" }, + { "isof('00000000-0000-0000-0000-000000000000',Edm.Guid)" }, + { "isof('23',Edm.Int16)" }, + { "isof('23',Edm.Int32)" }, + { "isof('23',Edm.Int64)" }, + { "isof('23',Edm.SByte)" }, + { "isof('23',Edm.Single)" }, + { "isof(23,Edm.String)" }, + { "isof('0',Microsoft.TestCommon.Types.FlagsEnum)" }, + { "isof('0',Microsoft.TestCommon.Types.SimpleEnum)" }, + + { "isof(ByteArrayProp,Edm.Binary)" }, + { "isof(BoolProp,'Edm.Boolean')" }, + { "isof(DateTimeOffsetProp,Edm.DateTimeOffset)" }, + { "isof(DecimalProp,Edm.Decimal)" }, + { "isof(DoubleProp,Edm.Double)" }, + { "isof(TimeSpanProp,'Edm.Duration')" }, + { "isof(GuidProp,Edm.Guid)" }, + { "isof(NullableShortProp,Edm.Int16)" }, + { "isof(IntProp,Edm.Int32)" }, + { "isof(LongProp,Edm.Int64)" }, + { "isof(FloatProp,Edm.Single)" }, + { "isof(StringProp,Edm.String)" }, + { "isof(IntProp,Microsoft.TestCommon.Types.SimpleEnum)" }, + { "isof(FlagsEnumProp,Microsoft.TestCommon.Types.FlagsEnum)" }, + { "isof(SimpleEnumProp,Microsoft.TestCommon.Types.SimpleEnum)" }, + + { "isof('Edm.Binary')" }, + { "isof('Edm.Boolean')" }, + { "isof('Edm.Byte')" }, + { "isof('Edm.DateTimeOffset')" }, + { "isof('Edm.Decimal')" }, + { "isof('Edm.Double')" }, + { "isof('Edm.Duration')" }, + { "isof('Edm.Guid')" }, + { "isof('Edm.Int16')" }, + { "isof('Edm.Int32')" }, + { "isof('Edm.Int64')" }, + { "isof('Edm.SByte')" }, + { "isof('Edm.Single')" }, + { "isof('Edm.Stream')" }, + { "isof('Edm.String')" }, + { "isof('Microsoft.TestCommon.Types.SimpleEnum')" }, + { "isof('Microsoft.TestCommon.Types.FlagsEnum')" }, + + { "isof(null,'Edm.Binary')" }, + { "isof(null,'Edm.Boolean')" }, + { "isof(null,'Edm.Byte')" }, + { "isof(null,'Edm.DateTimeOffset')" }, + { "isof(null,'Edm.Decimal')" }, + { "isof(null,'Edm.Double')" }, + { "isof(null,'Edm.Duration')" }, + { "isof(null,'Edm.Guid')" }, + { "isof(null,'Edm.Int16')" }, + { "isof(null,'Edm.Int32')" }, + { "isof(null,'Edm.Int64')" }, + { "isof(null,'Edm.SByte')" }, + { "isof(null,'Edm.Single')" }, + { "isof(null,'Edm.Stream')" }, + { "isof(null,'Edm.String')" }, + { "isof(null,'Microsoft.TestCommon.Types.SimpleEnum')" }, + { "isof(null,'Microsoft.TestCommon.Types.FlagsEnum')" }, + + { "isof(binary'T0RhdGE=','Edm.Binary')" }, + { "isof(false,'Edm.Boolean')" }, + { "isof(23,'Edm.Byte')" }, + { "isof(2001-01-01T12:00:00.000+08:00,'Edm.DateTimeOffset')" }, + { "isof(23,'Edm.Decimal')" }, + { "isof(23,'Edm.Double')" }, + { "isof(duration'PT12H','Edm.Duration')" }, + { "isof(00000000-0000-0000-0000-000000000000,'Edm.Guid')" }, + { "isof(23,'Edm.Int16')" }, + { "isof(23,'Edm.Int32')" }, + { "isof(23,'Edm.Int64')" }, + { "isof(23,'Edm.SByte')" }, + { "isof(23,'Edm.Single')" }, + { "isof('hello','Edm.Stream')" }, + { "isof('hello','Edm.String')" }, + { "isof(0,'Microsoft.TestCommon.Types.FlagsEnum')" }, + { "isof(Microsoft.TestCommon.Types.FlagsEnum'0','Microsoft.TestCommon.Types.FlagsEnum')" }, + { "isof(0,'Microsoft.TestCommon.Types.SimpleEnum')" }, + { "isof(Microsoft.TestCommon.Types.SimpleEnum'0','Microsoft.TestCommon.Types.SimpleEnum')" }, + + { "isof('OData','Edm.Binary')" }, + { "isof('false','Edm.Boolean')" }, + { "isof('23','Edm.Byte')" }, + { "isof('2001-01-01T12:00:00.000+08:00','Edm.DateTimeOffset')" }, + { "isof('23','Edm.Decimal')" }, + { "isof('23','Edm.Double')" }, + { "isof('PT12H','Edm.Duration')" }, + { "isof('00000000-0000-0000-0000-000000000000','Edm.Guid')" }, + { "isof('23','Edm.Int16')" }, + { "isof('23','Edm.Int32')" }, + { "isof('23','Edm.Int64')" }, + { "isof('23','Edm.SByte')" }, + { "isof('23','Edm.Single')" }, + { "isof(23,'Edm.String')" }, + { "isof('0','Microsoft.TestCommon.Types.FlagsEnum')" }, + { "isof('0','Microsoft.TestCommon.Types.SimpleEnum')" }, + + { "isof(ByteArrayProp,'Edm.Binary')" }, + { "isof(BoolProp,'Edm.Boolean')" }, + { "isof(DateTimeOffsetProp,'Edm.DateTimeOffset')" }, + { "isof(DecimalProp,'Edm.Decimal')" }, + { "isof(DoubleProp,'Edm.Double')" }, + { "isof(TimeSpanProp,'Edm.Duration')" }, + { "isof(GuidProp,'Edm.Guid')" }, + { "isof(NullableShortProp,'Edm.Int16')" }, + { "isof(IntProp,'Edm.Int32')" }, + { "isof(LongProp,'Edm.Int64')" }, + { "isof(FloatProp,'Edm.Single')" }, + { "isof(StringProp,'Edm.String')" }, + { "isof(IntProp,'Microsoft.TestCommon.Types.SimpleEnum')" }, + { "isof(FlagsEnumProp,'Microsoft.TestCommon.Types.FlagsEnum')" }, + { "isof(SimpleEnumProp,'Microsoft.TestCommon.Types.SimpleEnum')" }, + }; + } + } + + // Demonstrates a missing feature in FilterBinder. + [Theory] + [PropertyData("IsOfPrimitiveType")] + public void IsOfPrimitiveType_ThrowsNotImplemented(string filter) + { + // Arrange + var expectedMessage = "Unknown function 'isof'."; + + // Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + public static TheoryDataSet IsOfUnquotedComplexType + { + get + { + return new TheoryDataSet + { + { "isof(System.Web.OData.Query.Expressions.Address)" }, + { "isof(null,System.Web.OData.Query.Expressions.Address)" }, + { "isof(null, System.Web.OData.Query.Expressions.Address)" }, + { "isof(SupplierAddress,System.Web.OData.Query.Expressions.Address)" }, + { "isof(SupplierAddress, System.Web.OData.Query.Expressions.Address)" }, + }; + } + } + + [Theory] + [PropertyData("IsOfUnquotedComplexType")] + public void IsOfUnquotedComplexType_ThrowsODataException(string filter) + { + // Arrange + var expectedMessage = + "Encountered invalid type cast. " + + "'System.Web.OData.Query.Expressions.Address' is not assignable from 'System.Web.OData.Query.Expressions.Product'."; + + // Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + public static TheoryDataSet IsOfUnquotedEntityType + { + get + { + return new TheoryDataSet + { + { + "isof(System.Web.OData.Query.Expressions.DerivedProduct)", + "Cast or IsOf Function must have a type in its arguments." + }, + { + "isof(null,System.Web.OData.Query.Expressions.DerivedCategory)", + "Encountered invalid type cast. " + + "'System.Web.OData.Query.Expressions.DerivedCategory' is not assignable from 'System.Web.OData.Query.Expressions.Product'." + }, + { + "isof(null, System.Web.OData.Query.Expressions.DerivedCategory)", + "Encountered invalid type cast. " + + "'System.Web.OData.Query.Expressions.DerivedCategory' is not assignable from 'System.Web.OData.Query.Expressions.Product'." + }, + { + "isof(Category,System.Web.OData.Query.Expressions.DerivedCategory)", + "Encountered invalid type cast. " + + "'System.Web.OData.Query.Expressions.DerivedCategory' is not assignable from 'System.Web.OData.Query.Expressions.Product'." + }, + { + "isof(Category, System.Web.OData.Query.Expressions.DerivedCategory)", + "Encountered invalid type cast. " + + "'System.Web.OData.Query.Expressions.DerivedCategory' is not assignable from 'System.Web.OData.Query.Expressions.Product'." + }, + }; + } + } + + [Theory] + [PropertyData("IsOfUnquotedEntityType")] + public void IsOfUnquotedEntityType_ThrowsODataException(string filter, string expectedMessage) + { + // Arrange & Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + + public static TheoryDataSet IsOfQuotedNonPrimitiveType + { + get + { + return new TheoryDataSet + { + { "isof('System.Web.OData.Query.Expressions.Address')" }, + { "isof('System.Web.OData.Query.Expressions.DerivedProduct')" }, + { "isof(null,'System.Web.OData.Query.Expressions.Address')" }, + { "isof(null, 'System.Web.OData.Query.Expressions.Address')" }, + { "isof(null,'System.Web.OData.Query.Expressions.DerivedCategory')" }, + { "isof(null, 'System.Web.OData.Query.Expressions.DerivedCategory')" }, + { "isof(SupplierAddress,'System.Web.OData.Query.Expressions.Address')" }, + { "isof(SupplierAddress, 'System.Web.OData.Query.Expressions.Address')" }, + { "isof(Category,'System.Web.OData.Query.Expressions.DerivedCategory')" }, + { "isof(Category, 'System.Web.OData.Query.Expressions.DerivedCategory')" }, + }; + } + } + + // Demonstrates a missing feature in FilterBinder. + [Theory] + [PropertyData("IsOfQuotedNonPrimitiveType")] + public void IsOfQuotedNonPrimitiveType_ThrowsNotImplemented(string filter) + { + // Arrange + var expectedMessage = "Unknown function 'isof'."; + + // Act & Assert + Assert.Throws(() => Bind(filter), expectedMessage); + } + #endregion #region parameter alias for filter query option @@ -1979,7 +2668,7 @@ namespace System.Web.OData.Query.Expressions } else { - Assert.Equal(RunFilter(filterWithNullPropagation, product), expectedValue.WithNullPropagation); + Assert.Equal(expectedValue.WithNullPropagation, RunFilter(filterWithNullPropagation, product)); } var filterWithoutNullPropagation = filters.WithoutNullPropagation as Expression>; @@ -1989,7 +2678,7 @@ namespace System.Web.OData.Query.Expressions } else { - Assert.Equal(RunFilter(filterWithoutNullPropagation, product), expectedValue.WithoutNullPropagation); + Assert.Equal(expectedValue.WithoutNullPropagation, RunFilter(filterWithoutNullPropagation, product)); } } @@ -2068,6 +2757,12 @@ namespace System.Web.OData.Query.Expressions { ODataModelBuilder model = new ODataConventionModelBuilder(); model.EntitySet("Products"); + if (key == typeof(Product)) + { + model.EntityType().DerivesFrom(); + model.EntityType().DerivesFrom(); + } + value = _modelCache[key] = model.GetEdmModel(); } return value; diff --git a/OData/test/System.Web.OData.Test/OData/Query/Validators/FilterQueryValidatorTest.cs b/OData/test/System.Web.OData.Test/OData/Query/Validators/FilterQueryValidatorTest.cs index 52201ca0..3ca525e7 100644 --- a/OData/test/System.Web.OData.Test/OData/Query/Validators/FilterQueryValidatorTest.cs +++ b/OData/test/System.Web.OData.Test/OData/Query/Validators/FilterQueryValidatorTest.cs @@ -48,11 +48,396 @@ namespace System.Web.OData.Query.Validators } } + public static TheoryDataSet ArithmeticOperators + { + get + { + return new TheoryDataSet + { + { AllowedArithmeticOperators.Add, "UnitPrice add 0 eq 23", "Add" }, + { AllowedArithmeticOperators.Divide, "UnitPrice div 23 eq 1", "Divide" }, + { AllowedArithmeticOperators.Modulo, "UnitPrice mod 23 eq 0", "Modulo" }, + { AllowedArithmeticOperators.Multiply, "UnitPrice mul 1 eq 23", "Multiply" }, + { AllowedArithmeticOperators.Subtract, "UnitPrice sub 0 eq 23", "Subtract" }, + }; + } + } + + public static TheoryDataSet ArithmeticOperators_CheckArguments + { + get + { + return new TheoryDataSet + { + { "day(DiscontinuedDate) add 0 eq 23" }, + { "day(DiscontinuedDate) div 23 eq 1" }, + { "day(DiscontinuedDate) mod 23 eq 0" }, + { "day(DiscontinuedDate) mul 1 eq 23" }, + { "day(DiscontinuedDate) sub 0 eq 23" }, + { "0 add day(DiscontinuedDate) eq 23" }, + { "23 div day(DiscontinuedDate) eq 1" }, + { "23 mod day(DiscontinuedDate) eq 0" }, + { "1 mul day(DiscontinuedDate) eq 23" }, + { "0 sub day(DiscontinuedDate) eq -23" }, + }; + } + } + + // No support for OData v4 functions: + // date, fractionalseconds, maxdatetime, mindatetime, now, time, totaloffsetminutes, or totalseconds? + public static TheoryDataSet DateTimeFunctions + { + get + { + return new TheoryDataSet + { + { AllowedFunctions.Day, "day(null) eq 20", "day" }, + { AllowedFunctions.Day, "day(DiscontinuedDate) eq 20", "day" }, + { AllowedFunctions.Hour, "hour(null) eq 10", "hour" }, + { AllowedFunctions.Hour, "hour(DiscontinuedDate) eq 10", "hour" }, + { AllowedFunctions.Minute, "minute(null) eq 20", "minute" }, + { AllowedFunctions.Minute, "minute(DiscontinuedDate) eq 20", "minute" }, + { AllowedFunctions.Month, "month(null) eq 10", "month" }, + { AllowedFunctions.Month, "month(DiscontinuedDate) eq 10", "month" }, + { AllowedFunctions.Second, "second(null) eq 20", "second" }, + { AllowedFunctions.Second, "second(DiscontinuedDate) eq 20", "second" }, + { AllowedFunctions.Year, "year(null) eq 2000", "year" }, + { AllowedFunctions.Year, "year(DiscontinuedDate) eq 2000", "year" }, + }; + } + } + + // Not part of OData v4 but some code remains supporting these TimeSpan functions e.g. in ClrCanonicalFunctions. + public static TheoryDataSet DateTimeFunctions_Unsupported + { + get + { + return new TheoryDataSet + { + { AllowedFunctions.Days, "days(DiscontinuedSince) eq 6", "days" }, + { AllowedFunctions.Hours, "hours(DiscontinuedSince) eq 6", "hours" }, + { AllowedFunctions.Minutes, "minutes(DiscontinuedSince) eq 6", "minutes" }, + { AllowedFunctions.Months, "months(DiscontinuedSince) eq 6", "months" }, + { AllowedFunctions.Seconds, "seconds(DiscontinuedSince) eq 6", "seconds" }, + { AllowedFunctions.Years, "years(DiscontinuedSince) eq 6", "years" }, + }; + } + } + + public static TheoryDataSet MathFunctions + { + get + { + return new TheoryDataSet + { + { AllowedFunctions.Ceiling, "ceiling(null) eq 0", "ceiling" }, + { AllowedFunctions.Ceiling, "ceiling(Weight) eq 0", "ceiling" }, + { AllowedFunctions.Floor, "floor(null) eq 0", "floor" }, + { AllowedFunctions.Floor, "floor(Weight) eq 0", "floor" }, + { AllowedFunctions.Round, "round(null) eq 0", "round" }, + { AllowedFunctions.Round, "round(Weight) eq 0", "round" }, + }; + } + } + + public static TheoryDataSet OtherFunctions + { + get + { + return new TheoryDataSet + { + { AllowedFunctions.All, "AlternateIDs/all(t : null eq 1)", "all" }, + { AllowedFunctions.All, "AlternateIDs/all(t : t eq 1)", "all" }, + { AllowedFunctions.All, "AlternateAddresses/all(t : null eq 'Redmond')", "all" }, + { AllowedFunctions.All, "AlternateAddresses/all(t : t/City eq 'Redmond')", "all" }, + { AllowedFunctions.All, "Category/QueryableProducts/all(t : null eq 'Name')", "all" }, + { AllowedFunctions.All, "Category/QueryableProducts/all(t : t/ProductName eq 'Name')", "all" }, + { AllowedFunctions.All, "Category/EnumerableProducts/all(t : null eq 'Name')", "all" }, + { AllowedFunctions.All, "Category/EnumerableProducts/all(t : t/ProductName eq 'Name')", "all" }, + + { AllowedFunctions.Any, "AlternateIDs/any()", "any" }, + { AllowedFunctions.Any, "AlternateIDs/any(t : null eq 1)", "any" }, + { AllowedFunctions.Any, "AlternateIDs/any(t : t eq 1)", "any" }, + { AllowedFunctions.Any, "AlternateAddresses/any()", "any" }, + { AllowedFunctions.Any, "AlternateAddresses/any(t : null eq 'Redmond')", "any" }, + { AllowedFunctions.Any, "AlternateAddresses/any(t : t/City eq 'Redmond')", "any" }, + { AllowedFunctions.Any, "Category/QueryableProducts/any()", "any" }, + { AllowedFunctions.Any, "Category/QueryableProducts/any(t : null eq 'Name')", "any" }, + { AllowedFunctions.Any, "Category/QueryableProducts/any(t : t/ProductName eq 'Name')", "any" }, + { AllowedFunctions.Any, "Category/EnumerableProducts/any()", "any" }, + { AllowedFunctions.Any, "Category/EnumerableProducts/any(t : null eq 'Name')", "any" }, + { AllowedFunctions.Any, "Category/EnumerableProducts/any(t : t/ProductName eq 'Name')", "any" }, + + { AllowedFunctions.Cast, "cast('Edm.Int64') eq 0", "cast" }, + { AllowedFunctions.Cast, "cast(Edm.String) eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast('Edm.String') eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast('System.Web.OData.Query.Expressions.Address')/City eq 'Redmond'", "cast" }, + { AllowedFunctions.Cast, "cast('System.Web.OData.Query.Expressions.DerivedProduct')/DerivedProductName eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(null,'Edm.Int64') eq 0", "cast" }, + { AllowedFunctions.Cast, "cast(null, 'Edm.Int64') eq 0", "cast" }, + { AllowedFunctions.Cast, "cast(null,Edm.String) eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(null, Edm.String) eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(null,'Edm.String') eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(null, 'Edm.String') eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(null,'Microsoft.TestCommon.Types.SimpleEnum') eq Microsoft.TestCommon.Types.SimpleEnum'First'", "cast" }, + { AllowedFunctions.Cast, "cast(null, 'Microsoft.TestCommon.Types.SimpleEnum') eq Microsoft.TestCommon.Types.SimpleEnum'First'", "cast" }, + { AllowedFunctions.Cast, "cast(null,'System.Web.OData.Query.Expressions.Address')/City eq 'Redmond'", "cast" }, + { AllowedFunctions.Cast, "cast(null, 'System.Web.OData.Query.Expressions.Address')/City eq 'Redmond'", "cast" }, + { AllowedFunctions.Cast, "cast(Microsoft.TestCommon.Types.SimpleEnum'First','Edm.String') eq 'First'", "cast" }, + { AllowedFunctions.Cast, "cast(Microsoft.TestCommon.Types.SimpleEnum'First', 'Edm.String') eq 'First'", "cast" }, + { AllowedFunctions.Cast, "cast(CategoryID,'Edm.Int64') eq 0", "cast" }, + { AllowedFunctions.Cast, "cast(CategoryID, 'Edm.Int64') eq 0", "cast" }, + { AllowedFunctions.Cast, "cast(ReorderLevel,Edm.String) eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(ReorderLevel, Edm.String) eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(ReorderLevel,'Edm.String') eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(ReorderLevel, 'Edm.String') eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(Ranking,'Edm.String') eq 'First'", "cast" }, + { AllowedFunctions.Cast, "cast(Ranking, 'Edm.String') eq 'First'", "cast" }, + { AllowedFunctions.Cast, "cast(ProductName,'Microsoft.TestCommon.Types.SimpleEnum') eq Microsoft.TestCommon.Types.SimpleEnum'First'", "cast" }, + { AllowedFunctions.Cast, "cast(ProductName, 'Microsoft.TestCommon.Types.SimpleEnum') eq Microsoft.TestCommon.Types.SimpleEnum'First'", "cast" }, + { AllowedFunctions.Cast, "cast(SupplierAddress,'System.Web.OData.Query.Expressions.Address')/City eq 'Redmond'", "cast" }, + { AllowedFunctions.Cast, "cast(SupplierAddress, 'System.Web.OData.Query.Expressions.Address')/City eq 'Redmond'", "cast" }, + { AllowedFunctions.Cast, "cast(Category,'System.Web.OData.Query.Expressions.DerivedCategory')/DerivedCategoryName eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(Category, 'System.Web.OData.Query.Expressions.DerivedCategory')/DerivedCategoryName eq 'Name'", "cast" }, + + { AllowedFunctions.IsOf, "isof('Edm.Int64')", "isof" }, + { AllowedFunctions.IsOf, "isof(Edm.String)", "isof" }, + { AllowedFunctions.IsOf, "isof('Edm.String')", "isof" }, + { AllowedFunctions.IsOf, "isof('Microsoft.TestCommon.Types.SimpleEnum')", "isof" }, + { AllowedFunctions.IsOf, "isof('System.Web.OData.Query.Expressions.Address')", "isof" }, + { AllowedFunctions.IsOf, "isof('System.Web.OData.Query.Expressions.DerivedCategory')", "isof" }, + { AllowedFunctions.IsOf, "isof('System.Web.OData.Query.Expressions.DerivedProduct')", "isof" }, + { AllowedFunctions.IsOf, "isof(null,'Edm.Int64')", "isof" }, + { AllowedFunctions.IsOf, "isof(null, 'Edm.Int64')", "isof" }, + { AllowedFunctions.IsOf, "isof(null,Edm.String)", "isof" }, + { AllowedFunctions.IsOf, "isof(null, Edm.String)", "isof" }, + { AllowedFunctions.IsOf, "isof(null,'Edm.String')", "isof" }, + { AllowedFunctions.IsOf, "isof(null, 'Edm.String')", "isof" }, + { AllowedFunctions.IsOf, "isof(null,'Microsoft.TestCommon.Types.SimpleEnum')", "isof" }, + { AllowedFunctions.IsOf, "isof(null, 'Microsoft.TestCommon.Types.SimpleEnum')", "isof" }, + { AllowedFunctions.IsOf, "isof(null,'System.Web.OData.Query.Expressions.Address')", "isof" }, + { AllowedFunctions.IsOf, "isof(null, 'System.Web.OData.Query.Expressions.Address')", "isof" }, + { AllowedFunctions.IsOf, "isof(null,'System.Web.OData.Query.Expressions.DerivedCategory')", "isof" }, + { AllowedFunctions.IsOf, "isof(null, 'System.Web.OData.Query.Expressions.DerivedCategory')", "isof" }, + { AllowedFunctions.IsOf, "isof(CategoryID,'Edm.Int64')", "isof" }, + { AllowedFunctions.IsOf, "isof(CategoryID, 'Edm.Int64')", "isof" }, + { AllowedFunctions.IsOf, "isof(ReorderLevel,Edm.String)", "isof" }, + { AllowedFunctions.IsOf, "isof(ReorderLevel, Edm.String)", "isof" }, + { AllowedFunctions.IsOf, "isof(ReorderLevel,'Edm.String')", "isof" }, + { AllowedFunctions.IsOf, "isof(ReorderLevel, 'Edm.String')", "isof" }, + { AllowedFunctions.IsOf, "isof(Ranking,'Microsoft.TestCommon.Types.SimpleEnum')", "isof" }, + { AllowedFunctions.IsOf, "isof(Ranking, 'Microsoft.TestCommon.Types.SimpleEnum')", "isof" }, + { AllowedFunctions.IsOf, "isof(SupplierAddress,'System.Web.OData.Query.Expressions.Address')", "isof" }, + { AllowedFunctions.IsOf, "isof(SupplierAddress, 'System.Web.OData.Query.Expressions.Address')", "isof" }, + { AllowedFunctions.IsOf, "isof(Category,'System.Web.OData.Query.Expressions.DerivedCategory')", "isof" }, + { AllowedFunctions.IsOf, "isof(Category, 'System.Web.OData.Query.Expressions.DerivedCategory')", "isof" }, + }; + } + } + + public static TheoryDataSet OtherFunctions_SomeSingleParameterCasts + { + get + { + return new TheoryDataSet + { + // Single-parameter casts without quotes around the type name. + { AllowedFunctions.Cast, "cast(System.Web.OData.Query.Expressions.DerivedProduct)/DerivedProductName eq 'Name'", "cast" }, + { AllowedFunctions.IsOf, "isof(System.Web.OData.Query.Expressions.DerivedProduct)", "isof" }, + }; + } + } + + public static TheoryDataSet OtherFunctions_SomeTwoParameterCasts + { + get + { + return new TheoryDataSet + { + // Two-parameter casts without quotes around the type name. + { AllowedFunctions.Cast, "cast(null,System.Web.OData.Query.Expressions.DerivedCategory)/DerivedCategoryName eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(null, System.Web.OData.Query.Expressions.DerivedCategory)/DerivedCategoryName eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(Category,System.Web.OData.Query.Expressions.DerivedCategory)/DerivedCategoryName eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(Category, System.Web.OData.Query.Expressions.DerivedCategory)/DerivedCategoryName eq 'Name'", "cast" }, + + { AllowedFunctions.IsOf, "isof(null,System.Web.OData.Query.Expressions.DerivedCategory)", "isof" }, + { AllowedFunctions.IsOf, "isof(null, System.Web.OData.Query.Expressions.DerivedCategory)", "isof" }, + { AllowedFunctions.IsOf, "isof(Category,System.Web.OData.Query.Expressions.DerivedCategory)", "isof" }, + { AllowedFunctions.IsOf, "isof(Category, System.Web.OData.Query.Expressions.DerivedCategory)", "isof" }, + }; + } + } + + public static TheoryDataSet OtherFunctions_SomeQuotedTwoParameterCasts + { + get + { + return new TheoryDataSet + { + // Cast null to an entity type. Note 'isof' with same arguments is fine. + { AllowedFunctions.Cast, "cast(null,'System.Web.OData.Query.Expressions.DerivedCategory')/DerivedCategoryName eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(null, 'System.Web.OData.Query.Expressions.DerivedCategory')/DerivedCategoryName eq 'Name'", "cast" }, + }; + } + } + + public static TheoryDataSet StringFunctions + { + get + { + return new TheoryDataSet + { + { AllowedFunctions.Concat, "concat(null,'Name') eq 'Name'", "concat" }, + { AllowedFunctions.Concat, "concat(null, 'Name') eq 'Name'", "concat" }, + { AllowedFunctions.Concat, "concat(ProductName,'Name') eq 'Name'", "concat" }, + { AllowedFunctions.Concat, "concat(ProductName, 'Name') eq 'Name'", "concat" }, + { AllowedFunctions.EndsWith, "endswith(null,'Name')", "endswith" }, + { AllowedFunctions.EndsWith, "endswith(null, 'Name')", "endswith" }, + { AllowedFunctions.EndsWith, "endswith(ProductName,'Name')", "endswith" }, + { AllowedFunctions.EndsWith, "endswith(ProductName, 'Name')", "endswith" }, + { AllowedFunctions.IndexOf, "indexof(null,'Name') eq 1", "indexof" }, + { AllowedFunctions.IndexOf, "indexof(null, 'Name') eq 1", "indexof" }, + { AllowedFunctions.IndexOf, "indexof(ProductName,'Name') eq 1", "indexof" }, + { AllowedFunctions.IndexOf, "indexof(ProductName, 'Name') eq 1", "indexof" }, + { AllowedFunctions.Length, "length(null) eq 6", "length" }, + { AllowedFunctions.Length, "length(ProductName) eq 6", "length" }, + { AllowedFunctions.StartsWith, "startswith(null,'Name')", "startswith" }, + { AllowedFunctions.StartsWith, "startswith(null, 'Name')", "startswith" }, + { AllowedFunctions.StartsWith, "startswith(ProductName,'Name')", "startswith" }, + { AllowedFunctions.StartsWith, "startswith(ProductName, 'Name')", "startswith" }, + { AllowedFunctions.Substring, "substring(null,1) eq 'Name'", "substring" }, + { AllowedFunctions.Substring, "substring(null, 1) eq 'Name'", "substring" }, + { AllowedFunctions.Substring, "substring(ProductName,1) eq 'Name'", "substring" }, + { AllowedFunctions.Substring, "substring(ProductName, 1) eq 'Name'", "substring" }, + { AllowedFunctions.Substring, "substring(null,1,2) eq 'Name'", "substring" }, + { AllowedFunctions.Substring, "substring(null, 1, 2) eq 'Name'", "substring" }, + { AllowedFunctions.Substring, "substring(ProductName,1,2) eq 'Name'", "substring" }, + { AllowedFunctions.Substring, "substring(ProductName, 1, 2) eq 'Name'", "substring" }, + // Contains isn't in `AllowedFunctions` with expected name. + { AllowedFunctions.SubstringOf, "contains(null,'Name')", "contains" }, + { AllowedFunctions.SubstringOf, "contains(null, 'Name')", "contains" }, + { AllowedFunctions.SubstringOf, "contains(ProductName,'Name')", "contains" }, + { AllowedFunctions.SubstringOf, "contains(ProductName, 'Name')", "contains" }, + { AllowedFunctions.ToLower, "tolower(null) eq 'Name'", "tolower" }, + { AllowedFunctions.ToLower, "tolower(ProductName) eq 'Name'", "tolower" }, + { AllowedFunctions.ToUpper, "toupper(null) eq 'Name'", "toupper" }, + { AllowedFunctions.ToUpper, "toupper(ProductName) eq 'Name'", "toupper" }, + { AllowedFunctions.Trim, "trim(null) eq 'Name'", "trim" }, + { AllowedFunctions.Trim, "trim(ProductName) eq 'Name'", "trim" }, + }; + } + } + + public static TheoryDataSet Functions_CheckArguments + { + get + { + return new TheoryDataSet + { + { AllowedFunctions.Ceiling, AllowedFunctions.IndexOf, "ceiling(indexof(ProductName, 'Name')) eq 0", "indexof" }, + { AllowedFunctions.Floor, AllowedFunctions.IndexOf, "floor(indexof(ProductName, 'Name')) eq 0", "indexof" }, + { AllowedFunctions.Round, AllowedFunctions.IndexOf, "round(indexof(ProductName, 'Name')) eq 0", "indexof" }, + + { AllowedFunctions.All, AllowedFunctions.IndexOf, "AlternateAddresses/all(t : indexof(t/City, 'Name') eq 3)", "indexof" }, + { AllowedFunctions.Any, AllowedFunctions.IndexOf, "AlternateAddresses/any(t : indexof(t/City, 'Name') eq 3)", "indexof" }, + { AllowedFunctions.Cast, AllowedFunctions.IndexOf, "cast(indexof(ProductName, 'Name'), 'Edm.Int64') eq 0", "indexof" }, + { AllowedFunctions.IsOf, AllowedFunctions.IndexOf, "isof(indexof(ProductName, 'Name'), 'Edm.Int64')", "indexof" }, + + { AllowedFunctions.Concat, AllowedFunctions.Substring, "concat(substring(ProductName, 1), 'Name') eq 'Name'", "substring" }, + { AllowedFunctions.Concat, AllowedFunctions.Substring, "concat(ProductName, substring(ProductName, 1)) eq 'Name'", "substring" }, + { AllowedFunctions.EndsWith, AllowedFunctions.Substring, "endswith(substring(ProductName, 1), 'Name')", "substring" }, + { AllowedFunctions.EndsWith, AllowedFunctions.Substring, "endswith(ProductName, substring(ProductName, 1))", "substring" }, + { AllowedFunctions.IndexOf, AllowedFunctions.Substring, "indexof(substring(ProductName, 1), 'Name') eq 1", "substring" }, + { AllowedFunctions.IndexOf, AllowedFunctions.Substring, "indexof(ProductName, substring(ProductName, 1)) eq 1", "substring" }, + { AllowedFunctions.Length, AllowedFunctions.Substring, "length(substring(ProductName, 1)) eq 6", "substring" }, + { AllowedFunctions.StartsWith, AllowedFunctions.Substring, "startswith(substring(ProductName, 1), 'Name')", "substring" }, + { AllowedFunctions.StartsWith, AllowedFunctions.Substring, "startswith(ProductName, substring(ProductName, 1))", "substring" }, + { AllowedFunctions.Substring, AllowedFunctions.Concat, "substring(concat(ProductName, 'Name'), 1) eq 'Name'", "concat" }, + { AllowedFunctions.Substring, AllowedFunctions.IndexOf, "substring(ProductName, indexof(ProductName, 'Name')) eq 'Name'", "indexof" }, + { AllowedFunctions.Substring, AllowedFunctions.Concat, "substring(concat(ProductName, 'Name'), 1, 2) eq 'Name'", "concat" }, + { AllowedFunctions.Substring, AllowedFunctions.IndexOf, "substring(ProductName, indexof(ProductName, 'Name'), 2) eq 'Name'", "indexof" }, + { AllowedFunctions.Substring, AllowedFunctions.IndexOf, "substring(ProductName, 1, indexof(ProductName, 'Name')) eq 'Name'", "indexof" }, + { AllowedFunctions.SubstringOf, AllowedFunctions.Substring, "contains(substring(ProductName, 1), 'Name')", "substring" }, + { AllowedFunctions.SubstringOf, AllowedFunctions.Substring, "contains(ProductName, substring(ProductName, 1))", "substring" }, + { AllowedFunctions.ToLower, AllowedFunctions.Substring, "tolower(substring(ProductName, 1)) eq 'Name'", "substring" }, + { AllowedFunctions.ToUpper, AllowedFunctions.Substring, "toupper(substring(ProductName, 1)) eq 'Name'", "substring" }, + { AllowedFunctions.Trim, AllowedFunctions.Substring, "trim(substring(ProductName, 1)) eq 'Name'", "substring" }, + }; + } + } + + public static TheoryDataSet Functions_CheckNotFilterable + { + get + { + return new TheoryDataSet + { + { "day(NotFilterableDiscontinuedDate) eq 20", "NotFilterableDiscontinuedDate" }, + { "hour(NotFilterableDiscontinuedDate) eq 10", "NotFilterableDiscontinuedDate" }, + { "minute(NotFilterableDiscontinuedDate) eq 20", "NotFilterableDiscontinuedDate" }, + { "month(NotFilterableDiscontinuedDate) eq 10", "NotFilterableDiscontinuedDate" }, + { "second(NotFilterableDiscontinuedDate) eq 20", "NotFilterableDiscontinuedDate" }, + { "year(NotFilterableDiscontinuedDate) eq 2000", "NotFilterableDiscontinuedDate" }, + + { "NotFilterableAlternateAddresses/all(t : t/City eq 'Redmond')", "NotFilterableAlternateAddresses" }, + { "NotFilterableAlternateAddresses/any(t : t/City eq 'Redmond')", "NotFilterableAlternateAddresses" }, + }; + } + } + + public static TheoryDataSet LogicalOperators + { + get + { + return new TheoryDataSet + { + { AllowedLogicalOperators.And, "Discontinued and AlternateIDs/any()", "And" }, + { AllowedLogicalOperators.Equal, "UnitPrice add 0 eq UnitPrice", "Equal" }, + { AllowedLogicalOperators.GreaterThan, "UnitPrice add 1 gt UnitPrice", "GreaterThan" }, + { AllowedLogicalOperators.GreaterThanOrEqual, "UnitPrice add 0 ge UnitPrice", "GreaterThanOrEqual" }, + { AllowedLogicalOperators.Has, "Ranking has Microsoft.TestCommon.Types.SimpleEnum'First'", "Has" }, + { AllowedLogicalOperators.LessThan, "UnitPrice add -1 lt UnitPrice", "LessThan" }, + { AllowedLogicalOperators.LessThanOrEqual, "UnitPrice add 0 le UnitPrice", "LessThanOrEqual" }, + { AllowedLogicalOperators.Not, "not Discontinued", "Not" }, + { AllowedLogicalOperators.NotEqual, "UnitPrice add 1 ne UnitPrice", "NotEqual" }, + { AllowedLogicalOperators.Or, "Discontinued or AlternateIDs/any()", "Or" }, + }; + } + } + + public static TheoryDataSet LogicalOperators_CheckArguments + { + get + { + return new TheoryDataSet + { + { "(UnitPrice add 0 eq UnitPrice) and AlternateIDs/any()" }, + { "UnitPrice add 0 eq UnitPrice" }, + { "UnitPrice add 1 gt UnitPrice" }, + { "UnitPrice add 0 ge UnitPrice" }, + { "UnitPrice add -1 lt UnitPrice" }, + { "UnitPrice add 0 le UnitPrice" }, + { "not (UnitPrice add 0 eq UnitPrice)" }, + { "UnitPrice add 1 ne UnitPrice" }, + { "(UnitPrice add 0 eq UnitPrice) or AlternateIDs/any()" }, + + { "Discontinued and (UnitPrice add 0 eq UnitPrice)" }, + { "UnitPrice eq UnitPrice add 0" }, + { "UnitPrice gt UnitPrice add -1" }, + { "UnitPrice ge UnitPrice add 0" }, + { "UnitPrice lt UnitPrice add 1" }, + { "UnitPrice le UnitPrice add 0" }, + { "UnitPrice ne UnitPrice add 1" }, + { "Discontinued or (UnitPrice add 0 eq UnitPrice)" }, + }; + } + } + public FilterQueryValidatorTest() { _validator = new MyFilterValidator(); _context = ValidationTestHelper.CreateCustomerContext(); - _productContext = ValidationTestHelper.CreateProductContext(); + _productContext = ValidationTestHelper.CreateDerivedProductsContext(); } [Fact] @@ -69,58 +454,6 @@ namespace System.Web.OData.Query.Validators _validator.Validate(new FilterQueryOption("Name eq 'abc'", _context), null)); } - [Fact] - public void ValidateThrowsIfSubStringIsNotAllowed() - { - Assert.DoesNotThrow(() => - _validator.Validate(new FilterQueryOption("substring(Name,8,1) eq '7'", _context), - new ODataValidationSettings() { AllowedFunctions = AllowedFunctions.Substring })); - - Assert.Throws(() => - _validator.Validate(new FilterQueryOption("substring(Name,8,1) eq '7'", _context), - new ODataValidationSettings() { AllowedFunctions = AllowedFunctions.AllMathFunctions }), - "Function 'substring' is not allowed. To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings."); - } - - [Fact] - public void ValidateThrowsIfNotIsNotAllowed() - { - Assert.DoesNotThrow(() => - _validator.Validate(new FilterQueryOption("not (Name eq 'David')", _context), - new ODataValidationSettings() { AllowedLogicalOperators = AllowedLogicalOperators.Not | AllowedLogicalOperators.Equal })); - - Assert.Throws(() => - _validator.Validate(new FilterQueryOption("not (Name eq 'David')", _context), - new ODataValidationSettings() { AllowedLogicalOperators = AllowedLogicalOperators.Equal }), - "Logical operator 'Not' is not allowed. To allow it, set the 'AllowedLogicalOperators' property on EnableQueryAttribute or QueryValidationSettings."); - } - - [Fact] - public void ValidateThrowsIfModIsNotAllowed() - { - Assert.DoesNotThrow(() => - _validator.Validate(new FilterQueryOption("Id mod 2 eq 0", _context), - new ODataValidationSettings() { AllowedArithmeticOperators = AllowedArithmeticOperators.All })); - - Assert.Throws(() => - _validator.Validate(new FilterQueryOption("Id mod 2 eq 0", _context), - new ODataValidationSettings() { AllowedArithmeticOperators = AllowedArithmeticOperators.Add }), - "Arithmetic operator 'Modulo' is not allowed. To allow it, set the 'AllowedArithmeticOperators' property on EnableQueryAttribute or QueryValidationSettings."); - } - - [Fact] - public void ValidateThrowsIfHasIsNotAllowed() - { - Assert.DoesNotThrow(() => - _validator.Validate(new FilterQueryOption("FavoriteColor has System.Web.OData.Builder.TestModels.Color'Red'", _context), - new ODataValidationSettings() { AllowedLogicalOperators = AllowedLogicalOperators.All })); - - Assert.Throws(() => - _validator.Validate(new FilterQueryOption("FavoriteColor has System.Web.OData.Builder.TestModels.Color'Red'", _context), - new ODataValidationSettings() { AllowedLogicalOperators = AllowedLogicalOperators.Equal }), - "Logical operator 'Has' is not allowed. To allow it, set the 'AllowedLogicalOperators' property on EnableQueryAttribute or QueryValidationSettings."); - } - // want to test if all the virtual methods are being invoked correctly [Fact] public void ValidateVisitAll() @@ -273,55 +606,668 @@ namespace System.Web.OData.Query.Validators } [Fact] - public void AllowedArithmeticOperators_ThrowsOnNotAllowedOperators() + public void ArithmeticOperatorsDataSet_CoversAllValues() { // Arrange - ODataValidationSettings settings = new ODataValidationSettings + // Get all values in the AllowedArithmeticOperators enum. + var values = new HashSet( + Enum.GetValues(typeof(AllowedArithmeticOperators)).Cast()); + var groupValues = new[] { - AllowedArithmeticOperators = AllowedArithmeticOperators.All & ~AllowedArithmeticOperators.Modulo + AllowedArithmeticOperators.All, + AllowedArithmeticOperators.None, }; - FilterQueryOption option = new FilterQueryOption("ProductID mod 2 eq 0", _productContext); + // Act + // Remove the group items. + foreach (var allowed in groupValues) + { + values.Remove(allowed); + } + + // Remove the individual items. + foreach (var allowed in ArithmeticOperators.Select(item => (AllowedArithmeticOperators)(item[0]))) + { + values.Remove(allowed); + } + + // Assert + // Should have nothing left. + Assert.Empty(values); + } + + [Theory] + [PropertyData("ArithmeticOperators")] + public void AllowedArithmeticOperators_SucceedIfAllowed(AllowedArithmeticOperators allow, string query, string unused) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedArithmeticOperators = allow, + }; + var option = new FilterQueryOption(query, _productContext); // Act & Assert - Assert.Throws( - () => _validator.Validate(option, settings), - "Arithmetic operator 'Modulo' is not allowed. To allow it, set the 'AllowedArithmeticOperators' property on EnableQueryAttribute or QueryValidationSettings."); + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [PropertyData("ArithmeticOperators")] + public void AllowedArithmeticOperators_ThrowIfNotAllowed(AllowedArithmeticOperators exclude, string query, string operatorName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedArithmeticOperators = AllowedArithmeticOperators.All & ~exclude, + }; + var expectedMessage = string.Format( + "Arithmetic operator '{0}' is not allowed. " + + "To allow it, set the 'AllowedArithmeticOperators' property on EnableQueryAttribute or QueryValidationSettings.", + operatorName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("ArithmeticOperators")] + public void AllowedArithmeticOperators_ThrowIfNoneAllowed(AllowedArithmeticOperators unused, string query, string operatorName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedArithmeticOperators = AllowedArithmeticOperators.None, + }; + var expectedMessage = string.Format( + "Arithmetic operator '{0}' is not allowed. " + + "To allow it, set the 'AllowedArithmeticOperators' property on EnableQueryAttribute or QueryValidationSettings.", + operatorName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("ArithmeticOperators_CheckArguments")] + public void ArithmeticOperators_CheckArguments_SucceedIfAllowed(string query) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.Day, + }; + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [PropertyData("ArithmeticOperators_CheckArguments")] + public void ArithmeticOperators_CheckArguments_ThrowIfNotAllowed(string query) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllFunctions & ~AllowedFunctions.Day, + }; + var expectedMessage = string.Format( + "Function 'day' is not allowed. " + + "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings."); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); } [Fact] - public void AllowedFunctions_ThrowsOnNotAllowedFunctions() + public void AllowedFunctionDataSets_CoverAllValues() { // Arrange - ODataValidationSettings settings = new ODataValidationSettings + // Get all values in the AllowedFunctions enum. + var values = new HashSet(Enum.GetValues(typeof(AllowedFunctions)).Cast()); + + var groupValues = new[] { - AllowedFunctions = AllowedFunctions.All & ~AllowedFunctions.Length + AllowedFunctions.None, + AllowedFunctions.AllFunctions, + AllowedFunctions.AllDateTimeFunctions, + AllowedFunctions.AllMathFunctions, + AllowedFunctions.AllStringFunctions }; - FilterQueryOption option = new FilterQueryOption("length(ProductName) eq 6", _productContext); + // No need to include OtherFunctions_* here since they cover enum values also in OtherFunctions. + var dataSets = DateTimeFunctions + .Concat(DateTimeFunctions_Unsupported) + .Concat(MathFunctions) + .Concat(OtherFunctions) + .Concat(StringFunctions); + + // Act + // Remove the group items. + foreach (var allowed in groupValues) + { + values.Remove(allowed); + } + + // Remove the individual items. + foreach (var allowed in dataSets.Select(item => (AllowedFunctions)(item[0]))) + { + values.Remove(allowed); + } + + // Assert + // Should have nothing left. + Assert.Empty(values); + } + + [Theory] + [PropertyData("DateTimeFunctions")] + [PropertyData("MathFunctions")] + [PropertyData("OtherFunctions")] + [PropertyData("StringFunctions")] + public void AllowedFunctions_SucceedIfAllowed(AllowedFunctions allow, string query, string unused) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = allow, + }; + var option = new FilterQueryOption(query, _productContext); // Act & Assert - Assert.Throws( - () => _validator.Validate(option, settings), - "Function 'length' is not allowed. To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings."); + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + [Theory] + [PropertyData("DateTimeFunctions")] + [PropertyData("MathFunctions")] + [PropertyData("OtherFunctions")] + [PropertyData("StringFunctions")] + public void AllowedFunctions_ThrowIfNotAllowed(AllowedFunctions exclude, string query, string functionName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllFunctions & ~exclude, + }; + var expectedMessage = string.Format( + "Function '{0}' is not allowed. " + + "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", + functionName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("DateTimeFunctions")] + [PropertyData("MathFunctions")] + [PropertyData("OtherFunctions")] + [PropertyData("StringFunctions")] + public void AllowedFunctions_ThrowIfNoneAllowed(AllowedFunctions unused, string query, string functionName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.None, + }; + var expectedMessage = string.Format( + "Function '{0}' is not allowed. " + + "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", + functionName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("DateTimeFunctions")] + public void DateTimeFunctions_SucceedIfGroupAllowed(AllowedFunctions unused, string query, string unusedName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllDateTimeFunctions, + }; + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [PropertyData("DateTimeFunctions")] + public void DateTimeFunctions_ThrowIfGroupNotAllowed(AllowedFunctions unused, string query, string functionName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllFunctions & ~AllowedFunctions.AllDateTimeFunctions, + }; + var expectedMessage = string.Format( + "Function '{0}' is not allowed. " + + "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", + functionName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("MathFunctions")] + public void MathFunctions_SucceedIfGroupAllowed(AllowedFunctions unused, string query, string unusedName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllMathFunctions, + }; + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [PropertyData("MathFunctions")] + public void MathFunctions_ThrowIfGroupNotAllowed(AllowedFunctions unused, string query, string functionName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllFunctions & ~AllowedFunctions.AllMathFunctions, + }; + var expectedMessage = string.Format( + "Function '{0}' is not allowed. " + + "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", + functionName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("StringFunctions")] + public void StringFunctions_SucceedIfGroupAllowed(AllowedFunctions unused, string query, string unusedName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllStringFunctions, + }; + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [PropertyData("StringFunctions")] + public void StringFunctions_ThrowIfGroupNotAllowed(AllowedFunctions unused, string query, string functionName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllFunctions & ~AllowedFunctions.AllStringFunctions, + }; + var expectedMessage = string.Format( + "Function '{0}' is not allowed. " + + "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", + functionName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("DateTimeFunctions_Unsupported")] + public void DateTimeFunctions_Unsupported_ThrowODataException(AllowedFunctions unused, string query, string functionName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.None, + }; + var expectedMessage = string.Format( + "An unknown function with name '{0}' was found. " + + "This may also be a function import or a key lookup on a navigation property, which is not allowed.", + functionName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("OtherFunctions_SomeSingleParameterCasts")] + public void OtherFunctions_SomeSingleParameterCasts_ThrowODataException(AllowedFunctions unused, string query, string unusedName) + { + // Thrown at + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.FunctionCallBinder.ValidateIsOfOrCast(BindingState state, ..., ref List args) Line 600 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.FunctionCallBinder.ValidateAndBuildCastArgs(BindingState state, ref List args) Line 557 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.FunctionCallBinder.CreateUnboundFunctionNode(FunctionCallToken functionCallToken, ..., BindingState state) Line 525 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.FunctionCallBinder.BindAsBuiltInFunction(FunctionCallToken functionCallToken, ..., List argumentNodes) Line 265 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.FunctionCallBinder.BindFunctionCall(FunctionCallToken functionCallToken, BindingState state) Line 202 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.BindFunctionCall(FunctionCallToken functionCallToken) Line 323 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.Bind(QueryToken token) Line 172 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.EndPathBinder.DetermineParentNode(EndPathToken segmentToken, BindingState state) Line 188 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.EndPathBinder.BindEndPath(EndPathToken endPathToken, BindingState state) Line 138 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.BindEndPath(EndPathToken endPathToken) Line 312 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.Bind(QueryToken token) Line 169 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.BinaryOperatorBinder.GetOperandFromToken(BinaryOperatorKind operatorKind, QueryToken queryToken) Line 83 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.BinaryOperatorBinder.BindBinaryOperator(BinaryOperatorToken binaryOperatorToken) Line 46 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.BindBinaryOperator(BinaryOperatorToken binaryOperatorToken) Line 266 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.Bind(QueryToken token) Line 163 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.FilterBinder.BindFilter(QueryToken filter) Line 51 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.ODataQueryOptionParser.ParseFilterImplementation(string filter, ..., IEdmNavigationSource navigationSource) Line 250 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.ODataQueryOptionParser.ParseFilter() Line 112 + // System.Web.OData.dll!System.Web.OData.Query.FilterQueryOption.FilterClause.get() Line 99 + // System.Web.OData.dll!System.Web.OData.Query.Validators.FilterQueryValidator.Validate(FilterQueryOption filterQueryOption, ODataValidationSettings settings) Line 54 + + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.None, + }; + var expectedMessage = "Cast or IsOf Function must have a type in its arguments."; + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("OtherFunctions_SomeTwoParameterCasts")] + public void OtherFunctions_SomeTwoParameterCasts_ThrowODataException(AllowedFunctions unused, string query, string unusedName) + { + // Thrown at + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Metadata.UriEdmHelpers.CheckRelatedTo(IEdmType parentType, IEdmType childType) Line 108 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.DottedIdentifierBinder.BindDottedIdentifier(DottedIdentifierToken dottedIdentifierToken, BindingState state) Line 107 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.BindCast(DottedIdentifierToken dottedIdentifierToken) Line 288 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.Bind(QueryToken token) Line 175 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.BindFunctionParameter(FunctionParameterToken token) Line 223 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.Bind(QueryToken token) Line 184 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.FunctionCallBinder.BindFunctionCall.AnonymousMethod__8(FunctionParameterToken ar) Line 201 + // System.Core.dll!System.Linq.Enumerable.WhereSelectEnumerableIterator.MoveNext() Line 285 + // mscorlib.dll!System.Collections.Generic.List.List(System.Collections.Generic.IEnumerable collection) Line 105 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.FunctionCallBinder.BindFunctionCall(FunctionCallToken functionCallToken, BindingState state) Line 201 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.BindFunctionCall(FunctionCallToken functionCallToken) Line 323 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.Bind(QueryToken token) Line 172 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.EndPathBinder.DetermineParentNode(EndPathToken segmentToken, BindingState state) Line 188 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.EndPathBinder.BindEndPath(EndPathToken endPathToken, BindingState state) Line 138 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.BindEndPath(EndPathToken endPathToken) Line 312 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.Bind(QueryToken token) Line 169 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.BinaryOperatorBinder.GetOperandFromToken(BinaryOperatorKind operatorKind, QueryToken queryToken) Line 83 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.BinaryOperatorBinder.BindBinaryOperator(BinaryOperatorToken binaryOperatorToken) Line 46 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.BindBinaryOperator(BinaryOperatorToken binaryOperatorToken) Line 266 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.Bind(QueryToken token) Line 163 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.FilterBinder.BindFilter(QueryToken filter) Line 51 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.ODataQueryOptionParser.ParseFilterImplementation(string filter, ..., IEdmNavigationSource navigationSource) Line 250 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.ODataQueryOptionParser.ParseFilter() Line 112 + // System.Web.OData.dll!System.Web.OData.Query.FilterQueryOption.FilterClause.get() Line 99 + // System.Web.OData.dll!System.Web.OData.Query.Validators.FilterQueryValidator.Validate(FilterQueryOption filterQueryOption, ODataValidationSettings settings) Line 54 + + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.None, + }; + var expectedMessage = string.Format( + "Encountered invalid type cast. '{0}' is not assignable from '{1}'.", + typeof(DerivedCategory).FullName, + typeof(Product).FullName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("OtherFunctions_SomeQuotedTwoParameterCasts")] + public void OtherFunctions_SomeQuotedTwoParameterCasts_ThrowArgumentException(AllowedFunctions unused, string query, string unusedName) + { + // Thrown at: + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Semantic.SingleValueFunctionCallNode.SingleValueFunctionCallNode(string name, ..., QueryNode source) Line 92 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Semantic.SingleValueFunctionCallNode.SingleValueFunctionCallNode(string name, ..., IEdmTypeReference returnedTypeReference) Line 63 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.FunctionCallBinder.CreateUnboundFunctionNode(FunctionCallToken functionCallToken, ..., BindingState state) Line 546 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.FunctionCallBinder.BindAsBuiltInFunction(FunctionCallToken functionCallToken, ..., List argumentNodes) Line 265 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.FunctionCallBinder.BindFunctionCall(FunctionCallToken functionCallToken, BindingState state) Line 202 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.BindFunctionCall(FunctionCallToken functionCallToken) Line 323 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.Bind(QueryToken token) Line 172 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.EndPathBinder.DetermineParentNode(EndPathToken segmentToken, BindingState state) Line 188 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.EndPathBinder.BindEndPath(EndPathToken endPathToken, BindingState state) Line 138 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.BindEndPath(EndPathToken endPathToken) Line 312 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.Bind(QueryToken token) Line 169 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.BinaryOperatorBinder.GetOperandFromToken(BinaryOperatorKind operatorKind, QueryToken queryToken) Line 83 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.BinaryOperatorBinder.BindBinaryOperator(BinaryOperatorToken binaryOperatorToken) Line 46 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.BindBinaryOperator(BinaryOperatorToken binaryOperatorToken) Line 266 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.Bind(QueryToken token) Line 163 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.Parsers.FilterBinder.BindFilter(QueryToken filter) Line 51 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.ODataQueryOptionParser.ParseFilterImplementation(string filter, ..., IEdmNavigationSource navigationSource) Line 250 + // Microsoft.OData.Core.dll!Microsoft.OData.Core.UriParser.ODataQueryOptionParser.ParseFilter() Line 112 + // System.Web.OData.dll!System.Web.OData.Query.FilterQueryOption.FilterClause.get() Line 99 + // System.Web.OData.dll!System.Web.OData.Query.Validators.FilterQueryValidator.Validate(FilterQueryOption filterQueryOption, ODataValidationSettings settings) Line 54 + + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllFunctions, + }; + var expectedMessage = "An instance of SingleValueFunctionCallNode can only be created with a primitive, " + + "complex or enum type. For functions returning a single entity, use SingleEntityFunctionCallNode instead."; + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("Functions_CheckArguments")] + public void Functions_CheckArguments_SucceedIfAllowed(AllowedFunctions outer, AllowedFunctions inner, string query, string unused) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = outer | inner, + }; + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [PropertyData("Functions_CheckArguments")] + public void Functions_CheckArguments_ThrowIfNotAllowed(AllowedFunctions outer, AllowedFunctions inner, string query, string functionName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = outer, + }; + var expectedMessage = string.Format( + "Function '{0}' is not allowed. " + + "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", + functionName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("Functions_CheckNotFilterable")] + public void Functions_CheckNotFilterable_ThrowODataException(string query, string propertyName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllFunctions, + }; + var expectedMessage = string.Format( + "The property '{0}' cannot be used in the $filter query option.", + propertyName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); } [Fact] - public void AllowedLogicalOperators_ThrowsOnNotAllowedOperators() + public void LogicalOperatorsDataSet_CoversAllValues() { // Arrange - ODataValidationSettings settings = new ODataValidationSettings + // Get all values in the AllowedLogicalOperators enum. + var values = new HashSet( + Enum.GetValues(typeof(AllowedLogicalOperators)).Cast()); + var groupValues = new[] { - AllowedLogicalOperators = AllowedLogicalOperators.All & ~AllowedLogicalOperators.NotEqual + AllowedLogicalOperators.All, + AllowedLogicalOperators.None, }; - FilterQueryOption option = new FilterQueryOption("length(ProductName) ne 6", _productContext); + // Act + // Remove the group items. + foreach (var allowed in groupValues) + { + values.Remove(allowed); + } + + // Remove the individual items. + foreach (var allowed in LogicalOperators.Select(item => (AllowedLogicalOperators)(item[0]))) + { + values.Remove(allowed); + } + + // Assert + // Should have nothing left. + Assert.Empty(values); + } + + [Theory] + [PropertyData("LogicalOperators")] + public void AllowedLogicalOperators_SucceedIfAllowed(AllowedLogicalOperators allow, string query, string unused) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedLogicalOperators = allow, + }; + var option = new FilterQueryOption(query, _productContext); // Act & Assert - Assert.Throws( - () => _validator.Validate(option, settings), - "Logical operator 'NotEqual' is not allowed. To allow it, set the 'AllowedLogicalOperators' property on EnableQueryAttribute or QueryValidationSettings."); + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [PropertyData("LogicalOperators")] + public void AllowedLogicalOperators_ThrowIfNotAllowed(AllowedLogicalOperators exclude, string query, string operatorName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedLogicalOperators = AllowedLogicalOperators.All & ~exclude, + }; + var expectedMessage = string.Format( + "Logical operator '{0}' is not allowed. " + + "To allow it, set the 'AllowedLogicalOperators' property on EnableQueryAttribute or QueryValidationSettings.", + operatorName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("LogicalOperators")] + public void AllowedLogicalOperators_ThrowIfNoneAllowed(AllowedLogicalOperators unused, string query, string operatorName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedLogicalOperators = AllowedLogicalOperators.None, + }; + var expectedMessage = string.Format( + "Logical operator '{0}' is not allowed. " + + "To allow it, set the 'AllowedLogicalOperators' property on EnableQueryAttribute or QueryValidationSettings.", + operatorName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("LogicalOperators_CheckArguments")] + public void LogicalOperators_CheckArguments_SucceedIfAllowed(string query) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedArithmeticOperators = AllowedArithmeticOperators.Add, + }; + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [PropertyData("LogicalOperators_CheckArguments")] + public void LogicalOperators_CheckArguments_ThrowIfNotAllowed(string query) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedArithmeticOperators = AllowedArithmeticOperators.All & ~AllowedArithmeticOperators.Add, + }; + var expectedMessage = string.Format( + "Arithmetic operator 'Add' is not allowed. " + + "To allow it, set the 'AllowedArithmeticOperators' property on EnableQueryAttribute or QueryValidationSettings."); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Fact] + public void ArithmeticNegation_SucceedsIfLogicalNotIsAllowed() + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedLogicalOperators = AllowedLogicalOperators.LessThan | AllowedLogicalOperators.Not, + }; + var option = new FilterQueryOption("-UnitPrice lt 0", _productContext); + + // Act & Assert + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + // Note Negate is _not_ a logical operator. + [Fact] + public void ArithmeticNegation_ThrowsIfLogicalNotIsNotAllowed() + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedLogicalOperators = AllowedLogicalOperators.LessThan, + }; + var expectedMessage = string.Format( + "Logical operator 'Negate' is not allowed. " + + "To allow it, set the 'AllowedLogicalOperators' property on EnableQueryAttribute or QueryValidationSettings."); + var option = new FilterQueryOption("-UnitPrice lt 0", _productContext); + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); } [Fact] diff --git a/OData/test/System.Web.OData.Test/OData/Query/Validators/ODataQueryValidatorTest.cs b/OData/test/System.Web.OData.Test/OData/Query/Validators/ODataQueryValidatorTest.cs index c7de06fb..02e299e8 100644 --- a/OData/test/System.Web.OData.Test/OData/Query/Validators/ODataQueryValidatorTest.cs +++ b/OData/test/System.Web.OData.Test/OData/Query/Validators/ODataQueryValidatorTest.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +using System.Collections.Generic; +using System.Linq; using System.Net.Http; using Microsoft.OData.Core; using Microsoft.TestCommon; @@ -18,6 +20,35 @@ namespace System.Web.OData.Query.Validators _context = ValidationTestHelper.CreateCustomerContext(); } + public static TheoryDataSet SupportedQueryOptions + { + get + { + return new TheoryDataSet + { + { AllowedQueryOptions.Count, "$count=true", "Count" }, + { AllowedQueryOptions.Expand, "$expand=Contacts", "Expand" }, + { AllowedQueryOptions.Filter, "$filter=Name eq 'Name'", "Filter" }, + { AllowedQueryOptions.Format, "$format=json", "Format" }, + { AllowedQueryOptions.OrderBy, "$orderby=Name", "OrderBy" }, + { AllowedQueryOptions.Select, "$select=Name", "Select" }, + { AllowedQueryOptions.Skip, "$skip=5", "Skip" }, + { AllowedQueryOptions.Top, "$top=10", "Top" }, + }; + } + } + + public static TheoryDataSet UnsupportedQueryOptions + { + get + { + return new TheoryDataSet + { + { AllowedQueryOptions.SkipToken, "$skiptoken=__skip__", "SkipToken" }, + }; + } + } + [Fact] public void ValidateThrowsOnNullOption() { @@ -32,64 +63,171 @@ namespace System.Web.OData.Query.Validators _validator.Validate(new ODataQueryOptions(_context, new HttpRequestMessage()), null)); } - [Theory] - [InlineData("filter", "Name eq 'abc'", AllowedQueryOptions.Filter)] - [InlineData("orderby", "Name", AllowedQueryOptions.OrderBy)] - [InlineData("skip", "5", AllowedQueryOptions.Skip)] - [InlineData("top", "5", AllowedQueryOptions.Top)] - [InlineData("count", "false", AllowedQueryOptions.Count)] - [InlineData("select", "Name", AllowedQueryOptions.Select)] - [InlineData("expand", "Contacts", AllowedQueryOptions.Expand)] - [InlineData("format", "json", AllowedQueryOptions.Format)] - [InlineData("skiptoken", "token", AllowedQueryOptions.SkipToken)] - public void Validate_Throws_ForDisallowedQueryOptions(string queryOptionName, string queryValue, AllowedQueryOptions queryOption) + [Fact] + public void QueryOptionDataSets_CoverAllValues() { // Arrange - HttpRequestMessage message = new HttpRequestMessage( - HttpMethod.Get, - new Uri("http://localhost/?$" + queryOptionName + "=" + queryValue) - ); - ODataQueryOptions option = new ODataQueryOptions(_context, message); - ODataValidationSettings settings = new ODataValidationSettings() - { - AllowedQueryOptions = AllowedQueryOptions.All & ~queryOption - }; + // Get all values in the AllowedQueryOptions enum. + var values = new HashSet( + Enum.GetValues(typeof(AllowedQueryOptions)).Cast()); - // Act & Assert - var exception = Assert.Throws(() => _validator.Validate(option, settings)); - Assert.Equal( - "Query option '" + queryOptionName + "' is not allowed. To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings.", - exception.Message, - StringComparer.OrdinalIgnoreCase); + var groupValues = new[] + { + AllowedQueryOptions.All, + AllowedQueryOptions.None, + AllowedQueryOptions.Supported, + }; + var dataSets = SupportedQueryOptions.Concat(UnsupportedQueryOptions); + + // Act + // Remove the group items. + foreach (var allowed in groupValues) + { + values.Remove(allowed); + } + + // Remove the individual items. + foreach (var allowed in dataSets.Select(item => (AllowedQueryOptions)(item[0]))) + { + values.Remove(allowed); + } + + // Assert + // Should have nothing left. + Assert.Empty(values); } [Theory] - [InlineData("filter", "Name eq 'abc'", AllowedQueryOptions.Filter)] - [InlineData("orderby", "Name", AllowedQueryOptions.OrderBy)] - [InlineData("skip", "5", AllowedQueryOptions.Skip)] - [InlineData("top", "5", AllowedQueryOptions.Top)] - [InlineData("count", "false", AllowedQueryOptions.Count)] - [InlineData("select", "Name", AllowedQueryOptions.Select)] - [InlineData("expand", "Contacts", AllowedQueryOptions.Expand)] - [InlineData("format", "json", AllowedQueryOptions.Format)] - [InlineData("skiptoken", "token", AllowedQueryOptions.SkipToken)] - public void Validate_DoesNotThrow_ForAllowedQueryOptions(string queryOptionName, string queryValue, AllowedQueryOptions queryOption) + [PropertyData("SupportedQueryOptions")] + [PropertyData("UnsupportedQueryOptions")] + public void AllowedQueryOptions_SucceedIfAllowed(AllowedQueryOptions allow, string query, string unused) { // Arrange - HttpRequestMessage message = new HttpRequestMessage( - HttpMethod.Get, - new Uri("http://localhost/?$" + queryOptionName + "=" + queryValue) - ); + HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get, new Uri("http://localhost/?$" + query)); ODataQueryOptions option = new ODataQueryOptions(_context, message); ODataValidationSettings settings = new ODataValidationSettings() { - AllowedQueryOptions = queryOption + AllowedQueryOptions = allow, }; // Act & Assert Assert.DoesNotThrow(() => _validator.Validate(option, settings)); } + [Theory] + [PropertyData("SupportedQueryOptions")] + [PropertyData("UnsupportedQueryOptions")] + public void AllowedQueryOptions_ThrowIfNotAllowed(AllowedQueryOptions exclude, string query, string optionName) + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, new Uri("http://localhost/?" + query)); + var option = new ODataQueryOptions(_context, message); + var expectedMessage = string.Format( + "Query option '{0}' is not allowed. " + + "To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings.", + optionName); + var settings = new ODataValidationSettings() + { + AllowedQueryOptions = AllowedQueryOptions.All & ~exclude, + }; + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("SupportedQueryOptions")] + [PropertyData("UnsupportedQueryOptions")] + public void AllowedQueryOptions_ThrowIfNoneAllowed(AllowedQueryOptions unused, string query, string optionName) + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, new Uri("http://localhost/?" + query)); + var option = new ODataQueryOptions(_context, message); + var expectedMessage = string.Format( + "Query option '{0}' is not allowed. " + + "To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings.", + optionName); + var settings = new ODataValidationSettings() + { + AllowedQueryOptions = AllowedQueryOptions.None, + }; + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("SupportedQueryOptions")] + public void SupportedQueryOptions_SucceedIfGroupAllowed(AllowedQueryOptions unused, string query, string unusedName) + { + // Arrange + HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get, new Uri("http://localhost/?$" + query)); + ODataQueryOptions option = new ODataQueryOptions(_context, message); + ODataValidationSettings settings = new ODataValidationSettings() + { + AllowedQueryOptions = AllowedQueryOptions.Supported, + }; + + // Act & Assert + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [PropertyData("SupportedQueryOptions")] + public void SupportedQueryOptions_ThrowIfGroupNotAllowed(AllowedQueryOptions unused, string query, string optionName) + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, new Uri("http://localhost/?" + query)); + var option = new ODataQueryOptions(_context, message); + var expectedMessage = string.Format( + "Query option '{0}' is not allowed. " + + "To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings.", + optionName); + var settings = new ODataValidationSettings() + { + AllowedQueryOptions = AllowedQueryOptions.All & ~AllowedQueryOptions.Supported, + }; + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [PropertyData("UnsupportedQueryOptions")] + public void UnsupportedQueryOptions_SucceedIfGroupAllowed(AllowedQueryOptions unused, string query, string unusedName) + { + // Arrange + HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get, new Uri("http://localhost/?$" + query)); + ODataQueryOptions option = new ODataQueryOptions(_context, message); + ODataValidationSettings settings = new ODataValidationSettings() + { + AllowedQueryOptions = AllowedQueryOptions.All & ~AllowedQueryOptions.Supported, + }; + + // Act & Assert + Assert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [PropertyData("UnsupportedQueryOptions")] + public void UnsupportedQueryOptions_ThrowIfGroupNotAllowed(AllowedQueryOptions unused, string query, string optionName) + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, new Uri("http://localhost/?" + query)); + var option = new ODataQueryOptions(_context, message); + var expectedMessage = string.Format( + "Query option '{0}' is not allowed. " + + "To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings.", + optionName); + var settings = new ODataValidationSettings() + { + AllowedQueryOptions = AllowedQueryOptions.Supported, + }; + + // Act & Assert + Assert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + [Fact] public void Validate_ValidatesSelectExpandQueryOption_IfItIsNotNull() { diff --git a/OData/test/System.Web.OData.Test/OData/Query/Validators/ValidationTestHelper.cs b/OData/test/System.Web.OData.Test/OData/Query/Validators/ValidationTestHelper.cs index 43ec398f..c0ae313f 100644 --- a/OData/test/System.Web.OData.Test/OData/Query/Validators/ValidationTestHelper.cs +++ b/OData/test/System.Web.OData.Test/OData/Query/Validators/ValidationTestHelper.cs @@ -21,6 +21,11 @@ namespace System.Web.OData.Query.Validators return new ODataQueryContext(GetProductsModel(), typeof(Product)); } + internal static ODataQueryContext CreateDerivedProductsContext() + { + return new ODataQueryContext(GetDerivedProductsModel(), typeof(Product)); + } + private static IEdmModel GetCustomersModel() { HttpConfiguration configuration = new HttpConfiguration(); @@ -32,12 +37,27 @@ namespace System.Web.OData.Query.Validators } private static IEdmModel GetProductsModel() + { + var builder = GetProductsBuilder(); + return builder.GetEdmModel(); + } + + private static IEdmModel GetDerivedProductsModel() + { + var builder = GetProductsBuilder(); + builder.EntitySet("Product"); + builder.EntityType().DerivesFrom(); + builder.EntityType().DerivesFrom(); + return builder.GetEdmModel(); + } + + private static ODataConventionModelBuilder GetProductsBuilder() { HttpConfiguration configuration = new HttpConfiguration(); configuration.Services.Replace(typeof(IAssembliesResolver), new TestAssemblyResolver(typeof(Product))); ODataConventionModelBuilder builder = new ODataConventionModelBuilder(configuration); builder.EntitySet("Product"); - return builder.GetEdmModel(); + return builder; } } } diff --git a/OData/test/System.Web.OData.Test/System.Web.OData.Test.csproj b/OData/test/System.Web.OData.Test/System.Web.OData.Test.csproj index d8a20c41..1c42dee8 100644 --- a/OData/test/System.Web.OData.Test/System.Web.OData.Test.csproj +++ b/OData/test/System.Web.OData.Test/System.Web.OData.Test.csproj @@ -127,6 +127,7 @@ +