Skip to content

Commit 87f32a4

Browse files
committed
Add comprehensive test suite and JSON converter enhancements
- Add EdgeCaseAndErrorHandlingTests for extreme input validation - Add EnhancedJsonConverterTests for vulnerability and performance testing - Add HttpEngineModernizationTests for async patterns and error handling - Add NullableReferenceTypesCompatibilityTests for type safety validation - Enhance EnumMemberJsonConverter and PriceLevelJsonConverter error handling - Add centralized JsonSerializerConfiguration for consistency - Update MapsAPIGenericEngine with improved async patterns - Enhance GeocodingRequest validation and error handling - Remove outdated MODERNIZATION_ROADMAP.md documentation
1 parent 62f4199 commit 87f32a4

File tree

11 files changed

+1900
-471
lines changed

11 files changed

+1900
-471
lines changed

GoogleMapsApi.Test/EdgeCaseAndErrorHandlingTests.cs

Lines changed: 564 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
using System;
2+
using System.Text.Json;
3+
using GoogleMapsApi.Engine.JsonConverters;
4+
using GoogleMapsApi.Engine;
5+
using GoogleMapsApi.Entities.Directions.Response;
6+
using GoogleMapsApi.Entities.DistanceMatrix.Response;
7+
using GoogleMapsApi.Entities.PlacesDetails.Response;
8+
using GoogleMapsApi.Entities.Directions.Request;
9+
using NUnit.Framework;
10+
using System.Threading.Tasks;
11+
12+
namespace GoogleMapsApi.Test
13+
{
14+
[TestFixture]
15+
public class EnhancedJsonConverterTests
16+
{
17+
private JsonSerializerOptions _options;
18+
19+
[SetUp]
20+
public void Setup()
21+
{
22+
// Use centralized configuration for consistency with production code
23+
_options = JsonSerializerConfiguration.CreateOptions();
24+
25+
}
26+
27+
#region Enhanced DurationJsonConverter Tests - Vulnerability Testing
28+
29+
[Test]
30+
public void DurationJsonConverter_NullValues_HandledCorrectly()
31+
{
32+
// Test null value in duration object
33+
var json = """{"value": null, "text": null}""";
34+
35+
var duration = JsonSerializer.Deserialize<GoogleMapsApi.Entities.Directions.Response.Duration>(json, _options);
36+
37+
Assert.That(duration, Is.Not.Null);
38+
Assert.That(duration.Text, Is.Null);
39+
}
40+
41+
[Test]
42+
public void DurationJsonConverter_MissingProperties_DoesNotThrow()
43+
{
44+
// Test missing value property
45+
var json1 = """{"text": "1 hour"}""";
46+
Assert.DoesNotThrow(() => JsonSerializer.Deserialize<GoogleMapsApi.Entities.Directions.Response.Duration>(json1, _options));
47+
48+
// Test missing text property
49+
var json2 = """{"value": 3600}""";
50+
Assert.DoesNotThrow(() => JsonSerializer.Deserialize<GoogleMapsApi.Entities.Directions.Response.Duration>(json2, _options));
51+
52+
// Test completely empty object
53+
var json3 = """{}""";
54+
Assert.DoesNotThrow(() => JsonSerializer.Deserialize<GoogleMapsApi.Entities.Directions.Response.Duration>(json3, _options));
55+
}
56+
57+
[Test]
58+
public void DurationJsonConverter_UnexpectedPropertyTypes_DoesNotThrow()
59+
{
60+
// Test string where number expected
61+
var json1 = """{"value": "invalid", "text": "1 hour"}""";
62+
Assert.DoesNotThrow(() => JsonSerializer.Deserialize<GoogleMapsApi.Entities.Directions.Response.Duration>(json1, _options));
63+
64+
// Test number where string expected
65+
var json2 = """{"value": 3600, "text": 123}""";
66+
Assert.DoesNotThrow(() => JsonSerializer.Deserialize<GoogleMapsApi.Entities.Directions.Response.Duration>(json2, _options));
67+
}
68+
69+
[Test]
70+
public void DurationJsonConverter_ExtremeValues_HandledCorrectly()
71+
{
72+
// Test very large duration
73+
var json1 = """{"value": 2147483647, "text": "Very long time"}""";
74+
var duration1 = JsonSerializer.Deserialize<GoogleMapsApi.Entities.Directions.Response.Duration>(json1, _options);
75+
Assert.That(duration1, Is.Not.Null);
76+
Assert.That(duration1!.Value, Is.EqualTo(TimeSpan.FromSeconds(2147483647)));
77+
78+
// Test negative duration (should this be allowed?)
79+
var json2 = """{"value": -1000, "text": "Negative time"}""";
80+
Assert.DoesNotThrow(() => JsonSerializer.Deserialize<GoogleMapsApi.Entities.Directions.Response.Duration>(json2, _options));
81+
}
82+
83+
[Test]
84+
public void DurationJsonConverter_SerializationRoundTrip_Consistent()
85+
{
86+
var original = new GoogleMapsApi.Entities.Directions.Response.Duration
87+
{
88+
Value = TimeSpan.FromHours(2.5),
89+
Text = "2 hours 30 minutes"
90+
};
91+
92+
var json = JsonSerializer.Serialize(original, _options);
93+
var deserialized = JsonSerializer.Deserialize<GoogleMapsApi.Entities.Directions.Response.Duration>(json, _options);
94+
95+
Assert.That(deserialized, Is.Not.Null);
96+
Assert.That(deserialized!.Value, Is.EqualTo(original.Value));
97+
Assert.That(deserialized.Text, Is.EqualTo(original.Text));
98+
}
99+
100+
#endregion
101+
102+
#region Enhanced EnumMemberJsonConverter Tests - Cache and Edge Cases
103+
104+
[Test]
105+
public void EnumMemberJsonConverter_CachePerformance_Consistent()
106+
{
107+
// Test that repeated conversions use cache and are consistent
108+
var json = "\"DRIVING\"";
109+
110+
// First conversion - populates cache
111+
var result1 = JsonSerializer.Deserialize<TravelMode>(json, _options);
112+
113+
// Multiple subsequent conversions - should use cache
114+
for (int i = 0; i < 100; i++)
115+
{
116+
var result = JsonSerializer.Deserialize<TravelMode>(json, _options);
117+
Assert.That(result, Is.EqualTo(TravelMode.Driving));
118+
Assert.That(result, Is.EqualTo(result1));
119+
}
120+
}
121+
122+
[Test]
123+
public void EnumMemberJsonConverter_CaseVariations_Handled()
124+
{
125+
// Test that the converter handles exact case matches (Google APIs are case sensitive)
126+
var validJson = "\"DRIVING\"";
127+
var result = JsonSerializer.Deserialize<TravelMode>(validJson, _options);
128+
Assert.That(result, Is.EqualTo(TravelMode.Driving));
129+
130+
// Test lowercase (should fail with Google API format)
131+
var lowercaseJson = "\"driving\"";
132+
var desrialized = JsonSerializer.Deserialize<TravelMode>(lowercaseJson, _options);
133+
Assert.That(desrialized, Is.EqualTo(TravelMode.Driving));
134+
}
135+
136+
[Test]
137+
public void EnumMemberJsonConverter_AllEnumValues_SerializeAndDeserialize()
138+
{
139+
// Test all TravelMode values
140+
var modes = new[] { TravelMode.Driving, TravelMode.Walking, TravelMode.Bicycling, TravelMode.Transit };
141+
142+
foreach (var mode in modes)
143+
{
144+
var json = JsonSerializer.Serialize(mode, _options);
145+
var deserialized = JsonSerializer.Deserialize<TravelMode>(json, _options);
146+
Assert.That(deserialized, Is.EqualTo(mode));
147+
}
148+
}
149+
150+
[Test]
151+
public void EnumMemberJsonConverter_NumericValues_OutOfRange()
152+
{
153+
// Test behavior with numeric values outside enum range
154+
var json = "999";
155+
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<TravelMode>(json, _options));
156+
157+
// Test negative values
158+
var negativeJson = "-1";
159+
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<TravelMode>(negativeJson, _options));
160+
}
161+
162+
[Test]
163+
public void EnumMemberJsonConverter_EmptyAndWhitespace_Strings()
164+
{
165+
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<TravelMode>("\"\"", _options));
166+
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<TravelMode>("\" \"", _options));
167+
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<TravelMode>("\"\\t\"", _options));
168+
}
169+
170+
#endregion
171+
172+
#region Enhanced PriceLevelJsonConverter Tests - Dual Input Handling
173+
174+
[Test]
175+
public void PriceLevelJsonConverter_BoundaryValues_Handled()
176+
{
177+
// Test minimum valid value
178+
var json0 = "0";
179+
var result0 = JsonSerializer.Deserialize<PriceLevel?>(json0, _options);
180+
Assert.That(result0, Is.EqualTo(PriceLevel.Free));
181+
182+
// Test maximum valid value
183+
var json4 = "4";
184+
var result4 = JsonSerializer.Deserialize<PriceLevel?>(json4, _options);
185+
Assert.That(result4, Is.EqualTo(PriceLevel.VeryExpensive));
186+
}
187+
188+
[Test]
189+
public void PriceLevelJsonConverter_InvalidValues_ReturnNull()
190+
{
191+
// Test out-of-range values
192+
var json5 = "5";
193+
var result5 = JsonSerializer.Deserialize<PriceLevel?>(json5, _options);
194+
Assert.That(result5, Is.Null);
195+
196+
var jsonNeg = "-1";
197+
var resultNeg = JsonSerializer.Deserialize<PriceLevel?>(jsonNeg, _options);
198+
Assert.That(resultNeg, Is.Null);
199+
200+
// Test invalid string
201+
var jsonInvalid = "\"invalid\"";
202+
var resultInvalid = JsonSerializer.Deserialize<PriceLevel?>(jsonInvalid, _options);
203+
Assert.That(resultInvalid, Is.Null);
204+
}
205+
206+
[Test]
207+
public void PriceLevelJsonConverter_StringAndNumericEquivalence()
208+
{
209+
// Test that string and numeric representations give same result
210+
var numericJson = "2";
211+
var stringJson = "\"2\"";
212+
213+
var numericResult = JsonSerializer.Deserialize<PriceLevel?>(numericJson, _options);
214+
var stringResult = JsonSerializer.Deserialize<PriceLevel?>(stringJson, _options);
215+
216+
Assert.That(numericResult, Is.EqualTo(stringResult));
217+
Assert.That(numericResult, Is.EqualTo(PriceLevel.Moderate));
218+
}
219+
220+
[Test]
221+
public void PriceLevelJsonConverter_FloatingPointNumbers_Handled()
222+
{
223+
// Test that floating point numbers are handled (Google might send 2.0)
224+
var jsonFloat = "2.0";
225+
var result = JsonSerializer.Deserialize<PriceLevel?>(jsonFloat, _options);
226+
Assert.That(result, Is.EqualTo(PriceLevel.Moderate));
227+
}
228+
229+
#endregion
230+
231+
#region Enhanced OverviewPolylineJsonConverter Tests - Reflection Vulnerabilities
232+
233+
[Test]
234+
public void OverviewPolylineJsonConverter_EmptyAndNullPoints_Handled()
235+
{
236+
// Test empty points string
237+
var emptyJson = """{"points": ""}""";
238+
var emptyResult = JsonSerializer.Deserialize<OverviewPolyline>(emptyJson, _options);
239+
Assert.That(emptyResult, Is.Not.Null);
240+
241+
// Test missing points property
242+
var missingJson = """{}""";
243+
var missingResult = JsonSerializer.Deserialize<OverviewPolyline>(missingJson, _options);
244+
Assert.That(missingResult, Is.Not.Null);
245+
}
246+
247+
[Test]
248+
public void OverviewPolylineJsonConverter_LargePolylineData_HandlesCorrectly()
249+
{
250+
// Test with a large polyline string (realistic Google Maps data can be quite large)
251+
var largePoints = new string('a', 10000); // 10KB polyline
252+
var largeJson = $$"""{"points": "{{largePoints}}"}""";
253+
254+
Assert.DoesNotThrow(() =>
255+
{
256+
var result = JsonSerializer.Deserialize<OverviewPolyline>(largeJson, _options);
257+
Assert.That(result, Is.Not.Null);
258+
});
259+
}
260+
261+
[Test]
262+
public void OverviewPolylineJsonConverter_InvalidPolylineFormat_DoesNotCrash()
263+
{
264+
// Test invalid polyline encoding (should not crash OnDeserialized)
265+
var invalidJson = """{"points": "invalid_polyline_data_!@#$%"}""";
266+
267+
Assert.DoesNotThrow(() =>
268+
{
269+
var result = JsonSerializer.Deserialize<OverviewPolyline>(invalidJson, _options);
270+
Assert.That(result, Is.Not.Null);
271+
});
272+
}
273+
274+
#endregion
275+
276+
#region Stress Tests for All Converters
277+
278+
[Test]
279+
public void AllConverters_ConcurrentAccess_ThreadSafe()
280+
{
281+
var tasks = new Task[10];
282+
var exceptions = new System.Collections.Concurrent.ConcurrentBag<Exception>();
283+
284+
for (int i = 0; i < tasks.Length; i++)
285+
{
286+
tasks[i] = Task.Run(() =>
287+
{
288+
try
289+
{
290+
for (int j = 0; j < 100; j++)
291+
{
292+
// Test all converters concurrently
293+
JsonSerializer.Deserialize<GoogleMapsApi.Entities.Directions.Response.Duration>(
294+
"""{"value": 3600, "text": "1 hour"}""", _options);
295+
296+
JsonSerializer.Deserialize<TravelMode>("\"DRIVING\"", _options);
297+
JsonSerializer.Deserialize<PriceLevel?>("2", _options);
298+
JsonSerializer.Deserialize<OverviewPolyline>(
299+
"""{"points": "_p~iF~ps|U"}""", _options);
300+
}
301+
}
302+
catch (Exception ex)
303+
{
304+
exceptions.Add(ex);
305+
}
306+
});
307+
}
308+
309+
Task.WaitAll(tasks);
310+
Assert.That(exceptions.Count, Is.EqualTo(0),
311+
$"Concurrent access caused {exceptions.Count} exceptions: {string.Join(", ", exceptions)}");
312+
}
313+
314+
[Test]
315+
public void AllConverters_MalformedJson_DoNotCrash()
316+
{
317+
var malformedInputs = new[]
318+
{
319+
"not_json_at_all",
320+
"{",
321+
"}",
322+
"{\"incomplete\"",
323+
"{\"value\": }",
324+
"{\"text\": \"unclosed string}",
325+
"[]",
326+
"123.456.789",
327+
"{\"nested\": {\"too\": {\"deep\": true}}}",
328+
"null",
329+
"",
330+
" ",
331+
"{\"unicode\": \"\\u0000\\u0001\\u0002\"}",
332+
"{\"very_long_property_name_" + new string('x', 1000) + "\": \"value\"}"
333+
};
334+
335+
foreach (var malformed in malformedInputs)
336+
{
337+
// These should either succeed or throw JsonException, but not crash
338+
Assert.DoesNotThrow(() =>
339+
{
340+
try
341+
{
342+
JsonSerializer.Deserialize<GoogleMapsApi.Entities.Directions.Response.Duration>(malformed, _options);
343+
}
344+
catch (JsonException)
345+
{
346+
// Expected for malformed JSON
347+
}
348+
}, $"Input caused crash: {malformed}");
349+
}
350+
}
351+
352+
#endregion
353+
}
354+
}

0 commit comments

Comments
 (0)