diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index f14a97b2319..23e51dc5291 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/api/dtos" @@ -51,7 +52,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { role := models.ROLE_VIEWER t.Run("Should not be allowed to save an annotation", func(t *testing.T) { postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, - cmd, store, func(sc *scenarioContext) { + cmd, store, nil, func(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 403, sc.resp.Code) }) @@ -84,7 +85,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { role := models.ROLE_EDITOR t.Run("Should be able to save an annotation", func(t *testing.T) { postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, - cmd, store, func(sc *scenarioContext) { + cmd, store, nil, func(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) }) @@ -155,7 +156,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { t.Run("When user is an Org Viewer", func(t *testing.T) { role := models.ROLE_VIEWER t.Run("Should not be allowed to save an annotation", func(t *testing.T) { - postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, cmd, store, func(sc *scenarioContext) { + postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, cmd, store, nil, func(sc *scenarioContext) { setUpACL() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 403, sc.resp.Code) @@ -188,7 +189,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { t.Run("When user is an Org Editor", func(t *testing.T) { role := models.ROLE_EDITOR t.Run("Should be able to save an annotation", func(t *testing.T) { - postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, cmd, store, func(sc *scenarioContext) { + postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, cmd, store, nil, func(sc *scenarioContext) { setUpACL() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) @@ -221,19 +222,28 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { t.Run("When user is an Admin", func(t *testing.T) { role := models.ROLE_ADMIN - mock := mockstore.NewSQLStoreMock() + mockStore := mockstore.NewSQLStoreMock() t.Run("Should be able to do anything", func(t *testing.T) { - postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, cmd, store, func(sc *scenarioContext) { + postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, cmd, store, nil, func(sc *scenarioContext) { setUpACL() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) }) - postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, dashboardUIDCmd, mock, func(sc *scenarioContext) { + dashSvc := dashboards.NewFakeDashboardService(t) + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardQuery) + q.Result = &models.Dashboard{ + Id: q.Id, + Uid: q.Uid, + } + }).Return(nil) + postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, dashboardUIDCmd, mockStore, dashSvc, func(sc *scenarioContext) { setUpACL() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) + dashSvc.AssertCalled(t, "GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")) }) putAnnotationScenario(t, "When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) { @@ -249,17 +259,26 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { }) deleteAnnotationsScenario(t, "When calling POST on", "/api/annotations/mass-delete", - "/api/annotations/mass-delete", role, deleteCmd, store, func(sc *scenarioContext) { + "/api/annotations/mass-delete", role, deleteCmd, store, nil, func(sc *scenarioContext) { setUpACL() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) }) + dashSvc = dashboards.NewFakeDashboardService(t) + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardQuery) + q.Result = &models.Dashboard{ + Id: 1, + Uid: deleteWithDashboardUIDCmd.DashboardUID, + } + }).Return(nil) deleteAnnotationsScenario(t, "When calling POST with dashboardUID on", "/api/annotations/mass-delete", - "/api/annotations/mass-delete", role, deleteWithDashboardUIDCmd, mock, func(sc *scenarioContext) { + "/api/annotations/mass-delete", role, deleteWithDashboardUIDCmd, mockStore, dashSvc, func(sc *scenarioContext) { setUpACL() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) + dashSvc.AssertCalled(t, "GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")) }) }) }) @@ -320,11 +339,11 @@ func (repo *fakeAnnotationsRepo) LoadItems() { var fakeAnnoRepo *fakeAnnotationsRepo func postAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, - cmd dtos.PostAnnotationsCmd, store sqlstore.Store, fn scenarioFunc) { + cmd dtos.PostAnnotationsCmd, store sqlstore.Store, dashSvc dashboards.DashboardService, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { hs := setupSimpleHTTPServer(nil) hs.SQLStore = store - hs.dashboardService = &dashboards.FakeDashboardService{} + hs.dashboardService = dashSvc sc := setupScenarioContext(t, url) sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { @@ -405,11 +424,11 @@ func patchAnnotationScenario(t *testing.T, desc string, url string, routePattern } func deleteAnnotationsScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, - cmd dtos.MassDeleteAnnotationsCmd, store sqlstore.Store, fn scenarioFunc) { + cmd dtos.MassDeleteAnnotationsCmd, store sqlstore.Store, dashSvc dashboards.DashboardService, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { hs := setupSimpleHTTPServer(nil) hs.SQLStore = store - hs.dashboardService = &dashboards.FakeDashboardService{} + hs.dashboardService = dashSvc sc := setupScenarioContext(t, url) sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { diff --git a/pkg/api/dashboard_public_config_test.go b/pkg/api/dashboard_public_config_test.go index e642935b83f..f487b3ea252 100644 --- a/pkg/api/dashboard_public_config_test.go +++ b/pkg/api/dashboard_public_config_test.go @@ -7,12 +7,14 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestApiRetrieveConfig(t *testing.T) { @@ -51,11 +53,10 @@ func TestApiRetrieveConfig(t *testing.T) { for _, test := range testCases { t.Run(test.name, func(t *testing.T) { sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)) - - sc.hs.dashboardService = &dashboards.FakeDashboardService{ - PublicDashboardConfigResult: test.publicDashboardConfigResult, - PublicDashboardConfigError: test.publicDashboardConfigError, - } + dashSvc := dashboards.NewFakeDashboardService(t) + dashSvc.On("GetPublicDashboardConfig", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")). + Return(test.publicDashboardConfigResult, test.publicDashboardConfigError) + sc.hs.dashboardService = dashSvc setInitCtxSignedInViewer(sc.initCtx) response := callAPI( @@ -106,11 +107,10 @@ func TestApiPersistsValue(t *testing.T) { for _, test := range testCases { t.Run(test.name, func(t *testing.T) { sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)) - - sc.hs.dashboardService = &dashboards.FakeDashboardService{ - PublicDashboardConfigResult: &models.PublicDashboardConfig{IsPublic: true}, - PublicDashboardConfigError: test.saveDashboardError, - } + dashSvc := dashboards.NewFakeDashboardService(t) + dashSvc.On("SavePublicDashboardConfig", mock.Anything, mock.AnythingOfType("*dashboards.SavePublicDashboardConfigDTO")). + Return(&models.PublicDashboardConfig{IsPublic: true}, test.saveDashboardError) + sc.hs.dashboardService = dashSvc setInitCtxSignedInViewer(sc.initCtx) response := callAPI( diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index b25bef370d2..3d9a47a4bb2 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -9,9 +9,6 @@ import ( "net/http" "testing" - "github.com/grafana/grafana/pkg/coremodel/dashboard" - "github.com/grafana/grafana/pkg/cuectx" - "github.com/grafana/grafana/pkg/framework/coremodel" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -20,6 +17,9 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/coremodel/dashboard" + "github.com/grafana/grafana/pkg/cuectx" + "github.com/grafana/grafana/pkg/framework/coremodel" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/models" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" @@ -122,11 +122,11 @@ func TestDashboardAPIEndpoint(t *testing.T) { fakeDash.Id = 1 fakeDash.FolderId = 1 fakeDash.HasAcl = false - dashboardService := &dashboards.FakeDashboardService{} - dashboardService.GetDashboardFn = func(ctx context.Context, cmd *models.GetDashboardQuery) error { - cmd.Result = fakeDash - return nil - } + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardQuery) + q.Result = fakeDash + }).Return(nil) mockSQLStore := mockstore.NewSQLStoreMock() @@ -225,11 +225,11 @@ func TestDashboardAPIEndpoint(t *testing.T) { fakeDash.Id = 1 fakeDash.FolderId = 1 fakeDash.HasAcl = true - dashboardService := &dashboards.FakeDashboardService{} - dashboardService.GetDashboardFn = func(ctx context.Context, cmd *models.GetDashboardQuery) error { - cmd.Result = fakeDash - return nil - } + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardQuery) + q.Result = fakeDash + }).Return(nil) mockSQLStore := mockstore.NewSQLStoreMock() cfg := setting.NewCfg() @@ -289,7 +289,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { setUp() sc.sqlStore = mockSQLStore - hs.callDeleteDashboardByUID(t, sc, &dashboards.FakeDashboardService{}) + hs.callDeleteDashboardByUID(t, sc, dashboardService) assert.Equal(t, 403, sc.resp.Code) }, mockSQLStore) @@ -327,7 +327,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { setUp() - hs.callDeleteDashboardByUID(t, sc, &dashboards.FakeDashboardService{}) + hs.callDeleteDashboardByUID(t, sc, dashboardService) assert.Equal(t, 403, sc.resp.Code) }, mockSQLStore) @@ -374,17 +374,14 @@ func TestDashboardAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { setUpInner() - mockDashboard := &dashboards.FakeDashboardService{ - SaveDashboardResult: &models.Dashboard{ - Id: fakeDash.Id, - Uid: "uid", - Title: "Dash", - Slug: "dash", - Version: 2, - }, - } + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardQuery) + q.Result = models.NewDashboard("test") + }).Return(nil) + dashboardService.On("DeleteDashboard", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(nil) - hs.callDeleteDashboardByUID(t, sc, mockDashboard) + hs.callDeleteDashboardByUID(t, sc, dashboardService) assert.Equal(t, 200, sc.resp.Code) }, mockSQLStore) @@ -468,7 +465,13 @@ func TestDashboardAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { setUpInner() sc.sqlStore = mockSQLStore - hs.callDeleteDashboardByUID(t, sc, &dashboards.FakeDashboardService{}) + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardQuery) + q.Result = models.NewDashboard("test") + }).Return(nil) + dashboardService.On("DeleteDashboard", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(nil) + hs.callDeleteDashboardByUID(t, sc, dashboardService) assert.Equal(t, 200, sc.resp.Code) }, mockSQLStore) @@ -564,27 +567,13 @@ func TestDashboardAPIEndpoint(t *testing.T) { Message: "msg", } - mock := &dashboards.FakeDashboardService{ - SaveDashboardResult: &models.Dashboard{ - Id: dashID, - Uid: "uid", - Title: "Dash", - Slug: "dash", - Version: 2, - }, - } + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardService.On("SaveDashboard", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), mock.AnythingOfType("bool")). + Return(&models.Dashboard{Id: dashID, Uid: "uid", Title: "Dash", Slug: "dash", Version: 2}, nil) - postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", cmd, mock, nil, func(sc *scenarioContext) { + postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", cmd, dashboardService, nil, func(sc *scenarioContext) { callPostDashboardShouldReturnSuccess(sc) - dto := mock.SavedDashboards[0] - assert.Equal(t, cmd.OrgId, dto.OrgId) - assert.Equal(t, cmd.UserId, dto.User.UserId) - assert.Equal(t, folderID, dto.Dashboard.FolderId) - assert.Equal(t, "Dash", dto.Dashboard.Title) - assert.True(t, dto.Overwrite) - assert.Equal(t, "msg", dto.Message) - result := sc.ToJSON() assert.Equal(t, "success", result.Get("status").MustString()) assert.Equal(t, dashID, result.Get("id").MustInt64()) @@ -610,30 +599,17 @@ func TestDashboardAPIEndpoint(t *testing.T) { Message: "msg", } - mock := &dashboards.FakeDashboardService{ - SaveDashboardResult: &models.Dashboard{ - Id: dashID, - Uid: "uid", - Title: "Dash", - Slug: "dash", - Version: 2, - }, - } + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardService.On("SaveDashboard", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), mock.AnythingOfType("bool")). + Return(&models.Dashboard{Id: dashID, Uid: "uid", Title: "Dash", Slug: "dash", Version: 2}, nil) mockFolder := &fakeFolderService{ GetFolderByUIDResult: &models.Folder{Id: 1, Uid: "folderUID", Title: "Folder"}, } - postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", cmd, mock, mockFolder, func(sc *scenarioContext) { + postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", cmd, dashboardService, mockFolder, func(sc *scenarioContext) { callPostDashboardShouldReturnSuccess(sc) - dto := mock.SavedDashboards[0] - assert.Equal(t, cmd.OrgId, dto.OrgId) - assert.Equal(t, cmd.UserId, dto.User.UserId) - assert.Equal(t, "Dash", dto.Dashboard.Title) - assert.True(t, dto.Overwrite) - assert.Equal(t, "msg", dto.Message) - result := sc.ToJSON() assert.Equal(t, "success", result.Get("status").MustString()) assert.Equal(t, dashID, result.Get("id").MustInt64()) @@ -644,9 +620,6 @@ func TestDashboardAPIEndpoint(t *testing.T) { }) t.Run("Given a request with incorrect folder uid for creating a dashboard with", func(t *testing.T) { - const folderUid string = "folderUID" - const dashID int64 = 2 - cmd := models.SaveDashboardCommand{ OrgId: 1, UserId: 5, @@ -654,26 +627,18 @@ func TestDashboardAPIEndpoint(t *testing.T) { "title": "Dash", }), Overwrite: true, - FolderUid: folderUid, + FolderUid: "folderUID", IsFolder: false, Message: "msg", } - mock := &dashboards.FakeDashboardService{ - SaveDashboardResult: &models.Dashboard{ - Id: dashID, - Uid: "uid", - Title: "Dash", - Slug: "dash", - Version: 2, - }, - } + dashboardService := dashboards.NewFakeDashboardService(t) mockFolder := &fakeFolderService{ GetFolderByUIDError: errors.New("Error while searching Folder ID"), } - postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", cmd, mock, mockFolder, func(sc *scenarioContext) { + postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", cmd, dashboardService, mockFolder, func(sc *scenarioContext) { callPostDashboard(sc) assert.Equal(t, 500, sc.resp.Code) }) @@ -713,12 +678,11 @@ func TestDashboardAPIEndpoint(t *testing.T) { } for _, tc := range testCases { - mock := &dashboards.FakeDashboardService{ - SaveDashboardError: tc.SaveError, - } + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardService.On("SaveDashboard", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), mock.AnythingOfType("bool")).Return(nil, tc.SaveError) postDashboardScenario(t, fmt.Sprintf("Expect '%s' error when calling POST on", tc.SaveError.Error()), - "/api/dashboards", "/api/dashboards", cmd, mock, nil, func(sc *scenarioContext) { + "/api/dashboards", "/api/dashboards", cmd, dashboardService, nil, func(sc *scenarioContext) { callPostDashboard(sc) assert.Equal(t, tc.ExpectedStatusCode, sc.resp.Code) }) @@ -788,21 +752,17 @@ func TestDashboardAPIEndpoint(t *testing.T) { fakeDash.FolderId = folderID fakeDash.HasAcl = false - saved := &models.Dashboard{ - Id: 2, - Uid: "uid", - Title: "Dash", - Slug: "dash", - Version: 1, - } - - mock := &dashboards.FakeDashboardService{ - SaveDashboardResult: saved, - GetDashboardFn: func(ctx context.Context, cmd *models.GetDashboardQuery) error { - cmd.Result = fakeDash - return nil - }, - } + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardQuery) + q.Result = fakeDash + }).Return(nil) + dashboardService.On("SaveDashboard", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), mock.AnythingOfType("bool")).Run(func(args mock.Arguments) { + cmd := args.Get(1).(*dashboards.SaveDashboardDTO) + cmd.Dashboard = &models.Dashboard{ + Id: 2, Uid: "uid", Title: "Dash", Slug: "dash", Version: 1, + } + }).Return(nil, nil) cmd := dtos.RestoreDashboardVersionCommand{ Version: 1, @@ -814,15 +774,11 @@ func TestDashboardAPIEndpoint(t *testing.T) { Version: 1, Data: fakeDash.Data, }} + restoreDashboardVersionScenario(t, "When calling POST on", "/api/dashboards/id/1/restore", - "/api/dashboards/id/:dashboardId/restore", mock, cmd, func(sc *scenarioContext) { + "/api/dashboards/id/:dashboardId/restore", dashboardService, cmd, func(sc *scenarioContext) { callRestoreDashboardVersion(sc) assert.Equal(t, 200, sc.resp.Code) - dto := mock.SavedDashboards[0] - - assert.Equal(t, folderID, dto.Dashboard.FolderId) - assert.Equal(t, "Child dash", dto.Dashboard.Title) - assert.Equal(t, "Restored from version 1", dto.Message) }, mockSQLStore) }) @@ -831,21 +787,17 @@ func TestDashboardAPIEndpoint(t *testing.T) { fakeDash.Id = 2 fakeDash.HasAcl = false - saved := &models.Dashboard{ - Id: 2, - Uid: "uid", - Title: "Dash", - Slug: "dash", - Version: 1, - } - - mock := &dashboards.FakeDashboardService{ - SaveDashboardResult: saved, - GetDashboardFn: func(ctx context.Context, cmd *models.GetDashboardQuery) error { - cmd.Result = fakeDash - return nil - }, - } + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardQuery) + q.Result = fakeDash + }).Return(nil) + dashboardService.On("SaveDashboard", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), mock.AnythingOfType("bool")).Run(func(args mock.Arguments) { + cmd := args.Get(1).(*dashboards.SaveDashboardDTO) + cmd.Dashboard = &models.Dashboard{ + Id: 2, Uid: "uid", Title: "Dash", Slug: "dash", Version: 1, + } + }).Return(nil, nil) cmd := dtos.RestoreDashboardVersionCommand{ Version: 1, @@ -858,14 +810,9 @@ func TestDashboardAPIEndpoint(t *testing.T) { Data: fakeDash.Data, }} restoreDashboardVersionScenario(t, "When calling POST on", "/api/dashboards/id/1/restore", - "/api/dashboards/id/:dashboardId/restore", mock, cmd, func(sc *scenarioContext) { + "/api/dashboards/id/:dashboardId/restore", dashboardService, cmd, func(sc *scenarioContext) { callRestoreDashboardVersion(sc) assert.Equal(t, 200, sc.resp.Code) - dto := mock.SavedDashboards[0] - - assert.Equal(t, int64(0), dto.Dashboard.FolderId) - assert.Equal(t, "Child dash", dto.Dashboard.Title) - assert.Equal(t, "Restored from version 1", dto.Message) }, mockSQLStore) }) @@ -879,12 +826,12 @@ func TestDashboardAPIEndpoint(t *testing.T) { dataValue, err := simplejson.NewJson([]byte(`{"id": 1, "editable": true, "style": "dark"}`)) require.NoError(t, err) - dashboardService := &dashboards.FakeDashboardService{ - GetDashboardFn: func(ctx context.Context, cmd *models.GetDashboardQuery) error { - cmd.Result = &models.Dashboard{Id: 1, Data: dataValue} - return nil - }, - } + + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardQuery) + q.Result = &models.Dashboard{Id: 1, Data: dataValue} + }).Return(nil) loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) { setUp() @@ -950,15 +897,15 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr dashboardStore = database.ProvideDashboardStore(sql) } - if dashboardService == nil { - dashboardService = &dashboards.FakeDashboardService{} - } - libraryPanelsService := mockLibraryPanelService{} libraryElementsService := mockLibraryElementService{} cfg := setting.NewCfg() features := featuremgmt.WithFeatures() + if dashboardService == nil { + dashboardService = service.ProvideDashboardService(cfg, dashboardStore, nil, features, nil, accesscontrolmock.NewMockedPermissionsService()) + } + hs := &HTTPServer{ Cfg: cfg, LibraryPanelService: &libraryPanelsService, diff --git a/pkg/api/preferences_test.go b/pkg/api/preferences_test.go index a8178fa385f..490cde44649 100644 --- a/pkg/api/preferences_test.go +++ b/pkg/api/preferences_test.go @@ -1,7 +1,6 @@ package api import ( - "context" "encoding/json" "io/ioutil" "net/http" @@ -9,6 +8,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/models" @@ -34,12 +34,12 @@ var ( func TestAPIEndpoint_GetCurrentOrgPreferences_LegacyAccessControl(t *testing.T) { sc := setupHTTPServer(t, true, false) - dashSvc := &dashboards.FakeDashboardService{ - GetDashboardFn: func(ctx context.Context, cmd *models.GetDashboardQuery) error { - cmd.Result = &models.Dashboard{Uid: "home", Id: 1} - return nil - }, - } + dashSvc := dashboards.NewFakeDashboardService(t) + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardQuery) + q.Result = &models.Dashboard{Uid: "home", Id: 1} + }).Return(nil) + sc.hs.dashboardService = dashSvc prefService := preftest.NewPreferenceServiceFake() @@ -164,7 +164,11 @@ func TestAPIEndpoint_PatchUserPreferences(t *testing.T) { assert.Equal(t, http.StatusBadRequest, response.Code) }) input = strings.NewReader(testUpdateOrgPreferencesWithHomeDashboardUIDCmd) - dashSvc := &dashboards.FakeDashboardService{} + dashSvc := dashboards.NewFakeDashboardService(t) + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardQuery) + q.Result = &models.Dashboard{Uid: "home", Id: 1} + }).Return(nil) sc.hs.dashboardService = dashSvc t.Run("Returns 200 on success", func(t *testing.T) { response := callAPI(sc.server, http.MethodPatch, patchUserPreferencesUrl, input, t) diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go index 78d749636e2..b50af60fd64 100644 --- a/pkg/services/dashboards/dashboard.go +++ b/pkg/services/dashboards/dashboard.go @@ -6,17 +6,12 @@ import ( "github.com/grafana/grafana/pkg/models" ) -// Generating mocks is handled by vektra/mockery -// 1. install go mockery https://github.com/vektra/mockery#go-install -// 2. add your method to the relevant services -// 3. from the same directory as this file run `go generate` and it will update the mock -// If you don't see any output, this most likely means your OS can't find the mockery binary -// `which mockery` to confirm and follow one of the installation methods - +//go:generate mockery --name DashboardService --structname FakeDashboardService --inpackage --filename dashboard_service_mock.go // DashboardService is a service for operating on dashboards. type DashboardService interface { BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error + FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) GetDashboard(ctx context.Context, query *models.GetDashboardQuery) error GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error @@ -25,6 +20,7 @@ type DashboardService interface { MakeUserAdmin(ctx context.Context, orgID int64, userID, dashboardID int64, setViewAndEditPermissions bool) error SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error) SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*models.PublicDashboardConfig, error) + SearchDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) error UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardAcl) error } @@ -51,6 +47,7 @@ type DashboardProvisioningService interface { type Store interface { DeleteDashboard(ctx context.Context, cmd *models.DeleteDashboardCommand) error DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error + FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) GetDashboard(ctx context.Context, query *models.GetDashboardQuery) error GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error @@ -65,6 +62,7 @@ type Store interface { SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error) SaveProvisionedDashboard(cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) SavePublicDashboardConfig(cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboardConfig, error) + SearchDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) error UnprovisionDashboard(ctx context.Context, id int64) error UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardAcl) error // ValidateDashboardBeforeSave validates a dashboard before save. diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index 72a956e96c6..f06cee26d8b 100644 --- a/pkg/services/dashboards/dashboard_service_mock.go +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -1,79 +1,263 @@ +// Code generated by mockery v2.12.2. DO NOT EDIT. + package dashboards import ( - "context" + context "context" - "github.com/grafana/grafana/pkg/models" + models "github.com/grafana/grafana/pkg/models" + mock "github.com/stretchr/testify/mock" + + testing "testing" ) +// FakeDashboardService is an autogenerated mock type for the DashboardService type type FakeDashboardService struct { - DashboardService - - SaveDashboardResult *models.Dashboard - SavedDashboards []*SaveDashboardDTO - ProvisionedDashData *models.DashboardProvisioning - - PublicDashboardConfigResult *models.PublicDashboardConfig - PublicDashboardConfigError error - SaveDashboardError error - GetDashboardFn func(ctx context.Context, cmd *models.GetDashboardQuery) error + mock.Mock } -func (s *FakeDashboardService) SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error) { - s.SavedDashboards = append(s.SavedDashboards, dto) +// BuildSaveDashboardCommand provides a mock function with given fields: ctx, dto, shouldValidateAlerts, validateProvisionedDashboard +func (_m *FakeDashboardService) BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) { + ret := _m.Called(ctx, dto, shouldValidateAlerts, validateProvisionedDashboard) - if s.SaveDashboardResult == nil && s.SaveDashboardError == nil { - s.SaveDashboardResult = dto.Dashboard - } - - return s.SaveDashboardResult, s.SaveDashboardError -} - -func (s *FakeDashboardService) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) { - return s.PublicDashboardConfigResult, s.PublicDashboardConfigError -} - -func (s *FakeDashboardService) SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*models.PublicDashboardConfig, error) { - return s.PublicDashboardConfigResult, s.PublicDashboardConfigError -} - -func (s *FakeDashboardService) ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*models.Dashboard, error) { - return s.SaveDashboard(ctx, dto, true) -} - -func (s *FakeDashboardService) DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error { - for index, dash := range s.SavedDashboards { - if dash.Dashboard.Id == dashboardId && dash.OrgId == orgId { - s.SavedDashboards = append(s.SavedDashboards[:index], s.SavedDashboards[index+1:]...) - break + var r0 *models.SaveDashboardCommand + if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, bool, bool) *models.SaveDashboardCommand); ok { + r0 = rf(ctx, dto, shouldValidateAlerts, validateProvisionedDashboard) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.SaveDashboardCommand) } } - return nil -} -func (s *FakeDashboardService) GetProvisionedDashboardDataByDashboardID(id int64) (*models.DashboardProvisioning, error) { - return s.ProvisionedDashData, nil -} -func (s *FakeDashboardService) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error { - return nil -} - -func (s *FakeDashboardService) GetDashboard(ctx context.Context, cmd *models.GetDashboardQuery) error { - if s.GetDashboardFn != nil { - return s.GetDashboardFn(ctx, cmd) + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *SaveDashboardDTO, bool, bool) error); ok { + r1 = rf(ctx, dto, shouldValidateAlerts, validateProvisionedDashboard) + } else { + r1 = ret.Error(1) } - // A minimal result for tests that need a valid result, but don't care what's in it. - d := models.NewDashboard("mocked") - d.Id = 1 - d.Uid = "1" - cmd.Result = d - return nil + + return r0, r1 } -func (s *FakeDashboardService) GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error { - return nil +// DeleteDashboard provides a mock function with given fields: ctx, dashboardId, orgId +func (_m *FakeDashboardService) DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error { + ret := _m.Called(ctx, dashboardId, orgId) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, int64) error); ok { + r0 = rf(ctx, dashboardId, orgId) + } else { + r0 = ret.Error(0) + } + + return r0 } -func (s *FakeDashboardService) GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error { - return nil +// FindDashboards provides a mock function with given fields: ctx, query +func (_m *FakeDashboardService) FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) { + ret := _m.Called(ctx, query) + + var r0 []DashboardSearchProjection + if rf, ok := ret.Get(0).(func(context.Context, *models.FindPersistedDashboardsQuery) []DashboardSearchProjection); ok { + r0 = rf(ctx, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]DashboardSearchProjection) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *models.FindPersistedDashboardsQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetDashboard provides a mock function with given fields: ctx, query +func (_m *FakeDashboardService) GetDashboard(ctx context.Context, query *models.GetDashboardQuery) error { + ret := _m.Called(ctx, query) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *models.GetDashboardQuery) error); ok { + r0 = rf(ctx, query) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetDashboardUIDById provides a mock function with given fields: ctx, query +func (_m *FakeDashboardService) GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error { + ret := _m.Called(ctx, query) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *models.GetDashboardRefByIdQuery) error); ok { + r0 = rf(ctx, query) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetDashboards provides a mock function with given fields: ctx, query +func (_m *FakeDashboardService) GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error { + ret := _m.Called(ctx, query) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *models.GetDashboardsQuery) error); ok { + r0 = rf(ctx, query) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetPublicDashboardConfig provides a mock function with given fields: ctx, orgId, dashboardUid +func (_m *FakeDashboardService) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) { + ret := _m.Called(ctx, orgId, dashboardUid) + + var r0 *models.PublicDashboardConfig + if rf, ok := ret.Get(0).(func(context.Context, int64, string) *models.PublicDashboardConfig); ok { + r0 = rf(ctx, orgId, dashboardUid) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.PublicDashboardConfig) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok { + r1 = rf(ctx, orgId, dashboardUid) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ImportDashboard provides a mock function with given fields: ctx, dto +func (_m *FakeDashboardService) ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*models.Dashboard, error) { + ret := _m.Called(ctx, dto) + + var r0 *models.Dashboard + if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO) *models.Dashboard); ok { + r0 = rf(ctx, dto) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Dashboard) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *SaveDashboardDTO) error); ok { + r1 = rf(ctx, dto) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MakeUserAdmin provides a mock function with given fields: ctx, orgID, userID, dashboardID, setViewAndEditPermissions +func (_m *FakeDashboardService) MakeUserAdmin(ctx context.Context, orgID int64, userID int64, dashboardID int64, setViewAndEditPermissions bool) error { + ret := _m.Called(ctx, orgID, userID, dashboardID, setViewAndEditPermissions) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, int64, int64, bool) error); ok { + r0 = rf(ctx, orgID, userID, dashboardID, setViewAndEditPermissions) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SaveDashboard provides a mock function with given fields: ctx, dto, allowUiUpdate +func (_m *FakeDashboardService) SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error) { + ret := _m.Called(ctx, dto, allowUiUpdate) + + var r0 *models.Dashboard + if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, bool) *models.Dashboard); ok { + r0 = rf(ctx, dto, allowUiUpdate) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Dashboard) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *SaveDashboardDTO, bool) error); ok { + r1 = rf(ctx, dto, allowUiUpdate) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SavePublicDashboardConfig provides a mock function with given fields: ctx, dto +func (_m *FakeDashboardService) SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*models.PublicDashboardConfig, error) { + ret := _m.Called(ctx, dto) + + var r0 *models.PublicDashboardConfig + if rf, ok := ret.Get(0).(func(context.Context, *SavePublicDashboardConfigDTO) *models.PublicDashboardConfig); ok { + r0 = rf(ctx, dto) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.PublicDashboardConfig) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *SavePublicDashboardConfigDTO) error); ok { + r1 = rf(ctx, dto) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SearchDashboards provides a mock function with given fields: ctx, query +func (_m *FakeDashboardService) SearchDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) error { + ret := _m.Called(ctx, query) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *models.FindPersistedDashboardsQuery) error); ok { + r0 = rf(ctx, query) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateDashboardACL provides a mock function with given fields: ctx, uid, items +func (_m *FakeDashboardService) UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardAcl) error { + ret := _m.Called(ctx, uid, items) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, []*models.DashboardAcl) error); ok { + r0 = rf(ctx, uid, items) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewFakeDashboardService creates a new instance of FakeDashboardService. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. +func NewFakeDashboardService(t testing.TB) *FakeDashboardService { + mock := &FakeDashboardService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock } diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index be20d8a5529..684477e7291 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -15,19 +15,22 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/services/sqlstore/permissions" + "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/util" ) type DashboardStore struct { sqlStore *sqlstore.SQLStore log log.Logger + dialect migrator.Dialect } // DashboardStore implements the Store interface var _ dashboards.Store = (*DashboardStore)(nil) func ProvideDashboardStore(sqlStore *sqlstore.SQLStore) *DashboardStore { - return &DashboardStore{sqlStore: sqlStore, log: log.New("dashboard-store")} + return &DashboardStore{sqlStore: sqlStore, log: log.New("dashboard-store"), dialect: sqlStore.Dialect} } func (d *DashboardStore) ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error) { @@ -926,3 +929,143 @@ func (d *DashboardStore) GetDashboards(ctx context.Context, query *models.GetDas return err }) } + +func (d *DashboardStore) SearchDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) error { + res, err := d.FindDashboards(ctx, query) + if err != nil { + return err + } + + makeQueryResult(query, res) + + return nil +} + +func getHitType(item dashboards.DashboardSearchProjection) models.HitType { + var hitType models.HitType + if item.IsFolder { + hitType = models.DashHitFolder + } else { + hitType = models.DashHitDB + } + + return hitType +} + +func makeQueryResult(query *models.FindPersistedDashboardsQuery, res []dashboards.DashboardSearchProjection) { + query.Result = make([]*models.Hit, 0) + hits := make(map[int64]*models.Hit) + + for _, item := range res { + hit, exists := hits[item.ID] + if !exists { + hit = &models.Hit{ + ID: item.ID, + UID: item.UID, + Title: item.Title, + URI: "db/" + item.Slug, + URL: models.GetDashboardFolderUrl(item.IsFolder, item.UID, item.Slug), + Type: getHitType(item), + FolderID: item.FolderID, + FolderUID: item.FolderUID, + FolderTitle: item.FolderTitle, + Tags: []string{}, + } + + if item.FolderID > 0 { + hit.FolderURL = models.GetFolderUrl(item.FolderUID, item.FolderSlug) + } + + if query.Sort.MetaName != "" { + hit.SortMeta = item.SortMeta + hit.SortMetaName = query.Sort.MetaName + } + + query.Result = append(query.Result, hit) + hits[item.ID] = hit + } + if len(item.Term) > 0 { + hit.Tags = append(hit.Tags, item.Term) + } + } +} + +func (d *DashboardStore) FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) { + filters := []interface{}{ + permissions.DashboardPermissionFilter{ + OrgRole: query.SignedInUser.OrgRole, + OrgId: query.SignedInUser.OrgId, + Dialect: d.dialect, + UserId: query.SignedInUser.UserId, + PermissionLevel: query.Permission, + }, + } + + if !ac.IsDisabled(d.sqlStore.Cfg) { + // if access control is enabled, overwrite the filters so far + filters = []interface{}{ + permissions.NewAccessControlDashboardPermissionFilter(query.SignedInUser, query.Permission, query.Type), + } + } + + for _, filter := range query.Sort.Filter { + filters = append(filters, filter) + } + + filters = append(filters, query.Filters...) + + if query.OrgId != 0 { + filters = append(filters, searchstore.OrgFilter{OrgId: query.OrgId}) + } else if query.SignedInUser.OrgId != 0 { + filters = append(filters, searchstore.OrgFilter{OrgId: query.SignedInUser.OrgId}) + } + + if len(query.Tags) > 0 { + filters = append(filters, searchstore.TagsFilter{Tags: query.Tags}) + } + + if len(query.DashboardIds) > 0 { + filters = append(filters, searchstore.DashboardFilter{IDs: query.DashboardIds}) + } + + if query.IsStarred { + filters = append(filters, searchstore.StarredFilter{UserId: query.SignedInUser.UserId}) + } + + if len(query.Title) > 0 { + filters = append(filters, searchstore.TitleFilter{Dialect: d.dialect, Title: query.Title}) + } + + if len(query.Type) > 0 { + filters = append(filters, searchstore.TypeFilter{Dialect: d.dialect, Type: query.Type}) + } + + if len(query.FolderIds) > 0 { + filters = append(filters, searchstore.FolderFilter{IDs: query.FolderIds}) + } + + var res []dashboards.DashboardSearchProjection + sb := &searchstore.Builder{Dialect: d.dialect, Filters: filters} + + limit := query.Limit + if limit < 1 { + limit = 1000 + } + + page := query.Page + if page < 1 { + page = 1 + } + + sql, params := sb.ToSQL(limit, page) + + err := d.sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + return sess.SQL(sql, params...).Find(&res) + }) + + if err != nil { + return nil, err + } + + return res, nil +} diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index 724115b3483..df3ac369574 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -242,7 +242,7 @@ func TestDashboardDataAccess(t *testing.T) { SignedInUser: &models.SignedInUser{}, } - err = sqlStore.SearchDashboards(context.Background(), &query) + err = dashboardStore.SearchDashboards(context.Background(), &query) require.NoError(t, err) require.Equal(t, len(query.Result), 0) @@ -315,7 +315,7 @@ func TestDashboardDataAccess(t *testing.T) { }, } - err := sqlStore.SearchDashboards(context.Background(), &query) + err := dashboardStore.SearchDashboards(context.Background(), &query) require.NoError(t, err) require.Equal(t, len(query.Result), 1) @@ -339,7 +339,7 @@ func TestDashboardDataAccess(t *testing.T) { }, } - err := sqlStore.SearchDashboards(context.Background(), &query) + err := dashboardStore.SearchDashboards(context.Background(), &query) require.NoError(t, err) require.Equal(t, len(query.Result), 1) @@ -364,7 +364,7 @@ func TestDashboardDataAccess(t *testing.T) { }, } - err := sqlStore.SearchDashboards(context.Background(), &query) + err := dashboardStore.SearchDashboards(context.Background(), &query) require.NoError(t, err) require.Equal(t, len(query.Result), 1) @@ -386,7 +386,7 @@ func TestDashboardDataAccess(t *testing.T) { }, } - err := sqlStore.SearchDashboards(context.Background(), &query) + err := dashboardStore.SearchDashboards(context.Background(), &query) require.NoError(t, err) require.Equal(t, len(query.Result), 3) @@ -407,7 +407,7 @@ func TestDashboardDataAccess(t *testing.T) { }, } - err := sqlStore.SearchDashboards(context.Background(), &query) + err := dashboardStore.SearchDashboards(context.Background(), &query) require.NoError(t, err) require.Equal(t, len(query.Result), 2) @@ -433,7 +433,7 @@ func TestDashboardDataAccess(t *testing.T) { }, } - err := sqlStore.SearchDashboards(context.Background(), &query) + err := dashboardStore.SearchDashboards(context.Background(), &query) require.NoError(t, err) require.Equal(t, len(query.Result), 2) @@ -471,7 +471,7 @@ func TestDashboardDataAccess(t *testing.T) { }, IsStarred: true, } - err = sqlStore.SearchDashboards(context.Background(), &query) + err = dashboardStore.SearchDashboards(context.Background(), &query) require.NoError(t, err) require.Equal(t, len(query.Result), 1) diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index ef8cd5fafe1..662f9ad7cba 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -20,3 +20,17 @@ type SavePublicDashboardConfigDTO struct { OrgId int64 PublicDashboardConfig models.PublicDashboardConfig } + +type DashboardSearchProjection struct { + ID int64 `xorm:"id"` + UID string `xorm:"uid"` + Title string + Slug string + Term string + IsFolder bool + FolderID int64 `xorm:"folder_id"` + FolderUID string `xorm:"folder_uid"` + FolderSlug string + FolderTitle string + SortMeta int64 +} diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 2ef1e9ddb48..2be7f6fc686 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/alerting" - m "github.com/grafana/grafana/pkg/services/dashboards" + "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/setting" @@ -22,19 +22,19 @@ import ( var ( provisionerPermissions = map[string][]string{ - m.ActionFoldersCreate: {}, - m.ActionFoldersWrite: {m.ScopeFoldersAll}, - m.ActionDashboardsCreate: {m.ScopeFoldersAll}, - m.ActionDashboardsWrite: {m.ScopeFoldersAll}, + dashboards.ActionFoldersCreate: {}, + dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}, + dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersAll}, + dashboards.ActionDashboardsWrite: {dashboards.ScopeFoldersAll}, } // DashboardServiceImpl implements the DashboardService interface - _ m.DashboardService = (*DashboardServiceImpl)(nil) + _ dashboards.DashboardService = (*DashboardServiceImpl)(nil) ) type DashboardServiceImpl struct { cfg *setting.Cfg log log.Logger - dashboardStore m.Store + dashboardStore dashboards.Store dashAlertExtractor alerting.DashAlertExtractor features featuremgmt.FeatureToggles folderPermissions accesscontrol.FolderPermissionsService @@ -42,7 +42,7 @@ type DashboardServiceImpl struct { } func ProvideDashboardService( - cfg *setting.Cfg, store m.Store, dashAlertExtractor alerting.DashAlertExtractor, + cfg *setting.Cfg, store dashboards.Store, dashAlertExtractor alerting.DashAlertExtractor, features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService, dashboardPermissionsService accesscontrol.DashboardPermissionsService, ) *DashboardServiceImpl { @@ -69,7 +69,7 @@ func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardUID(orgID return dr.dashboardStore.GetProvisionedDataByDashboardUID(orgID, dashboardUID) } -func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, dto *m.SaveDashboardDTO, shouldValidateAlerts bool, +func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, dto *dashboards.SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) { dash := dto.Dashboard @@ -204,7 +204,7 @@ func validateDashboardRefreshInterval(dash *models.Dashboard) error { return nil } -func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dto *m.SaveDashboardDTO, +func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { if err := validateDashboardRefreshInterval(dto.Dashboard); err != nil { dr.log.Warn("Changing refresh interval for provisioned dashboard to minimum refresh interval", "dashboardUid", @@ -258,7 +258,7 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt return dash, nil } -func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.Context, dto *m.SaveDashboardDTO) (*models.Dashboard, error) { +func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*models.Dashboard, error) { dto.User = &models.SignedInUser{ UserId: 0, OrgRole: models.ROLE_ADMIN, @@ -299,7 +299,7 @@ func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.C return dash, nil } -func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *m.SaveDashboardDTO, +func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error) { if err := validateDashboardRefreshInterval(dto.Dashboard); err != nil { dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval", @@ -356,7 +356,7 @@ func (dr *DashboardServiceImpl) GetPublicDashboardConfig(ctx context.Context, or // SavePublicDashboardConfig is a helper method to persist the sharing config // to the database. It handles validations for sharing config and persistence -func (dr *DashboardServiceImpl) SavePublicDashboardConfig(ctx context.Context, dto *m.SavePublicDashboardConfigDTO) (*models.PublicDashboardConfig, error) { +func (dr *DashboardServiceImpl) SavePublicDashboardConfig(ctx context.Context, dto *dashboards.SavePublicDashboardConfigDTO) (*models.PublicDashboardConfig, error) { cmd := models.SavePublicDashboardConfigCommand{ Uid: dto.Uid, OrgId: dto.OrgId, @@ -440,7 +440,7 @@ func (dr *DashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId return dr.dashboardStore.DeleteDashboard(ctx, cmd) } -func (dr *DashboardServiceImpl) ImportDashboard(ctx context.Context, dto *m.SaveDashboardDTO) ( +func (dr *DashboardServiceImpl) ImportDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO) ( *models.Dashboard, error) { if err := validateDashboardRefreshInterval(dto.Dashboard); err != nil { dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval", @@ -476,7 +476,7 @@ func (dr *DashboardServiceImpl) GetDashboardsByPluginID(ctx context.Context, que return dr.dashboardStore.GetDashboardsByPluginID(ctx, query) } -func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto *m.SaveDashboardDTO, dash *models.Dashboard, provisioned bool) error { +func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto *dashboards.SaveDashboardDTO, dash *models.Dashboard, provisioned bool) error { inFolder := dash.FolderId > 0 if !accesscontrol.IsDisabled(dr.cfg) { var permissions []accesscontrol.SetResourcePermissionCommand @@ -522,3 +522,11 @@ func (dr *DashboardServiceImpl) GetDashboardUIDById(ctx context.Context, query * func (dr *DashboardServiceImpl) GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error { return dr.dashboardStore.GetDashboards(ctx, query) } + +func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) { + return dr.dashboardStore.FindDashboards(ctx, query) +} + +func (dr *DashboardServiceImpl) SearchDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) error { + return dr.dashboardStore.SearchDashboards(ctx, query) +} diff --git a/pkg/services/dashboards/service/folder_service_test.go b/pkg/services/dashboards/service/folder_service_test.go index aa025444a7e..38040ce189f 100644 --- a/pkg/services/dashboards/service/folder_service_test.go +++ b/pkg/services/dashboards/service/folder_service_test.go @@ -37,8 +37,7 @@ func TestProvideFolderService(t *testing.T) { ac := acmock.New() ProvideFolderService( - cfg, &dashboards.FakeDashboardService{DashboardService: dashboardService}, - store, nil, features, folderPermissions, ac, + cfg, dashboardService, store, nil, features, folderPermissions, ac, ) require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 2) diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index 35ad8b1afdd..bce5a3c9825 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_mock.go @@ -44,6 +44,29 @@ func (_m *FakeDashboardStore) DeleteOrphanedProvisionedDashboards(ctx context.Co return r0 } +// FindDashboards provides a mock function with given fields: ctx, query +func (_m *FakeDashboardStore) FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) { + ret := _m.Called(ctx, query) + + var r0 []DashboardSearchProjection + if rf, ok := ret.Get(0).(func(context.Context, *models.FindPersistedDashboardsQuery) []DashboardSearchProjection); ok { + r0 = rf(ctx, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]DashboardSearchProjection) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *models.FindPersistedDashboardsQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetDashboard provides a mock function with given fields: ctx, query func (_m *FakeDashboardStore) GetDashboard(ctx context.Context, query *models.GetDashboardQuery) error { ret := _m.Called(ctx, query) @@ -344,6 +367,20 @@ func (_m *FakeDashboardStore) SavePublicDashboardConfig(cmd models.SavePublicDas return r0, r1 } +// SearchDashboards provides a mock function with given fields: ctx, query +func (_m *FakeDashboardStore) SearchDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) error { + ret := _m.Called(ctx, query) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *models.FindPersistedDashboardsQuery) error); ok { + r0 = rf(ctx, query) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // UnprovisionDashboard provides a mock function with given fields: ctx, id func (_m *FakeDashboardStore) UnprovisionDashboard(ctx context.Context, id int64) error { ret := _m.Called(ctx, id) diff --git a/pkg/services/guardian/accesscontrol_guardian_test.go b/pkg/services/guardian/accesscontrol_guardian_test.go index cd07d18a44b..00ec21bd7e9 100644 --- a/pkg/services/guardian/accesscontrol_guardian_test.go +++ b/pkg/services/guardian/accesscontrol_guardian_test.go @@ -101,8 +101,7 @@ func TestAccessControlDashboardGuardian_CanSave(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - guardian, _ := setupAccessControlGuardianTest(t, tt.dashUID, tt.permissions) - + guardian, _ := setupAccessControlGuardianTest(t, tt.dashUID, tt.permissions, testDashSvc(t)) can, err := guardian.CanSave() require.NoError(t, err) assert.Equal(t, tt.expected, can) @@ -193,7 +192,7 @@ func TestAccessControlDashboardGuardian_CanEdit(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - guardian, _ := setupAccessControlGuardianTest(t, tt.dashUID, tt.permissions) + guardian, _ := setupAccessControlGuardianTest(t, tt.dashUID, tt.permissions, testDashSvc(t)) if tt.viewersCanEdit { setting.ViewersCanEdit = true @@ -277,7 +276,7 @@ func TestAccessControlDashboardGuardian_CanView(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - guardian, _ := setupAccessControlGuardianTest(t, tt.dashUID, tt.permissions) + guardian, _ := setupAccessControlGuardianTest(t, tt.dashUID, tt.permissions, testDashSvc(t)) can, err := guardian.CanView() require.NoError(t, err) @@ -381,7 +380,7 @@ func TestAccessControlDashboardGuardian_CanAdmin(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - guardian, _ := setupAccessControlGuardianTest(t, tt.dashUID, tt.permissions) + guardian, _ := setupAccessControlGuardianTest(t, tt.dashUID, tt.permissions, testDashSvc(t)) can, err := guardian.CanAdmin() require.NoError(t, err) @@ -461,7 +460,7 @@ func TestAccessControlDashboardGuardian_CanDelete(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - guardian, _ := setupAccessControlGuardianTest(t, tt.dashUID, tt.permissions) + guardian, _ := setupAccessControlGuardianTest(t, tt.dashUID, tt.permissions, testDashSvc(t)) can, err := guardian.CanDelete() require.NoError(t, err) @@ -525,7 +524,7 @@ func TestAccessControlDashboardGuardian_CanCreate(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - guardian, _ := setupAccessControlGuardianTest(t, "0", tt.permissions) + guardian, _ := setupAccessControlGuardianTest(t, "0", tt.permissions, nil) can, err := guardian.CanCreate(tt.folderID, tt.isFolder) require.NoError(t, err) @@ -557,7 +556,7 @@ func TestAccessControlDashboardGuardian_GetHiddenACL(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - guardian, _ := setupAccessControlGuardianTest(t, "1", nil) + guardian, _ := setupAccessControlGuardianTest(t, "1", nil, testDashSvc(t)) mocked := accesscontrolmock.NewMockedPermissionsService() guardian.dashboardPermissionsService = mocked @@ -579,7 +578,7 @@ func TestAccessControlDashboardGuardian_GetHiddenACL(t *testing.T) { } } -func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []*accesscontrol.Permission) (*AccessControlDashboardGuardian, *models.Dashboard) { +func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []*accesscontrol.Permission, dashboardSvc dashboards.DashboardService) (*AccessControlDashboardGuardian, *models.Dashboard) { t.Helper() store := sqlstore.InitTestDB(t) @@ -601,6 +600,20 @@ func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []*acc dashboardPermissions, err := ossaccesscontrol.ProvideDashboardPermissions( setting.NewCfg(), routing.NewRouteRegister(), store, ac, database.ProvideService(store), &dashboards.FakeDashboardStore{}) require.NoError(t, err) + if dashboardSvc == nil { + dashboardSvc = &dashboards.FakeDashboardService{} + } + return NewAccessControlDashboardGuardian(context.Background(), dash.Id, &models.SignedInUser{OrgId: 1}, store, ac, folderPermissions, dashboardPermissions, dashboardSvc), dash +} - return NewAccessControlDashboardGuardian(context.Background(), dash.Id, &models.SignedInUser{OrgId: 1}, store, ac, folderPermissions, dashboardPermissions, &dashboards.FakeDashboardService{}), dash +func testDashSvc(t *testing.T) dashboards.DashboardService { + dashSvc := dashboards.NewFakeDashboardService(t) + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardQuery) + d := models.NewDashboard("mocked") + d.Id = 1 + d.Uid = "1" + q.Result = d + }).Return(nil) + return dashSvc } diff --git a/pkg/services/screenshot/screenshot_test.go b/pkg/services/screenshot/screenshot_test.go index 22c9b4c1d72..9cc912a6d42 100644 --- a/pkg/services/screenshot/screenshot_test.go +++ b/pkg/services/screenshot/screenshot_test.go @@ -12,6 +12,7 @@ import ( "github.com/golang/mock/gomock" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/components/imguploader" @@ -78,19 +79,17 @@ func TestBrowserScreenshotService(t *testing.T) { s := NewBrowserScreenshotService(&d, r) // a non-existent dashboard should return error - d.GetDashboardFn = func(ctx context.Context, cmd *models.GetDashboardQuery) error { - return models.ErrDashboardNotFound - } + d.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Return(models.ErrDashboardNotFound).Once() ctx := context.Background() opts := ScreenshotOptions{} screenshot, err := s.Take(ctx, opts) assert.EqualError(t, err, "Dashboard not found") assert.Nil(t, screenshot) - d.GetDashboardFn = func(ctx context.Context, cmd *models.GetDashboardQuery) error { - cmd.Result = &models.Dashboard{Id: 1, Uid: "foo", Slug: "bar", OrgId: 2} - return nil - } + d.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardQuery) + q.Result = &models.Dashboard{Id: 1, Uid: "foo", Slug: "bar", OrgId: 2} + }).Return(nil) renderOpts := rendering.Opts{ AuthOpts: rendering.AuthOpts{