diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index d6258f5d958..7a2700a8dcc 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -25,8 +25,8 @@ import ( ) type Service struct { - store store - + store store + db db.DB log log.Logger cfg *setting.Cfg dashboardStore dashboards.Store @@ -57,6 +57,7 @@ func ProvideService( features: features, accessControl: ac, bus: bus, + db: db, } if features.IsEnabled(featuremgmt.FlagNestedFolders) { srv.DBMigration(db) @@ -423,33 +424,44 @@ func (s *Service) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) e if cmd.SignedInUser == nil { return folder.ErrBadRequest.Errorf("missing signed in user") } + result := []string{cmd.UID} + err := s.db.InTransaction(ctx, func(ctx context.Context) error { + if s.features.IsEnabled(featuremgmt.FlagNestedFolders) { + subfolders, err := s.nestedFolderDelete(ctx, cmd) - if s.features.IsEnabled(featuremgmt.FlagNestedFolders) { - err := s.nestedFolderDelete(ctx, cmd) - if err != nil { - logger.Error("the delete folder on folder table failed with err: ", "error", err) - return err + if err != nil { + logger.Error("the delete folder on folder table failed with err: ", "error", err) + return err + } + result = append(result, subfolders...) } - } - dashFolder, err := s.dashboardFolderStore.GetFolderByUID(ctx, cmd.OrgID, cmd.UID) - if err != nil { - return err - } + for _, folder := range result { + dashFolder, err := s.dashboardFolderStore.GetFolderByUID(ctx, cmd.OrgID, folder) + if err != nil { + return err + } - guard, err := guardian.NewByUID(ctx, dashFolder.UID, cmd.OrgID, cmd.SignedInUser) - if err != nil { - return err - } + guard, err := guardian.NewByUID(ctx, dashFolder.UID, cmd.OrgID, cmd.SignedInUser) + if err != nil { + return err + } - if canSave, err := guard.CanDelete(); err != nil || !canSave { - if err != nil { - return toFolderError(err) + if canSave, err := guard.CanDelete(); err != nil || !canSave { + if err != nil { + return toFolderError(err) + } + return dashboards.ErrFolderAccessDenied + } + err = s.legacyDelete(ctx, cmd, dashFolder) + if err != nil { + return err + } } - return dashboards.ErrFolderAccessDenied - } + return nil + }) - return s.legacyDelete(ctx, cmd, dashFolder) + return err } func (s *Service) legacyDelete(ctx context.Context, cmd *folder.DeleteFolderCommand, dashFolder *folder.Folder) error { @@ -508,10 +520,14 @@ func (s *Service) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*fol }) } -func (s *Service) nestedFolderDelete(ctx context.Context, cmd *folder.DeleteFolderCommand) error { +// nestedFolderDelete inspects the folder referenced by the cmd argument, deletes all the entries for +// its descendant folders (folders which are nested within it either directly or indirectly) from +// the folder store and returns the UIDs for all its descendants. +func (s *Service) nestedFolderDelete(ctx context.Context, cmd *folder.DeleteFolderCommand) ([]string, error) { logger := s.log.FromContext(ctx) + result := []string{} if cmd.SignedInUser == nil { - return folder.ErrBadRequest.Errorf("missing signed in user") + return result, folder.ErrBadRequest.Errorf("missing signed in user") } _, err := s.Get(ctx, &folder.GetFolderQuery{ @@ -520,28 +536,30 @@ func (s *Service) nestedFolderDelete(ctx context.Context, cmd *folder.DeleteFold SignedInUser: cmd.SignedInUser, }) if err != nil { - return err + return result, err } folders, err := s.store.GetChildren(ctx, folder.GetChildrenQuery{UID: cmd.UID, OrgID: cmd.OrgID}) if err != nil { - return err + return result, err } for _, f := range folders { + result = append(result, f.UID) logger.Info("deleting subfolder", "org_id", f.OrgID, "uid", f.UID) - err := s.nestedFolderDelete(ctx, &folder.DeleteFolderCommand{UID: f.UID, OrgID: f.OrgID, ForceDeleteRules: cmd.ForceDeleteRules, SignedInUser: cmd.SignedInUser}) + subfolders, err := s.nestedFolderDelete(ctx, &folder.DeleteFolderCommand{UID: f.UID, OrgID: f.OrgID, ForceDeleteRules: cmd.ForceDeleteRules, SignedInUser: cmd.SignedInUser}) if err != nil { logger.Error("failed deleting subfolder", "org_id", f.OrgID, "uid", f.UID, "error", err) - return err + return result, err } + result = append(result, subfolders...) } logger.Info("deleting folder", "org_id", cmd.OrgID, "uid", cmd.UID) err = s.store.Delete(ctx, cmd.UID, cmd.OrgID) if err != nil { logger.Info("failed deleting folder", "org_id", cmd.OrgID, "uid", cmd.UID, "err", err) - return err + return result, err } - return nil + return result, nil } // MakeUserAdmin is copy of DashboardServiceImpl.MakeUserAdmin diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index fc9b12f84a0..c032bfcdd6c 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -12,17 +12,22 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/guardian" + "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" + "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/util" @@ -67,6 +72,7 @@ func TestIntegrationFolderService(t *testing.T) { store: nestedFolderStore, features: features, bus: bus.ProvideBus(tracing.InitializeTracerForTest()), + db: db, } t.Run("Given user has no permissions", func(t *testing.T) { @@ -312,6 +318,113 @@ func TestIntegrationFolderService(t *testing.T) { }) } +func TestIntegrationDeleteNestedFolders(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + db := sqlstore.InitTestDB(t) + quotaService := quotatest.New(false, nil) + folderStore := ProvideDashboardFolderStore(db) + + cfg := setting.NewCfg() + + featuresFlagOn := featuremgmt.WithFeatures("nestedFolders") + dashStore, err := database.ProvideDashboardStore(db, db.Cfg, featuresFlagOn, tagimpl.ProvideService(db, db.Cfg), quotaService) + require.NoError(t, err) + nestedFolderStore := ProvideStore(db, db.Cfg, featuresFlagOn) + + serviceWithFlagOn := &Service{ + cfg: cfg, + log: log.New("test-folder-service"), + dashboardStore: dashStore, + dashboardFolderStore: folderStore, + store: nestedFolderStore, + features: featuresFlagOn, + bus: bus.ProvideBus(tracing.InitializeTracerForTest()), + db: db, + } + + signedInUser := user.SignedInUser{UserID: 1, OrgID: orgID} + createCmd := folder.CreateFolderCommand{ + OrgID: orgID, + ParentUID: "", + SignedInUser: &signedInUser, + } + + t.Run("With nested folder feature flag on", func(t *testing.T) { + origNewGuardian := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) + + ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, 3, "", createCmd) + + deleteCmd := folder.DeleteFolderCommand{ + UID: ancestorUIDs[0], + OrgID: orgID, + SignedInUser: &signedInUser, + } + err = serviceWithFlagOn.Delete(context.Background(), &deleteCmd) + require.NoError(t, err) + + for i, uid := range ancestorUIDs { + // dashboard table + _, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, uid) + require.ErrorIs(t, err, dashboards.ErrFolderNotFound) + // folder table + _, err = serviceWithFlagOn.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestorUIDs[i], OrgID: orgID}) + require.ErrorIs(t, err, folder.ErrFolderNotFound) + } + t.Cleanup(func() { + guardian.New = origNewGuardian + }) + }) + t.Run("With feature flag unset", func(t *testing.T) { + featuresFlagOff := featuremgmt.WithFeatures() + dashStore, err := database.ProvideDashboardStore(db, db.Cfg, featuresFlagOff, tagimpl.ProvideService(db, db.Cfg), quotaService) + require.NoError(t, err) + nestedFolderStore := ProvideStore(db, db.Cfg, featuresFlagOff) + + serviceWithFlagOff := &Service{ + cfg: cfg, + log: log.New("test-folder-service"), + dashboardStore: dashStore, + dashboardFolderStore: folderStore, + store: nestedFolderStore, + features: featuresFlagOff, + bus: bus.ProvideBus(tracing.InitializeTracerForTest()), + db: db, + } + + origNewGuardian := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) + + ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, 1, "", createCmd) + + deleteCmd := folder.DeleteFolderCommand{ + UID: ancestorUIDs[0], + OrgID: orgID, + SignedInUser: &signedInUser, + } + err = serviceWithFlagOff.Delete(context.Background(), &deleteCmd) + require.NoError(t, err) + + for i, uid := range ancestorUIDs { + // dashboard table + _, err := serviceWithFlagOff.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, uid) + require.ErrorIs(t, err, dashboards.ErrFolderNotFound) + // folder table + _, err = serviceWithFlagOff.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestorUIDs[i], OrgID: orgID}) + require.NoError(t, err) + } + t.Cleanup(func() { + guardian.New = origNewGuardian + for _, uid := range ancestorUIDs { + err := serviceWithFlagOff.store.Delete(context.Background(), uid, orgID) + require.NoError(t, err) + } + }) + }) +} + func TestNestedFolderServiceFeatureToggle(t *testing.T) { g := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) @@ -366,7 +479,7 @@ func TestNestedFolderService(t *testing.T) { nestedFolderStore := NewFakeStore() - folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures(), nil) + folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures(), nil, dbtest.NewFakeDB()) _, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: "myFolder", @@ -377,26 +490,6 @@ func TestNestedFolderService(t *testing.T) { // CreateFolder should not call the folder store create if the feature toggle is not enabled. require.False(t, nestedFolderStore.CreateCalled) }) - - t.Run("When delete folder, no delete in folder table done", func(t *testing.T) { - var actualCmd *dashboards.DeleteDashboardCommand - dashStore := &dashboards.FakeDashboardStore{} - dashStore.On("DeleteDashboard", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - actualCmd = args.Get(1).(*dashboards.DeleteDashboardCommand) - }).Return(nil).Once() - - dashboardFolderStore := foldertest.NewFakeFolderStore(t) - dashboardFolderStore.On("GetFolderByUID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).Return(&folder.Folder{}, nil) - - nestedFolderStore := NewFakeStore() - - folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures(), nil) - err := folderSvc.Delete(context.Background(), &folder.DeleteFolderCommand{UID: "myFolder", OrgID: orgID, SignedInUser: usr}) - require.NoError(t, err) - require.NotNil(t, actualCmd) - - require.False(t, nestedFolderStore.DeleteCalled) - }) }) t.Run("with nested folder feature flag on", func(t *testing.T) { @@ -419,7 +512,7 @@ func TestNestedFolderService(t *testing.T) { folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, - }) + }, dbtest.NewFakeDB()) _, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: "myFolder", @@ -450,7 +543,7 @@ func TestNestedFolderService(t *testing.T) { folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, - }) + }, dbtest.NewFakeDB()) f, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: "myFolder", @@ -504,7 +597,7 @@ func TestNestedFolderService(t *testing.T) { folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, - }) + }, dbtest.NewFakeDB()) _, err := folderSvc.Create(context.Background(), &cmd) require.Error(t, err, folder.ErrCircularReference) // CreateFolder should not call the folder store's create method. @@ -539,7 +632,7 @@ func TestNestedFolderService(t *testing.T) { // the service return success as long as the legacy create succeeds folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, - }) + }, dbtest.NewFakeDB()) _, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: "myFolder", @@ -569,7 +662,7 @@ func TestNestedFolderService(t *testing.T) { folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, - }) + }, dbtest.NewFakeDB()) _, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder", OrgID: orgID, SignedInUser: usr}) require.Error(t, err, dashboards.ErrFolderAccessDenied) }) @@ -590,7 +683,7 @@ func TestNestedFolderService(t *testing.T) { folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, - }) + }, dbtest.NewFakeDB()) _, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder", OrgID: orgID, SignedInUser: usr}) require.Error(t, err, dashboards.ErrFolderAccessDenied) }) @@ -616,7 +709,7 @@ func TestNestedFolderService(t *testing.T) { folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, - }) + }, dbtest.NewFakeDB()) f, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder", OrgID: orgID, SignedInUser: usr}) require.NoError(t, err) require.NotNil(t, f) @@ -638,7 +731,7 @@ func TestNestedFolderService(t *testing.T) { folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, - }) + }, dbtest.NewFakeDB()) f, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder", OrgID: orgID, SignedInUser: usr}) require.Error(t, err, folder.ErrCircularReference) require.Nil(t, f) @@ -664,7 +757,7 @@ func TestNestedFolderService(t *testing.T) { folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, - }) + }, dbtest.NewFakeDB()) f, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder2", OrgID: orgID, SignedInUser: usr}) require.Error(t, err, folder.ErrMaximumDepthReached) require.Nil(t, f) @@ -686,41 +779,12 @@ func TestNestedFolderService(t *testing.T) { folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, - }) + }, dbtest.NewFakeDB()) f, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder2", OrgID: orgID, SignedInUser: usr}) require.Error(t, err, folder.ErrCircularReference) require.Nil(t, f) }) - t.Run("delete with success", func(t *testing.T) { - g := guardian.New - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) - t.Cleanup(func() { - guardian.New = g - }) - - dashStore := &dashboards.FakeDashboardStore{} - var actualCmd *dashboards.DeleteDashboardCommand - dashStore.On("DeleteDashboard", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - actualCmd = args.Get(1).(*dashboards.DeleteDashboardCommand) - }).Return(nil).Once() - - dashboardFolderStore := foldertest.NewFakeFolderStore(t) - dashboardFolderStore.On("GetFolderByUID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).Return(&folder.Folder{}, nil) - - nestedFolderStore := NewFakeStore() - nestedFolderStore.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"} - - folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ - ExpectedEvaluate: true, - }) - err := folderSvc.Delete(context.Background(), &folder.DeleteFolderCommand{UID: "myFolder", OrgID: orgID, SignedInUser: usr}) - require.NoError(t, err) - require.NotNil(t, actualCmd) - - require.True(t, nestedFolderStore.DeleteCalled) - }) - t.Run("create returns error if maximum depth reached", func(t *testing.T) { // This test creates and deletes the dashboard, so needs some extra setup. g := guardian.New @@ -752,7 +816,7 @@ func TestNestedFolderService(t *testing.T) { folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, - }) + }, dbtest.NewFakeDB()) _, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ Title: "folder", OrgID: orgID, @@ -781,7 +845,7 @@ func TestNestedFolderService(t *testing.T) { folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, - }) + }, dbtest.NewFakeDB()) _, err := folderSvc.Get(context.Background(), &folder.GetFolderQuery{ OrgID: orgID, ID: &folder.GeneralFolder.ID, @@ -792,7 +856,44 @@ func TestNestedFolderService(t *testing.T) { }) } -func setup(t *testing.T, dashStore dashboards.Store, dashboardFolderStore folder.FolderStore, nestedFolderStore store, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl) folder.Service { +func CreateSubtreeInStore(t *testing.T, store *sqlStore, service *Service, depth int, prefix string, cmd folder.CreateFolderCommand) []string { + t.Helper() + + ancestorUIDs := []string{} + if cmd.ParentUID != "" { + ancestorUIDs = append(ancestorUIDs, cmd.ParentUID) + } + for i := 0; i < depth; i++ { + title := fmt.Sprintf("%sfolder-%d", prefix, i) + cmd.Title = title + cmd.UID = util.GenerateShortUID() + + f, err := service.Create(context.Background(), &cmd) + require.NoError(t, err) + require.Equal(t, title, f.Title) + require.NotEmpty(t, f.ID) + require.NotEmpty(t, f.UID) + + parents, err := store.GetParents(context.Background(), folder.GetParentsQuery{ + UID: f.UID, + OrgID: cmd.OrgID, + }) + require.NoError(t, err) + parentUIDs := []string{} + for _, p := range parents { + parentUIDs = append(parentUIDs, p.UID) + } + require.Equal(t, ancestorUIDs, parentUIDs) + + ancestorUIDs = append(ancestorUIDs, f.UID) + + cmd.ParentUID = f.UID + } + + return ancestorUIDs +} + +func setup(t *testing.T, dashStore dashboards.Store, dashboardFolderStore folder.FolderStore, nestedFolderStore store, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl, db db.DB) folder.Service { t.Helper() // nothing enabled yet @@ -806,5 +907,6 @@ func setup(t *testing.T, dashStore dashboards.Store, dashboardFolderStore folder store: nestedFolderStore, features: features, accessControl: ac, + db: db, } } diff --git a/pkg/services/folder/folderimpl/sqlstore_test.go b/pkg/services/folder/folderimpl/sqlstore_test.go index 3ab31583b80..3a6219d0b36 100644 --- a/pkg/services/folder/folderimpl/sqlstore_test.go +++ b/pkg/services/folder/folderimpl/sqlstore_test.go @@ -153,7 +153,7 @@ func TestIntegrationDelete(t *testing.T) { }) */ - ancestorUIDs := CreateSubTree(t, folderStore, orgID, "", folder.MaxNestedFolderDepth, "") + ancestorUIDs := CreateSubtree(t, folderStore, orgID, "", folder.MaxNestedFolderDepth, "") require.Len(t, ancestorUIDs, folder.MaxNestedFolderDepth) t.Cleanup(func() { @@ -603,8 +603,7 @@ func TestIntegrationGetHeight(t *testing.T) { UID: uid1, }) require.NoError(t, err) - subTree := CreateSubTree(t, folderStore, orgID, parent.UID, 4, "sub") - + subTree := CreateSubtree(t, folderStore, orgID, parent.UID, 4, "sub") t.Run("should successfully get height", func(t *testing.T) { height, err := folderStore.GetHeight(context.Background(), parent.UID, orgID, nil) require.NoError(t, err) @@ -632,7 +631,7 @@ func CreateOrg(t *testing.T, db *sqlstore.SQLStore) int64 { return orgID } -func CreateSubTree(t *testing.T, store *sqlStore, orgID int64, parentUID string, depth int, prefix string) []string { +func CreateSubtree(t *testing.T, store *sqlStore, orgID int64, parentUID string, depth int, prefix string) []string { t.Helper() ancestorUIDs := []string{}