package api import ( "fmt" "path" "strconv" "testing" "time" "github.com/google/uuid" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" //nolint:staticcheck "golang.org/x/exp/rand" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" . "github.com/grafana/grafana/pkg/services/ngalert/api/compat" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" . "github.com/grafana/grafana/pkg/services/ngalert/api/validation" ) var allNoData = []apimodels.NoDataState{ apimodels.OK, apimodels.NoData, apimodels.Alerting, } var allExecError = []apimodels.ExecutionErrorState{ apimodels.ErrorErrState, apimodels.AlertingErrState, } func config(t *testing.T) *setting.UnifiedAlertingSettings { t.Helper() baseInterval := time.Duration(rand.Intn(99)+1) * time.Second result := &setting.UnifiedAlertingSettings{ BaseInterval: baseInterval, DefaultRuleEvaluationInterval: baseInterval * time.Duration(rand.Intn(9)+1), } t.Logf("Config Base interval is [%v]", result.BaseInterval) return result } func makeLimits(cfg *setting.UnifiedAlertingSettings) RuleLimits { baseToggles := featuremgmt.WithFeatures() return RuleLimitsFromConfig(cfg, baseToggles) } func validRule() apimodels.PostableExtendedRuleNode { forDuration := model.Duration(rand.Int63n(1000)) keepFiringForDuration := model.Duration(rand.Int63n(1000)) uid := util.GenerateShortUID() return apimodels.PostableExtendedRuleNode{ ApiRuleNode: &apimodels.ApiRuleNode{ For: &forDuration, KeepFiringFor: &keepFiringForDuration, Labels: map[string]string{ "test-label": "data", }, Annotations: map[string]string{ "test-annotation": "data", }, }, GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ Title: fmt.Sprintf("TEST-ALERT-%s", uid), Condition: "A", Data: []apimodels.AlertQuery{ { RefID: "A", QueryType: "TEST", RelativeTimeRange: apimodels.RelativeTimeRange{ From: 10, To: 0, }, DatasourceUID: "DATASOURCE_TEST", Model: nil, }, }, UID: uid, NoDataState: allNoData[rand.Intn(len(allNoData))], ExecErrState: allExecError[rand.Intn(len(allExecError))], }, } } func validGroup(cfg *setting.UnifiedAlertingSettings, rules ...apimodels.PostableExtendedRuleNode) apimodels.PostableRuleGroupConfig { return apimodels.PostableRuleGroupConfig{ Name: "TEST-ALERTS-" + util.GenerateShortUID(), Interval: model.Duration(cfg.BaseInterval * time.Duration(rand.Int63n(10))), Rules: rules, } } func randFolder() *folder.Folder { title := "TEST-FOLDER-" + util.GenerateShortUID() return &folder.Folder{ UID: util.GenerateShortUID(), Title: title, // URL: "", // Version: 0, Created: time.Time{}, Updated: time.Time{}, // UpdatedBy: 0, // CreatedBy: 0, // HasACL: false, ParentUID: uuid.NewString(), Fullpath: path.Join("parent-folder", title), } } func TestValidateCondition(t *testing.T) { testcases := []struct { name string condition string data []apimodels.AlertQuery errorMsg string }{ { name: "error when condition is empty", condition: "", data: []apimodels.AlertQuery{}, errorMsg: "condition cannot be empty", }, { name: "error when data is empty", condition: "A", data: []apimodels.AlertQuery{}, errorMsg: "no queries or expressions are found", }, { name: "error when condition does not exist", condition: "A", data: []apimodels.AlertQuery{ { RefID: "B", }, { RefID: "C", }, }, errorMsg: "condition A does not exist, must be one of [B,C]", }, { name: "error when duplicated refId", condition: "A", data: []apimodels.AlertQuery{ { RefID: "A", }, { RefID: "A", }, }, errorMsg: "refID 'A' is already used by query/expression at index 0", }, { name: "error when refId is empty", condition: "A", data: []apimodels.AlertQuery{ { RefID: "", }, { RefID: "A", }, }, errorMsg: "refID is not specified for data query/expression at index 0", }, { name: "valid case", condition: "B", data: []apimodels.AlertQuery{ { RefID: "A", }, { RefID: "B", }, }, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { err := ValidateCondition(tc.condition, tc.data, false) if tc.errorMsg == "" { require.NoError(t, err) } else { require.ErrorContains(t, err, tc.errorMsg) } }) } } func TestValidateRuleGroup(t *testing.T) { orgId := rand.Int63() folder := randFolder() rules := make([]apimodels.PostableExtendedRuleNode, 0, rand.Intn(4)+1) for i := 0; i < cap(rules); i++ { rules = append(rules, validRule()) } cfg := config(t) limits := makeLimits(cfg) t.Run("should validate struct and rules", func(t *testing.T) { g := validGroup(cfg, rules...) alerts, err := ValidateRuleGroup(&g, orgId, folder.UID, limits) require.NoError(t, err) require.Len(t, alerts, len(rules)) }) t.Run("should default to default interval from config if group interval is 0", func(t *testing.T) { g := validGroup(cfg, rules...) g.Interval = 0 alerts, err := ValidateRuleGroup(&g, orgId, folder.UID, limits) require.NoError(t, err) for _, alert := range alerts { require.Equal(t, int64(cfg.DefaultRuleEvaluationInterval.Seconds()), alert.IntervalSeconds) require.False(t, alert.HasPause) } }) t.Run("should show the payload has isPaused field", func(t *testing.T) { for _, rule := range rules { isPaused := true rule.GrafanaManagedAlert.IsPaused = &isPaused isPaused = !(isPaused) } g := validGroup(cfg, rules...) alerts, err := ValidateRuleGroup(&g, orgId, folder.UID, limits) require.NoError(t, err) for _, alert := range alerts { require.True(t, alert.HasPause) } }) } func TestValidateRuleGroupFailures(t *testing.T) { orgId := rand.Int63() folder := randFolder() cfg := config(t) limits := makeLimits(cfg) testCases := []struct { name string group func() *apimodels.PostableRuleGroupConfig assert func(t *testing.T, apiModel *apimodels.PostableRuleGroupConfig, err error) }{ { name: "fail if title is empty", group: func() *apimodels.PostableRuleGroupConfig { g := validGroup(cfg) g.Name = "" return &g }, }, { name: "fail if title is too long", group: func() *apimodels.PostableRuleGroupConfig { g := validGroup(cfg) for len(g.Name) < store.AlertRuleMaxRuleGroupNameLength { g.Name += g.Name } return &g }, }, { name: "fail if interval is negative", group: func() *apimodels.PostableRuleGroupConfig { g := validGroup(cfg) g.Interval = model.Duration(-(rand.Int63n(1000) + 1)) return &g }, }, { name: "fail if interval is not aligned with base interval", group: func() *apimodels.PostableRuleGroupConfig { g := validGroup(cfg) g.Interval = model.Duration(cfg.BaseInterval + time.Duration(rand.Intn(10)+1)*time.Second) return &g }, }, { name: "fail if two rules have same UID", group: func() *apimodels.PostableRuleGroupConfig { r1 := validRule() r2 := validRule() uid := util.GenerateShortUID() r1.GrafanaManagedAlert.UID = uid r2.GrafanaManagedAlert.UID = uid g := validGroup(cfg, r1, r2) return &g }, assert: func(t *testing.T, apiModel *apimodels.PostableRuleGroupConfig, err error) { require.Contains(t, err.Error(), apiModel.Rules[0].GrafanaManagedAlert.UID) }, }, { name: "fail with 4xx if rule contains only panelID", group: func() *apimodels.PostableRuleGroupConfig { r1 := validRule() panelId := int64(42) r1.Annotations = map[string]string{ models.PanelIDAnnotation: strconv.FormatInt(panelId, 10), } g := validGroup(cfg, r1) return &g }, assert: func(t *testing.T, apiModel *apimodels.PostableRuleGroupConfig, err error) { require.ErrorIs(t, err, models.ErrAlertRuleFailedValidation) }, }, { name: "fail with 4xx if rule contains only dashboardUID", group: func() *apimodels.PostableRuleGroupConfig { r1 := validRule() dashboardUid := "oinwerfgiuac" r1.Annotations = map[string]string{ models.DashboardUIDAnnotation: dashboardUid, } g := validGroup(cfg, r1) return &g }, assert: func(t *testing.T, apiModel *apimodels.PostableRuleGroupConfig, err error) { require.ErrorIs(t, err, models.ErrAlertRuleFailedValidation) }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { g := testCase.group() _, err := ValidateRuleGroup(g, orgId, folder.UID, limits) require.Error(t, err) if testCase.assert != nil { testCase.assert(t, g, err) } }) } } func TestValidateRuleNode_NoUID(t *testing.T) { orgId := rand.Int63() folder := randFolder() name := util.GenerateShortUID() var cfg = config(t) limits := makeLimits(cfg) interval := cfg.BaseInterval * time.Duration(rand.Int63n(10)+1) testCases := []struct { name string rule func() *apimodels.PostableExtendedRuleNode limits *RuleLimits assert func(t *testing.T, model *apimodels.PostableExtendedRuleNode, rule *models.AlertRule) }{ { name: "coverts api model to AlertRule", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() return &r }, assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) { require.Equal(t, int64(0), alert.ID) require.Equal(t, orgId, alert.OrgID) require.Equal(t, api.GrafanaManagedAlert.Title, alert.Title) require.Equal(t, api.GrafanaManagedAlert.Condition, alert.Condition) require.Equal(t, AlertQueriesFromApiAlertQueries(api.GrafanaManagedAlert.Data), alert.Data) require.Equal(t, time.Time{}, alert.Updated) require.Equal(t, int64(interval.Seconds()), alert.IntervalSeconds) require.Equal(t, int64(0), alert.Version) require.Equal(t, api.GrafanaManagedAlert.UID, alert.UID) require.Equal(t, folder.UID, alert.NamespaceUID) require.Nil(t, alert.DashboardUID) require.Nil(t, alert.PanelID) require.Equal(t, name, alert.RuleGroup) require.Equal(t, models.NoDataState(api.GrafanaManagedAlert.NoDataState), alert.NoDataState) require.Equal(t, models.ExecutionErrorState(api.GrafanaManagedAlert.ExecErrState), alert.ExecErrState) require.Equal(t, time.Duration(*api.For), alert.For) require.Equal(t, time.Duration(*api.KeepFiringFor), alert.KeepFiringFor) require.Equal(t, api.Annotations, alert.Annotations) require.Equal(t, api.Labels, alert.Labels) require.Nil(t, alert.Record) }, }, { name: "coverts api without ApiRuleNode", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.ApiRuleNode = nil return &r }, assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) { require.Equal(t, time.Duration(0), alert.For) require.Equal(t, time.Duration(0), alert.KeepFiringFor) require.Nil(t, alert.Annotations) require.Nil(t, alert.Labels) }, }, { name: "defaults to NoData if NoDataState is empty", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.NoDataState = "" return &r }, assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) { require.Equal(t, models.NoData, alert.NoDataState) }, }, { name: "defaults to Alerting if ExecErrState is empty", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.ExecErrState = "" return &r }, assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) { require.Equal(t, models.AlertingErrState, alert.ExecErrState) }, }, { name: "extracts Dashboard UID and Panel Id from annotations", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.Annotations = map[string]string{ models.DashboardUIDAnnotation: util.GenerateShortUID(), models.PanelIDAnnotation: strconv.Itoa(rand.Int()), } return &r }, assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) { require.Equal(t, api.Annotations[models.DashboardUIDAnnotation], *alert.DashboardUID) panelId, err := strconv.Atoi(api.Annotations[models.PanelIDAnnotation]) require.NoError(t, err) require.Equal(t, int64(panelId), *alert.PanelID) }, }, { name: "accepts and converts recording rule", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.Record = &apimodels.Record{Metric: "some_metric", From: "A"} r.GrafanaManagedAlert.Condition = "" r.GrafanaManagedAlert.NoDataState = "" r.GrafanaManagedAlert.ExecErrState = "" r.GrafanaManagedAlert.NotificationSettings = nil r.For = nil r.KeepFiringFor = nil return &r }, assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) { // Shared fields require.Equal(t, int64(0), alert.ID) require.Equal(t, orgId, alert.OrgID) require.Equal(t, api.GrafanaManagedAlert.Title, alert.Title) require.Equal(t, AlertQueriesFromApiAlertQueries(api.GrafanaManagedAlert.Data), alert.Data) require.Equal(t, time.Time{}, alert.Updated) require.Equal(t, int64(interval.Seconds()), alert.IntervalSeconds) require.Equal(t, int64(0), alert.Version) require.Equal(t, api.GrafanaManagedAlert.UID, alert.UID) require.Equal(t, folder.UID, alert.NamespaceUID) require.Nil(t, alert.DashboardUID) require.Nil(t, alert.PanelID) require.Equal(t, name, alert.RuleGroup) require.Equal(t, api.Annotations, alert.Annotations) require.Equal(t, api.Labels, alert.Labels) // Alerting fields require.Empty(t, alert.Condition) require.Empty(t, alert.NoDataState) require.Empty(t, alert.ExecErrState) require.Nil(t, alert.NotificationSettings) require.Zero(t, alert.For) require.Zero(t, alert.KeepFiringFor) // Recording fields require.Equal(t, api.GrafanaManagedAlert.Record.From, alert.Record.From) require.Equal(t, api.GrafanaManagedAlert.Record.Metric, alert.Record.Metric) }, }, { name: "recording rules ignore fields that only make sense for Alerting rules", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.Record = &apimodels.Record{Metric: "some_metric", From: "A"} r.GrafanaManagedAlert.Condition = "A" r.GrafanaManagedAlert.NoDataState = apimodels.OK r.GrafanaManagedAlert.ExecErrState = apimodels.AlertingErrState r.GrafanaManagedAlert.NotificationSettings = &apimodels.AlertRuleNotificationSettings{} r.For = func() *model.Duration { five := model.Duration(time.Second * 5); return &five }() r.KeepFiringFor = func() *model.Duration { five := model.Duration(time.Second * 5); return &five }() return &r }, assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) { require.Empty(t, alert.Condition) require.Empty(t, alert.NoDataState) require.Empty(t, alert.ExecErrState) require.Nil(t, alert.NotificationSettings) require.Zero(t, alert.For) require.Zero(t, alert.KeepFiringFor) }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { r := testCase.rule() r.GrafanaManagedAlert.UID = "" lim := limits if testCase.limits != nil { lim = *testCase.limits } alert, err := ValidateRuleNode(r, name, interval, orgId, folder.UID, lim) require.NoError(t, err) testCase.assert(t, r, alert) }) } t.Run("accepts empty group name", func(t *testing.T) { r := validRule() alert, err := ValidateRuleNode(&r, "", interval, orgId, folder.UID, limits) require.NoError(t, err) require.Equal(t, "", alert.RuleGroup) }) } func TestValidateRuleNodeFailures_NoUID(t *testing.T) { orgId := rand.Int63() folder := randFolder() cfg := config(t) limits := makeLimits(cfg) testCases := []struct { name string interval *time.Duration rule func() *apimodels.PostableExtendedRuleNode limits *RuleLimits expErr string assert func(t *testing.T, model *apimodels.PostableExtendedRuleNode, err error) allowedIfNoUId bool }{ { name: "fail if GrafanaManagedAlert is not specified", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert = nil return &r }, }, { name: "fail if title is empty", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.Title = "" return &r }, }, { name: "fail if title is too long", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() for len(r.GrafanaManagedAlert.Title) < store.AlertRuleMaxTitleLength { r.GrafanaManagedAlert.Title += r.GrafanaManagedAlert.Title } return &r }, }, { name: "fail if NoDataState is not known", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.NoDataState = apimodels.NoDataState(util.GenerateShortUID()) return &r }, }, { name: "fail if ExecErrState is not known", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.ExecErrState = apimodels.ExecutionErrorState(util.GenerateShortUID()) return &r }, }, { name: "fail if there are not data (nil)", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.Data = nil return &r }, }, { name: "fail if there are not data (empty)", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.Data = make([]apimodels.AlertQuery, 0, 1) return &r }, }, { name: "fail if Dashboard UID is specified but not Panel ID", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.Annotations = map[string]string{ models.DashboardUIDAnnotation: util.GenerateShortUID(), } return &r }, }, { name: "fail if Dashboard UID is specified and Panel ID is NaN", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.Annotations = map[string]string{ models.DashboardUIDAnnotation: util.GenerateShortUID(), models.PanelIDAnnotation: util.GenerateShortUID(), } return &r }, }, { name: "fail if PanelID is specified but not Dashboard UID ", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.Annotations = map[string]string{ models.PanelIDAnnotation: "0", } return &r }, }, { name: "fail if Condition is empty", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.Condition = "" return &r }, }, { name: "fail if Data is empty", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.Data = nil return &r }, }, { name: "fail if Condition does not exist", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.Condition = uuid.NewString() return &r }, }, { name: "fail if Data has duplicate ref ID", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.Data = append(r.GrafanaManagedAlert.Data, r.GrafanaManagedAlert.Data...) return &r }, }, { name: "rejects recording rule with invalid metric name", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.Record = &apimodels.Record{Metric: "", From: "A"} r.GrafanaManagedAlert.Condition = "" r.GrafanaManagedAlert.NoDataState = "" r.GrafanaManagedAlert.ExecErrState = "" r.GrafanaManagedAlert.NotificationSettings = nil r.For = nil return &r }, expErr: "must be a valid Prometheus metric name", }, { name: "rejects recording rule with empty from", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.Record = &apimodels.Record{Metric: "my_metric", From: ""} r.GrafanaManagedAlert.Condition = "" r.GrafanaManagedAlert.NoDataState = "" r.GrafanaManagedAlert.ExecErrState = "" r.GrafanaManagedAlert.NotificationSettings = nil r.For = nil return &r }, expErr: "cannot be empty", }, { name: "rejects recording rule with from not matching", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.Record = &apimodels.Record{Metric: "my_metric", From: "NOTEXIST"} r.GrafanaManagedAlert.Condition = "" r.GrafanaManagedAlert.NoDataState = "" r.GrafanaManagedAlert.ExecErrState = "" r.GrafanaManagedAlert.NotificationSettings = nil r.For = nil return &r }, expErr: "NOTEXIST does not exist", }, { name: "fail if keep_firing_for is negative", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() keepFiringFor := model.Duration(-1) r.KeepFiringFor = &keepFiringFor return &r }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { r := testCase.rule() if r.GrafanaManagedAlert != nil { r.GrafanaManagedAlert.UID = "" } interval := cfg.BaseInterval if testCase.interval != nil { interval = *testCase.interval } lim := limits if testCase.limits != nil { lim = *testCase.limits } _, err := ValidateRuleNode(r, "", interval, orgId, folder.UID, lim) require.Error(t, err) if testCase.expErr != "" { require.ErrorContains(t, err, testCase.expErr) } if testCase.assert != nil { testCase.assert(t, r, err) } }) } } func TestValidateRuleNode_UID(t *testing.T) { orgId := rand.Int63() folder := randFolder() name := util.GenerateShortUID() var cfg = config(t) limits := makeLimits(cfg) interval := cfg.BaseInterval * time.Duration(rand.Int63n(10)+1) testCases := []struct { name string rule func() *apimodels.PostableExtendedRuleNode assert func(t *testing.T, model *apimodels.PostableExtendedRuleNode, rule *models.AlertRule) }{ { name: "use empty Title", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.Title = "" return &r }, assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) { require.Equal(t, "", alert.Title) }, }, { name: "use empty NoData if NoDataState is empty", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.NoDataState = "" return &r }, assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) { require.Equal(t, models.NoDataState(""), alert.NoDataState) }, }, { name: "use empty Alerting if ExecErrState is empty", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.ExecErrState = "" return &r }, assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) { require.Equal(t, models.ExecutionErrorState(""), alert.ExecErrState) }, }, { name: "use empty Condition and Data if they are empty", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.Condition = "" r.GrafanaManagedAlert.Data = nil if rand.Int63()%2 == 0 { r.GrafanaManagedAlert.Data = make([]apimodels.AlertQuery, 0) } return &r }, assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) { require.Equal(t, "", alert.Condition) require.Len(t, alert.Data, 0) }, }, { name: "extracts Dashboard UID and Panel Id from annotations", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.Annotations = map[string]string{ models.DashboardUIDAnnotation: util.GenerateShortUID(), models.PanelIDAnnotation: strconv.Itoa(rand.Int()), } return &r }, assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) { require.Equal(t, api.Annotations[models.DashboardUIDAnnotation], *alert.DashboardUID) panelId, err := strconv.Atoi(api.Annotations[models.PanelIDAnnotation]) require.NoError(t, err) require.Equal(t, int64(panelId), *alert.PanelID) }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { r := testCase.rule() alert, err := ValidateRuleNode(r, name, interval, orgId, folder.UID, limits) require.NoError(t, err) testCase.assert(t, r, alert) }) } t.Run("accepts empty group name", func(t *testing.T) { r := validRule() alert, err := ValidateRuleNode(&r, "", interval, orgId, folder.UID, limits) require.NoError(t, err) require.Equal(t, "", alert.RuleGroup) }) } func TestValidateRuleNodeFailures_UID(t *testing.T) { orgId := rand.Int63() folder := randFolder() cfg := config(t) limits := makeLimits(cfg) testCases := []struct { name string interval *time.Duration rule func() *apimodels.PostableExtendedRuleNode assert func(t *testing.T, model *apimodels.PostableExtendedRuleNode, err error) }{ { name: "fail if GrafanaManagedAlert is not specified", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert = nil return &r }, }, { name: "fail if title is too long", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() for len(r.GrafanaManagedAlert.Title) < store.AlertRuleMaxTitleLength { r.GrafanaManagedAlert.Title += r.GrafanaManagedAlert.Title } return &r }, }, { name: "fail if there are not data (nil) but condition is set", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.Data = nil r.GrafanaManagedAlert.Condition = "A" return &r }, }, { name: "fail if there are not data (empty) but condition is set", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.GrafanaManagedAlert.Data = make([]apimodels.AlertQuery, 0, 1) r.GrafanaManagedAlert.Condition = "A" return &r }, }, { name: "fail if Dashboard UID is specified but not Panel ID", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.Annotations = map[string]string{ models.DashboardUIDAnnotation: util.GenerateShortUID(), } return &r }, }, { name: "fail if Dashboard UID is specified and Panel ID is NaN", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.Annotations = map[string]string{ models.DashboardUIDAnnotation: util.GenerateShortUID(), models.PanelIDAnnotation: util.GenerateShortUID(), } return &r }, }, { name: "fail if PanelID is specified but not Dashboard UID ", rule: func() *apimodels.PostableExtendedRuleNode { r := validRule() r.Annotations = map[string]string{ models.PanelIDAnnotation: "0", } return &r }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { r := testCase.rule() interval := cfg.BaseInterval if testCase.interval != nil { interval = *testCase.interval } _, err := ValidateRuleNode(r, "", interval, orgId, folder.UID, limits) require.Error(t, err) if testCase.assert != nil { testCase.assert(t, r, err) } }) } } func TestValidateRuleNodeIntervalFailures(t *testing.T) { cfg := config(t) limits := makeLimits(cfg) testCases := []struct { name string interval time.Duration }{ { name: "fail if interval is negative", interval: -time.Duration(rand.Int63n(10)+1) * time.Second, }, { name: "fail if interval is 0", interval: 0, }, { name: "fail if interval is not multiple of base interval", interval: cfg.BaseInterval + time.Duration(rand.Int63n(int64(cfg.BaseInterval.Seconds())-2)+1)*time.Second, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { r := validRule() _, err := ValidateRuleNode(&r, util.GenerateShortUID(), testCase.interval, rand.Int63(), randFolder().UID, limits) require.Error(t, err) }) } } func TestValidateRuleNodeNotificationSettings(t *testing.T) { cfg := config(t) limits := makeLimits(cfg) validNotificationSettings := models.NotificationSettingsGen(models.NSMuts.WithGroupBy(model.AlertNameLabel, models.FolderTitleLabel)) testCases := []struct { name string notificationSettings models.NotificationSettings expErrorContains string }{ { name: "valid notification settings", notificationSettings: validNotificationSettings(), }, { name: "missing receiver is invalid", notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithReceiver("")), expErrorContains: "receiver", }, { name: "group by empty is valid", notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupBy()), }, { name: "group by ... is valid", notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupBy("...")), }, { name: "group by with alert name and folder name labels is valid", notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupBy(model.AlertNameLabel, models.FolderTitleLabel)), }, { name: "group by missing alert name label is valid", notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupBy(models.FolderTitleLabel)), }, { name: "group by missing folder name label is valid", notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupBy(model.AlertNameLabel)), }, { name: "group wait empty is valid", notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupWait(nil)), }, { name: "group wait positive is valid", notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupWait(util.Pointer(1*time.Second))), }, { name: "group wait negative is invalid", notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupWait(util.Pointer(-1*time.Second))), expErrorContains: "group wait", }, { name: "group interval empty is valid", notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupInterval(nil)), }, { name: "group interval positive is valid", notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupInterval(util.Pointer(1*time.Second))), }, { name: "group interval negative is invalid", notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupInterval(util.Pointer(-1*time.Second))), expErrorContains: "group interval", }, { name: "repeat interval empty is valid", notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithRepeatInterval(nil)), }, { name: "repeat interval positive is valid", notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithRepeatInterval(util.Pointer(1*time.Second))), }, { name: "repeat interval negative is invalid", notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithRepeatInterval(util.Pointer(-1*time.Second))), expErrorContains: "repeat interval", }, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { r := validRule() r.GrafanaManagedAlert.NotificationSettings = AlertRuleNotificationSettingsFromNotificationSettings([]models.NotificationSettings{tt.notificationSettings}) _, err := ValidateRuleNode(&r, util.GenerateShortUID(), cfg.BaseInterval*time.Duration(rand.Int63n(10)+1), rand.Int63(), randFolder().UID, limits) if tt.expErrorContains != "" { require.Error(t, err) require.ErrorContains(t, err, tt.expErrorContains) } else { require.NoError(t, err) } }) } } func TestValidateRuleNodeEditorSettings(t *testing.T) { cfg := config(t) limits := makeLimits(cfg) editorSettings := models.EditorSettings{ SimplifiedQueryAndExpressionsSection: true, SimplifiedNotificationsSection: true, } testCases := []struct { name string editorSettings models.EditorSettings }{ { name: "valid editor settings", editorSettings: editorSettings, }, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { r := validRule() r.GrafanaManagedAlert.Metadata = AlertRuleMetadataFromModelMetadata(models.AlertRuleMetadata{EditorSettings: tt.editorSettings}) newRule, err := ValidateRuleNode(&r, util.GenerateShortUID(), cfg.BaseInterval*time.Duration(rand.Int63n(10)+1), rand.Int63(), randFolder().UID, limits) require.NoError(t, err) require.Equal(t, tt.editorSettings, newRule.Metadata.EditorSettings) }) } } func TestValidateRuleNodeReservedLabels(t *testing.T) { cfg := config(t) limits := makeLimits(cfg) for label := range models.LabelsUserCannotSpecify { t.Run(label, func(t *testing.T) { r := validRule() r.Labels = map[string]string{ label: "true", } _, err := ValidateRuleNode(&r, util.GenerateShortUID(), cfg.BaseInterval*time.Duration(rand.Int63n(10)+1), rand.Int63(), randFolder().UID, limits) require.Error(t, err) require.ErrorContains(t, err, label) }) } }