From 7804f4d74ab553f722161d2b01fcfc5914d903f4 Mon Sep 17 00:00:00 2001 From: dawidwisn Date: Thu, 22 Aug 2024 08:01:37 +0200 Subject: [PATCH 1/8] PC-13699 Add startDate to Replay --- sdk/models/replay.go | 44 +++++++++++++++++++++++++++++++++------ sdk/models/replay_test.go | 24 +++++++++++++++++++++ 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/sdk/models/replay.go b/sdk/models/replay.go index 6639c8ea..85f4cfb4 100644 --- a/sdk/models/replay.go +++ b/sdk/models/replay.go @@ -15,9 +15,10 @@ const maximumAllowedReplayDuration = time.Hour * 24 * 30 // Replay Struct used for posting replay entity. type Replay struct { - Project string `json:"project"` - Slo string `json:"slo"` - Duration ReplayDuration `json:"duration"` + Project string `json:"project"` + Slo string `json:"slo"` + Duration ReplayDuration `json:"duration,omitempty"` + StartDate time.Time `json:"startDate,omitempty"` } type ReplayDuration struct { @@ -71,10 +72,25 @@ var replayValidation = validation.New[Replay]( Required(), validation.For(func(r Replay) ReplayDuration { return r.Duration }). WithName("duration"). - Required(). + When( + func(r Replay) bool { return !isEmpty(r.Duration) || (r.StartDate.IsZero() && isEmpty(r.Duration)) }, + ). Cascade(validation.CascadeModeStop). Include(replayDurationValidation). Rules(replayDurationValidationRule()), + validation.For(func(r Replay) time.Time { return r.StartDate }). + WithName("startDate"). + When( + func(r Replay) bool { return !r.StartDate.IsZero() }, + ). + Rules(replayStartTimeValidationRule()), + validation.For(func(r Replay) Replay { return r }). + Rules(validation.NewSingleRule(func(r Replay) error { + if !isEmpty(r.Duration) && !r.StartDate.IsZero() { + return errors.New("only one of duration or startDate can be set") + } + return nil + }).WithErrorCode(replayDurationAndStartDateValidationError)), ) var replayDurationValidation = validation.New[ReplayDuration]( @@ -97,8 +113,9 @@ func (r Replay) Validate() error { } const ( - replayDurationValidationErrorCode = "replay_duration" - replayDurationUnitValidationErrorCode = "replay_duration_unit" + replayDurationValidationErrorCode = "replay_duration" + replayDurationUnitValidationErrorCode = "replay_duration_unit" + replayDurationAndStartDateValidationError = "replay_duration_or_start_date" ) func replayDurationValidationRule() validation.SingleRule[ReplayDuration] { @@ -115,6 +132,17 @@ func replayDurationValidationRule() validation.SingleRule[ReplayDuration] { }).WithErrorCode(replayDurationValidationErrorCode) } +func replayStartTimeValidationRule() validation.SingleRule[time.Time] { + return validation.NewSingleRule(func(v time.Time) error { + duration := time.Since(v) + if duration > maximumAllowedReplayDuration { + return errors.Errorf("%s duration must not be greater than %s", + duration, maximumAllowedReplayDuration) + } + return nil + }).WithErrorCode(replayDurationValidationErrorCode) +} + // ParseJSONToReplayStruct parse raw json into v1alpha.Replay struct with validation. func ParseJSONToReplayStruct(data io.Reader) (Replay, error) { replay := Replay{} @@ -167,3 +195,7 @@ func ValidateReplayDurationUnit(unit string) error { } return ErrInvalidReplayDurationUnit } + +func isEmpty(duration ReplayDuration) bool { + return duration.Unit == "" || duration.Value == 0 +} diff --git a/sdk/models/replay_test.go b/sdk/models/replay_test.go index 719102e8..0769f7bc 100644 --- a/sdk/models/replay_test.go +++ b/sdk/models/replay_test.go @@ -130,6 +130,29 @@ func TestReplayStructDatesValidation(t *testing.T) { isValid: false, ErrorCode: validation.ErrorCodeRequired, }, + { + name: "correct struct start date", + replay: Replay{ + Project: "project", + Slo: "slo", + StartDate: time.Now().Add(-time.Hour * 24), + }, + isValid: true, + }, + { + name: "only one of duration or start date can be set", + replay: Replay{ + Project: "project", + Slo: "slo", + Duration: ReplayDuration{ + Unit: "Day", + Value: 30, + }, + StartDate: time.Now().Add(-time.Hour * 24), + }, + isValid: false, + ErrorCode: replayDurationAndStartDateValidationError, + }, } for _, tt := range tests { @@ -141,6 +164,7 @@ func TestReplayStructDatesValidation(t *testing.T) { if tc.isValid { assert.Nil(t, err) } else { + require.Error(t, err) require.IsType(t, &validation.ValidatorError{}, err) assert.True(t, validation.HasErrorCode(err, tc.ErrorCode)) From 6fa8244c5895fa60c77058e10bce82993928d20b Mon Sep 17 00:00:00 2001 From: dawidwisn Date: Thu, 22 Aug 2024 08:40:11 +0200 Subject: [PATCH 2/8] PC-13699 Add tests --- sdk/models/replay_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/sdk/models/replay_test.go b/sdk/models/replay_test.go index 0769f7bc..a836998c 100644 --- a/sdk/models/replay_test.go +++ b/sdk/models/replay_test.go @@ -153,6 +153,32 @@ func TestReplayStructDatesValidation(t *testing.T) { isValid: false, ErrorCode: replayDurationAndStartDateValidationError, }, + { + name: "use start date without duration", + replay: Replay{ + Project: "project", + Slo: "slo", + Duration: ReplayDuration{ + Unit: "", + Value: 0, + }, + StartDate: time.Now().Add(-time.Hour * 24), + }, + isValid: true, + }, + { + name: "only one of duration", + replay: Replay{ + Project: "project", + Slo: "slo", + Duration: ReplayDuration{ + Unit: "Day", + Value: 30, + }, + StartDate: time.Date(0001, 1, 1, 0, 0, 0, 0, time.UTC), + }, + isValid: true, + }, } for _, tt := range tests { From 1ab21ad02f4540e76e9708e96953fc08c946ac20 Mon Sep 17 00:00:00 2001 From: dawidwisn Date: Thu, 22 Aug 2024 10:13:01 +0200 Subject: [PATCH 3/8] PC-13699 Add ReplayTimeRange --- sdk/models/replay.go | 23 +++++++++++++++-------- sdk/models/replay_test.go | 20 ++++++++++++++------ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/sdk/models/replay.go b/sdk/models/replay.go index 85f4cfb4..576836e7 100644 --- a/sdk/models/replay.go +++ b/sdk/models/replay.go @@ -15,10 +15,10 @@ const maximumAllowedReplayDuration = time.Hour * 24 * 30 // Replay Struct used for posting replay entity. type Replay struct { - Project string `json:"project"` - Slo string `json:"slo"` - Duration ReplayDuration `json:"duration,omitempty"` - StartDate time.Time `json:"startDate,omitempty"` + Project string `json:"project"` + Slo string `json:"slo"` + Duration ReplayDuration `json:"duration""` + TimeRange ReplayTimeRange `json:"timeRange"` } type ReplayDuration struct { @@ -26,6 +26,11 @@ type ReplayDuration struct { Value int `json:"value"` } +type ReplayTimeRange struct { + StartDate time.Time `json:"startDate,omitempty"` + EndDate time.Time `json:"endDate,omitempty"` //not supported yet +} + // ReplayWithStatus used for returning Replay data with status. type ReplayWithStatus struct { Project string `json:"project"` @@ -73,20 +78,22 @@ var replayValidation = validation.New[Replay]( validation.For(func(r Replay) ReplayDuration { return r.Duration }). WithName("duration"). When( - func(r Replay) bool { return !isEmpty(r.Duration) || (r.StartDate.IsZero() && isEmpty(r.Duration)) }, + func(r Replay) bool { + return !isEmpty(r.Duration) || (r.TimeRange.StartDate.IsZero() && isEmpty(r.Duration)) + }, ). Cascade(validation.CascadeModeStop). Include(replayDurationValidation). Rules(replayDurationValidationRule()), - validation.For(func(r Replay) time.Time { return r.StartDate }). + validation.For(func(r Replay) time.Time { return r.TimeRange.StartDate }). WithName("startDate"). When( - func(r Replay) bool { return !r.StartDate.IsZero() }, + func(r Replay) bool { return !r.TimeRange.StartDate.IsZero() }, ). Rules(replayStartTimeValidationRule()), validation.For(func(r Replay) Replay { return r }). Rules(validation.NewSingleRule(func(r Replay) error { - if !isEmpty(r.Duration) && !r.StartDate.IsZero() { + if !isEmpty(r.Duration) && !r.TimeRange.StartDate.IsZero() { return errors.New("only one of duration or startDate can be set") } return nil diff --git a/sdk/models/replay_test.go b/sdk/models/replay_test.go index a836998c..245f42c7 100644 --- a/sdk/models/replay_test.go +++ b/sdk/models/replay_test.go @@ -133,9 +133,11 @@ func TestReplayStructDatesValidation(t *testing.T) { { name: "correct struct start date", replay: Replay{ - Project: "project", - Slo: "slo", - StartDate: time.Now().Add(-time.Hour * 24), + Project: "project", + Slo: "slo", + TimeRange: ReplayTimeRange{ + StartDate: time.Now().Add(-time.Hour * 24), + }, }, isValid: true, }, @@ -148,7 +150,9 @@ func TestReplayStructDatesValidation(t *testing.T) { Unit: "Day", Value: 30, }, - StartDate: time.Now().Add(-time.Hour * 24), + TimeRange: ReplayTimeRange{ + StartDate: time.Now().Add(-time.Hour * 24), + }, }, isValid: false, ErrorCode: replayDurationAndStartDateValidationError, @@ -162,7 +166,9 @@ func TestReplayStructDatesValidation(t *testing.T) { Unit: "", Value: 0, }, - StartDate: time.Now().Add(-time.Hour * 24), + TimeRange: ReplayTimeRange{ + StartDate: time.Now().Add(-time.Hour * 24), + }, }, isValid: true, }, @@ -175,7 +181,9 @@ func TestReplayStructDatesValidation(t *testing.T) { Unit: "Day", Value: 30, }, - StartDate: time.Date(0001, 1, 1, 0, 0, 0, 0, time.UTC), + TimeRange: ReplayTimeRange{ + StartDate: time.Date(0001, 1, 1, 0, 0, 0, 0, time.UTC), + }, }, isValid: true, }, From aa198f7c0bb80015b6eda3639e24e1002b152349 Mon Sep 17 00:00:00 2001 From: dawidwisn Date: Thu, 22 Aug 2024 10:14:32 +0200 Subject: [PATCH 4/8] PC-13699 Add ReplayTimeRange --- sdk/models/replay.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/models/replay.go b/sdk/models/replay.go index 576836e7..372061bf 100644 --- a/sdk/models/replay.go +++ b/sdk/models/replay.go @@ -17,8 +17,8 @@ const maximumAllowedReplayDuration = time.Hour * 24 * 30 type Replay struct { Project string `json:"project"` Slo string `json:"slo"` - Duration ReplayDuration `json:"duration""` - TimeRange ReplayTimeRange `json:"timeRange"` + Duration ReplayDuration `json:"duration,omitempty""` + TimeRange ReplayTimeRange `json:"timeRange,omitempty"` } type ReplayDuration struct { From 1e5cb6c98e4ebeef2e7ded81583d19330d7c4220 Mon Sep 17 00:00:00 2001 From: dawidwisn Date: Thu, 22 Aug 2024 10:27:29 +0200 Subject: [PATCH 5/8] PC-13699 fix --- sdk/models/replay.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/models/replay.go b/sdk/models/replay.go index 372061bf..46a9e2ad 100644 --- a/sdk/models/replay.go +++ b/sdk/models/replay.go @@ -17,7 +17,7 @@ const maximumAllowedReplayDuration = time.Hour * 24 * 30 type Replay struct { Project string `json:"project"` Slo string `json:"slo"` - Duration ReplayDuration `json:"duration,omitempty""` + Duration ReplayDuration `json:"duration,omitempty"` TimeRange ReplayTimeRange `json:"timeRange,omitempty"` } From 6edd56c8918f6357cda8fde755724e2e92a384f1 Mon Sep 17 00:00:00 2001 From: dawidwisn Date: Thu, 22 Aug 2024 11:13:13 +0200 Subject: [PATCH 6/8] PC-13699 lint --- sdk/models/replay.go | 2 +- sdk/models/replay_test.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/sdk/models/replay.go b/sdk/models/replay.go index 46a9e2ad..30faf25c 100644 --- a/sdk/models/replay.go +++ b/sdk/models/replay.go @@ -28,7 +28,7 @@ type ReplayDuration struct { type ReplayTimeRange struct { StartDate time.Time `json:"startDate,omitempty"` - EndDate time.Time `json:"endDate,omitempty"` //not supported yet + EndDate time.Time `json:"endDate,omitempty"` // not supported yet } // ReplayWithStatus used for returning Replay data with status. diff --git a/sdk/models/replay_test.go b/sdk/models/replay_test.go index 245f42c7..f5636095 100644 --- a/sdk/models/replay_test.go +++ b/sdk/models/replay_test.go @@ -182,7 +182,7 @@ func TestReplayStructDatesValidation(t *testing.T) { Value: 30, }, TimeRange: ReplayTimeRange{ - StartDate: time.Date(0001, 1, 1, 0, 0, 0, 0, time.UTC), + StartDate: time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC), }, }, isValid: true, @@ -198,7 +198,6 @@ func TestReplayStructDatesValidation(t *testing.T) { if tc.isValid { assert.Nil(t, err) } else { - require.Error(t, err) require.IsType(t, &validation.ValidatorError{}, err) assert.True(t, validation.HasErrorCode(err, tc.ErrorCode)) From 807287849c4f89297a208f61008e9d3d9b3f0d1a Mon Sep 17 00:00:00 2001 From: marcinlawnik Date: Thu, 29 Aug 2024 18:53:48 +0200 Subject: [PATCH 7/8] fixes after govy release --- sdk/models/replay.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/models/replay.go b/sdk/models/replay.go index 6e2bcbcb..8545d33b 100644 --- a/sdk/models/replay.go +++ b/sdk/models/replay.go @@ -93,7 +93,7 @@ var replayValidation = govy.New[Replay]( ). Rules(replayStartTimeValidationRule()), govy.For(func(r Replay) Replay { return r }). - Rules(govy.NewSingleRule(func(r Replay) error { + Rules(govy.NewRule(func(r Replay) error { if !isEmpty(r.Duration) && !r.TimeRange.StartDate.IsZero() { return errors.New("only one of duration or startDate can be set") } From 6f735d8fbd7a25f7dfd9835151b424bba3713d20 Mon Sep 17 00:00:00 2001 From: marcinlawnik Date: Thu, 29 Aug 2024 19:02:35 +0200 Subject: [PATCH 8/8] Add validation for startDate cannot be in the future, add test --- sdk/models/replay.go | 16 +++++++++++++++- sdk/models/replay_test.go | 12 ++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/sdk/models/replay.go b/sdk/models/replay.go index 8545d33b..72478bbe 100644 --- a/sdk/models/replay.go +++ b/sdk/models/replay.go @@ -91,7 +91,10 @@ var replayValidation = govy.New[Replay]( When( func(r Replay) bool { return !r.TimeRange.StartDate.IsZero() }, ). - Rules(replayStartTimeValidationRule()), + Rules( + replayStartTimeValidationRule(), + replayStartTimeNotInFutureValidationRule(), + ), govy.For(func(r Replay) Replay { return r }). Rules(govy.NewRule(func(r Replay) error { if !isEmpty(r.Duration) && !r.TimeRange.StartDate.IsZero() { @@ -124,6 +127,7 @@ const ( replayDurationValidationErrorCode = "replay_duration" replayDurationUnitValidationErrorCode = "replay_duration_unit" replayDurationAndStartDateValidationError = "replay_duration_or_start_date" + replayStartDateInTheFutureValidationError = "replay_duration_or_start_date_future" ) func replayDurationValidationRule() govy.Rule[ReplayDuration] { @@ -151,6 +155,16 @@ func replayStartTimeValidationRule() govy.Rule[time.Time] { }).WithErrorCode(replayDurationValidationErrorCode) } +func replayStartTimeNotInFutureValidationRule() govy.Rule[time.Time] { + return govy.NewRule(func(v time.Time) error { + now := time.Now() + if v.After(now) { + return errors.Errorf("startDate %s must not be in the future", v) + } + return nil + }).WithErrorCode(replayStartDateInTheFutureValidationError) +} + // ParseJSONToReplayStruct parse raw json into v1alpha.Replay struct with govy. func ParseJSONToReplayStruct(data io.Reader) (Replay, error) { replay := Replay{} diff --git a/sdk/models/replay_test.go b/sdk/models/replay_test.go index 37b7c2a6..4bc08e14 100644 --- a/sdk/models/replay_test.go +++ b/sdk/models/replay_test.go @@ -158,6 +158,18 @@ func TestReplayStructDatesValidation(t *testing.T) { isValid: false, ErrorCode: replayDurationAndStartDateValidationError, }, + { + name: "start date cannot be in the future", + replay: Replay{ + Project: "project", + Slo: "slo", + TimeRange: ReplayTimeRange{ + StartDate: time.Now().Add(time.Minute * 1), + }, + }, + isValid: false, + ErrorCode: replayStartDateInTheFutureValidationError, + }, { name: "use start date without duration", replay: Replay{