From 4a9c83b969297c9eaed3f729e21e5ffd07d632d2 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Sat, 30 Aug 2025 17:45:48 +0300 Subject: [PATCH] duration tests Signed-off-by: Mohammed Al Sahaf --- duration_test.go | 407 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 407 insertions(+) create mode 100644 duration_test.go diff --git a/duration_test.go b/duration_test.go new file mode 100644 index 000000000..990edc248 --- /dev/null +++ b/duration_test.go @@ -0,0 +1,407 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddy + +import ( + "encoding/json" + "math" + "strings" + "testing" + "time" +) + +func TestParseDuration_EdgeCases(t *testing.T) { + tests := []struct { + name string + input string + expectErr bool + expected time.Duration + }{ + { + name: "zero duration", + input: "0", + expected: 0, + }, + { + name: "invalid format", + input: "abc", + expectErr: true, + }, + { + name: "negative days", + input: "-2d", + expected: -48 * time.Hour, + }, + { + name: "decimal days", + input: "0.5d", + expected: 12 * time.Hour, + }, + { + name: "large decimal days", + input: "365.25d", + expected: time.Duration(365.25*24) * time.Hour, + }, + { + name: "multiple days in same string", + input: "1d2d3d", + expected: (24 * 6) * time.Hour, // 6 days total + }, + { + name: "days with other units", + input: "1d30m15s", + expected: 24*time.Hour + 30*time.Minute + 15*time.Second, + }, + { + name: "malformed days", + input: "d", + expectErr: true, + }, + { + name: "invalid day value", + input: "abcd", + expectErr: true, + }, + { + name: "overflow protection", + input: "9999999999999999999999999d", + expectErr: true, + }, + { + name: "zero days", + input: "0d", + expected: 0, + }, + { + name: "input at limit", + input: strings.Repeat("1", 1024) + "ns", + expectErr: true, // Likely to cause parsing error due to size + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, err := ParseDuration(test.input) + + if test.expectErr && err == nil { + t.Error("Expected error but got none") + } + if !test.expectErr && err != nil { + t.Errorf("Unexpected error: %v", err) + } + if !test.expectErr && result != test.expected { + t.Errorf("Expected %v, got %v", test.expected, result) + } + }) + } +} + +func TestParseDuration_InputLengthLimit(t *testing.T) { + // Test the 1024 character limit + longInput := strings.Repeat("1", 1025) + "s" + + _, err := ParseDuration(longInput) + if err == nil { + t.Error("Expected error for input longer than 1024 characters") + } + + expectedErrMsg := "parsing duration: input string too long" + if err.Error() != expectedErrMsg { + t.Errorf("Expected error message '%s', got '%s'", expectedErrMsg, err.Error()) + } +} + +func TestParseDuration_ComplexNumberFormats(t *testing.T) { + tests := []struct { + input string + expected time.Duration + }{ + { + input: "+1d", + expected: 24 * time.Hour, + }, + { + input: "-1.5d", + expected: -36 * time.Hour, + }, + { + input: "1.0d", + expected: 24 * time.Hour, + }, + { + input: "0.25d", + expected: 6 * time.Hour, + }, + { + input: "1.5d30m", + expected: 36*time.Hour + 30*time.Minute, + }, + { + input: "2.5d1h30m45s", + expected: 60*time.Hour + time.Hour + 30*time.Minute + 45*time.Second, + }, + } + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + result, err := ParseDuration(test.input) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if result != test.expected { + t.Errorf("Expected %v, got %v", test.expected, result) + } + }) + } +} + +func TestDuration_UnmarshalJSON_TypeValidation(t *testing.T) { + tests := []struct { + name string + input string + expectErr bool + expected time.Duration + }{ + { + name: "null value", + input: "null", + expectErr: false, + expected: 0, + }, + { + name: "boolean value", + input: "true", + expectErr: true, + }, + { + name: "array value", + input: `[1,2,3]`, + expectErr: true, + }, + { + name: "object value", + input: `{"duration": "5m"}`, + expectErr: true, + }, + { + name: "negative integer", + input: "-1000000000", + expected: -time.Second, + expectErr: false, + }, + { + name: "zero integer", + input: "0", + expected: 0, + expectErr: false, + }, + { + name: "large integer", + input: "9223372036854775807", // Max int64 + expected: time.Duration(math.MaxInt64), + expectErr: false, + }, + { + name: "float as integer (invalid JSON for int)", + input: "1.5", + expectErr: true, + }, + { + name: "string with special characters", + input: `"5m\"30s"`, + expectErr: true, + }, + { + name: "string with unicode", + input: `"5m🚀"`, + expectErr: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var d Duration + err := d.UnmarshalJSON([]byte(test.input)) + + if test.expectErr && err == nil { + t.Error("Expected error but got none") + } + if !test.expectErr && err != nil { + t.Errorf("Unexpected error: %v", err) + } + if !test.expectErr && time.Duration(d) != test.expected { + t.Errorf("Expected %v, got %v", test.expected, time.Duration(d)) + } + }) + } +} + +func TestDuration_JSON_RoundTrip(t *testing.T) { + tests := []struct { + duration time.Duration + asString bool + }{ + {duration: 5 * time.Minute, asString: true}, + {duration: 24 * time.Hour, asString: false}, // Will be stored as nanoseconds + {duration: 0, asString: false}, + {duration: -time.Hour, asString: true}, + {duration: time.Nanosecond, asString: false}, + {duration: time.Second, asString: false}, + } + + for _, test := range tests { + t.Run(test.duration.String(), func(t *testing.T) { + d := Duration(test.duration) + + // Marshal to JSON + jsonData, err := json.Marshal(d) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + + // Unmarshal back + var unmarshaled Duration + err = unmarshaled.UnmarshalJSON(jsonData) + if err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + + // Should be equal + if time.Duration(unmarshaled) != test.duration { + t.Errorf("Round trip failed: expected %v, got %v", test.duration, time.Duration(unmarshaled)) + } + }) + } +} + +func TestParseDuration_Precision(t *testing.T) { + // Test floating point precision with days + tests := []struct { + input string + expected time.Duration + }{ + { + input: "0.1d", + expected: time.Duration(0.1 * 24 * float64(time.Hour)), + }, + { + input: "0.01d", + expected: time.Duration(0.01 * 24 * float64(time.Hour)), + }, + { + input: "0.001d", + expected: time.Duration(0.001 * 24 * float64(time.Hour)), + }, + { + input: "1.23456789d", + expected: time.Duration(1.23456789 * 24 * float64(time.Hour)), + }, + } + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + result, err := ParseDuration(test.input) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // Allow for small floating point differences + diff := result - test.expected + if diff < 0 { + diff = -diff + } + if diff > time.Nanosecond { + t.Errorf("Expected %v, got %v (diff: %v)", test.expected, result, diff) + } + }) + } +} + +func TestParseDuration_Boundary_Values(t *testing.T) { + tests := []struct { + name string + input string + expectErr bool + }{ + { + name: "minimum day value", + input: "0.000000001d", // Very small but valid + }, + { + name: "very large day value", + input: "999999999999999999999d", + expectErr: true, // Should overflow + }, + { + name: "negative zero", + input: "-0d", + }, + { + name: "positive zero", + input: "+0d", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := ParseDuration(test.input) + + if test.expectErr && err == nil { + t.Error("Expected error but got none") + } + if !test.expectErr && err != nil { + t.Errorf("Unexpected error: %v", err) + } + }) + } +} + +func BenchmarkParseDuration_SimpleDay(b *testing.B) { + for i := 0; i < b.N; i++ { + ParseDuration("1d") + } +} + +func BenchmarkParseDuration_ComplexDay(b *testing.B) { + for i := 0; i < b.N; i++ { + ParseDuration("1.5d30m15.5s") + } +} + +func BenchmarkParseDuration_MultipleDays(b *testing.B) { + for i := 0; i < b.N; i++ { + ParseDuration("1d2d3d4d5d") + } +} + +func BenchmarkDuration_UnmarshalJSON_String(b *testing.B) { + input := []byte(`"5m30s"`) + var d Duration + + b.ResetTimer() + for i := 0; i < b.N; i++ { + d.UnmarshalJSON(input) + } +} + +func BenchmarkDuration_UnmarshalJSON_Integer(b *testing.B) { + input := []byte("300000000000") // 5 minutes in nanoseconds + var d Duration + + b.ResetTimer() + for i := 0; i < b.N; i++ { + d.UnmarshalJSON(input) + } +}