From 3c00b4decb2d4e2e269184e7ad003bc02b0032e9 Mon Sep 17 00:00:00 2001 From: Arnout Born Date: Fri, 3 Jun 2022 12:04:24 +0200 Subject: [PATCH] allow '<' & '>' to be used in strings --- Sieve/Models/FilterTerm.cs | 11 +- SieveUnitTests/GeneralSpecialChars.cs | 334 ++++++++++++++++++++++++ SieveUnitTests/StringFilterNullTests.cs | 65 +++-- 3 files changed, 382 insertions(+), 28 deletions(-) create mode 100644 SieveUnitTests/GeneralSpecialChars.cs diff --git a/Sieve/Models/FilterTerm.cs b/Sieve/Models/FilterTerm.cs index 8ed0da0..baccffa 100644 --- a/Sieve/Models/FilterTerm.cs +++ b/Sieve/Models/FilterTerm.cs @@ -22,19 +22,20 @@ public string Filter { set { - var filterSplits = Regex.Split(value, EscapeNegPatternForOper).Select(t => t.Trim()).ToArray(); + var filterSplits = Regex.Split(value, EscapeNegPatternForOper); - Names = Regex.Split(filterSplits[0], EscapedPipePattern).Select(t => t.Trim()).ToArray(); + Names = Regex.Split(filterSplits[0].Trim(), EscapedPipePattern).Select(t => t.Trim()).ToArray(); if (filterSplits.Length > 2) { - foreach (var match in Regex.Matches(filterSplits[2], EscapePosPatternForOper)) + var filterValue = string.Join("", filterSplits.Skip(2)).Trim(); + foreach (var match in Regex.Matches(filterValue, EscapePosPatternForOper)) { var matchStr = match.ToString(); - filterSplits[2] = filterSplits[2].Replace('\\' + matchStr, matchStr); + filterValue = filterValue.Replace('\\' + matchStr, matchStr); } - Values = Regex.Split(filterSplits[2], EscapedPipePattern) + Values = Regex.Split(filterValue, EscapedPipePattern) .Select(UnEscape) .ToArray(); } diff --git a/SieveUnitTests/GeneralSpecialChars.cs b/SieveUnitTests/GeneralSpecialChars.cs new file mode 100644 index 0000000..88c7758 --- /dev/null +++ b/SieveUnitTests/GeneralSpecialChars.cs @@ -0,0 +1,334 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Sieve.Exceptions; +using Sieve.Models; +using Sieve.Services; +using SieveUnitTests.Entities; +using SieveUnitTests.Services; +using Xunit; +using Xunit.Abstractions; + +namespace SieveUnitTests +{ + public class GeneralSpecialChars + { + private readonly ITestOutputHelper _testOutputHelper; + private readonly SieveProcessor _processor; + private readonly SieveProcessor _nullableProcessor; + private readonly IQueryable _posts; + private readonly IQueryable _comments; + + public GeneralSpecialChars(ITestOutputHelper testOutputHelper) + { + var nullableAccessor = new SieveOptionsAccessor(); + nullableAccessor.Value.IgnoreNullsOnNotEqual = false; + + _testOutputHelper = testOutputHelper; + _processor = new ApplicationSieveProcessor(new SieveOptionsAccessor(), + new SieveCustomSortMethods(), + new SieveCustomFilterMethods()); + + _nullableProcessor = new ApplicationSieveProcessor(nullableAccessor, + new SieveCustomSortMethods(), + new SieveCustomFilterMethods()); + + _posts = new List + { + new Post + { + Id = 0, + Title = "A<>1", + LikeCount = 100, + IsDraft = true, + CategoryId = null, + TopComment = new Comment { Id = 0, Text = "A1" }, + FeaturedComment = new Comment { Id = 4, Text = "A2" } + }, + new Post + { + Id = 1, + Title = "B>2", + LikeCount = 50, + IsDraft = false, + CategoryId = 1, + TopComment = new Comment { Id = 3, Text = "B1" }, + FeaturedComment = new Comment { Id = 5, Text = "B2" } + }, + new Post + { + Id = 2, + Title = "C==E", + LikeCount = 0, + CategoryId = 1, + TopComment = new Comment { Id = 2, Text = "C1" }, + FeaturedComment = new Comment { Id = 6, Text = "C2" } + }, + new Post + { + Id = 3, + Title = "D@=k", + LikeCount = 3, + IsDraft = true, + CategoryId = 2, + TopComment = new Comment { Id = 1, Text = "D1" }, + FeaturedComment = new Comment { Id = 7, Text = "D2" } + }, + new Post + { + Id = 4, + Title = "Yen!=Yin", + LikeCount = 5, + IsDraft = true, + CategoryId = 5, + TopComment = new Comment { Id = 4, Text = "Yen3" }, + FeaturedComment = new Comment { Id = 8, Text = "Yen4" } + } + }.AsQueryable(); + + _comments = new List + { + new Comment + { + Id = 0, + DateCreated = DateTimeOffset.UtcNow.AddDays(-20), + Text = "This is an old comment." + }, + new Comment + { + Id = 1, + DateCreated = DateTimeOffset.UtcNow.AddDays(-1), + Text = "This is a fairly new comment." + }, + new Comment + { + Id = 2, + DateCreated = DateTimeOffset.UtcNow, + Text = "This is a brand new comment. (Text in braces, comma separated)" + }, + }.AsQueryable(); + } + + [Fact] + public void ContainsCanBeCaseInsensitive() + { + var model = new SieveModel + { + Filters = "Title@=*a<" + }; + + var result = _processor.Apply(model, _posts); + + Assert.Equal(0, result.First().Id); + Assert.True(result.Count() == 1); + } + + [Fact] + public void NotEqualsCanBeCaseInsensitive() + { + var model = new SieveModel + { + Filters = "Title!=*a<>1" + }; + + var result = _processor.Apply(model, _posts); + + Assert.Equal(1, result.First().Id); + Assert.True(result.Count() == 4); + } + + [Fact] + public void EndsWithWorks() + { + var model = new SieveModel + { + Filters = "Title_-=n" + }; + + _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].Values.ToString()); + _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].Operator); + _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].OperatorParsed.ToString()); + + var result = _processor.Apply(model, _posts); + + Assert.Equal(4, result.First().Id); + Assert.True(result.Count() == 1); + } + + [Fact] + public void EndsWithCanBeCaseInsensitive() + { + var model = new SieveModel + { + Filters = "Title_-=*N" + }; + + _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].Values.ToString()); + _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].Operator); + _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].OperatorParsed.ToString()); + + var result = _processor.Apply(model, _posts); + + Assert.Equal(4, result.First().Id); + Assert.True(result.Count() == 1); + } + + [Fact] + public void ContainsIsCaseSensitive() + { + var model = new SieveModel + { + Filters = "Title@=a", + }; + + var result = _processor.Apply(model, _posts); + + Assert.True(!result.Any()); + } + + [Fact] + public void NotContainsWorks() + { + var model = new SieveModel + { + Filters = "Title!@=D", + }; + + var result = _processor.Apply(model, _posts); + + Assert.True(result.Count() == 4); + } + + [Theory] + [InlineData(@"Text@=*\,")] + [InlineData(@"Text@=*\, ")] + [InlineData(@"Text@=*braces\,")] + [InlineData(@"Text@=*braces\, comma")] + public void CanFilterWithEscapedComma(string filter) + { + var model = new SieveModel + { + Filters = filter + }; + + var result = _processor.Apply(model, _comments); + + Assert.True(result.Count() == 1); + } + + [Fact] + public void CustomFiltersWithOperatorsWork() + { + var model = new SieveModel + { + Filters = "HasInTitle==A", + }; + + var result = _processor.Apply(model, _posts); + + Assert.True(result.Any(p => p.Id == 0)); + Assert.True(result.Count() == 1); + } + + [Fact] + public void CustomFiltersMixedWithUsualWork1() + { + var model = new SieveModel + { + Filters = "Isnew,CategoryId==2", + }; + + var result = _processor.Apply(model, _posts); + + Assert.True(result.Any(p => p.Id == 3)); + Assert.True(result.Count() == 1); + } + + [Fact] + public void CombinedAndOrWithSpaceFilteringWorks() + { + var model = new SieveModel + { + Filters = "Title==D@=k, (Title|LikeCount)==3", + }; + + var result = _processor.Apply(model, _posts); + var entry = result.FirstOrDefault(); + var resultCount = result.Count(); + + Assert.NotNull(entry); + Assert.Equal(1, resultCount); + Assert.Equal(3, entry.Id); + } + + [Fact] + public void OrValueFilteringWorks() + { + var model = new SieveModel + { + Filters = "Title==C==E|D@=k", + }; + + var result = _processor.Apply(model, _posts); + Assert.Equal(2, result.Count()); + Assert.True(result.Any(p => p.Id == 2)); + Assert.True(result.Any(p => p.Id == 3)); + } + + [Fact] + public void OrValueFilteringWorks2() + { + var model = new SieveModel + { + Filters = "Text@=(|)", + }; + + var result = _processor.Apply(model, _comments); + Assert.Equal(1, result.Count()); + Assert.Equal(2, result.FirstOrDefault()?.Id); + } + + [Fact] + public void NestedFilteringWorks() + { + var model = new SieveModel + { + Filters = "TopComment.Text!@=A", + }; + + var result = _processor.Apply(model, _posts); + Assert.Equal(4, result.Count()); + var posts = result.ToList(); + Assert.Contains("B", posts[0].TopComment.Text); + Assert.Contains("C", posts[1].TopComment.Text); + Assert.Contains("D", posts[2].TopComment.Text); + Assert.Contains("Yen", posts[3].TopComment.Text); + } + + [Fact] + public void FilteringNullsWorks() + { + var posts = new List + { + new Post + { + Id = 1, + Title = null, + LikeCount = 0, + IsDraft = false, + CategoryId = null, + TopComment = null, + FeaturedComment = null + }, + }.AsQueryable(); + + var model = new SieveModel + { + Filters = "FeaturedComment.Text!@=Some value", + }; + + var result = _processor.Apply(model, posts); + Assert.Equal(0, result.Count()); + } + } +} diff --git a/SieveUnitTests/StringFilterNullTests.cs b/SieveUnitTests/StringFilterNullTests.cs index 42be74b..26328b4 100644 --- a/SieveUnitTests/StringFilterNullTests.cs +++ b/SieveUnitTests/StringFilterNullTests.cs @@ -30,23 +30,30 @@ public StringFilterNullTests() new Comment { Id = 1, - DateCreated = DateTimeOffset.UtcNow, + DateCreated = DateTimeOffset.UtcNow, Text = "null is here twice in the text ending by null", Author = "Cat", }, new Comment { - Id = 2, - DateCreated = DateTimeOffset.UtcNow, + Id = 2, + DateCreated = DateTimeOffset.UtcNow, Text = "Regular comment without n*ll", Author = "Mouse", }, new Comment { - Id = 100, - DateCreated = DateTimeOffset.UtcNow, + Id = 100, + DateCreated = DateTimeOffset.UtcNow, Text = null, Author = "null", + }, + new Comment + { + Id = 105, + DateCreated = DateTimeOffset.UtcNow, + Text = "The duck wrote this", + Author = "Duck <5", } }.AsQueryable(); } @@ -56,23 +63,35 @@ public StringFilterNullTests() [InlineData("Text==*null")] public void Filter_Equals_Null(string filter) { - var model = new SieveModel {Filters = filter}; + var model = new SieveModel { Filters = filter }; var result = _processor.Apply(model, _comments); Assert.Equal(100, result.Single().Id); } + [Theory] + [InlineData("Author==Duck <5")] + [InlineData("Text|Author==Duck <5")] + public void Filter_Equals_WithSpecialChar(string filter) + { + var model = new SieveModel { Filters = filter }; + + var result = _processor.Apply(model, _comments); + + Assert.Equal(105, result.Single().Id); + } + [Theory] [InlineData("Text!=null")] [InlineData("Text!=*null")] public void Filter_NotEquals_Null(string filter) { - var model = new SieveModel {Filters = filter}; + var model = new SieveModel { Filters = filter }; var result = _processor.Apply(model, _comments); - Assert.Equal(new[] {0, 1, 2}, result.Select(p => p.Id)); + Assert.Equal(new[] { 0, 1, 2, 105 }, result.Select(p => p.Id)); } [Theory] @@ -83,13 +102,13 @@ public void Filter_NotEquals_Null(string filter) [InlineData("Text@=*null|text")] public void Filter_Contains_NullString(string filter) { - var model = new SieveModel {Filters = filter}; + var model = new SieveModel { Filters = filter }; var result = _processor.Apply(model, _comments); - Assert.Equal(new[] {0, 1}, result.Select(p => p.Id)); + Assert.Equal(new[] { 0, 1 }, result.Select(p => p.Id)); } - + [Theory] [InlineData("Text|Author==null", 100)] [InlineData("Text|Author@=null", 0, 1, 100)] @@ -98,7 +117,7 @@ public void Filter_Contains_NullString(string filter) [InlineData("Text|Author_=*null", 1, 100)] public void MultiFilter_Contains_NullString(string filter, params int[] expectedIds) { - var model = new SieveModel {Filters = filter}; + var model = new SieveModel { Filters = filter }; var result = _processor.Apply(model, _comments); @@ -109,12 +128,12 @@ public void MultiFilter_Contains_NullString(string filter, params int[] expected [InlineData(@"Author==\null", 100)] [InlineData(@"Author==*\null", 100)] [InlineData(@"Author==*\NuLl", 100)] - [InlineData(@"Author!=*\null", 0, 1, 2)] - [InlineData(@"Author!=*\NulL", 0, 1, 2)] - [InlineData(@"Author!=\null", 0, 1, 2)] + [InlineData(@"Author!=*\null", 0, 1, 2, 105)] + [InlineData(@"Author!=*\NulL", 0, 1, 2, 105)] + [InlineData(@"Author!=\null", 0, 1, 2, 105)] public void SingleFilter_Equals_NullStringEscaped(string filter, params int[] expectedIds) { - var model = new SieveModel {Filters = filter}; + var model = new SieveModel { Filters = filter }; var result = _processor.Apply(model, _comments); @@ -129,11 +148,11 @@ public void SingleFilter_Equals_NullStringEscaped(string filter, params int[] ex [InlineData("Text_=*null|text")] public void Filter_StartsWith_NullString(string filter) { - var model = new SieveModel {Filters = filter}; + var model = new SieveModel { Filters = filter }; var result = _processor.Apply(model, _comments); - Assert.Equal(new[] {1}, result.Select(p => p.Id)); + Assert.Equal(new[] { 1 }, result.Select(p => p.Id)); } [Theory] @@ -159,11 +178,11 @@ public void Filter_EndsWith_NullString(string filter) [InlineData("Text!@=*null|text")] public void Filter_DoesNotContain_NullString(string filter) { - var model = new SieveModel {Filters = filter}; + var model = new SieveModel { Filters = filter }; var result = _processor.Apply(model, _comments); - Assert.Equal(new[] {2}, result.Select(p => p.Id)); + Assert.Equal(new[] { 2, 105 }, result.Select(p => p.Id)); } [Theory] @@ -173,11 +192,11 @@ public void Filter_DoesNotContain_NullString(string filter) [InlineData("Text!_=*NulL")] public void Filter_DoesNotStartsWith_NullString(string filter) { - var model = new SieveModel {Filters = filter}; + var model = new SieveModel { Filters = filter }; var result = _processor.Apply(model, _comments); - Assert.Equal(new[] {0, 2}, result.Select(p => p.Id)); + Assert.Equal(new[] { 0, 2, 105 }, result.Select(p => p.Id)); } [Theory] @@ -191,7 +210,7 @@ public void Filter_DoesNotEndsWith_NullString(string filter) var result = _processor.Apply(model, _comments); - Assert.Equal(new[] { 0, 2 }, result.Select(p => p.Id)); + Assert.Equal(new[] { 0, 2, 105 }, result.Select(p => p.Id)); } } }