package folders import ( "context" "fmt" "testing" "github.com/stretchr/testify/require" "google.golang.org/grpc" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/apimachinery/utils" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" ) func TestValidateCreate(t *testing.T) { tests := []struct { name string folder *folders.Folder getter *folders.FolderInfoList getterError error expectedErr string maxDepth int // defaults to 5 unless set }{{ name: "ok", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "p1", Annotations: map[string]string{"grafana.app/folder": "p2"}, }, Spec: folders.FolderSpec{ Title: "some title", }, }, getter: &folders.FolderInfoList{ Items: []folders.FolderInfo{ {Name: "p2", Parent: "p3"}, {Name: "p3"}, }, }, }, { name: "reserved name", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "general", // can not name something with general }, }, expectedErr: "invalid uid for folder provided", }, { name: "too long", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "a0123456789012345678901234567890123456789", // longer than 40 }, }, expectedErr: "uid too long, max 40 characters", }, { name: "bad name", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "hello world", // not a-z|0-9, }, }, expectedErr: "uid contains illegal characters", }, { name: "can not be a parent of yourself", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "p1", Annotations: map[string]string{"grafana.app/folder": "p1"}, }, Spec: folders.FolderSpec{ Title: "some title", }, }, expectedErr: "folder cannot be parent of itself", }, { name: "can not create a tree that is too deep", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "p1", Annotations: map[string]string{"grafana.app/folder": "p2"}, }, Spec: folders.FolderSpec{ Title: "some title", }, }, getter: &folders.FolderInfoList{ Items: []folders.FolderInfo{ {Name: "p2", Parent: "p3"}, {Name: "p3"}, }, }, maxDepth: 2, // will become 3 expectedErr: "folder max depth exceeded", }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { maxDepth := tt.maxDepth if maxDepth == 0 { maxDepth = 5 } err := validateOnCreate(context.Background(), tt.folder, func(ctx context.Context, folder *folders.Folder) (*folders.FolderInfoList, error) { return tt.getter, tt.getterError }, maxDepth) if tt.expectedErr == "" { require.NoError(t, err) } else { require.Error(t, err) require.Contains(t, err.Error(), tt.expectedErr) } }) } } func TestValidateUpdate(t *testing.T) { tests := []struct { name string folder *folders.Folder old *folders.Folder parents *folders.FolderInfoList parentsError error expectedErr string maxDepth int // defaults to 5 unless set }{{ name: "change title", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "nnn", }, Spec: folders.FolderSpec{ Title: "changed", }, }, old: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "nnn", }, Spec: folders.FolderSpec{ Title: "old title", }, }, }, { name: "error to move into k6 folder", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "nnn", Annotations: map[string]string{ utils.AnnoKeyFolder: "k6-app", }, }, Spec: folders.FolderSpec{ Title: "changed", }, }, old: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "nnn", }, Spec: folders.FolderSpec{ Title: "old title", }, }, expectedErr: "k6 project may not be moved", }, { name: "error when moving too deep", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Annotations: map[string]string{ utils.AnnoKeyFolder: "p1", }, }, Spec: folders.FolderSpec{ Title: "changed", }, }, old: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{}, Spec: folders.FolderSpec{ Title: "old title", }, }, parents: &folders.FolderInfoList{ Items: []folders.FolderInfo{ {Name: "p1", Parent: "p2"}, {Name: "p2", Parent: "p3"}, {Name: "p3"}, }, }, maxDepth: 2, // will become 3 expectedErr: "[folder.maximum-depth-reached]", }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { maxDepth := tt.maxDepth if maxDepth == 0 { maxDepth = 5 } m := grafanarest.NewMockStorage(t) if tt.parents != nil { for _, v := range tt.parents.Items { m.On("Get", context.Background(), v.Name, &metav1.GetOptions{}).Return(&folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: v.Name, }, Spec: folders.FolderSpec{ Title: v.Title, }, }, nil).Maybe() } } err := validateOnUpdate(context.Background(), tt.folder, tt.old, m, func(ctx context.Context, folder *folders.Folder) (*folders.FolderInfoList, error) { return tt.parents, tt.parentsError }, maxDepth) if tt.expectedErr == "" { require.NoError(t, err) } else { require.Error(t, err) require.Contains(t, err.Error(), tt.expectedErr) } }) } } func TestValidateDelete(t *testing.T) { tests := []struct { name string folder *folders.Folder searcher *mockSearchClient expectedErr string }{{ name: "simple delete", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "nnn", }, }, searcher: &mockSearchClient{ stats: &resourcepb.ResourceStatsResponse{ // Empty stats Stats: []*resourcepb.ResourceStatsResponse_Stats{}, }, }, }, { name: "stats error", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "nnn", }, }, searcher: &mockSearchClient{ stats: &resourcepb.ResourceStatsResponse{}, }, expectedErr: "could not verify if folder is empty", }, { name: "stats error", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "nnn", }, }, searcher: &mockSearchClient{ statsErr: fmt.Errorf("error running stats"), }, expectedErr: "error running stats", }, { name: "stats error", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "nnn", }, }, searcher: &mockSearchClient{ stats: &resourcepb.ResourceStatsResponse{ Error: &resourcepb.ErrorResult{ Reason: "error", }, }, }, expectedErr: "could not verify if folder is empty", }, { name: "folder not empty", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "nnn", }, }, searcher: &mockSearchClient{ stats: &resourcepb.ResourceStatsResponse{ Stats: []*resourcepb.ResourceStatsResponse_Stats{ { Group: "folders.grafana.app", Resource: "folders", Count: 10, // not empty }, }, }, }, expectedErr: "[folder.not-empty]", }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validateOnDelete(context.Background(), tt.folder, tt.searcher) if tt.expectedErr == "" { require.NoError(t, err) } else { require.Error(t, err) require.Contains(t, err.Error(), tt.expectedErr) } }) } } var ( _ = resourcepb.ResourceIndexClient(&mockSearchClient{}) ) type mockSearchClient struct { stats *resourcepb.ResourceStatsResponse statsErr error search *resourcepb.ResourceSearchResponse searchErr error } // GetStats implements resourcepb.ResourceIndexClient. func (m *mockSearchClient) GetStats(ctx context.Context, in *resourcepb.ResourceStatsRequest, opts ...grpc.CallOption) (*resourcepb.ResourceStatsResponse, error) { return m.stats, m.statsErr } // Search implements resourcepb.ResourceIndexClient. func (m *mockSearchClient) Search(ctx context.Context, in *resourcepb.ResourceSearchRequest, opts ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) { return m.search, m.searchErr }