grafana/pkg/services/dashboards/database/database_test.go

1328 lines
43 KiB
Go

package database
import (
"context"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
libmodel "github.com/grafana/grafana/pkg/services/libraryelements/model"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/search/model"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/grafana/grafana/pkg/util"
)
// run tests with cleanup
func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestIntegrationDashboardDataAccess(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
var sqlStore db.DB
var cfg *setting.Cfg
var savedFolder, savedDash, savedDash2 *dashboards.Dashboard
var dashboardStore dashboards.Store
setup := func() {
sqlStore, cfg = db.InitTestDBWithCfg(t)
var err error
dashboardStore, err = ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
// insertTestDashboard creates the following hierarchy:
// 1 test dash folder
// test dash 23
// test dash 45
// test dash 67
savedFolder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, "", true, "prod", "webapp")
savedDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, savedFolder.ID, savedFolder.UID, false, "prod", "webapp")
insertTestDashboard(t, dashboardStore, "test dash 45", 1, savedFolder.ID, savedFolder.UID, false, "prod")
savedDash2 = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, "", false, "prod")
insertTestDashboard(t, dashboardStore, "test dash org2", 2, 0, "", false, "")
insertTestRule(t, sqlStore, savedFolder.OrgID, savedFolder.UID)
}
t.Run("Should return dashboard model", func(t *testing.T) {
setup()
require.Equal(t, savedDash.Title, "test dash 23")
require.Equal(t, savedDash.Slug, "test-dash-23")
require.NotEqual(t, savedDash.ID, 0)
require.False(t, savedDash.IsFolder)
require.NotEmpty(t, savedDash.FolderUID)
require.Positive(t, len(savedDash.UID))
require.Equal(t, savedFolder.Title, "1 test dash folder")
require.Equal(t, savedFolder.Slug, "1-test-dash-folder")
require.NotEqual(t, savedFolder.ID, 0)
require.True(t, savedFolder.IsFolder)
require.Empty(t, savedFolder.FolderUID)
require.Positive(t, len(savedFolder.UID))
})
t.Run("Should be able to get counts per org", func(t *testing.T) {
setup()
count, err := dashboardStore.CountInOrg(context.Background(), 1, false)
require.NoError(t, err)
require.Equal(t, int64(3), count)
count, err = dashboardStore.CountInOrg(context.Background(), 2, false)
require.NoError(t, err)
require.Equal(t, int64(1), count)
count, err = dashboardStore.CountInOrg(context.Background(), 1, true)
require.NoError(t, err)
require.Equal(t, int64(1), count)
})
t.Run("Should be able to get dashboard by id", func(t *testing.T) {
setup()
query := dashboards.GetDashboardQuery{
ID: savedDash.ID,
OrgID: 1,
}
queryResult, err := dashboardStore.GetDashboard(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, queryResult.Title, "test dash 23")
require.Equal(t, queryResult.Slug, "test-dash-23")
require.Equal(t, queryResult.ID, savedDash.ID)
require.Equal(t, queryResult.UID, savedDash.UID)
require.False(t, queryResult.IsFolder)
})
t.Run("Should not be able to get dashboard by title alone", func(t *testing.T) {
setup()
query := dashboards.GetDashboardQuery{
Title: util.Pointer("test dash 23"),
OrgID: 1,
}
_, err := dashboardStore.GetDashboard(context.Background(), &query)
require.ErrorIs(t, err, dashboards.ErrDashboardIdentifierNotSet)
})
t.Run("Should be able to get root dashboard by title", func(t *testing.T) {
setup()
query := dashboards.GetDashboardQuery{
Title: util.Pointer("test dash 67"),
FolderUID: util.Pointer(""),
OrgID: 1,
}
_, err := dashboardStore.GetDashboard(context.Background(), &query)
require.Error(t, err)
})
t.Run("Should be able to get dashboard by title and folderID", func(t *testing.T) {
setup()
query := dashboards.GetDashboardQuery{
Title: util.Pointer("test dash 23"),
FolderID: &savedDash.ID,
OrgID: 1,
}
_, err := dashboardStore.GetDashboard(context.Background(), &query)
require.Error(t, err)
})
t.Run("Should be able to get dashboard by title and folderUID", func(t *testing.T) {
setup()
query := dashboards.GetDashboardQuery{
Title: util.Pointer("test dash 23"),
FolderUID: util.Pointer(savedFolder.UID),
OrgID: 1,
}
queryResult, err := dashboardStore.GetDashboard(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, queryResult.Title, "test dash 23")
require.Equal(t, queryResult.Slug, "test-dash-23")
require.Equal(t, queryResult.ID, savedDash.ID)
require.Equal(t, queryResult.UID, savedDash.UID)
require.False(t, queryResult.IsFolder)
})
t.Run("Should be able to get dashboard by uid", func(t *testing.T) {
setup()
query := dashboards.GetDashboardQuery{
UID: savedDash.UID,
OrgID: 1,
}
queryResult, err := dashboardStore.GetDashboard(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, queryResult.Title, "test dash 23")
require.Equal(t, queryResult.Slug, "test-dash-23")
require.Equal(t, queryResult.ID, savedDash.ID)
require.Equal(t, queryResult.UID, savedDash.UID)
require.False(t, queryResult.IsFolder)
})
t.Run("Shouldn't be able to get a dashboard with just an OrgID", func(t *testing.T) {
setup()
query := dashboards.GetDashboardQuery{
OrgID: 1,
}
_, err := dashboardStore.GetDashboard(context.Background(), &query)
require.Equal(t, err, dashboards.ErrDashboardIdentifierNotSet)
})
t.Run("Folder=0 should not be able to get a dashboard in a folder", func(t *testing.T) {
setup()
query := dashboards.GetDashboardQuery{
Title: util.Pointer("test dash 23"),
FolderUID: util.Pointer(""),
OrgID: 1,
}
_, err := dashboardStore.GetDashboard(context.Background(), &query)
require.Error(t, err, dashboards.ErrDashboardNotFound)
})
t.Run("Should be able to delete dashboard and associated tags", func(t *testing.T) {
setup()
dash := insertTestDashboard(t, dashboardStore, "delete me", 1, 0, "", false, "delete this")
tags, err := dashboardStore.GetDashboardTags(context.Background(), &dashboards.GetDashboardTagsQuery{OrgID: 1})
require.NoError(t, err)
terms := make([]string, len(tags))
for i, tag := range tags {
terms[i] = tag.Term
}
require.Contains(t, terms, "delete this")
err = dashboardStore.DeleteDashboard(context.Background(), &dashboards.DeleteDashboardCommand{
ID: dash.ID,
OrgID: 1,
})
require.NoError(t, err)
tags, err = dashboardStore.GetDashboardTags(context.Background(), &dashboards.GetDashboardTagsQuery{OrgID: 1})
require.NoError(t, err)
terms = make([]string, len(tags))
for i, tag := range tags {
terms[i] = tag.Term
}
require.NotContains(t, terms, "delete this")
})
t.Run("Should delete associated provisioning info, even without the dashboard existing in the db", func(t *testing.T) {
setup()
provisioningData := &dashboards.DashboardProvisioning{
ID: 1,
Name: "test",
CheckSum: "123",
Updated: 54321,
ExternalID: "/path/to/dashboard",
}
dash1, err := dashboardStore.SaveProvisionedDashboard(context.Background(), dashboards.SaveDashboardCommand{
OrgID: 1,
IsFolder: false,
Dashboard: simplejson.NewFromAny(map[string]any{
"id": nil,
"title": "provisioned",
}),
}, provisioningData)
require.NoError(t, err)
provisioningData2 := &dashboards.DashboardProvisioning{
ID: 1,
Name: "orphaned",
CheckSum: "123",
Updated: 54321,
ExternalID: "/path/to/dashboard",
}
_, err = dashboardStore.SaveProvisionedDashboard(context.Background(), dashboards.SaveDashboardCommand{
OrgID: 1,
IsFolder: false,
Dashboard: simplejson.NewFromAny(map[string]any{
"id": nil,
"title": "orphaned",
}),
}, provisioningData2)
require.NoError(t, err)
// get provisioning data
res, err := dashboardStore.GetProvisionedDashboardData(context.Background(), "test")
require.NoError(t, err)
require.Len(t, res, 1)
require.Equal(t, res[0], provisioningData)
// get dashboards within the provisioner
provisionedDashes, err := dashboardStore.GetProvisionedDashboardsByName(context.Background(), "test", 1)
require.NoError(t, err)
require.Len(t, provisionedDashes, 1)
provisionedDashes, err = dashboardStore.GetProvisionedDashboardsByName(context.Background(), "test", 2)
require.NoError(t, err)
require.Len(t, provisionedDashes, 0)
// find dashboards not within that provisioner
dashs, err := dashboardStore.GetOrphanedProvisionedDashboards(context.Background(), []string{"test"}, 1)
require.NoError(t, err)
require.Len(t, dashs, 1)
dashs, err = dashboardStore.GetOrphanedProvisionedDashboards(context.Background(), []string{"test"}, 2)
require.NoError(t, err)
require.Len(t, dashs, 0)
// if both are provided, nothing should be returned
dashs, err = dashboardStore.GetOrphanedProvisionedDashboards(context.Background(), []string{"test", "orphaned"}, 1)
require.NoError(t, err)
require.Len(t, dashs, 0)
err = dashboardStore.CleanupAfterDelete(context.Background(), &dashboards.DeleteDashboardCommand{
ID: dash1.ID,
OrgID: 1,
UID: "test",
})
require.NoError(t, err)
res, err = dashboardStore.GetProvisionedDashboardData(context.Background(), "test")
require.NoError(t, err)
require.Len(t, res, 0)
})
t.Run("Should be able to get all dashboards for an org", func(t *testing.T) {
setup()
dash1 := insertTestDashboard(t, dashboardStore, "org3test1", 3, 0, "", false, "org 1 test 1")
dash2 := insertTestDashboard(t, dashboardStore, "org3test2", 3, 0, "", false, "org 1 test 2")
dash3 := insertTestDashboard(t, dashboardStore, "org4test1", 4, 0, "", false, "org 2 test 1")
dashs, err := dashboardStore.GetAllDashboardsByOrgId(context.Background(), 3)
require.NoError(t, err)
require.Equal(t, len(dashs), 2)
uids := []string{}
for _, d := range dashs {
uids = append(uids, d.UID)
}
require.Contains(t, uids, dash1.UID)
require.Contains(t, uids, dash2.UID)
dashs, err = dashboardStore.GetAllDashboardsByOrgId(context.Background(), 4)
require.NoError(t, err)
require.Equal(t, len(dashs), 1)
require.Equal(t, dash3.UID, dashs[0].UID)
dashs, err = dashboardStore.GetAllDashboardsByOrgId(context.Background(), 5)
require.NoError(t, err)
require.Equal(t, len(dashs), 0)
})
t.Run("Should be able to create dashboard", func(t *testing.T) {
setup()
cmd := dashboards.SaveDashboardCommand{
OrgID: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "folderId",
"tags": []interface{}{},
}),
UserID: 100,
}
dashboard, err := dashboardStore.SaveDashboard(context.Background(), cmd)
require.NoError(t, err)
require.EqualValues(t, dashboard.CreatedBy, 100)
require.False(t, dashboard.Created.IsZero())
require.EqualValues(t, dashboard.UpdatedBy, 100)
require.False(t, dashboard.Updated.IsZero())
})
t.Run("Should populate FolderID if FolderUID is provided when creating a dashboard", func(t *testing.T) {
setup()
cmd := dashboards.SaveDashboardCommand{
OrgID: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "folderId",
"tags": []interface{}{},
}),
FolderUID: savedFolder.UID,
UserID: 100,
}
dashboard, err := dashboardStore.SaveDashboard(context.Background(), cmd)
require.NoError(t, err)
require.EqualValues(t, dashboard.FolderID, savedFolder.ID) //nolint:staticcheck
require.EqualValues(t, dashboard.FolderUID, savedFolder.UID)
})
t.Run("Should be able to update dashboard by id and remove folderId", func(t *testing.T) {
setup()
cmd := dashboards.SaveDashboardCommand{
OrgID: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDash.ID,
"title": "folderId",
"tags": []interface{}{},
}),
Overwrite: true,
FolderUID: savedFolder.UID,
UserID: 100,
}
dash, err := dashboardStore.SaveDashboard(context.Background(), cmd)
require.NoError(t, err)
require.EqualValues(t, dash.FolderUID, savedFolder.UID)
cmd = dashboards.SaveDashboardCommand{
OrgID: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDash.ID,
"title": "folderId",
"tags": []interface{}{},
}),
FolderUID: "",
Overwrite: true,
UserID: 100,
}
_, err = dashboardStore.SaveDashboard(context.Background(), cmd)
require.NoError(t, err)
query := dashboards.GetDashboardQuery{
ID: savedDash.ID,
OrgID: 1,
}
queryResult, err := dashboardStore.GetDashboard(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, queryResult.FolderUID, "")
require.Equal(t, queryResult.CreatedBy, savedDash.CreatedBy)
require.WithinDuration(t, queryResult.Created, savedDash.Created, 3*time.Second)
require.Equal(t, queryResult.UpdatedBy, int64(100))
require.False(t, queryResult.Updated.IsZero())
})
t.Run("Should be able to delete empty folder", func(t *testing.T) {
setup()
emptyFolder := insertTestDashboard(t, dashboardStore, "2 test dash folder", 1, 0, "", true, "prod", "webapp")
deleteCmd := &dashboards.DeleteDashboardCommand{ID: emptyFolder.ID}
err := dashboardStore.DeleteDashboard(context.Background(), deleteCmd)
require.NoError(t, err)
})
t.Run("Should return error if no dashboard is found for update when dashboard id is greater than zero", func(t *testing.T) {
cmd := dashboards.SaveDashboardCommand{
OrgID: 1,
Overwrite: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": float64(123412321),
"title": "Expect error",
"tags": []interface{}{},
}),
}
_, err := dashboardStore.SaveDashboard(context.Background(), cmd)
require.Equal(t, err, dashboards.ErrDashboardNotFound)
})
t.Run("Should not return error if no dashboard is found for update when dashboard id is zero", func(t *testing.T) {
cmd := dashboards.SaveDashboardCommand{
OrgID: 1,
Overwrite: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": 0,
"title": "New dash",
"tags": []interface{}{},
}),
}
_, err := dashboardStore.SaveDashboard(context.Background(), cmd)
require.NoError(t, err)
})
t.Run("Should be able to get dashboard tags", func(t *testing.T) {
setup()
query := dashboards.GetDashboardTagsQuery{OrgID: 1}
queryResult, err := dashboardStore.GetDashboardTags(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, len(queryResult), 2)
})
t.Run("Should be able to find dashboard folder", func(t *testing.T) {
setup()
query := dashboards.FindPersistedDashboardsQuery{
Title: "1 test dash folder",
OrgId: 1,
SignedInUser: &user.SignedInUser{
OrgID: 1,
OrgRole: org.RoleEditor,
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionFoldersRead: []string{dashboards.ScopeFoldersAll}},
},
},
}
hits, err := testSearchDashboards(dashboardStore, &query)
require.NoError(t, err)
require.Equal(t, len(hits), 1)
hit := hits[0]
require.Equal(t, hit.Type, model.DashHitFolder)
require.Equal(t, hit.URL, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.UID, savedFolder.Slug))
require.Equal(t, hit.FolderTitle, "")
})
t.Run("Should be able to limit find results", func(t *testing.T) {
setup()
query := dashboards.FindPersistedDashboardsQuery{
OrgId: 1,
Limit: 1,
SignedInUser: &user.SignedInUser{
OrgID: 1,
OrgRole: org.RoleEditor,
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionFoldersRead: []string{dashboards.ScopeFoldersAll}},
},
},
}
hits, err := testSearchDashboards(dashboardStore, &query)
require.NoError(t, err)
require.Equal(t, len(hits), 1)
require.EqualValues(t, hits[0].Title, "1 test dash folder")
})
t.Run("Should be able to find results beyond limit using paging", func(t *testing.T) {
setup()
query := dashboards.FindPersistedDashboardsQuery{
OrgId: 1,
Limit: 1,
Page: 2,
SignedInUser: &user.SignedInUser{
OrgID: 1,
OrgRole: org.RoleEditor,
Permissions: map[int64]map[string][]string{
1: {
dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll},
dashboards.ActionFoldersRead: []string{dashboards.ScopeFoldersAll},
},
},
},
}
hits, err := testSearchDashboards(dashboardStore, &query)
require.NoError(t, err)
require.Equal(t, len(hits), 1)
require.EqualValues(t, hits[0].Title, "test dash 23")
})
t.Run("Should be able to filter by tag and type", func(t *testing.T) {
setup()
query := dashboards.FindPersistedDashboardsQuery{
OrgId: 1,
Type: "dash-db",
Tags: []string{"prod"},
SignedInUser: &user.SignedInUser{
OrgID: 1,
OrgRole: org.RoleEditor,
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll}},
},
},
}
hits, err := testSearchDashboards(dashboardStore, &query)
require.NoError(t, err)
require.Equal(t, len(hits), 3)
require.Equal(t, hits[0].Title, "test dash 23")
})
t.Run("Should be able to find a dashboard folder's children", func(t *testing.T) {
setup()
query := dashboards.FindPersistedDashboardsQuery{
OrgId: 1,
FolderUIDs: []string{savedFolder.UID},
SignedInUser: &user.SignedInUser{
OrgID: 1,
OrgRole: org.RoleEditor,
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll}},
},
},
}
hits, err := testSearchDashboards(dashboardStore, &query)
require.NoError(t, err)
require.Equal(t, len(hits), 2)
hit := hits[0]
require.Equal(t, hit.ID, savedDash.ID)
require.Equal(t, hit.URL, fmt.Sprintf("/d/%s/%s", savedDash.UID, savedDash.Slug))
require.Equal(t, hit.FolderUID, savedFolder.UID)
require.Equal(t, hit.FolderTitle, savedFolder.Title)
require.Equal(t, hit.FolderURL, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.UID, savedFolder.Slug))
})
t.Run("Should be able to find a dashboard folder's children by UID", func(t *testing.T) {
setup()
query := dashboards.FindPersistedDashboardsQuery{
OrgId: 1,
FolderUIDs: []string{savedFolder.UID},
SignedInUser: &user.SignedInUser{
OrgID: 1,
OrgRole: org.RoleEditor,
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll}},
},
},
}
hits, err := testSearchDashboards(dashboardStore, &query)
require.NoError(t, err)
require.Equal(t, len(hits), 2)
hit := hits[0]
require.Equal(t, hit.ID, savedDash.ID)
require.Equal(t, hit.URL, fmt.Sprintf("/d/%s/%s", savedDash.UID, savedDash.Slug))
require.Equal(t, hit.FolderUID, savedFolder.UID)
require.Equal(t, hit.FolderTitle, savedFolder.Title)
require.Equal(t, hit.FolderURL, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.UID, savedFolder.Slug))
})
t.Run("Should be able to find dashboards by ids", func(t *testing.T) {
setup()
query := dashboards.FindPersistedDashboardsQuery{
DashboardIds: []int64{savedDash.ID, savedDash2.ID},
SignedInUser: &user.SignedInUser{
OrgID: 1,
OrgRole: org.RoleEditor,
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll}},
},
},
}
hits, err := testSearchDashboards(dashboardStore, &query)
require.NoError(t, err)
require.Equal(t, len(hits), 2)
hit := hits[0]
require.Equal(t, len(hit.Tags), 2)
hit2 := hits[1]
require.Equal(t, len(hit2.Tags), 1)
})
t.Run("Can delete dashboards in folder", func(t *testing.T) {
setup()
folder := insertTestDashboard(t, dashboardStore, "dash folder", 1, 0, "", true, "prod", "webapp")
_ = insertTestDashboard(t, dashboardStore, "delete me 1", 1, folder.ID, folder.UID, false, "delete this 1")
_ = insertTestDashboard(t, dashboardStore, "delete me 2", 1, folder.ID, folder.UID, false, "delete this 2")
err := dashboardStore.DeleteDashboardsInFolders(context.Background(), &dashboards.DeleteDashboardsInFolderRequest{OrgID: folder.OrgID, FolderUIDs: []string{folder.UID}})
require.NoError(t, err)
})
}
func TestIntegrationDashboardDataAccessGivenPluginWithImportedDashboards(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
sqlStore := db.InitTestDB(t)
dashboardStore, err := ProvideDashboardStore(sqlStore, &setting.Cfg{}, testFeatureToggles, tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
pluginId := "test-app"
insertTestDashboardForPlugin(t, dashboardStore, "app-test", 1, "", true, pluginId)
insertTestDashboardForPlugin(t, dashboardStore, "app-test", 1, "", true, pluginId)
insertTestDashboardForPlugin(t, dashboardStore, "app-dash1", 1, "", false, pluginId)
insertTestDashboardForPlugin(t, dashboardStore, "app-dash2", 1, "", false, pluginId)
query := dashboards.GetDashboardsByPluginIDQuery{
PluginID: pluginId,
OrgID: 1,
}
queryResult, err := dashboardStore.GetDashboardsByPluginID(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, len(queryResult), 2)
}
func TestIntegrationDashboard_SortingOptions(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
sqlStore := db.InitTestDB(t)
dashboardStore, err := ProvideDashboardStore(sqlStore, &setting.Cfg{}, testFeatureToggles, tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
dashB := insertTestDashboard(t, dashboardStore, "Beta", 1, 0, "", false)
dashA := insertTestDashboard(t, dashboardStore, "Alfa", 1, 0, "", false)
assert.NotZero(t, dashA.ID)
assert.Less(t, dashB.ID, dashA.ID)
qNoSort := &dashboards.FindPersistedDashboardsQuery{
SignedInUser: &user.SignedInUser{
OrgID: 1,
UserID: 1,
OrgRole: org.RoleAdmin,
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll}},
},
},
}
results, err := dashboardStore.FindDashboards(context.Background(), qNoSort)
require.NoError(t, err)
require.Len(t, results, 2)
assert.Equal(t, dashA.ID, results[0].ID)
assert.Equal(t, dashB.ID, results[1].ID)
qSort := &dashboards.FindPersistedDashboardsQuery{
SignedInUser: &user.SignedInUser{
OrgID: 1,
UserID: 1,
OrgRole: org.RoleAdmin,
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll}},
},
},
Sort: model.SortOption{
Filter: []model.SortOptionFilter{
searchstore.TitleSorter{Descending: true},
},
},
}
results, err = dashboardStore.FindDashboards(context.Background(), qSort)
require.NoError(t, err)
require.Len(t, results, 2)
assert.Equal(t, dashB.ID, results[0].ID)
assert.Equal(t, dashA.ID, results[1].ID)
}
func TestIntegrationDashboard_Filter(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
sqlStore := db.InitTestDB(t)
cfg := setting.NewCfg()
dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
insertTestDashboard(t, dashboardStore, "Alfa", 1, 0, "", false)
dashB := insertTestDashboard(t, dashboardStore, "Beta", 1, 0, "", false)
qNoFilter := &dashboards.FindPersistedDashboardsQuery{
SignedInUser: &user.SignedInUser{
OrgID: 1,
UserID: 1,
OrgRole: org.RoleAdmin,
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll}},
},
},
}
results, err := dashboardStore.FindDashboards(context.Background(), qNoFilter)
require.NoError(t, err)
require.Len(t, results, 2)
qFilter := &dashboards.FindPersistedDashboardsQuery{
SignedInUser: &user.SignedInUser{
OrgID: 1,
UserID: 1,
OrgRole: org.RoleAdmin,
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll}},
},
},
Filters: []interface{}{
searchstore.TitleFilter{
Dialect: sqlStore.GetDialect(),
Title: "Beta",
},
},
}
results, err = dashboardStore.FindDashboards(context.Background(), qFilter)
require.NoError(t, err)
require.Len(t, results, 1)
assert.Equal(t, dashB.ID, results[0].ID)
}
// has to be added to both the folder & the dashboard table
func insertTestFolder(t *testing.T, dashboardStore dashboards.Store, sqlStore db.DB, title string, orgId int64, parentUID string, tags ...interface{}) *dashboards.Dashboard {
t.Helper()
cmd := dashboards.SaveDashboardCommand{
OrgID: orgId,
FolderUID: parentUID,
IsFolder: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": title,
"tags": tags,
}),
}
dash, err := dashboardStore.SaveDashboard(context.Background(), cmd)
require.NoError(t, err)
require.NotNil(t, dash)
dash.Data.Set("id", dash.ID)
dash.Data.Set("uid", dash.UID)
err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
type folder struct {
ID int64 `xorm:"pk autoincr 'id'"`
OrgID int64 `xorm:"org_id"`
UID string `xorm:"uid"`
ParentUID *string `xorm:"parent_uid"`
Title string `xorm:"title"`
Description string `xorm:"description"`
Created time.Time `xorm:"created"`
Updated time.Time `xorm:"updated"`
}
f := &folder{
OrgID: orgId,
UID: dash.UID,
Title: title,
Description: "",
Created: time.Now(),
Updated: time.Now(),
}
if parentUID != "" {
f.ParentUID = &parentUID
}
_, err := sess.Insert(f)
return err
})
require.NoError(t, err)
return dash
}
func TestIntegrationFindDashboardsByTitle(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
sqlStore := db.InitTestDB(t)
cfg := setting.NewCfg()
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPanelTitleSearch)
dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
orgID := int64(1)
insertTestDashboard(t, dashboardStore, "dashboard under general", orgID, 0, "", false, []string{"tag1", "tag2"})
user := &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
orgID: {
dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll},
dashboards.ActionFoldersRead: []string{dashboards.ScopeFoldersAll},
dashboards.ActionFoldersWrite: []string{dashboards.ScopeFoldersAll},
dashboards.ActionFoldersCreate: []string{dashboards.ScopeFoldersAll},
},
},
}
f0 := insertTestFolder(t, dashboardStore, sqlStore, "f0", orgID, "")
insertTestDashboard(t, dashboardStore, "dashboard under f0", orgID, 0, f0.UID, false, []string{"tag3"})
subfolder := insertTestFolder(t, dashboardStore, sqlStore, "subfolder", orgID, f0.UID)
insertTestDashboard(t, dashboardStore, "dashboard under subfolder", orgID, 0, subfolder.UID, false)
type res struct {
title string
folderUID string
folderTitle string
}
testCases := []struct {
desc string
title string
titleExactMatch bool
expectedResult *res
typ string
}{
{
desc: "find dashboard under general",
title: "dashboard under general",
expectedResult: &res{title: "dashboard under general"},
},
{
desc: "find dashboard under f0",
title: "dashboard under f0",
expectedResult: &res{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
},
{
desc: "find dashboard under subfolder",
title: "dashboard under subfolder",
expectedResult: &res{title: "dashboard under subfolder", folderUID: subfolder.UID, folderTitle: subfolder.Title},
},
{
desc: "find folder 'sub' using partial match: 1 result found",
title: "sub",
titleExactMatch: false,
typ: "dash-folder",
expectedResult: &res{title: "subfolder", folderUID: f0.UID, folderTitle: f0.Title},
},
{
desc: "find folder 'sub' using exact match: no results",
title: "sub",
titleExactMatch: true,
typ: "dash-folder",
expectedResult: nil,
},
{
desc: "find folder 'subfolder' using exact match: 1 result",
title: "subfolder",
titleExactMatch: true,
typ: "dash-folder",
expectedResult: &res{title: "subfolder", folderUID: f0.UID, folderTitle: f0.Title},
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
res, err := dashboardStore.FindDashboards(context.Background(), &dashboards.FindPersistedDashboardsQuery{
SignedInUser: user,
Type: tc.typ,
Title: tc.title,
TitleExactMatch: tc.titleExactMatch,
})
require.NoError(t, err)
if tc.expectedResult == nil {
require.Equal(t, 0, len(res))
return
}
require.Equal(t, 1, len(res))
r := tc.expectedResult
assert.Equal(t, r.title, res[0].Title)
if r.folderUID != "" {
assert.Equal(t, r.folderUID, res[0].FolderUID)
} else {
assert.Empty(t, res[0].FolderUID)
}
if r.folderTitle != "" {
assert.Equal(t, r.folderTitle, res[0].FolderTitle)
} else {
assert.Empty(t, res[0].FolderTitle)
}
})
}
}
func TestIntegrationFindDashboardsByFolder(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
sqlStore, cfg := db.InitTestDBWithCfg(t)
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPanelTitleSearch)
dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
orgID := int64(1)
insertTestDashboard(t, dashboardStore, "dashboard under general", orgID, 0, "", false)
user := &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
orgID: {
dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll},
dashboards.ActionFoldersRead: []string{dashboards.ScopeFoldersAll},
dashboards.ActionFoldersWrite: []string{dashboards.ScopeFoldersAll},
dashboards.ActionFoldersCreate: []string{dashboards.ScopeFoldersAll},
},
},
}
f0 := insertTestFolder(t, dashboardStore, sqlStore, "f0", orgID, "")
insertTestDashboard(t, dashboardStore, "dashboard under f0", orgID, 0, f0.UID, false)
f1 := insertTestFolder(t, dashboardStore, sqlStore, "f1", orgID, "")
insertTestDashboard(t, dashboardStore, "dashboard under f1", orgID, 0, f1.UID, false)
subfolder := insertTestFolder(t, dashboardStore, sqlStore, "subfolder", orgID, f0.UID)
type res struct {
title string
folderUID string
folderTitle string
}
testCases := []struct {
desc string
// Deprecated: use FolderUID instead
folderIDs []int64
folderUIDs []string
query string
expectedResult map[string][]res
typ string
}{
{
desc: "find dashboard under general using folder UID",
folderUIDs: []string{folder.GeneralFolderUID},
typ: searchstore.TypeDashboard,
expectedResult: map[string][]res{
"": {{title: "dashboard under general"}},
featuremgmt.FlagNestedFolders: {{title: "dashboard under general"}},
},
},
{
desc: "find dashboard under general using folder UID",
folderUIDs: []string{folder.GeneralFolderUID},
typ: searchstore.TypeDashboard,
expectedResult: map[string][]res{
"": {{title: "dashboard under general"}},
featuremgmt.FlagNestedFolders: {{title: "dashboard under general"}},
},
},
{
desc: "find dashboard under f0 using folder UID",
folderUIDs: []string{f0.UID},
typ: searchstore.TypeDashboard,
expectedResult: map[string][]res{
"": {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}},
featuremgmt.FlagNestedFolders: {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}},
},
},
{
desc: "find dashboard under f0 or f1 using folder UID",
folderUIDs: []string{f0.UID, f1.UID},
typ: searchstore.TypeDashboard,
expectedResult: map[string][]res{
"": {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
{title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title}},
featuremgmt.FlagNestedFolders: {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
{title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title}},
},
},
{
desc: "find dashboard under general or f0 using folder UID",
folderUIDs: []string{folder.GeneralFolderUID, f0.UID},
typ: searchstore.TypeDashboard,
expectedResult: map[string][]res{
"": {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
{title: "dashboard under general"}},
featuremgmt.FlagNestedFolders: {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
{title: "dashboard under general"}},
},
},
{
desc: "find dashboard under general or f0 or f1 using folder UID",
folderUIDs: []string{folder.GeneralFolderUID, f0.UID, f1.UID},
typ: searchstore.TypeDashboard,
expectedResult: map[string][]res{
"": {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
{title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title},
{title: "dashboard under general"}},
featuremgmt.FlagNestedFolders: {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
{title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title},
{title: "dashboard under general"}},
},
},
{
desc: "find subfolder",
folderUIDs: []string{f0.UID},
typ: searchstore.TypeFolder,
expectedResult: map[string][]res{
"": {},
featuremgmt.FlagNestedFolders: {{title: subfolder.Title, folderUID: f0.UID, folderTitle: f0.Title}},
},
},
}
for _, tc := range testCases {
for featureFlags := range tc.expectedResult {
t.Run(fmt.Sprintf("%s with featureFlags: %v", tc.desc, featureFlags), func(t *testing.T) {
dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(featureFlags), tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
res, err := dashboardStore.FindDashboards(context.Background(), &dashboards.FindPersistedDashboardsQuery{
SignedInUser: user,
Type: tc.typ,
FolderUIDs: tc.folderUIDs,
})
require.NoError(t, err)
require.Equal(t, len(tc.expectedResult[featureFlags]), len(res))
for i, r := range tc.expectedResult[featureFlags] {
assert.Equal(t, r.title, res[i].Title)
if r.folderUID != "" {
assert.Equal(t, r.folderUID, res[i].FolderUID)
} else {
assert.Empty(t, res[i].FolderUID)
}
if r.folderTitle != "" {
assert.Equal(t, r.folderTitle, res[i].FolderTitle)
} else {
assert.Empty(t, res[i].FolderTitle)
}
}
})
}
}
}
func TestIntegrationGetDashboardsByLibraryPanelUID(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
sqlStore, cfg := db.InitTestDBWithCfg(t)
dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
t.Run("Should return dashboards connected to a library panel", func(t *testing.T) {
libraryElement := insertTestLibraryElement(t, sqlStore, "test-library-panel", 1, "", "Test Library Panel")
dash1 := insertTestDashboard(t, dashboardStore, "Dashboard 1", 1, 0, "", false, "prod")
dash2 := insertTestDashboard(t, dashboardStore, "Dashboard 2", 1, 0, "", false, "webapp")
dash3 := insertTestDashboard(t, dashboardStore, "Dashboard 3", 1, 0, "", false, "backend")
// connect all but the last one
insertTestLibraryElementConnection(t, sqlStore, libraryElement.ID, dash1.ID)
insertTestLibraryElementConnection(t, sqlStore, libraryElement.ID, dash2.ID)
connectedDashboards, err := dashboardStore.GetDashboardsByLibraryPanelUID(context.Background(), libraryElement.UID, 1)
require.NoError(t, err)
require.Len(t, connectedDashboards, 2)
uids := []string{connectedDashboards[0].UID, connectedDashboards[1].UID}
require.Contains(t, uids, dash1.UID)
require.Contains(t, uids, dash2.UID)
require.NotContains(t, uids, dash3.UID)
})
t.Run("Returns nothing when library panel has no connections", func(t *testing.T) {
libraryElement := insertTestLibraryElement(t, sqlStore, "empty-library-panel", 1, "", "Empty Library Panel")
connectedDashboards, err := dashboardStore.GetDashboardsByLibraryPanelUID(context.Background(), libraryElement.UID, 1)
require.NoError(t, err)
require.Len(t, connectedDashboards, 0)
})
t.Run("Returns nothing when library panel does not exist", func(t *testing.T) {
connectedDashboards, err := dashboardStore.GetDashboardsByLibraryPanelUID(context.Background(), "non-existent-uid", 1)
require.NoError(t, err)
require.Len(t, connectedDashboards, 0)
})
}
func insertTestLibraryElement(t *testing.T, sqlStore db.DB, uid string, orgID int64, folderUID string, name string) *libmodel.LibraryElement {
t.Helper()
element := &libmodel.LibraryElement{
OrgID: orgID,
FolderUID: folderUID,
UID: uid,
Name: name,
Kind: 1,
Type: "text",
Description: "Test library element",
Model: []byte(`{"type": "text", "content": "test"}`),
Version: 1,
Created: time.Now(),
Updated: time.Now(),
CreatedBy: 1,
UpdatedBy: 1,
}
err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
_, err := sess.Insert(element)
return err
})
require.NoError(t, err)
return element
}
func insertTestLibraryElementConnection(t *testing.T, sqlStore db.DB, elementID int64, dashboardID int64) {
t.Helper()
connection := &libmodel.LibraryElementConnection{
ElementID: elementID,
Kind: 1,
ConnectionID: dashboardID,
Created: time.Now(),
CreatedBy: 1,
}
err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
_, err := sess.Insert(connection)
return err
})
require.NoError(t, err)
}
func insertTestRule(t *testing.T, sqlStore db.DB, foderOrgID int64, folderUID string) {
err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
type alertQuery struct {
RefID string
DatasourceUID string
Model json.RawMessage
}
type alertRule struct {
ID int64 `xorm:"pk autoincr 'id'"`
OrgID int64 `xorm:"org_id"`
Title string
Updated time.Time
UID string `xorm:"uid"`
NamespaceUID string `xorm:"namespace_uid"`
RuleGroup string
Condition string
Data []alertQuery
}
rule := alertRule{
OrgID: foderOrgID,
NamespaceUID: folderUID,
UID: "rule",
RuleGroup: "rulegroup",
Updated: time.Now(),
Condition: "A",
Data: []alertQuery{
{
RefID: "A",
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(`{
"type": "math",
"expression": "2 + 3 > 1"
}`),
},
},
}
_, err := sess.Insert(&rule)
require.NoError(t, err)
type alertRuleVersion struct {
ID int64 `xorm:"pk autoincr 'id'"`
RuleOrgID int64 `xorm:"rule_org_id"`
RuleUID string `xorm:"rule_uid"`
RuleNamespaceUID string `xorm:"rule_namespace_uid"`
RuleGroup string
ParentVersion int64
RestoredFrom int64
Version int64
Created time.Time
Title string
Condition string
Data []alertQuery
IntervalSeconds int64
}
ruleVersion := alertRuleVersion{
RuleOrgID: rule.OrgID,
RuleUID: rule.UID,
RuleNamespaceUID: rule.NamespaceUID,
RuleGroup: rule.RuleGroup,
Created: rule.Updated,
Condition: rule.Condition,
Data: rule.Data,
ParentVersion: 0,
RestoredFrom: 0,
Version: 1,
IntervalSeconds: 60,
}
_, err = sess.Insert(&ruleVersion)
require.NoError(t, err)
return err
})
require.NoError(t, err)
}
func insertTestDashboard(t *testing.T, dashboardStore dashboards.Store, title string, orgId int64,
folderId int64, folderUID string, isFolder bool, tags ...interface{}) *dashboards.Dashboard {
t.Helper()
cmd := dashboards.SaveDashboardCommand{
OrgID: orgId,
FolderID: folderId, // nolint:staticcheck
FolderUID: folderUID,
IsFolder: isFolder,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": title,
"tags": tags,
}),
}
dash, err := dashboardStore.SaveDashboard(context.Background(), cmd)
require.NoError(t, err)
require.NotNil(t, dash)
dash.Data.Set("id", dash.ID)
dash.Data.Set("uid", dash.UID)
return dash
}
func insertTestDashboardForPlugin(t *testing.T, dashboardStore dashboards.Store, title string, orgId int64,
folderUID string, isFolder bool, pluginId string) *dashboards.Dashboard {
t.Helper()
cmd := dashboards.SaveDashboardCommand{
OrgID: orgId,
IsFolder: isFolder,
FolderUID: folderUID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": title,
}),
PluginID: pluginId,
}
dash, err := dashboardStore.SaveDashboard(context.Background(), cmd)
require.NoError(t, err)
return dash
}
// testSearchDashboards is a (near) copy of the dashboard service
// SearchDashboards, which is a wrapper around FindDashboards.
func testSearchDashboards(d dashboards.Store, query *dashboards.FindPersistedDashboardsQuery) (model.HitList, error) {
res, err := d.FindDashboards(context.Background(), query)
if err != nil {
return nil, err
}
hits := makeQueryResult(query, res)
return hits, nil
}
func makeQueryResult(query *dashboards.FindPersistedDashboardsQuery, res []dashboards.DashboardSearchProjection) model.HitList {
hitList := make([]*model.Hit, 0)
for _, item := range res {
hitType := model.DashHitDB
if item.IsFolder {
hitType = model.DashHitFolder
}
hit := &model.Hit{
ID: item.ID,
UID: item.UID,
Title: item.Title,
URI: "db/" + item.Slug,
URL: dashboards.GetDashboardFolderURL(item.IsFolder, item.UID, item.Slug),
Type: hitType,
FolderUID: item.FolderUID,
FolderTitle: item.FolderTitle,
Tags: item.Tags,
}
if item.FolderUID != "" {
hit.FolderURL = dashboards.GetFolderURL(item.FolderUID, item.FolderSlug)
}
if query.Sort.MetaName != "" {
hit.SortMeta = item.SortMeta
hit.SortMetaName = query.Sort.MetaName
}
hitList = append(hitList, hit)
}
return hitList
}