package api import ( "context" "encoding/json" "errors" "fmt" "math/rand" "net/http" "net/http/httptest" "net/url" "slices" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" 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/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/cmputil" "github.com/grafana/grafana/pkg/web" ) func TestRouteDeleteAlertRules(t *testing.T) { getRecordedCommand := func(ruleStore *fakes.RuleStore) []fakes.GenericRecordedQuery { results := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) { c, ok := cmd.(fakes.GenericRecordedQuery) if !ok || c.Name != "DeleteAlertRulesByUID" { return nil, false } return c, ok }) var result []fakes.GenericRecordedQuery for _, cmd := range results { result = append(result, cmd.(fakes.GenericRecordedQuery)) } return result } assertRulesDeleted := func(t *testing.T, expectedRules []*models.AlertRule, ruleStore *fakes.RuleStore) { deleteCommands := getRecordedCommand(ruleStore) require.Len(t, deleteCommands, 1) cmd := deleteCommands[0] actualUIDs := cmd.Params[3].([]string) require.Len(t, actualUIDs, len(expectedRules)) for _, rule := range expectedRules { require.Containsf(t, actualUIDs, rule.UID, "Rule %s was expected to be deleted but it wasn't", rule.UID) } } orgID := rand.Int63() folder := randFolder() gen := models.RuleGen.With(models.RuleGen.WithOrgID(orgID)) initFakeRuleStore := func(t *testing.T) *fakes.RuleStore { ruleStore := fakes.NewRuleStore(t) ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder) // add random data ruleStore.PutRule(context.Background(), gen.GenerateManyRef(1, 5)...) return ruleStore } t.Run("when fine-grained access is enabled", func(t *testing.T) { t.Run("and group argument is empty", func(t *testing.T) { t.Run("allow deleting without access to datasource", func(t *testing.T) { ruleStore := initFakeRuleStore(t) provisioningStore := fakes.NewFakeProvisioningStore() folderGen := gen.With(gen.WithNamespace(folder.ToFolderReference())) authorizedRulesInFolder := folderGen.With(gen.WithGroupPrefix("authz-")).GenerateManyRef(1, 5) ruleStore.PutRule(context.Background(), authorizedRulesInFolder...) permissions := createPermissionsForRulesWithoutDS(authorizedRulesInFolder, orgID) requestCtx := createRequestContextWithPerms(orgID, permissions, nil) response := createServiceWithProvenanceStore(ruleStore, provisioningStore).RouteDeleteAlertRules(requestCtx, folder.UID, "") require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body())) assertRulesDeleted(t, authorizedRulesInFolder, ruleStore) }) t.Run("return Forbidden if user is not authorized to access any group in the folder", func(t *testing.T) { ruleStore := initFakeRuleStore(t) ruleStore.PutRule(context.Background(), gen.With(gen.WithNamespace(folder.ToFolderReference())).GenerateManyRef(1, 5)...) request := createRequestContextWithPerms(orgID, map[int64]map[string][]string{}, nil) response := createService(ruleStore, nil).RouteDeleteAlertRules(request, folder.UID, "") require.Equalf(t, http.StatusForbidden, response.Status(), "Expected 403 but got %d: %v", response.Status(), string(response.Body())) require.Empty(t, getRecordedCommand(ruleStore)) }) t.Run("delete only non-provisioned groups that user is authorized", func(t *testing.T) { ruleStore := initFakeRuleStore(t) provisioningStore := fakes.NewFakeProvisioningStore() folderGen := gen.With(gen.WithNamespace(folder.ToFolderReference())) authorizedRulesInFolder := folderGen.With(gen.WithGroupPrefix("authz-")).GenerateManyRef(1, 5) provisionedRulesInFolder := folderGen.With(gen.WithGroupPrefix("provisioned-")).GenerateManyRef(1, 5) for _, rule := range provisionedRulesInFolder { err := provisioningStore.SetProvenance(context.Background(), rule, orgID, models.ProvenanceAPI) require.NoError(t, err) } ruleStore.PutRule(context.Background(), authorizedRulesInFolder...) ruleStore.PutRule(context.Background(), provisionedRulesInFolder...) permissions := createPermissionsForRules(append(authorizedRulesInFolder, provisionedRulesInFolder...), orgID) requestCtx := createRequestContextWithPerms(orgID, permissions, nil) response := createServiceWithProvenanceStore(ruleStore, provisioningStore).RouteDeleteAlertRules(requestCtx, folder.UID, "") require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body())) assertRulesDeleted(t, authorizedRulesInFolder, ruleStore) }) t.Run("return 400 if all rules user can access are provisioned", func(t *testing.T) { ruleStore := initFakeRuleStore(t) provisioningStore := fakes.NewFakeProvisioningStore() folderGen := gen.With(gen.WithNamespace(folder.ToFolderReference())) provisionedRulesInFolder := folderGen.With(gen.WithSameGroup()).GenerateManyRef(1, 5) err := provisioningStore.SetProvenance(context.Background(), provisionedRulesInFolder[0], orgID, models.ProvenanceAPI) require.NoError(t, err) ruleStore.PutRule(context.Background(), provisionedRulesInFolder...) permissions := createPermissionsForRules(provisionedRulesInFolder, orgID) requestCtx := createRequestContextWithPerms(orgID, permissions, nil) response := createServiceWithProvenanceStore(ruleStore, provisioningStore).RouteDeleteAlertRules(requestCtx, folder.UID, "") require.Equalf(t, 400, response.Status(), "Expected 400 but got %d: %v", response.Status(), string(response.Body())) require.Empty(t, getRecordedCommand(ruleStore)) }) t.Run("should return 202 if folder is empty", func(t *testing.T) { ruleStore := initFakeRuleStore(t) requestCtx := createRequestContext(orgID, nil) response := createService(ruleStore, nil).RouteDeleteAlertRules(requestCtx, folder.UID, "") require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body())) require.Empty(t, getRecordedCommand(ruleStore)) }) }) t.Run("and group argument is not empty", func(t *testing.T) { t.Run("return Forbidden if user is not authorized to access the group", func(t *testing.T) { ruleStore := initFakeRuleStore(t) groupGen := gen.With(gen.WithNamespace(folder.ToFolderReference()), gen.WithSameGroup()) authorizedRulesInGroup := groupGen.GenerateManyRef(1, 5) ruleStore.PutRule(context.Background(), authorizedRulesInGroup...) permissions := createPermissionsForRules([]*models.AlertRule{}, orgID) requestCtx := createRequestContextWithPerms(orgID, permissions, nil) response := createService(ruleStore, nil).RouteDeleteAlertRules(requestCtx, folder.UID, authorizedRulesInGroup[0].RuleGroup) require.Equalf(t, http.StatusForbidden, response.Status(), "Expected 403 but got %d: %v", response.Status(), string(response.Body())) deleteCommands := getRecordedCommand(ruleStore) require.Empty(t, deleteCommands) }) t.Run("return 400 if group is provisioned", func(t *testing.T) { ruleStore := initFakeRuleStore(t) provisioningStore := fakes.NewFakeProvisioningStore() groupGen := gen.With(gen.WithNamespace(folder.ToFolderReference()), gen.WithSameGroup()) provisionedRulesInFolder := groupGen.GenerateManyRef(1, 5) err := provisioningStore.SetProvenance(context.Background(), provisionedRulesInFolder[0], orgID, models.ProvenanceAPI) require.NoError(t, err) ruleStore.PutRule(context.Background(), provisionedRulesInFolder...) permissions := createPermissionsForRules(provisionedRulesInFolder, orgID) requestCtx := createRequestContextWithPerms(orgID, permissions, nil) response := createServiceWithProvenanceStore(ruleStore, provisioningStore).RouteDeleteAlertRules(requestCtx, folder.UID, provisionedRulesInFolder[0].RuleGroup) require.Equalf(t, 400, response.Status(), "Expected 400 but got %d: %v", response.Status(), string(response.Body())) deleteCommands := getRecordedCommand(ruleStore) require.Empty(t, deleteCommands) }) }) }) } func TestRouteGetNamespaceRulesConfig(t *testing.T) { gen := models.RuleGen t.Run("fine-grained access is enabled", func(t *testing.T) { t.Run("should return all rules, with or without data source access", func(t *testing.T) { orgID := rand.Int63() folder := randFolder() ruleStore := fakes.NewRuleStore(t) ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder) folderGen := gen.With(gen.WithOrgID(orgID), gen.WithNamespace(folder.ToFolderReference()), gen.WithUpdatedBy(util.Pointer(models.UserUID("test-user")))) queryAccessRules := folderGen.GenerateManyRef(2, 6) ruleStore.PutRule(context.Background(), queryAccessRules...) noQueryAccessRules := folderGen.GenerateManyRef(2, 6) ruleStore.PutRule(context.Background(), noQueryAccessRules...) allRules := make([]*models.AlertRule, 0, len(queryAccessRules)+len(noQueryAccessRules)) allRules = append(allRules, queryAccessRules...) allRules = append(allRules, noQueryAccessRules...) permissions := createPermissionsForRules(queryAccessRules, orgID) req := createRequestContextWithPerms(orgID, permissions, nil) fakeUserService := usertest.NewUserServiceFake() userUids := make([]string, 0) for _, rule := range allRules { if rule.UpdatedBy != nil { userUids = append(userUids, string(*rule.UpdatedBy)) } } fakeUserServiceResponse := []*user.User{} for i, uid := range userUids { fakeUserServiceResponse = append(fakeUserServiceResponse, &user.User{ID: int64(i + 1), UID: uid}) } fakeUserService.ExpectedListUsersByIdOrUid = fakeUserServiceResponse svc := createService(ruleStore, fakeUserService) response := svc.RouteGetNamespaceRulesConfig(req, folder.UID) require.Equal(t, http.StatusAccepted, response.Status()) require.Equal(t, 1, len(fakeUserService.ListUsersByIdOrUidCalls)) // only one call to the user service result := &apimodels.NamespaceConfigResponse{} require.NoError(t, json.Unmarshal(response.Body(), result)) require.NotNil(t, result) for namespace, groups := range *result { require.Equal(t, folder.Fullpath, namespace) for _, group := range groups { grouploop: for _, actualRule := range group.Rules { for i, expected := range allRules { if actualRule.GrafanaManagedAlert.UID == expected.UID { allRules = append(allRules[:i], allRules[i+1:]...) continue grouploop } } assert.Failf(t, "rule in a group was not found in expected", "rule %s group %s", actualRule.GrafanaManagedAlert.Title, group.Name) } } } assert.Emptyf(t, allRules, "not all expected rules were returned") }) }) t.Run("should return the provenance of the alert rules", func(t *testing.T) { orgID := rand.Int63() folder := randFolder() ruleStore := fakes.NewRuleStore(t) ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder) expectedRules := gen.With(gen.WithOrgID(orgID), gen.WithNamespace(folder.ToFolderReference())).GenerateManyRef(2, 6) ruleStore.PutRule(context.Background(), expectedRules...) fakeUserService := usertest.NewUserServiceFake() userUids := make([]string, 0) for _, rule := range expectedRules { if rule.UpdatedBy != nil { userUids = append(userUids, string(*rule.UpdatedBy)) } } fakeUserServiceResponse := []*user.User{} for i, uid := range userUids { fakeUserServiceResponse = append(fakeUserServiceResponse, &user.User{ID: int64(i + 1), UID: uid}) } fakeUserService.ExpectedListUsersByIdOrUid = fakeUserServiceResponse svc := createService(ruleStore, fakeUserService) // add provenance to the first generated rule rule := &models.AlertRule{ UID: expectedRules[0].UID, } err := svc.provenanceStore.SetProvenance(context.Background(), rule, orgID, models.ProvenanceAPI) require.NoError(t, err) perms := createPermissionsForRules(expectedRules, orgID) req := createRequestContextWithPerms(orgID, perms, nil) response := svc.RouteGetNamespaceRulesConfig(req, folder.UID) require.Equal(t, http.StatusAccepted, response.Status()) if len(userUids) > 0 { require.Equal(t, 1, len(fakeUserService.ListUsersByIdOrUidCalls)) } else { require.Equal(t, 0, len(fakeUserService.ListUsersByIdOrUidCalls)) } result := &apimodels.NamespaceConfigResponse{} require.NoError(t, json.Unmarshal(response.Body(), result)) require.NotNil(t, result) found := false for namespace, groups := range *result { require.Equal(t, folder.Fullpath, namespace) for _, group := range groups { for _, actualRule := range group.Rules { if actualRule.GrafanaManagedAlert.UID == expectedRules[0].UID { require.Equal(t, apimodels.Provenance(models.ProvenanceAPI), actualRule.GrafanaManagedAlert.Provenance) found = true } else { require.Equal(t, apimodels.Provenance(models.ProvenanceNone), actualRule.GrafanaManagedAlert.Provenance) } } } } require.True(t, found) }) t.Run("should enforce order of rules in the group", func(t *testing.T) { orgID := rand.Int63() folder := randFolder() ruleStore := fakes.NewRuleStore(t) ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder) groupKey := models.GenerateGroupKey(orgID) groupKey.NamespaceUID = folder.UID expectedRules := make([]*models.AlertRule, 0) for i := 0; i < 10; i++ { expectedRules = append(expectedRules, gen.With(gen.WithGroupKey(groupKey), gen.WithUniqueGroupIndex(), gen.WithUpdatedBy(util.Pointer(models.UserUID(util.GenerateShortUID())))).GenerateManyRef(5, 10)...) } ruleStore.PutRule(context.Background(), expectedRules...) perms := createPermissionsForRules(expectedRules, orgID) req := createRequestContextWithPerms(orgID, perms, nil) fakeUserService := usertest.NewUserServiceFake() userUids := make([]string, 0) for _, rule := range expectedRules { if rule.UpdatedBy != nil { userUids = append(userUids, string(*rule.UpdatedBy)) } } fakeUserServiceResponse := []*user.User{} for i, uid := range userUids { fakeUserServiceResponse = append(fakeUserServiceResponse, &user.User{ID: int64(i + 1), UID: uid}) } fakeUserService.ExpectedListUsersByIdOrUid = fakeUserServiceResponse svc := createService(ruleStore, fakeUserService) response := svc.RouteGetNamespaceRulesConfig(req, folder.UID) require.Equal(t, http.StatusAccepted, response.Status()) require.Equal(t, 1, len(fakeUserService.ListUsersByIdOrUidCalls)) // only one call to the user service result := &apimodels.NamespaceConfigResponse{} require.NoError(t, json.Unmarshal(response.Body(), result)) require.NotNil(t, result) models.RulesGroup(expectedRules).SortByGroupIndex() groups, ok := (*result)[folder.Fullpath] require.True(t, ok) require.Len(t, groups, 1) group := groups[0] require.Equal(t, groupKey.RuleGroup, group.Name) for i, actual := range groups[0].Rules { expected := expectedRules[i] if actual.GrafanaManagedAlert.UID != expected.UID { var actualUIDs []string var expectedUIDs []string for _, rule := range group.Rules { actualUIDs = append(actualUIDs, rule.GrafanaManagedAlert.UID) } for _, rule := range expectedRules { expectedUIDs = append(expectedUIDs, rule.UID) } require.Fail(t, fmt.Sprintf("rules are not sorted by group index. Expected: %v. Actual: %v", expectedUIDs, actualUIDs)) } } }) } func TestRouteGetRuleByUID(t *testing.T) { t.Run("rule is successfully fetched with the correct UID", func(t *testing.T) { orgID := rand.Int63() folder := randFolder() ruleStore := fakes.NewRuleStore(t) ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder) groupKey := models.GenerateGroupKey(orgID) groupKey.NamespaceUID = folder.UID gen := models.RuleGen.With(models.RuleGen.WithGroupKey(groupKey)) createdRules := gen.With( gen.WithUniqueGroupIndex(), gen.WithUniqueID(), gen.WithEditorSettingsSimplifiedQueryAndExpressionsSection(true), gen.WithEditorSettingsSimplifiedNotificationsSection(true), gen.WithKeepFiringFor(30*time.Second), ).GenerateManyRef(3) require.Len(t, createdRules, 3) ruleStore.PutRule(context.Background(), createdRules...) perms := createPermissionsForRules(createdRules, orgID) req := createRequestContextWithPerms(orgID, perms, nil) expectedRule := createdRules[1] fakeUserService := usertest.NewUserServiceFake() svc := createService(ruleStore, fakeUserService) response := svc.RouteGetRuleByUID(req, expectedRule.UID) require.Equal(t, http.StatusOK, response.Status()) result := &apimodels.GettableExtendedRuleNode{} require.NoError(t, json.Unmarshal(response.Body(), result)) require.NotNil(t, result) require.Equal(t, expectedRule.UID, result.GrafanaManagedAlert.UID) require.Equal(t, expectedRule.RuleGroup, result.GrafanaManagedAlert.RuleGroup) require.Equal(t, expectedRule.Title, result.GrafanaManagedAlert.Title) require.Equal(t, int64(expectedRule.KeepFiringFor), int64(*(result.KeepFiringFor))) require.True(t, result.GrafanaManagedAlert.Metadata.EditorSettings.SimplifiedQueryAndExpressionsSection) require.True(t, result.GrafanaManagedAlert.Metadata.EditorSettings.SimplifiedNotificationsSection) t.Run("should resolve Updated_by with user service", func(t *testing.T) { testcases := []struct { desc string UpdatedBy *models.UserUID User *user.User UserServiceError error UserServiceCalls []usertest.ListUsersByIdOrUidCall Expected *apimodels.UserInfo }{ { desc: "nil if UpdatedBy is nil", UpdatedBy: nil, User: nil, UserServiceError: nil, UserServiceCalls: nil, Expected: nil, }, { desc: "just UID if user is not found", UpdatedBy: util.Pointer(models.UserUID("test-uid")), User: nil, UserServiceError: nil, UserServiceCalls: []usertest.ListUsersByIdOrUidCall{{Uids: []string{"test-uid"}, Ids: []int64{}}}, Expected: &apimodels.UserInfo{ UID: "test-uid", }, }, { desc: "just UID if error", UpdatedBy: util.Pointer(models.UserUID("test-uid")), UserServiceError: errors.New("error"), UserServiceCalls: []usertest.ListUsersByIdOrUidCall{{Uids: []string{"test-uid"}, Ids: []int64{}}}, Expected: &apimodels.UserInfo{ UID: "test-uid", }, }, { desc: "login if it's known user", UpdatedBy: util.Pointer(models.UserUID("test-uid")), User: &user.User{ UID: "test-uid", Login: "Test", }, UserServiceError: nil, UserServiceCalls: []usertest.ListUsersByIdOrUidCall{{Uids: []string{"test-uid"}, Ids: []int64{}}}, Expected: &apimodels.UserInfo{ UID: "test-uid", Name: "Test", }, }, { desc: "recognize system identifier (alerting)", UpdatedBy: &models.AlertingUserUID, User: nil, UserServiceError: nil, UserServiceCalls: nil, Expected: &apimodels.UserInfo{ UID: string(models.AlertingUserUID), }, }, { desc: "recognize system identifier (provisioning)", UpdatedBy: &models.FileProvisioningUserUID, User: nil, UserServiceError: nil, UserServiceCalls: nil, Expected: &apimodels.UserInfo{ UID: string(models.FileProvisioningUserUID), }, }, } for _, tc := range testcases { t.Run(tc.desc, func(t *testing.T) { expectedRule.UpdatedBy = tc.UpdatedBy usvc := usertest.NewUserServiceFake() if tc.User != nil { usvc.ExpectedListUsersByIdOrUid = []*user.User{tc.User} } usvc.ExpectedError = tc.UserServiceError svc := createService(ruleStore, usvc) response := svc.RouteGetRuleByUID(req, expectedRule.UID) require.Equal(t, http.StatusOK, response.Status()) result := &apimodels.GettableExtendedRuleNode{} require.NoError(t, json.Unmarshal(response.Body(), result)) require.NotNil(t, result) require.Equal(t, tc.UserServiceCalls, usvc.ListUsersByIdOrUidCalls) require.Equal(t, tc.Expected, result.GrafanaManagedAlert.UpdatedBy) }) } }) }) t.Run("error when fetching rule with non-existent UID", func(t *testing.T) { orgID := rand.Int63() folder := randFolder() ruleStore := fakes.NewRuleStore(t) ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder) groupKey := models.GenerateGroupKey(orgID) groupKey.NamespaceUID = folder.UID gen := models.RuleGen.With(models.RuleGen.WithGroupKey(groupKey)) createdRules := gen.With(gen.WithUniqueGroupIndex(), gen.WithUniqueID()).GenerateManyRef(3) require.Len(t, createdRules, 3) ruleStore.PutRule(context.Background(), createdRules...) perms := createPermissionsForRules(createdRules, orgID) req := createRequestContextWithPerms(orgID, perms, nil) fakeUserService := usertest.NewUserServiceFake() svc := createService(ruleStore, fakeUserService) response := svc.RouteGetRuleByUID(req, "foobar") require.Equal(t, http.StatusNotFound, response.Status()) require.Equal(t, 0, len(fakeUserService.ListUsersByIdOrUidCalls)) }) } func TestRouteGetRuleVersionsByUID(t *testing.T) { orgID := rand.Int63() f := randFolder() groupKey := models.GenerateGroupKey(orgID) groupKey.NamespaceUID = f.UID gen := models.RuleGen.With(models.RuleGen.WithGroupKey(groupKey), models.RuleGen.WithUniqueID()) t.Run("rule history is successfully fetched with the correct UID", func(t *testing.T) { ruleStore := fakes.NewRuleStore(t) ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], f) rule := gen.GenerateRef() history := gen.With(gen.WithUID(rule.UID)).GenerateManyRef(3) // simulate order of the history rule.ID = 100 for i, alertRule := range history { alertRule.ID = rule.ID - int64(i) - 1 } ruleStore.PutRule(context.Background(), rule) ruleStore.History[rule.GUID] = append(ruleStore.History[rule.GUID], history...) perms := createPermissionsForRules([]*models.AlertRule{rule}, orgID) req := createRequestContextWithPerms(orgID, perms, nil) svc := createService(ruleStore, nil) response := svc.RouteGetRuleVersionsByUID(req, rule.UID) require.Equal(t, http.StatusOK, response.Status()) var result apimodels.GettableRuleVersions require.NoError(t, json.Unmarshal(response.Body(), &result)) require.NotNil(t, result) require.Len(t, result, len(history)+1) // history + current version t.Run("should be in correct order", func(t *testing.T) { expectedHistory := append([]*models.AlertRule{rule}, history...) for i, rul := range expectedHistory { assert.Equal(t, rul.UID, result[i].GrafanaManagedAlert.UID) } }) }) t.Run("NotFound when rule does not exist", func(t *testing.T) { ruleStore := fakes.NewRuleStore(t) ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], f) ruleKey := models.AlertRuleKey{ OrgID: orgID, UID: "test", } guid := uuid.NewString() history := gen.With(gen.WithGUID(guid), gen.WithKey(ruleKey)).GenerateManyRef(3) ruleStore.History[guid] = append(ruleStore.History[guid], history...) // even if history is full of records perms := createPermissionsForRules(history, orgID) req := createRequestContextWithPerms(orgID, perms, nil) response := createService(ruleStore, nil).RouteGetRuleVersionsByUID(req, ruleKey.UID) require.Equal(t, http.StatusNotFound, response.Status()) }) t.Run("Empty result when rule history is empty", func(t *testing.T) { ruleStore := fakes.NewRuleStore(t) ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], f) ruleKey := models.AlertRuleKey{ OrgID: orgID, UID: "test", } guid := uuid.NewString() rule := gen.With(gen.WithKey(ruleKey), gen.WithGUID(guid)).GenerateRef() ruleStore.PutRule(context.Background(), rule) ruleStore.History[guid] = nil perms := createPermissionsForRules([]*models.AlertRule{rule}, orgID) req := createRequestContextWithPerms(orgID, perms, nil) response := createService(ruleStore, nil).RouteGetRuleVersionsByUID(req, ruleKey.UID) require.Equal(t, http.StatusOK, response.Status()) var result apimodels.GettableRuleVersions require.NoError(t, json.Unmarshal(response.Body(), &result)) require.Empty(t, result) }) t.Run("Unauthorized if user does not have access to the current rule", func(t *testing.T) { ruleStore := fakes.NewRuleStore(t) anotherFolder := randFolder() ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], f, anotherFolder) ruleKey := models.AlertRuleKey{ OrgID: orgID, UID: "test", } guid := uuid.NewString() rule := gen.With(gen.WithGUID(guid), gen.WithKey(ruleKey), gen.WithNamespaceUID(anotherFolder.UID)).GenerateRef() ruleStore.PutRule(context.Background(), rule) history := gen.With(gen.WithGUID(guid), gen.WithKey(ruleKey)).GenerateManyRef(3) ruleStore.History[guid] = history perms := createPermissionsForRules(history, orgID) // grant permissions to all records in history but not the rule itself req := createRequestContextWithPerms(orgID, perms, nil) response := createService(ruleStore, nil).RouteGetRuleVersionsByUID(req, ruleKey.UID) require.Equal(t, http.StatusForbidden, response.Status()) }) } func TestRouteGetRulesConfig(t *testing.T) { gen := models.RuleGen t.Run("fine-grained access is enabled", func(t *testing.T) { t.Run("should check access to data source", func(t *testing.T) { orgID := rand.Int63() ruleStore := fakes.NewRuleStore(t) folder1 := randFolder() folder2 := randFolder() ruleStore.Folders[orgID] = []*folder.Folder{folder1, folder2} group1Key := models.GenerateGroupKey(orgID) group1Key.NamespaceUID = folder1.UID group2Key := models.GenerateGroupKey(orgID) group2Key.NamespaceUID = folder2.UID ruleUpdatedBy := util.Pointer(models.UserUID(util.GenerateShortUID())) group1 := gen.With(gen.WithGroupKey(group1Key), gen.WithUpdatedBy(ruleUpdatedBy)).GenerateManyRef(2, 6) group2 := gen.With(gen.WithGroupKey(group2Key), gen.WithUpdatedBy(ruleUpdatedBy)).GenerateManyRef(2, 6) ruleStore.PutRule(context.Background(), append(group1, group2...)...) t.Run("and do not return group if user does not have access to one of rules", func(t *testing.T) { permissions := createPermissionsForRules(append(group1, group2[1:]...), orgID) request := createRequestContextWithPerms(orgID, permissions, nil) fakeUserService := usertest.NewUserServiceFake() svc := createService(ruleStore, fakeUserService) response := svc.RouteGetRulesConfig(request) require.Equal(t, http.StatusOK, response.Status()) require.Equal(t, 1, len(fakeUserService.ListUsersByIdOrUidCalls)) // only one call to the user service result := &apimodels.NamespaceConfigResponse{} require.NoError(t, json.Unmarshal(response.Body(), result)) require.NotNil(t, result) require.Contains(t, *result, folder1.Fullpath) require.NotContains(t, *result, folder2.UID) groups := (*result)[folder1.Fullpath] require.Len(t, groups, 1) require.Equal(t, group1Key.RuleGroup, groups[0].Name) require.Len(t, groups[0].Rules, len(group1)) }) }) }) t.Run("should return rules in group sorted by group index", func(t *testing.T) { orgID := rand.Int63() folder := randFolder() ruleStore := fakes.NewRuleStore(t) ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder) groupKey := models.GenerateGroupKey(orgID) groupKey.NamespaceUID = folder.UID ruleUpdatedBy := util.Pointer(models.UserUID(util.GenerateShortUID())) expectedRules := gen.With(gen.WithGroupKey(groupKey), gen.WithUniqueGroupIndex(), gen.WithUpdatedBy(ruleUpdatedBy)).GenerateManyRef(5, 10) ruleStore.PutRule(context.Background(), expectedRules...) perms := createPermissionsForRules(expectedRules, orgID) req := createRequestContextWithPerms(orgID, perms, nil) fakeUserService := usertest.NewUserServiceFake() svc := createService(ruleStore, fakeUserService) response := svc.RouteGetRulesConfig(req) require.Equal(t, http.StatusOK, response.Status()) result := &apimodels.NamespaceConfigResponse{} require.NoError(t, json.Unmarshal(response.Body(), result)) require.NotNil(t, result) require.Equal(t, 1, len(fakeUserService.ListUsersByIdOrUidCalls)) // only one call to the user service models.RulesGroup(expectedRules).SortByGroupIndex() groups, ok := (*result)[folder.Fullpath] require.True(t, ok) require.Len(t, groups, 1) group := groups[0] require.Equal(t, groupKey.RuleGroup, group.Name) for i, actual := range groups[0].Rules { expected := expectedRules[i] if actual.GrafanaManagedAlert.UID != expected.UID { var actualUIDs []string var expectedUIDs []string for _, rule := range group.Rules { actualUIDs = append(actualUIDs, rule.GrafanaManagedAlert.UID) } for _, rule := range expectedRules { expectedUIDs = append(expectedUIDs, rule.UID) } require.Fail(t, fmt.Sprintf("rules are not sorted by group index. Expected: %v. Actual: %v", expectedUIDs, actualUIDs)) } } }) } func TestRouteGetRulesGroupConfig(t *testing.T) { gen := models.RuleGen t.Run("should return rules in group sorted by group index", func(t *testing.T) { orgID := rand.Int63() folder := randFolder() ruleStore := fakes.NewRuleStore(t) ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder) groupKey := models.GenerateGroupKey(orgID) groupKey.NamespaceUID = folder.UID ruleUpdatedBy := util.Pointer(models.UserUID(util.GenerateShortUID())) expectedRules := gen.With(gen.WithGroupKey(groupKey), gen.WithUniqueGroupIndex(), gen.WithUpdatedBy(ruleUpdatedBy)).GenerateManyRef(5, 10) ruleStore.PutRule(context.Background(), expectedRules...) perms := createPermissionsForRules(expectedRules, orgID) req := createRequestContextWithPerms(orgID, perms, nil) fakeUserService := usertest.NewUserServiceFake() svc := createService(ruleStore, fakeUserService) response := svc.RouteGetRulesGroupConfig(req, folder.UID, groupKey.RuleGroup) require.Equal(t, http.StatusAccepted, response.Status()) result := &apimodels.RuleGroupConfigResponse{} require.NoError(t, json.Unmarshal(response.Body(), result)) require.NotNil(t, result) require.Equal(t, 1, len(fakeUserService.ListUsersByIdOrUidCalls)) // only one call to the user service models.RulesGroup(expectedRules).SortByGroupIndex() for i, actual := range result.Rules { expected := expectedRules[i] if actual.GrafanaManagedAlert.UID != expected.UID { var actualUIDs []string var expectedUIDs []string for _, rule := range result.Rules { actualUIDs = append(actualUIDs, rule.GrafanaManagedAlert.UID) } for _, rule := range expectedRules { expectedUIDs = append(expectedUIDs, rule.UID) } require.Fail(t, fmt.Sprintf("rules are not sorted by group index. Expected: %v. Actual: %v", expectedUIDs, actualUIDs)) } } }) t.Run("should return a 404 when fetching a group that doesn't exist", func(t *testing.T) { orgID := rand.Int63() folder := randFolder() ruleStore := fakes.NewRuleStore(t) ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder) groupKey := models.GenerateGroupKey(orgID) groupKey.NamespaceUID = folder.UID expectedRules := gen.With(gen.WithGroupKey(groupKey), gen.WithUniqueGroupIndex()).GenerateManyRef(5, 10) ruleStore.PutRule(context.Background(), expectedRules...) perms := createPermissionsForRules(expectedRules, orgID) req := createRequestContextWithPerms(orgID, perms, nil) fakeUserService := usertest.NewUserServiceFake() svc := createService(ruleStore, fakeUserService) response := svc.RouteGetRulesGroupConfig(req, folder.UID, "non-existent-rule-group") require.Equal(t, http.StatusNotFound, response.Status()) require.Equal(t, 0, len(fakeUserService.ListUsersByIdOrUidCalls)) }) } func TestVerifyProvisionedRulesNotAffected(t *testing.T) { orgID := rand.Int63() group := models.GenerateGroupKey(orgID) affectedGroups := make(map[models.AlertRuleGroupKey]models.RulesGroup) gen := models.RuleGen var allRules []*models.AlertRule { rules := gen.With(gen.WithGroupKey(group)).GenerateManyRef(1, 4) allRules = append(allRules, rules...) affectedGroups[group] = rules for i := 0; i < rand.Intn(3)+1; i++ { g := models.GenerateGroupKey(orgID) rules := gen.With(gen.WithGroupKey(g)).GenerateManyRef(1, 4) allRules = append(allRules, rules...) affectedGroups[g] = rules } } ch := &store.GroupDelta{ GroupKey: group, AffectedGroups: affectedGroups, } t.Run("should return error if at least one rule in affected groups is provisioned", func(t *testing.T) { rand.Shuffle(len(allRules), func(i, j int) { allRules[j], allRules[i] = allRules[i], allRules[j] }) storeResult := make(map[string]models.Provenance, len(allRules)) storeResult[allRules[0].UID] = models.ProvenanceAPI storeResult[allRules[1].UID] = models.ProvenanceFile provenanceStore := &provisioning.MockProvisioningStore{} provenanceStore.EXPECT().GetProvenances(mock.Anything, orgID, "alertRule").Return(storeResult, nil) result := verifyProvisionedRulesNotAffected(context.Background(), provenanceStore, orgID, ch) require.Error(t, result) require.ErrorIs(t, result, errProvisionedResource) assert.Contains(t, result.Error(), allRules[0].GetGroupKey().String()) assert.Contains(t, result.Error(), allRules[1].GetGroupKey().String()) }) t.Run("should return nil if all have ProvenanceNone", func(t *testing.T) { storeResult := make(map[string]models.Provenance, len(allRules)) for _, rule := range allRules { storeResult[rule.UID] = models.ProvenanceNone } provenanceStore := &provisioning.MockProvisioningStore{} provenanceStore.EXPECT().GetProvenances(mock.Anything, orgID, "alertRule").Return(storeResult, nil) result := verifyProvisionedRulesNotAffected(context.Background(), provenanceStore, orgID, ch) require.NoError(t, result) }) t.Run("should return nil if no alerts have provisioning status", func(t *testing.T) { provenanceStore := &provisioning.MockProvisioningStore{} provenanceStore.EXPECT().GetProvenances(mock.Anything, orgID, "alertRule").Return(make(map[string]models.Provenance, len(allRules)), nil) result := verifyProvisionedRulesNotAffected(context.Background(), provenanceStore, orgID, ch) require.NoError(t, result) }) } func TestValidateQueries(t *testing.T) { gen := models.RuleGen delta := store.GroupDelta{ New: []*models.AlertRule{ gen.With(gen.WithCondition("New")).GenerateRef(), }, Update: []store.RuleDelta{ { Existing: gen.With(gen.WithCondition("New")).GenerateRef(), New: gen.With(gen.WithCondition("Update_New")).GenerateRef(), Diff: cmputil.DiffReport{ cmputil.Diff{ Path: "SomeField", }, }, }, { Existing: gen.With(gen.WithCondition("Update_Index_Existing")).GenerateRef(), New: gen.With(gen.WithCondition("Update_Index_New")).GenerateRef(), Diff: cmputil.DiffReport{ cmputil.Diff{ Path: "RuleGroupIndex", }, }, }, }, Delete: gen.With(gen.WithCondition("Deleted")).GenerateManyRef(1), } t.Run("should not validate deleted rules or updated rules with ignored fields", func(t *testing.T) { validator := &recordingConditionValidator{} err := validateQueries(context.Background(), &delta, validator, nil) require.NoError(t, err) noValidate := []string{"Deleted", "Update_Index_New"} for _, condition := range validator.recorded { if !slices.Contains(noValidate, condition.Condition) { continue } assert.Failf(t, "validated unexpected condition", "condition '%s' was validated but should not", condition.Condition) } }) t.Run("should return rule validate error if fails on new rule", func(t *testing.T) { validator := &recordingConditionValidator{ hook: func(c models.Condition) error { if c.Condition == "New" { return errors.New("test") } return nil }, } err := validateQueries(context.Background(), &delta, validator, nil) require.Error(t, err) require.ErrorIs(t, err, models.ErrAlertRuleFailedValidation) }) t.Run("should return rule validate error with UID if fails on updated rule", func(t *testing.T) { validator := &recordingConditionValidator{ hook: func(c models.Condition) error { if c.Condition == "Update_New" { return errors.New("test") } return nil }, } err := validateQueries(context.Background(), &delta, validator, nil) require.Error(t, err) require.ErrorIs(t, err, models.ErrAlertRuleFailedValidation) require.ErrorContains(t, err, delta.Update[0].New.UID) }) } func createServiceWithProvenanceStore(store *fakes.RuleStore, provenanceStore provisioning.ProvisioningStore) *RulerSrv { svc := createService(store, nil) svc.provenanceStore = provenanceStore return svc } func createService(store *fakes.RuleStore, _userService *usertest.FakeUserService) *RulerSrv { userService := _userService if _userService == nil { userService = usertest.NewUserServiceFake() } return &RulerSrv{ xactManager: store, store: store, QuotaService: nil, provenanceStore: fakes.NewFakeProvisioningStore(), log: log.New("test"), cfg: &setting.UnifiedAlertingSettings{ BaseInterval: 10 * time.Second, }, authz: accesscontrol.NewRuleService(acimpl.ProvideAccessControl(featuremgmt.WithFeatures())), amConfigStore: &fakeAMRefresher{}, amRefresher: &fakeAMRefresher{}, userService: userService, conditionValidator: &recordingConditionValidator{}, } } type fakeAMRefresher struct { } func (f *fakeAMRefresher) ApplyConfig(ctx context.Context, orgId int64, dbConfig *models.AlertConfiguration) error { return nil } func (f *fakeAMRefresher) GetLatestAlertmanagerConfiguration(ctx context.Context, orgID int64) (*models.AlertConfiguration, error) { return nil, nil } func createRequestContext(orgID int64, params map[string]string) *contextmodel.ReqContext { defaultPerms := map[int64]map[string][]string{orgID: {datasources.ActionQuery: []string{datasources.ScopeAll}}} return createRequestContextWithPerms(orgID, defaultPerms, params) } func createRequestContextWithPerms(orgID int64, permissions map[int64]map[string][]string, params map[string]string) *contextmodel.ReqContext { uri, _ := url.Parse("http://localhost") ctx := web.Context{ Req: &http.Request{ URL: uri, Header: make(http.Header), Form: make(url.Values), }, Resp: web.NewResponseWriter("GET", httptest.NewRecorder()), } if params != nil { ctx.Req = web.SetURLParams(ctx.Req, params) } return &contextmodel.ReqContext{ IsSignedIn: true, SignedInUser: &user.SignedInUser{ Permissions: permissions, OrgID: orgID, }, Context: &ctx, } } func createPermissionsForRules(rules []*models.AlertRule, orgID int64) map[int64]map[string][]string { ns := map[string]any{} permissions := map[string][]string{} for _, rule := range rules { if _, ok := ns[rule.NamespaceUID]; !ok { scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(rule.NamespaceUID) permissions[dashboards.ActionFoldersRead] = append(permissions[dashboards.ActionFoldersRead], scope) permissions[ac.ActionAlertingRuleRead] = append(permissions[ac.ActionAlertingRuleRead], scope) permissions[ac.ActionAlertingRuleUpdate] = append(permissions[ac.ActionAlertingRuleUpdate], scope) ns[rule.NamespaceUID] = struct{}{} } for _, query := range rule.Data { permissions[datasources.ActionQuery] = append(permissions[datasources.ActionQuery], datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID)) } } return map[int64]map[string][]string{orgID: permissions} } func createPermissionsForRulesWithoutDS(rules []*models.AlertRule, orgID int64) map[int64]map[string][]string { ns := map[string]any{} permissions := map[string][]string{} for _, rule := range rules { if _, ok := ns[rule.NamespaceUID]; !ok { scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(rule.NamespaceUID) permissions[dashboards.ActionFoldersRead] = append(permissions[dashboards.ActionFoldersRead], scope) permissions[ac.ActionAlertingRuleRead] = append(permissions[ac.ActionAlertingRuleRead], scope) ns[rule.NamespaceUID] = struct{}{} } } return map[int64]map[string][]string{orgID: permissions} } func TestRouteUpdateNamespaceRules(t *testing.T) { orgID := rand.Int63() folder := randFolder() gen := models.RuleGen.With( models.RuleGen.WithOrgID(orgID), models.RuleGen.WithNamespaceUID(folder.UID), ) initFakeRuleStore := func(t *testing.T) *fakes.RuleStore { ruleStore := fakes.NewRuleStore(t) ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder) return ruleStore } getRecordedUpdatedRules := func(ruleStore *fakes.RuleStore) []models.UpdateRule { raw := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) { if u, ok := cmd.([]models.UpdateRule); ok { return u, true } return nil, false }) updates := []models.UpdateRule{} for _, cmd := range raw { updates = append(updates, cmd.([]models.UpdateRule)...) } return updates } t.Run("should pause all non-provisioned rules in namespace", func(t *testing.T) { ruleStore := initFakeRuleStore(t) provisioningStore := fakes.NewFakeProvisioningStore() // Create 3 types of rules: paused, provisioned paused, and unpaused pausedRules := gen.With(gen.WithGroupPrefix("paused-"), gen.WithIsPaused(true)).GenerateManyRef(2) unpausedRules := gen.With(gen.WithGroupPrefix("unpaused-"), gen.WithIsPaused(false)).GenerateManyRef(1) provisionedRules := gen.With( gen.WithGroupPrefix("provisioned-"), gen.WithIsPaused(false), ).GenerateManyRef(3) for _, r := range provisionedRules { err := provisioningStore.SetProvenance(context.Background(), r, orgID, models.ProvenanceAPI) require.NoError(t, err) } ruleStore.PutRule(context.Background(), unpausedRules...) ruleStore.PutRule(context.Background(), provisionedRules...) ruleStore.PutRule(context.Background(), pausedRules...) allRules := append(append(unpausedRules, provisionedRules...), pausedRules...) permissions := createPermissionsForRules(allRules, orgID) requestCtx := createRequestContextWithPerms(orgID, permissions, nil) svc := createServiceWithProvenanceStore(ruleStore, provisioningStore) response := svc.RouteUpdateNamespaceRules(requestCtx, apimodels.UpdateNamespaceRulesRequest{ IsPaused: util.Pointer(true), }, folder.UID) require.Equal(t, http.StatusAccepted, response.Status()) result := &apimodels.UpdateNamespaceRulesResponse{} require.NoError(t, json.Unmarshal(response.Body(), result)) require.Equal(t, "rules updated successfully", result.Message) updatedRules := getRecordedUpdatedRules(ruleStore) require.Len(t, updatedRules, len(unpausedRules)) for _, update := range updatedRules { require.True(t, update.New.IsPaused) } }) t.Run("should unpause all non-provisioned rules in namespace", func(t *testing.T) { ruleStore := initFakeRuleStore(t) provisioningStore := fakes.NewFakeProvisioningStore() // Create 3 types of rules: paused, provisioned paused, and unpaused pausedRules := gen.With(gen.WithGroupPrefix("paused-"), gen.WithIsPaused(true)).GenerateManyRef(4) unpausedRules := gen.With(gen.WithGroupPrefix("unpaused-"), gen.WithIsPaused(false)).GenerateManyRef(3) provisionedRules := gen.With( gen.WithGroupPrefix("provisioned-"), gen.WithIsPaused(false), ).GenerateManyRef(2) for _, r := range provisionedRules { err := provisioningStore.SetProvenance(context.Background(), r, orgID, models.ProvenanceAPI) require.NoError(t, err) } ruleStore.PutRule(context.Background(), pausedRules...) ruleStore.PutRule(context.Background(), provisionedRules...) ruleStore.PutRule(context.Background(), unpausedRules...) allRules := append(append(pausedRules, provisionedRules...), unpausedRules...) permissions := createPermissionsForRules(allRules, orgID) requestCtx := createRequestContextWithPerms(orgID, permissions, nil) svc := createServiceWithProvenanceStore(ruleStore, provisioningStore) response := svc.RouteUpdateNamespaceRules(requestCtx, apimodels.UpdateNamespaceRulesRequest{ IsPaused: util.Pointer(false), }, folder.UID) require.Equal(t, http.StatusAccepted, response.Status()) result := &apimodels.UpdateNamespaceRulesResponse{} require.NoError(t, json.Unmarshal(response.Body(), result)) require.Equal(t, "rules updated successfully", result.Message) updatedRules := getRecordedUpdatedRules(ruleStore) require.Len(t, updatedRules, len(pausedRules)) // all rules are now unpaused for _, update := range updatedRules { require.False(t, update.New.IsPaused) } }) t.Run("returns 202 when no rules need updating", func(t *testing.T) { ruleStore := initFakeRuleStore(t) provisioningStore := fakes.NewFakeProvisioningStore() // Create already unpaused rules rules := gen.With(gen.WithGroupPrefix("paused-"), gen.WithIsPaused(false)).GenerateManyRef(5) ruleStore.PutRule(context.Background(), rules...) permissions := createPermissionsForRules(rules, orgID) requestCtx := createRequestContextWithPerms(orgID, permissions, nil) // Create request to unpause rules (they are already unpaused) svc := createServiceWithProvenanceStore(ruleStore, provisioningStore) response := svc.RouteUpdateNamespaceRules(requestCtx, apimodels.UpdateNamespaceRulesRequest{ IsPaused: util.Pointer(false), }, folder.UID) require.Equal(t, http.StatusAccepted, response.Status()) result := &apimodels.UpdateNamespaceRulesResponse{} require.NoError(t, json.Unmarshal(response.Body(), result)) require.Equal(t, "rules updated successfully", result.Message) // Verify no rules were updated updatedRules := getRecordedUpdatedRules(ruleStore) require.Empty(t, updatedRules) }) t.Run("should return 202 with 'no rules to update in namespace' when namespace is empty", func(t *testing.T) { ruleStore := initFakeRuleStore(t) provisioningStore := fakes.NewFakeProvisioningStore() requestCtx := createRequestContextWithPerms(orgID, map[int64]map[string][]string{}, nil) svc := createServiceWithProvenanceStore(ruleStore, provisioningStore) response := svc.RouteUpdateNamespaceRules(requestCtx, apimodels.UpdateNamespaceRulesRequest{ IsPaused: util.Pointer(true), }, folder.UID) require.Equal(t, http.StatusAccepted, response.Status()) result := &apimodels.UpdateNamespaceRulesResponse{} require.NoError(t, json.Unmarshal(response.Body(), result)) require.Equal(t, "no rules to update in namespace", result.Message) updatedRules := getRecordedUpdatedRules(ruleStore) require.Empty(t, updatedRules) }) t.Run("should handle folder not found", func(t *testing.T) { ruleStore := initFakeRuleStore(t) provisioningStore := fakes.NewFakeProvisioningStore() requestCtx := createRequestContextWithPerms(orgID, map[int64]map[string][]string{}, nil) svc := createServiceWithProvenanceStore(ruleStore, provisioningStore) response := svc.RouteUpdateNamespaceRules(requestCtx, apimodels.UpdateNamespaceRulesRequest{ IsPaused: util.Pointer(true), }, "non-existent-folder-uid") require.Equal(t, http.StatusNotFound, response.Status()) updatedRules := getRecordedUpdatedRules(ruleStore) require.Empty(t, updatedRules) }) t.Run("should return 202 with no updates when the user does not see any rules", func(t *testing.T) { ruleStore := initFakeRuleStore(t) provisioningStore := fakes.NewFakeProvisioningStore() rules := gen.GenerateManyRef(2) ruleStore.PutRule(context.Background(), rules...) permissions := map[int64]map[string][]string{orgID: {}} requestCtx := createRequestContextWithPerms(orgID, permissions, nil) svc := createServiceWithProvenanceStore(ruleStore, provisioningStore) response := svc.RouteUpdateNamespaceRules(requestCtx, apimodels.UpdateNamespaceRulesRequest{ IsPaused: util.Pointer(true), }, folder.UID) require.Equal(t, http.StatusAccepted, response.Status()) result := &apimodels.UpdateNamespaceRulesResponse{} require.NoError(t, json.Unmarshal(response.Body(), result)) require.Equal(t, "no rules to update in namespace", result.Message) updatedRules := getRecordedUpdatedRules(ruleStore) require.Empty(t, updatedRules) }) }