Skip to content

Commit 08dc697

Browse files
authored
🎨 standardize exception handling and improve culture-invariant string formatting (#11)
1 parent 5ea801d commit 08dc697

File tree

11 files changed

+373
-96
lines changed

11 files changed

+373
-96
lines changed

QsNet.Tests/DecodeOptionsTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ public void DecodeKey_ShouldThrow_When_DecodeDotInKeysTrue_And_AllowDotsFalse()
121121
};
122122

123123
Action act = () => options.DecodeKey("a%2Eb", Encoding.UTF8);
124-
act.Should().Throw<ArgumentException>()
124+
act.Should().Throw<InvalidOperationException>()
125125
.Where(e => e.Message.Contains("decodeDotInKeys", StringComparison.OrdinalIgnoreCase)
126126
&& e.Message.Contains("allowDots", StringComparison.OrdinalIgnoreCase));
127127
}

QsNet.Tests/DecodeTests.cs

Lines changed: 100 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections;
33
using System.Collections.Generic;
4+
using System.Linq;
45
using System.Text;
56
using System.Text.RegularExpressions;
67
using System.Web;
@@ -272,7 +273,7 @@ public void Should_Throw_When_Comma_List_Limit_Exceeded()
272273

273274
action
274275
.Should()
275-
.Throw<IndexOutOfRangeException>()
276+
.Throw<InvalidOperationException>()
276277
.WithMessage("List limit exceeded. Only 3 elements allowed in a list.");
277278
}
278279

@@ -2277,7 +2278,7 @@ public void Decode_StrictDepth_ThrowsExceptionForMultipleNestedObjectsWithStrict
22772278
var options = new DecodeOptions { Depth = 1, StrictDepth = true };
22782279

22792280
Action act = () => Qs.Decode("a[b][c][d][e][f][g][h][i]=j", options);
2280-
act.Should().Throw<IndexOutOfRangeException>();
2281+
act.Should().Throw<InvalidOperationException>();
22812282
}
22822283

22832284
[Fact]
@@ -2286,7 +2287,7 @@ public void Decode_StrictDepth_ThrowsExceptionForMultipleNestedListsWithStrictDe
22862287
var options = new DecodeOptions { Depth = 3, StrictDepth = true };
22872288

22882289
Action act = () => Qs.Decode("a[0][1][2][3][4]=b", options);
2289-
act.Should().Throw<IndexOutOfRangeException>();
2290+
act.Should().Throw<InvalidOperationException>();
22902291
}
22912292

22922293
[Fact]
@@ -2295,7 +2296,7 @@ public void Decode_StrictDepth_ThrowsExceptionForNestedMapsAndListsWithStrictDep
22952296
var options = new DecodeOptions { Depth = 3, StrictDepth = true };
22962297

22972298
Action act = () => Qs.Decode("a[b][c][0][d][e]=f", options);
2298-
act.Should().Throw<IndexOutOfRangeException>();
2299+
act.Should().Throw<InvalidOperationException>();
22992300
}
23002301

23012302
[Fact]
@@ -2304,7 +2305,7 @@ public void Decode_StrictDepth_ThrowsExceptionForDifferentTypesOfValuesWithStric
23042305
var options = new DecodeOptions { Depth = 3, StrictDepth = true };
23052306

23062307
Action act = () => Qs.Decode("a[b][c][d][e]=true&a[b][c][d][f]=42", options);
2307-
act.Should().Throw<IndexOutOfRangeException>();
2308+
act.Should().Throw<InvalidOperationException>();
23082309
}
23092310

23102311
[Fact]
@@ -2405,7 +2406,7 @@ public void Decode_ParameterLimit_ThrowsErrorWhenParameterLimitExceeded()
24052406
var options = new DecodeOptions { ParameterLimit = 3, ThrowOnLimitExceeded = true };
24062407

24072408
Action act = () => Qs.Decode("a=1&b=2&c=3&d=4&e=5&f=6", options);
2408-
act.Should().Throw<IndexOutOfRangeException>();
2409+
act.Should().Throw<InvalidOperationException>();
24092410
}
24102411

