2025-04-01 10:38:02 +09:00

453 lines
13 KiB
Go

package legacysearcher
import (
"context"
"encoding/json"
"strconv"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/selection"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
dashboard "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/search/model"
"github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/storage/unified/resource"
unisearch "github.com/grafana/grafana/pkg/storage/unified/search"
)
func TestDashboardSearchClient_Search(t *testing.T) {
mockStore := dashboards.NewFakeDashboardStore(t)
sortSvc := sort.ProvideService()
client := NewDashboardSearchClient(mockStore, sortSvc)
ctx := context.Background()
user := &user.SignedInUser{OrgID: 2}
ctx = identity.WithRequester(ctx, user)
emptyTags, err := json.Marshal([]string{})
require.NoError(t, err)
dashboardKey := &resource.ResourceKey{
Name: "uid",
Resource: dashboard.DASHBOARD_RESOURCE,
}
t.Run("Should parse results into GRPC", func(t *testing.T) {
sorter, _ := sortSvc.GetSortOption("alpha-asc")
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
SignedInUser: user, // user from context should be used
Type: "dash-db", // should set type based off of key
Sort: sorter,
}).Return([]dashboards.DashboardSearchProjection{
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1", Term: "term"},
{UID: "uid2", Title: "Test Dashboard2", FolderUID: "folder2"},
}, nil).Once()
req := &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: dashboardKey,
},
SortBy: []*resource.ResourceSearchRequest_Sort{
{
Field: resource.SEARCH_FIELD_TITLE,
},
},
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
tags, err := json.Marshal([]string{"term"})
require.NoError(t, err)
require.NotNil(t, resp)
searchFields := resource.StandardSearchFields()
require.Equal(t, &resource.ResourceSearchResponse{
TotalHits: 2,
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
searchFields.Field(resource.SEARCH_FIELD_TITLE),
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
searchFields.Field(resource.SEARCH_FIELD_TAGS),
{
Name: "", // sort by should be empty if title is what we sorted by
Type: resource.ResourceTableColumnDefinition_INT64,
},
},
Rows: []*resource.ResourceTableRow{
{
Key: &resource.ResourceKey{
Name: "uid",
Group: dashboard.GROUP,
Resource: dashboard.DASHBOARD_RESOURCE,
},
Cells: [][]byte{
[]byte("Test Dashboard"),
[]byte("folder1"),
tags,
[]byte(strconv.FormatInt(0, 10)),
},
},
{
Key: &resource.ResourceKey{
Name: "uid2",
Group: dashboard.GROUP,
Resource: dashboard.DASHBOARD_RESOURCE,
},
Cells: [][]byte{
[]byte("Test Dashboard2"),
[]byte("folder2"),
emptyTags,
[]byte(strconv.FormatInt(0, 10)),
},
},
},
},
}, resp)
mockStore.AssertExpectations(t)
})
t.Run("Sorting should be properly parsed into legacy sorting options (asc), and results added", func(t *testing.T) {
sortOptionAsc := model.SortOption{
Name: "viewed-asc", // should add -asc to the sort field and match on that
}
sortSvc.RegisterSortOption(sortOptionAsc)
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
SignedInUser: user,
Type: "dash-db",
Sort: sortOptionAsc,
}).Return([]dashboards.DashboardSearchProjection{
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder", SortMeta: int64(50)},
}, nil).Once()
req := &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: dashboardKey,
},
SortBy: []*resource.ResourceSearchRequest_Sort{
{
Field: resource.SEARCH_FIELD_PREFIX + unisearch.DASHBOARD_VIEWS_TOTAL, // "fields." prefix should be removed
Desc: false,
},
},
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
searchFields := resource.StandardSearchFields()
require.Equal(t, &resource.ResourceSearchResponse{
TotalHits: 1,
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
searchFields.Field(resource.SEARCH_FIELD_TITLE),
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
searchFields.Field(resource.SEARCH_FIELD_TAGS),
{
Name: "views_total",
Type: resource.ResourceTableColumnDefinition_INT64,
},
},
Rows: []*resource.ResourceTableRow{
{
Key: &resource.ResourceKey{
Name: "uid",
Group: dashboard.GROUP,
Resource: dashboard.DASHBOARD_RESOURCE,
},
Cells: [][]byte{
[]byte("Test Dashboard"),
[]byte("folder"),
emptyTags,
[]byte(strconv.FormatInt(50, 10)),
},
},
},
},
}, resp)
mockStore.AssertExpectations(t)
})
t.Run("Sorting should be properly parsed into legacy sorting options (desc)", func(t *testing.T) {
sortOptionAsc := model.SortOption{
Name: "errors-recently-desc", // should add -asc to the sort field and match on that
}
sortSvc.RegisterSortOption(sortOptionAsc)
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
SignedInUser: user,
Type: "dash-db",
Sort: sortOptionAsc,
}).Return([]dashboards.DashboardSearchProjection{
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder", SortMeta: int64(2)},
}, nil).Once()
req := &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: dashboardKey,
},
SortBy: []*resource.ResourceSearchRequest_Sort{
{
Field: unisearch.DASHBOARD_ERRORS_LAST_30_DAYS,
Desc: true,
},
},
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
searchFields := resource.StandardSearchFields()
require.Equal(t, &resource.ResourceSearchResponse{
TotalHits: 1,
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
searchFields.Field(resource.SEARCH_FIELD_TITLE),
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
searchFields.Field(resource.SEARCH_FIELD_TAGS),
{
Name: "errors_last_30_days",
Type: resource.ResourceTableColumnDefinition_INT64,
},
},
Rows: []*resource.ResourceTableRow{
{
Key: &resource.ResourceKey{
Name: "uid",
Group: dashboard.GROUP,
Resource: dashboard.DASHBOARD_RESOURCE,
},
Cells: [][]byte{
[]byte("Test Dashboard"),
[]byte("folder"),
emptyTags,
[]byte(strconv.FormatInt(2, 10)),
},
},
},
},
}, resp)
mockStore.AssertExpectations(t)
})
t.Run("Query for tags should return facet properly", func(t *testing.T) {
mockStore.On("GetDashboardTags", mock.Anything, &dashboards.GetDashboardTagsQuery{OrgID: 2}).Return([]*dashboards.DashboardTagCloudItem{
{Term: "tag1", Count: 1},
{Term: "tag2", Count: 5},
}, nil).Once()
req := &resource.ResourceSearchRequest{
Facet: map[string]*resource.ResourceSearchRequest_Facet{
"tags": {
Field: "tags",
},
},
Options: &resource.ListOptions{
Key: dashboardKey,
},
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
require.Equal(t, &resource.ResourceSearchResponse{
Results: &resource.ResourceTable{},
Facet: map[string]*resource.ResourceSearchResponse_Facet{
"tags": {
Terms: []*resource.ResourceSearchResponse_TermFacet{
{
Term: "tag1",
Count: 1,
},
{
Term: "tag2",
Count: 5,
},
},
},
},
}, resp)
mockStore.AssertExpectations(t)
})
t.Run("Query should be set as the title, and * should be removed", func(t *testing.T) {
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
Title: "test",
SignedInUser: user, // user from context should be used
Type: "dash-db", // should set type based off of key
}).Return([]dashboards.DashboardSearchProjection{
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"},
}, nil).Once()
req := &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: dashboardKey,
},
Query: "*test*",
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
mockStore.AssertExpectations(t)
})
t.Run("Should read labels for the dashboard ids", func(t *testing.T) {
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
DashboardIds: []int64{1, 2},
SignedInUser: user, // user from context should be used
Type: "dash-db", // should set type based off of key
}).Return([]dashboards.DashboardSearchProjection{
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"},
}, nil).Once()
req := &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: dashboardKey,
Labels: []*resource.Requirement{
{
Key: utils.LabelKeyDeprecatedInternalID,
Operator: "in",
Values: []string{"1", "2"},
},
},
},
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
mockStore.AssertExpectations(t)
})
t.Run("Should modify fields to legacy compatible queries", func(t *testing.T) {
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
DashboardUIDs: []string{"uid1", "uid2"},
Tags: []string{"tag1", "tag2"},
FolderUIDs: []string{"general", "folder1"},
SignedInUser: user, // user from context should be used
Type: "dash-db", // should set type based off of key
}).Return([]dashboards.DashboardSearchProjection{
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"},
}, nil).Once()
req := &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: dashboardKey,
Fields: []*resource.Requirement{
{
Key: resource.SEARCH_FIELD_TAGS,
Operator: "in",
Values: []string{"tag1", "tag2"},
},
{
Key: resource.SEARCH_FIELD_NAME, // name should be used as uid
Operator: "in",
Values: []string{"uid1", "uid2"},
},
{
Key: resource.SEARCH_FIELD_FOLDER,
Operator: "in",
Values: []string{"", "folder1"}, // empty folder should be general
},
},
},
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
mockStore.AssertExpectations(t)
})
t.Run("Should retrieve dashboards by plugin through a different function", func(t *testing.T) {
mockStore.On("GetDashboardsByPluginID", mock.Anything, &dashboards.GetDashboardsByPluginIDQuery{
PluginID: "slo",
OrgID: 2, // retrieved from the signed in user
}).Return([]*dashboards.Dashboard{
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"},
}, nil).Once()
req := &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: dashboardKey,
Fields: []*resource.Requirement{
{
Key: resource.SEARCH_FIELD_MANAGER_ID,
Operator: "in",
Values: []string{"slo"},
},
{
Key: resource.SEARCH_FIELD_MANAGER_KIND,
Operator: "in",
Values: []string{"plugin"},
},
},
},
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
mockStore.AssertExpectations(t)
})
t.Run("Should retrieve dashboards by provisioner name through a different function", func(t *testing.T) {
mockStore.On("GetProvisionedDashboardsByName", mock.Anything, "test").Return([]*dashboards.Dashboard{
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"},
}, nil).Once()
req := &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: dashboardKey,
Fields: []*resource.Requirement{
{
Key: resource.SEARCH_FIELD_MANAGER_KIND,
Operator: "=",
Values: []string{string(utils.ManagerKindClassicFP)}, // nolint:staticcheck
},
{
Key: resource.SEARCH_FIELD_MANAGER_ID,
Operator: "in",
Values: []string{"test"},
},
},
},
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
mockStore.AssertExpectations(t)
})
t.Run("Should retrieve orphaned dashboards if provisioner not in is specified", func(t *testing.T) {
mockStore.On("GetOrphanedProvisionedDashboards", mock.Anything, []string{"test", "test2"}).Return([]*dashboards.Dashboard{
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"},
}, nil).Once()
req := &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: dashboardKey,
Fields: []*resource.Requirement{
{
Key: resource.SEARCH_FIELD_MANAGER_KIND,
Operator: "=",
Values: []string{string(utils.ManagerKindClassicFP)}, // nolint:staticcheck
},
{
Key: resource.SEARCH_FIELD_MANAGER_ID,
Operator: string(selection.NotIn),
Values: []string{"test", "test2"},
},
},
},
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
mockStore.AssertExpectations(t)
})
}