The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/pkg/services/libraryelements/libraryelements_test.go

398 lines
12 KiB

package libraryelements
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/response"
"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/manager"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
)
const userInDbName = "user_in_db"
const userInDbAvatar = "/avatar/402d08de060496d6b6874495fe20f5ad"
func TestDeleteLibraryPanelsInFolder(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to delete a folder that contains connected library elements, 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{}{
"uid": sc.initialResult.Result.UID,
"name": sc.initialResult.Result.Name,
},
},
},
}
dash := models.Dashboard{
Title: "Testing DeleteLibraryElementsInFolder",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.Id)
require.NoError(t, err)
err = sc.service.DeleteLibraryElementsInFolder(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, sc.folder.Uid)
require.EqualError(t, err, ErrFolderHasConnectedLibraryElements.Error())
})
scenarioWithPanel(t, "When an admin tries to delete a folder uid that doesn't exist, it should fail",
func(t *testing.T, sc scenarioContext) {
err := sc.service.DeleteLibraryElementsInFolder(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, sc.folder.Uid+"xxxx")
require.EqualError(t, err, models.ErrFolderNotFound.Error())
})
scenarioWithPanel(t, "When an admin tries to delete a folder that contains disconnected elements, it should delete all disconnected elements too",
func(t *testing.T, sc scenarioContext) {
command := getCreateVariableCommand(sc.folder.Id, "query0")
sc.reqContext.Req.Body = mockRequestBody(command)
resp := sc.service.createHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
resp = sc.service.getAllHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
var result libraryElementsSearch
err := json.Unmarshal(resp.Body(), &result)
require.NoError(t, err)
require.NotNil(t, result.Result)
require.Equal(t, 2, len(result.Result.Elements))
err = sc.service.DeleteLibraryElementsInFolder(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, sc.folder.Uid)
require.NoError(t, err)
resp = sc.service.getAllHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
err = json.Unmarshal(resp.Body(), &result)
require.NoError(t, err)
require.NotNil(t, result.Result)
require.Equal(t, 0, len(result.Result.Elements))
})
}
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 map[string]interface{} `json:"model"`
Version int64 `json:"version"`
Meta LibraryElementDTOMeta `json:"meta"`
}
type libraryElementResult struct {
Result libraryElement `json:"result"`
}
type libraryElementArrayResult struct {
Result []libraryElement `json:"result"`
}
type libraryElementsSearch struct {
Result libraryElementsSearchResult `json:"result"`
}
type libraryElementsSearchResult struct {
TotalCount int64 `json:"totalCount"`
Elements []libraryElement `json:"elements"`
Page int `json:"page"`
PerPage int `json:"perPage"`
}
func getCreatePanelCommand(folderID int64, name string) CreateLibraryElementCommand {
command := getCreateCommandWithModel(folderID, name, models.PanelElement, []byte(`
{
"datasource": "${DS_GDEV-TESTDATA}",
"id": 1,
"title": "Text - Library Panel",
"type": "text",
"description": "A description"
}
`))
return command
}
func getCreateVariableCommand(folderID int64, name string) CreateLibraryElementCommand {
command := getCreateCommandWithModel(folderID, name, models.VariableElement, []byte(`
{
"datasource": "${DS_GDEV-TESTDATA}",
"name": "query0",
"type": "query",
"description": "A description"
}
`))
return command
}
func getCreateCommandWithModel(folderID int64, name string, kind models.LibraryElementKind, model []byte) CreateLibraryElementCommand {
command := CreateLibraryElementCommand{
FolderID: folderID,
Name: name,
Model: model,
Kind: int64(kind),
}
return command
}
type scenarioContext struct {
ctx *web.Context
service *LibraryElementService
reqContext *models.ReqContext
user models.SignedInUser
folder *models.Folder
initialResult libraryElementResult
sqlStore *sqlstore.SQLStore
}
type folderACLItem struct {
roleType models.RoleType
permission models.PermissionType
}
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)
dashAlertExtractor := alerting.ProvideDashAlertExtractorService(nil, nil, nil)
features := featuremgmt.WithFeatures()
cfg := setting.NewCfg()
cfg.IsFeatureToggleEnabled = features.IsEnabled
folderPermissions := acmock.NewMockedPermissionsService()
dashboardPermissions := acmock.NewMockedPermissionsService()
service := dashboardservice.ProvideDashboardService(
cfg, dashboardStore, dashAlertExtractor,
features, folderPermissions, dashboardPermissions,
)
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()
cfg := setting.NewCfg()
features := featuremgmt.WithFeatures()
cfg.IsFeatureToggleEnabled = features.IsEnabled
folderPermissions := acmock.NewMockedPermissionsService()
dashboardPermissions := acmock.NewMockedPermissionsService()
dashboardStore := database.ProvideDashboardStore(sqlStore)
d := dashboardservice.ProvideDashboardService(
cfg, dashboardStore, nil,
features, folderPermissions, dashboardPermissions,
)
ac := acmock.New()
s := dashboardservice.ProvideFolderService(
cfg, d, dashboardStore, nil,
features, folderPermissions, ac, nil,
)
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 validateAndUnMarshalResponse(t *testing.T, resp response.Response) libraryElementResult {
t.Helper()
require.Equal(t, 200, resp.Status())
var result = libraryElementResult{}
err := json.Unmarshal(resp.Body(), &result)
require.NoError(t, err)
return result
}
func validateAndUnMarshalArrayResponse(t *testing.T, resp response.Response) libraryElementArrayResult {
t.Helper()
require.Equal(t, 200, resp.Status())
var result = libraryElementArrayResult{}
err := json.Unmarshal(resp.Body(), &result)
require.NoError(t, err)
return result
}
func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) {
t.Helper()
store := mockstore.NewSQLStoreMock()
guardian.InitLegacyGuardian(store)
testScenario(t, desc, func(t *testing.T, sc scenarioContext) {
command := getCreatePanelCommand(sc.folder.Id, "Text - Library Panel")
sc.reqContext.Req.Body = mockRequestBody(command)
resp := sc.service.createHandler(sc.reqContext)
sc.initialResult = validateAndUnMarshalResponse(t, resp)
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) {
Security: Sync security changes on main (#45083) * * Teams: Appropriately apply user id filter in /api/teams/:id and /api/teams/search * Teams: Ensure that users searching for teams are only able see teams they have access to * Teams: Require teamGuardian admin privileges to list team members * Teams: Prevent org viewers from administering teams * Teams: Add org_id condition to team count query * Teams: clarify permission requirements in teams api docs * Teams: expand scenarios for team search tests * Teams: mock teamGuardian in tests Co-authored-by: Dan Cech <dcech@grafana.com> * remove duplicate WHERE statement * Fix for CVE-2022-21702 (cherry picked from commit 202d7c190082c094bc1dc13f7fe9464746c37f9e) * Lint and test fixes (cherry picked from commit 3e6b67d5504abf4a1d7b8d621f04d062c048e981) * check content type properly (cherry picked from commit 70b4458892bf2f776302720c10d24c9ff34edd98) * basic csrf origin check (cherry picked from commit 3adaa5ff39832364f6390881fb5b42ad47df92e1) * compare origin to host (cherry picked from commit 5443892699e8ed42836bb2b9a44744ff3e970f42) * simplify url parsing (cherry picked from commit b2ffbc9513fed75468628370a48b929d30af2b1d) * check csrf for GET requests, only compare origin (cherry picked from commit 8b81dc12d8f8a1f07852809c5b4d44f0f0b1d709) * parse content type properly (cherry picked from commit 16f76f4902e6f2188bea9606c68b551af186bdc0) * mentioned get in the comment (cherry picked from commit a7e61811ef8ae558ce721e2e3fed04ce7a5a5345) * add content-type: application/json to test HTTP requests * fix pluginproxy test * Fix linter when comparing errors Co-authored-by: Kevin Minehart <kmineh0151@gmail.com> Co-authored-by: Dan Cech <dcech@grafana.com> Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com> Co-authored-by: Serge Zaitsev <serge.zaitsev@grafana.com> Co-authored-by: Vardan Torosyan <vardants@gmail.com>
3 years ago
ctx := web.Context{Req: &http.Request{
Header: http.Header{
"Content-Type": []string{"application/json"},
},
}}
orgID := int64(1)
role := models.ROLE_ADMIN
sqlStore := sqlstore.InitTestDB(t)
guardian.InitLegacyGuardian(sqlStore)
dashboardStore := database.ProvideDashboardStore(sqlStore)
features := featuremgmt.WithFeatures()
cfg := setting.NewCfg()
cfg.IsFeatureToggleEnabled = features.IsEnabled
folderPermissions := acmock.NewMockedPermissionsService()
dashboardPermissions := acmock.NewMockedPermissionsService()
dashboardService := dashboardservice.ProvideDashboardService(
cfg, dashboardStore, nil,
features, folderPermissions, dashboardPermissions,
)
ac := acmock.New()
LibraryPanels: removes feature toggle (#33839) * WIP: intial structure * Refactor: adds create library element endpoint * Feature: adds delete library element * wip * Refactor: adds get api * Refactor: adds get all api * Refactor: adds patch api * Refactor: changes to library_element_connection * Refactor: add get connections api * wip: in the middle of refactor * wip * Refactor: consolidating both api:s * Refactor: points front end to library elements api * Tests: Fixes broken test * LibraryPanels: removes feature toggle * Fix: fixes delete library elements in folder and adds tests * Tests: fixes snapshot * Refactor: adds service interfaces so they can be easily mocked * Refactor: changes order of tabs in manage folder * Refactor: fixes so link does not cover whole card * Refactor: fixes index string name * Update pkg/services/libraryelements/libraryelements.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/services/libraryelements/libraryelements_permissions_test.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/services/libraryelements/database.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Chore: changes after PR comments * Update libraryelements.go * Update libraryelements.go * Chore: updates after PR comments * Chore: trying to fix build error * Refactor: fixed stupid mistake * Update libraryelements.go * Chore: tries to fix build errors * Refactor: trying to fix MySQL key length * Update libraryelements.go * Update pkg/services/libraryelements/libraryelements.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/services/librarypanels/librarypanels.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Refactor: changes after PR comments * Refactor: changes after PR comments * Tests: fixes tests * Refactor: renames connections to connectedDashboards Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
4 years ago
service := LibraryElementService{
Cfg: cfg,
SQLStore: sqlStore,
folderService: dashboardservice.ProvideFolderService(
cfg, dashboardService, dashboardStore, nil,
features, folderPermissions, ac, nil,
),
LibraryPanels: removes feature toggle (#33839) * WIP: intial structure * Refactor: adds create library element endpoint * Feature: adds delete library element * wip * Refactor: adds get api * Refactor: adds get all api * Refactor: adds patch api * Refactor: changes to library_element_connection * Refactor: add get connections api * wip: in the middle of refactor * wip * Refactor: consolidating both api:s * Refactor: points front end to library elements api * Tests: Fixes broken test * LibraryPanels: removes feature toggle * Fix: fixes delete library elements in folder and adds tests * Tests: fixes snapshot * Refactor: adds service interfaces so they can be easily mocked * Refactor: changes order of tabs in manage folder * Refactor: fixes so link does not cover whole card * Refactor: fixes index string name * Update pkg/services/libraryelements/libraryelements.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/services/libraryelements/libraryelements_permissions_test.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/services/libraryelements/database.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Chore: changes after PR comments * Update libraryelements.go * Update libraryelements.go * Chore: updates after PR comments * Chore: trying to fix build error * Refactor: fixed stupid mistake * Update libraryelements.go * Chore: tries to fix build errors * Refactor: trying to fix MySQL key length * Update libraryelements.go * Update pkg/services/libraryelements/libraryelements.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/services/librarypanels/librarypanels.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Refactor: changes after PR comments * Refactor: changes after PR comments * Tests: fixes tests * Refactor: renames connections to connectedDashboards Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
4 years ago
}
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: &ctx,
service: &service,
sqlStore: sqlStore,
reqContext: &models.ReqContext{
Context: &ctx,
SignedInUser: &user,
},
}
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()
}),
}
}
func mockRequestBody(v interface{}) io.ReadCloser {
b, _ := json.Marshal(v)
return io.NopCloser(bytes.NewReader(b))
}