(feature): migrate to System.Linq.Dynamic.Core, fix jwt tests
This commit is contained in:
parent
c5bf11270f
commit
896bcba334
@ -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<string> Roles = new List<string> { "Admin", "User" };
|
||||
|
||||
[Fact]
|
||||
|
||||
private JWTTokenGenerateRequest jWTTokenGenerateRequest = new JWTTokenGenerateRequest {
|
||||
Secret = "supersecretkey12345678901234567890",
|
||||
Issuer = "testIssuer",
|
||||
Audience = "testAudience",
|
||||
Expiration = 30, // 30 minutes
|
||||
Username = "testUser",
|
||||
Roles = new List<string> { "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<string>(), c => c == "Admin");
|
||||
Assert.Contains(jwtTokenClaims?.Roles ?? new List<string>(), 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);
|
||||
|
||||
@ -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<TestEntity>(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<TestEntity> GetTestQueryable() {
|
||||
return new List<TestEntity> {
|
||||
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<TestEntity>(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<TestEntity>(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<TestEntity>(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<TestEntity>(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<TestEntity>(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<TestEntity>(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; }
|
||||
}
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
<!-- NuGet package metadata -->
|
||||
<PackageId>MaksIT.Core</PackageId>
|
||||
<Version>1.2.8</Version>
|
||||
<Version>1.2.9</Version>
|
||||
<Authors>Maksym Sadovnychyy</Authors>
|
||||
<Company>MAKS-IT</Company>
|
||||
<Product>MaksIT.Core</Product>
|
||||
@ -28,7 +28,8 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.2.1" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.3.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
|
||||
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.5.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@ -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<string, string>? 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<Func<T, bool>>? BuildCollectionFilterExpression<T>(string collectionName) {
|
||||
Expression<Func<T, bool>>? globalFilterExpression = null;
|
||||
Expression<Func<T, bool>>? collectionFilterExpression = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(Filters)) {
|
||||
globalFilterExpression = BuildFilterExpression<T>(Filters);
|
||||
}
|
||||
|
||||
if (CollectionFilters != null && CollectionFilters.TryGetValue(collectionName, out var collectionFilter) && !string.IsNullOrEmpty(collectionFilter)) {
|
||||
collectionFilterExpression = BuildFilterExpression<T>(collectionFilter);
|
||||
}
|
||||
|
||||
if (globalFilterExpression == null && collectionFilterExpression == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (globalFilterExpression != null && collectionFilterExpression != null) {
|
||||
return globalFilterExpression.CombineWith(collectionFilterExpression);
|
||||
}
|
||||
|
||||
return globalFilterExpression ?? collectionFilterExpression;
|
||||
public IQueryable<T> ApplyFilters<T>(IQueryable<T> query) {
|
||||
if (!string.IsNullOrWhiteSpace(Filters)) {
|
||||
query = query.Where(Filters); // Filters interpreted directly
|
||||
}
|
||||
|
||||
public Expression<Func<T, bool>>? BuildFilterExpression<T>(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<string>();
|
||||
|
||||
// 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<Func<T, bool>>(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);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user