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