24112412
[Fact]
@@ -2483,7 +2484,7 @@ public void Decode_ListLimit_ThrowsErrorWhenListLimitExceeded()
24832484
var options = new DecodeOptions { ListLimit = 3, ThrowOnLimitExceeded = true };
24842485

24852486
Action act = () => Qs.Decode("a[]=1&a[]=2&a[]=3&a[]=4", options);
2486-
act.Should().Throw<IndexOutOfRangeException>();
2487+
act.Should().Throw<InvalidOperationException>();
24872488
}
24882489

24892490
[Fact]
@@ -2530,7 +2531,7 @@ public void Decode_ListLimit_HandlesNegativeListLimitCorrectly()
25302531
var options = new DecodeOptions { ListLimit = -1, ThrowOnLimitExceeded = true };
25312532

25322533
Action act = () => Qs.Decode("a[]=1&a[]=2", options);
2533-
act.Should().Throw<IndexOutOfRangeException>();
2534+
act.Should().Throw<InvalidOperationException>();
25342535
}
25352536

25362537
[Fact]
@@ -2539,7 +2540,7 @@ public void Decode_ListLimit_AppliesListLimitToNestedLists()
25392540
var options = new DecodeOptions { ListLimit = 3, ThrowOnLimitExceeded = true };
25402541

25412542
Action act = () => Qs.Decode("a[0][]=1&a[0][]=2&a[0][]=3&a[0][]=4", options);
2542-
act.Should().Throw<IndexOutOfRangeException>();
2543+
act.Should().Throw<InvalidOperationException>();
25432544
}
25442545

25452546
[Fact]
@@ -4127,10 +4128,50 @@ public void Decode_NonGeneric_Hashtable_Is_Normalised()
41274128
decoded.Should().Equal(new Dictionary<string, object?> { ["x"] = 1, ["2"] = "y" });
41284129
}
41294130

4131+
[Fact]
4132+
public void Decode_CommaSplit_NoTruncationWhenSumExceedsLimit_AndThrowOff()
4133+
{
4134+
var opts = new DecodeOptions
4135+
{
4136+
Comma = true,
4137+
ListLimit = 3,
4138+
ThrowOnLimitExceeded = false,
4139+
ParseLists = true,
4140+
Duplicates = Duplicates.Combine
4141+
};
4142+
4143+
var result = Qs.Decode("a=1,2&a=3,4,5", opts);
4144+
4145+
var dict = Assert.IsType<Dictionary<string, object?>>(result);
4146+
var list = Assert.IsType<List<object?>>(dict["a"]);
4147+
// With ThrowOnLimitExceeded = false, no truncation occurs; full concatenation is allowed
4148+
list.Select(x => x?.ToString()).Should().Equal("1", "2", "3", "4", "5");
4149+
}
4150+
4151+
[Fact]
4152+
public void Decode_BracketSingle_CommaSplit_YieldsNestedList()
4153+
{
4154+
var opts = new DecodeOptions { Comma = true };
4155+
4156+
// Control: unbracketed key
4157+
var res = Qs.Decode("a=1,2,3", opts);
4158+
var dict1 = Assert.IsType<Dictionary<string, object?>>(res);
4159+
var list1 = Assert.IsType<List<object?>>(dict1["a"]);
4160+
list1.Select(x => x?.ToString()).Should().Equal("1", "2", "3");
4161+
4162+
// Bracketed single occurrence yields a nested list: [["1","2","3"]]
4163+
var res2 = Qs.Decode("a[]=1,2,3", opts);
4164+
var dict2 = Assert.IsType<Dictionary<string, object?>>(res2);
4165+
var outer = Assert.IsType<List<object?>>(dict2["a"]);
4166+
outer.Should().HaveCount(1);
4167+
var inner = Assert.IsType<List<object?>>(outer[0]);
4168+
inner.Select(x => x?.ToString()).Should().Equal("1", "2", "3");
4169+
}
4170+
41304171
#region Encoded dot behavior in keys (%2E / %2e)
41314172

