Skip to content

Commit ca5531f

Browse files
authored
Query ExpressionBuilder BugFixing (#160)
* Implement proper quoted string parser * Further optimize and abstract out filter query gen * Start creating tests for query string tokenizer * Add empty string test * Fix assertions * Fix off-by-one bug * Couple fixes * More test and bugfixes * Fix more bugs, add more tests * And more fixes * Add Bogus * Fix tests
1 parent 7e1511e commit ca5531f

File tree

11 files changed

+853
-292
lines changed

11 files changed

+853
-292
lines changed

API/Controller/Admin/GetUsers.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using OpenShock.Common.Utils;
99
using Z.EntityFramework.Plus;
1010
using OpenShock.Common.OpenShockDb;
11+
using OpenShock.Common.Query;
1112

1213
namespace OpenShock.API.Controller.Admin;
1314

@@ -47,7 +48,11 @@ public async Task<IActionResult> GetUsers(
4748
query = query.OrderBy(u => u.CreatedAt);
4849
}
4950
}
50-
catch (ExpressionBuilder.ExpressionException e)
51+
catch (QueryStringTokenizerException e)
52+
{
53+
return Problem(ExpressionError.QueryStringInvalidError(e.Message));
54+
}
55+
catch (DBExpressionBuilderException e)
5156
{
5257
return Problem(ExpressionError.ExpressionExceptionError(e.Message));
5358
}

Common.Tests/Common.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<PackageReference Include="Testcontainers" Version="4.1.0" />
66
<PackageReference Include="Testcontainers.PostgreSql" Version="4.1.0" />
77
<PackageReference Include="Testcontainers.Redis" Version="4.1.0" />
8+
<PackageReference Include="Bogus" Version="35.6.1" />
89
<PackageReference Include="TUnit" Version="0.11.0" />
910
</ItemGroup>
1011

Common.Tests/Geo/Alpha2CountryCodeTests.cs

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using OpenShock.Common.Geo;
2+
using TUnit.Assertions.AssertConditions.Throws;
23

34
namespace OpenShock.Common.Tests.Geo;
45

@@ -22,15 +23,13 @@ public async Task ValidCode_ShouldParse(string str, char char1, char char2)
2223
[Arguments("INVALID")]
2324
public async Task InvalidCharCount_ShouldThrow_InvalidLength(string str)
2425
{
25-
// Act
26-
var ex = await Assert.ThrowsAsync<ArgumentException>(() =>
27-
{
28-
Alpha2CountryCode c = str;
29-
return Task.CompletedTask;
30-
});
31-
32-
// Assert
33-
await Assert.That(ex.Message).IsEqualTo("Country code must be exactly 2 characters long (Parameter 'str')");
26+
// Act & Assert
27+
await Assert.That(() =>
28+
{
29+
Alpha2CountryCode c = str;
30+
})
31+
.ThrowsExactly<ArgumentOutOfRangeException>()
32+
.WithMessage("Country code must be exactly 2 characters long (Parameter 'str')");
3433
}
3534

3635
[Test]
@@ -44,15 +43,13 @@ public async Task InvalidCharCount_ShouldThrow_InvalidLength(string str)
4443
[Arguments(":D")]
4544
public async Task InvalidCharTypes_ShouldThrow(string str)
4645
{
47-
// Act
48-
var ex = await Assert.ThrowsAsync<ArgumentException>(() =>
49-
{
50-
Alpha2CountryCode c = str;
51-
return Task.CompletedTask;
52-
});
53-
54-
// Assert
55-
await Assert.That(ex.Message).IsEqualTo("Country code must be uppercase ASCII characters only (Parameter 'str')");
46+
// Act & Assert
47+
await Assert.That(() =>
48+
{
49+
Alpha2CountryCode c = str;
50+
})
51+
.ThrowsExactly<ArgumentOutOfRangeException>()
52+
.WithMessage("Country code must be uppercase ASCII characters only (Parameter 'str')");
5653
}
5754

