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" "github.com/grafana/grafana/pkg/services/folder/folderimpl" 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 folderStore := folderimpl.ProvideStore(sqlStore) savedFolder = insertTestDashFolder(t, dashboardStore, folderStore, "1 test dash folder", 1, 0, "", "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.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.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 []res typ string }{ { desc: "find dashboard under general using folder UID", folderUIDs: []string{folder.GeneralFolderUID}, typ: searchstore.TypeDashboard, expectedResult: []res{{title: "dashboard under general"}}, }, { desc: "find dashboard under general using folder UID", folderUIDs: []string{folder.GeneralFolderUID}, typ: searchstore.TypeDashboard, expectedResult: []res{{title: "dashboard under general"}}, }, { desc: "find dashboard under f0 using folder UID", folderUIDs: []string{f0.UID}, typ: searchstore.TypeDashboard, expectedResult: []res{{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: []res{ {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: []res{ {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: []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"}, }, }, { desc: "find subfolder", folderUIDs: []string{f0.UID}, typ: searchstore.TypeFolder, expectedResult: []res{{title: subfolder.Title, folderUID: f0.UID, folderTitle: f0.Title}}, }, } for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), 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), len(res)) for i, r := range tc.expectedResult { 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 insertTestDashFolder(t *testing.T, dashboardStore dashboards.Store, folderStore folder.Store, title string, orgId int64, folderId int64, folderUID string, tags ...interface{}) *dashboards.Dashboard { t.Helper() cmd := dashboards.SaveDashboardCommand{ OrgID: orgId, FolderID: folderId, // nolint:staticcheck FolderUID: folderUID, 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 = folderStore.Create(context.Background(), folder.CreateFolderCommand{ Title: title, UID: dash.UID, OrgID: orgId, }) require.NoError(t, err) 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 }