@@ -221,3 +221,234 @@ func TestPaginatedResponse(t *testing.T) {
221221 assert .Equal (t , 100 , result .Total )
222222 assert .Equal (t , []string {"a" , "b" , "c" }, result .Items )
223223}
224+
225+ // --- ChaosFaultConfig UnmarshalJSON Tests ---
226+
227+ func TestChaosFaultConfig_UnmarshalJSON_Canonical (t * testing.T ) {
228+ t .Parallel ()
229+
230+ input := `{"type":"circuit_breaker","probability":1.0,"config":{"tripAfter":3,"openDuration":"10s"}}`
231+
232+ var f ChaosFaultConfig
233+ err := json .Unmarshal ([]byte (input ), & f )
234+ require .NoError (t , err )
235+
236+ assert .Equal (t , "circuit_breaker" , f .Type )
237+ assert .Equal (t , 1.0 , f .Probability )
238+ assert .Equal (t , float64 (3 ), f .Config ["tripAfter" ])
239+ assert .Equal (t , "10s" , f .Config ["openDuration" ])
240+ }
241+
242+ func TestChaosFaultConfig_UnmarshalJSON_Flat (t * testing.T ) {
243+ t .Parallel ()
244+
245+ // User sends config params at top level — the common mistake / natural format.
246+ input := `{"type":"circuit_breaker","probability":1.0,"tripAfter":3,"openDuration":"10s"}`
247+
248+ var f ChaosFaultConfig
249+ err := json .Unmarshal ([]byte (input ), & f )
250+ require .NoError (t , err )
251+
252+ assert .Equal (t , "circuit_breaker" , f .Type )
253+ assert .Equal (t , 1.0 , f .Probability )
254+ assert .Equal (t , float64 (3 ), f .Config ["tripAfter" ])
255+ assert .Equal (t , "10s" , f .Config ["openDuration" ])
256+ }
257+
258+ func TestChaosFaultConfig_UnmarshalJSON_BothFlatAndNested (t * testing.T ) {
259+ t .Parallel ()
260+
261+ // Explicit "config" value wins when both are present for the same key.
262+ input := `{
263+ "type": "circuit_breaker",
264+ "probability": 1.0,
265+ "tripAfter": 5,
266+ "openStatusCode": 502,
267+ "config": {
268+ "tripAfter": 3,
269+ "openDuration": "10s"
270+ }
271+ }`
272+
273+ var f ChaosFaultConfig
274+ err := json .Unmarshal ([]byte (input ), & f )
275+ require .NoError (t , err )
276+
277+ assert .Equal (t , "circuit_breaker" , f .Type )
278+ assert .Equal (t , 1.0 , f .Probability )
279+ // Explicit config wins for tripAfter
280+ assert .Equal (t , float64 (3 ), f .Config ["tripAfter" ])
281+ // openDuration comes from config
282+ assert .Equal (t , "10s" , f .Config ["openDuration" ])
283+ // openStatusCode comes from flat (not in config)
284+ assert .Equal (t , float64 (502 ), f .Config ["openStatusCode" ])
285+ }
286+
287+ func TestChaosFaultConfig_UnmarshalJSON_NoExtras (t * testing.T ) {
288+ t .Parallel ()
289+
290+ // Basic fault with no config params at all — Config stays nil.
291+ input := `{"type":"timeout","probability":0.5}`
292+
293+ var f ChaosFaultConfig
294+ err := json .Unmarshal ([]byte (input ), & f )
295+ require .NoError (t , err )
296+
297+ assert .Equal (t , "timeout" , f .Type )
298+ assert .Equal (t , 0.5 , f .Probability )
299+ assert .Nil (t , f .Config )
300+ }
301+
302+ func TestChaosFaultConfig_UnmarshalJSON_EmptyConfig (t * testing.T ) {
303+ t .Parallel ()
304+
305+ input := `{"type":"latency","probability":0.3,"config":{}}`
306+
307+ var f ChaosFaultConfig
308+ err := json .Unmarshal ([]byte (input ), & f )
309+ require .NoError (t , err )
310+
311+ assert .Equal (t , "latency" , f .Type )
312+ assert .Equal (t , 0.3 , f .Probability )
313+ // Empty config stays as empty map (not nil) since it was explicitly provided.
314+ assert .NotNil (t , f .Config )
315+ assert .Empty (t , f .Config )
316+ }
317+
318+ func TestChaosFaultConfig_UnmarshalJSON_RetryAfterFlat (t * testing.T ) {
319+ t .Parallel ()
320+
321+ input := `{"type":"retry_after","probability":1.0,"statusCode":429,"retryAfter":"30s","body":"rate limited"}`
322+
323+ var f ChaosFaultConfig
324+ err := json .Unmarshal ([]byte (input ), & f )
325+ require .NoError (t , err )
326+
327+ assert .Equal (t , "retry_after" , f .Type )
328+ assert .Equal (t , float64 (429 ), f .Config ["statusCode" ])
329+ assert .Equal (t , "30s" , f .Config ["retryAfter" ])
330+ assert .Equal (t , "rate limited" , f .Config ["body" ])
331+ }
332+
333+ func TestChaosFaultConfig_UnmarshalJSON_ProgressiveDegradationFlat (t * testing.T ) {
334+ t .Parallel ()
335+
336+ input := `{
337+ "type": "progressive_degradation",
338+ "probability": 1.0,
339+ "initialDelay": "20ms",
340+ "delayIncrement": "5ms",
341+ "maxDelay": "5s",
342+ "errorAfter": 100
343+ }`
344+
345+ var f ChaosFaultConfig
346+ err := json .Unmarshal ([]byte (input ), & f )
347+ require .NoError (t , err )
348+
349+ assert .Equal (t , "progressive_degradation" , f .Type )
350+ assert .Equal (t , "20ms" , f .Config ["initialDelay" ])
351+ assert .Equal (t , "5ms" , f .Config ["delayIncrement" ])
352+ assert .Equal (t , "5s" , f .Config ["maxDelay" ])
353+ assert .Equal (t , float64 (100 ), f .Config ["errorAfter" ])
354+ }
355+
356+ func TestChaosFaultConfig_UnmarshalJSON_InvalidJSON (t * testing.T ) {
357+ t .Parallel ()
358+
359+ input := `{"type":"circuit_breaker","probability":}`
360+
361+ var f ChaosFaultConfig
362+ err := json .Unmarshal ([]byte (input ), & f )
363+ assert .Error (t , err )
364+ }
365+
366+ func TestChaosFaultConfig_UnmarshalJSON_NestedArray (t * testing.T ) {
367+ t .Parallel ()
368+
369+ // ChaosRuleConfig.Faults is a slice of ChaosFaultConfig — ensure the
370+ // custom UnmarshalJSON works correctly when deserialized as part of a parent struct.
371+ input := `{
372+ "pathPattern": "/api/.*",
373+ "faults": [
374+ {"type":"circuit_breaker","probability":1.0,"tripAfter":3},
375+ {"type":"latency","probability":0.5,"config":{"min":"100ms","max":"500ms"}}
376+ ]
377+ }`
378+
379+ var rule ChaosRuleConfig
380+ err := json .Unmarshal ([]byte (input ), & rule )
381+ require .NoError (t , err )
382+
383+ require .Len (t , rule .Faults , 2 )
384+
385+ // First fault: flat format
386+ assert .Equal (t , "circuit_breaker" , rule .Faults [0 ].Type )
387+ assert .Equal (t , float64 (3 ), rule .Faults [0 ].Config ["tripAfter" ])
388+
389+ // Second fault: canonical format
390+ assert .Equal (t , "latency" , rule .Faults [1 ].Type )
391+ assert .Equal (t , "100ms" , rule .Faults [1 ].Config ["min" ])
392+ assert .Equal (t , "500ms" , rule .Faults [1 ].Config ["max" ])
393+ }
394+
395+ func TestChaosFaultConfig_UnmarshalJSON_FullChaosConfig (t * testing.T ) {
396+ t .Parallel ()
397+
398+ // End-to-end: full ChaosConfig with flat fault params, as a user would send it.
399+ input := `{
400+ "enabled": true,
401+ "rules": [
402+ {
403+ "pathPattern": "/api/orders.*",
404+ "faults": [
405+ {
406+ "type": "circuit_breaker",
407+ "probability": 1.0,
408+ "tripAfter": 3,
409+ "openDuration": "10s",
410+ "openStatusCode": 503
411+ }
412+ ]
413+ }
414+ ]
415+ }`
416+
417+ var cfg ChaosConfig
418+ err := json .Unmarshal ([]byte (input ), & cfg )
419+ require .NoError (t , err )
420+
421+ require .True (t , cfg .Enabled )
422+ require .Len (t , cfg .Rules , 1 )
423+ require .Len (t , cfg .Rules [0 ].Faults , 1 )
424+
425+ fault := cfg .Rules [0 ].Faults [0 ]
426+ assert .Equal (t , "circuit_breaker" , fault .Type )
427+ assert .Equal (t , 1.0 , fault .Probability )
428+ assert .Equal (t , float64 (3 ), fault .Config ["tripAfter" ])
429+ assert .Equal (t , "10s" , fault .Config ["openDuration" ])
430+ assert .Equal (t , float64 (503 ), fault .Config ["openStatusCode" ])
431+ }
432+
433+ func TestChaosFaultConfig_MarshalJSON_RoundTrip (t * testing.T ) {
434+ t .Parallel ()
435+
436+ // Marshal always produces the canonical format with nested "config".
437+ original := ChaosFaultConfig {
438+ Type : "circuit_breaker" ,
439+ Probability : 1.0 ,
440+ Config : map [string ]any {"tripAfter" : float64 (3 ), "openDuration" : "10s" },
441+ }
442+
443+ data , err := json .Marshal (original )
444+ require .NoError (t , err )
445+
446+ var roundTripped ChaosFaultConfig
447+ err = json .Unmarshal (data , & roundTripped )
448+ require .NoError (t , err )
449+
450+ assert .Equal (t , original .Type , roundTripped .Type )
451+ assert .Equal (t , original .Probability , roundTripped .Probability )
452+ assert .Equal (t , original .Config ["tripAfter" ], roundTripped .Config ["tripAfter" ])
453+ assert .Equal (t , original .Config ["openDuration" ], roundTripped .Config ["openDuration" ])
454+ }
0 commit comments