The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/ngalert/api/api_ruler_test.go

1260 lines
49 KiB

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