Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions QueryKit.IntegrationTests/Tests/FilterParameterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ namespace QueryKit.IntegrationTests.Tests;

using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using QueryKit.Configuration;
using SharedTestingHelper.Fakes;
using WebApiTestProject.Entities;

public class FilterParameterTests() : TestBase
{
private static readonly QueryKitConfiguration ParameterizedConfig =
new(settings => settings.ParameterizeFilterValues = true);

[Theory]
[InlineData("""Title == "lamb" """, """Title == "chicken" """)]
[InlineData("""Title @=* "lamb" """, """Title @=* "chicken" """)]
Expand Down Expand Up @@ -44,15 +48,47 @@ public async Task in_list_is_one_array_parameter_and_still_filters()
await testingServiceScope.InsertAsync(lamb, chicken, beef);

var input = $"""Title ^^* ["{lamb.Title!.ToUpper()}", "{chicken.Title}"]""";
var query = testingServiceScope.DbContext().People.ApplyQueryKitFilter(input);
var query = testingServiceScope.DbContext().People.ApplyQueryKitFilter(input, ParameterizedConfig);
var people = await query.ToListAsync();

SqlWithoutParameterValues(query).Should().Contain("= ANY (@");
people.Select(x => x.Id).Should().BeEquivalentTo(new[] { lamb.Id, chicken.Id });
}

[Theory]
[InlineData("""Title == "lamb" """, "'lamb'")]
[InlineData("Age > 30", "> 30")]
[InlineData("Date == 2022-07-01", "DATE '2022-07-01'")]
[InlineData("(Age + 5) > 30", "+ 5")]
public void filter_values_are_sql_literals_by_default(string input, string expectedLiteral)
{
var testingServiceScope = new TestingServiceScope();

var sql = testingServiceScope.DbContext().People.ApplyQueryKitFilter(input).ToQueryString();

sql.Should().NotContain("@");
sql.Should().Contain(expectedLiteral);
}

[Fact]
public async Task in_list_is_a_literal_list_by_default_and_still_filters()
{
var testingServiceScope = new TestingServiceScope();
var lamb = new FakeTestingPersonBuilder().WithTitle($"lamb {Guid.NewGuid()}").Build();
var chicken = new FakeTestingPersonBuilder().WithTitle($"chicken {Guid.NewGuid()}").Build();
var beef = new FakeTestingPersonBuilder().WithTitle($"beef {Guid.NewGuid()}").Build();
await testingServiceScope.InsertAsync(lamb, chicken, beef);

var input = $"""Title ^^ ["{lamb.Title}", "{chicken.Title}"]""";
var query = testingServiceScope.DbContext().People.ApplyQueryKitFilter(input);
var people = await query.ToListAsync();

query.ToQueryString().Should().Contain($"IN ('{lamb.Title}', '{chicken.Title}')");
people.Select(x => x.Id).Should().BeEquivalentTo(new[] { lamb.Id, chicken.Id });
}

private static string SqlWithoutParameterValues(TestingServiceScope testingServiceScope, string input)
=> SqlWithoutParameterValues(testingServiceScope.DbContext().People.ApplyQueryKitFilter(input));
=> SqlWithoutParameterValues(testingServiceScope.DbContext().People.ApplyQueryKitFilter(input, ParameterizedConfig));

// ToQueryString() writes each parameter value in a "-- @p='...'" comment line before the SQL.
private static string SqlWithoutParameterValues(IQueryable<TestingPerson> query)
Expand Down
32 changes: 32 additions & 0 deletions QueryKit.IntegrationTests/Tests/PropertyResolverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,33 @@ namespace QueryKit.IntegrationTests.Tests;

