mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
	
	
		
			717 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			Go
		
	
	
	
		
		
			
		
	
	
			717 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			Go
		
	
	
	
|  | package api | ||
|  | 
 | ||
|  | import ( | ||
|  | 	"errors" | ||
|  | 	"fmt" | ||
|  | 	"strconv" | ||
|  | 	"testing" | ||
|  | 	"time" | ||
|  | 
 | ||
|  | 	"github.com/prometheus/common/model" | ||
|  | 	"github.com/stretchr/testify/require" | ||
|  | 	"golang.org/x/exp/rand" | ||
|  | 
 | ||
|  | 	models2 "github.com/grafana/grafana/pkg/models" | ||
|  | 	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" | ||
|  | ) | ||
|  | 
 | ||
|  | 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 validRule() apimodels.PostableExtendedRuleNode { | ||
|  | 	return apimodels.PostableExtendedRuleNode{ | ||
|  | 		ApiRuleNode: &apimodels.ApiRuleNode{ | ||
|  | 			For: model.Duration(rand.Int63n(1000)), | ||
|  | 			Labels: map[string]string{ | ||
|  | 				"test-label": "data", | ||
|  | 			}, | ||
|  | 			Annotations: map[string]string{ | ||
|  | 				"test-annotation": "data", | ||
|  | 			}, | ||
|  | 		}, | ||
|  | 		GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ | ||
|  | 			Title:     fmt.Sprintf("TEST-ALERT-%d", rand.Int63()), | ||
|  | 			Condition: "A", | ||
|  | 			Data: []models.AlertQuery{ | ||
|  | 				{ | ||
|  | 					RefID:     "A", | ||
|  | 					QueryType: "TEST", | ||
|  | 					RelativeTimeRange: models.RelativeTimeRange{ | ||
|  | 						From: 10, | ||
|  | 						To:   0, | ||
|  | 					}, | ||
|  | 					DatasourceUID: "DATASOURCE_TEST", | ||
|  | 					Model:         nil, | ||
|  | 				}, | ||
|  | 			}, | ||
|  | 			UID:          util.GenerateShortUID(), | ||
|  | 			NoDataState:  allNoData[rand.Intn(len(allNoData)-1)], | ||
|  | 			ExecErrState: allExecError[rand.Intn(len(allExecError)-1)], | ||
|  | 		}, | ||
|  | 	} | ||
|  | } | ||
|  | 
 | ||
|  | 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() *models2.Folder { | ||
|  | 	return &models2.Folder{ | ||
|  | 		Id:        rand.Int63(), | ||
|  | 		Uid:       util.GenerateShortUID(), | ||
|  | 		Title:     "TEST-FOLDER-" + util.GenerateShortUID(), | ||
|  | 		Url:       "", | ||
|  | 		Version:   0, | ||
|  | 		Created:   time.Time{}, | ||
|  | 		Updated:   time.Time{}, | ||
|  | 		UpdatedBy: 0, | ||
|  | 		CreatedBy: 0, | ||
|  | 		HasAcl:    false, | ||
|  | 	} | ||
|  | } | ||
|  | 
 | ||
|  | 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) | ||
|  | 
 | ||
|  | 	t.Run("should validate struct and rules", func(t *testing.T) { | ||
|  | 		g := validGroup(cfg, rules...) | ||
|  | 		conditionValidations := 0 | ||
|  | 		alerts, err := validateRuleGroup(&g, orgId, folder, func(condition models.Condition) error { | ||
|  | 			conditionValidations++ | ||
|  | 			return nil | ||
|  | 		}, cfg) | ||
|  | 		require.NoError(t, err) | ||
|  | 		require.Len(t, alerts, len(rules)) | ||
|  | 		require.Equal(t, len(rules), conditionValidations) | ||
|  | 	}) | ||
|  | 	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, func(condition models.Condition) error { | ||
|  | 			return nil | ||
|  | 		}, cfg) | ||
|  | 		require.NoError(t, err) | ||
|  | 		for _, alert := range alerts { | ||
|  | 			require.Equal(t, int64(cfg.DefaultRuleEvaluationInterval.Seconds()), alert.IntervalSeconds) | ||
|  | 		} | ||
|  | 	}) | ||
|  | } | ||
|  | 
 | ||
