Repository: kyverno/kyverno
Pull request #5955 (2023-01-10 05:06:12)

feat: add more time jmespath filters - Cherry pick 5950
Signed-off-by: ShutingZhao <shuting@nirmata.com>

Cherry-pick #5950.


codecov[bot]: # [Codecov](https://codecov.io/gh/kyverno/kyverno/pull/5955?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kyverno) Report
> Merging [#5955](https://codecov.io/gh/kyverno/kyverno/pull/5955?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kyverno) (69474e0) into [release-1.9](https://codecov.io/gh/kyverno/kyverno/commit/9ec18195b8d6d403f2f1c3ed5b142156e235950f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kyverno) (9ec1819) will **increase** coverage by `0.18%`.
> The diff coverage is `61.32%`.

```diff
@@               Coverage Diff               @@
##           release-1.9    #5955      +/-   ##
===============================================
+ Coverage        35.50%   35.69%   +0.18%     
===============================================
  Files              187      188       +1     
  Lines            20845    20935      +90     
===============================================
+ Hits              7401     7472      +71     
- Misses           12628    12641      +13     
- Partials           816      822       +6     
```


| [Impacted Files](https://codecov.io/gh/kyverno/kyverno/pull/5955?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kyverno) | Coverage Δ | |
|---|---|---|
| [pkg/engine/jmespath/time.go](https://codecov.io/gh/kyverno/kyverno/pull/5955?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kyverno#diff-cGtnL2VuZ2luZS9qbWVzcGF0aC90aW1lLmdv) | `36.92% <36.92%> (ø)` | |
| [pkg/engine/jmespath/functions.go](https://codecov.io/gh/kyverno/kyverno/pull/5955?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kyverno#diff-cGtnL2VuZ2luZS9qbWVzcGF0aC9mdW5jdGlvbnMuZ28=) | `77.45% <100.00%> (+5.63%)` | :arrow_up: |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kyverno)


Patch:
diff --git a/pkg/engine/jmespath/functions.go b/pkg/engine/jmespath/functions.go
index 91ae20508..01f07c9c7 100644
--- a/pkg/engine/jmespath/functions.go
+++ b/pkg/engine/jmespath/functions.go
@@ -70,8 +70,6 @@ var (
 	modulo                 = "modulo"
 	base64Decode           = "base64_decode"
 	base64Encode           = "base64_encode"
-	timeSince              = "time_since"
-	timeNow                = "time_now"
 	pathCanonicalize       = "path_canonicalize"
 	truncate               = "truncate"
 	semverCompare          = "semver_compare"
@@ -81,9 +79,6 @@ var (
 	objectFromLists        = "object_from_lists"
 	random                 = "random"
 	x509_decode            = "x509_decode"
-	timeAdd                = "time_add"
-	timeParse              = "time_parse"
-	timeToCron             = "time_to_cron"
 )
 
 const (
@@ -389,6 +384,14 @@ func GetFunctions() []*FunctionEntry {
 			ReturnType: []JpType{JpString},
 			Note:       "returns current time in RFC 3339 format",
 		},
+		{
+			Entry: &gojmespath.FunctionEntry{
+				Name:    timeNowUtc,
+				Handler: jpTimeNowUtc,
+			},
+			ReturnType: []JpType{JpString},
+			Note:       "returns current UTC time in RFC 3339 format",
+		},
 		{
 			Entry: &gojmespath.FunctionEntry{
 				Name: pathCanonicalize,
@@ -502,7 +505,7 @@ func GetFunctions() []*FunctionEntry {
 				Handler: jpTimeToCron,
 			},
 			ReturnType: []JpType{JpString},
-			Note:       "converts an absolute time (RFC 3339) to a cron expression (string).",
+			Note:       "converts a time (RFC 3339) to a cron expression (string).",
 		},
 		{
 			Entry: &gojmespath.FunctionEntry{
@@ -510,12 +513,11 @@ func GetFunctions() []*FunctionEntry {
 				Arguments: []ArgSpec{
 					{Types: []JpType{JpString}},
 					{Types: []JpType{JpString}},
-					{Types: []JpType{JpString}},
 				},
 				Handler: jpTimeAdd,
 			},
 			ReturnType: []JpType{JpString},
-			Note:       "adds duration (third string) to a time value (second string) of a specified layout (first string)",
+			Note:       "adds duration (second string) to a time value (first string)",
 		},
 		{
 			Entry: &gojmespath.FunctionEntry{
@@ -529,6 +531,78 @@ func GetFunctions() []*FunctionEntry {
 			ReturnType: []JpType{JpString},
 			Note:       "changes a time value of a given layout to RFC 3339",
 		},
+		{
+			Entry: &gojmespath.FunctionEntry{
+				Name: timeUtc,
+				Arguments: []ArgSpec{
+					{Types: []JpType{JpString}},
+				},
+				Handler: jpTimeUtc,
+			},
+			ReturnType: []JpType{JpString},
+			Note:       "calcutes time in UTC from a given time in RFC 3339 format",
+		},
+		{
+			Entry: &gojmespath.FunctionEntry{
+				Name: timeDiff,
+				Arguments: []ArgSpec{
+					{Types: []JpType{JpString}},
+					{Types: []JpType{JpString}},
+				},
+				Handler: jpTimeDiff,
+			},
+			ReturnType: []JpType{JpString},
+			Note:       "calculate the difference between a start and end date in RFC3339 format",
+		},
+		{
+			Entry: &gojmespath.FunctionEntry{
+				Name: timeBefore,
+				Arguments: []ArgSpec{
+					{Types: []JpType{JpString}},
+					{Types: []JpType{JpString}},
+				},
+				Handler: jpTimeBefore,
+			},
+			ReturnType: []JpType{JpBool},
+			Note:       "checks if a time is before another time, both in RFC3339 format",
+		},
+		{
+			Entry: &gojmespath.FunctionEntry{
+				Name: timeAfter,
+				Arguments: []ArgSpec{
+					{Types: []JpType{JpString}},
+					{Types: []JpType{JpString}},
+				},
+				Handler: jpTimeAfter,
+			},
+			ReturnType: []JpType{JpBool},
+			Note:       "checks if a time is after another time, both in RFC3339 format",
+		},
+		{
+			Entry: &gojmespath.FunctionEntry{
+				Name: timeBetween,
+				Arguments: []ArgSpec{
+					{Types: []JpType{JpString}},
+					{Types: []JpType{JpString}},
+					{Types: []JpType{JpString}},
+				},
+				Handler: jpTimeBetween,
+			},
+			ReturnType: []JpType{JpBool},
+			Note:       "checks if a time is between a start and end time, all in RFC3339 format",
+		},
+		{
+			Entry: &gojmespath.FunctionEntry{
+				Name: timeTruncate,
+				Arguments: []ArgSpec{
+					{Types: []JpType{JpString}},
+					{Types: []JpType{JpString}},
+				},
+				Handler: jpTimeTruncate,
+			},
+			ReturnType: []JpType{JpString},
+			Note:       "returns the result of rounding time down to a multiple of duration",
+		},
 	}
 }
 
@@ -833,72 +907,6 @@ func jpBase64Encode(arguments []interface{}) (interface{}, error) {
 	return base64.StdEncoding.EncodeToString([]byte(str.String())), nil
 }
 
-func jpTimeSince(arguments []interface{}) (interface{}, error) {
-	var err error
-	layout, err := validateArg("", arguments, 0, reflect.String)
-	if err != nil {
-		return nil, err
-	}
-
-	ts1, err := validateArg("", arguments, 1, reflect.String)
-	if err != nil {
-		return nil, err
-	}
-
-	ts2, err := validateArg("", arguments, 2, reflect.String)
-	if err != nil {
-		return nil, err
-	}
-
-	var t1, t2 time.Time
-	if layout.String() != "" {
-		t1, err = time.Parse(layout.String(), ts1.String())
-	} else {
-		t1, err = time.Parse(time.RFC3339, ts1.String())
-	}
-	if err != nil {
-		return nil, err
-	}
-
-	t2 = time.Now()
-	if ts2.String() != "" {
-		if layout.String() != "" {
-			t2, err = time.Parse(layout.String(), ts2.String())
-		} else {
-			t2, err = time.Parse(time.RFC3339, ts2.String())
-		}
-
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	return t2.Sub(t1).String(), nil
-}
-
-func jpTimeNow(arguments []interface{}) (interface{}, error) {
-	var err error
-	layout, err := validateArg("", arguments, 0, reflect.String)
-	if err != nil {
-		return nil, err
-	}
-
-	var t time.Time
-
-	t = time.Now()
-	if layout.String() != "" {
-		t, err = time.Parse(layout.String(), t.String())
-	} else {
-		t, err = time.Parse(time.RFC3339, t.String())
-	}
-
-	if err != nil {
-		return nil, err
-	}
-
-	return t.String(), nil
-}
-
 func jpPathCanonicalize(arguments []interface{}) (interface{}, error) {
 	var err error
 	str, err := validateArg(pathCanonicalize, arguments, 0, reflect.String)
@@ -1133,85 +1141,3 @@ func jpX509Decode(arguments []interface{}) (interface{}, error) {
 
 	return res, nil
 }
-
-func jpTimeToCron(arguments []interface{}) (interface{}, error) {
-	var err error
-
-	ts, err := validateArg("", arguments, 0, reflect.String)
-	if err != nil {
-		return nil, err
-	}
-
-	var t time.Time
-	t, err = time.Parse(time.RFC3339, ts.String())
-	if err != nil {
-		return nil, err
-	}
-	t = t.UTC()
-
-	var cron string = ""
-	cron += strconv.Itoa(t.Minute()) + " "
-	cron += strconv.Itoa(t.Hour()) + " "
-	cron += strconv.Itoa(t.Day()) + " "
-	cron += strconv.Itoa(int(t.Month())) + " "
-	cron += strconv.Itoa(int(t.Weekday()))
-
-	return cron, nil
-}
-
-func jpTimeAdd(arguments []interface{}) (interface{}, error) {
-	var err error
-	layout, err := validateArg("", arguments, 0, reflect.String)
-	if err != nil {
-		return nil, err
-	}
-
-	ts, err := validateArg("", arguments, 1, reflect.String)
-	if err != nil {
-		return nil, err
-	}
-
-	dr, err := validateArg("", arguments, 2, reflect.String)
-	if err != nil {
-		return nil, err
-	}
-
-	var t time.Time
-	if layout.String() != "" {
-		t, err = time.Parse(layout.String(), ts.String())
-	} else {
-		t, err = time.Parse(time.RFC3339, ts.String())
-	}
-	if err != nil {
-		return nil, err
-	}
-
-	var d time.Duration
-	d, err = time.ParseDuration(dr.String())
-	if err != nil {
-		return nil, err
-	}
-
-	return t.Add(d).Format(time.RFC3339), nil
-}
-
-func jpTimeParse(arguments []interface{}) (interface{}, error) {
-	var err error
-	layout, err := validateArg("", arguments, 0, reflect.String)
-	if err != nil {
-		return nil, err
-	}
-
-	ts, err := validateArg("", arguments, 1, reflect.String)
-	if err != nil {
-		return nil, err
-	}
-
-	var t time.Time
-	t, err = time.Parse(layout.String(), ts.String())
-	if err != nil {
-		return nil, err
-	}
-
-	return t.Format(time.RFC3339), nil
-}
diff --git a/pkg/engine/jmespath/time.go b/pkg/engine/jmespath/time.go
new file mode 100644
index 000000000..8f7ab8415
--- /dev/null
+++ b/pkg/engine/jmespath/time.go
@@ -0,0 +1,182 @@
+package jmespath
+
+import (
+	"reflect"
+	"strconv"
+	"time"
+)
+
+// function names
+var (
+	timeSince    = "time_since"
+	timeNow      = "time_now"
+	timeNowUtc   = "time_now_utc"
+	timeAdd      = "time_add"
+	timeParse    = "time_parse"
+	timeToCron   = "time_to_cron"
+	timeUtc      = "time_utc"
+	timeDiff     = "time_diff"
+	timeBefore   = "time_before"
+	timeAfter    = "time_after"
+	timeBetween  = "time_between"
+	timeTruncate = "time_truncate"
+)
+
+func getTimeArg(f string, arguments []interface{}, index int) (time.Time, error) {
+	var empty time.Time
+	arg, err := validateArg(f, arguments, index, reflect.String)
+	if err != nil {
+		return empty, err
+	}
+	return time.Parse(time.RFC3339, arg.String())
+}
+
+func getDurationArg(f string, arguments []interface{}, index int) (time.Duration, error) {
+	var empty time.Duration
+	arg, err := validateArg(f, arguments, index, reflect.String)
+	if err != nil {
+		return empty, err
+	}
+	return time.ParseDuration(arg.String())
+}
+
+func jpTimeSince(arguments []interface{}) (interface{}, error) {
+	var err error
+	layout, err := validateArg(timeSince, arguments, 0, reflect.String)
+	if err != nil {
+		return nil, err
+	}
+	ts1, err := validateArg(timeSince, arguments, 1, reflect.String)
+	if err != nil {
+		return nil, err
+	}
+	ts2, err := validateArg(timeSince, arguments, 2, reflect.String)
+	if err != nil {
+		return nil, err
+	}
+	var t1, t2 time.Time
+	if layout.String() != "" {
+		t1, err = time.Parse(layout.String(), ts1.String())
+	} else {
+		t1, err = time.Parse(time.RFC3339, ts1.String())
+	}
+	if err != nil {
+		return nil, err
+	}
+	t2 = time.Now()
+	if ts2.String() != "" {
+		if layout.String() != "" {
+			t2, err = time.Parse(layout.String(), ts2.String())
+		} else {
+			t2, err = time.Parse(time.RFC3339, ts2.String())
+		}
+		if err != nil {
+			return nil, err
+		}
+	}
+	return t2.Sub(t1).String(), nil
+}
+
+func jpTimeNow(arguments []interface{}) (interface{}, error) {
+	return time.Now().Format(time.RFC3339), nil
+}
+
+func jpTimeNowUtc(arguments []interface{}) (interface{}, error) {
+	return time.Now().UTC().Format(time.RFC3339), nil
+}
+
+func jpTimeToCron(arguments []interface{}) (interface{}, error) {
+	if t, err := getTimeArg(timeToCron, arguments, 0); err != nil {
+		return nil, err
+	} else {
+		var cron string = ""
+		cron += strconv.Itoa(t.Minute()) + " "
+		cron += strconv.Itoa(t.Hour()) + " "
+		cron += strconv.Itoa(t.Day()) + " "
+		cron += strconv.Itoa(int(t.Month())) + " "
+		cron += strconv.Itoa(int(t.Weekday()))
+		return cron, nil
+	}
+}
+
+func jpTimeAdd(arguments []interface{}) (interface{}, error) {
+	if t, err := getTimeArg(timeToCron, arguments, 0); err != nil {
+		return nil, err
+	} else if d, err := getDurationArg(timeToCron, arguments, 1); err != nil {
+		return nil, err
+	} else {
+		return t.Add(d).Format(time.RFC3339), nil
+	}
+}
+
+func jpTimeParse(arguments []interface{}) (interface{}, error) {
+	if layout, err := validateArg(timeParse, arguments, 0, reflect.String); err != nil {
+		return nil, err
+	} else if ts, err := validateArg(timeParse, arguments, 1, reflect.String); err != nil {
+		return nil, err
+	} else if t, err := time.Parse(layout.String(), ts.String()); err != nil {
+		return nil, err
+	} else {
+		return t.Format(time.RFC3339), nil
+	}
+}
+
+func jpTimeUtc(arguments []interface{}) (interface{}, error) {
+	if t, err := getTimeArg(timeUtc, arguments, 0); err != nil {
+		return nil, err
+	} else {
+		return t.UTC().Format(time.RFC3339), nil
+	}
+}
+
+func jpTimeDiff(arguments []interface{}) (interface{}, error) {
+	if t1, err := getTimeArg(timeDiff, arguments, 0); err != nil {
+		return nil, err
+	} else if t2, err := getTimeArg(timeDiff, arguments, 1); err != nil {
+		return nil, err
+	} else {
+		return t2.Sub(t1).String(), nil
+	}
+}
+
+func jpTimeBefore(arguments []interface{}) (interface{}, error) {
+	if t1, err := getTimeArg(timeBefore, arguments, 0); err != nil {
+		return nil, err
+	} else if t2, err := getTimeArg(timeBefore, arguments, 1); err != nil {
+		return nil, err
+	} else {
+		return t1.Before(t2), nil
+	}
+}
+
+func jpTimeAfter(arguments []interface{}) (interface{}, error) {
+	if t1, err := getTimeArg(timeAfter, arguments, 0); err != nil {
+		return nil, err
+	} else if t2, err := getTimeArg(timeAfter, arguments, 1); err != nil {
+		return nil, err
+	} else {
+		return t1.After(t2), nil
+	}
+}
+
+func jpTimeBetween(arguments []interface{}) (interface{}, error) {
+	if t, err := getTimeArg(timeBetween, arguments, 0); err != nil {
+		return nil, err
+	} else if start, err := getTimeArg(timeBetween, arguments, 1); err != nil {
+		return nil, err
+	} else if end, err := getTimeArg(timeBetween, arguments, 2); err != nil {
+		return nil, err
+	} else {
+		return t.After(start) && t.Before(end), nil
+	}
+}
+
+func jpTimeTruncate(arguments []interface{}) (interface{}, error) {
+	if t, err := getTimeArg(timeTruncate, arguments, 0); err != nil {
+		return nil, err
+	} else if d, err := getDurationArg(timeTruncate, arguments, 1); err != nil {
+		return nil, err
+	} else {
+		return t.Truncate(d).Format(time.RFC3339), nil
+	}
+}


Test patch:
diff --git a/pkg/engine/jmespath/functions_test.go b/pkg/engine/jmespath/functions_test.go
index a20b3c536..0c6fd4410 100644
--- a/pkg/engine/jmespath/functions_test.go
+++ b/pkg/engine/jmespath/functions_test.go
@@ -1161,36 +1161,6 @@ func Test_Base64Decode_Secret(t *testing.T) {
 	assert.Equal(t, string(result), "Hello, world!")
 }
 
-func Test_TimeSince(t *testing.T) {
-	testCases := []struct {
-		test           string
-		expectedResult string
-	}{
-		{
-			test:           "time_since('', '2021-01-02T15:04:05-07:00', '2021-01-10T03:14:05-07:00')",
-			expectedResult: "180h10m0s",
-		},
-		{
-			test:           "time_since('Mon Jan _2 15:04:05 MST 2006', 'Mon Jan 02 15:04:05 MST 2021', 'Mon Jan 10 03:14:16 MST 2021')",
-			expectedResult: "180h10m11s",
-		},
-	}
-	for i, tc := range testCases {
-		t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
-			query, err := New(tc.test)
-			assert.NilError(t, err)
-
-			res, err := query.Search("")
-			assert.NilError(t, err)
-
-			result, ok := res.(string)
-			assert.Assert(t, ok)
-
-			assert.Equal(t, result, tc.expectedResult)
-		})
-	}
-}
-
 func Test_PathCanonicalize(t *testing.T) {
 	testCases := []struct {
 		jmesPath       string
@@ -1549,93 +1519,3 @@ UFOZZVoELaasWS559wy8og39Eq21dDMynb8Bndn/
 		})
 	}
 }
-
-func Test_TimeToCron(t *testing.T) {
-	testCases := []struct {
-		test           string
-		expectedResult string
-	}{
-		{
-			test:           "time_to_cron('2023-02-02T15:04:05Z')",
-			expectedResult: "4 15 2 2 4",
-		},
-		{
-			test:           "time_to_cron('2023-02-02T15:04:05-07:00')",
-			expectedResult: "4 22 2 2 4",
-		},
-	}
-	for i, tc := range testCases {
-		t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
-			query, err := New(tc.test)
-			assert.NilError(t, err)
-
-			res, err := query.Search("")
-			assert.NilError(t, err)
-
-			result, ok := res.(string)
-			assert.Assert(t, ok)
-
-			assert.Equal(t, result, tc.expectedResult)
-		})
-	}
-}
-
-func Test_TimeAdd(t *testing.T) {
-	testCases := []struct {
-		test           string
-		expectedResult string
-	}{
-		{
-			test:           "time_add('', '2021-01-02T15:04:05-07:00', '3h')",
-			expectedResult: "2021-01-02T18:04:05-07:00",
-		},
-		{
-			test:           "time_add('Mon Jan 02 15:04:05 MST 2006', 'Sat Jan 02 15:04:05 MST 2021', '5h30m40s')",
-			expectedResult: "2021-01-02T20:34:45Z",
-		},
-	}
-	for i, tc := range testCases {
-		t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
-			query, err := New(tc.test)
-			assert.NilError(t, err)
-
-			res, err := query.Search("")
-			assert.NilError(t, err)
-
-			result, ok := res.(string)
-			assert.Assert(t, ok)
-
-			assert.Equal(t, result, tc.expectedResult)
-		})
-	}
-}
-
-func Test_TimeParse(t *testing.T) {
-	testCases := []struct {
-		test           string
-		expectedResult string
-	}{
-		{
-			test:           "time_parse('2006-01-02T15:04:05Z07:00', '2021-01-02T15:04:05-07:00')",
-			expectedResult: "2021-01-02T15:04:05-07:00",
-		},
-		{
-			test:           "time_parse('Mon Jan 02 15:04:05 MST 2006', 'Sat Jan 02 15:04:05 MST 2021')",
-			expectedResult: "2021-01-02T15:04:05Z",
-		},
-	}
-	for i, tc := range testCases {
-		t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
-			query, err := New(tc.test)
-			assert.NilError(t, err)
-
-			res, err := query.Search("")
-			assert.NilError(t, err)
-
-			result, ok := res.(string)
-			assert.Assert(t, ok)
-
-			assert.Equal(t, result, tc.expectedResult)
-		})
-	}
-}
diff --git a/pkg/engine/jmespath/time_test.go b/pkg/engine/jmespath/time_test.go
new file mode 100644
index 000000000..3e237344b
--- /dev/null
+++ b/pkg/engine/jmespath/time_test.go
@@ -0,0 +1,184 @@
+package jmespath
+
+import (
+	"fmt"
+	"testing"
+
+	"gotest.tools/assert"
+)
+
+func Test_TimeSince(t *testing.T) {
+	testCases := []struct {
+		test           string
+		expectedResult string
+	}{
+		{
+			test:           "time_since('', '2021-01-02T15:04:05-07:00', '2021-01-10T03:14:05-07:00')",
+			expectedResult: "180h10m0s",
+		},
+		{
+			test:           "time_since('Mon Jan _2 15:04:05 MST 2006', 'Mon Jan 02 15:04:05 MST 2021', 'Mon Jan 10 03:14:16 MST 2021')",
+			expectedResult: "180h10m11s",
+		},
+	}
+	for i, tc := range testCases {
+		t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
+			query, err := New(tc.test)
+			assert.NilError(t, err)
+
+			res, err := query.Search("")
+			assert.NilError(t, err)
+
+			result, ok := res.(string)
+			assert.Assert(t, ok)
+
+			assert.Equal(t, result, tc.expectedResult)
+		})
+	}
+}
+
+func Test_TimeToCron(t *testing.T) {
+	testCases := []struct {
+		test           string
+		expectedResult string
+	}{
+		{
+			test:           "time_to_cron('2023-02-02T15:04:05Z')",
+			expectedResult: "4 15 2 2 4",
+		},
+		{
+			test:           "time_to_cron(time_utc('2023-02-02T15:04:05-07:00'))",
+			expectedResult: "4 22 2 2 4",
+		},
+	}
+	for i, tc := range testCases {
+		t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
+			query, err := New(tc.test)
+			assert.NilError(t, err)
+
+			res, err := query.Search("")
+			assert.NilError(t, err)
+
+			result, ok := res.(string)
+			assert.Assert(t, ok)
+
+			assert.Equal(t, result, tc.expectedResult)
+		})
+	}
+}
+
+func Test_TimeAdd(t *testing.T) {
+	testCases := []struct {
+		test           string
+		expectedResult string
+	}{
+		{
+			test:           "time_add('2021-01-02T15:04:05-07:00', '3h')",
+			expectedResult: "2021-01-02T18:04:05-07:00",
+		},
+		{
+			test:           "time_add(time_parse('Mon Jan 02 15:04:05 MST 2006', 'Sat Jan 02 15:04:05 MST 2021'), '5h30m40s')",
+			expectedResult: "2021-01-02T20:34:45Z",
+		},
+	}
+	for i, tc := range testCases {
+		t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
+			query, err := New(tc.test)
+			assert.NilError(t, err)
+
+			res, err := query.Search("")
+			assert.NilError(t, err)
+
+			result, ok := res.(string)
+			assert.Assert(t, ok)
+
+			assert.Equal(t, result, tc.expectedResult)
+		})
+	}
+}
+
+func Test_TimeParse(t *testing.T) {
+	testCases := []struct {
+		test           string
+		expectedResult string
+	}{
+		{
+			test:           "time_parse('2006-01-02T15:04:05Z07:00', '2021-01-02T15:04:05-07:00')",
+			expectedResult: "2021-01-02T15:04:05-07:00",
+		},
+		{
+			test:           "time_parse('Mon Jan 02 15:04:05 MST 2006', 'Sat Jan 02 15:04:05 MST 2021')",
+			expectedResult: "2021-01-02T15:04:05Z",
+		},
+	}
+	for i, tc := range testCases {
+		t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
+			query, err := New(tc.test)
+			assert.NilError(t, err)
+
+			res, err := query.Search("")
+			assert.NilError(t, err)
+
+			result, ok := res.(string)
+			assert.Assert(t, ok)
+
+			assert.Equal(t, result, tc.expectedResult)
+		})
+	}
+}
+
+func Test_TimeUtc(t *testing.T) {
+	testCases := []struct {
+		test           string
+		expectedResult string
+	}{
+		{
+			test:           "time_utc('2023-02-02T15:04:05Z')",
+			expectedResult: "2023-02-02T15:04:05Z",
+		},
+		{
+			test:           "time_utc('2023-02-02T15:04:05-07:00')",
+			expectedResult: "2023-02-02T22:04:05Z",
+		},
+	}
+	for i, tc := range testCases {
+		t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
+			query, err := New(tc.test)
+			assert.NilError(t, err)
+
+			res, err := query.Search("")
+			assert.NilError(t, err)
+
+			result, ok := res.(string)
+			assert.Assert(t, ok)
+
+			assert.Equal(t, result, tc.expectedResult)
+		})
+	}
+}
+
+func Test_TimeDiff(t *testing.T) {
+	testCases := []struct {
+		test           string
+		expectedResult string
+	}{
+		{
+			test:           "time_diff('2021-01-02T15:04:05-07:00', '2021-01-10T03:14:05-07:00')",
+			expectedResult: "180h10m0s",
+		},
+	}
+	for i, tc := range testCases {
+		t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
+			query, err := New(tc.test)
+			assert.NilError(t, err)
+
+			res, err := query.Search("")
+			assert.NilError(t, err)
+
+			result, ok := res.(string)
+			assert.Assert(t, ok)
+
+			assert.Equal(t, result, tc.expectedResult)
+		})
+	}
+}