Chore: Move folder service into a separate package (#56591)

* Chore: move folder service interface into a separate package

* copy implementation into a standalone package

* move implementation and tests to the new folder package

* remove leftovers from wire

* add test doubles for folder service

* fix tests in library panels/elements

* fix provideservice in ngalert
This commit is contained in:
Serge Zaitsev
2022-10-10 21:47:53 +02:00
committed by GitHub
parent eb077db2b0
commit 53baecd71f
18 changed files with 148 additions and 308 deletions
-22
View File
@@ -1,22 +0,0 @@
package dashboards
import (
"context"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/user"
)
// FolderService is a service for operating on folders.
//
//go:generate mockery --name FolderService --structname FakeFolderService --inpackage --filename folder_service_mock.go
type FolderService interface {
GetFolders(ctx context.Context, user *user.SignedInUser, orgID int64, limit int64, page int64) ([]*models.Folder, error)
GetFolderByID(ctx context.Context, user *user.SignedInUser, id int64, orgID int64) (*models.Folder, error)
GetFolderByUID(ctx context.Context, user *user.SignedInUser, orgID int64, uid string) (*models.Folder, error)
GetFolderByTitle(ctx context.Context, user *user.SignedInUser, orgID int64, title string) (*models.Folder, error)
CreateFolder(ctx context.Context, user *user.SignedInUser, orgID int64, title, uid string) (*models.Folder, error)
UpdateFolder(ctx context.Context, user *user.SignedInUser, orgID int64, existingUid string, cmd *models.UpdateFolderCommand) error
DeleteFolder(ctx context.Context, user *user.SignedInUser, orgID int64, uid string, forceDeleteRules bool) (*models.Folder, error)
MakeUserAdmin(ctx context.Context, orgID int64, userID, folderID int64, setViewAndEditPermissions bool) error
}
@@ -1,194 +0,0 @@
// Code generated by mockery v2.12.1. DO NOT EDIT.
package dashboards
import (
context "context"
models "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/user"
mock "github.com/stretchr/testify/mock"
testing "testing"
)
// FakeFolderService is an autogenerated mock type for the FolderService type
type FakeFolderService struct {
mock.Mock
}
// CreateFolder provides a mock function with given fields: ctx, user, orgID, title, uid
func (_m *FakeFolderService) CreateFolder(ctx context.Context, usr *user.SignedInUser, orgID int64, title string, uid string) (*models.Folder, error) {
ret := _m.Called(ctx, usr, orgID, title, uid)
var r0 *models.Folder
if rf, ok := ret.Get(0).(func(context.Context, *user.SignedInUser, int64, string, string) *models.Folder); ok {
r0 = rf(ctx, usr, orgID, title, uid)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.Folder)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *user.SignedInUser, int64, string, string) error); ok {
r1 = rf(ctx, usr, orgID, title, uid)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteFolder provides a mock function with given fields: ctx, user, orgID, uid, forceDeleteRules
func (_m *FakeFolderService) DeleteFolder(ctx context.Context, usr *user.SignedInUser, orgID int64, uid string, forceDeleteRules bool) (*models.Folder, error) {
ret := _m.Called(ctx, usr, orgID, uid, forceDeleteRules)
var r0 *models.Folder
if rf, ok := ret.Get(0).(func(context.Context, *user.SignedInUser, int64, string, bool) *models.Folder); ok {
r0 = rf(ctx, usr, orgID, uid, forceDeleteRules)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.Folder)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *user.SignedInUser, int64, string, bool) error); ok {
r1 = rf(ctx, usr, orgID, uid, forceDeleteRules)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetFolderByID provides a mock function with given fields: ctx, user, id, orgID
func (_m *FakeFolderService) GetFolderByID(ctx context.Context, usr *user.SignedInUser, id int64, orgID int64) (*models.Folder, error) {
ret := _m.Called(ctx, usr, id, orgID)
var r0 *models.Folder
if rf, ok := ret.Get(0).(func(context.Context, *user.SignedInUser, int64, int64) *models.Folder); ok {
r0 = rf(ctx, usr, id, orgID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.Folder)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *user.SignedInUser, int64, int64) error); ok {
r1 = rf(ctx, usr, id, orgID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetFolderByTitle provides a mock function with given fields: ctx, user, orgID, title
func (_m *FakeFolderService) GetFolderByTitle(ctx context.Context, usr *user.SignedInUser, orgID int64, title string) (*models.Folder, error) {
ret := _m.Called(ctx, usr, orgID, title)
var r0 *models.Folder
if rf, ok := ret.Get(0).(func(context.Context, *user.SignedInUser, int64, string) *models.Folder); ok {
r0 = rf(ctx, usr, orgID, title)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.Folder)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *user.SignedInUser, int64, string) error); ok {
r1 = rf(ctx, usr, orgID, title)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetFolderByUID provides a mock function with given fields: ctx, user, orgID, uid
func (_m *FakeFolderService) GetFolderByUID(ctx context.Context, usr *user.SignedInUser, orgID int64, uid string) (*models.Folder, error) {
ret := _m.Called(ctx, usr, orgID, uid)
var r0 *models.Folder
if rf, ok := ret.Get(0).(func(context.Context, *user.SignedInUser, int64, string) *models.Folder); ok {
r0 = rf(ctx, usr, orgID, uid)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.Folder)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *user.SignedInUser, int64, string) error); ok {
r1 = rf(ctx, usr, orgID, uid)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetFolders provides a mock function with given fields: ctx, user, orgID, limit, page
func (_m *FakeFolderService) GetFolders(ctx context.Context, usr *user.SignedInUser, orgID int64, limit int64, page int64) ([]*models.Folder, error) {
ret := _m.Called(ctx, usr, orgID, limit, page)
var r0 []*models.Folder
if rf, ok := ret.Get(0).(func(context.Context, *user.SignedInUser, int64, int64, int64) []*models.Folder); ok {
r0 = rf(ctx, usr, orgID, limit, page)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Folder)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *user.SignedInUser, int64, int64, int64) error); ok {
r1 = rf(ctx, usr, orgID, limit, page)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MakeUserAdmin provides a mock function with given fields: ctx, orgID, userID, folderID, setViewAndEditPermissions
func (_m *FakeFolderService) MakeUserAdmin(ctx context.Context, orgID int64, userID int64, folderID int64, setViewAndEditPermissions bool) error {
ret := _m.Called(ctx, orgID, userID, folderID, setViewAndEditPermissions)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64, int64, int64, bool) error); ok {
r0 = rf(ctx, orgID, userID, folderID, setViewAndEditPermissions)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateFolder provides a mock function with given fields: ctx, user, orgID, existingUid, cmd
func (_m *FakeFolderService) UpdateFolder(ctx context.Context, usr *user.SignedInUser, orgID int64, existingUid string, cmd *models.UpdateFolderCommand) error {
ret := _m.Called(ctx, usr, orgID, existingUid, cmd)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *user.SignedInUser, int64, string, *models.UpdateFolderCommand) error); ok {
r0 = rf(ctx, usr, orgID, existingUid, cmd)
} else {
r0 = ret.Error(0)
}
return r0
}
// NewFakeFolderService creates a new instance of FakeFolderService. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
func NewFakeFolderService(t testing.TB) *FakeFolderService {
mock := &FakeFolderService{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
@@ -1,315 +0,0 @@
package service
import (
"context"
"errors"
"strings"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/events"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
type FolderServiceImpl struct {
log log.Logger
cfg *setting.Cfg
dashboardService dashboards.DashboardService
dashboardStore dashboards.Store
searchService *search.SearchService
features featuremgmt.FeatureToggles
permissions accesscontrol.FolderPermissionsService
// bus is currently used to publish events that cause scheduler to update rules.
bus bus.Bus
}
func ProvideFolderService(
cfg *setting.Cfg, dashboardService dashboards.DashboardService, dashboardStore dashboards.Store,
searchService *search.SearchService, features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService,
ac accesscontrol.AccessControl, bus bus.Bus,
) *FolderServiceImpl {
ac.RegisterScopeAttributeResolver(dashboards.NewFolderNameScopeResolver(dashboardStore))
ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(dashboardStore))
return &FolderServiceImpl{
cfg: cfg,
log: log.New("folder-service"),
dashboardService: dashboardService,
dashboardStore: dashboardStore,
searchService: searchService,
features: features,
permissions: folderPermissionsService,
bus: bus,
}
}
func (f *FolderServiceImpl) GetFolders(ctx context.Context, user *user.SignedInUser, orgID int64, limit int64, page int64) ([]*models.Folder, error) {
searchQuery := search.Query{
SignedInUser: user,
DashboardIds: make([]int64, 0),
FolderIds: make([]int64, 0),
Limit: limit,
OrgId: orgID,
Type: "dash-folder",
Permission: models.PERMISSION_VIEW,
Page: page,
}
if err := f.searchService.SearchHandler(ctx, &searchQuery); err != nil {
return nil, err
}
folders := make([]*models.Folder, 0)
for _, hit := range searchQuery.Result {
folders = append(folders, &models.Folder{
Id: hit.ID,
Uid: hit.UID,
Title: hit.Title,
})
}
return folders, nil
}
func (f *FolderServiceImpl) GetFolderByID(ctx context.Context, user *user.SignedInUser, id int64, orgID int64) (*models.Folder, error) {
if id == 0 {
return &models.Folder{Id: id, Title: "General"}, nil
}
dashFolder, err := f.dashboardStore.GetFolderByID(ctx, orgID, id)
if err != nil {
return nil, err
}
g := guardian.New(ctx, dashFolder.Id, orgID, user)
if canView, err := g.CanView(); err != nil || !canView {
if err != nil {
return nil, toFolderError(err)
}
return nil, dashboards.ErrFolderAccessDenied
}
return dashFolder, nil
}
func (f *FolderServiceImpl) GetFolderByUID(ctx context.Context, user *user.SignedInUser, orgID int64, uid string) (*models.Folder, error) {
dashFolder, err := f.dashboardStore.GetFolderByUID(ctx, orgID, uid)
if err != nil {
return nil, err
}
g := guardian.New(ctx, dashFolder.Id, orgID, user)
if canView, err := g.CanView(); err != nil || !canView {
if err != nil {
return nil, toFolderError(err)
}
return nil, dashboards.ErrFolderAccessDenied
}
return dashFolder, nil
}
func (f *FolderServiceImpl) GetFolderByTitle(ctx context.Context, user *user.SignedInUser, orgID int64, title string) (*models.Folder, error) {
dashFolder, err := f.dashboardStore.GetFolderByTitle(ctx, orgID, title)
if err != nil {
return nil, err
}
g := guardian.New(ctx, dashFolder.Id, orgID, user)
if canView, err := g.CanView(); err != nil || !canView {
if err != nil {
return nil, toFolderError(err)
}
return nil, dashboards.ErrFolderAccessDenied
}
return dashFolder, nil
}
func (f *FolderServiceImpl) CreateFolder(ctx context.Context, user *user.SignedInUser, orgID int64, title, uid string) (*models.Folder, error) {
dashFolder := models.NewDashboardFolder(title)
dashFolder.OrgId = orgID
trimmedUID := strings.TrimSpace(uid)
if trimmedUID == accesscontrol.GeneralFolderUID {
return nil, dashboards.ErrFolderInvalidUID
}
dashFolder.SetUid(trimmedUID)
userID := user.UserID
if userID == 0 {
userID = -1
}
dashFolder.CreatedBy = userID
dashFolder.UpdatedBy = userID
dashFolder.UpdateSlug()
dto := &dashboards.SaveDashboardDTO{
Dashboard: dashFolder,
OrgId: orgID,
User: user,
}
saveDashboardCmd, err := f.dashboardService.BuildSaveDashboardCommand(ctx, dto, false, false)
if err != nil {
return nil, toFolderError(err)
}
dash, err := f.dashboardStore.SaveDashboard(ctx, *saveDashboardCmd)
if err != nil {
return nil, toFolderError(err)
}
var folder *models.Folder
folder, err = f.dashboardStore.GetFolderByID(ctx, orgID, dash.Id)
if err != nil {
return nil, err
}
var permissionErr error
if !accesscontrol.IsDisabled(f.cfg) {
var permissions []accesscontrol.SetResourcePermissionCommand
if user.IsRealUser() && !user.IsAnonymous {
permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{
UserID: userID, Permission: models.PERMISSION_ADMIN.String(),
})
}
permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{
{BuiltinRole: string(org.RoleEditor), Permission: models.PERMISSION_EDIT.String()},
{BuiltinRole: string(org.RoleViewer), Permission: models.PERMISSION_VIEW.String()},
}...)
_, permissionErr = f.permissions.SetPermissions(ctx, orgID, folder.Uid, permissions...)
} else if f.cfg.EditorsCanAdmin && user.IsRealUser() && !user.IsAnonymous {
permissionErr = f.MakeUserAdmin(ctx, orgID, userID, folder.Id, true)
}
if permissionErr != nil {
f.log.Error("Could not make user admin", "folder", folder.Title, "user", userID, "error", permissionErr)
}
return folder, nil
}
func (f *FolderServiceImpl) UpdateFolder(ctx context.Context, user *user.SignedInUser, orgID int64, existingUid string, cmd *models.UpdateFolderCommand) error {
query := models.GetDashboardQuery{OrgId: orgID, Uid: existingUid}
if _, err := f.dashboardStore.GetDashboard(ctx, &query); err != nil {
return toFolderError(err)
}
dashFolder := query.Result
currentTitle := dashFolder.Title
if !dashFolder.IsFolder {
return dashboards.ErrFolderNotFound
}
cmd.UpdateDashboardModel(dashFolder, orgID, user.UserID)
dto := &dashboards.SaveDashboardDTO{
Dashboard: dashFolder,
OrgId: orgID,
User: user,
Overwrite: cmd.Overwrite,
}
saveDashboardCmd, err := f.dashboardService.BuildSaveDashboardCommand(ctx, dto, false, false)
if err != nil {
return toFolderError(err)
}
dash, err := f.dashboardStore.SaveDashboard(ctx, *saveDashboardCmd)
if err != nil {
return toFolderError(err)
}
var folder *models.Folder
folder, err = f.dashboardStore.GetFolderByID(ctx, orgID, dash.Id)
if err != nil {
return err
}
cmd.Result = folder
if currentTitle != folder.Title {
if err := f.bus.Publish(ctx, &events.FolderTitleUpdated{
Timestamp: folder.Updated,
Title: folder.Title,
ID: dash.Id,
UID: dash.Uid,
OrgID: orgID,
}); err != nil {
f.log.Error("failed to publish FolderTitleUpdated event", "folder", folder.Title, "user", user.UserID, "error", err)
}
}
return nil
}
func (f *FolderServiceImpl) DeleteFolder(ctx context.Context, user *user.SignedInUser, orgID int64, uid string, forceDeleteRules bool) (*models.Folder, error) {
dashFolder, err := f.dashboardStore.GetFolderByUID(ctx, orgID, uid)
if err != nil {
return nil, err
}
guard := guardian.New(ctx, dashFolder.Id, orgID, user)
if canSave, err := guard.CanDelete(); err != nil || !canSave {
if err != nil {
return nil, toFolderError(err)
}
return nil, dashboards.ErrFolderAccessDenied
}
deleteCmd := models.DeleteDashboardCommand{OrgId: orgID, Id: dashFolder.Id, ForceDeleteFolderRules: forceDeleteRules}
if err := f.dashboardStore.DeleteDashboard(ctx, &deleteCmd); err != nil {
return nil, toFolderError(err)
}
return dashFolder, nil
}
func (f *FolderServiceImpl) MakeUserAdmin(ctx context.Context, orgID int64, userID, folderID int64, setViewAndEditPermissions bool) error {
return f.dashboardService.MakeUserAdmin(ctx, orgID, userID, folderID, setViewAndEditPermissions)
}
func toFolderError(err error) error {
if errors.Is(err, dashboards.ErrDashboardTitleEmpty) {
return dashboards.ErrFolderTitleEmpty
}
if errors.Is(err, dashboards.ErrDashboardUpdateAccessDenied) {
return dashboards.ErrFolderAccessDenied
}
if errors.Is(err, dashboards.ErrDashboardWithSameNameInFolderExists) {
return dashboards.ErrFolderSameNameExists
}
if errors.Is(err, dashboards.ErrDashboardWithSameUIDExists) {
return dashboards.ErrFolderWithSameUIDExists
}
if errors.Is(err, dashboards.ErrDashboardVersionMismatch) {
return dashboards.ErrFolderVersionMismatch
}
if errors.Is(err, dashboards.ErrDashboardNotFound) {
return dashboards.ErrFolderNotFound
}
if errors.Is(err, dashboards.ErrDashboardFailedGenerateUniqueUid) {
err = dashboards.ErrFolderFailedGenerateUniqueUid
}
return err
}
@@ -1,260 +0,0 @@
package service
import (
"context"
"math/rand"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
busmock "github.com/grafana/grafana/pkg/bus/mock"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
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/guardian"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
var orgID = int64(1)
var usr = &user.SignedInUser{UserID: 1}
func TestIntegrationProvideFolderService(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Run("should register scope resolvers", func(t *testing.T) {
cfg := setting.NewCfg()
ac := acmock.New()
ProvideFolderService(cfg, nil, nil, nil, nil, nil, ac, busmock.New())
require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 2)
})
}
func TestIntegrationFolderService(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Run("Folder service tests", func(t *testing.T) {
store := &dashboards.FakeDashboardStore{}
cfg := setting.NewCfg()
cfg.RBACEnabled = false
features := featuremgmt.WithFeatures()
cfg.IsFeatureToggleEnabled = features.IsEnabled
folderPermissions := acmock.NewMockedPermissionsService()
dashboardPermissions := acmock.NewMockedPermissionsService()
dashboardService := ProvideDashboardService(cfg, store, nil, features, folderPermissions, dashboardPermissions, acmock.New())
service := FolderServiceImpl{
cfg: cfg,
log: log.New("test-folder-service"),
dashboardService: dashboardService,
dashboardStore: store,
searchService: nil,
features: features,
permissions: folderPermissions,
bus: busmock.New(),
}
t.Run("Given user has no permissions", func(t *testing.T) {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{})
folderId := rand.Int63()
folderUID := util.GenerateShortUID()
folder := models.NewFolder("Folder")
folder.Id = folderId
folder.Uid = folderUID
store.On("GetFolderByID", mock.Anything, orgID, folderId).Return(folder, nil)
store.On("GetFolderByUID", mock.Anything, orgID, folderUID).Return(folder, nil)
t.Run("When get folder by id should return access denied error", func(t *testing.T) {
_, err := service.GetFolderByID(context.Background(), usr, folderId, orgID)
require.Equal(t, err, dashboards.ErrFolderAccessDenied)
})
t.Run("When get folder by id, with id = 0 should return default folder", func(t *testing.T) {
folder, err := service.GetFolderByID(context.Background(), usr, 0, orgID)
require.NoError(t, err)
require.Equal(t, folder, &models.Folder{Id: 0, Title: "General"})
})
t.Run("When get folder by uid should return access denied error", func(t *testing.T) {
_, err := service.GetFolderByUID(context.Background(), usr, orgID, folderUID)
require.Equal(t, err, dashboards.ErrFolderAccessDenied)
})
t.Run("When creating folder should return access denied error", func(t *testing.T) {
store.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything).Return(true, nil).Times(2)
_, err := service.CreateFolder(context.Background(), usr, orgID, folder.Title, folderUID)
require.Equal(t, err, dashboards.ErrFolderAccessDenied)
})
t.Run("When updating folder should return access denied error", func(t *testing.T) {
store.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) {
folder := args.Get(1).(*models.GetDashboardQuery)
folder.Result = models.NewDashboard("dashboard-test")
folder.Result.IsFolder = true
}).Return(&models.Dashboard{}, nil)
err := service.UpdateFolder(context.Background(), usr, orgID, folderUID, &models.UpdateFolderCommand{
Uid: folderUID,
Title: "Folder-TEST",
})
require.Equal(t, err, dashboards.ErrFolderAccessDenied)
})
t.Run("When deleting folder by uid should return access denied error", func(t *testing.T) {
_, err := service.DeleteFolder(context.Background(), usr, orgID, folderUID, false)
require.Error(t, err)
require.Equal(t, err, dashboards.ErrFolderAccessDenied)
})
t.Cleanup(func() {
guardian.New = origNewGuardian
})
})
t.Run("Given user has permission to save", func(t *testing.T) {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
t.Run("When creating folder should not return access denied error", func(t *testing.T) {
dash := models.NewDashboardFolder("Test-Folder")
dash.Id = rand.Int63()
f := models.DashboardToFolder(dash)
store.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything).Return(true, nil)
store.On("SaveDashboard", mock.Anything).Return(dash, nil).Once()
store.On("GetFolderByID", mock.Anything, orgID, dash.Id).Return(f, nil)
actualFolder, err := service.CreateFolder(context.Background(), usr, orgID, dash.Title, "")
require.NoError(t, err)
require.Equal(t, f, actualFolder)
})
t.Run("When creating folder should return error if uid is general", func(t *testing.T) {
dash := models.NewDashboardFolder("Test-Folder")
dash.Id = rand.Int63()
_, err := service.CreateFolder(context.Background(), usr, orgID, dash.Title, "general")
require.ErrorIs(t, err, dashboards.ErrFolderInvalidUID)
})
t.Run("When updating folder should not return access denied error", func(t *testing.T) {
dashboardFolder := models.NewDashboardFolder("Folder")
dashboardFolder.Id = rand.Int63()
dashboardFolder.Uid = util.GenerateShortUID()
f := models.DashboardToFolder(dashboardFolder)
store.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything).Return(true, nil)
store.On("SaveDashboard", mock.Anything).Return(dashboardFolder, nil)
store.On("GetFolderByID", mock.Anything, orgID, dashboardFolder.Id).Return(f, nil)
req := &models.UpdateFolderCommand{
Uid: dashboardFolder.Uid,
Title: "TEST-Folder",
}
err := service.UpdateFolder(context.Background(), usr, orgID, dashboardFolder.Uid, req)
require.NoError(t, err)
require.Equal(t, f, req.Result)
})
t.Run("When deleting folder by uid should not return access denied error", func(t *testing.T) {
f := models.NewFolder(util.GenerateShortUID())
f.Id = rand.Int63()
f.Uid = util.GenerateShortUID()
store.On("GetFolderByUID", mock.Anything, orgID, f.Uid).Return(f, nil)
var actualCmd *models.DeleteDashboardCommand
store.On("DeleteDashboard", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
actualCmd = args.Get(1).(*models.DeleteDashboardCommand)
}).Return(nil).Once()
expectedForceDeleteRules := rand.Int63()%2 == 0
_, err := service.DeleteFolder(context.Background(), usr, orgID, f.Uid, expectedForceDeleteRules)
require.NoError(t, err)
require.NotNil(t, actualCmd)
require.Equal(t, f.Id, actualCmd.Id)
require.Equal(t, orgID, actualCmd.OrgId)
require.Equal(t, expectedForceDeleteRules, actualCmd.ForceDeleteFolderRules)
})
t.Cleanup(func() {
guardian.New = origNewGuardian
})
})
t.Run("Given user has permission to view", func(t *testing.T) {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true})
t.Run("When get folder by id should return folder", func(t *testing.T) {
expected := models.NewFolder(util.GenerateShortUID())
expected.Id = rand.Int63()
store.On("GetFolderByID", mock.Anything, orgID, expected.Id).Return(expected, nil)
actual, err := service.GetFolderByID(context.Background(), usr, expected.Id, orgID)
require.Equal(t, expected, actual)
require.NoError(t, err)
})
t.Run("When get folder by uid should return folder", func(t *testing.T) {
expected := models.NewFolder(util.GenerateShortUID())
expected.Uid = util.GenerateShortUID()
store.On("GetFolderByUID", mock.Anything, orgID, expected.Uid).Return(expected, nil)
actual, err := service.GetFolderByUID(context.Background(), usr, orgID, expected.Uid)
require.Equal(t, expected, actual)
require.NoError(t, err)
})
t.Run("When get folder by title should return folder", func(t *testing.T) {
expected := models.NewFolder("TEST-" + util.GenerateShortUID())
store.On("GetFolderByTitle", mock.Anything, orgID, expected.Title).Return(expected, nil)
actual, err := service.GetFolderByTitle(context.Background(), usr, orgID, expected.Title)
require.Equal(t, expected, actual)
require.NoError(t, err)
})
t.Cleanup(func() {
guardian.New = origNewGuardian
})
})
t.Run("Should map errors correct", func(t *testing.T) {
testCases := []struct {
ActualError error
ExpectedError error
}{
{ActualError: dashboards.ErrDashboardTitleEmpty, ExpectedError: dashboards.ErrFolderTitleEmpty},
{ActualError: dashboards.ErrDashboardUpdateAccessDenied, ExpectedError: dashboards.ErrFolderAccessDenied},
{ActualError: dashboards.ErrDashboardWithSameNameInFolderExists, ExpectedError: dashboards.ErrFolderSameNameExists},
{ActualError: dashboards.ErrDashboardWithSameUIDExists, ExpectedError: dashboards.ErrFolderWithSameUIDExists},
{ActualError: dashboards.ErrDashboardVersionMismatch, ExpectedError: dashboards.ErrFolderVersionMismatch},
{ActualError: dashboards.ErrDashboardNotFound, ExpectedError: dashboards.ErrFolderNotFound},
{ActualError: dashboards.ErrDashboardFailedGenerateUniqueUid, ExpectedError: dashboards.ErrFolderFailedGenerateUniqueUid},
{ActualError: dashboards.ErrDashboardInvalidUid, ExpectedError: dashboards.ErrDashboardInvalidUid},
}
for _, tc := range testCases {
actualError := toFolderError(tc.ActualError)
assert.EqualErrorf(t, actualError, tc.ExpectedError.Error(),
"For error '%s' expected error '%s', actual '%s'", tc.ActualError, tc.ExpectedError, actualError)
}
})
})
}