41324173
[Fact]
4133-
public void EncodedDot_TopLevel_AllowDotsTrue_DecodeDotInKeysTrue_PlainDotSplits_EncodedDotDoesNotSplit()
4174+
public void EncodedDot_TopLevel_AllowDotsTrue_DecodeDotInKeysTrue_PlainAndEncodedDotSplit()
41344175
{
41354176
var opt = new DecodeOptions { AllowDots = true, DecodeDotInKeys = true };
41364177

@@ -4157,7 +4198,7 @@ public void EncodedDot_TopLevel_AllowDotsTrue_DecodeDotInKeysTrue_PlainDotSplits
41574198
}
41584199

41594200
[Fact]
4160-
public void EncodedDot_TopLevel_AllowDotsTrue_DecodeDotInKeysFalse_EncodedDotRemainsPercentSequence()
4201+
public void EncodedDot_TopLevel_AllowDotsTrue_DecodeDotInKeysFalse_EncodedDotAlsoSplits()
41614202
{
41624203
var opt = new DecodeOptions { AllowDots = true, DecodeDotInKeys = false };
41634204

@@ -4181,7 +4222,7 @@ public void EncodedDot_AllowDotsFalse_DecodeDotInKeysTrue_IsInvalid()
41814222
{
41824223
var opt = new DecodeOptions { AllowDots = false, DecodeDotInKeys = true };
41834224
Action act = () => Qs.Decode("a%2Eb=c", opt);
4184-
act.Should().Throw<ArgumentException>();
4225+
act.Should().Throw<InvalidOperationException>();
41854226
}
41864227

41874228
[Fact]
@@ -4205,7 +4246,7 @@ public void EncodedDot_BracketSegment_MapsToDot_WhenDecodeDotInKeysTrue()
42054246
}
42064247

42074248
[Fact]
4208-
public void EncodedDot_BracketSegment_RemainsPercentSequence_WhenDecodeDotInKeysFalse()
4249+
public void EncodedDot_BracketSegment_DecodesToDot_WhenDecodeDotInKeysFalse()
42094250
{
42104251
var opt = new DecodeOptions { AllowDots = true, DecodeDotInKeys = false };
42114252

@@ -4349,8 +4390,9 @@ public void MixedCase_EncodedBrackets_EncodedDot_AllowDotsFalse_DecodeDotInKeysT
43494390
{
43504391
var opt = new DecodeOptions { AllowDots = false, DecodeDotInKeys = true };
43514392
Action act = () => Qs.Decode("a%5Bb%5D%5Bc%5D%2Ed=x", opt);
4352-
act.Should().Throw<ArgumentException>()
4353-
.WithMessage("*decodeDotInKeys*allowDots*");
4393+
act.Should().Throw<InvalidOperationException>()
4394+
.WithMessage("*DecodeDotInKeys*AllowDots*")
4395+
.WithMessage("*DecodeDotInKeys=true*AllowDots=true*");
43544396
}
43554397

43564398
[Fact]
@@ -4646,7 +4688,7 @@ public void StrictDepthOverflow_RaisesForWellFormed()
46464688
{
46474689
var act = () =>
46484690
InternalDecoder.SplitKeyIntoSegments("a[b][c][d]", false, 1, true);
4649-
act.Should().Throw<IndexOutOfRangeException>();
4691+
act.Should().Throw<InvalidOperationException>();
46504692
}
46514693

46524694
[Fact]
@@ -4681,4 +4723,46 @@ public void LeadingDot_EncodedBracket_AllowDotsTrue_DecodeDotInKeysTrue()
46814723
}
46824724

