package service import ( "context" "encoding/json" "errors" "fmt" "strings" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/dashboards" dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database" dashsvc "github.com/grafana/grafana/pkg/services/dashboards/service" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder/folderimpl" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" . "github.com/grafana/grafana/pkg/services/publicdashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/publicdashboards/service/intervalv2" "github.com/grafana/grafana/pkg/services/publicdashboards/validation" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/search/sort" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" ) var timeSettings = &TimeSettings{From: "now-12h", To: "now"} var defaultPubdashTimeSettings = &TimeSettings{} var dashboardData = simplejson.NewFromAny(map[string]any{"time": map[string]any{"from": "now-8h", "to": "now"}}) var SignedInUser = &user.SignedInUser{UserID: 1234, Login: "user@login.com"} func TestMain(m *testing.M) { testsuite.Run(m) } func TestLogPrefix(t *testing.T) { assert.Equal(t, LogPrefix, "publicdashboards.service") } func TestGetPublicDashboardForView(t *testing.T) { type storeResp struct { pd *PublicDashboard d *dashboards.Dashboard err error } const dashboardWithRowsAndHiddenQueries = ` { "panels": [ { "id": 2, "targets": [ { "datasource": { "type": "prometheus", "uid": "_yxMP8Ynk" }, "exemplar": true, "expr": "go_goroutines{job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A", "hide": true }, { "datasource": { "type": "prometheus", "uid": "promds2" }, "exemplar": true, "expr": "query2", "interval": "", "legendFormat": "", "refId": "B" } ], "title": "Panel Title", "type": "timeseries" }, { "id": 3, "collapsed": true, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 9 }, "title": "This panel is a Row", "type": "row", "panels": [ { "id": 4, "targets": [ { "datasource": { "type": "prometheus", "uid": "_yxMP8Ynk" }, "exemplar": true, "expr": "go_goroutines{job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "promds2" }, "exemplar": true, "expr": "query2", "interval": "", "legendFormat": "", "refId": "B" } ], "title": "Panel inside a row", "type": "timeseries" } ] }, { "aliasColors": { "total avg": "#6ed0e0" }, "bars": false, "dashLength": 10, "dashes": false, "datasource": { "type": "mssql", "uid": "P6B08AC199690F328" }, "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 2, "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 0, "y": 0 }, "hiddenSeries": false, "id": 4, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "10.2.0-pre", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "total avg", "fill": 0, "pointradius": 3, "points": true } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "alias": "", "datasource": { "type": "mssql", "uid": "P6B08AC199690F328" }, "format": "time_series", "rawSql": "SELECT\n $__timeGroup(createdAt,'$summarize') as time,\n avg(value) as value,\n hostname as metric\nFROM \n grafana_metric\nWHERE\n $__timeFilter(createdAt) AND\n measurement = 'logins.count' AND\n hostname IN($host)\nGROUP BY $__timeGroup(createdAt,'$summarize'), hostname\nORDER BY 1", "refId": "A" }, { "alias": "", "datasource": { "type": "mssql", "uid": "P6B08AC199690F328" }, "format": "time_series", "rawSql": "SELECT\n $__timeGroup(createdAt,'$summarize') as time,\n min(value) as value,\n 'total avg' as metric\nFROM \n grafana_metric\nWHERE\n $__timeFilter(createdAt) AND\n measurement = 'logins.count'\nGROUP BY $__timeGroup(createdAt,'$summarize')\nORDER BY 1", "refId": "B" } ], "thresholds": [], "timeRegions": [], "title": "Average logins / $summarize", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "show": true }, { "format": "short", "logBase": 1, "show": true } ], "yaxis": { "align": false } }, { "datasource": { "type": "influxdb", "uid": "P49A45DF074423DFB" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 9, "w": 12, "x": 0, "y": 0 }, "id": 5, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "/^retentionPeriod 4a2f27036bf63a3c$/", "values": false }, "textMode": "auto" }, "pluginVersion": "10.2.0-pre", "targets": [ { "datasource": { "type": "influxdb", "uid": "P49A45DF074423DFB" }, "query": "buckets()", "refId": "A" } ], "title": "Panel Title", "type": "stat" } ], "schemaVersion": 35, "timepicker": { "hidden": false } }` data, _ := simplejson.NewJson([]byte(dashboardWithRowsAndHiddenQueries)) now := time.Now() // #nosec G101 -- This is dummy/test token accessToken := "c54b1c4dd2b143a1a7a43005264d256d" d := &dashboards.Dashboard{UID: "mydashboard", OrgID: 0, Data: data, Slug: "dashboardSlug", Created: now, Updated: now, Version: 1, FolderUID: "myFolder"} testCases := []struct { Name string AccessToken string StoreResp *storeResp ErrResp error DashResp *dtos.DashboardFullWithMeta }{ { Name: "returns a dashboard with the time picker shown", AccessToken: accessToken, StoreResp: &storeResp{ pd: &PublicDashboard{AccessToken: accessToken, IsEnabled: true, TimeSelectionEnabled: true}, d: d, err: nil, }, ErrResp: nil, DashResp: &dtos.DashboardFullWithMeta{ Dashboard: data, Meta: dtos.DashboardMeta{ Slug: d.Slug, Type: dashboards.DashTypeDB, CanStar: false, CanSave: false, CanEdit: false, CanAdmin: false, CanDelete: false, Created: d.Created, Updated: d.Updated, Version: d.Version, IsFolder: false, FolderUid: d.FolderUID, PublicDashboardEnabled: true, }, }, }, { Name: "returns a dashboard with the time picker hidden", AccessToken: accessToken, StoreResp: &storeResp{ pd: &PublicDashboard{AccessToken: accessToken, IsEnabled: true, TimeSelectionEnabled: false}, d: d, err: nil, }, ErrResp: nil, DashResp: &dtos.DashboardFullWithMeta{ Dashboard: data, Meta: dtos.DashboardMeta{ Slug: d.Slug, Type: dashboards.DashTypeDB, CanStar: false, CanSave: false, CanEdit: false, CanAdmin: false, CanDelete: false, Created: d.Created, Updated: d.Updated, Version: d.Version, IsFolder: false, FolderUid: d.FolderUID, PublicDashboardEnabled: true, }, }, }, } for _, test := range testCases { t.Run(test.Name, func(t *testing.T) { fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.Anything).Return(test.StoreResp.pd, test.StoreResp.err) fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(test.StoreResp.d, test.StoreResp.err) service, _, _ := newPublicDashboardServiceImpl(t, nil, nil, fakeStore, fakeDashboardService, nil) dashboardFullWithMeta, err := service.GetPublicDashboardForView(context.Background(), test.AccessToken) if test.ErrResp != nil { assert.Error(t, test.ErrResp, err) } else { require.NoError(t, err) } //assert.Equal(t, test.DashResp, dash) if test.DashResp != nil { assert.Equal(t, test.DashResp.Meta.Slug, dashboardFullWithMeta.Meta.Slug) assert.Equal(t, test.DashResp.Meta.Type, dashboardFullWithMeta.Meta.Type) assert.Equal(t, false, dashboardFullWithMeta.Meta.CanStar) assert.Equal(t, false, dashboardFullWithMeta.Meta.CanSave) assert.Equal(t, false, dashboardFullWithMeta.Meta.CanEdit) assert.Equal(t, false, dashboardFullWithMeta.Meta.CanAdmin) assert.Equal(t, false, dashboardFullWithMeta.Meta.CanDelete) assert.Equal(t, test.DashResp.Meta.Created, dashboardFullWithMeta.Meta.Created) assert.Equal(t, test.DashResp.Meta.Updated, dashboardFullWithMeta.Meta.Updated) assert.Equal(t, test.DashResp.Meta.Version, dashboardFullWithMeta.Meta.Version) assert.Equal(t, false, dashboardFullWithMeta.Meta.IsFolder) assert.Equal(t, test.DashResp.Meta.FolderUid, dashboardFullWithMeta.Meta.FolderUid) assert.Equal(t, test.DashResp.Meta.PublicDashboardEnabled, dashboardFullWithMeta.Meta.PublicDashboardEnabled) // hide the timepicker if the time selection is disabled assert.Equal(t, test.StoreResp.pd.TimeSelectionEnabled, !dashboardFullWithMeta.Dashboard.Get("timepicker").Get("hidden").MustBool()) for _, panelObj := range dashboardFullWithMeta.Dashboard.Get("panels").MustArray() { panel := simplejson.NewFromAny(panelObj) // if the panel is a row and it is collapsed, get the queries from the panels inside the row if panel.Get("type").MustString() == "row" && panel.Get("collapsed").MustBool() { // recursive call to get queries from panels inside a row sanitizeData(panel) continue } for _, targetObj := range panel.Get("targets").MustArray() { target := simplejson.NewFromAny(targetObj) assert.Empty(t, target.Get("expr").MustString()) assert.Empty(t, target.Get("query").MustString()) assert.Empty(t, target.Get("rawSql").MustString()) } } } }) } } func TestGetPublicDashboard(t *testing.T) { type storeResp struct { pd *PublicDashboard d *dashboards.Dashboard err error } testCases := []struct { Name string AccessToken string StoreResp *storeResp ErrResp error DashResp *dashboards.Dashboard }{ { Name: "returns a dashboard", AccessToken: "abc123", StoreResp: &storeResp{ pd: &PublicDashboard{AccessToken: "abcdToken", IsEnabled: true}, d: &dashboards.Dashboard{UID: "mydashboard", Data: dashboardData}, err: nil, }, ErrResp: nil, DashResp: &dashboards.Dashboard{UID: "mydashboard", Data: dashboardData}, }, { Name: "returns dashboard when isEnabled is false", AccessToken: "abc123", StoreResp: &storeResp{ pd: &PublicDashboard{AccessToken: "abcdToken", IsEnabled: false}, d: &dashboards.Dashboard{UID: "mydashboard", Data: dashboardData}, err: nil, }, ErrResp: nil, DashResp: &dashboards.Dashboard{UID: "mydashboard", Data: dashboardData}, }, { Name: "returns ErrPublicDashboardNotFound if PublicDashboard missing", AccessToken: "abc123", StoreResp: &storeResp{pd: nil, d: nil, err: nil}, ErrResp: ErrPublicDashboardNotFound, DashResp: nil, }, { Name: "returns ErrPublicDashboardNotFound if Dashboard missing", AccessToken: "abc123", StoreResp: &storeResp{pd: nil, d: nil, err: nil}, ErrResp: ErrPublicDashboardNotFound, DashResp: nil, }, } for _, test := range testCases { t.Run(test.Name, func(t *testing.T) { fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(test.StoreResp.d, test.StoreResp.err) fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.Anything).Return(test.StoreResp.pd, test.StoreResp.err) service, _, _ := newPublicDashboardServiceImpl(t, nil, nil, fakeStore, fakeDashboardService, nil) pdc, dash, err := service.FindPublicDashboardAndDashboardByAccessToken(context.Background(), test.AccessToken) if test.ErrResp != nil { assert.Error(t, test.ErrResp, err) } else { require.NoError(t, err) } assert.Equal(t, test.DashResp, dash) if test.DashResp != nil { assert.NotNil(t, dash.CreatedBy) assert.Equal(t, test.StoreResp.pd, pdc) } }) } } func TestGetEnabledPublicDashboard(t *testing.T) { type storeResp struct { pd *PublicDashboard d *dashboards.Dashboard err error } testCases := []struct { Name string AccessToken string StoreResp *storeResp ErrResp error DashResp *dashboards.Dashboard }{ { Name: "returns a dashboard", AccessToken: "abc123", StoreResp: &storeResp{ pd: &PublicDashboard{AccessToken: "abcdToken", IsEnabled: true}, d: &dashboards.Dashboard{UID: "mydashboard", Data: dashboardData}, err: nil, }, ErrResp: nil, DashResp: &dashboards.Dashboard{UID: "mydashboard", Data: dashboardData}, }, { Name: "returns ErrPublicDashboardNotFound when isEnabled is false", AccessToken: "abc123", StoreResp: &storeResp{ pd: &PublicDashboard{AccessToken: "abcdToken", IsEnabled: false}, d: &dashboards.Dashboard{UID: "mydashboard"}, err: nil, }, ErrResp: ErrPublicDashboardNotFound, DashResp: nil, }, } for _, test := range testCases { t.Run(test.Name, func(t *testing.T) { fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.Anything).Return(test.StoreResp.pd, test.StoreResp.err) fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(test.StoreResp.d, test.StoreResp.err) service, _, _ := newPublicDashboardServiceImpl(t, nil, nil, fakeStore, fakeDashboardService, nil) pdc, dash, err := service.FindEnabledPublicDashboardAndDashboardByAccessToken(context.Background(), test.AccessToken) if test.ErrResp != nil { assert.Error(t, test.ErrResp, err) } else { require.NoError(t, err) } assert.Equal(t, test.DashResp, dash) if test.DashResp != nil { assert.NotNil(t, dash.CreatedBy) assert.Equal(t, test.StoreResp.pd, pdc) } }) } } // We're using sqlite here because testing all of the behaviors with mocks in // the correct order is convoluted. func TestCreatePublicDashboard(t *testing.T) { t.Run("Create public dashboard", func(t *testing.T) { fakeDashboardService := &dashboards.FakeDashboardService{} service, sqlStore, cfg := newPublicDashboardServiceImpl(t, nil, nil, nil, fakeDashboardService, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore)) require.NoError(t, err) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) isEnabled, annotationsEnabled, timeSelectionEnabled := true, false, true dto := &SavePublicDashboardDTO{ DashboardUid: dashboard.UID, UserId: 7, OrgID: dashboard.OrgID, PublicDashboard: &PublicDashboardDTO{ IsEnabled: &isEnabled, AnnotationsEnabled: &annotationsEnabled, TimeSelectionEnabled: &timeSelectionEnabled, Share: EmailShareType, }, } _, err = service.Create(context.Background(), SignedInUser, dto) require.NoError(t, err) pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgID, dashboard.UID) require.NoError(t, err) // DashboardUid/OrgId/CreatedBy set by the command, not parameters assert.Equal(t, dashboard.UID, pubdash.DashboardUid) assert.Equal(t, dashboard.OrgID, pubdash.OrgId) assert.Equal(t, dto.UserId, pubdash.CreatedBy) assert.Equal(t, *dto.PublicDashboard.AnnotationsEnabled, pubdash.AnnotationsEnabled) assert.Equal(t, *dto.PublicDashboard.TimeSelectionEnabled, pubdash.TimeSelectionEnabled) // ExistsEnabledByDashboardUid set by parameters assert.Equal(t, *dto.PublicDashboard.IsEnabled, pubdash.IsEnabled) // CreatedAt set to non-zero time assert.NotEqual(t, &time.Time{}, pubdash.CreatedAt) assert.Equal(t, dto.PublicDashboard.Share, pubdash.Share) // accessToken is valid uuid _, err = uuid.Parse(pubdash.AccessToken) require.NoError(t, err, "expected a valid UUID, got %s", pubdash.AccessToken) }) trueBooleanField := true testCases := []struct { Name string IsEnabled *bool TimeSelectionEnabled *bool AnnotationsEnabled *bool }{ { Name: "isEnabled", IsEnabled: nil, TimeSelectionEnabled: &trueBooleanField, AnnotationsEnabled: &trueBooleanField, }, { Name: "timeSelectionEnabled", IsEnabled: &trueBooleanField, TimeSelectionEnabled: nil, AnnotationsEnabled: &trueBooleanField, }, { Name: "annotationsEnabled", IsEnabled: &trueBooleanField, TimeSelectionEnabled: &trueBooleanField, AnnotationsEnabled: nil, }, { Name: "isEnabled, timeSelectionEnabled and annotationsEnabled", IsEnabled: nil, TimeSelectionEnabled: nil, AnnotationsEnabled: nil, }, } for _, tt := range testCases { t.Run(fmt.Sprintf("Create public dashboard with %s null boolean fields stores them as false", tt.Name), func(t *testing.T) { fakeDashboardService := &dashboards.FakeDashboardService{} service, sqlStore, cfg := newPublicDashboardServiceImpl(t, nil, nil, nil, fakeDashboardService, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore)) require.NoError(t, err) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) dto := &SavePublicDashboardDTO{ DashboardUid: dashboard.UID, UserId: 7, OrgID: dashboard.OrgID, PublicDashboard: &PublicDashboardDTO{ IsEnabled: tt.IsEnabled, TimeSelectionEnabled: tt.TimeSelectionEnabled, AnnotationsEnabled: tt.AnnotationsEnabled, Share: PublicShareType, }, } _, err = service.Create(context.Background(), SignedInUser, dto) require.NoError(t, err) pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgID, dashboard.UID) require.NoError(t, err) assertFalseIfNull(t, pubdash.IsEnabled, dto.PublicDashboard.IsEnabled) assertFalseIfNull(t, pubdash.TimeSelectionEnabled, dto.PublicDashboard.TimeSelectionEnabled) assertFalseIfNull(t, pubdash.AnnotationsEnabled, dto.PublicDashboard.AnnotationsEnabled) }) } t.Run("Validate pubdash has default time setting value", func(t *testing.T) { fakeDashboardService := &dashboards.FakeDashboardService{} service, sqlStore, cfg := newPublicDashboardServiceImpl(t, nil, nil, nil, fakeDashboardService, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore)) require.NoError(t, err) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) isEnabled := true dto := &SavePublicDashboardDTO{ DashboardUid: dashboard.UID, OrgID: dashboard.OrgID, UserId: 7, PublicDashboard: &PublicDashboardDTO{ IsEnabled: &isEnabled, }, } _, err = service.Create(context.Background(), SignedInUser, dto) require.NoError(t, err) pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgID, dashboard.UID) require.NoError(t, err) assert.Equal(t, defaultPubdashTimeSettings, pubdash.TimeSettings) }) t.Run("Creates pubdash whose dashboard has template variables successfully", func(t *testing.T) { fakeDashboardService := &dashboards.FakeDashboardService{} service, sqlStore, cfg := newPublicDashboardServiceImpl(t, nil, nil, nil, fakeDashboardService, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore)) require.NoError(t, err) templateVars := make([]map[string]any, 1) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, templateVars, nil) fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) isEnabled := true dto := &SavePublicDashboardDTO{ DashboardUid: dashboard.UID, OrgID: dashboard.OrgID, UserId: 7, PublicDashboard: &PublicDashboardDTO{ IsEnabled: &isEnabled, }, } _, err = service.Create(context.Background(), SignedInUser, dto) require.NoError(t, err) pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgID, dashboard.UID) require.NoError(t, err) assert.Equal(t, dashboard.UID, pubdash.DashboardUid) assert.Equal(t, dashboard.OrgID, pubdash.OrgId) }) t.Run("Throws an error when given pubdash uid already exists", func(t *testing.T) { dashboard := dashboards.NewDashboard("testDashie") pubdash := &PublicDashboard{ Uid: "ExistingUid", IsEnabled: true, AnnotationsEnabled: false, DashboardUid: "NOTTHESAME", OrgId: dashboard.OrgID, TimeSettings: timeSettings, } publicDashboardStore := &FakePublicDashboardStore{} publicDashboardStore.On("Find", mock.Anything, "ExistingUid").Return(pubdash, nil) publicDashboardStore.On("FindByDashboardUid", mock.Anything, mock.Anything, mock.Anything).Return(nil, ErrPublicDashboardNotFound.Errorf("")) fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) service, _, _ := newPublicDashboardServiceImpl(t, nil, nil, publicDashboardStore, fakeDashboardService, nil) isEnabled := true dto := &SavePublicDashboardDTO{ DashboardUid: "an-id", OrgID: dashboard.OrgID, UserId: 7, PublicDashboard: &PublicDashboardDTO{ Uid: "ExistingUid", IsEnabled: &isEnabled, }, } _, err := service.Create(context.Background(), SignedInUser, dto) require.Error(t, err) require.Equal(t, err, ErrPublicDashboardUidExists.Errorf("Create: public dashboard uid %s already exists", dto.PublicDashboard.Uid)) }) t.Run("Create public dashboard with given pubdash uid", func(t *testing.T) { fakeDashboardService := &dashboards.FakeDashboardService{} service, sqlStore, cfg := newPublicDashboardServiceImpl(t, nil, nil, nil, fakeDashboardService, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore)) require.NoError(t, err) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) isEnabled := true dto := &SavePublicDashboardDTO{ DashboardUid: dashboard.UID, UserId: 7, OrgID: dashboard.OrgID, PublicDashboard: &PublicDashboardDTO{ Uid: "GivenUid", IsEnabled: &isEnabled, }, } _, err = service.Create(context.Background(), SignedInUser, dto) require.NoError(t, err) pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgID, dashboard.UID) require.NoError(t, err) assert.Equal(t, dto.PublicDashboard.Uid, pubdash.Uid) }) t.Run("Throws an error when pubdash with given access token input already exists", func(t *testing.T) { dashboard := dashboards.NewDashboard("testDashie") pubdash := &PublicDashboard{ Uid: "ExistingUid", AccessToken: "ExistingAccessToken", IsEnabled: true, AnnotationsEnabled: false, DashboardUid: "NOTTHESAME", OrgId: dashboard.OrgID, TimeSettings: timeSettings, } publicDashboardStore := &FakePublicDashboardStore{} publicDashboardStore.On("Find", mock.Anything, mock.Anything).Return(nil, nil) publicDashboardStore.On("FindByAccessToken", mock.Anything, "ExistingAccessToken").Return(pubdash, nil) publicDashboardStore.On("FindByDashboardUid", mock.Anything, mock.Anything, mock.Anything).Return(nil, ErrPublicDashboardNotFound.Errorf("")) fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) service, _, _ := newPublicDashboardServiceImpl(t, nil, nil, publicDashboardStore, fakeDashboardService, nil) isEnabled := true dto := &SavePublicDashboardDTO{ DashboardUid: "an-id", OrgID: dashboard.OrgID, UserId: 7, PublicDashboard: &PublicDashboardDTO{ AccessToken: "ExistingAccessToken", IsEnabled: &isEnabled, }, } _, err := service.Create(context.Background(), SignedInUser, dto) require.Error(t, err) require.Equal(t, err, ErrPublicDashboardAccessTokenExists.Errorf("Create: public dashboard access token %s already exists", dto.PublicDashboard.AccessToken)) }) t.Run("Create public dashboard with given pubdash access token", func(t *testing.T) { fakeDashboardService := &dashboards.FakeDashboardService{} service, sqlStore, cfg := newPublicDashboardServiceImpl(t, nil, nil, nil, fakeDashboardService, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore)) require.NoError(t, err) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]interface{}{}, nil) fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) isEnabled := true dto := &SavePublicDashboardDTO{ DashboardUid: dashboard.UID, UserId: 7, OrgID: dashboard.OrgID, PublicDashboard: &PublicDashboardDTO{ AccessToken: "GivenAccessToken", IsEnabled: &isEnabled, }, } _, err = service.Create(context.Background(), SignedInUser, dto) require.NoError(t, err) pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgID, dashboard.UID) require.NoError(t, err) assert.Equal(t, dto.PublicDashboard.AccessToken, pubdash.AccessToken) }) t.Run("Throws an error when pubdash with generated access token already exists", func(t *testing.T) { dashboard := dashboards.NewDashboard("testDashie") pubdash := &PublicDashboard{ IsEnabled: true, AnnotationsEnabled: false, DashboardUid: "NOTTHESAME", OrgId: dashboard.OrgID, TimeSettings: timeSettings, } publicDashboardStore := &FakePublicDashboardStore{} publicDashboardStore.On("FindByAccessToken", mock.Anything, mock.Anything).Return(pubdash, nil) service, _, _ := newPublicDashboardServiceImpl(t, nil, nil, publicDashboardStore, nil, nil) _, err := service.NewPublicDashboardAccessToken(context.Background()) require.Error(t, err) require.Equal(t, err, ErrInternalServerError.Errorf("failed to generate a unique accessToken for public dashboard")) }) t.Run("Returns error if public dashboard exists", func(t *testing.T) { publicdashboardStore := &FakePublicDashboardStore{} publicdashboardStore.On("FindByDashboardUid", mock.Anything, mock.Anything, mock.Anything).Return(&PublicDashboard{Uid: "newPubdashUid"}, nil) publicdashboardStore.On("Find", mock.Anything, mock.Anything).Return(nil, nil) fakeDashboardService := &dashboards.FakeDashboardService{} service, sqlStore, cfg := newPublicDashboardServiceImpl(t, nil, nil, publicdashboardStore, fakeDashboardService, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore)) require.NoError(t, err) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) isEnabled, annotationsEnabled := true, false dto := &SavePublicDashboardDTO{ DashboardUid: dashboard.UID, UserId: 7, PublicDashboard: &PublicDashboardDTO{ AnnotationsEnabled: &annotationsEnabled, IsEnabled: &isEnabled, }, } savedPubdash, err := service.Create(context.Background(), SignedInUser, dto) assert.Error(t, err) assert.Nil(t, savedPubdash) assert.True(t, ErrDashboardIsPublic.Is(err)) }) t.Run("Validate pubdash has default share value", func(t *testing.T) { fakeDashboardService := &dashboards.FakeDashboardService{} service, sqlStore, cfg := newPublicDashboardServiceImpl(t, nil, nil, nil, fakeDashboardService, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore)) require.NoError(t, err) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) isEnabled := true dto := &SavePublicDashboardDTO{ DashboardUid: dashboard.UID, OrgID: dashboard.OrgID, UserId: 7, PublicDashboard: &PublicDashboardDTO{ IsEnabled: &isEnabled, }, } _, err = service.Create(context.Background(), SignedInUser, dto) require.NoError(t, err) pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgID, dashboard.UID) require.NoError(t, err) // if share type is empty should be populated with public by default assert.Equal(t, PublicShareType, pubdash.Share) }) } func assertFalseIfNull(t *testing.T, expectedValue bool, nullableValue *bool) { if nullableValue == nil { assert.Equal(t, expectedValue, false) } else { assert.Equal(t, expectedValue, *nullableValue) } } func TestUpdatePublicDashboard(t *testing.T) { fakeDashboardService := &dashboards.FakeDashboardService{} service, sqlStore, cfg := newPublicDashboardServiceImpl(t, nil, nil, nil, fakeDashboardService, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore)) require.NoError(t, err) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) dashboard2 := insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, "", true, []map[string]any{}, nil) fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) t.Run("Updating public dashboard", func(t *testing.T) { isEnabled, annotationsEnabled, timeSelectionEnabled := true, false, false dto := &SavePublicDashboardDTO{ DashboardUid: dashboard.UID, UserId: 7, PublicDashboard: &PublicDashboardDTO{ IsEnabled: &isEnabled, AnnotationsEnabled: &annotationsEnabled, TimeSelectionEnabled: &timeSelectionEnabled, }, } // insert initial pubdash savedPubdash, err := service.Create(context.Background(), SignedInUser, dto) require.NoError(t, err) isEnabled, annotationsEnabled, timeSelectionEnabled = true, true, true dto = &SavePublicDashboardDTO{ Uid: savedPubdash.Uid, DashboardUid: dashboard.UID, OrgID: 9, UserId: 8, PublicDashboard: &PublicDashboardDTO{ IsEnabled: &isEnabled, AnnotationsEnabled: &annotationsEnabled, TimeSelectionEnabled: &timeSelectionEnabled, }, } updatedPubdash, err := service.Update(context.Background(), SignedInUser, dto) require.NoError(t, err) // don't get updated assert.Equal(t, savedPubdash.DashboardUid, updatedPubdash.DashboardUid) assert.Equal(t, savedPubdash.OrgId, updatedPubdash.OrgId) assert.Equal(t, savedPubdash.CreatedAt, updatedPubdash.CreatedAt) assert.Equal(t, savedPubdash.CreatedBy, updatedPubdash.CreatedBy) assert.Equal(t, savedPubdash.AccessToken, updatedPubdash.AccessToken) // gets updated assert.Equal(t, *dto.PublicDashboard.IsEnabled, updatedPubdash.IsEnabled) assert.Equal(t, *dto.PublicDashboard.AnnotationsEnabled, updatedPubdash.AnnotationsEnabled) assert.Equal(t, *dto.PublicDashboard.TimeSelectionEnabled, updatedPubdash.TimeSelectionEnabled) assert.Equal(t, dto.UserId, updatedPubdash.UpdatedBy) assert.NotEqual(t, &time.Time{}, updatedPubdash.UpdatedAt) }) t.Run("Updating set empty time settings", func(t *testing.T) { isEnabled := true dto := &SavePublicDashboardDTO{ DashboardUid: dashboard.UID, UserId: 7, PublicDashboard: &PublicDashboardDTO{ IsEnabled: &isEnabled, }, } savedPubdash, err := service.Create(context.Background(), SignedInUser, dto) require.NoError(t, err) dto = &SavePublicDashboardDTO{ Uid: savedPubdash.Uid, DashboardUid: dashboard.UID, OrgID: 9, UserId: 8, PublicDashboard: &PublicDashboardDTO{ IsEnabled: &isEnabled, }, } updatedPubdash, err := service.Update(context.Background(), SignedInUser, dto) require.NoError(t, err) assert.Equal(t, &TimeSettings{}, updatedPubdash.TimeSettings) }) t.Run("Should fail when public dashboard uid does not match dashboard uid", func(t *testing.T) { isEnabled := true dto := &SavePublicDashboardDTO{ DashboardUid: dashboard.UID, UserId: 7, PublicDashboard: &PublicDashboardDTO{ IsEnabled: &isEnabled, }, } // insert initial pubdash savedPubdash, err := service.Create(context.Background(), SignedInUser, dto) require.NoError(t, err) dto = &SavePublicDashboardDTO{ Uid: savedPubdash.Uid, DashboardUid: dashboard2.UID, OrgID: 9, UserId: 8, PublicDashboard: &PublicDashboardDTO{ IsEnabled: &isEnabled, }, } _, err = service.Update(context.Background(), SignedInUser, dto) assert.Error(t, err) }) t.Run("Updating not existent dashboard", func(t *testing.T) { dto := &SavePublicDashboardDTO{ DashboardUid: "NOTEXISTENTDASHBOARD", UserId: 7, PublicDashboard: &PublicDashboardDTO{}, } fds := &dashboards.FakeDashboardService{} fds.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(nil, dashboards.ErrDashboardNotFound) service.dashboardService = fds updatedPubdash, err := service.Update(context.Background(), SignedInUser, dto) assert.Error(t, err) var grafanaErr errutil.Error ok := errors.As(err, &grafanaErr) assert.True(t, ok) assert.Equal(t, "publicdashboards.dashboardNotFound", grafanaErr.MessageID) assert.Empty(t, updatedPubdash) }) trueBooleanField := true timeSettings := &TimeSettings{From: "now-8", To: "now"} shareType := EmailShareType testCases := []struct { Name string IsEnabled *bool TimeSelectionEnabled *bool AnnotationsEnabled *bool TimeSettings *TimeSettings ShareType ShareType }{ { Name: "isEnabled", IsEnabled: nil, TimeSelectionEnabled: &trueBooleanField, AnnotationsEnabled: &trueBooleanField, TimeSettings: timeSettings, ShareType: shareType, }, { Name: "timeSelectionEnabled", IsEnabled: &trueBooleanField, TimeSelectionEnabled: nil, AnnotationsEnabled: &trueBooleanField, TimeSettings: timeSettings, ShareType: shareType, }, { Name: "annotationsEnabled", IsEnabled: &trueBooleanField, TimeSelectionEnabled: &trueBooleanField, AnnotationsEnabled: nil, TimeSettings: timeSettings, ShareType: shareType, }, { Name: "isEnabled, timeSelectionEnabled and annotationsEnabled", IsEnabled: nil, TimeSelectionEnabled: nil, AnnotationsEnabled: nil, TimeSettings: nil, ShareType: "", }, } for _, tt := range testCases { t.Run(fmt.Sprintf("Update public dashboard with %s null boolean fields let those fields with old persisted value", tt.Name), func(t *testing.T) { fakeDashboardService := &dashboards.FakeDashboardService{} service, sqlStore, cfg := newPublicDashboardServiceImpl(t, nil, nil, nil, fakeDashboardService, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore)) require.NoError(t, err) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) isEnabled, annotationsEnabled, timeSelectionEnabled := true, true, false dto := &SavePublicDashboardDTO{ DashboardUid: dashboard.UID, UserId: 7, PublicDashboard: &PublicDashboardDTO{ IsEnabled: &isEnabled, AnnotationsEnabled: &annotationsEnabled, TimeSelectionEnabled: &timeSelectionEnabled, Share: PublicShareType, }, } // insert initial pubdash savedPubdash, err := service.Create(context.Background(), SignedInUser, dto) require.NoError(t, err) dto = &SavePublicDashboardDTO{ Uid: savedPubdash.Uid, DashboardUid: dashboard.UID, OrgID: 9, UserId: 8, PublicDashboard: &PublicDashboardDTO{ IsEnabled: tt.IsEnabled, AnnotationsEnabled: tt.AnnotationsEnabled, TimeSelectionEnabled: tt.TimeSelectionEnabled, Share: tt.ShareType, }, } updatedPubdash, err := service.Update(context.Background(), SignedInUser, dto) require.NoError(t, err) assertOldValueIfNull(t, updatedPubdash.IsEnabled, savedPubdash.IsEnabled, dto.PublicDashboard.IsEnabled) assertOldValueIfNull(t, updatedPubdash.AnnotationsEnabled, savedPubdash.AnnotationsEnabled, dto.PublicDashboard.AnnotationsEnabled) assertOldValueIfNull(t, updatedPubdash.TimeSelectionEnabled, savedPubdash.TimeSelectionEnabled, dto.PublicDashboard.TimeSelectionEnabled) if dto.PublicDashboard.Share == "" { assert.Equal(t, updatedPubdash.Share, savedPubdash.Share) } else { assert.Equal(t, updatedPubdash.Share, dto.PublicDashboard.Share) } }) } } func assertOldValueIfNull(t *testing.T, expectedValue bool, oldValue bool, nullableValue *bool) { if nullableValue == nil { assert.Equal(t, expectedValue, oldValue) } else { assert.Equal(t, expectedValue, *nullableValue) } } func TestDeletePublicDashboard(t *testing.T) { pubdash := &PublicDashboard{Uid: "2", OrgId: 1, DashboardUid: "uid"} type mockFindResponse struct { PublicDashboard *PublicDashboard Err error } type mockDeleteResponse struct { AffectedRowsResp int64 StoreRespErr error } testCases := []struct { Name string ExpectedErrResp error mockFindStore *mockFindResponse mockDeleteStore *mockDeleteResponse }{ { Name: "Successfully deletes a public dashboard", ExpectedErrResp: nil, mockFindStore: &mockFindResponse{pubdash, nil}, mockDeleteStore: &mockDeleteResponse{1, nil}, }, { Name: "Public dashboard not found", ExpectedErrResp: ErrInternalServerError.Errorf("Delete: failed to find public dashboard by uid: pubdashUID: error"), mockFindStore: &mockFindResponse{pubdash, errors.New("error")}, mockDeleteStore: &mockDeleteResponse{0, nil}, }, { Name: "Public dashboard not found by UID", ExpectedErrResp: ErrPublicDashboardNotFound.Errorf("Delete: public dashboard not found by uid: pubdashUID"), mockFindStore: &mockFindResponse{nil, nil}, mockDeleteStore: &mockDeleteResponse{0, nil}, }, { Name: "Public dashboard UID does not belong to the dashboard", ExpectedErrResp: ErrInvalidUid.Errorf("Delete: the public dashboard does not belong to the dashboard"), mockFindStore: &mockFindResponse{&PublicDashboard{Uid: "2", OrgId: 1, DashboardUid: "wrong"}, nil}, mockDeleteStore: &mockDeleteResponse{0, nil}, }, { Name: "Failed to delete - Database error", ExpectedErrResp: ErrInternalServerError.Errorf("Delete: failed to delete a public dashboard by Uid: pubdashUID db error!"), mockFindStore: &mockFindResponse{pubdash, nil}, mockDeleteStore: &mockDeleteResponse{1, errors.New("db error!")}, }, } for _, tt := range testCases { t.Run(tt.Name, func(t *testing.T) { store := NewFakePublicDashboardStore(t) store.On("Find", mock.Anything, mock.Anything).Return(tt.mockFindStore.PublicDashboard, tt.mockFindStore.Err) if tt.ExpectedErrResp == nil || tt.mockDeleteStore.StoreRespErr != nil { store.On("Delete", mock.Anything, mock.Anything).Return(tt.mockDeleteStore.AffectedRowsResp, tt.mockDeleteStore.StoreRespErr) } service, _, _ := newPublicDashboardServiceImpl(t, nil, nil, store, nil, nil) err := service.Delete(context.Background(), "pubdashUID", "uid") if tt.ExpectedErrResp != nil { assert.Equal(t, tt.ExpectedErrResp.Error(), err.Error()) } else { assert.NoError(t, err) } }) } } func TestPublicDashboardServiceImpl_getSafeIntervalAndMaxDataPoints(t *testing.T) { type args struct { reqDTO PublicDashboardQueryDTO ts TimeSettings } tests := []struct { name string args args wantSafeInterval int64 wantSafeMaxDataPoints int64 }{ { name: "return original interval", args: args{ reqDTO: PublicDashboardQueryDTO{ IntervalMs: 10000, MaxDataPoints: 300, }, ts: TimeSettings{ From: "now-3h", To: "now", }, }, wantSafeInterval: 10000, wantSafeMaxDataPoints: 300, }, { name: "return safe interval because of a small interval", args: args{ reqDTO: PublicDashboardQueryDTO{ IntervalMs: 1000, MaxDataPoints: 300, }, ts: TimeSettings{ From: "now-6h", To: "now", }, }, wantSafeInterval: 2000, wantSafeMaxDataPoints: 11000, }, { name: "return safe interval for long time range", args: args{ reqDTO: PublicDashboardQueryDTO{ IntervalMs: 100, MaxDataPoints: 300, }, ts: TimeSettings{ From: "now-90d", To: "now", }, }, wantSafeInterval: 600000, wantSafeMaxDataPoints: 11000, }, { name: "return safe interval when reqDTO is empty", args: args{ reqDTO: PublicDashboardQueryDTO{}, ts: TimeSettings{ From: "now-90d", To: "now", }, }, wantSafeInterval: 600000, wantSafeMaxDataPoints: 11000, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pd := &PublicDashboardServiceImpl{ intervalCalculator: intervalv2.NewCalculator(), } got, got1 := pd.getSafeIntervalAndMaxDataPoints(tt.args.reqDTO, tt.args.ts) assert.Equalf(t, tt.wantSafeInterval, got, "getSafeIntervalAndMaxDataPoints(%v, %v)", tt.args.reqDTO, tt.args.ts) assert.Equalf(t, tt.wantSafeMaxDataPoints, got1, "getSafeIntervalAndMaxDataPoints(%v, %v)", tt.args.reqDTO, tt.args.ts) }) } } func TestDashboardEnabledChanged(t *testing.T) { t.Run("created isEnabled: false", func(t *testing.T) { assert.False(t, publicDashboardIsEnabledChanged(nil, &PublicDashboard{IsEnabled: false})) }) t.Run("created isEnabled: true", func(t *testing.T) { assert.True(t, publicDashboardIsEnabledChanged(nil, &PublicDashboard{IsEnabled: true})) }) t.Run("updated isEnabled same", func(t *testing.T) { assert.False(t, publicDashboardIsEnabledChanged(&PublicDashboard{IsEnabled: true}, &PublicDashboard{IsEnabled: true})) }) t.Run("updated isEnabled changed", func(t *testing.T) { assert.True(t, publicDashboardIsEnabledChanged(&PublicDashboard{IsEnabled: false}, &PublicDashboard{IsEnabled: true})) }) } func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) { features := featuremgmt.WithFeatures() testDB, cfg := db.InitTestDBWithCfg(t) dashStore, err := dashboardsDB.ProvideDashboardStore(testDB, cfg, features, tagimpl.ProvideService(testDB)) require.NoError(t, err) ac := acmock.New() fStore := folderimpl.ProvideStore(testDB) folderPermissions := acmock.NewMockedPermissionsService() folderStore := folderimpl.ProvideDashboardFolderStore(testDB) folderSvc := folderimpl.ProvideService( fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, nil, testDB, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService()) dashboardService, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService()) require.NoError(t, err) dashboardService.RegisterDashboardPermissions(&actest.FakePermissionsService{}) fakeGuardian := &guardian.FakeDashboardGuardian{ CanSaveValue: true, CanEditUIDs: []string{}, CanViewUIDs: []string{}, } guardian.MockDashboardGuardian(fakeGuardian) // insert in test data so we can check that permissions are working properly through the dashboard service // this will create 4 dashboards and 3 users // user1 has access to all dashboards ("*") // user2 has access to solely one dashboard // user3 has access to all created dashboards through specific permissions creatingUser := &user.SignedInUser{ UserID: 1, OrgID: 1, OrgRole: org.RoleAdmin, } dashboardsToSave := []dashboards.SaveDashboardDTO{ { OrgID: 1, User: creatingUser, Dashboard: &dashboards.Dashboard{ OrgID: 1, UID: "9S6TmO67z", Title: "test", Slug: "test", Data: simplejson.New(), }, }, { OrgID: 1, User: creatingUser, Dashboard: &dashboards.Dashboard{ OrgID: 1, UID: "1S6TmO67z", Title: "my first dashboard", Slug: "my-first-dashboard", Data: simplejson.New(), }, }, { OrgID: 1, User: creatingUser, Dashboard: &dashboards.Dashboard{ OrgID: 1, UID: "2S6TmO67z", Title: "my second dashboard", Slug: "my-second-dashboard", Data: simplejson.New(), }, }, { OrgID: 1, User: creatingUser, Dashboard: &dashboards.Dashboard{ OrgID: 1, UID: "0S6TmO67z", Title: "my zero dashboard", Slug: "my-zero-dashboard", Data: simplejson.New(), }, }, } for _, dash := range dashboardsToSave { _, err = dashboardService.SaveDashboard(context.Background(), &dash, true) require.NoError(t, err) } users := []user.User{ { ID: 1, UID: "user1", Email: "test1@gmail.com", Login: "user1", Created: time.Now(), Updated: time.Now(), }, { ID: 2, UID: "user2", Login: "user2", Email: "test2@gmail.com", Created: time.Now(), Updated: time.Now(), }, { ID: 3, UID: "user3", Login: "user3", Email: "test3@gmail.com", Created: time.Now(), Updated: time.Now(), }, } roles := []accesscontrol.Role{ { ID: 1, UID: "role1", Name: "forUser1", Created: time.Now(), Updated: time.Now(), }, { ID: 2, UID: "role2", Name: "forUser2", Created: time.Now(), Updated: time.Now(), }, { ID: 3, UID: "role3", Name: "forUser3", Created: time.Now(), Updated: time.Now(), }, } userRoles := []accesscontrol.UserRole{ { ID: 1, OrgID: 1, UserID: 1, RoleID: 1, Created: time.Now(), }, { ID: 2, OrgID: 1, UserID: 2, RoleID: 2, Created: time.Now(), }, { ID: 3, OrgID: 1, UserID: 3, RoleID: 3, Created: time.Now(), }, } permissions := []accesscontrol.Permission{ { ID: 1, RoleID: 1, Action: dashboards.ActionDashboardsRead, Scope: "*", Kind: "dashboards", Created: time.Now(), Updated: time.Now(), }, { ID: 2, RoleID: 2, Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:1S6TmO67z", Attribute: "uid", Identifier: "1S6TmO67z", Kind: "dashboards", Created: time.Now(), Updated: time.Now(), }, { ID: 3, RoleID: 3, Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:0S6TmO67z", Identifier: "0S6TmO67z", Attribute: "uid", Kind: "dashboards", Created: time.Now(), Updated: time.Now(), }, { ID: 4, RoleID: 3, Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:1S6TmO67z", Identifier: "1S6TmO67z", Kind: "dashboards", Attribute: "uid", Created: time.Now(), Updated: time.Now(), }, { ID: 5, RoleID: 3, Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:2S6TmO67z", Identifier: "2S6TmO67z", Kind: "dashboards", Attribute: "uid", Created: time.Now(), Updated: time.Now(), }, { ID: 6, RoleID: 3, Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:9S6TmO67z", Identifier: "9S6TmO67z", Kind: "dashboards", Attribute: "uid", Created: time.Now(), Updated: time.Now(), }, { ID: 7, RoleID: 1, Action: dashboards.ActionFoldersRead, Scope: "*", Kind: "folders", Created: time.Now(), Updated: time.Now(), }, { ID: 8, RoleID: 2, Action: dashboards.ActionFoldersRead, Scope: "*", Kind: "folders", Created: time.Now(), Updated: time.Now(), }, { ID: 9, RoleID: 3, Action: dashboards.ActionFoldersRead, Scope: "*", Kind: "folders", Created: time.Now(), Updated: time.Now(), }, } err = testDB.WithDbSession(context.Background(), func(sess *db.Session) error { if _, err := sess.Insert(users); err != nil { return err } if _, err := sess.Insert(roles); err != nil { return err } if _, err := sess.Insert(userRoles); err != nil { return err } _, err := sess.Insert(permissions) return err }) require.NoError(t, err) type args struct { ctx context.Context query *PublicDashboardListQuery } type mockResponse struct { PublicDashboardListResponseWithPagination *PublicDashboardListResponseWithPagination Err error DashboardResponse []dashboards.DashboardSearchProjection DashboardErr error } expectedFinalResponse := []*PublicDashboardListResponse{ { Uid: "1GwW7mgVk", AccessToken: "1b458cb7fe7f42c68712078bcacee6e3", DashboardUid: "1S6TmO67z", Title: "my first dashboard", Slug: "my-first-dashboard", IsEnabled: true, }, { Uid: "2GwW7mgVk", AccessToken: "2b458cb7fe7f42c68712078bcacee6e3", DashboardUid: "2S6TmO67z", Title: "my second dashboard", Slug: "my-second-dashboard", IsEnabled: false, }, { Uid: "0GwW7mgVk", AccessToken: "0b458cb7fe7f42c68712078bcacee6e3", DashboardUid: "0S6TmO67z", Title: "my zero dashboard", Slug: "my-zero-dashboard", IsEnabled: true, }, { Uid: "9GwW7mgVk", AccessToken: "deletedashboardaccesstoken", DashboardUid: "9S6TmO67z", Title: "test", Slug: "test", IsEnabled: true, }, } mockedStoreResponse := []*PublicDashboardListResponse{ { Uid: "0GwW7mgVk", AccessToken: "0b458cb7fe7f42c68712078bcacee6e3", DashboardUid: "0S6TmO67z", IsEnabled: true, }, { Uid: "1GwW7mgVk", AccessToken: "1b458cb7fe7f42c68712078bcacee6e3", DashboardUid: "1S6TmO67z", IsEnabled: true, }, { Uid: "2GwW7mgVk", AccessToken: "2b458cb7fe7f42c68712078bcacee6e3", DashboardUid: "2S6TmO67z", IsEnabled: false, }, { Uid: "9GwW7mgVk", AccessToken: "deletedashboardaccesstoken", DashboardUid: "9S6TmO67z", IsEnabled: true, }, } testCases := []struct { name string args args want *PublicDashboardListResponseWithPagination mockResponse *mockResponse wantErr assert.ErrorAssertionFunc }{ { name: "should return full response when user has access to all dashboards", args: args{ ctx: context.Background(), query: &PublicDashboardListQuery{ User: &user.SignedInUser{OrgID: 1, UserID: 1, Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"*"}, "folders:read": {"*"}}}}, OrgID: 1, Page: 1, Limit: 50, }, }, mockResponse: &mockResponse{ PublicDashboardListResponseWithPagination: &PublicDashboardListResponseWithPagination{ TotalCount: int64(len(mockedStoreResponse)), PublicDashboards: mockedStoreResponse, }, Err: nil, }, want: &PublicDashboardListResponseWithPagination{ Page: 1, PerPage: 50, TotalCount: int64(len(expectedFinalResponse)), PublicDashboards: expectedFinalResponse, }, wantErr: assert.NoError, }, { name: "should only return the one dashboard user 2 has access to", args: args{ ctx: context.Background(), query: &PublicDashboardListQuery{ User: &user.SignedInUser{OrgID: 1, UserID: 2, Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"dashboards:uid:1S6TmO67z"}, "folders:read": {"*"}}}}, OrgID: 1, Page: 1, Limit: 50, }, }, mockResponse: &mockResponse{ PublicDashboardListResponseWithPagination: &PublicDashboardListResponseWithPagination{ TotalCount: int64(len(mockedStoreResponse)), PublicDashboards: mockedStoreResponse, }, Err: nil, }, want: &PublicDashboardListResponseWithPagination{ Page: 1, PerPage: 50, TotalCount: 1, PublicDashboards: []*PublicDashboardListResponse{expectedFinalResponse[0]}, }, wantErr: assert.NoError, }, { name: "should return full response when user 3 has specific access to all dashboards", args: args{ ctx: context.Background(), query: &PublicDashboardListQuery{ User: &user.SignedInUser{OrgID: 1, UserID: 3, Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"dashboards:uid:0S6TmO67z", "dashboards:uid:1S6TmO67z", "dashboards:uid:2S6TmO67z", "dashboards:uid:9S6TmO67z"}, "folders:read": {"*"}}}}, OrgID: 1, Page: 1, Limit: 50, }, }, mockResponse: &mockResponse{ PublicDashboardListResponseWithPagination: &PublicDashboardListResponseWithPagination{ TotalCount: int64(len(mockedStoreResponse)), PublicDashboards: mockedStoreResponse, }, Err: nil, }, want: &PublicDashboardListResponseWithPagination{ Page: 1, PerPage: 50, TotalCount: int64(len(expectedFinalResponse)), PublicDashboards: expectedFinalResponse, }, wantErr: assert.NoError, }, { name: "should an empty response for a user with no access", args: args{ ctx: context.Background(), query: &PublicDashboardListQuery{ User: &user.SignedInUser{OrgID: 1, UserID: 4, Permissions: map[int64]map[string][]string{}}, OrgID: 1, Page: 1, Limit: 50, }, }, mockResponse: &mockResponse{ PublicDashboardListResponseWithPagination: &PublicDashboardListResponseWithPagination{ TotalCount: int64(len(mockedStoreResponse)), PublicDashboards: mockedStoreResponse, }, Err: nil, }, want: &PublicDashboardListResponseWithPagination{ Page: 1, PerPage: 50, TotalCount: 0, PublicDashboards: []*PublicDashboardListResponse{}, }, wantErr: assert.NoError, }, { name: "should return correct pagination response if limited", args: args{ ctx: context.Background(), query: &PublicDashboardListQuery{ User: &user.SignedInUser{OrgID: 1, UserID: 1, Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"*"}, "folders:read": {"*"}}}}, OrgID: 1, Page: 1, Limit: 2, }, }, mockResponse: &mockResponse{ PublicDashboardListResponseWithPagination: &PublicDashboardListResponseWithPagination{ TotalCount: int64(len(mockedStoreResponse)), PublicDashboards: mockedStoreResponse, }, Err: nil, }, want: &PublicDashboardListResponseWithPagination{ Page: 1, PerPage: 2, TotalCount: 4, PublicDashboards: expectedFinalResponse[:2], }, wantErr: assert.NoError, }, { name: "should return correct page", args: args{ ctx: context.Background(), query: &PublicDashboardListQuery{ User: &user.SignedInUser{OrgID: 1, UserID: 1, Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"*"}, "folders:read": {"*"}}}}, OrgID: 1, Page: 2, Limit: 2, }, }, mockResponse: &mockResponse{ PublicDashboardListResponseWithPagination: &PublicDashboardListResponseWithPagination{ TotalCount: int64(len(mockedStoreResponse)), PublicDashboards: mockedStoreResponse, }, Err: nil, }, want: &PublicDashboardListResponseWithPagination{ Page: 2, PerPage: 2, TotalCount: 4, PublicDashboards: expectedFinalResponse[2:], }, wantErr: assert.NoError, }, { name: "should return error when store returns error", args: args{ ctx: context.Background(), query: &PublicDashboardListQuery{ User: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{ 1: {"dashboards:read": {"dashboards:uid:0S6TmO67z"}}}, }, OrgID: 1, Page: 1, Limit: 50, }, }, mockResponse: &mockResponse{ PublicDashboardListResponseWithPagination: nil, Err: errors.New("an err"), }, want: nil, wantErr: assert.Error, }, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { store := NewFakePublicDashboardStore(t) store.On("FindAll", mock.Anything, mock.Anything). Return(tt.mockResponse.PublicDashboardListResponseWithPagination, tt.mockResponse.Err) pd, _, _ := newPublicDashboardServiceImpl(t, testDB, cfg, store, dashboardService, nil) pd.ac = ac got, err := pd.FindAllWithPagination(tt.args.ctx, tt.args.query) if !tt.wantErr(t, err, fmt.Sprintf("FindAllWithPagination(%v, %v)", tt.args.ctx, tt.args.query)) { return } assert.Equalf(t, tt.want, got, "FindAllWithPagination(%v, %v)", tt.args.ctx, tt.args.query) }) } } func TestPublicDashboardServiceImpl_NewPublicDashboardUid(t *testing.T) { mockedDashboard := &PublicDashboard{ IsEnabled: true, AnnotationsEnabled: false, DashboardUid: "NOTTHESAME", OrgId: 9999999, TimeSettings: timeSettings, } type args struct { ctx context.Context } type mockResponse struct { PublicDashboard *PublicDashboard Err error } tests := []struct { name string args args mockStore *mockResponse want string wantErr assert.ErrorAssertionFunc }{ { name: "should return a new uid", args: args{ctx: context.Background()}, mockStore: &mockResponse{nil, nil}, want: "NOTTHESAME", wantErr: assert.NoError, }, { name: "should return an error if the generated uid exists 3 times", args: args{ctx: context.Background()}, mockStore: &mockResponse{mockedDashboard, nil}, want: "", wantErr: assert.Error, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { store := NewFakePublicDashboardStore(t) store.On("Find", mock.Anything, mock.Anything). Return(tt.mockStore.PublicDashboard, tt.mockStore.Err) pd := &PublicDashboardServiceImpl{store: store} got, err := pd.NewPublicDashboardUid(tt.args.ctx) if !tt.wantErr(t, err, fmt.Sprintf("NewPublicDashboardUid(%v)", tt.args.ctx)) { return } if err == nil { assert.NotEqual(t, got, tt.want, "NewPublicDashboardUid(%v)", tt.args.ctx) assert.True(t, util.IsValidShortUID(got), "NewPublicDashboardUid(%v)", tt.args.ctx) store.AssertNumberOfCalls(t, "Find", 1) } else { store.AssertNumberOfCalls(t, "Find", 3) assert.True(t, ErrInternalServerError.Is(err)) } }) } } func TestPublicDashboardServiceImpl_NewPublicDashboardAccessToken(t *testing.T) { mockedDashboard := &PublicDashboard{ IsEnabled: true, AnnotationsEnabled: false, DashboardUid: "NOTTHESAME", OrgId: 9999999, TimeSettings: timeSettings, } type args struct { ctx context.Context } type mockResponse struct { PublicDashboard *PublicDashboard Err error } tests := []struct { name string args args mockStore *mockResponse want string wantErr assert.ErrorAssertionFunc }{ { name: "should return a new access token", args: args{ctx: context.Background()}, mockStore: &mockResponse{nil, nil}, want: "6522e152530f4ee76522e152530f4ee7", wantErr: assert.NoError, }, { name: "should return an error if the generated access token exists 3 times", args: args{ctx: context.Background()}, mockStore: &mockResponse{mockedDashboard, nil}, want: "", wantErr: assert.Error, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { store := NewFakePublicDashboardStore(t) store.On("FindByAccessToken", mock.Anything, mock.Anything). Return(tt.mockStore.PublicDashboard, tt.mockStore.Err) pd := &PublicDashboardServiceImpl{store: store} got, err := pd.NewPublicDashboardAccessToken(tt.args.ctx) if !tt.wantErr(t, err, fmt.Sprintf("NewPublicDashboardAccessToken(%v)", tt.args.ctx)) { return } if err == nil { assert.NotEqual(t, got, tt.want, "NewPublicDashboardAccessToken(%v)", tt.args.ctx) assert.True(t, validation.IsValidAccessToken(got), "NewPublicDashboardAccessToken(%v)", tt.args.ctx) store.AssertNumberOfCalls(t, "FindByAccessToken", 1) } else { store.AssertNumberOfCalls(t, "FindByAccessToken", 3) assert.True(t, ErrInternalServerError.Is(err)) } }) } } func TestGenerateAccessToken(t *testing.T) { accessToken, err := GenerateAccessToken() t.Run("length", func(t *testing.T) { require.NoError(t, err) assert.Equal(t, 32, len(accessToken)) }) t.Run("no - ", func(t *testing.T) { assert.False(t, strings.Contains("-", accessToken)) }) } func CreateDatasource(dsType string, uid string) struct { Type *string `json:"type,omitempty"` Uid *string `json:"uid,omitempty"` } { return struct { Type *string `json:"type,omitempty"` Uid *string `json:"uid,omitempty"` }{ Type: &dsType, Uid: &uid, } } func AddAnnotationsToDashboard(t *testing.T, dash *dashboards.Dashboard, annotations []DashAnnotation) *dashboards.Dashboard { type annotationsDto struct { List []DashAnnotation `json:"list"` } annos := annotationsDto{} annos.List = annotations annoJSON, err := json.Marshal(annos) require.NoError(t, err) dashAnnos, err := simplejson.NewJson(annoJSON) require.NoError(t, err) dash.Data.Set("annotations", dashAnnos) return dash } func insertTestDashboard(t *testing.T, dashboardStore dashboards.Store, title string, orgId int64, folderId int64, folderUID string, isFolder bool, templateVars []map[string]any, customPanels []any, tags ...any) *dashboards.Dashboard { t.Helper() var dashboardPanels []any if customPanels != nil { dashboardPanels = customPanels } else { dashboardPanels = []any{ map[string]any{ "id": 1, "datasource": map[string]any{ "uid": "ds1", }, "targets": []any{ map[string]any{ "datasource": map[string]any{ "type": "mysql", "uid": "ds1", }, "refId": "A", }, map[string]any{ "datasource": map[string]any{ "type": "prometheus", "uid": "ds2", }, "refId": "B", }, }, }, map[string]any{ "id": 2, "datasource": map[string]any{ "uid": "ds3", }, "targets": []any{ map[string]any{ "datasource": map[string]any{ "type": "mysql", "uid": "ds3", }, "refId": "C", }, }, }, } } cmd := dashboards.SaveDashboardCommand{ OrgID: orgId, FolderUID: folderUID, IsFolder: isFolder, Dashboard: simplejson.NewFromAny(map[string]any{ "id": nil, "title": title, "tags": tags, "panels": dashboardPanels, "templating": map[string]any{ "list": templateVars, }, "time": map[string]any{ "from": "2022-09-01T00:00:00.000Z", "to": "2022-09-01T12:00:00.000Z", }, }), } dash, err := dashboardStore.SaveDashboard(context.Background(), cmd) require.NoError(t, err) require.NotNil(t, dash) dash.Data.Set("id", dash.ID) dash.Data.Set("uid", dash.UID) return dash }