Files
grafana/pkg/services/ngalert/api/api_ruler_test.go
2025-06-24 11:25:51 +02:00

1261 lines
49 KiB
Go

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)
})
}