public class PropertyResolverTests : TestBase
{
[Fact]
public async Task unknown_property_clause_under_or_is_true_by_default()
{
// Arrange
var testingServiceScope = new TestingServiceScope();
var title = new Faker().Lorem.Sentence();
var fakePerson = new FakeTestingPersonBuilder()
.WithTitle(title)
.WithAge(30)
.Build();
await testingServiceScope.InsertAsync(fakePerson);

var input = $"""Title == "{title}" && (Nope == "x" || Age > 100)""";
var config = new QueryKitConfiguration(config =>
{
config.AllowUnknownProperties = true;
});

// Act
var people = await testingServiceScope.DbContext().People
.ApplyQueryKitFilter(input, config)
.ToListAsync();

// Assert
people.Should().ContainSingle(x => x.Id == fakePerson.Id);
}

[Fact]
public async Task unknown_property_clause_under_or_does_not_return_every_row()
{
Expand All @@ -26,6 +53,7 @@ public async Task unknown_property_clause_under_or_does_not_return_every_row()
var input = $"""Title == "{title}" && (Nope == "x" || Age > 100)""";
var config = new QueryKitConfiguration(config =>
{
config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove;
config.AllowUnknownProperties = true;
});

Expand Down Expand Up @@ -54,6 +82,7 @@ public async Task prevented_property_clause_under_or_does_not_return_every_row()
var input = $"""Title == "{title}" && (Rating == 1 || Age > 100)""";
var config = new QueryKitConfiguration(config =>
{
config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove;
config.Property<TestingPerson>(x => x.Rating).PreventFilter();
});

Expand Down Expand Up @@ -82,6 +111,7 @@ public async Task prevented_property_clause_by_its_query_name_under_or_does_not_
var input = $"""Title == "{title}" && (first == "Paul" || Age > 100)""";
var config = new QueryKitConfiguration(config =>
{
config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove;
config.Property<TestingPerson>(x => x.FirstName).HasQueryName("first").PreventFilter().PreventSort();
});

Expand Down Expand Up @@ -138,6 +168,7 @@ public async Task prevented_property_on_the_right_side_is_not_compared()
var input = $"""Title == "{title}" && (FirstName == LastName || Age > 100)""";
var config = new QueryKitConfiguration(config =>
{
config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove;
config.Property<TestingPerson>(x => x.LastName).PreventFilter();
});

Expand Down Expand Up @@ -398,6 +429,7 @@ public async Task unknown_property_in_arithmetic_removes_the_clause_when_unknown
var input = $"""Title == "{title}" && ((Nope + 1) > 3 || Age > 100)""";
var config = new QueryKitConfiguration(config =>
{
config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove;
config.AllowUnknownProperties = true;
});

Expand Down
33 changes: 33 additions & 0 deletions QueryKit.UnitTests/ArithmeticOperatorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace QueryKit.UnitTests;

using FluentAssertions;
using QueryKit.Operators;

public class ArithmeticOperatorTests
{
[Theory]
[InlineData("+", "+")]
[InlineData("-", "-")]
[InlineData("*", "*")]
[InlineData("/", "/")]
[InlineData("%", "%")]
public void from_symbol_returns_the_operator_for_the_symbol(string symbol, string expectedSymbol)
{
#pragma warning disable CS0618 // FromSymbol stays for v1.14.2 compatibility
var op = ArithmeticOperator.FromSymbol(symbol);
#pragma warning restore CS0618

op.Should().NotBeNull();
op!.Symbol.Should().Be(expectedSymbol);
}

[Fact]
public void from_symbol_returns_null_for_an_unknown_symbol()
{
#pragma warning disable CS0618 // FromSymbol stays for v1.14.2 compatibility
var op = ArithmeticOperator.FromSymbol("^");
#pragma warning restore CS0618

op.Should().BeNull();
}
}
10 changes: 5 additions & 5 deletions QueryKit.UnitTests/CustomFilterPropertyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ public void can_have_custom_prop_excluded_from_filter()
config.Property<TestingPerson>(x => x.Id).PreventFilter();
});
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input, config);
filterExpression.ToDisplayString().Should().Be($"""x => (x.Title == "{stringValue}")""");
filterExpression.ToDisplayString().Should().Be($"""x => ((x.Title == "{stringValue}") OrElse (True == True))""");
}

