From 896bcba3348c3ea5ee77f5ff6225ecbf79207819 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Mon, 30 Dec 2024 16:48:14 +0100 Subject: [PATCH] (feature): migrate to System.Linq.Dynamic.Core, fix jwt tests --- .../Security/JwtGeneratorTests.cs | 28 +-- .../Webapi/Models/PagedRequestTests.cs | 164 +++++++++++------- src/MaksIT.Core/MaksIT.Core.csproj | 7 +- src/MaksIT.Core/Webapi/Models/PagedRequest.cs | 162 ++--------------- 4 files changed, 140 insertions(+), 221 deletions(-) diff --git a/src/MaksIT.Core.Tests/Security/JwtGeneratorTests.cs b/src/MaksIT.Core.Tests/Security/JwtGeneratorTests.cs index 61228ee..13f0b26 100644 --- a/src/MaksIT.Core.Tests/Security/JwtGeneratorTests.cs +++ b/src/MaksIT.Core.Tests/Security/JwtGeneratorTests.cs @@ -4,17 +4,21 @@ using MaksIT.Core.Security; namespace MaksIT.Core.Tests.Security { public class JwtGeneratorTests { - private const string Secret = "supersecretkey12345678901234567890"; - private const string Issuer = "testIssuer"; - private const string Audience = "testAudience"; - private const double Expiration = 30; // 30 minutes - private const string Username = "testUser"; - private readonly List Roles = new List { "Admin", "User" }; - [Fact] + + private JWTTokenGenerateRequest jWTTokenGenerateRequest = new JWTTokenGenerateRequest { + Secret = "supersecretkey12345678901234567890", + Issuer = "testIssuer", + Audience = "testAudience", + Expiration = 30, // 30 minutes + Username = "testUser", + Roles = new List { "Admin", "User" }, + }; + + [Fact] public void GenerateToken_ShouldReturnValidToken() { // Act - var result = JwtGenerator.TryGenerateToken(Secret, Issuer, Audience, Expiration, Username, Roles, out var tokenData, out var errorMessage); + var result = JwtGenerator.TryGenerateToken(jWTTokenGenerateRequest, out var tokenData, out var errorMessage); // Assert Assert.True(result); @@ -26,15 +30,15 @@ namespace MaksIT.Core.Tests.Security { [Fact] public void ValidateToken_ShouldReturnClaimsPrincipal_WhenTokenIsValid() { // Arrange - JwtGenerator.TryGenerateToken(Secret, Issuer, Audience, Expiration, Username, Roles, out var tokenData, out var generateErrorMessage); + JwtGenerator.TryGenerateToken(jWTTokenGenerateRequest, out var tokenData, out var generateErrorMessage); // Act - var result = JwtGenerator.TryValidateToken(Secret, Issuer, Audience, tokenData?.Item1, out var jwtTokenClaims, out var validateErrorMessage); + var result = JwtGenerator.TryValidateToken(jWTTokenGenerateRequest.Secret, jWTTokenGenerateRequest.Issuer, jWTTokenGenerateRequest.Audience, tokenData?.Item1, out var jwtTokenClaims, out var validateErrorMessage); // Assert Assert.True(result); Assert.NotNull(jwtTokenClaims); - Assert.Equal(Username, jwtTokenClaims?.Username); + Assert.Equal(jWTTokenGenerateRequest.Username, jwtTokenClaims?.Username); Assert.Contains(jwtTokenClaims?.Roles ?? new List(), c => c == "Admin"); Assert.Contains(jwtTokenClaims?.Roles ?? new List(), c => c == "User"); Assert.Null(validateErrorMessage); @@ -46,7 +50,7 @@ namespace MaksIT.Core.Tests.Security { var invalidToken = "invalidToken"; // Act - var result = JwtGenerator.TryValidateToken(Secret, Issuer, Audience, invalidToken, out var jwtTokenClaims, out var errorMessage); + var result = JwtGenerator.TryValidateToken(jWTTokenGenerateRequest.Secret, jWTTokenGenerateRequest.Issuer, jWTTokenGenerateRequest.Audience, invalidToken, out var jwtTokenClaims, out var errorMessage); // Assert Assert.False(result); diff --git a/src/MaksIT.Core.Tests/Webapi/Models/PagedRequestTests.cs b/src/MaksIT.Core.Tests/Webapi/Models/PagedRequestTests.cs index d84b14d..49c344d 100644 --- a/src/MaksIT.Core.Tests/Webapi/Models/PagedRequestTests.cs +++ b/src/MaksIT.Core.Tests/Webapi/Models/PagedRequestTests.cs @@ -1,125 +1,171 @@ - -using MaksIT.Core.Webapi.Models; - -namespace MaksIT.Core.Tests.Webapi.Models; +namespace MaksIT.Core.Tests.Webapi.Models; public class PagedRequestTests { - [Fact] - public void BuildFilterExpression_ShouldHandleEqualsOperator() { - // Arrange - var request = new PagedRequest { - Filters = "Name='John'" - }; + public class TestEntity { + public string? Name { get; set; } + public int Age { get; set; } + } - // Act - var expression = request.BuildFilterExpression(request.Filters); - var compiled = expression!.Compile(); - - // Assert - var testEntity = new TestEntity { Name = "John" }; - Assert.True(compiled(testEntity)); + // Setup a mock IQueryable to test against + private IQueryable GetTestQueryable() { + return new List { + new TestEntity { Name = "John", Age = 31 }, + new TestEntity { Name = "Jane", Age = 29 }, + new TestEntity { Name = "Doe", Age = 35 } + }.AsQueryable(); } [Fact] - public void BuildFilterExpression_ShouldHandleNotEqualsOperator() { + public void ApplyFilters_ShouldHandleEqualsOperator() { // Arrange + var queryable = GetTestQueryable(); var request = new PagedRequest { - Filters = "Name!='John'" + Filters = "Name == \"John\"" }; // Act - var expression = request.BuildFilterExpression(request.Filters); - var compiled = expression!.Compile(); + var filtered = request.ApplyFilters(queryable); // Assert - var testEntity = new TestEntity { Name = "John" }; - Assert.False(compiled(testEntity)); + Assert.Contains(filtered, t => t.Name == "John"); + Assert.Single(filtered); } [Fact] - public void BuildFilterExpression_ShouldHandleGreaterThanOperator() { + public void ApplyFilters_ShouldHandleNotEqualsOperator() { // Arrange + var queryable = GetTestQueryable(); var request = new PagedRequest { - Filters = "Age>30" + Filters = "Name != \"John\"" }; // Act - var expression = request.BuildFilterExpression(request.Filters); - var compiled = expression!.Compile(); + var filtered = request.ApplyFilters(queryable); // Assert - var testEntity = new TestEntity { Age = 31 }; - Assert.True(compiled(testEntity)); + Assert.DoesNotContain(filtered, t => t.Name == "John"); } [Fact] - public void BuildFilterExpression_ShouldHandleLessThanOperator() { + public void ApplyFilters_ShouldHandleGreaterThanOperator() { // Arrange + var queryable = GetTestQueryable(); var request = new PagedRequest { - Filters = "Age<30" + Filters = "Age > 30" }; // Act - var expression = request.BuildFilterExpression(request.Filters); - var compiled = expression!.Compile(); + var filtered = request.ApplyFilters(queryable); // Assert - var testEntity = new TestEntity { Age = 29 }; - Assert.True(compiled(testEntity)); + Assert.All(filtered, t => Assert.True(t.Age > 30)); } [Fact] - public void BuildFilterExpression_ShouldHandleAndOperator() { + public void ApplyFilters_ShouldHandleLessThanOperator() { // Arrange + var queryable = GetTestQueryable(); var request = new PagedRequest { - Filters = "Name='John' && Age>30" + Filters = "Age < 30" }; // Act - var expression = request.BuildFilterExpression(request.Filters); - var compiled = expression!.Compile(); + var filtered = request.ApplyFilters(queryable); // Assert - var testEntity = new TestEntity { Name = "John", Age = 31 }; - Assert.True(compiled(testEntity)); + Assert.All(filtered, t => Assert.True(t.Age < 30)); } [Fact] - public void BuildFilterExpression_ShouldHandleOrOperator() { + public void ApplyFilters_ShouldHandleAndOperator() { // Arrange + var queryable = GetTestQueryable(); var request = new PagedRequest { - Filters = "Name='John' || Age>30" + Filters = "Name == \"John\" && Age > 30" }; // Act - var expression = request.BuildFilterExpression(request.Filters); - var compiled = expression!.Compile(); + var filtered = request.ApplyFilters(queryable); // Assert - var testEntity = new TestEntity { Name = "Doe", Age = 31 }; - Assert.True(compiled(testEntity)); + Assert.Contains(filtered, t => t.Name == "John" && t.Age > 30); + Assert.Single(filtered); } [Fact] - public void BuildFilterExpression_ShouldHandleNegation() { + public void ApplyFilters_ShouldHandleOrOperator() { // Arrange + var queryable = GetTestQueryable(); var request = new PagedRequest { - Filters = "!Name='John'" + Filters = "Name == \"John\" || Age > 30" }; // Act - var expression = request.BuildFilterExpression(request.Filters); - var compiled = expression!.Compile(); + var filtered = request.ApplyFilters(queryable); // Assert - var testEntity = new TestEntity { Name = "Doe" }; - Assert.True(compiled(testEntity)); + Assert.Contains(filtered, t => t.Name == "John" || t.Age > 30); + } + + [Fact] + public void ApplyFilters_ShouldHandleNegation() { + // Arrange + var queryable = GetTestQueryable(); + var request = new PagedRequest { + Filters = "!(Name == \"John\")" + }; + + // Act + var filtered = request.ApplyFilters(queryable); + + // Assert + Assert.DoesNotContain(filtered, t => t.Name == "John"); + } + + [Fact] + public void ApplyFilters_ShouldHandleContainsOperator() { + // Arrange + var queryable = GetTestQueryable(); + var request = new PagedRequest { + Filters = "Name.Contains(\"oh\")" + }; + + // Act + var filtered = request.ApplyFilters(queryable); + + // Assert + Assert.Contains(filtered, t => t.Name.Contains("oh")); + } + + [Fact] + public void ApplyFilters_ShouldHandleStartsWithOperator() { + // Arrange + var queryable = GetTestQueryable(); + var request = new PagedRequest { + Filters = "Name.StartsWith(\"Jo\")" + }; + + // Act + var filtered = request.ApplyFilters(queryable); + + // Assert + Assert.Contains(filtered, t => t.Name.StartsWith("John")); + Assert.Single(filtered); // Assuming only "Johnny" starts with "John" + } + + [Fact] + public void ApplyFilters_ShouldHandleEndsWithOperator() { + // Arrange + var queryable = GetTestQueryable(); + var request = new PagedRequest { + Filters = "Name.EndsWith(\"hn\")" + }; + + // Act + var filtered = request.ApplyFilters(queryable); + + // Assert + Assert.Contains(filtered, t => t.Name.EndsWith("hn")); } } - -// Helper class for testing purposes -public class TestEntity { - public string? Name { get; set; } - public int Age { get; set; } -} \ No newline at end of file diff --git a/src/MaksIT.Core/MaksIT.Core.csproj b/src/MaksIT.Core/MaksIT.Core.csproj index 7824c4e..8fe9f44 100644 --- a/src/MaksIT.Core/MaksIT.Core.csproj +++ b/src/MaksIT.Core/MaksIT.Core.csproj @@ -8,7 +8,7 @@ MaksIT.Core - 1.2.8 + 1.2.9 Maksym Sadovnychyy MAKS-IT MaksIT.Core @@ -28,7 +28,8 @@ - - + + + diff --git a/src/MaksIT.Core/Webapi/Models/PagedRequest.cs b/src/MaksIT.Core/Webapi/Models/PagedRequest.cs index bb3b4df..177a7ce 100644 --- a/src/MaksIT.Core/Webapi/Models/PagedRequest.cs +++ b/src/MaksIT.Core/Webapi/Models/PagedRequest.cs @@ -1,157 +1,25 @@ -using System.Linq.Expressions; -using MaksIT.Core.Extensions; +using System.Linq.Dynamic.Core; + using MaksIT.Core.Abstractions.Webapi; -namespace MaksIT.Core.Webapi.Models { - public class PagedRequest : RequestModelBase { - public int PageSize { get; set; } = 100; - public int PageNumber { get; set; } = 1; +public class PagedRequest : RequestModelBase { + public int PageSize { get; set; } = 100; + public int PageNumber { get; set; } = 1; + public string? Filters { get; set; } - public string? Filters { get; set; } - public Dictionary? CollectionFilters { get; set; } + public string? SortBy { get; set; } + public bool IsAscending { get; set; } = true; - public string? SortBy { get; set; } - public bool IsAscending { get; set; } = true; - - public Expression>? BuildCollectionFilterExpression(string collectionName) { - Expression>? globalFilterExpression = null; - Expression>? collectionFilterExpression = null; - - if (!string.IsNullOrEmpty(Filters)) { - globalFilterExpression = BuildFilterExpression(Filters); - } - - if (CollectionFilters != null && CollectionFilters.TryGetValue(collectionName, out var collectionFilter) && !string.IsNullOrEmpty(collectionFilter)) { - collectionFilterExpression = BuildFilterExpression(collectionFilter); - } - - if (globalFilterExpression == null && collectionFilterExpression == null) { - return null; - } - - if (globalFilterExpression != null && collectionFilterExpression != null) { - return globalFilterExpression.CombineWith(collectionFilterExpression); - } - - return globalFilterExpression ?? collectionFilterExpression; + public IQueryable ApplyFilters(IQueryable query) { + if (!string.IsNullOrWhiteSpace(Filters)) { + query = query.Where(Filters); // Filters interpreted directly } - public Expression>? BuildFilterExpression(string filter) { - if (string.IsNullOrEmpty(filter)) { - return null; - } - - var parameter = Expression.Parameter(typeof(T), "x"); - Expression? combinedExpression = null; - - // Split filters based on && and || operators - var tokens = filter.Split(new[] { "&&", "||" }, StringSplitOptions.None); - var operators = new List(); - - // Extract operators (&&, ||) from the original filter string - int lastIndex = 0; - for (int i = 0; i < filter.Length; i++) { - if (filter.Substring(i).StartsWith("&&")) { - operators.Add("&&"); - lastIndex = i + 2; - } - else if (filter.Substring(i).StartsWith("||")) { - operators.Add("||"); - lastIndex = i + 2; - } - } - - for (int i = 0; i < tokens.Length; i++) { - string processedFilter = tokens[i].Trim(); - bool isNegated = false; - - // Check for '!' at the beginning for negation - if (processedFilter.StartsWith("!")) { - isNegated = true; - processedFilter = processedFilter.Substring(1).Trim(); // Remove '!' and trim - } - - Expression? expression = null; - - if (processedFilter.Contains("!=")) { - var parts = processedFilter.Split(new[] { "!=" }, StringSplitOptions.None); - expression = BuildEqualityExpression(parameter, parts[0], parts[1], isNegated: true); - } - else if (processedFilter.Contains('=')) { - var parts = processedFilter.Split('='); - expression = BuildEqualityExpression(parameter, parts[0], parts[1], isNegated: false); - } - else if (processedFilter.Contains('>') || processedFilter.Contains('<')) { - // Handle comparison (>, <, >=, <=) - expression = BuildComparisonExpression(processedFilter, parameter); - } - - // Apply negation if '!' was found at the beginning - if (isNegated && expression != null) { - expression = Expression.Not(expression); - } - - // Only combine expressions if the new expression is not null - if (expression != null) { - if (combinedExpression == null) { - combinedExpression = expression; - } - else if (i - 1 < operators.Count) // Ensure we don't exceed the operators list size - { - var operatorType = operators[i - 1]; - combinedExpression = operatorType == "&&" - ? Expression.AndAlso(combinedExpression, expression) - : Expression.OrElse(combinedExpression, expression); - } - } - } - - return combinedExpression != null - ? Expression.Lambda>(combinedExpression, parameter) - : null; + if (!string.IsNullOrWhiteSpace(SortBy)) { + var direction = IsAscending ? "ascending" : "descending"; + query = query.OrderBy($"{SortBy} {direction}"); } - private static Expression BuildEqualityExpression(ParameterExpression parameter, string propertyName, string value, bool isNegated) { - var property = Expression.Property(parameter, propertyName.Trim()); - var constant = Expression.Constant(ConvertValue(property.Type, value.Trim().Replace("'", ""))); - - return isNegated - ? Expression.NotEqual(property, constant) - : Expression.Equal(property, constant); - } - - private static Expression? BuildComparisonExpression(string subFilter, ParameterExpression parameter) { - var comparisonType = subFilter.Contains(">=") - ? ">=" - : subFilter.Contains("<=") - ? "<=" - : subFilter.Contains(">") - ? ">" - : "<"; - - var parts = subFilter.Split(new[] { '>', '<', '=', ' ' }, StringSplitOptions.RemoveEmptyEntries); - var propertyName = parts[0].Trim(); - var value = parts[1].Trim().Replace("'", ""); - - var property = Expression.Property(parameter, propertyName); - var constant = Expression.Constant(ConvertValue(property.Type, value)); - - return comparisonType switch { - ">" => Expression.GreaterThan(property, constant), - "<" => Expression.LessThan(property, constant), - ">=" => Expression.GreaterThanOrEqual(property, constant), - "<=" => Expression.LessThanOrEqual(property, constant), - _ => null - }; - } - - private static object ConvertValue(Type type, string value) { - return type switch { - var t when t == typeof(int) => int.Parse(value), - var t when t == typeof(bool) => bool.Parse(value), - var t when t == typeof(DateTime) => DateTime.Parse(value), - _ => value - }; - } + return query.Skip((PageNumber - 1) * PageSize).Take(PageSize); } }