From 06b62cd7167863484ddba37349705d10f042c5ce Mon Sep 17 00:00:00 2001 From: Christoph Hartmann Date: Sun, 11 Feb 2024 20:39:50 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20parse=20time=20formats=20case-in?= =?UTF-8?q?sensitive=20(#3265)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ๐Ÿ› parse time formats case-insensitive * ๐Ÿงน handle case where compiler cannot check the args type for parse.date --- llx/builtin_resource.go | 15 ++++++-- providers-sdk/v1/testutils/testutils.go | 25 +++++++++++++ providers/core/resources/parse_test.go | 50 +++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/llx/builtin_resource.go b/llx/builtin_resource.go index 6558a07acf..3fbc4d8ede 100644 --- a/llx/builtin_resource.go +++ b/llx/builtin_resource.go @@ -256,16 +256,25 @@ func resourceDateV2(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) ( return nil, rref, err } + timestamp, ok := args[0].(string) + if !ok { + return nil, 0, errors.New("failed to parse time, timestamp needs to be a string") + } + var format string if len(args) >= 2 { - format = args[1].(string) + format, ok = args[1].(string) + if !ok { + return nil, 0, errors.New("provided time format is not provided as string") + } + format = strings.ToLower(format) if f, ok := timeFormats[format]; ok { format = f } } if format != "" { - parsed, err := time.Parse(format, args[0].(string)) + parsed, err := time.Parse(format, timestamp) if err != nil { return nil, 0, errors.New("failed to parse time: " + err.Error()) } @@ -275,7 +284,7 @@ func resourceDateV2(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) ( // Note: Yes, this approach is much slower than giving us a hint // about which time format is used. for _, format := range defaultTimeFormatsOrder { - parsed, err := time.Parse(format, args[0].(string)) + parsed, err := time.Parse(format, timestamp) if err != nil { continue } diff --git a/providers-sdk/v1/testutils/testutils.go b/providers-sdk/v1/testutils/testutils.go index 1ec57740d4..9a51895cf2 100644 --- a/providers-sdk/v1/testutils/testutils.go +++ b/providers-sdk/v1/testutils/testutils.go @@ -93,6 +93,19 @@ func (ctx *tester) ExecuteCode(bundle *llx.CodeBundle, props map[string]*llx.Pri return mql.ExecuteCode(ctx.Runtime, bundle, props, Features) } +func (ctx *tester) TestQueryPWithError(t *testing.T, query string, props map[string]*llx.Primitive) ([]*llx.RawResult, error) { + t.Helper() + bundle, err := mqlc.Compile(query, props, mqlc.NewConfig(ctx.Runtime.Schema(), Features)) + if err != nil { + return nil, fmt.Errorf("failed to compile code: %w", err) + } + err = mqlc.Invariants.Check(bundle) + if err != nil { + return nil, fmt.Errorf("failed to check invariants: %w", err) + } + return ctx.TestMqlc(t, bundle, props), nil +} + func (ctx *tester) TestQueryP(t *testing.T, query string, props map[string]*llx.Primitive) []*llx.RawResult { t.Helper() bundle, err := mqlc.Compile(query, props, mqlc.NewConfig(ctx.Runtime.Schema(), Features)) @@ -358,6 +371,18 @@ func (ctx *tester) TestSimpleErrors(t *testing.T, tests []SimpleTest) { } } +func (ctx *tester) TestCompileErrors(t *testing.T, tests []SimpleTest) { + for i := range tests { + cur := tests[i] + t.Run(cur.Code, func(t *testing.T) { + res := ctx.TestQuery(t, cur.Code) + assert.NotEmpty(t, res) + assert.Equal(t, cur.Expectation, res[cur.ResultIndex].Result().Error) + assert.Nil(t, res[cur.ResultIndex].Data.Value) + }) + } +} + func TestNoResultErrors(t *testing.T, r []*llx.RawResult) bool { var found bool for i := range r { diff --git a/providers/core/resources/parse_test.go b/providers/core/resources/parse_test.go index e7e4ac8029..4d4cbf2a67 100644 --- a/providers/core/resources/parse_test.go +++ b/providers/core/resources/parse_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "go.mondoo.com/cnquery/v10/llx" "go.mondoo.com/cnquery/v10/providers-sdk/v1/testutils" ) @@ -33,9 +34,58 @@ func TestParse_Date(t *testing.T) { ResultIndex: 0, Expectation: &simpleDate, }, + { + Code: "parse.date('2023-12-23T00:00:00Z', 'rfc3339')", + ResultIndex: 0, + Expectation: &simpleDate, + }, + { + Code: "parse.date('2023-12-23T00:00:00Z', 'RFC3339')", // ensure the format finding is case-insensitive + ResultIndex: 0, + Expectation: &simpleDate, + }, }) } +func TestParse_DateError(t *testing.T) { + tests := []testutils.SimpleTest{ + { + Code: "parse.date('2023-12-23T00:00:00Z', 1234)", // handle invalid format + ResultIndex: 0, + Expectation: "failed to compile code: Incorrect type on argument 1 in parse.date: expected string, got: int", + }, + { + Code: "parse.date(123456)", // handle invalid timestamp format + ResultIndex: 0, + Expectation: "failed to compile code: Incorrect type on argument 0 in parse.date: expected string, got: int", + }, + { + Code: "dict = { 'x': 'y'}; parse.date(dict)", // handle invalid input that is a dict + ResultIndex: 0, + Expectation: "failed to compile code: Incorrect type on argument 0 in parse.date: expected string, got: map[string]string", + }, + { + Code: "parse.date(mondoo.jobEnvironment)", // handle invalid input that is a dict and cannot be handled during compile time + ResultIndex: 0, + Expectation: "failed to parse time, timestamp needs to be a string", + }, + } + for i := range tests { + cur := tests[i] + t.Run(cur.Code, func(t *testing.T) { + res, err := x.TestQueryPWithError(t, cur.Code, nil) + var errMsg string + if err != nil { + errMsg = err.Error() + } + if res != nil && res[0].Data.Error != nil { + errMsg = res[0].Data.Error.Error() + } + assert.Equal(t, cur.Expectation, errMsg) + }) + } +} + func TestParse_Duration(t *testing.T) { twoSecs := llx.DurationToTime(2) tenMin := llx.DurationToTime(10 * 60)