46834725
#endregion
4726+
4727+
#region Decode comma limit
4728+
4729+
[Fact]
4730+
public void Decode_CommaSplit_AllowedWhenSumEqualsLimit()
4731+
{
4732+
var opts = new DecodeOptions
4733+
{
4734+
Comma = true,
4735+
ListLimit = 5,
4736+
ThrowOnLimitExceeded = true,
4737+
ParseLists = true,
4738+
Duplicates = Duplicates.Combine
4739+
};
4740+
4741+
// Existing N=2 from first part, incoming M=3; N+M = 5 == limit → allowed
4742+
var result = Assert.IsType<Dictionary<string, object?>>(Qs.Decode("a=1,2&a=3,4,5", opts));
4743+
result.Should().ContainKey("a");
4744+
4745+
var list = Assert.IsType<List<object?>>(result["a"]);
4746+
list.Should().HaveCount(5);
4747+
list.Select(x => x?.ToString()).Should().Equal("1", "2", "3", "4", "5");
4748+
}
4749+
4750+
[Fact]
4751+
public void Decode_CommaSplit_ThrowsWhenSumExceedsLimitAndThrowOn()
4752+
{
4753+
var opts = new DecodeOptions
4754+
{
4755+
Comma = true,
4756+
ListLimit = 5,
4757+
ThrowOnLimitExceeded = true,
4758+
ParseLists = true,
4759+
Duplicates = Duplicates.Combine
4760+
};
4761+
4762+
// Existing N=2, incoming M=4; N+M = 6 > limit and ThrowOnLimitExceeded = true → throws
4763+
Action act = () => Qs.Decode("a=1,2&a=3,4,5,6", opts);
4764+
act.Should().Throw<InvalidOperationException>();
4765+
}
4766+
4767+
#endregion
46844768
}

QsNet.Tests/EncodeTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1790,13 +1790,13 @@ public void Encode_DoesNotCrashWhenParsingCircularReferences()
17901790

17911791
Action act1 = () =>
17921792
Qs.Encode(new Dictionary<string, object?> { { "foo[bar]", "baz" }, { "foo[baz]", a } });
1793-
act1.Should().Throw<IndexOutOfRangeException>();
1793+
act1.Should().Throw<InvalidOperationException>();
17941794

17951795
var circular = new Dictionary<string, object?> { { "a", "value" } };
17961796
circular["a"] = circular;
17971797

17981798
Action act2 = () => Qs.Encode(circular);
1799-
act2.Should().Throw<IndexOutOfRangeException>();
1799+
act2.Should().Throw<InvalidOperationException>();
18001800

18011801
var arr = new List<object?> { "a" };
18021802
Action act3 = () =>
@@ -3405,7 +3405,7 @@ public void Encode_ThrowsOnSelfReferentialMap()
34053405
a["self"] = a;
34063406

34073407
var act = () => Qs.Encode(new Dictionary<string, object?> { { "a", a } });
3408-
act.Should().Throw<IndexOutOfRangeException>();
3408+
act.Should().Throw<InvalidOperationException>();
34093409
}
34103410

34113411
[Fact]
@@ -3415,7 +3415,7 @@ public void Encode_ThrowsOnSelfReferentialList()
34153415
l.Add(l);
34163416

34173417
var act = () => Qs.Encode(new Dictionary<string, object?> { { "l", l } });
3418-
act.Should().Throw<IndexOutOfRangeException>();
3418+
act.Should().Throw<InvalidOperationException>();
34193419
}
34203420

34213421
[Fact]

QsNet.Tests/Fixtures/DummyEnum.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ internal enum DummyEnum
55
// ReSharper disable InconsistentNaming
66
LOREM,
77
IPSUM,
8+
89
DOLOR
910
// ReSharper restore InconsistentNaming
1011
}

QsNet.Tests/UtilsTests.cs

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,8 +1285,8 @@ public void InterpretNumericEntities_MalformedOrUnsupportedPatternsRemainUnchang
12851285
Utils.InterpretNumericEntities("&#;").Should().Be("&#;");
12861286
// Missing terminating semicolon
12871287
Utils.InterpretNumericEntities("&#12").Should().Be("&#12");
1288-
// Hex form not supported by this decoder
1289-
Utils.InterpretNumericEntities("&#x41;").Should().Be("&#x41;");
1288+
// Hex form is supported by this decoder
1289+
Utils.InterpretNumericEntities("&#x41;").Should().Be("A");
12901290
// Space inside
12911291
Utils.InterpretNumericEntities("&# 12;").Should().Be("&# 12;");
12921292
// Negative / non-digit after '#'
@@ -1302,6 +1302,62 @@ public void InterpretNumericEntities_OutOfRangeCodePointsRemainUnchanged()
13021302
Utils.InterpretNumericEntities("&#1114112;").Should().Be("&#1114112;");
13031303
}
13041304

