grafana/pkg/services/librarypanels/librarypanels_test.go
Karl Persson c4a75f9eb3
RBAC: Add scope resolvers for dashboards (#50110)
* Inject access control into dashboard service

* Add function to parse id scopes

* Add dashboard as return value

* Update mock

* Return only err to keep service interface

* Add scope resolvers for dashboard id scopes

* Add function to parse uid scopes

* Add dashboard uid scope resolver

* Register scope resolvers for dashboards

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
2022-06-07 11:02:20 +02:00

1553 lines
45 KiB
Go

package librarypanels
import (
"context"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/database"
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
)
const userInDbName = "user_in_db"
const userInDbAvatar = "/avatar/402d08de060496d6b6874495fe20f5ad"
func TestLoadLibraryPanelsForDashboard(t *testing.T) {
scenarioWithLibraryPanel(t, "When an admin tries to load a dashboard with a library panel, it should copy JSON properties from library panel.",
func(t *testing.T, sc scenarioContext) {
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"libraryPanel": map[string]interface{}{
"uid": sc.initialResult.Result.UID,
"name": sc.initialResult.Result.Name,
},
},
},
}
dash := models.Dashboard{
Title: "Testing LoadLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err := sc.elementService.ConnectElementsToDashboard(sc.ctx, sc.user, []string{sc.initialResult.Result.UID}, dashInDB.Id)
require.NoError(t, err)
err = sc.service.LoadLibraryPanelsForDashboard(sc.ctx, dashInDB)
require.NoError(t, err)
expectedJSON := map[string]interface{}{
"title": "Testing LoadLibraryPanelsForDashboard",
"uid": dashInDB.Uid,
"version": dashInDB.Version,
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"datasource": "${DS_GDEV-TESTDATA}",
"description": "A description",
"libraryPanel": map[string]interface{}{
"uid": sc.initialResult.Result.UID,
"name": sc.initialResult.Result.Name,
"type": sc.initialResult.Result.Type,
"description": sc.initialResult.Result.Description,
"version": sc.initialResult.Result.Version,
"meta": map[string]interface{}{
"folderName": "ScenarioFolder",
"folderUid": sc.folder.Uid,
"connectedDashboards": int64(1),
"created": sc.initialResult.Result.Meta.Created,
"updated": sc.initialResult.Result.Meta.Updated,
"createdBy": map[string]interface{}{
"id": sc.initialResult.Result.Meta.CreatedBy.ID,
"name": userInDbName,
"avatarUrl": userInDbAvatar,
},
"updatedBy": map[string]interface{}{
"id": sc.initialResult.Result.Meta.UpdatedBy.ID,
"name": userInDbName,
"avatarUrl": userInDbAvatar,
},
},
},
"title": "Text - Library Panel",
"type": "text",
},
},
}
expected := simplejson.NewFromAny(expectedJSON)
if diff := cmp.Diff(expected.Interface(), dash.Data.Interface(), getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
scenarioWithLibraryPanel(t, "When an admin tries to load a dashboard with library panels inside and outside of rows, it should copy JSON properties from library panels",
func(t *testing.T, sc scenarioContext) {
cmd := libraryelements.CreateLibraryElementCommand{
FolderID: sc.initialResult.Result.FolderID,
Name: "Outside row",
Model: []byte(`
{
"datasource": "${DS_GDEV-TESTDATA}",
"id": 1,
"title": "Text - Library Panel",
"type": "text",
"description": "A description"
}
`),
Kind: int64(models.PanelElement),
}
outsidePanel, err := sc.elementService.CreateElement(sc.ctx, sc.user, cmd)
require.NoError(t, err)
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"collapsed": true,
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 6,
},
"id": int64(2),
"type": "row",
"panels": []interface{}{
map[string]interface{}{
"id": int64(3),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 7,
},
},
map[string]interface{}{
"id": int64(4),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 13,
},
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"uid": sc.initialResult.Result.UID,
},
"title": "Inside row",
"type": "text",
},
},
},
map[string]interface{}{
"id": int64(5),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 19,
},
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"uid": outsidePanel.UID,
},
"title": "Outside row",
"type": "text",
},
},
}
dash := models.Dashboard{
Title: "Testing LoadLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err = sc.elementService.ConnectElementsToDashboard(sc.ctx, sc.user, []string{outsidePanel.UID, sc.initialResult.Result.UID}, dashInDB.Id)
require.NoError(t, err)
err = sc.service.LoadLibraryPanelsForDashboard(sc.ctx, dashInDB)
require.NoError(t, err)
expectedJSON := map[string]interface{}{
"title": "Testing LoadLibraryPanelsForDashboard",
"uid": dashInDB.Uid,
"version": dashInDB.Version,
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"collapsed": true,
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 6,
},
"id": int64(2),
"type": "row",
"panels": []interface{}{
map[string]interface{}{
"id": int64(3),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 7,
},
},
map[string]interface{}{
"id": int64(4),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 13,
},
"datasource": "${DS_GDEV-TESTDATA}",
"description": "A description",
"libraryPanel": map[string]interface{}{
"uid": sc.initialResult.Result.UID,
"name": sc.initialResult.Result.Name,
"type": sc.initialResult.Result.Type,
"description": sc.initialResult.Result.Description,
"version": sc.initialResult.Result.Version,
"meta": map[string]interface{}{
"folderName": "ScenarioFolder",
"folderUid": sc.folder.Uid,
"connectedDashboards": int64(1),
"created": sc.initialResult.Result.Meta.Created,
"updated": sc.initialResult.Result.Meta.Updated,
"createdBy": map[string]interface{}{
"id": sc.initialResult.Result.Meta.CreatedBy.ID,
"name": userInDbName,
"avatarUrl": userInDbAvatar,
},
"updatedBy": map[string]interface{}{
"id": sc.initialResult.Result.Meta.UpdatedBy.ID,
"name": userInDbName,
"avatarUrl": userInDbAvatar,
},
},
},
"title": "Text - Library Panel",
"type": "text",
},
},
},
map[string]interface{}{
"id": int64(5),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 19,
},
"datasource": "${DS_GDEV-TESTDATA}",
"description": "A description",
"libraryPanel": map[string]interface{}{
"uid": outsidePanel.UID,
"name": outsidePanel.Name,
"type": outsidePanel.Type,
"description": outsidePanel.Description,
"version": outsidePanel.Version,
"meta": map[string]interface{}{
"folderName": "ScenarioFolder",
"folderUid": sc.folder.Uid,
"connectedDashboards": int64(1),
"created": outsidePanel.Meta.Created,
"updated": outsidePanel.Meta.Updated,
"createdBy": map[string]interface{}{
"id": outsidePanel.Meta.CreatedBy.ID,
"name": userInDbName,
"avatarUrl": userInDbAvatar,
},
"updatedBy": map[string]interface{}{
"id": outsidePanel.Meta.UpdatedBy.ID,
"name": userInDbName,
"avatarUrl": userInDbAvatar,
},
},
},
"title": "Text - Library Panel",
"type": "text",
},
},
}
expected := simplejson.NewFromAny(expectedJSON)
if diff := cmp.Diff(expected.Interface(), dash.Data.Interface(), getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
scenarioWithLibraryPanel(t, "When an admin tries to load a dashboard with a library panel without uid, it should fail",
func(t *testing.T, sc scenarioContext) {
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"libraryPanel": map[string]interface{}{
"name": sc.initialResult.Result.Name,
},
},
},
}
dash := models.Dashboard{
Title: "Testing LoadLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err := sc.elementService.ConnectElementsToDashboard(sc.ctx, sc.user, []string{sc.initialResult.Result.UID}, dashInDB.Id)
require.NoError(t, err)
err = sc.service.LoadLibraryPanelsForDashboard(sc.ctx, dashInDB)
require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error())
})
scenarioWithLibraryPanel(t, "When an admin tries to load a dashboard with a library panel that is not connected, it should set correct JSON and continue",
func(t *testing.T, sc scenarioContext) {
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"libraryPanel": map[string]interface{}{
"uid": sc.initialResult.Result.UID,
},
},
},
}
dash := models.Dashboard{
Title: "Testing LoadLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err := sc.service.LoadLibraryPanelsForDashboard(sc.ctx, dashInDB)
require.NoError(t, err)
expectedJSON := map[string]interface{}{
"title": "Testing LoadLibraryPanelsForDashboard",
"uid": dashInDB.Uid,
"version": dashInDB.Version,
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"libraryPanel": map[string]interface{}{
"uid": sc.initialResult.Result.UID,
},
"type": fmt.Sprintf("Library panel with UID: \"%s\"", sc.initialResult.Result.UID),
},
},
}
expected := simplejson.NewFromAny(expectedJSON)
if diff := cmp.Diff(expected.Interface(), dash.Data.Interface(), getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
}
func TestCleanLibraryPanelsForDashboard(t *testing.T) {
scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with a library panel, it should just keep the correct JSON properties in library panel",
func(t *testing.T, sc scenarioContext) {
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"uid": sc.initialResult.Result.UID,
},
"title": "Text - Library Panel",
"type": "text",
},
},
}
dash := models.Dashboard{
Title: "Testing CleanLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err := sc.service.CleanLibraryPanelsForDashboard(dashInDB)
require.NoError(t, err)
expectedJSON := map[string]interface{}{
"title": "Testing CleanLibraryPanelsForDashboard",
"uid": dashInDB.Uid,
"version": dashInDB.Version,
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"libraryPanel": map[string]interface{}{
"uid": sc.initialResult.Result.UID,
},
},
},
}
expected := simplejson.NewFromAny(expectedJSON)
if diff := cmp.Diff(expected.Interface(), dash.Data.Interface(), getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with library panels inside and outside of rows, it should just keep the correct JSON properties",
func(t *testing.T, sc scenarioContext) {
cmd := libraryelements.CreateLibraryElementCommand{
FolderID: sc.initialResult.Result.FolderID,
Name: "Outside row",
Model: []byte(`
{
"datasource": "${DS_GDEV-TESTDATA}",
"id": 1,
"title": "Text - Library Panel",
"type": "text",
"description": "A description"
}
`),
Kind: int64(models.PanelElement),
}
outsidePanel, err := sc.elementService.CreateElement(sc.ctx, sc.user, cmd)
require.NoError(t, err)
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"collapsed": true,
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 6,
},
"id": int64(2),
"type": "row",
"panels": []interface{}{
map[string]interface{}{
"id": int64(3),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 7,
},
},
map[string]interface{}{
"id": int64(4),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 13,
},
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"uid": sc.initialResult.Result.UID,
},
"title": "Inside row",
"type": "text",
},
},
},
map[string]interface{}{
"id": int64(5),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 19,
},
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"uid": outsidePanel.UID,
},
"title": "Outside row",
"type": "text",
},
},
}
dash := models.Dashboard{
Title: "Testing CleanLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err = sc.service.CleanLibraryPanelsForDashboard(dashInDB)
require.NoError(t, err)
expectedJSON := map[string]interface{}{
"title": "Testing CleanLibraryPanelsForDashboard",
"uid": dashInDB.Uid,
"version": dashInDB.Version,
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"collapsed": true,
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 6,
},
"id": int64(2),
"type": "row",
"panels": []interface{}{
map[string]interface{}{
"id": int64(3),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 7,
},
},
map[string]interface{}{
"id": int64(4),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 13,
},
"libraryPanel": map[string]interface{}{
"uid": sc.initialResult.Result.UID,
},
},
},
},
map[string]interface{}{
"id": int64(5),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 19,
},
"libraryPanel": map[string]interface{}{
"uid": outsidePanel.UID,
},
},
},
}
expected := simplejson.NewFromAny(expectedJSON)
if diff := cmp.Diff(expected.Interface(), dash.Data.Interface(), getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with a library panel without uid, it should fail",
func(t *testing.T, sc scenarioContext) {
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"name": sc.initialResult.Result.Name,
},
"title": "Text - Library Panel",
"type": "text",
},
},
}
dash := models.Dashboard{
Title: "Testing CleanLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err := sc.service.CleanLibraryPanelsForDashboard(dashInDB)
require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error())
})
}
func TestConnectLibraryPanelsForDashboard(t *testing.T) {
scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with a library panel, it should connect the two",
func(t *testing.T, sc scenarioContext) {
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"uid": sc.initialResult.Result.UID,
},
"title": "Text - Library Panel",
"type": "text",
},
},
}
dash := models.Dashboard{
Title: "Testing ConnectLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err := sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB)
require.NoError(t, err)
elements, err := sc.elementService.GetElementsForDashboard(sc.ctx, dashInDB.Id)
require.NoError(t, err)
require.Len(t, elements, 1)
require.Equal(t, sc.initialResult.Result.UID, elements[sc.initialResult.Result.UID].UID)
})
scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with library panels inside and outside of rows, it should connect all",
func(t *testing.T, sc scenarioContext) {
cmd := libraryelements.CreateLibraryElementCommand{
FolderID: sc.initialResult.Result.FolderID,
Name: "Outside row",
Model: []byte(`
{
"datasource": "${DS_GDEV-TESTDATA}",
"id": 1,
"title": "Text - Library Panel",
"type": "text",
"description": "A description"
}
`),
Kind: int64(models.PanelElement),
}
outsidePanel, err := sc.elementService.CreateElement(sc.ctx, sc.user, cmd)
require.NoError(t, err)
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"collapsed": true,
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 6,
},
"id": int64(2),
"type": "row",
"panels": []interface{}{
map[string]interface{}{
"id": int64(3),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 7,
},
},
map[string]interface{}{
"id": int64(4),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 13,
},
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"uid": sc.initialResult.Result.UID,
},
"title": "Inside row",
"type": "text",
},
},
},
map[string]interface{}{
"id": int64(5),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 19,
},
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"uid": outsidePanel.UID,
},
"title": "Outside row",
"type": "text",
},
},
}
dash := models.Dashboard{
Title: "Testing ConnectLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err = sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB)
require.NoError(t, err)
elements, err := sc.elementService.GetElementsForDashboard(sc.ctx, dashInDB.Id)
require.NoError(t, err)
require.Len(t, elements, 2)
require.Equal(t, sc.initialResult.Result.UID, elements[sc.initialResult.Result.UID].UID)
require.Equal(t, outsidePanel.UID, elements[outsidePanel.UID].UID)
})
scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with a library panel without uid, it should fail",
func(t *testing.T, sc scenarioContext) {
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"name": sc.initialResult.Result.Name,
},
"title": "Text - Library Panel",
"type": "text",
},
},
}
dash := models.Dashboard{
Title: "Testing ConnectLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err := sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB)
require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error())
})
scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with unused/removed library panels, it should disconnect unused/removed library panels",
func(t *testing.T, sc scenarioContext) {
unused, err := sc.elementService.CreateElement(sc.ctx, sc.user, libraryelements.CreateLibraryElementCommand{
FolderID: sc.folder.Id,
Name: "Unused Libray Panel",
Model: []byte(`
{
"datasource": "${DS_GDEV-TESTDATA}",
"id": 4,
"title": "Unused Libray Panel",
"type": "text",
"description": "Unused description"
}
`),
Kind: int64(models.PanelElement),
})
require.NoError(t, err)
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": int64(4),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"uid": unused.UID,
},
"title": "Unused Libray Panel",
"description": "Unused description",
},
},
}
dash := models.Dashboard{
Title: "Testing ConnectLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err = sc.elementService.ConnectElementsToDashboard(sc.ctx, sc.user, []string{sc.initialResult.Result.UID}, dashInDB.Id)
require.NoError(t, err)
panelJSON := []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"uid": sc.initialResult.Result.UID,
},
"title": "Text - Library Panel",
"type": "text",
},
}
dashInDB.Data.Set("panels", panelJSON)
err = sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB)
require.NoError(t, err)
elements, err := sc.elementService.GetElementsForDashboard(sc.ctx, dashInDB.Id)
require.NoError(t, err)
require.Len(t, elements, 1)
require.Equal(t, sc.initialResult.Result.UID, elements[sc.initialResult.Result.UID].UID)
})
}
func TestImportLibraryPanelsForDashboard(t *testing.T) {
testScenario(t, "When an admin tries to import a dashboard with a library panel that does not exist, it should import the library panel",
func(t *testing.T, sc scenarioContext) {
var missingUID = "jL6MrxCMz"
var missingName = "Missing Library Panel"
var missingModel = map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": int64(6),
"w": int64(6),
"x": int64(0),
"y": int64(0),
},
"description": "",
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"uid": missingUID,
"name": missingName,
},
"title": "Text - Library Panel",
"type": "text",
}
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
missingModel,
},
}
dash := models.Dashboard{
Title: "Testing ImportLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
_, err := sc.elementService.GetElement(sc.ctx, sc.user, missingUID)
require.EqualError(t, err, libraryelements.ErrLibraryElementNotFound.Error())
err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB, 0)
require.NoError(t, err)
element, err := sc.elementService.GetElement(sc.ctx, sc.user, missingUID)
require.NoError(t, err)
var expected = getExpected(t, element, missingUID, missingName, missingModel)
var result = toLibraryElement(t, element)
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
scenarioWithLibraryPanel(t, "When an admin tries to import a dashboard with a library panel that already exist, it should not import the library panel and existing library panel should be unchanged",
func(t *testing.T, sc scenarioContext) {
var existingUID = sc.initialResult.Result.UID
var existingName = sc.initialResult.Result.Name
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": int64(1),
"description": "Updated description",
"datasource": "Updated datasource",
"libraryPanel": map[string]interface{}{
"uid": sc.initialResult.Result.UID,
"name": sc.initialResult.Result.Name,
},
"title": "Updated Title",
"type": "stat",
},
},
}
dash := models.Dashboard{
Title: "Testing ImportLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
_, err := sc.elementService.GetElement(sc.ctx, sc.user, existingUID)
require.NoError(t, err)
err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB, sc.folder.Id)
require.NoError(t, err)
element, err := sc.elementService.GetElement(sc.ctx, sc.user, existingUID)
require.NoError(t, err)
var expected = getExpected(t, element, existingUID, existingName, sc.initialResult.Result.Model)
expected.FolderID = sc.initialResult.Result.FolderID
expected.Description = sc.initialResult.Result.Description
expected.Meta.FolderUID = sc.folder.Uid
expected.Meta.FolderName = sc.folder.Title
var result = toLibraryElement(t, element)
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
testScenario(t, "When an admin tries to import a dashboard with library panels inside and outside of rows, it should import all that do not exist",
func(t *testing.T, sc scenarioContext) {
var outsideUID = "jL6MrxCMz"
var outsideName = "Outside Library Panel"
var outsideModel = map[string]interface{}{
"id": int64(5),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 19,
},
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"uid": outsideUID,
"name": outsideName,
},
"title": "Outside row",
"type": "text",
}
var insideUID = "iK7NsyDNz"
var insideName = "Inside Library Panel"
var insideModel = map[string]interface{}{
"id": int64(4),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 13,
},
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"uid": insideUID,
"name": insideName,
},
"title": "Inside row",
"type": "text",
}
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"collapsed": true,
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 6,
},
"id": int64(2),
"type": "row",
"panels": []interface{}{
map[string]interface{}{
"id": int64(3),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 7,
},
},
insideModel,
},
},
outsideModel,
},
}
dash := models.Dashboard{
Title: "Testing ImportLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
_, err := sc.elementService.GetElement(sc.ctx, sc.user, outsideUID)
require.EqualError(t, err, libraryelements.ErrLibraryElementNotFound.Error())
_, err = sc.elementService.GetElement(sc.ctx, sc.user, insideUID)
require.EqualError(t, err, libraryelements.ErrLibraryElementNotFound.Error())
err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB, 0)
require.NoError(t, err)
element, err := sc.elementService.GetElement(sc.ctx, sc.user, outsideUID)
require.NoError(t, err)
expected := getExpected(t, element, outsideUID, outsideName, outsideModel)
result := toLibraryElement(t, element)
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
element, err = sc.elementService.GetElement(sc.ctx, sc.user, insideUID)
require.NoError(t, err)
expected = getExpected(t, element, insideUID, insideName, insideModel)
result = toLibraryElement(t, element)
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
}
type libraryPanel struct {
ID int64
OrgID int64
FolderID int64
UID string
Name string
Type string
Description string
Model map[string]interface{}
Version int64
Meta libraryelements.LibraryElementDTOMeta
}
type libraryElementGridPos struct {
H int64 `json:"h"`
W int64 `json:"w"`
X int64 `json:"x"`
Y int64 `json:"y"`
}
type libraryElementLibraryPanel struct {
UID string `json:"uid"`
Name string `json:"name"`
}
type libraryElementModel struct {
ID int64 `json:"id"`
Datasource string `json:"datasource"`
Description string `json:"description"`
Title string `json:"title"`
Type string `json:"type"`
GridPos libraryElementGridPos `json:"gridPos"`
LibraryPanel libraryElementLibraryPanel `json:"libraryPanel"`
}
type libraryElement struct {
ID int64 `json:"id"`
OrgID int64 `json:"orgId"`
FolderID int64 `json:"folderId"`
UID string `json:"uid"`
Name string `json:"name"`
Kind int64 `json:"kind"`
Type string `json:"type"`
Description string `json:"description"`
Model libraryElementModel `json:"model"`
Version int64 `json:"version"`
Meta libraryelements.LibraryElementDTOMeta `json:"meta"`
}
type libraryPanelResult struct {
Result libraryPanel `json:"result"`
}
type scenarioContext struct {
ctx context.Context
service Service
elementService libraryelements.Service
user *models.SignedInUser
folder *models.Folder
initialResult libraryPanelResult
sqlStore *sqlstore.SQLStore
}
type folderACLItem struct {
roleType models.RoleType
permission models.PermissionType
}
func toLibraryElement(t *testing.T, res libraryelements.LibraryElementDTO) libraryElement {
var model = libraryElementModel{}
err := json.Unmarshal(res.Model, &model)
require.NoError(t, err)
return libraryElement{
ID: res.ID,
OrgID: res.OrgID,
FolderID: res.FolderID,
UID: res.UID,
Name: res.Name,
Type: res.Type,
Description: res.Description,
Kind: res.Kind,
Model: model,
Version: res.Version,
Meta: libraryelements.LibraryElementDTOMeta{
FolderName: res.Meta.FolderName,
FolderUID: res.Meta.FolderUID,
ConnectedDashboards: res.Meta.ConnectedDashboards,
Created: res.Meta.Created,
Updated: res.Meta.Updated,
CreatedBy: libraryelements.LibraryElementDTOMetaUser{
ID: res.Meta.CreatedBy.ID,
Name: res.Meta.CreatedBy.Name,
AvatarURL: res.Meta.CreatedBy.AvatarURL,
},
UpdatedBy: libraryelements.LibraryElementDTOMetaUser{
ID: res.Meta.UpdatedBy.ID,
Name: res.Meta.UpdatedBy.Name,
AvatarURL: res.Meta.UpdatedBy.AvatarURL,
},
},
}
}
func getExpected(t *testing.T, res libraryelements.LibraryElementDTO, UID string, name string, model map[string]interface{}) libraryElement {
marshalled, err := json.Marshal(model)
require.NoError(t, err)
var libModel libraryElementModel
err = json.Unmarshal(marshalled, &libModel)
require.NoError(t, err)
return libraryElement{
ID: res.ID,
OrgID: 1,
FolderID: 0,
UID: UID,
Name: name,
Type: "text",
Description: "",
Kind: 1,
Model: libModel,
Version: 1,
Meta: libraryelements.LibraryElementDTOMeta{
FolderName: "General",
FolderUID: "",
ConnectedDashboards: 0,
Created: res.Meta.Created,
Updated: res.Meta.Updated,
CreatedBy: libraryelements.LibraryElementDTOMetaUser{
ID: 1,
Name: userInDbName,
AvatarURL: userInDbAvatar,
},
UpdatedBy: libraryelements.LibraryElementDTOMetaUser{
ID: 1,
Name: userInDbName,
AvatarURL: userInDbAvatar,
},
},
}
}
func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user *models.SignedInUser, dash *models.Dashboard, folderID int64) *models.Dashboard {
dash.FolderId = folderID
dashItem := &dashboards.SaveDashboardDTO{
Dashboard: dash,
Message: "",
OrgId: user.OrgId,
User: user,
Overwrite: false,
}
dashboardStore := database.ProvideDashboardStore(sqlStore)
dashAlertService := alerting.ProvideDashAlertExtractorService(nil, nil, nil)
cfg := setting.NewCfg()
cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled
ac := acmock.New()
service := dashboardservice.ProvideDashboardService(
cfg, dashboardStore, dashAlertService,
featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), acmock.NewMockedPermissionsService(), ac,
)
dashboard, err := service.SaveDashboard(context.Background(), dashItem, true)
require.NoError(t, err)
return dashboard
}
func createFolderWithACL(t *testing.T, sqlStore *sqlstore.SQLStore, title string, user *models.SignedInUser,
items []folderACLItem) *models.Folder {
t.Helper()
ac := acmock.New()
cfg := setting.NewCfg()
cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled
features := featuremgmt.WithFeatures()
folderPermissions := acmock.NewMockedPermissionsService()
dashboardPermissions := acmock.NewMockedPermissionsService()
dashboardStore := database.ProvideDashboardStore(sqlStore)
d := dashboardservice.ProvideDashboardService(cfg, dashboardStore, nil, features, folderPermissions, dashboardPermissions, ac)
s := dashboardservice.ProvideFolderService(cfg, d, dashboardStore, nil, features, folderPermissions, ac)
t.Logf("Creating folder with title and UID %q", title)
folder, err := s.CreateFolder(context.Background(), user, user.OrgId, title, title)
require.NoError(t, err)
updateFolderACL(t, dashboardStore, folder.Id, items)
return folder
}
func updateFolderACL(t *testing.T, dashboardStore *database.DashboardStore, folderID int64, items []folderACLItem) {
t.Helper()
if len(items) == 0 {
return
}
var aclItems []*models.DashboardAcl
for _, item := range items {
role := item.roleType
permission := item.permission
aclItems = append(aclItems, &models.DashboardAcl{
DashboardID: folderID,
Role: &role,
Permission: permission,
Created: time.Now(),
Updated: time.Now(),
})
}
err := dashboardStore.UpdateDashboardACL(context.Background(), folderID, aclItems)
require.NoError(t, err)
}
func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) {
store := mockstore.NewSQLStoreMock()
guardian.InitLegacyGuardian(store, &dashboards.FakeDashboardService{})
t.Helper()
testScenario(t, desc, func(t *testing.T, sc scenarioContext) {
command := libraryelements.CreateLibraryElementCommand{
FolderID: sc.folder.Id,
Name: "Text - Library Panel",
Model: []byte(`
{
"datasource": "${DS_GDEV-TESTDATA}",
"id": 1,
"title": "Text - Library Panel",
"type": "text",
"description": "A description"
}
`),
Kind: int64(models.PanelElement),
}
resp, err := sc.elementService.CreateElement(sc.ctx, sc.user, command)
require.NoError(t, err)
var model map[string]interface{}
err = json.Unmarshal(resp.Model, &model)
require.NoError(t, err)
sc.initialResult = libraryPanelResult{
Result: libraryPanel{
ID: resp.ID,
OrgID: resp.OrgID,
FolderID: resp.FolderID,
UID: resp.UID,
Name: resp.Name,
Type: resp.Type,
Description: resp.Description,
Model: model,
Version: resp.Version,
Meta: resp.Meta,
},
}
fn(t, sc)
})
}
// testScenario is a wrapper around t.Run performing common setup for library panel tests.
// It takes your real test function as a callback.
func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) {
t.Helper()
t.Run(desc, func(t *testing.T) {
cfg := setting.NewCfg()
orgID := int64(1)
role := models.ROLE_ADMIN
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := database.ProvideDashboardStore(sqlStore)
features := featuremgmt.WithFeatures()
ac := acmock.New()
folderPermissions := acmock.NewMockedPermissionsService()
dashboardPermissions := acmock.NewMockedPermissionsService()
dashboardService := dashboardservice.ProvideDashboardService(
cfg, dashboardStore, &alerting.DashAlertExtractorService{},
features, folderPermissions, dashboardPermissions, ac,
)
folderService := dashboardservice.ProvideFolderService(
cfg, dashboardService, dashboardStore, nil,
features, folderPermissions, ac,
)
elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService)
service := LibraryPanelService{
Cfg: cfg,
SQLStore: sqlStore,
LibraryElementService: elementService,
}
user := &models.SignedInUser{
UserId: 1,
Name: "Signed In User",
Login: "signed_in_user",
Email: "signed.in.user@test.com",
OrgId: orgID,
OrgRole: role,
LastSeenAt: time.Now(),
}
// deliberate difference between signed in user and user in db to make it crystal clear
// what to expect in the tests
// In the real world these are identical
cmd := models.CreateUserCommand{
Email: "user.in.db@test.com",
Name: "User In DB",
Login: userInDbName,
}
_, err := sqlStore.CreateUser(context.Background(), cmd)
require.NoError(t, err)
sc := scenarioContext{
user: user,
ctx: context.Background(),
service: &service,
elementService: elementService,
sqlStore: sqlStore,
}
sc.folder = createFolderWithACL(t, sc.sqlStore, "ScenarioFolder", sc.user, []folderACLItem{})
fn(t, sc)
})
}
func getCompareOptions() []cmp.Option {
return []cmp.Option{
cmp.Transformer("Time", func(in time.Time) int64 {
return in.UTC().Unix()
}),
}
}