[Fact]
Expand All @@ -159,7 +159,7 @@ public void can_have_custom_prop_excluded_from_filter_with_custom_propname()
config.Property<TestingPerson>(x => x.Id).HasQueryName("identifier").PreventFilter();
});
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input, config);
filterExpression.ToDisplayString().Should().Be($"""x => (x.Title == "{stringValue}")""");
filterExpression.ToDisplayString().Should().Be($"""x => ((x.Title == "{stringValue}") OrElse (True == True))""");
}

[Fact]
Expand Down Expand Up @@ -243,7 +243,7 @@ public void can_have_derived_prop_work_with_collection_filters()
}

[Fact]
public void filter_prevented_props_are_removed_regardless_of_comparison()
public void filter_prevented_props_always_have_true_equals_true_regardless_of_comparison()
{
var faker = new Faker();
var filterOperator = faker.PickRandom(ComparisonOperator.List.Where(x => x != ComparisonOperator.EqualsOperator()).ToList());
Expand All @@ -255,7 +255,7 @@ public void filter_prevented_props_are_removed_regardless_of_comparison()
config.Property<TestingPerson>(x => x.Id).PreventFilter();
});
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input, config);
filterExpression.ToDisplayString().Should().Be("x => True");
filterExpression.ToDisplayString().Should().Be("x => (True == True)");
}

[Fact]
Expand Down Expand Up @@ -284,6 +284,6 @@ public void can_handle_nonexistent_property()
config.AllowUnknownProperties = true;
});
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input, config);
filterExpression.ToDisplayString().Should().Be("x => True");
filterExpression.ToDisplayString().Should().Be("x => (True == True)");
}
}
2 changes: 1 addition & 1 deletion QueryKit.UnitTests/FilterParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public void complex_with_lots_of_types()

var filterExpression = FilterParser.ParseFilter<TestingPerson>(input);
filterExpression.ToDisplayString().Should()
.Be(""""x => (((((((((x.Title != null) AndAlso x.Title.ToLower().Contains("waffle & chicken".ToLower())) AndAlso (x.Age > 30)) OrElse (x.Id == aa648248-cb69-4217-ac95-d7484795afb2)) OrElse (x.Title == "lamb")) OrElse (x.Title == null)) AndAlso ((x.Age < 18) OrElse ((x.BirthMonth == January) AndAlso ((x.Title != null) AndAlso x.Title.StartsWith("ally"))))) OrElse (x.Rating > 3.5)) OrElse ((x.SpecificDate == new Nullable`1(new DateTimeOffset(637922304030000000, 00:00:00))) AndAlso ((x.Date == new Nullable`1(new DateOnly(2022, 7, 1))) OrElse (x.Time == new Nullable`1(new TimeOnly(0, 0, 3, 0, 0))))))"""");
.Be(""""x => (((((((((x.Title != null) AndAlso x.Title.ToLower().Contains("waffle & chicken".ToLower())) AndAlso (x.Age > 30)) OrElse (x.Id == aa648248-cb69-4217-ac95-d7484795afb2)) OrElse (x.Title == "lamb")) OrElse (x.Title == null)) AndAlso ((x.Age < 18) OrElse ((x.BirthMonth == new Nullable`1(January)) AndAlso ((x.Title != null) AndAlso x.Title.StartsWith("ally"))))) OrElse (x.Rating > 3.5)) OrElse ((x.SpecificDate == new Nullable`1(new DateTimeOffset(637922304030000000, 00:00:00))) AndAlso ((x.Date == new Nullable`1(new DateOnly(2022, 7, 1))) OrElse (x.Time == new Nullable`1(new TimeOnly(0, 0, 3, 0, 0))))))"""");
}

[Fact]
Expand Down
43 changes: 41 additions & 2 deletions QueryKit.UnitTests/FilterValueParameterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ namespace QueryKit.UnitTests;

using System.Linq.Expressions;
using FluentAssertions;
using QueryKit.Configuration;
using WebApiTestProject.Entities;

public class FilterValueParameterTests
{
private static readonly QueryKitConfiguration ParameterizedConfig =
new(settings => settings.ParameterizeFilterValues = true);

[Theory]
[InlineData("""Title == "lamb" """)]
[InlineData("""Title @=* "waffle" """)]
Expand All @@ -27,7 +31,7 @@ public class FilterValueParameterTests
[InlineData("(Age + 5) > 30")]
public void filter_values_are_field_reads_so_ef_core_sends_them_as_parameters(string input)
{
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input);
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input, ParameterizedConfig);

var constants = new ConstantCollector();
constants.Visit(filterExpression);
Expand All @@ -37,10 +41,45 @@ public void filter_values_are_field_reads_so_ef_core_sends_them_as_parameters(st
value.GetType().GetGenericTypeDefinition().Should().Be(typeof(FilterValue<>)));
}

