diff --git a/server/templates/go_template_test.go b/server/templates/go_template_test.go new file mode 100644 index 0000000..0442d6a --- /dev/null +++ b/server/templates/go_template_test.go @@ -0,0 +1,41 @@ +package templates + +import ( + "testing" + "time" + + "github.com/smocker-dev/smocker/server/types" +) + +// TestGoTemplateJsonDelay reproduces #305: a delay written as duration strings ("10ms") in a +// go_template_json dynamic response must be applied — it was previously silently dropped because +// Delay.UnmarshalJSON only accepted numeric nanoseconds. +func TestGoTemplateJsonDelay(t *testing.T) { + script := `{ + "body": {"message": "request path {{.Request.Path}}"}, + "headers": {"Content-Type": ["application/json"]}, + "delay": {"min": "0", "max": "10ms"} +}` + res, err := NewGoTemplateJsonEngine().Execute(types.Request{Path: "/test"}, script) + if err != nil { + t.Fatalf("Execute: %v", err) + } + if res.Delay.Min != 0 { + t.Errorf("delay.min = %v, want 0", res.Delay.Min) + } + if res.Delay.Max != 10*time.Millisecond { + t.Errorf("delay.max = %v, want 10ms", res.Delay.Max) + } +} + +// TestGoTemplateJsonDelayScalar covers the single-value shorthand ("delay": "5ms"). +func TestGoTemplateJsonDelayScalar(t *testing.T) { + res, err := NewGoTemplateJsonEngine().Execute(types.Request{Path: "/test"}, + `{"body": "hi", "delay": "5ms"}`) + if err != nil { + t.Fatalf("Execute: %v", err) + } + if res.Delay.Min != 5*time.Millisecond || res.Delay.Max != 5*time.Millisecond { + t.Errorf("delay = {%v, %v}, want {5ms, 5ms}", res.Delay.Min, res.Delay.Max) + } +} diff --git a/server/types/mock.go b/server/types/mock.go index f90071b..93eb784 100644 --- a/server/types/mock.go +++ b/server/types/mock.go @@ -165,26 +165,59 @@ type Delay struct { } func (d *Delay) UnmarshalJSON(data []byte) error { - var s time.Duration - if err := json.Unmarshal(data, &s); err == nil { - d.Min = s - d.Max = s + // Scalar form: a single duration applied to both bounds. + if v, ok, err := parseJSONDuration(data); err != nil { + return err + } else if ok { + d.Min, d.Max = v, v return d.validate() } + // Object form: {"min": ..., "max": ...}. Each bound is a duration string ("10ms") or a number + // of nanoseconds — the same shapes the Lua and go_template_yaml engines already accept. var res struct { - Min time.Duration `json:"min"` - Max time.Duration `json:"max"` + Min json.RawMessage `json:"min"` + Max json.RawMessage `json:"max"` } - if err := json.Unmarshal(data, &res); err != nil { return err } - d.Min = res.Min - d.Max = res.Max + if len(res.Min) > 0 { + v, _, err := parseJSONDuration(res.Min) + if err != nil { + return err + } + d.Min = v + } + if len(res.Max) > 0 { + v, _, err := parseJSONDuration(res.Max) + if err != nil { + return err + } + d.Max = v + } return d.validate() } +// parseJSONDuration reads a JSON scalar as a duration: a string like "10ms" (time.ParseDuration) +// or a number of nanoseconds. ok is false when the value is not a scalar (e.g. an object), so the +// caller can fall back to the {min, max} form. +func parseJSONDuration(data []byte) (time.Duration, bool, error) { + var v interface{} + if err := json.Unmarshal(data, &v); err != nil { + return 0, false, err + } + switch x := v.(type) { + case string: + dur, err := time.ParseDuration(x) + return dur, true, err + case float64: + return time.Duration(int64(x)), true, nil + default: + return 0, false, nil + } +} + func (d *Delay) UnmarshalYAML(unmarshal func(interface{}) error) error { var s time.Duration if err := unmarshal(&s); err == nil {