Public dashboards: move to integration tests (#108735)
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -9,32 +8,15 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/datasources/guardian"
|
||||
datasourceService "github.com/grafana/grafana/pkg/services/datasources/service"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/licensing/licensingtest"
|
||||
"github.com/grafana/grafana/pkg/services/mtdsclient"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
||||
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||
publicdashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||
"github.com/grafana/grafana/pkg/services/query"
|
||||
fakeSecrets "github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
@@ -108,77 +90,3 @@ func callAPI(server *web.Mux, method, path string, body io.Reader, t *testing.T)
|
||||
server.ServeHTTP(recorder, req)
|
||||
return recorder
|
||||
}
|
||||
|
||||
// helper to query.Service
|
||||
// allows us to stub the cache and plugin clients
|
||||
func buildQueryDataService(t *testing.T, cs datasources.CacheService, fpc *fakePluginClient, store db.DB) *query.ServiceImpl {
|
||||
// build database if we need one
|
||||
if store == nil {
|
||||
store = db.InitTestDB(t)
|
||||
}
|
||||
|
||||
// default cache service
|
||||
if cs == nil {
|
||||
cs = datasourceService.ProvideCacheService(localcache.ProvideService(), store, guardian.ProvideGuardian())
|
||||
}
|
||||
|
||||
// default fakePluginClient
|
||||
if fpc == nil {
|
||||
fpc = &fakePluginClient{
|
||||
QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
resp := backend.Responses{
|
||||
"A": backend.DataResponse{
|
||||
Frames: []*data.Frame{{}},
|
||||
},
|
||||
}
|
||||
return &backend.QueryDataResponse{Responses: resp}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
ds := &fakeDatasources.FakeDataSourceService{}
|
||||
pCtxProvider := plugincontext.ProvideService(setting.NewCfg(),
|
||||
localcache.ProvideService(), &pluginstore.FakePluginStore{
|
||||
PluginList: []pluginstore.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "mysql",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, &fakeDatasources.FakeCacheService{}, ds,
|
||||
pluginSettings.ProvideService(store, fakeSecrets.NewFakeSecretsService()), pluginconfig.NewFakePluginRequestConfigProvider())
|
||||
|
||||
return query.ProvideService(
|
||||
setting.NewCfg(),
|
||||
cs,
|
||||
nil,
|
||||
&fakeDataSourceRequestValidator{},
|
||||
fpc,
|
||||
pCtxProvider,
|
||||
mtdsclient.NewNullMTDatasourceClientBuilder(),
|
||||
)
|
||||
}
|
||||
|
||||
// copied from pkg/api/metrics_test.go
|
||||
type fakeDataSourceRequestValidator struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (rv *fakeDataSourceRequestValidator) Validate(ds *datasources.DataSource, req *http.Request) error {
|
||||
return rv.err
|
||||
}
|
||||
|
||||
// copied from pkg/api/plugins_test.go
|
||||
type fakePluginClient struct {
|
||||
plugins.Client
|
||||
backend.QueryDataHandlerFunc
|
||||
}
|
||||
|
||||
func (c *fakePluginClient) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
if c.QueryDataHandlerFunc != nil {
|
||||
return c.QueryDataHandlerFunc.QueryData(ctx, req)
|
||||
}
|
||||
|
||||
return backend.NewQueryDataResponse(), nil
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -20,35 +19,9 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/kvstore"
|
||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/serverlock"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/client"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
dashboardStore "github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/service"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/datasources/guardian"
|
||||
datasourcesService "github.com/grafana/grafana/pkg/services/datasources/service"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||
"github.com/grafana/grafana/pkg/services/folder/foldertest"
|
||||
"github.com/grafana/grafana/pkg/services/licensing/licensingtest"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||
publicdashboardsStore "github.com/grafana/grafana/pkg/services/publicdashboards/database"
|
||||
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||
publicdashboardsService "github.com/grafana/grafana/pkg/services/publicdashboards/service"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"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/web"
|
||||
)
|
||||
|
||||
@@ -258,126 +231,6 @@ func getValidQueryPath(accessToken string) string {
|
||||
return fmt.Sprintf("/api/public/dashboards/%s/panels/2/query", accessToken)
|
||||
}
|
||||
|
||||
func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
db, cfg := db.InitTestDBWithCfg(t)
|
||||
|
||||
cacheService := datasourcesService.ProvideCacheService(localcache.ProvideService(), db, guardian.ProvideGuardian())
|
||||
qds := buildQueryDataService(t, cacheService, nil, db)
|
||||
dsStore := datasourcesService.CreateStore(db, log.New("publicdashboards.test"))
|
||||
_, _ = dsStore.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{
|
||||
UID: "ds1",
|
||||
OrgID: 1,
|
||||
Name: "laban",
|
||||
Type: datasources.DS_MYSQL,
|
||||
Access: datasources.DS_ACCESS_DIRECT,
|
||||
URL: "http://test",
|
||||
Database: "site",
|
||||
ReadOnly: true,
|
||||
})
|
||||
|
||||
// Create Dashboard
|
||||
saveDashboardCmd := dashboards.SaveDashboardCommand{
|
||||
OrgID: 1,
|
||||
FolderUID: "",
|
||||
IsFolder: false,
|
||||
Dashboard: simplejson.NewFromAny(map[string]any{
|
||||
"id": nil,
|
||||
"title": "test",
|
||||
"panels": []map[string]any{
|
||||
{
|
||||
"id": 1,
|
||||
"targets": []map[string]any{
|
||||
{
|
||||
"datasource": map[string]string{
|
||||
"type": "mysql",
|
||||
"uid": "ds1",
|
||||
},
|
||||
"refId": "A",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
// create dashboard
|
||||
dashboardStoreService, err := dashboardStore.ProvideDashboardStore(db, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(db))
|
||||
require.NoError(t, err)
|
||||
dashboard, err := dashboardStoreService.SaveDashboard(context.Background(), saveDashboardCmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create public dashboard
|
||||
isEnabled := true
|
||||
savePubDashboardCmd := &SavePublicDashboardDTO{
|
||||
DashboardUid: dashboard.UID,
|
||||
OrgID: dashboard.OrgID,
|
||||
PublicDashboard: &PublicDashboardDTO{
|
||||
IsEnabled: &isEnabled,
|
||||
},
|
||||
}
|
||||
|
||||
annotationsService := annotationstest.NewFakeAnnotationsRepo()
|
||||
|
||||
// create public dashboard
|
||||
store := publicdashboardsStore.ProvideStore(db, cfg, featuremgmt.WithFeatures())
|
||||
cfg.PublicDashboardsEnabled = true
|
||||
ac := actest.FakeAccessControl{}
|
||||
ws := publicdashboardsService.ProvideServiceWrapper(store)
|
||||
folderStore := folderimpl.ProvideDashboardFolderStore(db)
|
||||
dashPermissionService := acmock.NewMockedPermissionsService()
|
||||
dashService, err := service.ProvideDashboardServiceImpl(
|
||||
cfg, dashboardStoreService, folderStore,
|
||||
featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), ac, actest.FakeService{},
|
||||
foldertest.NewFakeService(), nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil,
|
||||
nil, dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
serverlock.ProvideService(db, tracing.InitializeTracerForTest()),
|
||||
kvstore.NewFakeKVStore(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
dashService.RegisterDashboardPermissions(dashPermissionService)
|
||||
|
||||
license := licensingtest.NewFakeLicensing()
|
||||
license.On("FeatureEnabled", FeaturePublicDashboardsEmailSharing).Return(false)
|
||||
pds := publicdashboardsService.ProvideService(cfg, featuremgmt.WithFeatures(), store, qds, annotationsService, ac, ws, dashService, license)
|
||||
pubdash, err := pds.Create(context.Background(), &user.SignedInUser{}, savePubDashboardCmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
// setup test server
|
||||
server := setupTestServer(t, cfg, pds, anonymousUser)
|
||||
|
||||
resp := callAPI(server, http.MethodPost,
|
||||
fmt.Sprintf("/api/public/dashboards/%s/panels/1/query", pubdash.AccessToken),
|
||||
strings.NewReader(`{}`),
|
||||
t,
|
||||
)
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(
|
||||
t,
|
||||
`{
|
||||
"results": {
|
||||
"A": {
|
||||
"status": 200,
|
||||
"frames": [
|
||||
{
|
||||
"data": {
|
||||
"values": []
|
||||
},
|
||||
"schema": {
|
||||
"fields": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`,
|
||||
resp.Body.String(),
|
||||
)
|
||||
}
|
||||
|
||||
func TestAPIGetAnnotations(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Name string
|
||||
|
||||
@@ -16,33 +16,16 @@ import (
|
||||
|
||||
"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/kvstore"
|
||||
"github.com/grafana/grafana/pkg/infra/serverlock"
|
||||
"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"
|
||||
"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/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"
|
||||
)
|
||||
@@ -1407,548 +1390,6 @@ func TestDashboardEnabledChanged(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
features := featuremgmt.WithFeatures()
|
||||
testDB, cfg := db.InitTestDBWithCfg(t)
|
||||
dashStore, err := dashboardsDB.ProvideDashboardStore(testDB, cfg, features, tagimpl.ProvideService(testDB))
|
||||
require.NoError(t, err)
|
||||
ac := actest.FakeAccessControl{ExpectedEvaluate: true}
|
||||
|
||||
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(), apiserver.WithoutRestConfig)
|
||||
|
||||
dashboardService, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, ac, actest.FakeService{}, folderSvc, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
serverlock.ProvideService(testDB, tracing.InitializeTracerForTest()),
|
||||
kvstore.NewFakeKVStore())
|
||||
require.NoError(t, err)
|
||||
dashboardService.RegisterDashboardPermissions(&actest.FakePermissionsService{})
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
package publicdashboards
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/tests"
|
||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||
)
|
||||
|
||||
func TestPublicDashboardQueryAPI(t *testing.T) {
|
||||
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
AppModeProduction: false,
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagPublicDashboardsEmailSharing,
|
||||
},
|
||||
})
|
||||
|
||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
|
||||
adminUsername := fmt.Sprintf("testadmin-%d", time.Now().UnixNano())
|
||||
tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
|
||||
DefaultOrgRole: string(org.RoleAdmin),
|
||||
Login: adminUsername,
|
||||
Password: "admin",
|
||||
IsAdmin: true,
|
||||
})
|
||||
adminClient := createHTTPClient(grafanaListedAddr, adminUsername, "admin")
|
||||
|
||||
datasourcePayload := map[string]interface{}{
|
||||
"name": "Test Data Source",
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus",
|
||||
"url": "http://localhost:9090",
|
||||
"access": "proxy",
|
||||
}
|
||||
datasourceBytes, err := json.Marshal(datasourcePayload)
|
||||
require.NoError(t, err)
|
||||
var datasourceResult map[string]interface{}
|
||||
createDatasourceResp := doRequest(t, adminClient, "POST", "/api/datasources", datasourceBytes, &datasourceResult)
|
||||
require.Equal(t, 200, createDatasourceResp.StatusCode)
|
||||
|
||||
t.Run("unauthenticated user can query public dashboard panel", func(t *testing.T) {
|
||||
// create dashboard first
|
||||
dashboardPayload := map[string]interface{}{
|
||||
"dashboard": map[string]interface{}{
|
||||
"title": "Test Dashboard for Query",
|
||||
"time": map[string]interface{}{
|
||||
"from": "now-1h",
|
||||
"to": "now",
|
||||
},
|
||||
"panels": []map[string]interface{}{
|
||||
{
|
||||
"id": 1,
|
||||
"type": "stat",
|
||||
"title": "Test Panel",
|
||||
"targets": []map[string]interface{}{
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "up",
|
||||
"datasource": map[string]interface{}{
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"folderUid": "",
|
||||
"overwrite": false,
|
||||
}
|
||||
payloadBytes, err := json.Marshal(dashboardPayload)
|
||||
require.NoError(t, err)
|
||||
var dashboardResult map[string]interface{}
|
||||
createDashboardResp := doRequest(t, adminClient, "POST", "/api/dashboards/db", payloadBytes, &dashboardResult)
|
||||
require.Equal(t, 200, createDashboardResp.StatusCode)
|
||||
|
||||
// make it public
|
||||
dashboardUID := dashboardResult["uid"].(string)
|
||||
publicDashboardPayload := map[string]interface{}{
|
||||
"isEnabled": true,
|
||||
"annotationsEnabled": false,
|
||||
"timeSelectionEnabled": false,
|
||||
"share": "public",
|
||||
}
|
||||
payloadBytes, err = json.Marshal(publicDashboardPayload)
|
||||
require.NoError(t, err)
|
||||
createURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards", dashboardUID)
|
||||
var publicDashboard map[string]interface{}
|
||||
createResp := doRequest(t, adminClient, "POST", createURL, payloadBytes, &publicDashboard)
|
||||
require.Equal(t, 200, createResp.StatusCode)
|
||||
assert.Equal(t, true, publicDashboard["isEnabled"])
|
||||
assert.NotEmpty(t, publicDashboard["accessToken"])
|
||||
|
||||
// test unauthenticated query to the public dashboard panel
|
||||
accessToken := publicDashboard["accessToken"].(string)
|
||||
queryPayload := map[string]interface{}{}
|
||||
queryBytes, err := json.Marshal(queryPayload)
|
||||
require.NoError(t, err)
|
||||
queryURL := fmt.Sprintf("/api/public/dashboards/%s/panels/1/query", accessToken)
|
||||
unauthenticatedClient := createUnauthenticatedClient(grafanaListedAddr)
|
||||
|
||||
var queryResult map[string]interface{}
|
||||
doRequest(t, unauthenticatedClient, "POST", queryURL, queryBytes, &queryResult)
|
||||
assert.NotNil(t, queryResult["results"])
|
||||
results := queryResult["results"].(map[string]interface{})
|
||||
assert.NotNil(t, results["A"])
|
||||
})
|
||||
|
||||
t.Run("unauthenticated user cannot query disabled public dashboard", func(t *testing.T) {
|
||||
// create the dashboard
|
||||
dashboardPayload := map[string]interface{}{
|
||||
"dashboard": map[string]interface{}{
|
||||
"title": "Test Disabled Dashboard",
|
||||
"time": map[string]interface{}{
|
||||
"from": "now-1h",
|
||||
"to": "now",
|
||||
},
|
||||
"panels": []map[string]interface{}{
|
||||
{
|
||||
"id": 1,
|
||||
"type": "stat",
|
||||
"title": "Test Panel",
|
||||
},
|
||||
},
|
||||
},
|
||||
"folderUid": "",
|
||||
"overwrite": false,
|
||||
}
|
||||
payloadBytes, err := json.Marshal(dashboardPayload)
|
||||
require.NoError(t, err)
|
||||
var dashboardResult map[string]interface{}
|
||||
createDashboardResp := doRequest(t, adminClient, "POST", "/api/dashboards/db", payloadBytes, &dashboardResult)
|
||||
require.Equal(t, 200, createDashboardResp.StatusCode)
|
||||
|
||||
// make it a disabled public dashboard
|
||||
dashboardUID := dashboardResult["uid"].(string)
|
||||
publicDashboardPayload := map[string]interface{}{
|
||||
"isEnabled": false,
|
||||
"annotationsEnabled": false,
|
||||
"timeSelectionEnabled": true,
|
||||
"share": "public",
|
||||
}
|
||||
payloadBytes, err = json.Marshal(publicDashboardPayload)
|
||||
require.NoError(t, err)
|
||||
createURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards", dashboardUID)
|
||||
var publicDashboard map[string]interface{}
|
||||
createResp := doRequest(t, adminClient, "POST", createURL, payloadBytes, &publicDashboard)
|
||||
require.Equal(t, 200, createResp.StatusCode)
|
||||
assert.Equal(t, false, publicDashboard["isEnabled"])
|
||||
assert.NotEmpty(t, publicDashboard["accessToken"])
|
||||
|
||||
accessToken := publicDashboard["accessToken"].(string)
|
||||
|
||||
queryPayload := map[string]interface{}{
|
||||
"intervalMs": 1000,
|
||||
"maxDataPoints": 100,
|
||||
"timeRange": map[string]interface{}{
|
||||
"from": "now-1h",
|
||||
"to": "now",
|
||||
},
|
||||
}
|
||||
queryBytes, err := json.Marshal(queryPayload)
|
||||
require.NoError(t, err)
|
||||
|
||||
// should not be able to query anymore
|
||||
queryURL := fmt.Sprintf("/api/public/dashboards/%s/panels/1/query", accessToken)
|
||||
unauthenticatedClient := createUnauthenticatedClient(grafanaListedAddr)
|
||||
var queryResult map[string]interface{}
|
||||
queryResp := doRequest(t, unauthenticatedClient, "POST", queryURL, queryBytes, &queryResult)
|
||||
require.Equal(t, 403, queryResp.StatusCode)
|
||||
require.Nil(t, queryResult["results"])
|
||||
})
|
||||
}
|
||||
|
||||
func createUnauthenticatedClient(host string) *httpClient {
|
||||
baseURL := fmt.Sprintf("http://%s", host)
|
||||
return &httpClient{
|
||||
baseURL: baseURL,
|
||||
client: &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
package publicdashboards
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/tests"
|
||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
testsuite.Run(m)
|
||||
}
|
||||
|
||||
func TestPublicDashboardsAPI(t *testing.T) {
|
||||
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
AppModeProduction: false,
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagPublicDashboardsEmailSharing,
|
||||
},
|
||||
})
|
||||
|
||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
|
||||
adminUsername := fmt.Sprintf("testadmin-%d", time.Now().UnixNano())
|
||||
tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
|
||||
DefaultOrgRole: string(org.RoleAdmin),
|
||||
Login: adminUsername,
|
||||
Password: "admin",
|
||||
IsAdmin: true,
|
||||
})
|
||||
adminClient := createHTTPClient(grafanaListedAddr, adminUsername, "admin")
|
||||
|
||||
t.Run("should create, get, update, and delete public dashboard", func(t *testing.T) {
|
||||
dashboardPayload := map[string]interface{}{
|
||||
"dashboard": map[string]interface{}{
|
||||
"title": "Test Dashboard",
|
||||
"panels": []map[string]interface{}{
|
||||
{
|
||||
"id": 1,
|
||||
"type": "stat",
|
||||
"title": "Test Panel",
|
||||
},
|
||||
},
|
||||
},
|
||||
"folderUid": "",
|
||||
"overwrite": false,
|
||||
}
|
||||
|
||||
payloadBytes, err := json.Marshal(dashboardPayload)
|
||||
require.NoError(t, err)
|
||||
|
||||
var dashboardResult map[string]interface{}
|
||||
createDashboardResp := doRequest(t, adminClient, "POST", "/api/dashboards/db", payloadBytes, &dashboardResult)
|
||||
require.Equal(t, 200, createDashboardResp.StatusCode)
|
||||
|
||||
dashboardUID := dashboardResult["uid"].(string)
|
||||
|
||||
var listResult map[string]interface{}
|
||||
doRequest(t, adminClient, "GET", "/api/dashboards/public-dashboards", nil, &listResult)
|
||||
publicDashboardPayload := map[string]interface{}{
|
||||
"isEnabled": true,
|
||||
"annotationsEnabled": false,
|
||||
"timeSelectionEnabled": true,
|
||||
"share": "public",
|
||||
}
|
||||
|
||||
payloadBytes, err = json.Marshal(publicDashboardPayload)
|
||||
require.NoError(t, err)
|
||||
|
||||
createURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards", dashboardUID)
|
||||
var publicDashboard map[string]interface{}
|
||||
createResp := doRequest(t, adminClient, "POST", createURL, payloadBytes, &publicDashboard)
|
||||
require.Equal(t, 200, createResp.StatusCode)
|
||||
assert.Equal(t, true, publicDashboard["isEnabled"])
|
||||
assert.Equal(t, false, publicDashboard["annotationsEnabled"])
|
||||
assert.Equal(t, true, publicDashboard["timeSelectionEnabled"])
|
||||
assert.Equal(t, "public", publicDashboard["share"])
|
||||
assert.NotEmpty(t, publicDashboard["accessToken"])
|
||||
assert.NotEmpty(t, publicDashboard["uid"])
|
||||
|
||||
accessToken := publicDashboard["accessToken"].(string)
|
||||
publicDashboardUID := publicDashboard["uid"].(string)
|
||||
|
||||
// get the public dashboard
|
||||
getURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards", dashboardUID)
|
||||
var retrievedPD map[string]interface{}
|
||||
getResp := doRequest(t, adminClient, "GET", getURL, nil, &retrievedPD)
|
||||
require.Equal(t, 200, getResp.StatusCode)
|
||||
|
||||
// view the public dashboard
|
||||
viewURL := fmt.Sprintf("/api/public/dashboards/%s", accessToken)
|
||||
var dashboardData map[string]interface{}
|
||||
viewResp := doRequest(t, adminClient, "GET", viewURL, nil, &dashboardData)
|
||||
require.Equal(t, 200, viewResp.StatusCode)
|
||||
assert.Equal(t, "Test Dashboard", dashboardData["dashboard"].(map[string]interface{})["title"])
|
||||
assert.Equal(t, "Test Panel", dashboardData["dashboard"].(map[string]interface{})["panels"].([]interface{})[0].(map[string]interface{})["title"])
|
||||
|
||||
updatePayload := map[string]interface{}{
|
||||
"isEnabled": false,
|
||||
"annotationsEnabled": true,
|
||||
"timeSelectionEnabled": false,
|
||||
"share": "email",
|
||||
}
|
||||
updateBytes, err := json.Marshal(updatePayload)
|
||||
require.NoError(t, err)
|
||||
updateURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards/%s", dashboardUID, publicDashboardUID)
|
||||
var updatedPD map[string]interface{}
|
||||
updateResp := doRequest(t, adminClient, "PATCH", updateURL, updateBytes, &updatedPD)
|
||||
require.Equal(t, 200, updateResp.StatusCode)
|
||||
assert.Equal(t, false, updatedPD["isEnabled"])
|
||||
assert.Equal(t, true, updatedPD["annotationsEnabled"])
|
||||
assert.Equal(t, false, updatedPD["timeSelectionEnabled"])
|
||||
assert.Equal(t, "email", updatedPD["share"])
|
||||
|
||||
deleteURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards/%s", dashboardUID, publicDashboardUID)
|
||||
var deleteResult map[string]interface{}
|
||||
deleteResp := doRequest(t, adminClient, "DELETE", deleteURL, nil, &deleteResult)
|
||||
require.Equal(t, 200, deleteResp.StatusCode)
|
||||
var getAfterDeleteResult map[string]interface{}
|
||||
getAfterDeleteResp := doRequest(t, adminClient, "GET", getURL, nil, &getAfterDeleteResult)
|
||||
require.Equal(t, 404, getAfterDeleteResp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("should list public dashboards", func(t *testing.T) {
|
||||
dashboardPayload := map[string]interface{}{
|
||||
"dashboard": map[string]interface{}{
|
||||
"title": "Test Dashboard for List",
|
||||
"panels": []map[string]interface{}{
|
||||
{
|
||||
"id": 1,
|
||||
"type": "stat",
|
||||
"title": "Test Panel",
|
||||
},
|
||||
},
|
||||
},
|
||||
"folderUid": "",
|
||||
"overwrite": false,
|
||||
}
|
||||
|
||||
payloadBytes, err := json.Marshal(dashboardPayload)
|
||||
require.NoError(t, err)
|
||||
|
||||
var dashboardResult map[string]interface{}
|
||||
createDashboardResp := doRequest(t, adminClient, "POST", "/api/dashboards/db", payloadBytes, &dashboardResult)
|
||||
require.Equal(t, 200, createDashboardResp.StatusCode)
|
||||
dashboardUID := dashboardResult["uid"].(string)
|
||||
|
||||
publicDashboardPayload := map[string]interface{}{
|
||||
"isEnabled": true,
|
||||
"annotationsEnabled": false,
|
||||
"timeSelectionEnabled": true,
|
||||
"share": "public",
|
||||
}
|
||||
|
||||
payloadBytes, err = json.Marshal(publicDashboardPayload)
|
||||
require.NoError(t, err)
|
||||
|
||||
createURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards", dashboardUID)
|
||||
var createResult map[string]interface{}
|
||||
createResp := doRequest(t, adminClient, "POST", createURL, payloadBytes, &createResult)
|
||||
require.Equal(t, 200, createResp.StatusCode)
|
||||
|
||||
var listData map[string]interface{}
|
||||
listResp := doRequest(t, adminClient, "GET", "/api/dashboards/public-dashboards", nil, &listData)
|
||||
require.Equal(t, 200, listResp.StatusCode)
|
||||
assert.NotEmpty(t, listData["publicDashboards"])
|
||||
publicDashboards := listData["publicDashboards"].([]interface{})
|
||||
assert.GreaterOrEqual(t, len(publicDashboards), 1)
|
||||
})
|
||||
|
||||
t.Run("should handle invalid access token", func(t *testing.T) {
|
||||
var viewResult map[string]interface{}
|
||||
viewResp := doRequest(t, adminClient, "GET", "/api/public/dashboards/invalid-token", nil, &viewResult)
|
||||
require.Equal(t, 400, viewResp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("should handle disabled public dashboard", func(t *testing.T) {
|
||||
dashboardPayload := map[string]interface{}{
|
||||
"dashboard": map[string]interface{}{
|
||||
"title": "Test Dashboard Disabled",
|
||||
"panels": []map[string]interface{}{
|
||||
{
|
||||
"id": 1,
|
||||
"type": "stat",
|
||||
"title": "Test Panel",
|
||||
},
|
||||
},
|
||||
},
|
||||
"folderUid": "",
|
||||
"overwrite": false,
|
||||
}
|
||||
|
||||
payloadBytes, err := json.Marshal(dashboardPayload)
|
||||
require.NoError(t, err)
|
||||
|
||||
var dashboardResult map[string]interface{}
|
||||
createDashboardResp := doRequest(t, adminClient, "POST", "/api/dashboards/db", payloadBytes, &dashboardResult)
|
||||
require.Equal(t, 200, createDashboardResp.StatusCode)
|
||||
|
||||
dashboardUID := dashboardResult["uid"].(string)
|
||||
publicDashboardPayload := map[string]interface{}{
|
||||
"isEnabled": false,
|
||||
"annotationsEnabled": false,
|
||||
"timeSelectionEnabled": true,
|
||||
"share": "public",
|
||||
}
|
||||
|
||||
payloadBytes, err = json.Marshal(publicDashboardPayload)
|
||||
require.NoError(t, err)
|
||||
|
||||
createURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards", dashboardUID)
|
||||
var publicDashboard map[string]interface{}
|
||||
createResp := doRequest(t, adminClient, "POST", createURL, payloadBytes, &publicDashboard)
|
||||
require.Equal(t, 200, createResp.StatusCode)
|
||||
accessToken := publicDashboard["accessToken"].(string)
|
||||
|
||||
var viewResult map[string]interface{}
|
||||
viewResp := doRequest(t, adminClient, "GET", fmt.Sprintf("/api/public/dashboards/%s", accessToken), nil, &viewResult)
|
||||
require.Equal(t, 403, viewResp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("permission test", func(t *testing.T) {
|
||||
dashboards := []map[string]interface{}{
|
||||
{
|
||||
"dashboard": map[string]interface{}{
|
||||
"title": "test",
|
||||
"uid": "9S6TmO67z",
|
||||
"panels": []map[string]interface{}{
|
||||
{
|
||||
"id": 1,
|
||||
"type": "stat",
|
||||
"title": "Test Panel",
|
||||
},
|
||||
},
|
||||
},
|
||||
"folderUid": "",
|
||||
"overwrite": false,
|
||||
},
|
||||
{
|
||||
"dashboard": map[string]interface{}{
|
||||
"title": "my first dashboard",
|
||||
"uid": "1S6TmO67z",
|
||||
"panels": []map[string]interface{}{
|
||||
{
|
||||
"id": 1,
|
||||
"type": "stat",
|
||||
"title": "Test Panel",
|
||||
},
|
||||
},
|
||||
},
|
||||
"folderUid": "",
|
||||
"overwrite": false,
|
||||
},
|
||||
{
|
||||
"dashboard": map[string]interface{}{
|
||||
"title": "my second dashboard",
|
||||
"uid": "2S6TmO67z",
|
||||
"panels": []map[string]interface{}{
|
||||
{
|
||||
"id": 1,
|
||||
"type": "stat",
|
||||
"title": "Test Panel",
|
||||
},
|
||||
},
|
||||
},
|
||||
"folderUid": "",
|
||||
"overwrite": false,
|
||||
},
|
||||
{
|
||||
"dashboard": map[string]interface{}{
|
||||
"title": "my zero dashboard",
|
||||
"uid": "0S6TmO67z",
|
||||
"panels": []map[string]interface{}{
|
||||
{
|
||||
"id": 1,
|
||||
"type": "stat",
|
||||
"title": "Test Panel",
|
||||
},
|
||||
},
|
||||
},
|
||||
"folderUid": "",
|
||||
"overwrite": false,
|
||||
},
|
||||
}
|
||||
|
||||
dashboardUIDs := make([]string, len(dashboards))
|
||||
publicDashboardUIDs := make([]string, len(dashboards))
|
||||
|
||||
for i, dashboardPayload := range dashboards {
|
||||
payloadBytes, err := json.Marshal(dashboardPayload)
|
||||
require.NoError(t, err)
|
||||
|
||||
var dashboardResult map[string]interface{}
|
||||
createDashboardResp := doRequest(t, adminClient, "POST", "/api/dashboards/db", payloadBytes, &dashboardResult)
|
||||
require.Equal(t, 200, createDashboardResp.StatusCode)
|
||||
dashboardUIDs[i] = dashboardResult["uid"].(string)
|
||||
|
||||
isEnabled := i != 1
|
||||
publicDashboardPayload := map[string]interface{}{
|
||||
"isEnabled": isEnabled,
|
||||
"annotationsEnabled": false,
|
||||
"timeSelectionEnabled": true,
|
||||
"share": "public",
|
||||
}
|
||||
|
||||
payloadBytes, err = json.Marshal(publicDashboardPayload)
|
||||
require.NoError(t, err)
|
||||
|
||||
createURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards", dashboardUIDs[i])
|
||||
var publicDashboard map[string]interface{}
|
||||
createResp := doRequest(t, adminClient, "POST", createURL, payloadBytes, &publicDashboard)
|
||||
require.Equal(t, 200, createResp.StatusCode)
|
||||
publicDashboardUIDs[i] = publicDashboard["uid"].(string)
|
||||
}
|
||||
|
||||
t.Run("admin user should see all dashboards", func(t *testing.T) {
|
||||
var listData map[string]interface{}
|
||||
listResp := doRequest(t, adminClient, "GET", "/api/dashboards/public-dashboards?page=1&perpage=50", nil, &listData)
|
||||
require.Equal(t, 200, listResp.StatusCode)
|
||||
|
||||
totalCount := int64(listData["totalCount"].(float64))
|
||||
assert.GreaterOrEqual(t, totalCount, int64(4))
|
||||
})
|
||||
|
||||
t.Run("user with access to just one dashboard should see only that dashboard", func(t *testing.T) {
|
||||
limitedUserUsername := fmt.Sprintf("limiteduser-%d", time.Now().UnixNano())
|
||||
limitedUserID := tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
|
||||
DefaultOrgRole: string(org.RoleNone),
|
||||
Login: limitedUserUsername,
|
||||
Password: "password",
|
||||
IsAdmin: false,
|
||||
})
|
||||
limitedUserClient := createHTTPClient(grafanaListedAddr, limitedUserUsername, "password")
|
||||
permissionPayload := map[string]interface{}{
|
||||
"permission": "View",
|
||||
}
|
||||
permissionBytes, err := json.Marshal(permissionPayload)
|
||||
require.NoError(t, err)
|
||||
|
||||
permissionURL := fmt.Sprintf("/api/access-control/dashboards/9S6TmO67z/users/%d", limitedUserID)
|
||||
var permissionResult map[string]interface{}
|
||||
permissionResp := doRequest(t, adminClient, "POST", permissionURL, permissionBytes, &permissionResult)
|
||||
require.Equal(t, 200, permissionResp.StatusCode)
|
||||
|
||||
var listData map[string]interface{}
|
||||
listResp := doRequest(t, limitedUserClient, "GET", "/api/dashboards/public-dashboards?page=1&perpage=50", nil, &listData)
|
||||
require.Equal(t, 200, listResp.StatusCode)
|
||||
|
||||
totalCount := int64(listData["totalCount"].(float64))
|
||||
assert.Equal(t, int64(1), totalCount)
|
||||
})
|
||||
|
||||
t.Run("pagination should work correctly", func(t *testing.T) {
|
||||
var listData map[string]interface{}
|
||||
listResp := doRequest(t, adminClient, "GET", "/api/dashboards/public-dashboards?page=1&perpage=2", nil, &listData)
|
||||
require.Equal(t, 200, listResp.StatusCode)
|
||||
assert.NotEmpty(t, listData["publicDashboards"])
|
||||
publicDashboards := listData["publicDashboards"].([]interface{})
|
||||
assert.Equal(t, 2, len(publicDashboards))
|
||||
totalCount := int64(listData["totalCount"].(float64))
|
||||
assert.GreaterOrEqual(t, totalCount, int64(4))
|
||||
|
||||
var listDataPage2 map[string]interface{}
|
||||
listRespPage2 := doRequest(t, adminClient, "GET", "/api/dashboards/public-dashboards?page=2&perpage=2", nil, &listDataPage2)
|
||||
require.Equal(t, 200, listRespPage2.StatusCode)
|
||||
publicDashboardsPage2 := listDataPage2["publicDashboards"].([]interface{})
|
||||
assert.Equal(t, 2, len(publicDashboardsPage2))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type httpClient struct {
|
||||
baseURL string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func createHTTPClient(host, username, password string) *httpClient {
|
||||
baseURL := fmt.Sprintf("http://%s:%s@%s", username, password, host)
|
||||
return &httpClient{
|
||||
baseURL: baseURL,
|
||||
client: &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type httpResponse struct {
|
||||
StatusCode int
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func doRequest(t *testing.T, client *httpClient, method, path string, body []byte, result interface{}) httpResponse {
|
||||
t.Helper()
|
||||
|
||||
var req *http.Request
|
||||
var err error
|
||||
|
||||
url := client.baseURL + path
|
||||
if body != nil {
|
||||
req, err = http.NewRequest(method, url, bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
} else {
|
||||
req, err = http.NewRequest(method, url, nil)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := client.client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close() // nolint:errcheck
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
response := httpResponse{
|
||||
StatusCode: resp.StatusCode,
|
||||
Body: respBody,
|
||||
}
|
||||
|
||||
if result != nil && len(respBody) > 0 {
|
||||
err = json.Unmarshal(respBody, result)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
Reference in New Issue
Block a user