[Theory]
[InlineData("""Title == "lamb" """, """x => (x.Title == "lamb")""")]
[InlineData("Age > 30", "x => (x.Age > 30)")]
[InlineData("BirthMonth == \"January\"", "x => (x.BirthMonth == new Nullable`1(January))")]
[InlineData("SpecificDate == 2022-07-01T00:00:03Z",
"x => (x.SpecificDate == new Nullable`1(new DateTimeOffset(637922304030000000, 00:00:00)))")]
[InlineData("Date == 2022-07-01", "x => (x.Date == new Nullable`1(new DateOnly(2022, 7, 1)))")]
[InlineData("""Time == "00:00:03.123456" """, "x => (x.Time == new Nullable`1(new TimeOnly(0, 0, 3, 123, 456)))")]
[InlineData("(Age + 5) > 30", "x => ((x.Age + Convert(5, Nullable`1)) > Convert(30, Nullable`1))")]
public void filter_values_are_constants_by_default(string input, string expected)
{
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input);

filterExpression.ToString().Should().Be(expected);
}

[Theory]
[InlineData("""Title == "lamb" """)]
[InlineData("SpecificDateTime > 2022-07-01T00:00:03")]
[InlineData("""Title ^^ ["lamb", "chicken"]""")]
[InlineData("""Title ^^* ["lamb", "chicken"]""")]
[InlineData("Age ^^ [18, 30]")]
[InlineData("(Age + 5) > 30")]
public void filter_values_do_not_use_the_holder_by_default(string input)
{
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input);

var constants = new ConstantCollector();
constants.Visit(filterExpression);

constants.Values.Should().NotBeEmpty();
constants.Values.Should().NotContain(value =>
value.GetType().IsGenericType && value.GetType().GetGenericTypeDefinition() == typeof(FilterValue<>));
}

[Fact]
public void filter_values_keep_their_value_and_type()
{
var filterExpression = FilterParser.ParseFilter<TestingPerson>("""Time == "00:00:03.123456" """);
var filterExpression = FilterParser.ParseFilter<TestingPerson>("""Time == "00:00:03.123456" """, ParameterizedConfig);

var comparison = (BinaryExpression)filterExpression.Body;
var read = (MemberExpression)comparison.Right;
Expand Down
76 changes: 76 additions & 0 deletions QueryKit.UnitTests/ParseLimitsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,80 @@ public void filter_within_configured_input_length_parses()
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input, config);
filterExpression.Should().NotBeNull();
}

