From fcb17379ea03d5606a9fa58f89fd07a65adcaa1a Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Fri, 27 Sep 2024 14:22:29 +0200 Subject: [PATCH] LibraryElements: add fake service implementation and replace its usage in Dashboard API (#93783) * LibraryElements: add fake service implementation * Dashboards: replace fake LibraryElements implementation --- pkg/api/dashboard_test.go | 50 +------ .../fake/libraryelements_service.go | 125 +++++++++++++++++ .../fake/libraryelements_service_test.go | 132 ++++++++++++++++++ 3 files changed, 264 insertions(+), 43 deletions(-) create mode 100644 pkg/services/libraryelements/fake/libraryelements_service.go create mode 100644 pkg/services/libraryelements/fake/libraryelements_service_test.go diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 7f3b8dce121..ad246fec912 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -42,7 +42,7 @@ import ( "github.com/grafana/grafana/pkg/services/folder/folderimpl" "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/guardian" - "github.com/grafana/grafana/pkg/services/libraryelements/model" + libraryelementsfake "github.com/grafana/grafana/pkg/services/libraryelements/fake" "github.com/grafana/grafana/pkg/services/librarypanels" "github.com/grafana/grafana/pkg/services/licensing/licensingtest" "github.com/grafana/grafana/pkg/services/live" @@ -271,7 +271,7 @@ func TestHTTPServer_DeleteDashboardByUID_AccessControl(t *testing.T) { hs.starService = startest.NewStarServiceFake() hs.LibraryPanelService = &mockLibraryPanelService{} - hs.LibraryElementService = &mockLibraryElementService{} + hs.LibraryElementService = &libraryelementsfake.LibraryElementService{} pubDashService := publicdashboards.NewFakePublicDashboardService(t) pubDashService.On("DeleteByDashboard", mock.Anything, mock.Anything).Return(nil).Maybe() @@ -677,7 +677,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { Cfg: setting.NewCfg(), ProvisioningService: fakeProvisioningService, LibraryPanelService: &mockLibraryPanelService{}, - LibraryElementService: &mockLibraryElementService{}, + LibraryElementService: &libraryelementsfake.LibraryElementService{}, dashboardProvisioningService: mockDashboardProvisioningService{}, SQLStore: mockSQLStore, AccessControl: accesscontrolmock.New(), @@ -827,7 +827,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr } libraryPanelsService := mockLibraryPanelService{} - libraryElementsService := mockLibraryElementService{} + libraryElementsService := libraryelementsfake.LibraryElementService{} cfg := setting.NewCfg() ac := accesscontrolmock.New() folderPermissions := accesscontrolmock.NewMockedPermissionsService() @@ -909,7 +909,7 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s QuotaService: quotatest.New(false, nil), pluginStore: &pluginstore.FakePluginStore{}, LibraryPanelService: &mockLibraryPanelService{}, - LibraryElementService: &mockLibraryElementService{}, + LibraryElementService: &libraryelementsfake.LibraryElementService{}, DashboardService: dashboardService, folderService: folderService, Features: featuremgmt.WithFeatures(), @@ -947,7 +947,7 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string Live: newTestLive(t, db.InitTestDB(t)), QuotaService: quotatest.New(false, nil), LibraryPanelService: &mockLibraryPanelService{}, - LibraryElementService: &mockLibraryElementService{}, + LibraryElementService: &libraryelementsfake.LibraryElementService{}, SQLStore: sqlmock, dashboardVersionService: fakeDashboardVersionService, Features: featuremgmt.WithFeatures(), @@ -990,7 +990,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout Live: newTestLive(t, db.InitTestDB(t)), QuotaService: quotatest.New(false, nil), LibraryPanelService: &mockLibraryPanelService{}, - LibraryElementService: &mockLibraryElementService{}, + LibraryElementService: &libraryelementsfake.LibraryElementService{}, DashboardService: mock, SQLStore: sqlStore, Features: featuremgmt.WithFeatures(), @@ -1050,39 +1050,3 @@ func (m *mockLibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Con func (m *mockLibraryPanelService) ImportLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error { return nil } - -type mockLibraryElementService struct{} - -func (l *mockLibraryElementService) CreateElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) { - return model.LibraryElementDTO{}, nil -} - -// GetElement gets an element from a UID. -func (l *mockLibraryElementService) GetElement(c context.Context, signedInUser identity.Requester, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) { - return model.LibraryElementDTO{}, nil -} - -// GetElementsForDashboard gets all connected elements for a specific dashboard. -func (l *mockLibraryElementService) GetElementsForDashboard(c context.Context, dashboardID int64) (map[string]model.LibraryElementDTO, error) { - return map[string]model.LibraryElementDTO{}, nil -} - -// ConnectElementsToDashboard connects elements to a specific dashboard. -func (l *mockLibraryElementService) ConnectElementsToDashboard(c context.Context, signedInUser identity.Requester, elementUIDs []string, dashboardID int64) error { - return nil -} - -// DisconnectElementsFromDashboard disconnects elements from a specific dashboard. -func (l *mockLibraryElementService) DisconnectElementsFromDashboard(c context.Context, dashboardID int64) error { - return nil -} - -// DeleteLibraryElementsInFolder deletes all elements for a specific folder. -func (l *mockLibraryElementService) DeleteLibraryElementsInFolder(c context.Context, signedInUser identity.Requester, folderUID string) error { - return nil -} - -// GetAll gets all library elements with support to query filters. -func (l *mockLibraryElementService) GetAllElements(c context.Context, signedInUser identity.Requester, query model.SearchLibraryElementsQuery) (model.LibraryElementSearchResult, error) { - return model.LibraryElementSearchResult{}, nil -} diff --git a/pkg/services/libraryelements/fake/libraryelements_service.go b/pkg/services/libraryelements/fake/libraryelements_service.go new file mode 100644 index 00000000000..e8f0b3615d5 --- /dev/null +++ b/pkg/services/libraryelements/fake/libraryelements_service.go @@ -0,0 +1,125 @@ +package fake + +import ( + "context" + "sync" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/libraryelements" + "github.com/grafana/grafana/pkg/services/libraryelements/model" + "github.com/grafana/grafana/pkg/util" +) + +// LibraryElementService is a fake with only the required methods implemented while the others are stubbed. +type LibraryElementService struct { + elements map[string]model.LibraryElementDTO + mx sync.RWMutex + idCounter int64 +} + +var _ libraryelements.Service = (*LibraryElementService)(nil) + +func (l *LibraryElementService) CreateElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) { + l.mx.Lock() + defer l.mx.Unlock() + + if len(l.elements) == 0 { + l.elements = make(map[string]model.LibraryElementDTO, 0) + } + + var orgID int64 = 1 + if signedInOrgID := signedInUser.GetOrgID(); signedInOrgID != 0 { + orgID = signedInOrgID + } + + var folderUID string + if cmd.FolderUID != nil { + folderUID = *cmd.FolderUID + } + + createUID := cmd.UID + if len(createUID) == 0 { + createUID = util.GenerateShortUID() + } + + if _, exists := l.elements[createUID]; exists { + return model.LibraryElementDTO{}, model.ErrLibraryElementAlreadyExists + } + + l.idCounter++ + + dto := model.LibraryElementDTO{ + ID: l.idCounter, + OrgID: orgID, + FolderID: cmd.FolderID, //nolint: staticcheck + FolderUID: folderUID, + UID: createUID, + Name: cmd.Name, + Kind: cmd.Kind, + Type: "text", + Description: "A description", + Model: cmd.Model, + Version: 1, + Meta: model.LibraryElementDTOMeta{}, + } + + l.elements[createUID] = dto + + return dto, nil +} + +func (l *LibraryElementService) GetElement(c context.Context, signedInUser identity.Requester, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) { + l.mx.RLock() + defer l.mx.RUnlock() + + libraryElement, exists := l.elements[cmd.UID] + if !exists { + return model.LibraryElementDTO{}, model.ErrLibraryElementNotFound + } + + return libraryElement, nil +} + +func (l *LibraryElementService) GetElementsForDashboard(c context.Context, dashboardID int64) (map[string]model.LibraryElementDTO, error) { + return map[string]model.LibraryElementDTO{}, nil +} + +func (l *LibraryElementService) ConnectElementsToDashboard(c context.Context, signedInUser identity.Requester, elementUIDs []string, dashboardID int64) error { + return nil +} + +func (l *LibraryElementService) DisconnectElementsFromDashboard(c context.Context, dashboardID int64) error { + return nil +} + +func (l *LibraryElementService) DeleteLibraryElementsInFolder(c context.Context, signedInUser identity.Requester, folderUID string) error { + return nil +} + +func (l *LibraryElementService) GetAllElements(c context.Context, signedInUser identity.Requester, query model.SearchLibraryElementsQuery) (model.LibraryElementSearchResult, error) { + elements := make([]model.LibraryElementDTO, 0, len(l.elements)) + + var orgID int64 = 1 + if signedInOrgID := signedInUser.GetOrgID(); signedInOrgID != 0 { + orgID = signedInOrgID + } + + l.mx.RLock() + defer l.mx.RUnlock() + + for _, element := range l.elements { + if element.OrgID != orgID { + continue + } + + elements = append(elements, element) + } + + // For this fake ignore pagination to make it simpler. + return model.LibraryElementSearchResult{ + TotalCount: int64(len(elements)), + Elements: elements, + Page: 1, + PerPage: len(elements), + }, nil +} diff --git a/pkg/services/libraryelements/fake/libraryelements_service_test.go b/pkg/services/libraryelements/fake/libraryelements_service_test.go new file mode 100644 index 00000000000..a246e4c6e2b --- /dev/null +++ b/pkg/services/libraryelements/fake/libraryelements_service_test.go @@ -0,0 +1,132 @@ +package fake_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/libraryelements/fake" + "github.com/grafana/grafana/pkg/services/libraryelements/model" +) + +func TestLibraryElementService(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("GetElement", func(t *testing.T) { + t.Parallel() + + user := &identity.StaticRequester{} + + t.Run("when the element does not exist, it returns a `model.ErrLibraryElementNotFound` error", func(t *testing.T) { + t.Parallel() + + svc := &fake.LibraryElementService{} + + element, err := svc.GetElement(ctx, user, model.GetLibraryElementCommand{UID: "does-not-exist"}) + require.ErrorIs(t, err, model.ErrLibraryElementNotFound) + require.Empty(t, element) + }) + + t.Run("when the element exists, it returns it", func(t *testing.T) { + t.Parallel() + + svc := &fake.LibraryElementService{} + + uid := "uid-1" + + createdElement, err := svc.CreateElement(ctx, user, model.CreateLibraryElementCommand{ + Name: "ElementName", + Model: []byte{}, + Kind: int64(model.PanelElement), + UID: uid, + }) + require.NoError(t, err) + require.NotEmpty(t, createdElement) + + element, err := svc.GetElement(ctx, user, model.GetLibraryElementCommand{UID: uid}) + require.NoError(t, err) + require.EqualValues(t, element, createdElement) + }) + }) + + t.Run("CreateElement", func(t *testing.T) { + t.Parallel() + + user := &identity.StaticRequester{} + + t.Run("when the uid already exists, it returns a `model.ErrLibraryElementAlreadyExists` error", func(t *testing.T) { + t.Parallel() + + svc := &fake.LibraryElementService{} + + cmd := model.CreateLibraryElementCommand{ + Name: "ElementName", + Model: []byte{}, + Kind: int64(model.PanelElement), + UID: "uid-1", + } + + createdElement, err := svc.CreateElement(ctx, user, cmd) + require.NoError(t, err) + require.NotEmpty(t, createdElement) + + createdElement, err = svc.CreateElement(ctx, user, cmd) + require.ErrorIs(t, err, model.ErrLibraryElementAlreadyExists) + require.Empty(t, createdElement) + }) + + t.Run("when the uid is not passed in, it generates a new one", func(t *testing.T) { + t.Parallel() + + svc := &fake.LibraryElementService{} + + cmd := model.CreateLibraryElementCommand{ + Name: "ElementName", + Model: []byte{}, + Kind: int64(model.PanelElement), + } + + createdElement, err := svc.CreateElement(ctx, user, cmd) + require.NoError(t, err) + require.NotEmpty(t, createdElement) + require.NotEmpty(t, createdElement.UID) + }) + }) + + t.Run("GetAllElements", func(t *testing.T) { + t.Parallel() + + t.Run("only returns the elements belonging to the requestor org", func(t *testing.T) { + t.Parallel() + + user1 := &identity.StaticRequester{OrgID: 1} + user2 := &identity.StaticRequester{OrgID: 2} + + svc := &fake.LibraryElementService{} + + cmd := model.CreateLibraryElementCommand{ + Name: "ElementName", + Model: []byte{}, + Kind: int64(model.PanelElement), + } + + createdElement1, err := svc.CreateElement(ctx, user1, cmd) + require.NoError(t, err) + require.NotEmpty(t, createdElement1) + + createdElement2, err := svc.CreateElement(ctx, user2, cmd) + require.NoError(t, err) + require.NotEmpty(t, createdElement2) + + result, err := svc.GetAllElements(ctx, user2, model.SearchLibraryElementsQuery{}) + require.NoError(t, err) + require.Len(t, result.Elements, 1) + require.Equal(t, createdElement2.UID, result.Elements[0].UID) + }) + }) +}