|  | func TestValidateRuleGroupFailures(t *testing.T) { | ||
|  | 	orgId := rand.Int63() | ||
|  | 	folder := randFolder() | ||
|  | 	cfg := config(t) | ||
|  | 
 | ||
|  | 	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) | ||
|  | 			}, | ||
|  | 		}, | ||
|  | 	} | ||
|  | 
 | ||
|  | 	for _, testCase := range testCases { | ||
|  | 		t.Run(testCase.name, func(t *testing.T) { | ||
|  | 			g := testCase.group() | ||
|  | 			_, err := validateRuleGroup(g, orgId, folder, func(condition models.Condition) error { | ||
|  | 				return nil | ||
|  | 			}, cfg) | ||
|  | 			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) | ||
|  | 	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: "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, 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.ApiRuleNode.For), alert.For) | ||
|  | 				require.Equal(t, api.ApiRuleNode.Annotations, alert.Annotations) | ||
|  | 				require.Equal(t, api.ApiRuleNode.Labels, alert.Labels) | ||
|  | 			}, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			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.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.ApiRuleNode.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.ApiRuleNode.Annotations[models.DashboardUIDAnnotation], *alert.DashboardUID) | ||
|  | 				panelId, err := strconv.Atoi(api.ApiRuleNode.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() | ||
|  | 			r.GrafanaManagedAlert.UID = "" | ||
|  | 
 | ||
|  | 			alert, err := validateRuleNode(r, name, interval, orgId, folder, func(condition models.Condition) error { | ||
|  | 				return nil | ||
|  | 			}, cfg) | ||
|  | 			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, func(condition models.Condition) error { | ||
|  | 			return nil | ||
|  | 		}, cfg) | ||
|  | 		require.NoError(t, err) | ||
|  | 		require.Equal(t, "", alert.RuleGroup) | ||
|  | 	}) | ||
|  | } | ||
|  | 
 | ||
|  | func TestValidateRuleNodeFailures_NoUID(t *testing.T) { | ||
|  | 	orgId := rand.Int63() | ||
|  | 	folder := randFolder() | ||
|  | 	cfg := config(t) | ||
|  | 	successValidation := func(condition models.Condition) error { | ||
|  | 		return nil | ||
|  | 	} | ||
|  | 
 | ||
|  | 	testCases := []struct { | ||
|  | 		name                string | ||
|  | 		interval            *time.Duration | ||
|  | 		rule                func() *apimodels.PostableExtendedRuleNode | ||
|  | 		conditionValidation func(condition models.Condition) error | ||
|  | 		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([]models.AlertQuery, 0, 1) | ||
|  | 				return &r | ||
|  | 			}, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			name: "fail if validator function returns error", | ||
|  | 			rule: func() *apimodels.PostableExtendedRuleNode { | ||
|  | 				r := validRule() | ||
|  | 				return &r | ||
|  | 			}, | ||
|  | 			conditionValidation: func(condition models.Condition) error { | ||
|  | 				return errors.New("BAD alert condition") | ||
|  | 			}, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			name: "fail if Dashboard UID is specified but not Panel ID", | ||
|  | 			rule: func() *apimodels.PostableExtendedRuleNode { | ||
|  | 				r := validRule() | ||
|  | 				r.ApiRuleNode.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.ApiRuleNode.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.ApiRuleNode.Annotations = map[string]string{ | ||
|  | 					models.PanelIDAnnotation: "0", | ||
|  | 				} | ||
|  | 				return &r | ||
|  | 			}, | ||
|  | 		}, | ||
|  | 	} | ||
|  | 
 | ||