5855
[Test]
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
using OpenShock.Common.Query;
2+
using TUnit.Assertions.AssertConditions.Throws;
3+
using Bogus;
4+
5+
namespace OpenShock.Common.Tests.Query;
6+
7+
public class DBExpressionBuilderTests
8+
{
9+
public sealed class TestClass
10+
{
11+
public required Guid Id { get; set; }
12+
public required string Name { get; set; }
13+
public required int Age { get; set; }
14+
public required uint Height { get; set; }
15+
public required bool IsActive { get; set; }
16+
public required DateTime CreatedAt { get; set; }
17+
public required TestEnum Status { get; set; }
18+
public required float Score { get; set; }
19+
public required double Precision { get; set; }
20+
}
21+
22+
public enum TestEnum
23+
{
24+
Pending,
25+
Active,
26+
Inactive
27+
}
28+
29+
private readonly TestClass[] TestArray;
30+
31+
public DBExpressionBuilderTests()
32+
{
33+
var faker = new Faker<TestClass>()
34+
.UseSeed(12345)
35+
.RuleFor(t => t.Id, f => Guid.CreateVersion7())
36+
.RuleFor(t => t.Name, f => f.Name.FullName())
37+
.RuleFor(t => t.Age, f => f.Random.Int(18, 99))
38+
.RuleFor(t => t.Height, f => f.Random.UInt())
39+
.RuleFor(t => t.IsActive, f => f.Random.Bool())
40+
.RuleFor(t => t.CreatedAt, f => f.Date.Past(10))
41+
.RuleFor(t => t.Status, f => f.PickRandom<TestEnum>())
42+
.RuleFor(t => t.Score, f => f.Random.Float(0, 100))
43+
.RuleFor(t => t.Precision, f => f.Random.Double(0, 100));
44+
45+
TestArray = faker.Generate(100).ToArray();
46+
}
47+
48+
[Test]
49+
public async Task EmptyString_ThrowsException()
50+
{
51+
// Act & Assert
52+
await Assert
53+
.That(() => DBExpressionBuilder.GetFilterExpression<TestClass>(""))
54+
.ThrowsExactly<DBExpressionBuilderException>();
55+
}
56+
57+
[Test]
58+
public async Task IntegerBounds_ThrowsExceptionOnOverflow()
59+
{
60+
// Act & Assert
61+
await Assert
62+
.That(() => DBExpressionBuilder.GetFilterExpression<TestClass>("age eq 2147483648"))
63+
.ThrowsExactly<OverflowException>();
64+
}
65+
66+
[Test]
67+
public async Task UnsignedIntegerBounds_ThrowsExceptionOnNegative()
68+
{
69+
// Act & Assert
70+
await Assert
71+
.That(() => DBExpressionBuilder.GetFilterExpression<TestClass>("height eq -1"))
72+
.ThrowsExactly<OverflowException>();
73+
}
74+
75+
[Test]
76+
public async Task Guid_ExactMatch()
77+
{
78+
// Act
79+
var testGuid = TestArray.First().Id; // Grab a Guid from the test data
80+
var expression = DBExpressionBuilder.GetFilterExpression<TestClass>($"id eq {testGuid}");
81+
var result = TestArray.AsQueryable().Where(expression).ToArray();
82+
83+
// Assert
84+
await Assert.That(result).ContainsOnly(x => x.Id == testGuid);
85+
}
86+
87+
[Test]
88+
public async Task Integer_GreaterThanOrEquals()
89+
{
90+
// Act
91+
var expression = DBExpressionBuilder.GetFilterExpression<TestClass>("age gte 42");
92+
var result = TestArray.AsQueryable().Where(expression).ToArray();
93+
94+
// Assert
95+
await Assert.That(result).ContainsOnly(x => x.Age >= 42);
96+
}
97+
98+
[Test]
99+
public async Task Integer_LessThanOrEquals()
100+
{
101+
// Act
102+
var expression = DBExpressionBuilder.GetFilterExpression<TestClass>("age lte 51");
103+
var result = TestArray.AsQueryable().Where(expression).ToArray();
104+
105+
// Assert
106+
await Assert.That(result).ContainsOnly(x => x.Age <= 51);
107+
}
108+
109+
// TODO: Make enums work
110+
/*
111+
[Test]
112+
public async Task Enum_ChecksValidValues()
113+
{
114+
// Act
115+
var expression = DBExpressionBuilder.GetFilterExpression<TestClass>("status eq Active");
116+
var result = TestArray.AsQueryable().Where(expression).ToArray();
117+
118+
// Assert
119+
await Assert.That(result).HasCount().GreaterThan(0);
120+
}
121+
122+
[Test]
123+
public async Task Enum_InvalidValue_ThrowsException()
124+
{
125+
// Act & Assert
126+
await Assert
127+
.That(() => DBExpressionBuilder.GetFilterExpression<TestClass>("status eq Invalid"))
128+
.ThrowsExactly<DBExpressionBuilderException>();
129+
}
130+
*/
131+
132+
[Test]
133+
public async Task Boolean_TrueMatches()
134+
{
135+
// Act
136+
var expression = DBExpressionBuilder.GetFilterExpression<TestClass>("isActive eq true");
137+
var result = TestArray.AsQueryable().Where(expression).ToArray();
138+
139+
// Assert
140+
await Assert.That(result).ContainsOnly(x => x.IsActive == true);
141+
}
142+
143+
[Test]
144+
public async Task Boolean_FalseMatches()
145+
{
146+
// Act
147+
var expression = DBExpressionBuilder.GetFilterExpression<TestClass>("isActive eq false");
148+
var result = TestArray.AsQueryable().Where(expression).ToArray();
149+
150+
// Assert
151+
await Assert.That(result).ContainsOnly(x => x.IsActive == false);
152+
}
153+
154+
[Test]
155+
public async Task DateTime_ExactMatch()
156+
{
157+
// Act
158+
var testDate = TestArray[20].CreatedAt;
159+
var expression = DBExpressionBuilder.GetFilterExpression<TestClass>($"createdAt eq {testDate:O}");
160+
var result = TestArray.AsQueryable().Where(expression).ToArray();
161+
162+
// Assert
163+
await Assert.That(result).ContainsOnly(x => x.CreatedAt == testDate);
164+
}
165+
166+
[Test]
167+
public async Task DateTime_LessThan()
168+
{
169+
// Act
170+
var referenceDate = DateTime.UtcNow.AddMonths(-6);
171+
var expression = DBExpressionBuilder.GetFilterExpression<TestClass>($"createdAt lt {referenceDate:O}");
172+
var result = TestArray.AsQueryable().Where(expression).ToArray();
173+
174+
// Assert
175+
await Assert.That(result).ContainsOnly(x => x.CreatedAt < referenceDate);
176+
}
177+
178+
[Test]
179+
public async Task Float_GreaterThan()
180+
{
181+
// Act
182+
var expression = DBExpressionBuilder.GetFilterExpression<TestClass>("score gt 50");
183+
var result = TestArray.AsQueryable().Where(expression).ToArray();
184+
185+
// Assert
186+
await Assert.That(result).ContainsOnly(x => x.Score > 50f);
187+
}
188+
189+
[Test]
190+
public async Task Double_LessThan()
191+
{
192+
// Act
193+
var expression = DBExpressionBuilder.GetFilterExpression<TestClass>("precision lt 50");
194+
var result = TestArray.AsQueryable().Where(expression).ToArray();
195+
196+
// Assert
197+
await Assert.That(result).ContainsOnly(x => x.Precision < 50f);
198+
}
199+
}

0 commit comments

Comments
 (0)