package timeinterval import ( "context" "embed" "encoding/json" "fmt" "net/http" "path" "slices" "testing" "github.com/prometheus/alertmanager/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/dynamic" "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/timeinterval/v0alpha1" "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/timeinterval/v0alpha1/fakes" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/tests/api/alerting" "github.com/grafana/grafana/pkg/tests/apis" "github.com/grafana/grafana/pkg/tests/testinfra" "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" ) //go:embed test-data/*.* var testData embed.FS func TestMain(m *testing.M) { testsuite.RunButSkipOnSpanner(m) } func getTestHelper(t *testing.T) *apis.K8sTestHelper { return apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{}) } func TestIntegrationResourceIdentifier(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } ctx := context.Background() helper := getTestHelper(t) client := newClient(t, helper.Org1.Admin) newInterval := &v0alpha1.TimeInterval{ ObjectMeta: v1.ObjectMeta{ Namespace: "default", }, Spec: v0alpha1.Spec{ Name: "time-newInterval", TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(2), }, } t.Run("create should fail if object name is specified", func(t *testing.T) { interval := newInterval.Copy().(*v0alpha1.TimeInterval) interval.Name = "time-newInterval" _, err := client.Create(ctx, interval, v1.CreateOptions{}) require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest but got %s", err) }) var resourceID string t.Run("create should succeed and provide resource name", func(t *testing.T) { actual, err := client.Create(ctx, newInterval, v1.CreateOptions{}) require.NoError(t, err) require.NotEmptyf(t, actual.Name, "Resource name should not be empty") require.NotEmptyf(t, actual.UID, "Resource UID should not be empty") resourceID = actual.Name }) var existingInterval *v0alpha1.TimeInterval t.Run("resource should be available by the identifier", func(t *testing.T) { actual, err := client.Get(ctx, resourceID, v1.GetOptions{}) require.NoError(t, err) require.NotEmptyf(t, actual.Name, "Resource name should not be empty") require.Equal(t, newInterval.Spec, actual.Spec) existingInterval = actual }) t.Run("update should rename interval if name in the specification changes", func(t *testing.T) { if existingInterval == nil { t.Skip() } updated := existingInterval.Copy().(*v0alpha1.TimeInterval) updated.Spec.Name = "another-newInterval" actual, err := client.Update(ctx, updated, v1.UpdateOptions{}) require.NoError(t, err) require.Equal(t, updated.Spec, actual.Spec) require.NotEqualf(t, updated.Name, actual.Name, "Update should change the resource name but it didn't") require.NotEqualf(t, updated.ResourceVersion, actual.ResourceVersion, "Update should change the resource version but it didn't") resource, err := client.Get(ctx, actual.Name, v1.GetOptions{}) require.NoError(t, err) require.Equal(t, actual, resource) }) } func TestIntegrationTimeIntervalAccessControl(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } ctx := context.Background() helper := getTestHelper(t) org1 := helper.Org1 type testCase struct { user apis.User canRead bool canUpdate bool canCreate bool canDelete bool } reader := helper.CreateUser("IntervalsReader", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{ { Actions: []string{ accesscontrol.ActionAlertingNotificationsTimeIntervalsRead, }, }, }) writer := helper.CreateUser("IntervalsWriter", "Org1", org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{ { Actions: []string{ accesscontrol.ActionAlertingNotificationsTimeIntervalsRead, accesscontrol.ActionAlertingNotificationsTimeIntervalsWrite, }, }, }) deleter := helper.CreateUser("IntervalsDeleter", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{ { Actions: []string{ accesscontrol.ActionAlertingNotificationsTimeIntervalsRead, accesscontrol.ActionAlertingNotificationsTimeIntervalsDelete, }, }, }) testCases := []testCase{ { user: org1.Admin, canRead: true, canUpdate: true, canCreate: true, canDelete: true, }, { user: org1.Editor, canRead: true, canUpdate: true, canCreate: true, canDelete: true, }, { user: org1.Viewer, canRead: true, }, { user: reader, canRead: true, }, { user: writer, canRead: true, canCreate: true, canUpdate: true, }, { user: deleter, canRead: true, canDelete: true, }, } adminClient := newClient(t, helper.Org1.Admin) for _, tc := range testCases { t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) { client := newClient(t, tc.user) var expected = &v0alpha1.TimeInterval{ ObjectMeta: v1.ObjectMeta{ Namespace: "default", }, Spec: v0alpha1.Spec{ Name: fmt.Sprintf("time-interval-1-%s", tc.user.Identity.GetLogin()), TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(2), }, } expected.SetProvenanceStatus("") d, err := json.Marshal(expected) require.NoError(t, err) if tc.canCreate { t.Run("should be able to create time interval", func(t *testing.T) { actual, err := client.Create(ctx, expected, v1.CreateOptions{}) require.NoErrorf(t, err, "Payload %s", string(d)) require.Equal(t, expected.Spec, actual.Spec) t.Run("should fail if already exists", func(t *testing.T) { _, err := client.Create(ctx, actual, v1.CreateOptions{}) require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err) }) expected = actual }) } else { t.Run("should be forbidden to create", func(t *testing.T) { _, err := client.Create(ctx, expected, v1.CreateOptions{}) require.Truef(t, errors.IsForbidden(err), "Payload %s", string(d)) }) // create resource to proceed with other tests expected, err = adminClient.Create(ctx, expected, v1.CreateOptions{}) require.NoErrorf(t, err, "Payload %s", string(d)) require.NotNil(t, expected) } if tc.canRead { t.Run("should be able to list time intervals", func(t *testing.T) { list, err := client.List(ctx, v1.ListOptions{}) require.NoError(t, err) require.Len(t, list.Items, 1) }) t.Run("should be able to read time interval by resource identifier", func(t *testing.T) { got, err := client.Get(ctx, expected.Name, v1.GetOptions{}) require.NoError(t, err) require.Equal(t, expected, got) t.Run("should get NotFound if resource does not exist", func(t *testing.T) { _, err := client.Get(ctx, "Notfound", v1.GetOptions{}) require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err) }) }) } else { t.Run("should be forbidden to list time intervals", func(t *testing.T) { _, err := client.List(ctx, v1.ListOptions{}) require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) }) t.Run("should be forbidden to read time interval by name", func(t *testing.T) { _, err := client.Get(ctx, expected.Name, v1.GetOptions{}) require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) t.Run("should get forbidden even if name does not exist", func(t *testing.T) { _, err := client.Get(ctx, "Notfound", v1.GetOptions{}) require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) }) }) } updatedExpected := expected.Copy().(*v0alpha1.TimeInterval) updatedExpected.Spec.TimeIntervals = fakes.IntervalGenerator{}.GenerateMany(2) d, err = json.Marshal(updatedExpected) require.NoError(t, err) if tc.canUpdate { t.Run("should be able to update time interval", func(t *testing.T) { updated, err := client.Update(ctx, updatedExpected, v1.UpdateOptions{}) require.NoErrorf(t, err, "Payload %s", string(d)) expected = updated t.Run("should get NotFound if name does not exist", func(t *testing.T) { up := updatedExpected.Copy().(*v0alpha1.TimeInterval) up.Name = "notFound" _, err := client.Update(ctx, up, v1.UpdateOptions{}) require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err) }) }) } else { t.Run("should be forbidden to update time interval", func(t *testing.T) { _, err := client.Update(ctx, updatedExpected, v1.UpdateOptions{}) require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) t.Run("should get forbidden even if resource does not exist", func(t *testing.T) { up := updatedExpected.Copy().(*v0alpha1.TimeInterval) up.Name = "notFound" _, err := client.Update(ctx, up, v1.UpdateOptions{}) require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) }) }) } deleteOptions := v1.DeleteOptions{Preconditions: &v1.Preconditions{ResourceVersion: util.Pointer(expected.ResourceVersion)}} if tc.canDelete { t.Run("should be able to delete time interval", func(t *testing.T) { err := client.Delete(ctx, expected.Name, deleteOptions) require.NoError(t, err) t.Run("should get NotFound if name does not exist", func(t *testing.T) { err := client.Delete(ctx, "notfound", v1.DeleteOptions{}) require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err) }) }) } else { t.Run("should be forbidden to delete time interval", func(t *testing.T) { err := client.Delete(ctx, expected.Name, deleteOptions) require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) t.Run("should be forbidden even if resource does not exist", func(t *testing.T) { err := client.Delete(ctx, "notfound", v1.DeleteOptions{}) require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) }) }) require.NoError(t, adminClient.Delete(ctx, expected.Name, v1.DeleteOptions{})) } if tc.canRead { t.Run("should get empty list if no mute timings", func(t *testing.T) { list, err := client.List(ctx, v1.ListOptions{}) require.NoError(t, err) require.Len(t, list.Items, 0) }) } }) } } func TestIntegrationTimeIntervalProvisioning(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } ctx := context.Background() helper := getTestHelper(t) org := helper.Org1 admin := org.Admin adminClient := newClient(t, helper.Org1.Admin) env := helper.GetEnv() ac := acimpl.ProvideAccessControl(env.FeatureToggles) db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest())) require.NoError(t, err) created, err := adminClient.Create(ctx, &v0alpha1.TimeInterval{ ObjectMeta: v1.ObjectMeta{ Namespace: "default", }, Spec: v0alpha1.Spec{ Name: "time-interval-1", TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(2), }, }, v1.CreateOptions{}) require.NoError(t, err) require.Equal(t, "none", created.GetProvenanceStatus()) t.Run("should provide provenance status", func(t *testing.T) { require.NoError(t, db.SetProvenance(ctx, &definitions.MuteTimeInterval{ MuteTimeInterval: config.MuteTimeInterval{ Name: created.Spec.Name, }, }, admin.Identity.GetOrgID(), "API")) got, err := adminClient.Get(ctx, created.Name, v1.GetOptions{}) require.NoError(t, err) require.Equal(t, "API", got.GetProvenanceStatus()) }) t.Run("should not let update if provisioned", func(t *testing.T) { updated := created.Copy().(*v0alpha1.TimeInterval) updated.Spec.TimeIntervals = fakes.IntervalGenerator{}.GenerateMany(2) _, err := adminClient.Update(ctx, updated, v1.UpdateOptions{}) require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) }) t.Run("should not let delete if provisioned", func(t *testing.T) { err := adminClient.Delete(ctx, created.Name, v1.DeleteOptions{}) require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) }) } func TestIntegrationTimeIntervalOptimisticConcurrency(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } ctx := context.Background() helper := getTestHelper(t) adminClient := newClient(t, helper.Org1.Admin) interval := v0alpha1.TimeInterval{ ObjectMeta: v1.ObjectMeta{ Namespace: "default", }, Spec: v0alpha1.Spec{ Name: "time-interval", TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(2), }, } created, err := adminClient.Create(ctx, &interval, v1.CreateOptions{}) require.NoError(t, err) require.NotNil(t, created) require.NotEmpty(t, created.ResourceVersion) t.Run("should forbid if version does not match", func(t *testing.T) { updated := created.Copy().(*v0alpha1.TimeInterval) updated.ResourceVersion = "test" _, err := adminClient.Update(ctx, updated, v1.UpdateOptions{}) require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err) }) t.Run("should update if version matches", func(t *testing.T) { updated := created.Copy().(*v0alpha1.TimeInterval) updated.Spec.TimeIntervals = fakes.IntervalGenerator{}.GenerateMany(2) actualUpdated, err := adminClient.Update(ctx, updated, v1.UpdateOptions{}) require.NoError(t, err) require.EqualValues(t, updated.Spec, actualUpdated.Spec) require.NotEqual(t, updated.ResourceVersion, actualUpdated.ResourceVersion) }) t.Run("should update if version is empty", func(t *testing.T) { updated := created.Copy().(*v0alpha1.TimeInterval) updated.ResourceVersion = "" updated.Spec.TimeIntervals = fakes.IntervalGenerator{}.GenerateMany(2) actualUpdated, err := adminClient.Update(ctx, updated, v1.UpdateOptions{}) require.NoError(t, err) require.EqualValues(t, updated.Spec, actualUpdated.Spec) require.NotEqual(t, created.ResourceVersion, actualUpdated.ResourceVersion) }) t.Run("should fail to delete if version does not match", func(t *testing.T) { actual, err := adminClient.Get(ctx, created.Name, v1.GetOptions{}) require.NoError(t, err) err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{ Preconditions: &v1.Preconditions{ ResourceVersion: util.Pointer("something"), }, }) require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err) }) t.Run("should succeed if version matches", func(t *testing.T) { actual, err := adminClient.Get(ctx, created.Name, v1.GetOptions{}) require.NoError(t, err) err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{ Preconditions: &v1.Preconditions{ ResourceVersion: util.Pointer(actual.ResourceVersion), }, }) require.NoError(t, err) }) t.Run("should succeed if version is empty", func(t *testing.T) { actual, err := adminClient.Create(ctx, &interval, v1.CreateOptions{}) require.NoError(t, err) err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{ Preconditions: &v1.Preconditions{ ResourceVersion: util.Pointer(actual.ResourceVersion), }, }) require.NoError(t, err) }) } func TestIntegrationTimeIntervalPatch(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } ctx := context.Background() helper := getTestHelper(t) adminClient := newClient(t, helper.Org1.Admin) interval := v0alpha1.TimeInterval{ ObjectMeta: v1.ObjectMeta{ Namespace: "default", }, Spec: v0alpha1.Spec{ Name: "time-interval", TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(2), }, } current, err := adminClient.Create(ctx, &interval, v1.CreateOptions{}) require.NoError(t, err) require.NotNil(t, current) require.NotEmpty(t, current.ResourceVersion) t.Run("should patch with merge patch", func(t *testing.T) { patch := `{ "spec": { "time_intervals" : [] } }` result, err := adminClient.Patch(ctx, current.Name, types.MergePatchType, []byte(patch), v1.PatchOptions{}) require.NoError(t, err) require.Empty(t, result.Spec.TimeIntervals) current = result }) t.Run("should patch with json patch", func(t *testing.T) { expected := fakes.IntervalGenerator{}.Generate() patch := []map[string]interface{}{ { "op": "add", "path": "/spec/time_intervals/-", "value": expected, }, } patchData, err := json.Marshal(patch) require.NoError(t, err) result, err := adminClient.Patch(ctx, current.Name, types.JSONPatchType, patchData, v1.PatchOptions{}) require.NoError(t, err) expectedSpec := v0alpha1.Spec{ Name: current.Spec.Name, TimeIntervals: []v0alpha1.Interval{ expected, }, } require.EqualValues(t, expectedSpec, result.Spec) current = result }) } func TestIntegrationTimeIntervalListSelector(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } ctx := context.Background() helper := getTestHelper(t) adminClient := newClient(t, helper.Org1.Admin) interval1 := &v0alpha1.TimeInterval{ ObjectMeta: v1.ObjectMeta{ Namespace: "default", }, Spec: v0alpha1.Spec{ Name: "test1", TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(2), }, } interval1, err := adminClient.Create(ctx, interval1, v1.CreateOptions{}) require.NoError(t, err) interval2 := &v0alpha1.TimeInterval{ ObjectMeta: v1.ObjectMeta{ Namespace: "default", }, Spec: v0alpha1.Spec{ Name: "test2", TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(2), }, } interval2, err = adminClient.Create(ctx, interval2, v1.CreateOptions{}) require.NoError(t, err) env := helper.GetEnv() ac := acimpl.ProvideAccessControl(env.FeatureToggles) db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest())) require.NoError(t, err) require.NoError(t, db.SetProvenance(ctx, &definitions.MuteTimeInterval{ MuteTimeInterval: config.MuteTimeInterval{ Name: interval2.Spec.Name, }, }, helper.Org1.Admin.Identity.GetOrgID(), "API")) interval2, err = adminClient.Get(ctx, interval2.Name, v1.GetOptions{}) require.NoError(t, err) intervals, err := adminClient.List(ctx, v1.ListOptions{}) require.NoError(t, err) require.Len(t, intervals.Items, 2) t.Run("should filter by interval name", func(t *testing.T) { list, err := adminClient.List(ctx, v1.ListOptions{ FieldSelector: "spec.name=" + interval1.Spec.Name, }) require.NoError(t, err) require.Len(t, list.Items, 1) require.Equal(t, interval1.Name, list.Items[0].Name) }) t.Run("should filter by interval metadata name", func(t *testing.T) { list, err := adminClient.List(ctx, v1.ListOptions{ FieldSelector: "metadata.name=" + interval2.Name, }) require.NoError(t, err) require.Len(t, list.Items, 1) require.Equal(t, interval2.Name, list.Items[0].Name) }) t.Run("should filter by multiple filters", func(t *testing.T) { list, err := adminClient.List(ctx, v1.ListOptions{ FieldSelector: fmt.Sprintf("metadata.name=%s,spec.name=%s", interval2.Name, interval2.Spec.Name), }) require.NoError(t, err) require.Len(t, list.Items, 1) require.Equal(t, interval2.Name, list.Items[0].Name) }) t.Run("should be empty when filter does not match", func(t *testing.T) { list, err := adminClient.List(ctx, v1.ListOptions{ FieldSelector: fmt.Sprintf("metadata.name=%s", "unknown"), }) require.NoError(t, err) require.Empty(t, list.Items) }) } func TestIntegrationTimeIntervalReferentialIntegrity(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } ctx := context.Background() helper := getTestHelper(t) env := helper.GetEnv() ac := acimpl.ProvideAccessControl(env.FeatureToggles) db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest())) require.NoError(t, err) orgID := helper.Org1.Admin.Identity.GetOrgID() cliCfg := helper.Org1.Admin.NewRestConfig() legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password) // Prepare environment and create notification policy and rule that use time interval alertmanagerRaw, err := testData.ReadFile(path.Join("test-data", "notification-settings.json")) require.NoError(t, err) var amConfig definitions.PostableUserConfig require.NoError(t, json.Unmarshal(alertmanagerRaw, &amConfig)) success, err := legacyCli.PostConfiguration(t, amConfig) require.Truef(t, success, "Failed to post Alertmanager configuration: %s", err) postGroupRaw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1.json")) require.NoError(t, err) var ruleGroup definitions.PostableRuleGroupConfig require.NoError(t, json.Unmarshal(postGroupRaw, &ruleGroup)) folderUID := "test-folder" legacyCli.CreateFolder(t, folderUID, "TEST") _, status, data := legacyCli.PostRulesGroupWithStatus(t, folderUID, &ruleGroup, false) require.Equalf(t, http.StatusAccepted, status, "Failed to post Rule: %s", data) currentRoute := legacyCli.GetRoute(t) currentRuleGroup, status := legacyCli.GetRulesGroup(t, folderUID, ruleGroup.Name) require.Equal(t, http.StatusAccepted, status) adminClient := newClient(t, helper.Org1.Admin) intervals, err := adminClient.List(ctx, v1.ListOptions{}) require.NoError(t, err) require.Len(t, intervals.Items, 2) intervalIdx := slices.IndexFunc(intervals.Items, func(interval v0alpha1.TimeInterval) bool { return interval.Spec.Name == "test-interval" }) interval := intervals.Items[intervalIdx] replace := func(input []string, oldName, newName string) []string { result := make([]string, 0, len(input)) for _, s := range input { if s == oldName { result = append(result, newName) continue } result = append(result, s) } return result } t.Run("Update", func(t *testing.T) { t.Run("should rename all references if name changes", func(t *testing.T) { renamed := interval.Copy().(*v0alpha1.TimeInterval) renamed.Spec.Name += "-new" actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{}) require.NoError(t, err) updatedRuleGroup, status := legacyCli.GetRulesGroup(t, folderUID, ruleGroup.Name) require.Equal(t, http.StatusAccepted, status) for idx, rule := range updatedRuleGroup.Rules { expectedTimeIntervals := currentRuleGroup.Rules[idx].GrafanaManagedAlert.NotificationSettings.MuteTimeIntervals expectedTimeIntervals = replace(expectedTimeIntervals, interval.Spec.Name, actual.Spec.Name) assert.Equalf(t, expectedTimeIntervals, rule.GrafanaManagedAlert.NotificationSettings.MuteTimeIntervals, "time interval in rules should have been renamed but it did not") } updatedRoute := legacyCli.GetRoute(t) for idx, route := range updatedRoute.Routes { expectedTimeIntervals := replace(currentRoute.Routes[idx].MuteTimeIntervals, interval.Spec.Name, actual.Spec.Name) assert.Equalf(t, expectedTimeIntervals, route.MuteTimeIntervals, "time interval in routes should have been renamed but it did not") } interval = *actual }) t.Run("should fail if at least one resource is provisioned", func(t *testing.T) { require.NoError(t, err) renamed := interval.Copy().(*v0alpha1.TimeInterval) renamed.Spec.Name += util.GenerateShortUID() t.Run("provisioned route", func(t *testing.T) { require.NoError(t, db.SetProvenance(ctx, ¤tRoute, orgID, "API")) t.Cleanup(func() { require.NoError(t, db.DeleteProvenance(ctx, ¤tRoute, orgID)) }) actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{}) require.Errorf(t, err, "Expected error but got successful result: %v", actual) require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err) }) t.Run("provisioned rules", func(t *testing.T) { ruleUid := currentRuleGroup.Rules[0].GrafanaManagedAlert.UID resource := &ngmodels.AlertRule{UID: ruleUid} require.NoError(t, db.SetProvenance(ctx, resource, orgID, "API")) t.Cleanup(func() { require.NoError(t, db.DeleteProvenance(ctx, resource, orgID)) }) actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{}) require.Errorf(t, err, "Expected error but got successful result: %v", actual) require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err) }) }) }) t.Run("Delete", func(t *testing.T) { t.Run("should fail to delete if time interval is used in rule and routes", func(t *testing.T) { err := adminClient.Delete(ctx, interval.Name, v1.DeleteOptions{}) require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err) }) t.Run("should fail to delete if time interval is used in only rule", func(t *testing.T) { route := legacyCli.GetRoute(t) route.Routes[0].MuteTimeIntervals = nil legacyCli.UpdateRoute(t, route, true) err = adminClient.Delete(ctx, interval.Name, v1.DeleteOptions{}) require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err) }) }) } func TestIntegrationTimeIntervalValidation(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } ctx := context.Background() helper := getTestHelper(t) adminClient := newClient(t, helper.Org1.Admin) testCases := []struct { name string interval v0alpha1.Spec }{ { name: "missing name", interval: v0alpha1.Spec{ Name: "", TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(1), }, }, { name: "invalid interval", interval: v0alpha1.Spec{ Name: "test", TimeIntervals: []v0alpha1.Interval{ { DaysOfMonth: []string{"1-31"}, }, }, }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { i := &v0alpha1.TimeInterval{ ObjectMeta: v1.ObjectMeta{ Namespace: "default", }, Spec: tc.interval, } _, err := adminClient.Create(ctx, i, v1.CreateOptions{}) require.Error(t, err) require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest, got: %s", err) }) } } func newClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1.TimeInterval, v0alpha1.TimeIntervalList] { t.Helper() client, err := dynamic.NewForConfig(user.NewRestConfig()) require.NoError(t, err) return &apis.TypedClient[v0alpha1.TimeInterval, v0alpha1.TimeIntervalList]{ Client: client.Resource( schema.GroupVersionResource{ Group: v0alpha1.Kind().Group(), Version: v0alpha1.Kind().Version(), Resource: v0alpha1.Kind().Plural(), }).Namespace("default"), } }