|  | 	for _, testCase := range testCases { | ||
|  | 		t.Run(testCase.name, func(t *testing.T) { | ||
|  | 			r := testCase.rule() | ||
|  | 			if r.GrafanaManagedAlert != nil { | ||
|  | 				r.GrafanaManagedAlert.UID = "" | ||
|  | 			} | ||
|  | 			f := successValidation | ||
|  | 			if testCase.conditionValidation != nil { | ||
|  | 				f = testCase.conditionValidation | ||
|  | 			} | ||
|  | 
 | ||
|  | 			interval := cfg.BaseInterval | ||
|  | 			if testCase.interval != nil { | ||
|  | 				interval = *testCase.interval | ||
|  | 			} | ||
|  | 
 | ||
|  | 			_, err := validateRuleNode(r, "", interval, orgId, folder, f, cfg) | ||
|  | 			require.Error(t, err) | ||
|  | 			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) | ||
|  | 	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([]models.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.ApiRuleNode.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.ApiRuleNode.Annotations[models.DashboardUIDAnnotation], *alert.DashboardUID) | ||
|  | 				panelId, err := strconv.Atoi(api.ApiRuleNode.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, func(condition models.Condition) error { | ||
|  | 				return nil | ||
|  | 			}, cfg) | ||
|  | 			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, func(condition models.Condition) error { | ||
|  | 			return nil | ||
|  | 		}, cfg) | ||
|  | 		require.NoError(t, err) | ||
|  | 		require.Equal(t, "", alert.RuleGroup) | ||
|  | 	}) | ||
|  | } | ||
|  | 
 | ||
|  | func TestValidateRuleNodeFailures_UID(t *testing.T) { | ||
|  | 	orgId := rand.Int63() | ||
|  | 	folder := randFolder() | ||
|  | 	cfg := config(t) | ||
|  | 	successValidation := func(condition models.Condition) error { | ||
|  | 		return nil | ||
|  | 	} | ||
|  | 
 | ||
|  | 	testCases := []struct { | ||
|  | 		name                string | ||
|  | 		interval            *time.Duration | ||
|  | 		rule                func() *apimodels.PostableExtendedRuleNode | ||
|  | 		conditionValidation func(condition models.Condition) error | ||
|  | 		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([]models.AlertQuery, 0, 1) | ||
|  | 				r.GrafanaManagedAlert.Condition = "A" | ||
|  | 				return &r | ||
|  | 			}, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			name: "fail if validator function returns error", | ||
|  | 			rule: func() *apimodels.PostableExtendedRuleNode { | ||
|  | 				r := validRule() | ||
|  | 				return &r | ||
|  | 			}, | ||
|  | 			conditionValidation: func(condition models.Condition) error { | ||
|  | 				return errors.New("BAD alert condition") | ||
|  | 			}, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			name: "fail if Dashboard UID is specified but not Panel ID", | ||
|  | 			rule: func() *apimodels.PostableExtendedRuleNode { | ||
|  | 				r := validRule() | ||
|  | 				r.ApiRuleNode.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.ApiRuleNode.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.ApiRuleNode.Annotations = map[string]string{ | ||
|  | 					models.PanelIDAnnotation: "0", | ||
|  | 				} | ||
|  | 				return &r | ||
|  | 			}, | ||
|  | 		}, | ||
|  | 	} | ||
|  | 
 | ||
|  | 	for _, testCase := range testCases { | ||
|  | 		t.Run(testCase.name, func(t *testing.T) { | ||
|  | 			r := testCase.rule() | ||
|  | 			f := successValidation | ||
|  | 			if testCase.conditionValidation != nil { | ||
|  | 				f = testCase.conditionValidation | ||
|  | 			} | ||
|  | 
 | ||
|  | 			interval := cfg.BaseInterval | ||
|  | 			if testCase.interval != nil { | ||
|  | 				interval = *testCase.interval | ||
|  | 			} | ||
|  | 
 | ||
|  | 			_, err := validateRuleNode(r, "", interval, orgId, folder, f, cfg) | ||
|  | 			require.Error(t, err) | ||
|  | 			if testCase.assert != nil { | ||
|  | 				testCase.assert(t, r, err) | ||
|  | 			} | ||
|  | 		}) | ||
|  | 	} | ||
|  | } | ||
|  | 
 | ||
|  | func TestValidateRuleNodeIntervalFailures(t *testing.T) { | ||
|  | 	cfg := config(t) | ||
|  | 
 | ||
|  | 	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() | ||
|  | 			f := func(condition models.Condition) error { | ||
|  | 				return nil | ||
|  | 			} | ||
|  | 
 | ||
|  | 			_, err := validateRuleNode(&r, util.GenerateShortUID(), testCase.interval, rand.Int63(), randFolder(), f, cfg) | ||
|  | 			require.Error(t, err) | ||
|  | 		}) | ||
|  | 	} | ||
|  | } |