Alerting: Move some tests to full integration tests (#108747)
* Alerting: Move some tests to full integration tests * add back two error handling tests * imports --------- Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
co-authored by
Ryan McKinley
parent
0c10600f95
commit
1716173f71
@@ -17,30 +17,23 @@ import (
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/expr"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||
"github.com/grafana/grafana/pkg/services/folder/foldertest"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/testutil"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
// note: additional integration tests are in /pkg/tests/api/alerting/api_provisioning_test.go
|
||||
|
||||
func TestIntegrationAlertRuleService(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
@@ -1989,93 +1982,6 @@ func TestDeleteRuleGroups(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationProvisiongWithFullpath(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
tracer := tracing.InitializeTracerForTest()
|
||||
inProcBus := bus.ProvideBus(tracer)
|
||||
sqlStore, cfg := db.InitTestDBWithCfg(t)
|
||||
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
|
||||
_, dashboardStore := testutil.SetupDashboardService(t, sqlStore, folderStore, cfg)
|
||||
ac := acmock.New()
|
||||
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
|
||||
fStore := folderimpl.ProvideStore(sqlStore)
|
||||
folderService := folderimpl.ProvideService(
|
||||
fStore, ac, inProcBus, dashboardStore, folderStore,
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig)
|
||||
|
||||
ruleService := createAlertRuleService(t, folderService)
|
||||
var orgID int64 = 1
|
||||
|
||||
signedInUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{
|
||||
orgID: {
|
||||
dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersAll},
|
||||
dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll},
|
||||
dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}},
|
||||
}}
|
||||
namespaceUID := "my-namespace"
|
||||
namespaceTitle := namespaceUID
|
||||
rootFolder, err := folderService.Create(context.Background(), &folder.CreateFolderCommand{
|
||||
UID: namespaceUID,
|
||||
Title: namespaceTitle,
|
||||
OrgID: orgID,
|
||||
SignedInUser: &signedInUser,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("for a rule under a root folder should set the right fullpath", func(t *testing.T) {
|
||||
r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(&signedInUser), []models.AlertRule{
|
||||
createTestRule("my-cool-group", "my-cool-group", orgID, namespaceUID),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, r, 1)
|
||||
|
||||
res, err := ruleService.GetAlertRuleWithFolderFullpath(context.Background(), &signedInUser, r[0].UID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, namespaceTitle, res.FolderFullpath)
|
||||
|
||||
res2, err := ruleService.GetAlertRuleGroupWithFolderFullpath(context.Background(), &signedInUser, namespaceUID, "my-cool-group")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, namespaceTitle, res2.FolderFullpath)
|
||||
|
||||
res3, err := ruleService.GetAlertGroupsWithFolderFullpath(context.Background(), &signedInUser, &FilterOptions{NamespaceUIDs: []string{namespaceUID}})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, namespaceTitle, res3[0].FolderFullpath)
|
||||
})
|
||||
|
||||
t.Run("for a rule under a subfolder should set the right fullpath", func(t *testing.T) {
|
||||
otherNamespaceUID := "my-other-namespace"
|
||||
otherNamespaceTitle := "my-other-namespace containing multiple //"
|
||||
_, err := folderService.Create(context.Background(), &folder.CreateFolderCommand{
|
||||
UID: otherNamespaceUID,
|
||||
Title: otherNamespaceTitle,
|
||||
OrgID: orgID,
|
||||
ParentUID: rootFolder.UID,
|
||||
SignedInUser: &signedInUser,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(&signedInUser), []models.AlertRule{
|
||||
createTestRule("my-cool-group-2", "my-cool-group-2", orgID, otherNamespaceUID),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, r, 1)
|
||||
|
||||
res, err := ruleService.GetAlertRuleWithFolderFullpath(context.Background(), &signedInUser, r[0].UID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "my-namespace/my-other-namespace containing multiple \\/\\/", res.FolderFullpath)
|
||||
|
||||
res2, err := ruleService.GetAlertRuleGroupWithFolderFullpath(context.Background(), &signedInUser, otherNamespaceUID, "my-cool-group-2")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "my-namespace/my-other-namespace containing multiple \\/\\/", res2.FolderFullpath)
|
||||
|
||||
res3, err := ruleService.GetAlertGroupsWithFolderFullpath(context.Background(), &signedInUser, &FilterOptions{NamespaceUIDs: []string{otherNamespaceUID}})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "my-namespace/my-other-namespace containing multiple \\/\\/", res3[0].FolderFullpath)
|
||||
})
|
||||
}
|
||||
|
||||
func getDeleteQueries(ruleStore *fakes.RuleStore) []fakes.GenericRecordedQuery {
|
||||
generic := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
|
||||
a, ok := cmd.(fakes.GenericRecordedQuery)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/folder/foldertest"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@@ -67,422 +68,26 @@ func TestIntegration_GetUserVisibleNamespaces(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegration_GetNamespaceByUID(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
func TestGetNamespaceByTitle(t *testing.T) {
|
||||
folderService := foldertest.NewFakeService()
|
||||
folderService.ExpectedError = dashboards.ErrFolderNotFound
|
||||
store := DBstore{
|
||||
FolderService: folderService,
|
||||
}
|
||||
_, err := store.GetNamespaceByTitle(context.Background(), "Test Folder", 1, nil, folder.RootFolderUID)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, dashboards.ErrFolderNotFound)
|
||||
|
||||
sqlStore := db.InitTestDB(t)
|
||||
cfg := setting.NewCfg()
|
||||
folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
|
||||
b := &fakeBus{}
|
||||
logger := log.New("test-dbstore")
|
||||
store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b)
|
||||
|
||||
u := &user.SignedInUser{
|
||||
UserID: 1,
|
||||
OrgID: 1,
|
||||
OrgRole: org.RoleAdmin,
|
||||
IsGrafanaAdmin: true,
|
||||
}
|
||||
|
||||
uid := uuid.NewString()
|
||||
parentUid := uuid.NewString()
|
||||
title := "folder/title"
|
||||
parentTitle := "parent-title"
|
||||
createFolder(t, store, parentUid, parentTitle, 1, "")
|
||||
createFolder(t, store, uid, title, 1, parentUid)
|
||||
|
||||
actual, err := store.GetNamespaceByUID(context.Background(), uid, 1, u)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, title, actual.Title)
|
||||
require.Equal(t, uid, actual.UID)
|
||||
require.Equal(t, title, actual.Fullpath)
|
||||
|
||||
t.Run("error when user does not have permissions", func(t *testing.T) {
|
||||
someUser := &user.SignedInUser{
|
||||
UserID: 2,
|
||||
OrgID: 1,
|
||||
OrgRole: org.RoleViewer,
|
||||
}
|
||||
_, err = store.GetNamespaceByUID(context.Background(), uid, 1, someUser)
|
||||
require.ErrorIs(t, err, dashboards.ErrFolderAccessDenied)
|
||||
})
|
||||
|
||||
t.Run("error when folder does not exist", func(t *testing.T) {
|
||||
nonExistentUID := uuid.NewString()
|
||||
_, err := store.GetNamespaceByUID(context.Background(), nonExistentUID, 1, u)
|
||||
require.ErrorIs(t, err, dashboards.ErrFolderAccessDenied)
|
||||
})
|
||||
|
||||
t.Run("when nested folders are enabled full path should be populated with correct value", func(t *testing.T) {
|
||||
store.FolderService = setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders))
|
||||
actual, err := store.GetNamespaceByUID(context.Background(), uid, 1, u)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, title, actual.Title)
|
||||
require.Equal(t, uid, actual.UID)
|
||||
require.Equal(t, "parent-title/folder\\/title", actual.Fullpath)
|
||||
})
|
||||
// note: most tests are in /pkg/tests/api/alerting/api_namespace_test.go
|
||||
}
|
||||
|
||||
func TestIntegration_GetNamespaceByTitle(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
func TestGetOrCreateNamespaceByTitle(t *testing.T) {
|
||||
store := DBstore{}
|
||||
_, err := store.GetOrCreateNamespaceByTitle(context.Background(), "", 1, nil, folder.RootFolderUID)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "title is empty")
|
||||
|
||||
sqlStore := db.InitTestDB(t)
|
||||
cfg := setting.NewCfg()
|
||||
folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
|
||||
b := &fakeBus{}
|
||||
logger := log.New("test-dbstore")
|
||||
store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b)
|
||||
store.FolderService = setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders))
|
||||
|
||||
u := &user.SignedInUser{
|
||||
UserID: 1,
|
||||
OrgID: 1,
|
||||
OrgRole: org.RoleAdmin,
|
||||
IsGrafanaAdmin: true,
|
||||
}
|
||||
|
||||
// Create parent folder
|
||||
parentUID := uuid.NewString()
|
||||
parentTitle := "parent-folder"
|
||||
createFolder(t, store, parentUID, parentTitle, 1, "")
|
||||
|
||||
// Create child folder under parent
|
||||
childUID := uuid.NewString()
|
||||
childTitle := "child-folder"
|
||||
createFolder(t, store, childUID, childTitle, 1, parentUID)
|
||||
|
||||
// Create another folder with same title but under root
|
||||
sameTitleInRoot := uuid.NewString()
|
||||
createFolder(t, store, sameTitleInRoot, childTitle, 1, "")
|
||||
|
||||
t.Run("should find folder by title and parent UID", func(t *testing.T) {
|
||||
actual, err := store.GetNamespaceByTitle(context.Background(), childTitle, 1, u, parentUID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, childTitle, actual.Title)
|
||||
require.Equal(t, childUID, actual.UID)
|
||||
require.Equal(t, parentUID, actual.ParentUID)
|
||||
})
|
||||
|
||||
t.Run("should find folder by title in root", func(t *testing.T) {
|
||||
actual, err := store.GetNamespaceByTitle(context.Background(), childTitle, 1, u, folder.RootFolderUID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, childTitle, actual.Title)
|
||||
require.Equal(t, sameTitleInRoot, actual.UID)
|
||||
require.Equal(t, folder.RootFolderUID, actual.ParentUID)
|
||||
})
|
||||
|
||||
t.Run("should return ErrFolderNotFound when folder with title doesn't exist under specified parent", func(t *testing.T) {
|
||||
nonExistentTitle := "non-existent-folder"
|
||||
f, err := store.GetNamespaceByTitle(context.Background(), nonExistentTitle, 1, u, parentUID)
|
||||
require.Nil(t, f)
|
||||
require.ErrorIs(t, err, dashboards.ErrFolderNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegration_GetOrCreateNamespaceByTitle(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
u := &user.SignedInUser{
|
||||
UserID: 1,
|
||||
OrgID: 1,
|
||||
OrgRole: org.RoleAdmin,
|
||||
IsGrafanaAdmin: true,
|
||||
}
|
||||
|
||||
setupStore := func(t *testing.T) *DBstore {
|
||||
sqlStore := db.InitTestDB(t)
|
||||
cfg := setting.NewCfg()
|
||||
folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
|
||||
b := &fakeBus{}
|
||||
logger := log.New("test-dbstore")
|
||||
store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b)
|
||||
store.FolderService = setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders))
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
t.Run("should return error when title is empty", func(t *testing.T) {
|
||||
store := setupStore(t)
|
||||
_, err := store.GetOrCreateNamespaceByTitle(context.Background(), "", 1, u, folder.RootFolderUID)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "title is empty")
|
||||
})
|
||||
|
||||
t.Run("should create folder when it does not exist", func(t *testing.T) {
|
||||
store := setupStore(t)
|
||||
|
||||
f, err := store.GetOrCreateNamespaceByTitle(context.Background(), "new folder", 1, u, folder.RootFolderUID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "new folder", f.Title)
|
||||
require.NotEmpty(t, f.UID)
|
||||
require.Equal(t, folder.RootFolderUID, f.ParentUID)
|
||||
|
||||
folders, err := store.FolderService.GetFolders(
|
||||
context.Background(),
|
||||
folder.GetFoldersQuery{
|
||||
OrgID: 1,
|
||||
WithFullpath: true,
|
||||
SignedInUser: u,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, folders, 1)
|
||||
})
|
||||
|
||||
t.Run("should return existing folder when it exists", func(t *testing.T) {
|
||||
store := setupStore(t)
|
||||
|
||||
title := "existing folder"
|
||||
createFolder(t, store, "", title, 1, "")
|
||||
f, err := store.GetOrCreateNamespaceByTitle(context.Background(), title, 1, u, folder.RootFolderUID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, title, f.Title)
|
||||
|
||||
folders, err := store.FolderService.GetFolders(
|
||||
context.Background(),
|
||||
folder.GetFoldersQuery{
|
||||
OrgID: 1,
|
||||
WithFullpath: true,
|
||||
SignedInUser: u,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, folders, 1)
|
||||
})
|
||||
|
||||
t.Run("should create folder under specified parent when it does not exist", func(t *testing.T) {
|
||||
store := setupStore(t)
|
||||
|
||||
// Create parent folder first
|
||||
parentTitle := "parent folder"
|
||||
parentFolder, err := store.GetOrCreateNamespaceByTitle(context.Background(), parentTitle, 1, u, folder.RootFolderUID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Now create a child folder under the parent
|
||||
childTitle := "child folder"
|
||||
childFolder, err := store.GetOrCreateNamespaceByTitle(context.Background(), childTitle, 1, u, parentFolder.UID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the child folder was created under the parent
|
||||
folders, err := store.FolderService.GetChildren(context.Background(), &folder.GetChildrenQuery{UID: parentFolder.UID, OrgID: 1, SignedInUser: u})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, folders, 1)
|
||||
require.Equal(t, childFolder.UID, folders[0].UID)
|
||||
|
||||
folders, err = store.FolderService.GetChildren(context.Background(), &folder.GetChildrenQuery{UID: folder.RootFolderUID, OrgID: 1, SignedInUser: u})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, folders, 1)
|
||||
require.Equal(t, parentFolder.UID, folders[0].UID)
|
||||
})
|
||||
|
||||
t.Run("should get correct folder when same title exists under different parents", func(t *testing.T) {
|
||||
store := setupStore(t)
|
||||
|
||||
// Create first parent folder
|
||||
parent1Title := "parent folder 1"
|
||||
parent1, err := store.GetOrCreateNamespaceByTitle(context.Background(), parent1Title, 1, u, folder.RootFolderUID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create second parent folder
|
||||
parent2Title := "parent folder 2"
|
||||
parent2, err := store.GetOrCreateNamespaceByTitle(context.Background(), parent2Title, 1, u, folder.RootFolderUID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create folders with same title under different parents
|
||||
sameTitle := "same title folder"
|
||||
|
||||
// Create under first parent
|
||||
folder1, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, parent1.UID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create under second parent
|
||||
folder2, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, parent2.UID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create under root
|
||||
folder3, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, folder.RootFolderUID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify we get the correct folders when specifying the parent
|
||||
gotFolder1, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, parent1.UID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, folder1.UID, gotFolder1.UID)
|
||||
require.Equal(t, parent1.UID, gotFolder1.ParentUID)
|
||||
|
||||
gotFolder2, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, parent2.UID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, folder2.UID, gotFolder2.UID)
|
||||
require.Equal(t, parent2.UID, gotFolder2.ParentUID)
|
||||
|
||||
gotFolder3, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, folder.RootFolderUID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, folder3.UID, gotFolder3.UID)
|
||||
require.Equal(t, folder.RootFolderUID, gotFolder3.ParentUID)
|
||||
})
|
||||
|
||||
t.Run("should create folder with deterministic UID and handle race conditions", func(t *testing.T) {
|
||||
store := setupStore(t)
|
||||
|
||||
folderTitle := "race condition test folder"
|
||||
parentUID := folder.RootFolderUID
|
||||
|
||||
// Calculate the expected UID that would be generated
|
||||
expectedUID, err := generateAlertingFolderUID(folderTitle, parentUID, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a folder first, simulating another concurrent call that succeeded first
|
||||
createFolder(t, store, expectedUID, folderTitle, 1, parentUID)
|
||||
|
||||
// Now try to create a folder with the same title and parent
|
||||
// This should not create a duplicate folder but return the existing one
|
||||
f, err := store.GetOrCreateNamespaceByTitle(context.Background(), folderTitle, 1, u, parentUID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedUID, f.UID, "Should return folder with same UID as would be deterministically generated")
|
||||
require.Equal(t, folderTitle, f.Title)
|
||||
require.Equal(t, parentUID, f.ParentUID)
|
||||
|
||||
// Verify only one folder was created
|
||||
folders, err := store.FolderService.GetFolders(
|
||||
context.Background(),
|
||||
folder.GetFoldersQuery{
|
||||
OrgID: 1,
|
||||
WithFullpath: true,
|
||||
SignedInUser: u,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, folders, 1, "Only one folder should exist")
|
||||
})
|
||||
|
||||
t.Run("should handle special characters in folder titles", func(t *testing.T) {
|
||||
store := setupStore(t)
|
||||
specialTitles := []string{
|
||||
"folder/with/slashes",
|
||||
"folder with spaces",
|
||||
"folder-with-dashes",
|
||||
"folder_with_underscores",
|
||||
"folder.with.dots",
|
||||
"!@#$%^&*()",
|
||||
}
|
||||
|
||||
for _, title := range specialTitles {
|
||||
t.Run(title, func(t *testing.T) {
|
||||
f, err := store.GetOrCreateNamespaceByTitle(context.Background(), title, 1, u, folder.RootFolderUID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, title, f.Title)
|
||||
|
||||
// Verify retrieval works
|
||||
retrieved, err := store.GetNamespaceByTitle(context.Background(), title, 1, u, folder.RootFolderUID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, f.UID, retrieved.UID)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegration_GetNamespaceChildren(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
sqlStore := db.InitTestDB(t)
|
||||
cfg := setting.NewCfg()
|
||||
folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
|
||||
b := &fakeBus{}
|
||||
logger := log.New("test-dbstore")
|
||||
store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b)
|
||||
store.FolderService = setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders))
|
||||
|
||||
admin := &user.SignedInUser{
|
||||
UserID: 1,
|
||||
OrgID: 1,
|
||||
OrgRole: org.RoleAdmin,
|
||||
IsGrafanaAdmin: true,
|
||||
}
|
||||
|
||||
// Create root folders
|
||||
rootFolder1 := uuid.NewString()
|
||||
rootFolder2 := uuid.NewString()
|
||||
createFolder(t, store, rootFolder1, "Root Folder 1", 1, "")
|
||||
createFolder(t, store, rootFolder2, "Root Folder 2", 1, "")
|
||||
|
||||
// Create child folders under root folder 1
|
||||
child1 := uuid.NewString()
|
||||
child2 := uuid.NewString()
|
||||
createFolder(t, store, child1, "Child Folder 1", 1, rootFolder1)
|
||||
createFolder(t, store, child2, "Child Folder 2", 1, rootFolder1)
|
||||
|
||||
// Create nested child under child1
|
||||
nestedChild := uuid.NewString()
|
||||
createFolder(t, store, nestedChild, "Nested Child", 1, child1)
|
||||
|
||||
differentOrgID := int64(999)
|
||||
createFolder(t, store, util.GenerateShortUID(), "Root Folder 1", differentOrgID, "")
|
||||
|
||||
/*
|
||||
* Folder structure:
|
||||
*
|
||||
* Root Folder 1
|
||||
* - Child Folder 1
|
||||
* - Nested Child
|
||||
* - Child Folder 2
|
||||
* Root Folder 2
|
||||
*/
|
||||
|
||||
t.Run("should return direct children of a folder", func(t *testing.T) {
|
||||
children, err := store.GetNamespaceChildren(context.Background(), rootFolder1, 1, admin)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, children, 2)
|
||||
|
||||
require.ElementsMatch(t, []string{child1, child2}, []string{children[0].UID, children[1].UID})
|
||||
|
||||
// Verify parent UID
|
||||
for _, child := range children {
|
||||
require.Equal(t, rootFolder1, child.ParentUID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should return direct children of a nested folder", func(t *testing.T) {
|
||||
children, err := store.GetNamespaceChildren(context.Background(), child1, 1, admin)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, children, 1)
|
||||
require.Equal(t, nestedChild, children[0].UID)
|
||||
require.Equal(t, child1, children[0].ParentUID)
|
||||
})
|
||||
|
||||
t.Run("should return nil when folder does not exist", func(t *testing.T) {
|
||||
nonExistentUID := uuid.NewString()
|
||||
children, err := store.GetNamespaceChildren(context.Background(), nonExistentUID, 1, admin)
|
||||
require.NotNil(t, children)
|
||||
require.Empty(t, children)
|
||||
require.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("should return empty array for folders with no children", func(t *testing.T) {
|
||||
children, err := store.GetNamespaceChildren(context.Background(), rootFolder2, 1, admin)
|
||||
require.Empty(t, children)
|
||||
require.NotNil(t, children)
|
||||
require.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("should return no children for a different org", func(t *testing.T) {
|
||||
children, err := store.GetNamespaceChildren(context.Background(), rootFolder1, differentOrgID, admin)
|
||||
require.Empty(t, children)
|
||||
require.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("should return children from root folder", func(t *testing.T) {
|
||||
children, err := store.GetNamespaceChildren(context.Background(), "", 1, admin)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(children), 2)
|
||||
require.ElementsMatch(t, []string{rootFolder1, rootFolder2}, []string{children[0].UID, children[1].UID})
|
||||
})
|
||||
// note: most tests are in /pkg/tests/api/alerting/api_namespace_test.go
|
||||
}
|
||||
|
||||
func TestGenerateAlertingFolderUID(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,478 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"bytes"
|
||||
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/org/orgimpl"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotaimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||
)
|
||||
|
||||
func TestIntegration_NamespacingForRules(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
})
|
||||
|
||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
|
||||
store, cfg := env.SQLStore, env.Cfg
|
||||
orgID := int64(1)
|
||||
createUser(t, store, cfg, user.CreateUserCommand{
|
||||
DefaultOrgRole: string(org.RoleEditor),
|
||||
OrgID: orgID,
|
||||
Password: "editor",
|
||||
Login: "editor",
|
||||
})
|
||||
createUser(t, store, cfg, user.CreateUserCommand{
|
||||
DefaultOrgRole: string(org.RoleViewer),
|
||||
OrgID: orgID,
|
||||
Password: "viewer",
|
||||
Login: "viewer",
|
||||
})
|
||||
|
||||
adminClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
|
||||
editorClient := newAlertingApiClient(grafanaListedAddr, "editor", "editor")
|
||||
viewerClient := newAlertingApiClient(grafanaListedAddr, "viewer", "viewer")
|
||||
|
||||
// create test folders, with a rule in each folder
|
||||
folder1UID := "test-folder-1"
|
||||
folder2UID := "test-folder-2"
|
||||
folder3UID := "test-folder-3"
|
||||
adminClient.CreateFolder(t, folder1UID, "Test Folder 1")
|
||||
adminClient.CreateFolder(t, folder2UID, "Test Folder 2")
|
||||
adminClient.CreateFolder(t, folder3UID, "Test Folder 3")
|
||||
rule1 := createTestAlertRule("Test Rule 1", folder1UID)
|
||||
rule2 := createTestAlertRule("Test Rule 2", folder2UID)
|
||||
rule3 := createTestAlertRule("Test Rule 3", folder3UID)
|
||||
group1 := apimodels.PostableRuleGroupConfig{
|
||||
Name: "test-group-1",
|
||||
Interval: 60,
|
||||
Rules: []apimodels.PostableExtendedRuleNode{rule1},
|
||||
}
|
||||
group2 := apimodels.PostableRuleGroupConfig{
|
||||
Name: "test-group-2",
|
||||
Interval: 60,
|
||||
Rules: []apimodels.PostableExtendedRuleNode{rule2},
|
||||
}
|
||||
group3 := apimodels.PostableRuleGroupConfig{
|
||||
Name: "test-group-3",
|
||||
Interval: 60,
|
||||
Rules: []apimodels.PostableExtendedRuleNode{rule3},
|
||||
}
|
||||
adminClient.PostRulesGroup(t, folder1UID, &group1, false)
|
||||
adminClient.PostRulesGroup(t, folder2UID, &group2, false)
|
||||
adminClient.PostRulesGroup(t, folder3UID, &group3, false)
|
||||
|
||||
t.Run("admin, editor, and viewer should be able to see all rules in a given folder", func(t *testing.T) {
|
||||
// admin
|
||||
rules, status, _ := adminClient.GetAllRulesGroupInFolderWithStatus(t, folder1UID)
|
||||
require.Equal(t, http.StatusAccepted, status)
|
||||
require.Len(t, rules, 1)
|
||||
|
||||
// editor
|
||||
rules, status, _ = editorClient.GetAllRulesGroupInFolderWithStatus(t, folder1UID)
|
||||
require.Equal(t, http.StatusAccepted, status)
|
||||
require.Len(t, rules, 1)
|
||||
|
||||
// viewer
|
||||
rules, status, _ = viewerClient.GetAllRulesGroupInFolderWithStatus(t, folder1UID)
|
||||
require.Equal(t, http.StatusAccepted, status)
|
||||
require.Len(t, rules, 1)
|
||||
})
|
||||
|
||||
t.Run("admin should be able to access restricted folder, but no one else can", func(t *testing.T) {
|
||||
restrictedFolderUID := "restricted-folder"
|
||||
adminClient.CreateFolder(t, restrictedFolderUID, "Restricted Folder")
|
||||
restrictedRule := createTestAlertRule("Restricted Rule", restrictedFolderUID)
|
||||
restrictedGroup := apimodels.PostableRuleGroupConfig{
|
||||
Name: "restricted-group",
|
||||
Interval: 60,
|
||||
Rules: []apimodels.PostableExtendedRuleNode{restrictedRule},
|
||||
}
|
||||
adminClient.PostRulesGroup(t, restrictedFolderUID, &restrictedGroup, false)
|
||||
setFolderPermissions(t, grafanaListedAddr, restrictedFolderUID, []map[string]interface{}{
|
||||
{
|
||||
"userId": 1,
|
||||
"permission": 4,
|
||||
},
|
||||
})
|
||||
|
||||
// admin ok
|
||||
_, status, _ := adminClient.GetAllRulesGroupInFolderWithStatus(t, restrictedFolderUID)
|
||||
require.Equal(t, http.StatusAccepted, status)
|
||||
|
||||
// editor and viewer forbidden
|
||||
_, status, _ = editorClient.GetAllRulesGroupInFolderWithStatus(t, restrictedFolderUID)
|
||||
require.Equal(t, http.StatusForbidden, status)
|
||||
|
||||
_, status, _ = viewerClient.GetAllRulesGroupInFolderWithStatus(t, restrictedFolderUID)
|
||||
require.Equal(t, http.StatusForbidden, status)
|
||||
})
|
||||
|
||||
t.Run("errors when a folder does not exist", func(t *testing.T) {
|
||||
_, status, _ := adminClient.GetAllRulesGroupInFolderWithStatus(t, "non-existent-folder")
|
||||
// even if a folder does not exist, it will return a forbidden error (so users cannot enumerate folders)
|
||||
require.Equal(t, http.StatusForbidden, status)
|
||||
})
|
||||
|
||||
t.Run("permissions are respected for nested folders", func(t *testing.T) {
|
||||
parentFolderUID := "parent-folder"
|
||||
childFolderUID := "child-folder"
|
||||
adminClient.CreateFolder(t, parentFolderUID, "Parent Folder")
|
||||
adminClient.CreateFolder(t, childFolderUID, "Child Folder", parentFolderUID)
|
||||
parentRule := createTestAlertRule("Parent Rule", parentFolderUID)
|
||||
parentGroup := apimodels.PostableRuleGroupConfig{
|
||||
Name: "parent-group",
|
||||
Interval: 60,
|
||||
Rules: []apimodels.PostableExtendedRuleNode{parentRule},
|
||||
}
|
||||
adminClient.PostRulesGroup(t, parentFolderUID, &parentGroup, false)
|
||||
childRule := createTestAlertRule("Child Rule", childFolderUID)
|
||||
childGroup := apimodels.PostableRuleGroupConfig{
|
||||
Name: "child-group",
|
||||
Interval: 60,
|
||||
Rules: []apimodels.PostableExtendedRuleNode{childRule},
|
||||
}
|
||||
adminClient.PostRulesGroup(t, childFolderUID, &childGroup, false)
|
||||
|
||||
// allow admin to access parent folder
|
||||
setFolderPermissions(t, grafanaListedAddr, parentFolderUID, []map[string]interface{}{
|
||||
{
|
||||
"userId": 1,
|
||||
"permission": 4,
|
||||
},
|
||||
})
|
||||
|
||||
// admin can get both folders
|
||||
allRules, status, _ := adminClient.GetAllRulesWithStatus(t)
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
require.Contains(t, allRules, "Parent Folder")
|
||||
require.Contains(t, allRules, "Parent Folder/Child Folder")
|
||||
|
||||
// editor cannot access either
|
||||
allRules, status, _ = editorClient.GetAllRulesWithStatus(t)
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
require.NotContains(t, allRules, "Parent Folder")
|
||||
require.NotContains(t, allRules, "Parent Folder/Child Folder")
|
||||
|
||||
// viewer cannot access either folder
|
||||
allRules, status, _ = viewerClient.GetAllRulesWithStatus(t)
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
require.NotContains(t, allRules, "Parent Folder")
|
||||
require.NotContains(t, allRules, "Parent Folder/Child Folder")
|
||||
})
|
||||
|
||||
t.Run("org separation", func(t *testing.T) {
|
||||
orgService, err := orgimpl.ProvideService(store, cfg, quotaimpl.ProvideService(store, cfg))
|
||||
require.NoError(t, err)
|
||||
newOrg, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "Test Org 2"})
|
||||
require.NoError(t, err)
|
||||
createUser(t, store, cfg, user.CreateUserCommand{
|
||||
DefaultOrgRole: string(org.RoleAdmin),
|
||||
OrgID: newOrg.ID,
|
||||
Password: "other-admin",
|
||||
Login: "other-admin-folder-perms",
|
||||
})
|
||||
otherOrgFolderUID := "other-org-folder"
|
||||
otherOrgClient := newAlertingApiClient(grafanaListedAddr, "other-admin-folder-perms", "other-admin")
|
||||
otherOrgClient.CreateFolder(t, otherOrgFolderUID, "Other Org Folder")
|
||||
otherOrgRule := createTestAlertRule("Other Org Rule", otherOrgFolderUID)
|
||||
otherOrgGroup := apimodels.PostableRuleGroupConfig{
|
||||
Name: "other-org-group",
|
||||
Interval: 60,
|
||||
Rules: []apimodels.PostableExtendedRuleNode{otherOrgRule},
|
||||
}
|
||||
otherOrgClient.PostRulesGroup(t, otherOrgFolderUID, &otherOrgGroup, false)
|
||||
|
||||
// admin from org 1 cannot access org 2 alert rules
|
||||
allRules, status, _ := adminClient.GetAllRulesWithStatus(t)
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
require.NotContains(t, allRules, otherOrgFolderUID)
|
||||
|
||||
// admin from org 2 cannot access org 1 alert rules
|
||||
allRules, status, _ = otherOrgClient.GetAllRulesWithStatus(t)
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
require.NotContains(t, allRules, folder1UID)
|
||||
require.NotContains(t, allRules, folder2UID)
|
||||
require.NotContains(t, allRules, folder3UID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegration_NamespacingForPrometheusRules(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
})
|
||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
|
||||
store, cfg := env.SQLStore, env.Cfg
|
||||
orgID := int64(1)
|
||||
createUser(t, store, cfg, user.CreateUserCommand{
|
||||
DefaultOrgRole: string(org.RoleEditor),
|
||||
OrgID: orgID,
|
||||
Password: "editor",
|
||||
Login: "editor",
|
||||
})
|
||||
createUser(t, store, cfg, user.CreateUserCommand{
|
||||
DefaultOrgRole: string(org.RoleViewer),
|
||||
OrgID: orgID,
|
||||
Password: "viewer",
|
||||
Login: "viewer",
|
||||
})
|
||||
adminClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
|
||||
editorClient := newAlertingApiClient(grafanaListedAddr, "editor", "editor")
|
||||
viewerClient := newAlertingApiClient(grafanaListedAddr, "viewer", "viewer")
|
||||
|
||||
// create prometheus rules in 3 separate folders
|
||||
ds := adminClient.CreateDatasource(t, "prometheus")
|
||||
dsUID := ds.Body.Datasource.UID
|
||||
folder1UID := "prometheus-folder-1"
|
||||
folder2UID := "prometheus-folder-2"
|
||||
folder3UID := "prometheus-folder-3"
|
||||
adminClient.CreateFolder(t, folder1UID, "Prometheus Folder 1")
|
||||
adminClient.CreateFolder(t, folder2UID, "Prometheus Folder 2")
|
||||
adminClient.CreateFolder(t, folder3UID, "Prometheus Folder 3")
|
||||
duration1m := model.Duration(1 * 60 * 1000)
|
||||
prometheusRules1 := map[string][]apimodels.PrometheusRuleGroup{
|
||||
"Prometheus Folder 1": {
|
||||
{
|
||||
Name: "test-group-1",
|
||||
Rules: []apimodels.PrometheusRule{
|
||||
{
|
||||
Alert: "HighCPUUsage",
|
||||
Expr: "cpu_usage > 80",
|
||||
For: &duration1m,
|
||||
Labels: map[string]string{
|
||||
"severity": "warning",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"summary": "High CPU usage detected",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
prometheusRules2 := map[string][]apimodels.PrometheusRuleGroup{
|
||||
"Prometheus Folder 2": {
|
||||
{
|
||||
Name: "test-group-2",
|
||||
Rules: []apimodels.PrometheusRule{
|
||||
{
|
||||
Alert: "HighMemoryUsage",
|
||||
Expr: "memory_usage > 90",
|
||||
For: &duration1m,
|
||||
Labels: map[string]string{
|
||||
"severity": "critical",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"summary": "High memory usage detected",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
prometheusRules3 := map[string][]apimodels.PrometheusRuleGroup{
|
||||
"Prometheus Folder 3": {
|
||||
{
|
||||
Name: "test-group-3",
|
||||
Rules: []apimodels.PrometheusRule{
|
||||
{
|
||||
Alert: "DiskSpaceLow",
|
||||
Expr: "disk_usage > 95",
|
||||
For: &duration1m,
|
||||
Labels: map[string]string{
|
||||
"severity": "warning",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"summary": "Disk space is running low",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
// then import them
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"X-Datasource-UID": dsUID,
|
||||
}
|
||||
adminClient.ConvertPrometheusPostRuleGroups(t, dsUID, prometheusRules1, headers)
|
||||
adminClient.ConvertPrometheusPostRuleGroups(t, dsUID, prometheusRules2, headers)
|
||||
adminClient.ConvertPrometheusPostRuleGroups(t, dsUID, prometheusRules3, headers)
|
||||
|
||||
t.Run("admin, editor, and viewer should be able to get all Prometheus rules", func(t *testing.T) {
|
||||
// admin
|
||||
rules := adminClient.ConvertPrometheusGetAllRules(t, headers)
|
||||
require.Len(t, rules, 3)
|
||||
require.Contains(t, rules, "Prometheus Folder 1")
|
||||
require.Contains(t, rules, "Prometheus Folder 2")
|
||||
require.Contains(t, rules, "Prometheus Folder 3")
|
||||
|
||||
// editor
|
||||
rules = editorClient.ConvertPrometheusGetAllRules(t, headers)
|
||||
require.Len(t, rules, 3)
|
||||
require.Contains(t, rules, "Prometheus Folder 1")
|
||||
require.Contains(t, rules, "Prometheus Folder 2")
|
||||
require.Contains(t, rules, "Prometheus Folder 3")
|
||||
|
||||
// viewer
|
||||
rules = viewerClient.ConvertPrometheusGetAllRules(t, headers)
|
||||
require.Len(t, rules, 3)
|
||||
require.Contains(t, rules, "Prometheus Folder 1")
|
||||
require.Contains(t, rules, "Prometheus Folder 2")
|
||||
require.Contains(t, rules, "Prometheus Folder 3")
|
||||
})
|
||||
|
||||
t.Run("only admin can view restricted folder", func(t *testing.T) {
|
||||
restrictedFolderUID := "restricted-prometheus-folder"
|
||||
adminClient.CreateFolder(t, restrictedFolderUID, "Restricted Prometheus Folder")
|
||||
restrictedPrometheusRules := map[string][]apimodels.PrometheusRuleGroup{
|
||||
"Restricted Prometheus Folder": {
|
||||
{
|
||||
Name: "restricted-group",
|
||||
Rules: []apimodels.PrometheusRule{
|
||||
{
|
||||
Alert: "RestrictedAlert",
|
||||
Expr: "restricted_metric > 100",
|
||||
For: &duration1m,
|
||||
Labels: map[string]string{
|
||||
"severity": "critical",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
adminClient.ConvertPrometheusPostRuleGroups(t, dsUID, restrictedPrometheusRules, headers)
|
||||
setFolderPermissions(t, grafanaListedAddr, restrictedFolderUID, []map[string]interface{}{
|
||||
{
|
||||
"userId": 1,
|
||||
"permission": 4,
|
||||
},
|
||||
})
|
||||
|
||||
// admin can see the restricted folder
|
||||
rules := adminClient.ConvertPrometheusGetAllRules(t, headers)
|
||||
require.Contains(t, rules, "Restricted Prometheus Folder")
|
||||
|
||||
// editor and viewer cannot
|
||||
rules = editorClient.ConvertPrometheusGetAllRules(t, headers)
|
||||
require.NotContains(t, rules, "Restricted Prometheus Folder")
|
||||
rules = viewerClient.ConvertPrometheusGetAllRules(t, headers)
|
||||
require.NotContains(t, rules, "Restricted Prometheus Folder")
|
||||
})
|
||||
|
||||
t.Run("should maintain org separation for Prometheus rules", func(t *testing.T) {
|
||||
orgService, err := orgimpl.ProvideService(store, cfg, quotaimpl.ProvideService(store, cfg))
|
||||
require.NoError(t, err)
|
||||
newOrg, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "Prometheus Test Org 2"})
|
||||
require.NoError(t, err)
|
||||
createUser(t, store, cfg, user.CreateUserCommand{
|
||||
DefaultOrgRole: string(org.RoleAdmin),
|
||||
OrgID: newOrg.ID,
|
||||
Password: "other-prometheus-admin",
|
||||
Login: "other-prometheus-admin",
|
||||
})
|
||||
otherOrgClient := newAlertingApiClient(grafanaListedAddr, "other-prometheus-admin", "other-prometheus-admin")
|
||||
otherOrgDs := otherOrgClient.CreateDatasource(t, "prometheus")
|
||||
otherOrgDsUID := otherOrgDs.Body.Datasource.UID
|
||||
otherOrgFolderUID := "other-org-prometheus-folder"
|
||||
otherOrgClient.CreateFolder(t, otherOrgFolderUID, "Other Org Prometheus Folder")
|
||||
otherOrgPrometheusRules := map[string][]apimodels.PrometheusRuleGroup{
|
||||
"Other Org Prometheus Folder": {
|
||||
{
|
||||
Name: "other-org-group",
|
||||
Rules: []apimodels.PrometheusRule{
|
||||
{
|
||||
Alert: "OtherOrgAlert",
|
||||
Expr: "other_org_metric > 75",
|
||||
For: &duration1m,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
otherOrgHeaders := map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"X-Datasource-UID": otherOrgDsUID,
|
||||
}
|
||||
otherOrgClient.ConvertPrometheusPostRuleGroups(t, otherOrgDsUID, otherOrgPrometheusRules, otherOrgHeaders)
|
||||
|
||||
// admin from org 1 cannot see org 2 rules
|
||||
rules := adminClient.ConvertPrometheusGetAllRules(t, headers)
|
||||
require.NotContains(t, rules, "Other Org Prometheus Folder")
|
||||
|
||||
// admin from org 2 cannot see org 1 rules
|
||||
rules = otherOrgClient.ConvertPrometheusGetAllRules(t, otherOrgHeaders)
|
||||
require.NotContains(t, rules, "Prometheus Folder 1")
|
||||
require.NotContains(t, rules, "Prometheus Folder 2")
|
||||
require.NotContains(t, rules, "Prometheus Folder 3")
|
||||
require.Contains(t, rules, "Other Org Prometheus Folder")
|
||||
})
|
||||
}
|
||||
|
||||
func createTestAlertRule(title, folderUID string) apimodels.PostableExtendedRuleNode {
|
||||
return apimodels.PostableExtendedRuleNode{
|
||||
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
||||
Title: title,
|
||||
Condition: "A",
|
||||
Data: []apimodels.AlertQuery{
|
||||
{
|
||||
RefID: "A",
|
||||
RelativeTimeRange: apimodels.RelativeTimeRange{
|
||||
From: 600,
|
||||
To: 0,
|
||||
},
|
||||
DatasourceUID: "-100",
|
||||
Model: json.RawMessage(`{
|
||||
"type": "math",
|
||||
"expression": "2 + 3 > 1"
|
||||
}`),
|
||||
},
|
||||
},
|
||||
NoDataState: "NoData",
|
||||
ExecErrState: "Error",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func setFolderPermissions(t *testing.T, grafanaListedAddr string, folderUID string, permissions []map[string]interface{}) {
|
||||
t.Helper()
|
||||
|
||||
permissionPayload := map[string]interface{}{
|
||||
"items": permissions,
|
||||
}
|
||||
|
||||
payloadBytes, err := json.Marshal(permissionPayload)
|
||||
require.NoError(t, err)
|
||||
|
||||
u := fmt.Sprintf("http://admin:admin@%s/api/folders/%s/permissions", grafanaListedAddr, folderUID)
|
||||
resp, err := http.Post(u, "application/json", bytes.NewBuffer(payloadBytes)) // nolint:gosec
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -1184,3 +1184,128 @@ func TestIntegrationExportFileProvisionContactPoints(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationFullpath(t *testing.T) {
|
||||
dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
})
|
||||
|
||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
|
||||
|
||||
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
|
||||
DefaultOrgRole: string(org.RoleEditor),
|
||||
Password: "password",
|
||||
Login: "grafana",
|
||||
})
|
||||
|
||||
apiClient := newAlertingApiClient(grafanaListedAddr, "grafana", "password")
|
||||
|
||||
namespaceUID := "my-namespace"
|
||||
namespaceTitle := namespaceUID
|
||||
apiClient.CreateFolder(t, namespaceUID, namespaceTitle)
|
||||
|
||||
t.Run("for a rule under a root folder should set the right fullpath", func(t *testing.T) {
|
||||
interval, err := model.ParseDuration("1m")
|
||||
require.NoError(t, err)
|
||||
doubleInterval := 2 * interval
|
||||
rules := definitions.PostableRuleGroupConfig{
|
||||
Name: "group",
|
||||
Interval: interval,
|
||||
Rules: []definitions.PostableExtendedRuleNode{
|
||||
{
|
||||
ApiRuleNode: &definitions.ApiRuleNode{
|
||||
For: &doubleInterval,
|
||||
Labels: map[string]string{"label1": "val1"},
|
||||
Annotations: map[string]string{"annotation1": "val1"},
|
||||
},
|
||||
GrafanaManagedAlert: &definitions.PostableGrafanaRule{
|
||||
Title: "rule",
|
||||
Condition: "A",
|
||||
Data: []definitions.AlertQuery{
|
||||
{
|
||||
RefID: "A",
|
||||
RelativeTimeRange: definitions.RelativeTimeRange{
|
||||
From: definitions.Duration(time.Duration(5) * time.Hour),
|
||||
To: definitions.Duration(time.Duration(3) * time.Hour),
|
||||
},
|
||||
DatasourceUID: expr.DatasourceUID,
|
||||
Model: json.RawMessage(`{
|
||||
"type": "math",
|
||||
"expression": "2 + 3 > 1"
|
||||
}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, status, _ := apiClient.PostRulesGroupWithStatus(t, namespaceUID, &rules, false)
|
||||
require.Equal(t, http.StatusAccepted, status)
|
||||
require.Len(t, resp.Created, 1)
|
||||
ruleUID := resp.Created[0]
|
||||
|
||||
status, response := apiClient.GetProvisioningAlertRuleExport(t, ruleUID, &definitions.ExportQueryParams{Format: "json"})
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
var export definitions.AlertingFileExport
|
||||
require.NoError(t, json.Unmarshal([]byte(response), &export))
|
||||
require.Len(t, export.Groups, 1)
|
||||
assert.Equal(t, "", export.Groups[0].Folder)
|
||||
})
|
||||
|
||||
t.Run("for a rule under a subfolder should set the right fullpath", func(t *testing.T) {
|
||||
otherNamespaceUID := "my-other-namespace"
|
||||
otherNamespaceTitle := "my-other-namespace containing multiple //"
|
||||
apiClient.CreateFolder(t, otherNamespaceUID, otherNamespaceTitle, namespaceUID)
|
||||
|
||||
interval, err := model.ParseDuration("1m")
|
||||
require.NoError(t, err)
|
||||
doubleInterval := 2 * interval
|
||||
rules := definitions.PostableRuleGroupConfig{
|
||||
Name: "group-2",
|
||||
Interval: interval,
|
||||
Rules: []definitions.PostableExtendedRuleNode{
|
||||
{
|
||||
ApiRuleNode: &definitions.ApiRuleNode{
|
||||
For: &doubleInterval,
|
||||
Labels: map[string]string{"label1": "val1"},
|
||||
Annotations: map[string]string{"annotation1": "val1"},
|
||||
},
|
||||
GrafanaManagedAlert: &definitions.PostableGrafanaRule{
|
||||
Title: "rule-2",
|
||||
Condition: "A",
|
||||
Data: []definitions.AlertQuery{
|
||||
{
|
||||
RefID: "A",
|
||||
RelativeTimeRange: definitions.RelativeTimeRange{
|
||||
From: definitions.Duration(time.Duration(5) * time.Hour),
|
||||
To: definitions.Duration(time.Duration(3) * time.Hour),
|
||||
},
|
||||
DatasourceUID: expr.DatasourceUID,
|
||||
Model: json.RawMessage(`{
|
||||
"type": "math",
|
||||
"expression": "2 + 3 > 1"
|
||||
}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, status, _ := apiClient.PostRulesGroupWithStatus(t, otherNamespaceUID, &rules, false)
|
||||
require.Equal(t, http.StatusAccepted, status)
|
||||
require.Len(t, resp.Created, 1)
|
||||
ruleUID := resp.Created[0]
|
||||
|
||||
status, response := apiClient.GetProvisioningAlertRuleExport(t, ruleUID, &definitions.ExportQueryParams{Format: "json"})
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
var export definitions.AlertingFileExport
|
||||
require.NoError(t, json.Unmarshal([]byte(response), &export))
|
||||
require.Len(t, export.Groups, 1)
|
||||
assert.Equal(t, "my-namespace/my-other-namespace containing multiple //", export.Groups[0].Folder)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1473,3 +1473,34 @@ func createUser(t *testing.T, db db.DB, cfg *setting.Cfg, cmd user.CreateUserCom
|
||||
require.NoError(t, err)
|
||||
return u.ID
|
||||
}
|
||||
|
||||
func (a apiClient) GetProvisioningAlertRuleExport(t *testing.T, ruleUID string, params *apimodels.ExportQueryParams) (int, string) {
|
||||
t.Helper()
|
||||
u, err := url.Parse(fmt.Sprintf("%s/api/v1/provisioning/alert-rules/%s/export", a.url, ruleUID))
|
||||
require.NoError(t, err)
|
||||
if params != nil {
|
||||
q := url.Values{}
|
||||
if params.Format != "" {
|
||||
q.Set("format", params.Format)
|
||||
}
|
||||
if params.Download {
|
||||
q.Set("download", "true")
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
return resp.StatusCode, string(b)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user