1305+
[Fact]
1306+
public void InterpretNumericEntities_DecodesSingleHexEntity()
1307+
{
1308+
Utils.InterpretNumericEntities("&#x41;").Should().Be("A"); // uppercase hex digits
1309+
Utils.InterpretNumericEntities("&#x6d;").Should().Be("m"); // lowercase hex digits
1310+
}
1311+
1312+
[Fact]
1313+
public void InterpretNumericEntities_DecodesSingleHexEntity_UppercaseX()
1314+
{
1315+
Utils.InterpretNumericEntities("&#X41;").Should().Be("A");
1316+
}
1317+
1318+
[Fact]
1319+
public void InterpretNumericEntities_AcceptsMaxValidHexAndRejectsBeyond()
1320+
{
1321+
// U+10FFFF is valid
1322+
Utils.InterpretNumericEntities("&#x10FFFF;").Should().Be(char.ConvertFromUtf32(0x10FFFF));
1323+
// One above max should remain unchanged
1324+
Utils.InterpretNumericEntities("&#x110000;").Should().Be("&#x110000;");
1325+
}
1326+
1327+
[Fact]
1328+
public void InterpretNumericEntities_EmptyHexDigitsRemainUnchanged()
1329+
{
1330+
Utils.InterpretNumericEntities("&#x;").Should().Be("&#x;");
1331+
Utils.InterpretNumericEntities("&#X;").Should().Be("&#X;");
1332+
}
1333+
1334+
[Fact]
1335+
public void InterpretNumericEntities_DecodesMultipleHexEntities()
1336+
{
1337+
Utils.InterpretNumericEntities("&#x48;&#x0069;!").Should().Be("Hi!");
1338+
}
1339+
1340+
[Fact]
1341+
public void InterpretNumericEntities_DecodesHexSurrogatePair()
1342+
{
1343+
// U+1F4A9 (💩) as surrogate halves: 0xD83D, 0xDCA9
1344+
Utils.InterpretNumericEntities("&#xD83D;&#xDCA9;").Should().Be("💩");
1345+
}
1346+
1347+
[Fact]
1348+
public void InterpretNumericEntities_MixedDecimalAndHexEntities()
1349+
{
1350+
Utils.InterpretNumericEntities("A = &#x41; and &#66;").Should().Be("A = A and B");
1351+
}
1352+
1353+
[Fact]
1354+
public void InterpretNumericEntities_InvalidHexEntitiesRemainUnchanged()
1355+
{
1356+
Utils.InterpretNumericEntities("&#xZZ;").Should().Be("&#xZZ;"); // non-hex digits
1357+
Utils.InterpretNumericEntities("&#x1G;").Should().Be("&#x1G;"); // invalid hex digit
1358+
Utils.InterpretNumericEntities("&#x41").Should().Be("&#x41"); // missing semicolon
1359+
}
1360+
13051361
[Fact]
13061362
public void Apply_OnScalarAndList()
13071363
{
@@ -1567,4 +1623,24 @@ public void ToStringKeyDeepNonRecursive_Converts_Nested_Lists_And_Dicts()
15671623

15681624
outTopList[2].Should().Be(4);
15691625
}
1626+
1627+
[Fact]
1628+
public void EnsureAstralCharactersAtSegmentLimitMinus1OrSegmentLimitEncodeAs4ByteSequences()
1629+
{
1630+
const int SegmentLimit = 1024;
1631+
// Ensure astral characters at SegmentLimit-1/SegmentLimit encode as 4-byte sequences
1632+
var s = new string('a', SegmentLimit - 1) + "\U0001F600" + "b";
1633+
var encoded = Utils.Encode(s, Encoding.UTF8, Format.Rfc3986);
1634+
Assert.Contains("%F0%9F%98%80", encoded);
1635+
}
1636+
1637+
[Fact]
1638+
public void EnsureAstralCharactersAtSegmentLimitEncodeAs4ByteSequences()
1639+
{
1640+
const int SegmentLimit = 1024;
1641+
// Astral character starts exactly at the chunk boundary (index == SegmentLimit)
1642+
var s = new string('a', SegmentLimit) + "\U0001F600" + "b";
1643+
var encoded = Utils.Encode(s, Encoding.UTF8, Format.Rfc3986);
1644+
Assert.Contains("%F0%9F%98%80", encoded);
1645+
}
15701646
}

0 commit comments

Comments
 (0)