diff --git a/test/EndToEndTests/Common/Microsoft.OData.Client.E2E.TestCommon/Helpers/ODataValueAssertEqualHelper.cs b/test/EndToEndTests/Common/Microsoft.OData.Client.E2E.TestCommon/Helpers/ODataValueAssertEqualHelper.cs new file mode 100644 index 0000000000..b2043f7b3d --- /dev/null +++ b/test/EndToEndTests/Common/Microsoft.OData.Client.E2E.TestCommon/Helpers/ODataValueAssertEqualHelper.cs @@ -0,0 +1,123 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +using Xunit; + +namespace Microsoft.OData.Client.E2E.TestCommon.Helpers; + +public static class ODataValueAssertEqualHelper +{ + #region Util methods to AssertEqual ODataValues + + public static void AssertODataValueEqual(ODataValue expected, ODataValue actual) + { + ODataPrimitiveValue expectedPrimitiveValue = expected as ODataPrimitiveValue; + ODataPrimitiveValue actualPrimitiveValue = actual as ODataPrimitiveValue; + if (expectedPrimitiveValue != null && actualPrimitiveValue != null) + { + AssertODataPrimitiveValueEqual(expectedPrimitiveValue, actualPrimitiveValue); + } + else + { + ODataEnumValue expectedEnumValue = expected as ODataEnumValue; + ODataEnumValue actualEnumValue = actual as ODataEnumValue; + if (expectedEnumValue != null && actualEnumValue != null) + { + AssertODataEnumValueEqual(expectedEnumValue, actualEnumValue); + } + else + { + ODataCollectionValue expectedCollectionValue = (ODataCollectionValue)expected; + ODataCollectionValue actualCollectionValue = (ODataCollectionValue)actual; + AssertODataCollectionValueEqual(expectedCollectionValue, actualCollectionValue); + } + } + } + + private static void AssertODataCollectionValueEqual(ODataCollectionValue expectedCollectionValue, ODataCollectionValue actualCollectionValue) + { + Assert.NotNull(expectedCollectionValue); + Assert.NotNull(actualCollectionValue); + Assert.Equal(expectedCollectionValue.TypeName, actualCollectionValue.TypeName); + var expectedItemsArray = expectedCollectionValue.Items.OfType().ToArray(); + var actualItemsArray = actualCollectionValue.Items.OfType().ToArray(); + + Assert.Equal(expectedItemsArray.Length, actualItemsArray.Length); + for (int i = 0; i < expectedItemsArray.Length; i++) + { + var expectedOdataValue = expectedItemsArray[i] as ODataValue; + var actualOdataValue = actualItemsArray[i] as ODataValue; + if (expectedOdataValue != null && actualOdataValue != null) + { + AssertODataValueEqual(expectedOdataValue, actualOdataValue); + } + else + { + Assert.Equal(expectedItemsArray[i], actualItemsArray[i]); + } + } + } + + public static void AssertODataPropertiesEqual(IEnumerable expectedProperties, IEnumerable actualProperties) + { + if (expectedProperties == null && actualProperties == null) + { + return; + } + + Assert.NotNull(expectedProperties); + Assert.NotNull(actualProperties); + var expectedPropertyArray = expectedProperties.ToArray(); + var actualPropertyArray = actualProperties.ToArray(); + Assert.Equal(expectedPropertyArray.Length, actualPropertyArray.Length); + for (int i = 0; i < expectedPropertyArray.Length; i++) + { + AssertODataPropertyEqual(expectedPropertyArray[i], actualPropertyArray[i]); + } + } + + public static void AssertODataPropertyEqual(ODataProperty expectedOdataProperty, ODataProperty actualOdataProperty) + { + Assert.NotNull(expectedOdataProperty); + Assert.NotNull(actualOdataProperty); + Assert.Equal(expectedOdataProperty.Name, actualOdataProperty.Name); + AssertODataValueEqual(ToODataValue(expectedOdataProperty.Value), ToODataValue(actualOdataProperty.Value)); + } + + private static ODataValue ToODataValue(object value) + { + if (value == null) + { + return new ODataNullValue(); + } + + var odataValue = value as ODataValue; + if (odataValue != null) + { + return odataValue; + } + + return new ODataPrimitiveValue(value); + } + + private static void AssertODataPrimitiveValueEqual(ODataPrimitiveValue expectedPrimitiveValue, ODataPrimitiveValue actualPrimitiveValue) + { + Assert.NotNull(expectedPrimitiveValue); + Assert.NotNull(actualPrimitiveValue); + Assert.Equal(expectedPrimitiveValue.Value, actualPrimitiveValue.Value); + } + + private static void AssertODataEnumValueEqual(ODataEnumValue expectedEnumValue, ODataEnumValue actualEnumValue) + { + Assert.NotNull(expectedEnumValue); + Assert.NotNull(actualEnumValue); + Assert.Equal(expectedEnumValue.Value, actualEnumValue.Value); + Assert.Equal(expectedEnumValue.TypeName, actualEnumValue.TypeName); + } + + #endregion Util methods to AssertEqual ODataValues + +} diff --git a/test/EndToEndTests/Common/Microsoft.OData.Client.E2E.TestCommon/Logs/LogAssertTraceListener.cs b/test/EndToEndTests/Common/Microsoft.OData.Client.E2E.TestCommon/Logs/LogAssertTraceListener.cs new file mode 100644 index 0000000000..f5ccbc6ce4 --- /dev/null +++ b/test/EndToEndTests/Common/Microsoft.OData.Client.E2E.TestCommon/Logs/LogAssertTraceListener.cs @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Diagnostics; + +namespace Microsoft.OData.Client.E2E.TestCommon.Logs; + +public class LogAssertTraceListener : TraceListener +{ + public LogAssertTraceListener() + { + // Clear existing listeners and add this listener + Trace.Listeners.Clear(); + Trace.Listeners.Add(this); + } + + public override void Write(string? message) { } + + public override void WriteLine(string? message) { } + + public override void Fail(string? message, string? detailMessage) + { + // Log the assertion failure + Console.WriteLine($"DEBUG ASSERTION FAILED: {message} {detailMessage}"); + } +} + diff --git a/test/EndToEndTests/Common/Microsoft.OData.Client.E2E.TestCommon/Microsoft.OData.Client.E2E.TestCommon.csproj b/test/EndToEndTests/Common/Microsoft.OData.Client.E2E.TestCommon/Microsoft.OData.Client.E2E.TestCommon.csproj index 260ea0cec5..3becb49367 100644 --- a/test/EndToEndTests/Common/Microsoft.OData.Client.E2E.TestCommon/Microsoft.OData.Client.E2E.TestCommon.csproj +++ b/test/EndToEndTests/Common/Microsoft.OData.Client.E2E.TestCommon/Microsoft.OData.Client.E2E.TestCommon.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/test/EndToEndTests/Common/Microsoft.OData.Client.E2E.TestCommon/TestStartupBase.cs b/test/EndToEndTests/Common/Microsoft.OData.Client.E2E.TestCommon/TestStartupBase.cs index 992dbc77e0..a22c18cef3 100644 --- a/test/EndToEndTests/Common/Microsoft.OData.Client.E2E.TestCommon/TestStartupBase.cs +++ b/test/EndToEndTests/Common/Microsoft.OData.Client.E2E.TestCommon/TestStartupBase.cs @@ -25,6 +25,8 @@ public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env) { ConfigureBeforeRouting(app, env); + app.UseODataRouteDebug(); + app.UseODataBatching(); app.UseRouting(); diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Server/Default/DefaultDataSource.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Server/Default/DefaultDataSource.cs index 1c881cf620..76d26f4405 100644 --- a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Server/Default/DefaultDataSource.cs +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Server/Default/DefaultDataSource.cs @@ -435,13 +435,20 @@ public void Initialize() }, ]; - this.ProductDetails[0].Reviews = [this.ProductReviews[0]]; - this.ProductDetails[0].Reviews = [this.ProductReviews[1]]; - this.ProductDetails[0].Reviews = [this.ProductReviews[2]]; - this.ProductDetails[0].Reviews = [this.ProductReviews[3]]; - this.ProductDetails[1].Reviews = [this.ProductReviews[1]]; - this.ProductDetails[1].Reviews = [this.ProductReviews[2]]; - this.ProductDetails[1].Reviews = [this.ProductReviews[3]]; + this.ProductDetails[0].Reviews = new List + { + this.ProductReviews[0], + this.ProductReviews[1], + this.ProductReviews[2], + this.ProductReviews[3] + }; + + this.ProductDetails[1].Reviews = new List + { + this.ProductReviews[1], + this.ProductReviews[2], + this.ProductReviews[3] + }; this.Calendars = new List() { diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Microsoft.OData.Client.E2E.Tests.csproj b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Microsoft.OData.Client.E2E.Tests.csproj index 4718705b5f..ab8a5503fe 100644 --- a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Microsoft.OData.Client.E2E.Tests.csproj +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Microsoft.OData.Client.E2E.Tests.csproj @@ -40,8 +40,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Server/QueryOptionTestsController.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Server/QueryOptionTestsController.cs new file mode 100644 index 0000000000..7102760344 --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Server/QueryOptionTestsController.cs @@ -0,0 +1,118 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OData.Query; +using Microsoft.AspNetCore.OData.Routing.Controllers; +using Microsoft.OData.Client.E2E.Tests.Common.Server.Default; + +namespace Microsoft.OData.Client.E2E.Tests.QueryOptionTests.Server +{ + public class QueryOptionTestsController : ODataController + { + private static DefaultDataSource _dataSource; + + [EnableQuery] + [HttpGet("odata/Products")] + public IActionResult GetProducts() + { + var products = _dataSource.Products; + + return Ok(products); + } + + [EnableQuery] + [HttpGet("odata/Products({key})")] + public IActionResult GetProduct([FromRoute] int key) + { + var product = _dataSource.Products?.SingleOrDefault(a => a.ProductID == key); + + if (product == null) + { + return NotFound(); + } + + return Ok(product); + } + + [EnableQuery] + [HttpGet("odata/People")] + public IActionResult GetPeople() + { + var people = _dataSource.People; + + return Ok(people); + } + + [EnableQuery] + [HttpGet("odata/People({key})")] + public IActionResult GetPerson([FromRoute] int key) + { + var person = _dataSource.People?.SingleOrDefault(a => a.PersonID == key); + + if (person == null) + { + return NotFound(); + } + + return Ok(person); + } + + [EnableQuery] + [HttpGet("odata/Customers")] + public IActionResult GetCustomers() + { + var customers = _dataSource.Customers; + + return Ok(customers); + } + + [EnableQuery] + [HttpGet("odata/Customers({key})")] + public IActionResult GetCustomer([FromRoute] int key) + { + var customer = _dataSource.Customers?.SingleOrDefault(a => a.PersonID == key); + + if (customer == null) + { + return NotFound(); + } + + return Ok(customer); + } + + [EnableQuery] + [HttpGet("odata/Customers({key})/Numbers")] + public IActionResult GetNumbersFromCustomer([FromRoute] int key) + { + var customer = _dataSource.Customers?.SingleOrDefault(a => a.PersonID == key); + + if (customer == null) + { + return NotFound(); + } + + return Ok(customer.Numbers); + } + + [EnableQuery] + [HttpGet("odata/ProductDetails")] + public IActionResult GetProductDetails() + { + var productDetails = _dataSource.ProductDetails; + + return Ok(productDetails); + } + + [HttpPost("odata/queryoption/Default.ResetDefaultDataSource")] + public IActionResult ResetDefaultDataSource() + { + _dataSource = DefaultDataSource.CreateInstance(); + + return Ok(); + } + } +} diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Tests/CustomSearchBinder.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Tests/CustomSearchBinder.cs new file mode 100644 index 0000000000..1544bd2692 --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Tests/CustomSearchBinder.cs @@ -0,0 +1,103 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.AspNetCore.OData.Query.Expressions; +using Microsoft.OData.Client.E2E.Tests.Common.Server.Default; +using Microsoft.OData.UriParser; + +namespace Microsoft.OData.Client.E2E.Tests.QueryOptionTests.Tests; + +public class CustomSearchBinder : QueryBinder, ISearchBinder +{ + internal readonly MethodInfo StringEqualsMethodInfo = + typeof(string).GetMethod("Equals", new[] + { + typeof(string), + typeof(string), + typeof(StringComparison) + })!; + + internal readonly MethodInfo StringContainsMethodInfo = + typeof(string).GetMethod("Contains", new[] { typeof(string), typeof(StringComparison) })!; + + + public Expression BindSearch(SearchClause searchClause, QueryBinderContext context) + { + Expression exp = BindSingleValueNode(searchClause.Expression, context); + + LambdaExpression lambdaExp = Expression.Lambda(exp, context.CurrentParameter); + + return lambdaExp; + } + + public override Expression BindSingleValueNode(SingleValueNode node, QueryBinderContext context) + { + switch (node.Kind) + { + case QueryNodeKind.BinaryOperator: + return BindBinaryOperatorNode(node as BinaryOperatorNode, context); + + case QueryNodeKind.SearchTerm: + return BindSearchTerm((SearchTermNode)node, context); + + case QueryNodeKind.UnaryOperator: + return BindUnaryOperatorNode(node as UnaryOperatorNode, context); + } + + return null; + } + + public Expression BindSearchTerm(SearchTermNode node, QueryBinderContext context) + { + // Source is from context; + Expression source = context.CurrentParameter; + string text = node.Text.ToLowerInvariant(); + + if (context.ElementClrType == typeof(ProductReview)) + { + // $it.Comment + Expression commentProperty = Expression.Property(source, "Comment"); + + // $it.Comment.ToString() + Expression commentPropertyString = Expression.Call(commentProperty, "ToString", typeArguments: null, arguments: null); + + // string.Contains($it.Comment.ToString(), text, StringComparison.OrdinalIgnoreCase) + return Expression.Call(commentPropertyString, StringContainsMethodInfo, + Expression.Constant(text, typeof(string)), Expression.Constant(StringComparison.OrdinalIgnoreCase, typeof(StringComparison))); + } + + if (context.ElementClrType == typeof(ProductDetail)) + { + // $it.Description + Expression descriptionProperty = Expression.Property(source, "Description"); + + // $it.Description.ToString() + Expression descriptionPropertyString = Expression.Call(descriptionProperty, "ToString", typeArguments: null, arguments: null); + + // string.Contains($it.Description.ToString(), text, StringComparison.OrdinalIgnoreCase) + return Expression.Call(descriptionPropertyString, StringContainsMethodInfo, + Expression.Constant(text, typeof(string)), Expression.Constant(StringComparison.OrdinalIgnoreCase, typeof(StringComparison))); + } + + return Expression.Constant(false, typeof(bool)); + } + + private Expression CombineContainsAndEquals(Expression propertyString, string text) + { + // string.Contains(propertyString, text, StringComparison.OrdinalIgnoreCase) + Expression containsExpression = Expression.Call(propertyString, StringContainsMethodInfo, + Expression.Constant(text, typeof(string)), Expression.Constant(StringComparison.OrdinalIgnoreCase, typeof(StringComparison))); + + // string.Equals(propertyString, text, StringComparison.OrdinalIgnoreCase) + Expression equalsExpression = Expression.Call(propertyString, StringEqualsMethodInfo, + Expression.Constant(text, typeof(string)), Expression.Constant(StringComparison.OrdinalIgnoreCase, typeof(StringComparison))); + + // Combine Contains and Equals using OR + return Expression.OrElse(containsExpression, equalsExpression); + } +} diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Tests/ExpandQueryOptionTests.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Tests/ExpandQueryOptionTests.cs new file mode 100644 index 0000000000..a58ffe24d0 --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Tests/ExpandQueryOptionTests.cs @@ -0,0 +1,299 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +using Microsoft.AspNetCore.OData; +using Microsoft.AspNetCore.OData.Query.Expressions; +using Microsoft.AspNetCore.OData.Routing.Controllers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OData.Client.E2E.TestCommon; +using Microsoft.OData.Client.E2E.TestCommon.Common; +using Microsoft.OData.Client.E2E.Tests.Common.Client.Default.Default; +using Microsoft.OData.Client.E2E.Tests.Common.Server.Default; +using Microsoft.OData.Client.E2E.Tests.QueryOptionTests.Server; +using Microsoft.OData.Edm; +using Xunit; + +namespace Microsoft.OData.Client.E2E.Tests.QueryOptionTests.Tests; + +public class ExpandQueryOptionTests : EndToEndTestBase +{ + private readonly Uri _baseUri; + private readonly Container _context; + private readonly IEdmModel _model; + + public class TestsStartup : TestStartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(QueryOptionTestsController), typeof(MetadataController)); + + services.AddControllers().AddOData(opt => + opt.EnableQueryFeatures().AddRouteComponents("odata", DefaultEdmModel.GetEdmModel())); + } + } + + public ExpandQueryOptionTests(TestWebApplicationFactory fixture) + : base(fixture) + { + if (Client.BaseAddress == null) + { + throw new ArgumentNullException(nameof(Client.BaseAddress), "Base address cannot be null"); + } + + _baseUri = new Uri(Client.BaseAddress, "odata/"); + + _context = new Container(_baseUri) + { + HttpClientFactory = HttpClientFactory + }; + + _model = DefaultEdmModel.GetEdmModel(); + ResetDefaultDataSource(); + } + + public static IEnumerable MimeTypesData + { + get + { + yield return new object[] { MimeTypes.ApplicationJson + MimeTypes.ODataParameterFullMetadata }; + yield return new object[] { MimeTypes.ApplicationJson + MimeTypes.ODataParameterMinimalMetadata }; + yield return new object[] { MimeTypes.ApplicationJson + MimeTypes.ODataParameterNoMetadata }; + } + } + + #region $expand and $top Query Options + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task ExpandWithTopQueryOptionTestAsync(string mimeType) + { + // Arrange & Act + var queryText = "Products(5)?$expand=Details($top=3)"; + var entries = await this.TestsHelper.QueryResourceEntriesAsync(queryText, mimeType); + + // Assert + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + var details = entries.FindAll(e => e.Id.AbsoluteUri.Contains("ProductDetails")); + Assert.Equal(3, details.Count); + } + } + + #endregion + + #region $expand and $skip Query Options + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task ExpandWithSkipQueryOptionTestAsync(string mimeType) + { + // Arrange & Act + var queryText = "Products(5)?$expand=Details($skip=2)"; + var entries = await this.TestsHelper.QueryResourceEntriesAsync(queryText, mimeType); + + // Assert + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + var details = entries.FindAll(e => e.Id.AbsoluteUri.Contains("ProductDetails")); + Assert.Equal(3, details.Count); + } + } + + #endregion + + #region $expand and $orderby Query Options + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task ExpandWithOrderByQueryOptionTestAsync(string mimeType) + { + // Arrange & Act + var queryText = "Products(5)?$expand=Details($orderby=Description desc)"; + var entries = await this.TestsHelper.QueryResourceEntriesAsync(queryText, mimeType); + + // Assert + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + var details = entries.FindAll(e => e.Id.AbsoluteUri.Contains("ProductDetails")); + var productIDProperty = details.First().Properties.Single(p => p.Name == "ProductDetailID") as ODataProperty; + var descriptionProperty = details.First().Properties.Single(p => p.Name == "Description") as ODataProperty; + var productNameProperty = details.First().Properties.Single(p => p.Name == "ProductName") as ODataProperty; + + Assert.NotNull(productIDProperty); + Assert.NotNull(descriptionProperty); + Assert.NotNull(productNameProperty); + + Assert.Equal(3, productIDProperty.Value); + Assert.Equal("suger soft drink", descriptionProperty.Value); + Assert.Equal("CokeCola", productNameProperty.Value); + } + } + + #endregion + + #region $expand and $filter Query Options + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task ExpandWithFilterQueryOptionTestAsync(string mimeType) + { + // Arrange & Act + var queryText = "Products(5)?$expand=Details($filter=Description eq 'spicy snack')"; + var entries = await this.TestsHelper.QueryResourceEntriesAsync(queryText, mimeType); + + // Assert + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + var details = entries.FindAll(e => e.Id.AbsoluteUri.Contains("ProductDetails")); + var productIDProperty = details.First().Properties.Single(p => p.Name == "ProductDetailID") as ODataProperty; + var productNameProperty = details.First().Properties.Single(p => p.Name == "ProductName") as ODataProperty; + + Assert.NotNull(productIDProperty); + Assert.NotNull(productNameProperty); + + Assert.Equal(5, productIDProperty.Value); + Assert.Equal("Mustard", productNameProperty.Value); + } + } + + #endregion + + #region $expand and $count Query Options + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task ExpandWithCountQueryOptionTestAsync(string mimeType) + { + // Arrange & Act + var queryText = "Products(5)?$expand=Details($count=true)"; + var feed = await this.TestsHelper.QueryInnerFeedAsync(queryText, mimeType); + + // Assert + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata) && !mimeType.Contains(MimeTypes.ApplicationAtomXml)) + { + Assert.NotNull(feed); + Assert.Equal(5, feed.Count); + } + } + + #endregion + + #region $expand with $ref option + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task ExpandWithRefQueryOptionTestAsync(string mimeType) + { + // Arrange & Act + var queryText = "Products(5)?$expand=Details/$ref"; + var entries = await this.TestsHelper.QueryResourceEntriesAsync(queryText, mimeType); + + // Assert + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + var details = entries.FindAll(e => e.Id.AbsoluteUri.Contains("ProductDetails")); + + Assert.Equal(5, details.Count); + Assert.Contains("ProductDetailID=2", entries.First().Id.ToString()); + } + } + + #endregion + + #region $expand with Nested option on $ref + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task ExpandWithNestedRefQueryOptionTestAsync(string mimeType) + { + // Arrange & Act + var queryText = "Products(5)?$expand=Details/$ref($orderby=Description desc)"; + var entries = await this.TestsHelper.QueryResourceEntriesAsync(queryText, mimeType); + + // Assert + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + var details = entries.FindAll(e => e.Id.AbsoluteUri.Contains("ProductDetails")); + + Assert.Equal(5, details.Count); + Assert.Contains("ProductDetailID=3", entries.First().Id.ToString()); + } + } + + #endregion + + #region $expand query option with $orderby, $skip, $top, and $select nested options. + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task ExpandWithOrderBySkipTopSelectQueryOptionTestAsync(string mimeType) + { + // Arrange & Act + var queryText = "Products(5)?$expand=Details($orderby=Description;$skip=2;$top=1)"; + var entries = await this.TestsHelper.QueryResourceEntriesAsync(queryText, mimeType); + + // Assert + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + var details = entries.FindAll(e => e.Id.AbsoluteUri.Contains("ProductDetails")); + Assert.Single(details); + + var descriptionProperty = details.First().Properties.Single(p => p.Name == "Description") as ODataProperty; + Assert.NotNull(descriptionProperty); + Assert.Equal("fitness drink!", descriptionProperty.Value); + } + } + + #endregion + + #region $expand query option with nested $expand, $filter, and $select options. + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task ExpandWithNestedExpandQueryOptionTestAsync(string mimeType) + { + // Arrange & Act + var queryText = "Products(5)?$expand=Details($expand=Reviews($filter=contains(Comment,'good');$select=Comment))"; + if(mimeType.Contains(MimeTypes.ODataParameterMinimalMetadata)) + { + queryText = "Products(5)?$expand=Details($expand=Reviews($filter=contains(Comment,'good')))"; + } + + var entries = await this.TestsHelper.QueryResourceEntriesAsync(queryText, mimeType); + + // Assert + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + Assert.Equal(8, entries.Count); + var reviews = entries.FindAll(e => e.Id.AbsoluteUri.Contains("ProductReviews")); + Assert.Equal(2, reviews.Count); + + var commentProperty = reviews.First().Properties.Single(p => p.Name == "Comment") as ODataProperty; + Assert.NotNull(commentProperty); + Assert.Equal("Not so good as other brands", commentProperty.Value); + } + } + + #endregion + + #region Private methods + + private QueryOptionTestsHelper TestsHelper + { + get + { + return new QueryOptionTestsHelper(_baseUri, _model, Client); + } + } + + private void ResetDefaultDataSource() + { + var actionUri = new Uri(_baseUri + "queryoption/Default.ResetDefaultDataSource", UriKind.Absolute); + _context.Execute(actionUri, "POST"); + } + + #endregion +} diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Tests/FilterQueryTests.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Tests/FilterQueryTests.cs new file mode 100644 index 0000000000..3b26985905 --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Tests/FilterQueryTests.cs @@ -0,0 +1,158 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +using Microsoft.AspNetCore.OData; +using Microsoft.AspNetCore.OData.Routing.Controllers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OData.Client.E2E.TestCommon; +using Microsoft.OData.Client.E2E.TestCommon.Common; +using Microsoft.OData.Client.E2E.Tests.Common.Client.Default.Default; +using Microsoft.OData.Client.E2E.Tests.Common.Server.Default; +using Microsoft.OData.Client.E2E.Tests.QueryOptionTests.Server; +using Microsoft.OData.Edm; +using Xunit; + +namespace Microsoft.OData.Client.E2E.Tests.QueryOptionTests.Tests; + +public class FilterQueryTests : EndToEndTestBase +{ + private readonly Uri _baseUri; + private readonly Container _context; + private readonly IEdmModel _model; + + public class TestsStartup : TestStartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(QueryOptionTestsController), typeof(MetadataController)); + + services.AddControllers().AddOData(opt => + opt.EnableQueryFeatures().AddRouteComponents("odata", DefaultEdmModel.GetEdmModel())); + } + } + + public FilterQueryTests(TestWebApplicationFactory fixture) : base(fixture) + { + if (Client.BaseAddress == null) + { + throw new ArgumentNullException(nameof(Client.BaseAddress), "Base address cannot be null"); + } + + _baseUri = new Uri(Client.BaseAddress, "odata/"); + + _context = new Container(_baseUri) + { + HttpClientFactory = HttpClientFactory + }; + + _model = DefaultEdmModel.GetEdmModel(); + ResetDefaultDataSource(); + } + + #region $filter with $count collection of primitive type + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task FilterWithCountCollectionOfPrimitiveTypeAsync(string mimeType) + { + // Arrange & Act + var entries = await this.TestsHelper.QueryResourceSetFeedAsync("People?$filter=Emails/$count lt 2", mimeType); + var details = entries.Where(r => r != null && r.Id != null).ToList(); + + // Assert + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + Assert.Equal(4, details.Count); + } + } + + #endregion + + #region $filter with $count collection of enum type + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task FilterWithCountCollectionOfEnumTypeAsync(string mimeType) + { + // Arrange & Act + var entries = await this.TestsHelper.QueryResourceSetFeedAsync("Products?$filter=CoverColors/$count lt 2", mimeType); + var details = entries.Where(r => r != null && r.Id != null).ToList(); + + // Assert + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + Assert.Single(details); + } + } + + #endregion + + #region $filter with $count collection of complex type + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task FilterWithCountCollectionOfComplexTypeAsync(string mimeType) + { + // Arrange & Act + var entries = await this.TestsHelper.QueryResourceSetFeedAsync("People?$filter=Addresses/$count eq 2", mimeType); + var details = entries.Where(r => r != null && r.Id != null).ToList(); + + // Assert + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + Assert.Equal(2, details.Count); + } + } + + #endregion + + #region $filter with $count collection of entity type + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task FilterWithCountCollectionOfEntityTypeAsync(string mimeType) + { + // Arrange & Act + var entries = await this.TestsHelper.QueryResourceSetFeedAsync("Customers?$filter=Orders/$count lt 2", mimeType); + var details = entries.Where(r => r != null && r.Id != null).ToList(); + + // Assert + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + Assert.Single(details); + } + } + + #endregion + + public static IEnumerable MimeTypesData + { + get + { + yield return new object[] { MimeTypes.ApplicationJson + MimeTypes.ODataParameterFullMetadata }; + yield return new object[] { MimeTypes.ApplicationJson + MimeTypes.ODataParameterMinimalMetadata }; + yield return new object[] { MimeTypes.ApplicationJson + MimeTypes.ODataParameterNoMetadata }; + } + } + + #region Private methods + + private QueryOptionTestsHelper TestsHelper + { + get + { + return new QueryOptionTestsHelper(_baseUri, _model, Client); + } + } + + private void ResetDefaultDataSource() + { + var actionUri = new Uri(_baseUri + "queryoption/Default.ResetDefaultDataSource", UriKind.Absolute); + _context.Execute(actionUri, "POST"); + } + + #endregion +} diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Tests/OrderbyQueryTests.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Tests/OrderbyQueryTests.cs new file mode 100644 index 0000000000..889faa695b --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Tests/OrderbyQueryTests.cs @@ -0,0 +1,203 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +using Microsoft.AspNetCore.OData; +using Microsoft.AspNetCore.OData.Routing.Controllers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OData.Client.E2E.TestCommon; +using Microsoft.OData.Client.E2E.TestCommon.Common; +using Microsoft.OData.Client.E2E.Tests.Common.Client.Default.Default; +using Microsoft.OData.Client.E2E.Tests.Common.Server.Default; +using Microsoft.OData.Client.E2E.Tests.QueryOptionTests.Server; +using Microsoft.OData.Edm; +using Xunit; + +namespace Microsoft.OData.Client.E2E.Tests.QueryOptionTests.Tests; + +public class OrderbyQueryTests : EndToEndTestBase +{ + private readonly Uri _baseUri; + private readonly Container _context; + private readonly IEdmModel _model; + + public class TestsStartup : TestStartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(QueryOptionTestsController), typeof(MetadataController)); + + services.AddControllers().AddOData(opt => + opt.EnableQueryFeatures().AddRouteComponents("odata", DefaultEdmModel.GetEdmModel())); + } + } + + public OrderbyQueryTests(TestWebApplicationFactory fixture) + : base(fixture) + { + if (Client.BaseAddress == null) + { + throw new ArgumentNullException(nameof(Client.BaseAddress), "Base address cannot be null"); + } + + _baseUri = new Uri(Client.BaseAddress, "odata/"); + + _context = new Container(_baseUri) + { + HttpClientFactory = HttpClientFactory + }; + + _model = DefaultEdmModel.GetEdmModel(); + ResetDefaultDataSource(); + } + + public static IEnumerable MimeTypesData + { + get + { + yield return new object[] { MimeTypes.ApplicationJson + MimeTypes.ODataParameterFullMetadata }; + yield return new object[] { MimeTypes.ApplicationJson + MimeTypes.ODataParameterMinimalMetadata }; + yield return new object[] { MimeTypes.ApplicationJson + MimeTypes.ODataParameterNoMetadata }; + } + } + + #region $orderby with $count collection of primitive type + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task OrderbyWithCountCollectionOfPrimitiveTypeAsync(string mimeType) + { + // Arrange & Act + Func, List> getEntries = (res) => res.Where(r => r != null && + (r.TypeName.Contains("Person") || r.TypeName.Contains("Customer") || r.TypeName.Contains("Product") || r.TypeName.Contains("Employee"))).ToList(); + + var resources = await TestsHelper.QueryResourceSetFeedAsync("People?$orderby=Emails/$count", mimeType); + var details = getEntries(resources); + + // Assert + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + var firstNameProperty = details.First().Properties.Single(p => p.Name == "FirstName") as ODataProperty; + Assert.NotNull(firstNameProperty); + Assert.Equal("Jill", firstNameProperty.Value); + } + } + + #endregion + + #region $orderby with $count collection of primitive type, descending + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task OrderbyWithCountCollectionOfPrimitiveTypeDescTypeAsync(string mimeType) + { + // Arrange & Act + Func, List> getEntries = (res) => res.Where(r => r != null && + (r.TypeName.Contains("Person") || r.TypeName.Contains("Customer") || r.TypeName.Contains("Product") || r.TypeName.Contains("Employee"))).ToList(); + + var resources = await TestsHelper.QueryResourceSetFeedAsync("People?$orderby=Emails/$count desc", mimeType); + var details = getEntries(resources); + + // Assert + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + var firstNameProperty = details.First().Properties.Single(p => p.Name == "FirstName") as ODataProperty; + Assert.NotNull(firstNameProperty); + Assert.Equal("Elmo", firstNameProperty.Value); + } + } + + #endregion + + #region $orderby with $count collection of enum type + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task OrderbyWithCountCollectionOfEnumTypeAsync(string mimeType) + { + // Arrange & Act + Func, List> getEntries = (res) => res.Where(r => r != null && + (r.TypeName.Contains("Person") || r.TypeName.Contains("Customer") || r.TypeName.Contains("Product") || r.TypeName.Contains("Employee"))).ToList(); + + var resources = await TestsHelper.QueryResourceSetFeedAsync("Products?$orderby=CoverColors/$count", mimeType); + var details = getEntries(resources); + + // Assert + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + var nameProperty = details.First().Properties.Single(p => p.Name == "Name") as ODataProperty; + Assert.NotNull(nameProperty); + Assert.Equal("Apple", nameProperty.Value); + } + } + + #endregion + + #region $orderby with $count collection of complex type + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task OrderbyWithCountCollectionOfComplexTypeAsync(string mimeType) + { + // Arrange & Act + Func, List> getEntries = (res) => res.Where(r => r != null && + (r.TypeName.Contains("Person") || r.TypeName.Contains("Customer") || r.TypeName.Contains("Product") || r.TypeName.Contains("Employee"))).ToList(); + + var resources = await TestsHelper.QueryResourceSetFeedAsync("People?$orderby=Addresses/$count", mimeType); + var details = getEntries(resources); + + // Assert + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + var firstNameProperty = details.First().Properties.Single(p => p.Name == "FirstName") as ODataProperty; + Assert.NotNull(firstNameProperty); + Assert.Equal("Jill", firstNameProperty.Value); + } + } + + #endregion + + #region $orderby with $count collection of entity type + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task OrderbyWithCountCollectionOfEntityTypeAsync(string mimeType) + { + // Arrange & Act + Func, List> getEntries = (res) => res.Where(r => r != null && + (r.TypeName.Contains("Person") || r.TypeName.Contains("Customer") || r.TypeName.Contains("Product") || r.TypeName.Contains("Employee"))).ToList(); + + var resources = await TestsHelper.QueryResourceSetFeedAsync("Customers?$orderby=Orders/$count", mimeType); + var details = getEntries(resources); + + // Assert + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + var firstNameProperty = details.First().Properties.Single(p => p.Name == "FirstName") as ODataProperty; + Assert.NotNull(firstNameProperty); + Assert.Equal("Bob", firstNameProperty.Value); + } + } + + #endregion + + #region Private methods + + private QueryOptionTestsHelper TestsHelper + { + get + { + return new QueryOptionTestsHelper(_baseUri, _model, Client); + } + } + + private void ResetDefaultDataSource() + { + var actionUri = new Uri(_baseUri + "queryoption/Default.ResetDefaultDataSource", UriKind.Absolute); + _context.Execute(actionUri, "POST"); + } + + #endregion +} diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Tests/QueryOptionOnCollectionTypePropertyTests.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Tests/QueryOptionOnCollectionTypePropertyTests.cs new file mode 100644 index 0000000000..776d6b7c75 --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Tests/QueryOptionOnCollectionTypePropertyTests.cs @@ -0,0 +1,205 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +using Microsoft.AspNetCore.OData; +using Microsoft.AspNetCore.OData.Routing.Controllers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OData.Client.E2E.TestCommon; +using Microsoft.OData.Client.E2E.TestCommon.Common; +using Microsoft.OData.Client.E2E.TestCommon.Helpers; +using Microsoft.OData.Client.E2E.TestCommon.Logs; +using Microsoft.OData.Client.E2E.Tests.Common.Client.Default.Default; +using Microsoft.OData.Client.E2E.Tests.Common.Server.Default; +using Microsoft.OData.Client.E2E.Tests.QueryOptionTests.Server; +using Microsoft.OData.Edm; +using Xunit; + +namespace Microsoft.OData.Client.E2E.Tests.QueryOptionTests.Tests; + +public class QueryOptionOnCollectionTypePropertyTests : EndToEndTestBase +{ + private readonly Uri _baseUri; + private readonly Container _context; + private readonly IEdmModel _model; + + public class TestsStartup : TestStartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(QueryOptionTestsController), typeof(MetadataController)); + + services.AddControllers().AddOData(opt => + opt.EnableQueryFeatures().AddRouteComponents("odata", DefaultEdmModel.GetEdmModel())); + } + } + + public QueryOptionOnCollectionTypePropertyTests(TestWebApplicationFactory factory) + : base(factory) + { + if (Client.BaseAddress == null) + { + throw new ArgumentNullException(nameof(Client.BaseAddress), "Base address cannot be null"); + } + + _baseUri = new Uri(Client.BaseAddress, "odata/"); + + _context = new Container(_baseUri) + { + HttpClientFactory = HttpClientFactory + }; + + _model = DefaultEdmModel.GetEdmModel(); + ResetDefaultDataSource(); + + // Add the custom TraceListener to log assertion failures + _ = new LogAssertTraceListener(); + } + + private const string MimeType = MimeTypes.ApplicationJson + MimeTypes.ODataParameterMinimalMetadata; + + #region $skip + + [Fact] + public async Task SkipQueryOptionTest() + { + // Arrange & Act + var property = await this.TestsHelper.QueryPropertyAsync("Customers(1)/Numbers?$skip=2", MimeType); + + // Assert + Assert.NotNull(property); + + var collectionValue = property.Value as ODataCollectionValue; + Assert.NotNull(collectionValue); + + var items = collectionValue.Items.Cast().ToList(); + Assert.Equal(3, items.Count); + Assert.Equal("310", items[0]); + Assert.Equal("bca", items[1]); + Assert.Equal("ayz", items[2]); + } + + #endregion + + #region $top + + [Fact] + public async Task TopQueryOptionTest() + { + // Arrange & Act + var property = await this.TestsHelper.QueryPropertyAsync("Customers(1)/Numbers?$top=3", MimeType); + + // Assert + Assert.NotNull(property); + + var collectionValue = property.Value as ODataCollectionValue; + Assert.NotNull(collectionValue); + + var items = collectionValue.Items.Cast().ToList(); + Assert.Equal(3, items.Count); + Assert.Equal("111-111-1111", items[0]); + Assert.Equal("012", items[1]); + Assert.Equal("310", items[2]); + } + + #endregion + + #region Combine Query Options + + [Theory] + [InlineData("$orderby=$it", new string[] { "012", "111-111-1111", "310", "ayz", "bca" })] + [InlineData("$orderby=$it&$top=1", new string[] { "012" })] + [InlineData("$skip=1&$filter=contains($it,'a')", new string[] { "ayz" })] + [InlineData("$filter=$it eq '012'", new string[] { "012" })] + public async Task CombineVariousQueryOptionsTest(string queryOptions, string[] collectionItems) + { + // Arrange & Act + var property = await this.TestsHelper.QueryPropertyAsync($"Customers(1)/Numbers?{queryOptions}", MimeType); + + // Assert + Assert.NotNull(property); + + var collectionValue = property.Value as ODataCollectionValue; + Assert.NotNull(collectionValue); + + var expectedValue = new ODataCollectionValue() + { + TypeName = "Collection(Edm.String)", + Items = collectionItems + }; + + ODataValueAssertEqualHelper.AssertODataValueEqual(expectedValue, collectionValue); + } + + #endregion + + #region Client Tests + + [Fact] + public void FilterOnCollectionCountTest() + { + // Arrange & Act & Assert + var person = _context.People.Where(p => p.Emails.Count == 2) as DataServiceQuery; + Assert.NotNull(person?.RequestUri?.OriginalString); + Assert.EndsWith("/People?$filter=Emails/$count eq 2", person.RequestUri.OriginalString); + + var product = _context.Products.Where(p => p.CoverColors.Count == 2) as DataServiceQuery; + Assert.NotNull(product?.RequestUri?.OriginalString); + Assert.EndsWith("/Products?$filter=CoverColors/$count eq 2", product.RequestUri.OriginalString); + + person = _context.People.Where(p => p.Addresses.Count == 2) as DataServiceQuery; + Assert.NotNull(person?.RequestUri?.OriginalString); + Assert.EndsWith("/People?$filter=Addresses/$count eq 2", person.RequestUri.OriginalString); + + var customers = _context.Customers.Where(p => p.Orders.Count == 2) as DataServiceQuery; + Assert.NotNull(customers?.RequestUri?.OriginalString); + Assert.EndsWith("/Customers?$filter=Orders/$count eq 2", customers.RequestUri.OriginalString); + } + + [Fact] + public void OrderbyOnCollectionCountTest() + { + // Arrange & Act & Assert + var people = _context.People.OrderBy(p => p.Emails.Count) as DataServiceQuery; + Assert.NotNull(people?.RequestUri?.OriginalString); + Assert.EndsWith("/People?$orderby=Emails/$count", people.RequestUri.OriginalString); + + people = _context.People.OrderByDescending(p => p.Emails.Count) as DataServiceQuery; + Assert.NotNull(people?.RequestUri?.OriginalString); + Assert.EndsWith("/People?$orderby=Emails/$count desc", people.RequestUri.OriginalString); + + var products = _context.Products.OrderBy(p => p.CoverColors.Count) as DataServiceQuery; + Assert.NotNull(products?.RequestUri?.OriginalString); + Assert.EndsWith("/Products?$orderby=CoverColors/$count", products.RequestUri.OriginalString); + + people = _context.People.OrderBy(p => p.Addresses.Count) as DataServiceQuery; + Assert.NotNull(people?.RequestUri?.OriginalString); + Assert.EndsWith("/People?$orderby=Addresses/$count", people.RequestUri.OriginalString); + + var customers = _context.Customers.OrderBy(p => p.Orders.Count) as DataServiceQuery; + Assert.NotNull(customers?.RequestUri?.OriginalString); + Assert.EndsWith("/Customers?$orderby=Orders/$count", customers.RequestUri.OriginalString); + } + + #endregion + + #region Private methods + + private QueryOptionTestsHelper TestsHelper + { + get + { + return new QueryOptionTestsHelper(_baseUri, _model, Client); + } + } + + private void ResetDefaultDataSource() + { + var actionUri = new Uri(_baseUri + "queryoption/Default.ResetDefaultDataSource", UriKind.Absolute); + _context.Execute(actionUri, "POST"); + } + + #endregion +} diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Tests/QueryOptionTestsHelper.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Tests/QueryOptionTestsHelper.cs new file mode 100644 index 0000000000..5c2f669afc --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Tests/QueryOptionTestsHelper.cs @@ -0,0 +1,152 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +using System.Reflection; +using Microsoft.OData.Client.E2E.TestCommon.Common; +using Microsoft.OData.Edm; +using Xunit; + +namespace Microsoft.OData.Client.E2E.Tests.QueryOptionTests.Tests; + +public class QueryOptionTestsHelper +{ + private readonly Uri BaseUri; + private readonly IEdmModel Model; + private readonly HttpClient Client; + private const string IncludeAnnotation = "odata.include-annotations"; + + public QueryOptionTestsHelper(Uri baseUri, IEdmModel model, HttpClient client) + { + this.BaseUri = baseUri; + this.Model = model; + this.Client = client; + } + + public async Task> QueryResourceEntriesAsync(string queryText, string mimeType) + { + ODataMessageReaderSettings readerSettings = new() { BaseUri = BaseUri }; + var requestUrl = new Uri(BaseUri.AbsoluteUri + queryText, UriKind.Absolute); + + var requestMessage = new TestHttpClientRequestMessage(requestUrl, Client); + requestMessage.SetHeader("Accept", mimeType); + requestMessage.SetHeader("Prefer", string.Format("{0}={1}", IncludeAnnotation, "*")); + + var responseMessage = await requestMessage.GetResponseAsync(); + + Assert.Equal(200, responseMessage.StatusCode); + + var entries = new List(); + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + using (var messageReader = new ODataMessageReader(responseMessage, readerSettings, Model)) + { + var reader = await messageReader.CreateODataResourceReaderAsync(); + while (await reader.ReadAsync()) + { + if (reader.State == ODataReaderState.ResourceEnd && reader.Item is ODataResource odataResource) + { + entries.Add(odataResource); + } + } + Assert.Equal(ODataReaderState.Completed, reader.State); + } + } + + return entries; + } + + public async Task> QueryResourceSetFeedAsync(string queryText, string mimeType) + { + ODataMessageReaderSettings readerSettings = new() { BaseUri = BaseUri }; + var requestUrl = new Uri(BaseUri.AbsoluteUri + queryText, UriKind.Absolute); + + var requestMessage = new TestHttpClientRequestMessage(requestUrl, Client); + requestMessage.SetHeader("Accept", mimeType); + + var responseMessage = await requestMessage.GetResponseAsync(); + + Assert.Equal(200, responseMessage.StatusCode); + + var entries = new List(); + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + using (var messageReader = new ODataMessageReader(responseMessage, readerSettings, Model)) + { + var reader = await messageReader.CreateODataResourceSetReaderAsync(); + while (await reader.ReadAsync()) + { + if (reader.State == ODataReaderState.ResourceEnd) + { + if (reader.Item is ODataResource odataResource) + { + entries.Add(odataResource); + } + } + } + Assert.Equal(ODataReaderState.Completed, reader.State); + } + + } + + return entries; + } + + public async Task QueryInnerFeedAsync(string queryText, string mimeType) + { + ODataMessageReaderSettings readerSettings = new() { BaseUri = BaseUri }; + var requestUrl = new Uri(BaseUri.AbsoluteUri + queryText, UriKind.Absolute); + + var requestMessage = new TestHttpClientRequestMessage(requestUrl, Client); + requestMessage.SetHeader("Accept", mimeType); + + var responseMessage = await requestMessage.GetResponseAsync(); + + Assert.Equal(200, responseMessage.StatusCode); + + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + using (var messageReader = new ODataMessageReader(responseMessage, readerSettings, Model)) + { + var reader = await messageReader.CreateODataResourceReaderAsync(); + while (await reader.ReadAsync()) + { + if (reader.State == ODataReaderState.ResourceSetEnd && reader.Item is ODataResourceSet oDataResourceSet) + { + return oDataResourceSet; + } + } + } + } + + return null; + } + + public async Task QueryPropertyAsync(string requestUri, string mimeType) + { + var readerSettings = new ODataMessageReaderSettings() { BaseUri = BaseUri }; + + var uri = new Uri(BaseUri.AbsoluteUri + requestUri, UriKind.Absolute); + var requestMessage = new TestHttpClientRequestMessage(uri, Client); + + requestMessage.SetHeader("Accept", mimeType); + + var responseMessage = await requestMessage.GetResponseAsync(); + + Assert.Equal(200, responseMessage.StatusCode); + + ODataProperty? property = null; + + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + using (var messageReader = new ODataMessageReader(responseMessage, readerSettings, Model)) + { + property = messageReader.ReadProperty(); + } + } + + return property; + } +} diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Tests/SearchQueryTests.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Tests/SearchQueryTests.cs new file mode 100644 index 0000000000..547af690e3 --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/QueryOptionTests/Tests/SearchQueryTests.cs @@ -0,0 +1,213 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +using Microsoft.AspNetCore.OData; +using Microsoft.AspNetCore.OData.Query.Expressions; +using Microsoft.AspNetCore.OData.Routing.Controllers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OData.Client.E2E.TestCommon; +using Microsoft.OData.Client.E2E.TestCommon.Common; +using Microsoft.OData.Client.E2E.Tests.Common.Client.Default.Default; +using Microsoft.OData.Client.E2E.Tests.Common.Server.Default; +using Microsoft.OData.Client.E2E.Tests.QueryOptionTests.Server; +using Microsoft.OData.Edm; +using Xunit; + +namespace Microsoft.OData.Client.E2E.Tests.QueryOptionTests.Tests; + +public class SearchQueryTests : EndToEndTestBase +{ + private readonly Uri _baseUri; + private readonly Container _context; + private readonly IEdmModel _model; + + public class TestsStartup : TestStartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(QueryOptionTestsController), typeof(MetadataController)); + + services.AddControllers().AddOData(opt => + opt.EnableQueryFeatures().AddRouteComponents("odata", DefaultEdmModel.GetEdmModel(), services => + services.AddSingleton())); + } + } + + public SearchQueryTests(TestWebApplicationFactory fixture) + : base(fixture) + { + if (Client.BaseAddress == null) + { + throw new ArgumentNullException(nameof(Client.BaseAddress), "Base address cannot be null"); + } + + _baseUri = new Uri(Client.BaseAddress, "odata/"); + + _context = new Container(_baseUri) + { + HttpClientFactory = HttpClientFactory + }; + + _model = DefaultEdmModel.GetEdmModel(); + ResetDefaultDataSource(); + } + + public static IEnumerable MimeTypesData + { + get + { + yield return new object[] { MimeTypes.ApplicationJson + MimeTypes.ODataParameterFullMetadata }; + yield return new object[] { MimeTypes.ApplicationJson + MimeTypes.ODataParameterMinimalMetadata }; + yield return new object[] { MimeTypes.ApplicationJson + MimeTypes.ODataParameterNoMetadata }; + } + } + + #region $search query with 'AND', 'OR', and 'NOT' operators + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task SearchQueryWithAndOrNotAsync(string mimeType) + { + List details = await this.TestsHelper.QueryResourceSetFeedAsync("ProductDetails?$search=(drink OR snack) AND (suger OR sweet) AND NOT \"0\"", mimeType); + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + Assert.Equal(2, details.Count); + } + } + + #endregion + + #region $search query with 'NOT' operator + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task SearchQueryWithNotOperatorAsync(string mimeType) + { + List details = await this.TestsHelper.QueryResourceSetFeedAsync("ProductDetails?$search=NOT (drink OR snack)", mimeType); + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + Assert.Empty(details); + } + } + + #endregion + + #region $search query with implicit 'AND' operator + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task SearchQueryWithImplicitAndAsync(string mimeType) + { + List details = await this.TestsHelper.QueryResourceSetFeedAsync("ProductDetails?$search=snack sweet", mimeType); + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + Assert.Single(details); + + var descriptionProperty = details.First().Properties.Single(p => p.Name == "Description") as ODataProperty; + Assert.NotNull(descriptionProperty); + Assert.Equal("sweet snack", descriptionProperty.Value); + } + } + + #endregion + + #region $search query with 'NOT' and 'AND' operators + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task SearchQueryWithNotAndAsync(string mimeType) + { + List details = await this.TestsHelper.QueryResourceSetFeedAsync("ProductDetails?$search=snack NOT sweet", mimeType); + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + Assert.Equal(2, details.Count); + } + } + + #endregion + + #region $search query with priority of 'AND', 'NOT', and 'OR' operators + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task SearchQueryWithPriorityOperatorsAsync(string mimeType) + { + List details = await this.TestsHelper.QueryResourceSetFeedAsync("ProductDetails?$search=snack OR drink AND soft AND NOT \"0\"", mimeType); + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + Assert.Equal(4, details.Count); + } + } + + #endregion + + + #region $search Combined With $filter, and $select Query Options + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task SearchCombinedWithFilterAndSelectQueryOptionsTest(string mimeType) + { + List details = await this.TestsHelper.QueryResourceSetFeedAsync("ProductDetails?$filter=contains(Description,'drink')&$search=suger OR spicy NOT \"0\"&$select=Description", mimeType); + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + Assert.Equal(2, details.Count); + foreach (var detail in details) + { + Assert.Single(detail.Properties); + + var descriptionProperty = detail.Properties.Single(p => p.Name == "Description") as ODataProperty; + Assert.NotNull(descriptionProperty); + + var description = Assert.IsType(descriptionProperty.Value); + Assert.Contains("drink", description); + } + } + } + + #endregion + + #region $search query Combined With $orderby and $expand Query Options + + [Theory] + [MemberData(nameof(MimeTypesData))] + public async Task SearchCombinedWithOrderbyAndExpandQueryOptionsTest(string mimeType) + { + List entries = await this.TestsHelper.QueryResourceSetFeedAsync("ProductDetails?$search=suger OR sweet&$orderby=ProductName&$expand=Reviews", mimeType); + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + Assert.Equal(7, entries.Count); + + var productDetails = entries.FindAll(e => e.Id.AbsoluteUri.Contains("ProductDetails")); + var productNameProperty = productDetails.First().Properties.Single(p => p.Name == "ProductName") as ODataProperty; + Assert.NotNull(productNameProperty); + Assert.Equal("Candy", productNameProperty.Value); + + var reviews = entries.FindAll(e => e.Id.AbsoluteUri.Contains("ProductReviews")); + Assert.Equal(4, reviews.Count); + } + } + + #endregion + + #region Private methods + + private QueryOptionTestsHelper TestsHelper + { + get + { + return new QueryOptionTestsHelper(_baseUri, _model, Client); + } + } + + private void ResetDefaultDataSource() + { + var actionUri = new Uri(_baseUri + "queryoption/Default.ResetDefaultDataSource", UriKind.Absolute); + _context.Execute(actionUri, "POST"); + } + + #endregion +}