mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			300 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			300 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			Go
		
	
	
	
| package api
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"errors"
 | |
| 	"math/rand"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/stretchr/testify/require"
 | |
| 
 | |
| 	models2 "github.com/grafana/grafana/pkg/models"
 | |
| 	"github.com/grafana/grafana/pkg/services/ngalert/models"
 | |
| 	"github.com/grafana/grafana/pkg/services/ngalert/store"
 | |
| 	"github.com/grafana/grafana/pkg/util"
 | |
| )
 | |
| 
 | |
| func TestCalculateChanges(t *testing.T) {
 | |
| 	orgId := rand.Int63()
 | |
| 
 | |
| 	t.Run("detects alerts that need to be added", func(t *testing.T) {
 | |
| 		fakeStore := store.NewFakeRuleStore(t)
 | |
| 
 | |
| 		namespace := randFolder()
 | |
| 		groupName := util.GenerateShortUID()
 | |
| 		submitted := models.GenerateAlertRules(rand.Intn(5)+1, models.AlertRuleGen(withOrgID(orgId), simulateSubmitted, withoutUID))
 | |
| 
 | |
| 		changes, err := calculateChanges(context.Background(), fakeStore, orgId, namespace, groupName, submitted)
 | |
| 		require.NoError(t, err)
 | |
| 
 | |
| 		require.Len(t, changes.New, len(submitted))
 | |
| 		require.Empty(t, changes.Delete)
 | |
| 		require.Empty(t, changes.Update)
 | |
| 
 | |
| 	outerloop:
 | |
| 		for _, expected := range submitted {
 | |
| 			for _, rule := range changes.New {
 | |
| 				if len(expected.Diff(rule)) == 0 {
 | |
| 					continue outerloop
 | |
| 				}
 | |
| 			}
 | |
| 			require.Fail(t, "changes did not contain rule that was submitted")
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	t.Run("detects alerts that need to be deleted", func(t *testing.T) {
 | |
| 		namespace := randFolder()
 | |
| 		groupName := util.GenerateShortUID()
 | |
| 		inDatabaseMap, inDatabase := models.GenerateUniqueAlertRules(rand.Intn(5)+1, models.AlertRuleGen(withOrgID(orgId), withGroup(groupName), withNamespace(namespace)))
 | |
| 
 | |
| 		fakeStore := store.NewFakeRuleStore(t)
 | |
| 		fakeStore.PutRule(context.Background(), inDatabase...)
 | |
| 
 | |
| 		changes, err := calculateChanges(context.Background(), fakeStore, orgId, namespace, groupName, make([]*models.AlertRule, 0))
 | |
| 		require.NoError(t, err)
 | |
| 
 | |
| 		require.Empty(t, changes.New)
 | |
| 		require.Empty(t, changes.Update)
 | |
| 		require.Len(t, changes.Delete, len(inDatabaseMap))
 | |
| 		for _, toDelete := range changes.Delete {
 | |
| 			require.Contains(t, inDatabaseMap, toDelete.UID)
 | |
| 			db := inDatabaseMap[toDelete.UID]
 | |
| 			require.Equal(t, db, toDelete)
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	t.Run("should detect alerts that needs to be updated", func(t *testing.T) {
 | |
| 		namespace := randFolder()
 | |
| 		groupName := util.GenerateShortUID()
 | |
| 		inDatabaseMap, inDatabase := models.GenerateUniqueAlertRules(rand.Intn(5)+1, models.AlertRuleGen(withOrgID(orgId), withGroup(groupName), withNamespace(namespace)))
 | |
| 		submittedMap, submitted := models.GenerateUniqueAlertRules(len(inDatabase), models.AlertRuleGen(simulateSubmitted, withOrgID(orgId), withGroup(groupName), withNamespace(namespace), withUIDs(inDatabaseMap)))
 | |
| 
 | |
| 		fakeStore := store.NewFakeRuleStore(t)
 | |
| 		fakeStore.PutRule(context.Background(), inDatabase...)
 | |
| 
 | |
| 		changes, err := calculateChanges(context.Background(), fakeStore, orgId, namespace, groupName, submitted)
 | |
| 		require.NoError(t, err)
 | |
| 
 | |
| 		require.Len(t, changes.Update, len(inDatabase))
 | |
| 		for _, upsert := range changes.Update {
 | |
| 			require.NotNil(t, upsert.Existing)
 | |
| 			require.Equal(t, upsert.Existing.UID, upsert.New.UID)
 | |
| 			require.Equal(t, inDatabaseMap[upsert.Existing.UID], upsert.Existing)
 | |
| 			require.Equal(t, submittedMap[upsert.Existing.UID], upsert.New)
 | |
| 			require.NotEmpty(t, upsert.Diff)
 | |
| 		}
 | |
| 		require.Empty(t, changes.Delete)
 | |
| 		require.Empty(t, changes.New)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("should include only if there are changes ignoring specific fields", func(t *testing.T) {
 | |
| 		namespace := randFolder()
 | |
| 		groupName := util.GenerateShortUID()
 | |
| 		_, inDatabase := models.GenerateUniqueAlertRules(rand.Intn(5)+1, models.AlertRuleGen(withOrgID(orgId), withGroup(groupName), withNamespace(namespace)))
 | |
| 
 | |
| 		submitted := make([]*models.AlertRule, 0, len(inDatabase))
 | |
| 		for _, rule := range inDatabase {
 | |
| 			r := models.CopyRule(rule)
 | |
| 
 | |
| 			// Ignore difference in the following fields as submitted models do not have them set
 | |
| 			r.ID = rand.Int63()
 | |
| 			r.Version = rand.Int63()
 | |
| 			r.Updated = r.Updated.Add(1 * time.Minute)
 | |
| 
 | |
| 			submitted = append(submitted, r)
 | |
| 		}
 | |
| 
 | |
| 		fakeStore := store.NewFakeRuleStore(t)
 | |
| 		fakeStore.PutRule(context.Background(), inDatabase...)
 | |
| 
 | |
| 		changes, err := calculateChanges(context.Background(), fakeStore, orgId, namespace, groupName, submitted)
 | |
| 		require.NoError(t, err)
 | |
| 
 | |
| 		require.Empty(t, changes.Update)
 | |
| 		require.Empty(t, changes.Delete)
 | |
| 		require.Empty(t, changes.New)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("should patch rule with UID specified by existing rule", func(t *testing.T) {
 | |
| 		testCases := []struct {
 | |
| 			name    string
 | |
| 			mutator func(r *models.AlertRule)
 | |
| 		}{
 | |
| 			{
 | |
| 				name: "title is empty",
 | |
| 				mutator: func(r *models.AlertRule) {
 | |
| 					r.Title = ""
 | |
| 				},
 | |
| 			},
 | |
| 			{
 | |
| 				name: "condition and data are empty",
 | |
| 				mutator: func(r *models.AlertRule) {
 | |
| 					r.Condition = ""
 | |
| 					r.Data = nil
 | |
| 				},
 | |
| 			},
 | |
| 			{
 | |
| 				name: "ExecErrState is empty",
 | |
| 				mutator: func(r *models.AlertRule) {
 | |
| 					r.ExecErrState = ""
 | |
| 				},
 | |
| 			},
 | |
| 			{
 | |
| 				name: "NoDataState is empty",
 | |
| 				mutator: func(r *models.AlertRule) {
 | |
| 					r.NoDataState = ""
 | |
| 				},
 | |
| 			},
 | |
| 			{
 | |
| 				name: "For is 0",
 | |
| 				mutator: func(r *models.AlertRule) {
 | |
| 					r.For = 0
 | |
| 				},
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		dbRule := models.AlertRuleGen(withOrgID(orgId))()
 | |
| 
 | |
| 		fakeStore := store.NewFakeRuleStore(t)
 | |
| 		fakeStore.PutRule(context.Background(), dbRule)
 | |
| 
 | |
| 		namespace := randFolder()
 | |
| 		groupName := util.GenerateShortUID()
 | |
| 
 | |
| 		for _, testCase := range testCases {
 | |
| 			t.Run(testCase.name, func(t *testing.T) {
 | |
| 				expected := models.AlertRuleGen(simulateSubmitted, testCase.mutator)()
 | |
| 				expected.UID = dbRule.UID
 | |
| 				submitted := *expected
 | |
| 				changes, err := calculateChanges(context.Background(), fakeStore, orgId, namespace, groupName, []*models.AlertRule{&submitted})
 | |
| 				require.NoError(t, err)
 | |
| 				require.Len(t, changes.Update, 1)
 | |
| 				ch := changes.Update[0]
 | |
| 				require.Equal(t, ch.Existing, dbRule)
 | |
| 				fixed := *expected
 | |
| 				models.PatchPartialAlertRule(dbRule, &fixed)
 | |
| 				require.Equal(t, fixed, *ch.New)
 | |
| 			})
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	t.Run("should be able to find alerts by UID in other group/namespace", func(t *testing.T) {
 | |
| 		inDatabaseMap, inDatabase := models.GenerateUniqueAlertRules(rand.Intn(10)+10, models.AlertRuleGen(withOrgID(orgId)))
 | |
| 
 | |
| 		fakeStore := store.NewFakeRuleStore(t)
 | |
| 		fakeStore.PutRule(context.Background(), inDatabase...)
 | |
| 
 | |
| 		namespace := randFolder()
 | |
| 		groupName := util.GenerateShortUID()
 | |
| 		submittedMap, submitted := models.GenerateUniqueAlertRules(rand.Intn(len(inDatabase)-5)+5, models.AlertRuleGen(simulateSubmitted, withOrgID(orgId), withGroup(groupName), withNamespace(namespace), withUIDs(inDatabaseMap)))
 | |
| 
 | |
| 		changes, err := calculateChanges(context.Background(), fakeStore, orgId, namespace, groupName, submitted)
 | |
| 		require.NoError(t, err)
 | |
| 
 | |
| 		require.Empty(t, changes.Delete)
 | |
| 		require.Empty(t, changes.New)
 | |
| 		require.Len(t, changes.Update, len(submitted))
 | |
| 		for _, update := range changes.Update {
 | |
| 			require.NotNil(t, update.Existing)
 | |
| 			require.Equal(t, update.Existing.UID, update.New.UID)
 | |
| 			require.Equal(t, inDatabaseMap[update.Existing.UID], update.Existing)
 | |
| 			require.Equal(t, submittedMap[update.Existing.UID], update.New)
 | |
| 			require.NotEmpty(t, update.Diff)
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	t.Run("should fail when submitted rule has UID that does not exist in db", func(t *testing.T) {
 | |
| 		fakeStore := store.NewFakeRuleStore(t)
 | |
| 
 | |
| 		namespace := randFolder()
 | |
| 		groupName := util.GenerateShortUID()
 | |
| 		submitted := models.AlertRuleGen(withOrgID(orgId), simulateSubmitted)()
 | |
| 		require.NotEqual(t, "", submitted.UID)
 | |
| 
 | |
| 		_, err := calculateChanges(context.Background(), fakeStore, orgId, namespace, groupName, []*models.AlertRule{submitted})
 | |
| 		require.Error(t, err)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("should fail if cannot fetch current rules in the group", func(t *testing.T) {
 | |
| 		fakeStore := store.NewFakeRuleStore(t)
 | |
| 		expectedErr := errors.New("TEST ERROR")
 | |
| 		fakeStore.Hook = func(cmd interface{}) error {
 | |
| 			switch cmd.(type) {
 | |
| 			case models.ListRuleGroupAlertRulesQuery:
 | |
| 				return expectedErr
 | |
| 			}
 | |
| 			return nil
 | |
| 		}
 | |
| 
 | |
| 		namespace := randFolder()
 | |
| 		groupName := util.GenerateShortUID()
 | |
| 		submitted := models.AlertRuleGen(withOrgID(orgId), simulateSubmitted, withoutUID)()
 | |
| 
 | |
| 		_, err := calculateChanges(context.Background(), fakeStore, orgId, namespace, groupName, []*models.AlertRule{submitted})
 | |
| 		require.ErrorIs(t, err, expectedErr)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("should fail if cannot fetch rule by UID", func(t *testing.T) {
 | |
| 		fakeStore := store.NewFakeRuleStore(t)
 | |
| 		expectedErr := errors.New("TEST ERROR")
 | |
| 		fakeStore.Hook = func(cmd interface{}) error {
 | |
| 			switch cmd.(type) {
 | |
| 			case models.GetAlertRuleByUIDQuery:
 | |
| 				return expectedErr
 | |
| 			}
 | |
| 			return nil
 | |
| 		}
 | |
| 
 | |
| 		namespace := randFolder()
 | |
| 		groupName := util.GenerateShortUID()
 | |
| 		submitted := models.AlertRuleGen(withOrgID(orgId), simulateSubmitted)()
 | |
| 
 | |
| 		_, err := calculateChanges(context.Background(), fakeStore, orgId, namespace, groupName, []*models.AlertRule{submitted})
 | |
| 		require.Error(t, err, expectedErr)
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func withOrgID(orgId int64) func(rule *models.AlertRule) {
 | |
| 	return func(rule *models.AlertRule) {
 | |
| 		rule.OrgID = orgId
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func withGroup(groupName string) func(rule *models.AlertRule) {
 | |
| 	return func(rule *models.AlertRule) {
 | |
| 		rule.RuleGroup = groupName
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func withNamespace(namespace *models2.Folder) func(rule *models.AlertRule) {
 | |
| 	return func(rule *models.AlertRule) {
 | |
| 		rule.NamespaceUID = namespace.Uid
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // simulateSubmitted resets some fields of the structure that are not populated by API model to model conversion
 | |
| func simulateSubmitted(rule *models.AlertRule) {
 | |
| 	rule.ID = 0
 | |
| 	rule.Version = 0
 | |
| 	rule.Updated = time.Time{}
 | |
| }
 | |
| 
 | |
| func withoutUID(rule *models.AlertRule) {
 | |
| 	rule.UID = ""
 | |
| }
 | |
| 
 | |
| func withUIDs(uids map[string]*models.AlertRule) func(rule *models.AlertRule) {
 | |
| 	unused := make([]string, 0, len(uids))
 | |
| 	for s := range uids {
 | |
| 		unused = append(unused, s)
 | |
| 	}
 | |
| 	return func(rule *models.AlertRule) {
 | |
| 		if len(unused) == 0 {
 | |
| 			return
 | |
| 		}
 | |
| 		rule.UID = unused[0]
 | |
| 		unused = unused[1:]
 | |
| 	}
 | |
| }
 |