// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. using System.Linq; using System.Linq.Expressions; using Microsoft.TestCommon; using Xunit; using Xunit.Extensions; using Assert = Microsoft.TestCommon.AssertEx; namespace System.Web.Http.Query { public class ODataQueryDeserializerTests { [Fact] public void SimpleMultipartQuery() { VerifyQueryDeserialization( "$filter=ProductName eq 'Doritos'&$orderby=UnitPrice&$top=100", "Where(Param_0 => (Param_0.ProductName == \"Doritos\")).OrderBy(Param_1 => Param_1.UnitPrice).Take(100)"); } #region Ordering [Fact] public void OrderBy() { VerifyQueryDeserialization( "$orderby=UnitPrice", "OrderBy(Param_0 => Param_0.UnitPrice)"); } [Fact] public void OrderByAscending() { VerifyQueryDeserialization( "$orderby=UnitPrice asc", "OrderBy(Param_0 => Param_0.UnitPrice)"); } [Fact] public void OrderByDescending() { VerifyQueryDeserialization( "$orderby=UnitPrice desc", "OrderByDescending(Param_0 => Param_0.UnitPrice)"); } [Fact] public void OrderByAscendingThenDesscending() { VerifyQueryDeserialization( "$orderby=UnitPrice desc, ProductName asc", "OrderByDescending(Param_0 => Param_0.UnitPrice).ThenBy(Param_0 => Param_0.ProductName)"); } [Theory] [InlineData("UnitPrice desc, ProductName asc", "OrderByDescending(Param_0 => Param_0.UnitPrice).ThenBy(Param_0 => Param_0.ProductName)")] [InlineData("UnitPrice desc, ProductName desc", "OrderByDescending(Param_0 => Param_0.UnitPrice).ThenByDescending(Param_0 => Param_0.ProductName)")] [InlineData("UnitPrice , ProductName ", "OrderBy(Param_0 => Param_0.UnitPrice).ThenBy(Param_0 => Param_0.ProductName)")] [InlineData("Discontinued, UnitsOnOrder, DiscontinuedDate desc", "OrderBy(Param_0 => Param_0.Discontinued).ThenBy(Param_0 => Param_0.UnitsOnOrder).ThenByDescending(Param_0 => Param_0.DiscontinuedDate)")] public void MultipleOrderBy(string clause, string expressionResult) { VerifyQueryDeserialization( "$orderby=" + clause, expressionResult); } #endregion #region Inequalities [Fact] public void EqualityOperator() { VerifyQueryDeserialization( "$filter=ProductName eq 'Doritos'", "Where(Param_0 => (Param_0.ProductName == \"Doritos\"))"); } [Fact] public void NotEqualOperator() { VerifyQueryDeserialization( "$filter=ProductName ne 'Doritos'", "Where(Param_0 => (Param_0.ProductName != \"Doritos\"))"); } [Fact] public void GreaterThanOperator() { VerifyQueryDeserialization( "$filter=UnitPrice gt 5.00", "Where(Param_0 => (Param_0.UnitPrice > 5.00))"); } [Fact] public void GreaterThanEqualOperator() { VerifyQueryDeserialization( "$filter=UnitPrice ge 5.00", "Where(Param_0 => (Param_0.UnitPrice >= 5.00))"); } [Fact] public void LessThanOperator() { VerifyQueryDeserialization( "$filter=UnitPrice lt 5.00", "Where(Param_0 => (Param_0.UnitPrice < 5.00))"); } [Fact] public void LessThanOrEqualOperator() { VerifyQueryDeserialization( "$filter=UnitPrice le 5.00", "Where(Param_0 => (Param_0.UnitPrice <= 5.00))"); } [Fact] public void NegativeNumbers() { VerifyQueryDeserialization( "$filter=UnitPrice le -5.00", "Where(Param_0 => (Param_0.UnitPrice <= -5.00))"); } [Theory] [InlineData("DateTimeProp eq datetime'2000-12-12T12:00'", "Where(Param_0 => (Param_0.DateTimeProp == 12/12/2000 12:00:00 PM))")] [InlineData("DateTimeOffsetProp eq datetimeoffset'2002-10-10T17:00:00Z'", "Where(Param_0 => (Param_0.DateTimeOffsetProp == 10/10/2002 5:00:00 PM +00:00))")] [InlineData("DateTimeOffsetProp eq DateTimeOffsetProp", "Where(Param_0 => (Param_0.DateTimeOffsetProp == Param_0.DateTimeOffsetProp))")] [InlineData("DateTimeOffsetProp ne DateTimeOffsetProp", "Where(Param_0 => (Param_0.DateTimeOffsetProp != Param_0.DateTimeOffsetProp))")] [InlineData("DateTimeOffsetProp ge DateTimeOffsetProp", "Where(Param_0 => (Param_0.DateTimeOffsetProp >= Param_0.DateTimeOffsetProp))")] [InlineData("DateTimeOffsetProp le DateTimeOffsetProp", "Where(Param_0 => (Param_0.DateTimeOffsetProp <= Param_0.DateTimeOffsetProp))")] public void DateInEqualities(string clause, string expectedExpression) { VerifyQueryDeserialization( "$filter=" + clause, expectedExpression); } #endregion #region Logical Operators [Fact] public void OrOperator() { VerifyQueryDeserialization( "$filter=UnitPrice eq 5.00 or UnitPrice eq 10.00", "Where(Param_0 => ((Param_0.UnitPrice == 5.00) OrElse (Param_0.UnitPrice == 10.00)))"); } [Fact] public void AndOperator() { VerifyQueryDeserialization( "$filter=UnitPrice eq 5.00 and UnitPrice eq 10.00", "Where(Param_0 => ((Param_0.UnitPrice == 5.00) AndAlso (Param_0.UnitPrice == 10.00)))"); } [Fact] public void Negation() { VerifyQueryDeserialization( "$filter=not (UnitPrice eq 5.00)", "Where(Param_0 => Not((Param_0.UnitPrice == 5.00)))"); } [Fact] public void BoolNegation() { VerifyQueryDeserialization( "$filter=not Discontinued", "Where(Param_0 => Not(Param_0.Discontinued))"); } [Fact] public void NestedNegation() { VerifyQueryDeserialization( "$filter=not (not(not (Discontinued)))", "Where(Param_0 => Not(Not(Not(Param_0.Discontinued))))"); } #endregion #region Arithmetic Operators [Fact] public void Subtraction() { VerifyQueryDeserialization( "$filter=UnitPrice sub 1.00 lt 5.00", "Where(Param_0 => ((Param_0.UnitPrice - 1.00) < 5.00))"); } [Fact] public void Addition() { VerifyQueryDeserialization( "$filter=UnitPrice add 1.00 lt 5.00", "Where(Param_0 => ((Param_0.UnitPrice + 1.00) < 5.00))"); } [Fact] public void Multiplication() { VerifyQueryDeserialization( "$filter=UnitPrice mul 1.00 lt 5.00", "Where(Param_0 => ((Param_0.UnitPrice * 1.00) < 5.00))"); } [Fact] public void Division() { VerifyQueryDeserialization( "$filter=UnitPrice div 1.00 lt 5.00", "Where(Param_0 => ((Param_0.UnitPrice / 1.00) < 5.00))"); } [Fact] public void Modulo() { VerifyQueryDeserialization( "$filter=UnitPrice mod 1.00 lt 5.00", "Where(Param_0 => ((Param_0.UnitPrice % 1.00) < 5.00))"); } #endregion [Fact] public void Grouping() { VerifyQueryDeserialization( "$filter=((ProductName ne 'Doritos') or (UnitPrice lt 5.00))", "Where(Param_0 => ((Param_0.ProductName != \"Doritos\") OrElse (Param_0.UnitPrice < 5.00)))"); } [Fact] public void MemberExpressions() { VerifyQueryDeserialization( "$filter=Category/CategoryName eq 'Snacks'", "Where(Param_0 => (Param_0.Category.CategoryName == \"Snacks\"))"); } #region String Functions [Fact] public void StringSubstringOf() { // In OData, the order of parameters is actually reversed in the resulting // string.Contains expression VerifyQueryDeserialization( "$filter=substringof('Abc', ProductName) eq true", "Where(Param_0 => (Param_0.ProductName.Contains(\"Abc\") == True))"); VerifyQueryDeserialization( "$filter=substringof(ProductName, 'Abc') eq true", "Where(Param_0 => (\"Abc\".Contains(Param_0.ProductName) == True))"); } [Fact] public void StringStartsWith() { VerifyQueryDeserialization( "$filter=startswith(ProductName, 'Abc') eq true", "Where(Param_0 => (Param_0.ProductName.StartsWith(\"Abc\") == True))"); } [Fact] public void StringEndsWith() { VerifyQueryDeserialization( "$filter=endswith(ProductName, 'Abc') eq true", "Where(Param_0 => (Param_0.ProductName.EndsWith(\"Abc\") == True))"); } [Fact] public void StringLength() { VerifyQueryDeserialization( "$filter=length(ProductName) gt 0", "Where(Param_0 => (Param_0.ProductName.Length > 0))"); } [Fact] public void StringIndexOf() { VerifyQueryDeserialization( "$filter=indexof(ProductName, 'Abc') eq 5", "Where(Param_0 => (Param_0.ProductName.IndexOf(\"Abc\") == 5))"); } [Fact] public void StringReplace() { VerifyQueryDeserialization( "$filter=replace(ProductName, 'Abc', 'Def') eq 'FooDef'", "Where(Param_0 => (Param_0.ProductName.Replace(\"Abc\", \"Def\") == \"FooDef\"))"); } [Fact] public void StringSubstring() { VerifyQueryDeserialization( "$filter=substring(ProductName, 3) eq 'uctName'", "Where(Param_0 => (Param_0.ProductName.Substring(3) == \"uctName\"))"); VerifyQueryDeserialization( "$filter=substring(ProductName, 3, 4) eq 'uctN'", "Where(Param_0 => (Param_0.ProductName.Substring(3, 4) == \"uctN\"))"); } [Fact] public void StringToLower() { VerifyQueryDeserialization( "$filter=tolower(ProductName) eq 'tasty treats'", "Where(Param_0 => (Param_0.ProductName.ToLower() == \"tasty treats\"))"); } [Fact] public void StringToUpper() { VerifyQueryDeserialization( "$filter=toupper(ProductName) eq 'TASTY TREATS'", "Where(Param_0 => (Param_0.ProductName.ToUpper() == \"TASTY TREATS\"))"); } [Fact] public void StringTrim() { VerifyQueryDeserialization( "$filter=trim(ProductName) eq 'Tasty Treats'", "Where(Param_0 => (Param_0.ProductName.Trim() == \"Tasty Treats\"))"); } [Fact] public void StringConcat() { VerifyQueryDeserialization( "$filter=concat('Foo', 'Bar') eq 'FooBar'", "Where(Param_0 => (Concat(\"Foo\", \"Bar\") == \"FooBar\"))"); } #endregion #region Date Functions [Fact] public void DateDay() { VerifyQueryDeserialization( "$filter=day(DiscontinuedDate) eq 8", "Where(Param_0 => (Param_0.DiscontinuedDate.Day == 8))"); } [Fact] public void DateMonth() { VerifyQueryDeserialization( "$filter=month(DiscontinuedDate) eq 8", "Where(Param_0 => (Param_0.DiscontinuedDate.Month == 8))"); } [Fact] public void DateYear() { VerifyQueryDeserialization( "$filter=year(DiscontinuedDate) eq 1974", "Where(Param_0 => (Param_0.DiscontinuedDate.Year == 1974))"); } [Fact] public void DateHour() { VerifyQueryDeserialization("$filter=hour(DiscontinuedDate) eq 8", "Where(Param_0 => (Param_0.DiscontinuedDate.Hour == 8))"); } [Fact] public void DateMinute() { VerifyQueryDeserialization( "$filter=minute(DiscontinuedDate) eq 12", "Where(Param_0 => (Param_0.DiscontinuedDate.Minute == 12))"); } [Fact] public void DateSecond() { VerifyQueryDeserialization( "$filter=second(DiscontinuedDate) eq 33", "Where(Param_0 => (Param_0.DiscontinuedDate.Second == 33))"); } #endregion #region Math Functions [Fact] public void MathRound() { VerifyQueryDeserialization( "$filter=round(UnitPrice) gt 5.00", "Where(Param_0 => (Round(Param_0.UnitPrice) > 5.00))"); } [Fact] public void MathFloor() { VerifyQueryDeserialization( "$filter=floor(UnitPrice) eq 5", "Where(Param_0 => (Floor(Param_0.UnitPrice) == 5))"); } [Fact] public void MathCeiling() { VerifyQueryDeserialization( "$filter=ceiling(UnitPrice) eq 5", "Where(Param_0 => (Ceiling(Param_0.UnitPrice) == 5))"); } #endregion #region Data Types [Fact] public void GuidExpression() { VerifyQueryDeserialization( "$filter=GuidProp eq guid'0EFDAECF-A9F0-42F3-A384-1295917AF95E'", "Where(Param_0 => (Param_0.GuidProp == 0efdaecf-a9f0-42f3-a384-1295917af95e))"); // verify case insensitivity VerifyQueryDeserialization( "$filter=GuidProp eq GuiD'0EFDAECF-A9F0-42F3-A384-1295917AF95E'", "Where(Param_0 => (Param_0.GuidProp == 0efdaecf-a9f0-42f3-a384-1295917af95e))"); } [Fact] public void DateTimeExpression() { VerifyQueryDeserialization( "$filter=DateTimeProp lt datetime'2000-12-12T12:00'", "Where(Param_0 => (Param_0.DateTimeProp < 12/12/2000 12:00:00 PM))"); } [Fact] public void DateTimeOffsetExpression() { VerifyQueryDeserialization( "$filter=DateTimeOffsetProp ge datetimeoffset'2002-10-10T17:00:00Z'", "Where(Param_0 => (Param_0.DateTimeOffsetProp >= 10/10/2002 5:00:00 PM +00:00))"); } [Fact] public void TimeExpression() { VerifyQueryDeserialization( "$filter=TimeSpanProp ge time'13:20:00'", "Where(Param_0 => (Param_0.TimeSpanProp >= 13:20:00))"); // verify parse error for invalid literal format Assert.Throws(delegate { VerifyQueryDeserialization("$filter=TimeSpanProp ge time'invalid'", String.Empty); }, "Parse error in $filter. String was not recognized as a valid TimeSpan. (at index 20)"); } [Fact] public void BinaryExpression() { string binary = "23ABFF"; byte[] bytes = new byte[] { byte.Parse("23", Globalization.NumberStyles.HexNumber), byte.Parse("AB", Globalization.NumberStyles.HexNumber), byte.Parse("FF", Globalization.NumberStyles.HexNumber) }; VerifyQueryDeserialization( String.Format("$filter=ByteArrayProp eq binary'{0}'", binary), "Where(Param_0 => (Param_0.ByteArrayProp == value(System.Byte[])))", q => { // verify that the binary data was deserialized into a constant expression of type byte[] LambdaExpression lex = (LambdaExpression)((UnaryExpression)((MethodCallExpression)q.Expression).Arguments[1]).Operand; BinaryExpression bex = (BinaryExpression)lex.Body; byte[] actualBytes = (byte[])((ConstantExpression)bex.Right).Value; Assert.True(actualBytes.SequenceEqual(bytes)); }); // test alternate 'X' syntax VerifyQueryDeserialization( String.Format("$filter=ByteArrayProp eq X'{0}'", binary), "Where(Param_0 => (Param_0.ByteArrayProp == value(System.Byte[])))", q => { // verify that the binary data was deserialized into a constant expression of type byte[] LambdaExpression lex = (LambdaExpression)((UnaryExpression)((MethodCallExpression)q.Expression).Arguments[1]).Operand; BinaryExpression bex = (BinaryExpression)lex.Body; byte[] actualBytes = (byte[])((ConstantExpression)bex.Right).Value; Assert.True(actualBytes.SequenceEqual(bytes)); }); // verify parse error for invalid literal format Assert.Throws(delegate { VerifyQueryDeserialization( String.Format("$filter=ByteArrayProp eq binary'{0}'", "WXYZ"), String.Empty); }, "Parse error in $filter. Input string was not in a correct format. (at index 23)"); // verify parse error for invalid hex literal (odd hex strings are not supported) Assert.Throws(delegate { VerifyQueryDeserialization( String.Format("$filter=ByteArrayProp eq binary'23A'", "XYZ"), String.Empty); }, "Parse error in $filter. Invalid hexadecimal literal. (at index 23)"); } [Fact] public void IntegerLiteralSuffix() { // long L VerifyQueryDeserialization( "$filter=LongProp lt 987654321L and LongProp gt 123456789l", "Where(Param_0 => ((Param_0.LongProp < 987654321) AndAlso (Param_0.LongProp > 123456789)))"); VerifyQueryDeserialization( "$filter=LongProp lt -987654321L and LongProp gt -123456789l", "Where(Param_0 => ((Param_0.LongProp < -987654321) AndAlso (Param_0.LongProp > -123456789)))"); } [Fact] public void RealLiteralSuffixes() { // Float F VerifyQueryDeserialization( "$filter=FloatProp lt 4321.56F and FloatProp gt 1234.56f", "Where(Param_0 => ((Param_0.FloatProp < 4321.56) AndAlso (Param_0.FloatProp > 1234.56)))"); // Decimal M VerifyQueryDeserialization( "$filter=DecimalProp lt 4321.56M and DecimalProp gt 1234.56m", "Where(Param_0 => ((Param_0.DecimalProp < 4321.56) AndAlso (Param_0.DecimalProp > 1234.56)))"); } [Theory] [InlineData("'hello,world'", "hello,world")] [InlineData("'''hello,world'", "'hello,world")] [InlineData("'hello,world'''", "hello,world'")] [InlineData("'hello,''wor''ld'", "hello,'wor'ld")] [InlineData("'hello,''''''world'", "hello,'''world")] [InlineData("'\"hello,world\"'", "\"hello,world\"")] [InlineData("'\"hello,world'", "\"hello,world")] [InlineData("'hello,world\"'", "hello,world\"")] [InlineData("'hello,\"world'", "hello,\"world")] [InlineData("'México D.F.'", "México D.F.")] public void StringLiterals(string literal, string expected) { VerifyQueryDeserialization( "$filter=ProductName eq " + literal, String.Format("Where(Param_0 => (Param_0.ProductName == \"{0}\"))", expected)); } #endregion #region Negative tests [Theory] [InlineData('+')] [InlineData('*')] [InlineData('%')] [InlineData('[')] [InlineData(']')] public void InvalidCharactersInQuery_TokenizerFails(char ch) { Assert.Throws(delegate { string filter = String.Format("2 {0} 3", ch); VerifyQueryDeserialization("$filter=" + Uri.EscapeDataString(filter), String.Empty); }, String.Format("Parse error in $filter. Syntax error '{0}' (at index 2)", ch)); } [Fact] public void InvalidTypeCreationExpression() { // underminated string literal Assert.Throws(delegate { VerifyQueryDeserialization("$filter=TimeSpanProp ge time'13:20:00", String.Empty); }, "Parse error in $filter. Unterminated string literal (at index 29)"); // use of parens rather than quotes Assert.Throws(delegate { VerifyQueryDeserialization("$filter=TimeSpanProp ge time(13:20:00)", String.Empty); }, "Parse error in $filter. Invalid 'time' type creation expression. (at index 16)"); // verify the exception returned when type expression that isn't // one of the supported keyword types is used. In this case it falls // through as a member expression Assert.Throws(delegate { VerifyQueryDeserialization("$filter=math'123' eq true", String.Empty); }, "Parse error in $filter. No property or field 'math' exists in type 'Product' (at index 0)"); } [Fact] public void InvalidMethodCall() { // incorrect casing of supported method Assert.Throws(delegate { VerifyQueryDeserialization("$filter=Startswith(ProductName, 'Abc') eq true", String.Empty); }, "Parse error in $filter. Unknown identifier 'Startswith' (at index 0)"); // attempt to access a method defined on the entity type Assert.Throws(delegate { VerifyQueryDeserialization("$filter=Inaccessable() eq \"Bar\"", String.Empty); }, "Parse error in $filter. Unknown identifier 'Inaccessable' (at index 0)"); // verify that Type methods like string.PadLeft, etc. are not supported. Assert.Throws(delegate { VerifyQueryDeserialization("$filter=ProductName/PadLeft(100000000000000000000) eq \"Foo\"", String.Empty); }, "Parse error in $filter. Unknown identifier 'PadLeft' (at index 12)"); } [Fact] public void InvalidQueryParameterToTop() { Assert.Throws( () => VerifyQueryDeserialization("$top=-42", String.Empty), "The OData query parameter '$top' has an invalid value. The value should be a positive integer. The provided value was '-42'"); } [Fact] public void InvalidQueryParameterToSkip() { Assert.Throws( () => VerifyQueryDeserialization("$skip=-42", String.Empty), "The OData query parameter '$skip' has an invalid value. The value should be a positive integer. The provided value was '-42'"); } [Fact] public void InvalidProperty_NotExists() { Assert.Throws( () => VerifyQueryDeserialization("$filter=length mod n(ProductName) eq 12", String.Empty), "Parse error in $filter. No property or field 'length' exists in type 'Product' (at index 0)"); } [Fact] public void InvalidFunctionCall_EmptyArguments() { Assert.Throws( () => VerifyQueryDeserialization("$filter=length() eq 12", String.Empty), "Parse error in $filter. No applicable method 'Length' exists in type 'System.String' (at index 0)"); } [Theory] [InlineData("(2 add 3 eq 2", 13)] [InlineData("(2 add (3) eq 2", 15)] [InlineData("(((( 2 eq 2", 11)] public void Missing_Parantheses(string clause, int index) { Assert.Throws( () => VerifyQueryDeserialization("$filter=" + clause, String.Empty), String.Format("Parse error in $filter. ')' or operator expected (at index {0})", index)); } [Theory] [InlineData("'hello,world", 12)] [InlineData("'''hello,world", 14)] [InlineData("'hello,world''", 14)] [InlineData("'hello,''wor''ld", 16)] public void UnterminatedStringLiterals(string clause, int index) { Assert.Throws( () => VerifyQueryDeserialization("$filter=" + clause, String.Empty), String.Format("Parse error in $filter. Unterminated string literal (at index {0})", index)); } [Theory] [InlineData("\"hello,world\"", 0)] public void InvalidStringLiterals(string clause, int index) { Assert.Throws( () => VerifyQueryDeserialization("$filter=" + clause, String.Empty), String.Format("Parse error in $filter. Syntax error '\"' (at index {0})", index)); } #endregion [Fact(DisplayName = "ODataQueryDeserializer is internal.")] public void TypeIsCorrect() { Assert.Type.HasProperties(typeof(ODataQueryDeserializer), TypeAssert.TypeProperties.IsStatic | TypeAssert.TypeProperties.IsClass); } /// /// Call the query deserializer and verify the results /// /// The URL query string to deserialize (e.g. $filter=ProductName eq 'Doritos') /// The Expression.ToString() representation of the expected result (e.g. Where(Param_0 => (Param_0.ProductName == \"Doritos\")) private void VerifyQueryDeserialization(string queryString, string expectedResult) { VerifyQueryDeserialization(queryString, expectedResult, null); } private void VerifyQueryDeserialization(string queryString, string expectedResult) { VerifyQueryDeserialization(queryString, expectedResult, null); } private void VerifyQueryDeserialization(string queryString, string expectedResult, Action> verify) { string uri = "http://myhost/odata.svc/Get?" + queryString; IQueryable baseQuery = new T[0].AsQueryable(); IQueryable resultQuery = (IQueryable)ODataQueryDeserializer.Deserialize(baseQuery, new Uri(uri)); VerifyExpression(resultQuery, expectedResult); if (verify != null) { verify(resultQuery); } QueryValidator.Instance.Validate(resultQuery); } private void VerifyExpression(IQueryable query, string expectedExpression) { // strip off the beginning part of the expression to get to the first // actual query operator string resultExpression = query.Expression.ToString(); int startIdx = (query.ElementType.FullName + "[]").Length + 1; resultExpression = resultExpression.Substring(startIdx); Assert.True(resultExpression == expectedExpression, String.Format("Expected expression '{0}' but the deserializer produced '{1}'", expectedExpression, resultExpression)); } } }