[Fact]
public void configuration_that_implements_only_the_interface_uses_the_default_limits()
{
var config = new InterfaceOnlyConfiguration();

var filterExpression = FilterParser.ParseFilter<TestingPerson>("""Title == "salt" """, config);
filterExpression.Should().NotBeNull();

var tooDeep = new string('(', QueryKitSettings.DefaultMaxNestingDepth + 1)
+ """Title == "salt" """
+ new string(')', QueryKitSettings.DefaultMaxNestingDepth + 1);
var actDeep = () => FilterParser.ParseFilter<TestingPerson>(tooDeep, config);
actDeep.Should().Throw<QueryKitNestingDepthExceededException>()
.WithMessage($"*maximum allowed depth of {QueryKitSettings.DefaultMaxNestingDepth}*");

var tooLong = $"""Title == "{new string('a', QueryKitSettings.DefaultMaxInputLength)}" """;
var actLong = () => FilterParser.ParseFilter<TestingPerson>(tooLong, config);
actLong.Should().Throw<QueryKitInputLengthExceededException>()
.WithMessage($"*maximum allowed length of {QueryKitSettings.DefaultMaxInputLength}*");
}

[Fact]
public void configuration_that_implements_the_parse_limits_uses_its_own_limits()
{
var config = new InterfaceOnlyConfigurationWithLimits { MaxNestingDepth = 2, MaxInputLength = 100 };
var input = new string('(', 3) + """Title == "salt" """ + new string(')', 3);

var act = () => FilterParser.ParseFilter<TestingPerson>(input, config);
act.Should().Throw<QueryKitNestingDepthExceededException>()
.WithMessage("*depth of 3*maximum allowed depth of 2*");
}

private sealed class InterfaceOnlyConfigurationWithLimits : InterfaceOnlyConfiguration, IQueryKitParseLimits
{
public int MaxNestingDepth { get; set; }
public int MaxInputLength { get; set; }
}

// Implements only the IQueryKitConfiguration members of v1.14.2. This class does not compile
// when the interface gets a new member.
private class InterfaceOnlyConfiguration : IQueryKitConfiguration
{
public QueryKitPropertyMappings PropertyMappings { get; } = new();
public string EqualsOperator { get; set; } = "==";
public string NotEqualsOperator { get; set; } = "!=";
public string GreaterThanOperator { get; set; } = ">";
public string LessThanOperator { get; set; } = "<";
public string GreaterThanOrEqualOperator { get; set; } = ">=";
public string LessThanOrEqualOperator { get; set; } = "<=";
public string ContainsOperator { get; set; } = "@=";
public string StartsWithOperator { get; set; } = "_=";
public string EndsWithOperator { get; set; } = "_-=";
public string NotContainsOperator { get; set; } = "!@=";
public string NotStartsWithOperator { get; set; } = "!_=";
public string NotEndsWithOperator { get; set; } = "!_-=";
public string InOperator { get; set; } = "^^";
public string NotInOperator { get; set; } = "!^^";
public string SoundsLikeOperator { get; set; } = "~~";
public string DoesNotSoundLikeOperator { get; set; } = "!~";
public string CaseInsensitiveAppendix { get; set; } = "*";
public string AndOperator { get; set; } = "&&";
public string OrOperator { get; set; } = "||";
public bool AllowUnknownProperties { get; set; }
public Type? DbContextType { get; set; }
public string HasCountEqualToOperator { get; set; } = "#==";
public string HasCountNotEqualToOperator { get; set; } = "#!=";
public string HasCountGreaterThanOperator { get; set; } = "#>";
public string HasCountLessThanOperator { get; set; } = "#<";
public string HasCountGreaterThanOrEqualOperator { get; set; } = "#>=";
public string HasCountLessThanOrEqualOperator { get; set; } = "#<=";
public string HasOperator { get; set; } = "^$";
public string DoesNotHaveOperator { get; set; } = "!^$";
public int? MaxPropertyDepth { get; set; }
public CaseInsensitiveMode CaseInsensitiveComparison { get; set; } = CaseInsensitiveMode.Lower;
}
}
Loading
Loading