Files
grafana/pkg/services/folder/folderimpl/unifiedstore_test.go
Will Assis 51825cfffe unified-storage: Reduce calls to the user service (#102934)
* Create ListByIdOrUID in user service

* create UnstructuredToLegacyFolderList 

* update GetFolders to use list parser

* update GetDescendants to use list parser

* update UnstructuredToLegacyFolder to also make a single call to the user service

---------

Co-authored-by: Stephanie Hingtgen <stephanie.hingtgen@grafana.com>
2025-03-27 22:01:07 +02:00

392 lines
13 KiB
Go

package folderimpl
import (
"context"
"net/http"
"testing"
claims "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver/client"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/user/usertest"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/selection"
)
func TestComputeFullPath(t *testing.T) {
testCases := []struct {
name string
parents []*folder.Folder
wantPath string
wantPathUIDs string
}{
{
name: "empty slice should return empty paths",
parents: []*folder.Folder{},
wantPath: "",
wantPathUIDs: "",
},
{
name: "single element should return single path",
parents: []*folder.Folder{
{
Title: "Element",
UID: "Element-uid",
},
},
wantPath: "Element",
wantPathUIDs: "Element-uid",
},
{
name: "multiple parents should return hierarchical path",
parents: []*folder.Folder{
{
Title: "Grandparent",
UID: "grandparent-uid",
},
{
Title: "Parent",
UID: "parent-uid",
},
{
Title: "Element",
UID: "Element-uid",
},
},
wantPath: "Grandparent/Parent/Element",
wantPathUIDs: "grandparent-uid/parent-uid/Element-uid",
},
{
name: "should handle special characters in titles",
parents: []*folder.Folder{
{
Title: "Parent/With/Slashes",
UID: "parent-uid",
},
{
Title: "Element With Spaces",
UID: "Element-uid",
},
},
wantPath: "Parent/With/Slashes/Element With Spaces",
wantPathUIDs: "parent-uid/Element-uid",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gotPath, gotPathUIDs := computeFullPath(tc.parents)
require.Equal(t, tc.wantPath, gotPath)
require.Equal(t, tc.wantPathUIDs, gotPathUIDs)
})
}
}
func TestGetParents(t *testing.T) {
mockCli := new(client.MockK8sHandler)
store := FolderUnifiedStoreImpl{
k8sclient: mockCli,
userService: usertest.NewUserServiceFake(),
}
ctx := context.Background()
orgID := int64(1)
t.Run("should return list of parent folders of a given folder uid", func(t *testing.T) {
mockCli.On("Get", mock.Anything, "parentone", orgID, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "parentone",
"annotations": map[string]interface{}{"grafana.app/folder": "parenttwo"},
},
},
}, nil).Once()
mockCli.On("Get", mock.Anything, "parenttwo", orgID, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "parenttwo",
"annotations": map[string]interface{}{"grafana.app/folder": "parentthree"},
},
},
}, nil).Once()
mockCli.On("Get", mock.Anything, "parentthree", orgID, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "parentthree",
"annotations": map[string]interface{}{"grafana.app/folder": "parentfour"},
},
},
}, nil).Once()
mockCli.On("Get", mock.Anything, "parentfour", orgID, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "parentfour",
},
},
}, nil).Once()
result, err := store.GetParents(ctx, folder.GetParentsQuery{
UID: "parentone",
OrgID: orgID,
})
require.NoError(t, err)
require.Len(t, result, 3)
require.Equal(t, "parentfour", result[0].UID)
require.Equal(t, "parentthree", result[1].UID)
require.Equal(t, "parenttwo", result[2].UID)
})
t.Run("should stop if user doesnt have access to the parent folder", func(t *testing.T) {
mockCli.On("Get", mock.Anything, "parentone", orgID, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "parentone",
"annotations": map[string]interface{}{"grafana.app/folder": "parenttwo"},
},
},
}, nil).Once()
mockCli.On("Get", mock.Anything, "parenttwo", orgID, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "parenttwo",
"annotations": map[string]interface{}{"grafana.app/folder": "parentthree"},
},
},
}, nil).Once()
mockCli.On("Get", mock.Anything, "parentthree", orgID, mock.Anything, mock.Anything).Return(nil, &apierrors.StatusError{
ErrStatus: metav1.Status{Code: http.StatusForbidden},
}).Once()
result, err := store.GetParents(ctx, folder.GetParentsQuery{
UID: "parentone",
OrgID: orgID,
})
require.NoError(t, err)
require.Len(t, result, 1)
require.Equal(t, "parenttwo", result[0].UID)
})
t.Run("should stop if parent folder is not found", func(t *testing.T) {
mockCli.On("Get", mock.Anything, "parentone", orgID, mock.Anything, mock.Anything).Return(nil, apierrors.NewNotFound(schema.GroupResource{Group: "folders.folder.grafana.app", Resource: "folder"}, "parentone")).Once()
_, err := store.GetParents(ctx, folder.GetParentsQuery{
UID: "parentone",
OrgID: orgID,
})
require.ErrorIs(t, err, dashboards.ErrFolderNotFound)
})
}
func TestGetChildren(t *testing.T) {
mockCli := new(client.MockK8sHandler)
store := FolderUnifiedStoreImpl{
k8sclient: mockCli,
userService: usertest.NewUserServiceFake(),
}
ctx := context.Background()
orgID := int64(2)
t.Run("should be able to find children folders, and set defaults for pages", func(t *testing.T) {
mockCli.On("Search", mock.Anything, orgID, &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Fields: []*resource.Requirement{
{
Key: resource.SEARCH_FIELD_FOLDER,
Operator: string(selection.In),
Values: []string{"folder1"},
},
},
},
Limit: folderSearchLimit, // should default to folderSearchLimit
Offset: 0, // should be set as limit * (page - 1)
Page: 1, // should be set to 1 by default
}).Return(&resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
{Name: "folder", Type: resource.ResourceTableColumnDefinition_STRING},
},
Rows: []*resource.ResourceTableRow{
{
Key: &resource.ResourceKey{Name: "folder2", Resource: "folder"},
Cells: [][]byte{[]byte("folder1")},
},
{
Key: &resource.ResourceKey{Name: "folder3", Resource: "folder"},
Cells: [][]byte{[]byte("folder1")},
},
},
},
TotalHits: 1,
}, nil).Once()
mockCli.On("Get", mock.Anything, "folder1", orgID, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{"name": "folder1"},
},
}, nil).Once()
mockCli.On("Get", mock.Anything, "folder2", orgID, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{"name": "folder2"},
},
}, nil).Once()
mockCli.On("Get", mock.Anything, "folder3", orgID, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{"name": "folder3"},
},
}, nil).Once()
// don't set page or limit - should be automatically added
result, err := store.GetChildren(ctx, folder.GetChildrenQuery{
UID: "folder1",
OrgID: orgID,
})
require.NoError(t, err)
require.Len(t, result, 2)
require.Equal(t, "folder2", result[0].UID)
require.Equal(t, "folder3", result[1].UID)
})
t.Run("should return an error if the folder is not found", func(t *testing.T) {
mockCli.On("Search", mock.Anything, orgID, &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Fields: []*resource.Requirement{
{
Key: resource.SEARCH_FIELD_FOLDER,
Operator: string(selection.In),
Values: []string{"folder1"},
},
},
},
Limit: folderSearchLimit, // should default to folderSearchLimit
Offset: 0, // should be set as limit * (page - 1)
Page: 1, // should be set to 1 by default
}).Return(&resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
{Name: "folder", Type: resource.ResourceTableColumnDefinition_STRING},
},
Rows: []*resource.ResourceTableRow{
{
Key: &resource.ResourceKey{Name: "folder2", Resource: "folder"},
Cells: [][]byte{[]byte("folder1")},
},
{
Key: &resource.ResourceKey{Name: "folder3", Resource: "folder"},
Cells: [][]byte{[]byte("folder1")},
},
},
},
TotalHits: 1,
}, nil).Once()
mockCli.On("Get", mock.Anything, "folder1", orgID, mock.Anything, mock.Anything).Return(nil, apierrors.NewNotFound(schema.GroupResource{Group: "folders.folder.grafana.app", Resource: "folder"}, "folder1")).Once()
_, err := store.GetChildren(ctx, folder.GetChildrenQuery{
UID: "folder1",
OrgID: orgID,
})
require.ErrorIs(t, err, dashboards.ErrFolderNotFound)
})
t.Run("pages should be able to be set, general folder should be turned to empty string, and folder uids should be passed in", func(t *testing.T) {
mockCli.On("Search", mock.Anything, orgID, &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Fields: []*resource.Requirement{
{
Key: resource.SEARCH_FIELD_FOLDER,
Operator: string(selection.In),
Values: []string{""}, // should be an empty string if general is passed in
},
{
Key: resource.SEARCH_FIELD_NAME,
Operator: string(selection.In),
Values: []string{"folder2"},
},
},
},
Limit: 10,
Offset: 20, // should be set as limit * (page - 1)
Page: 3,
}).Return(&resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
{Name: "folder", Type: resource.ResourceTableColumnDefinition_STRING},
},
Rows: []*resource.ResourceTableRow{
{
Key: &resource.ResourceKey{Name: "folder2", Resource: "folder"},
Cells: [][]byte{[]byte("folder1")},
},
},
},
TotalHits: 1,
}, nil).Once()
mockCli.On("Get", mock.Anything, "folder2", orgID, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{"name": "folder2"},
},
}, nil).Once()
result, err := store.GetChildren(ctx, folder.GetChildrenQuery{
UID: "general",
OrgID: orgID,
Limit: 10,
Page: 3,
FolderUIDs: []string{"folder2"},
})
require.NoError(t, err)
require.Len(t, result, 1)
require.Equal(t, "folder2", result[0].UID)
})
t.Run("k6 folder should only be returned to service accounts", func(t *testing.T) {
mockCli.On("Search", mock.Anything, orgID, mock.Anything).Return(&resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
{Name: "folder", Type: resource.ResourceTableColumnDefinition_STRING},
},
Rows: []*resource.ResourceTableRow{
{
Key: &resource.ResourceKey{Name: accesscontrol.K6FolderUID, Resource: "folder"},
Cells: [][]byte{[]byte("folder1")},
},
},
},
TotalHits: 1,
}, nil)
mockCli.On("Get", mock.Anything, "folder", orgID, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{"name": "folder"},
},
}, nil)
mockCli.On("Get", mock.Anything, accesscontrol.K6FolderUID, orgID, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{"name": accesscontrol.K6FolderUID},
},
}, nil)
result, err := store.GetChildren(ctx, folder.GetChildrenQuery{
UID: "folder",
OrgID: orgID,
})
require.NoError(t, err)
require.Len(t, result, 0)
result, err = store.GetChildren(ctx, folder.GetChildrenQuery{
UID: "folder",
OrgID: orgID,
SignedInUser: &identity.StaticRequester{Type: claims.TypeServiceAccount},
})
require.NoError(t, err)
require.Len(t, result, 1)
})
}