From cc6a75d0211ceb392f14d68656487c46cb553e40 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Wed, 31 Dec 2025 08:15:20 -0700 Subject: [PATCH] Zanzana: Add folder integration tests (#115766) --- pkg/tests/apis/folder/folder_tree_test.go | 193 +++++++++++++--------- pkg/tests/testinfra/testinfra.go | 16 ++ 2 files changed, 130 insertions(+), 79 deletions(-) diff --git a/pkg/tests/apis/folder/folder_tree_test.go b/pkg/tests/apis/folder/folder_tree_test.go index 1b3d5df37f2..26e7b5f6884 100644 --- a/pkg/tests/apis/folder/folder_tree_test.go +++ b/pkg/tests/apis/folder/folder_tree_test.go @@ -8,6 +8,7 @@ import ( "net/http" "strings" "testing" + "time" "github.com/stretchr/testify/require" "github.com/xlab/treeprint" @@ -31,6 +32,33 @@ import ( "github.com/grafana/grafana/pkg/util/testutil" ) +func TestIntegrationFolderTreeZanzana(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + runIntegrationFolderTree(t, testinfra.GrafanaOpts{ + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": { + DualWriterMode: grafanarest.Mode5, + }, + folderV1.RESOURCEGROUP: { + DualWriterMode: grafanarest.Mode5, + }, + }, + EnableFeatureToggles: []string{ + "zanzana", + "zanzanaNoLegacyClient", + "kubernetesAuthzZanzanaSync", + }, + UnifiedStorageEnableSearch: true, + ZanzanaReconciliationInterval: 100 * time.Millisecond, + DisableZanzanaCache: true, + }) +} + func TestIntegrationFolderTree(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) @@ -47,7 +75,7 @@ func TestIntegrationFolderTree(t *testing.T) { } for _, mode := range modes { t.Run(fmt.Sprintf("mode %d", mode), func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + runIntegrationFolderTree(t, testinfra.GrafanaOpts{ DisableDataMigrations: true, AppModeProduction: true, DisableAnonymous: true, @@ -62,113 +90,120 @@ func TestIntegrationFolderTree(t *testing.T) { }, UnifiedStorageEnableSearch: mode >= grafanarest.Mode3, // make sure modes 0-3 work without search enabled }) - defer helper.Shutdown() + }) + } +} - tests := []struct { - Name string - Definition FolderDefinition - Expected []ExpectedTree - }{ - { - Name: "admin-only-tree", - Definition: FolderDefinition{ +func runIntegrationFolderTree(t *testing.T, opts testinfra.GrafanaOpts) { + if !db.IsTestDbSQLite() { + t.Skip("test only on sqlite for now") + } + + helper := apis.NewK8sTestHelper(t, opts) + defer helper.Shutdown() + + tests := []struct { + Name string + Definition FolderDefinition + Expected []ExpectedTree + }{ + { + Name: "admin-only-tree", + Definition: FolderDefinition{ + Children: []FolderDefinition{ + {Name: "top", + Creator: helper.Org1.Admin, Children: []FolderDefinition{ - {Name: "top", + {Name: "middle", Creator: helper.Org1.Admin, Children: []FolderDefinition{ - {Name: "middle", + {Name: "child", Creator: helper.Org1.Admin, - Children: []FolderDefinition{ - {Name: "child", - Creator: helper.Org1.Admin, - Permissions: []FolderPermission{{ - Permission: "View", - User: helper.Org1.None, - }}, - }, - }, + Permissions: []FolderPermission{{ + Permission: "View", + User: helper.Org1.None, + }}, }, }, }, }, }, - Expected: []ExpectedTree{ - {User: helper.Org1.Admin, Listing: ` + }, + }, + Expected: []ExpectedTree{ + {User: helper.Org1.Admin, Listing: ` └── top (admin,edit,save,delete) ....└── middle (admin,edit,save,delete) ........└── child (admin,edit,save,delete)`}, - {User: helper.Org1.Viewer, Listing: ` + {User: helper.Org1.Viewer, Listing: ` └── top (view) ....└── middle (view) ........└── child (view)`}, - {User: helper.Org1.None, Listing: ` + {User: helper.Org1.None, Listing: ` └── sharedwithme (???) ....└── child (view)`, - E403: []string{"top", "middle"}, - }, - }, + E403: []string{"top", "middle"}, }, - } + }, + }, + } - var statusCode int - for _, tt := range tests { - t.Run(tt.Name, func(t *testing.T) { - tt.Definition.RequireUniqueName(t, make(map[string]bool)) + var statusCode int + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + tt.Definition.RequireUniqueName(t, make(map[string]bool)) - tt.Definition.CreateWithLegacyAPI(t, helper, "") - // CreateWithLegacyAPI + tt.Definition.CreateWithLegacyAPI(t, helper, "") - for _, expect := range tt.Expected { - unstructured, client := getFolderClients(t, expect.User) - t.Run(fmt.Sprintf("query as %s", expect.User.Identity.GetLogin()), func(t *testing.T) { - legacy := getFoldersFromLegacyAPISearch(t, client) - legacy.requireEqual(t, expect.Listing, "legacy") + for _, expect := range tt.Expected { + unstructured, client := getFolderClients(t, expect.User) + t.Run(fmt.Sprintf("query as %s", expect.User.Identity.GetLogin()), func(t *testing.T) { + legacy := getFoldersFromLegacyAPISearch(t, client) + legacy.requireEqual(t, expect.Listing, "legacy") - listed := getFoldersFromAPIServerList(t, unstructured) - listed.requireEqual(t, expect.Listing, "listed") + listed := getFoldersFromAPIServerList(t, unstructured) + listed.requireEqual(t, expect.Listing, "listed") - search := getFoldersFromDashboardV0Search(t, client, expect.User.Identity.GetNamespace()) - search.requireEqual(t, expect.Listing, "search") + search := getFoldersFromDashboardV0Search(t, client, expect.User.Identity.GetNamespace()) + search.requireEqual(t, expect.Listing, "search") - // ensure sure GET also works on each folder we can list - listed.forEach(func(fv *FolderView) { - if fv.Name == folder.SharedWithMeFolderUID { - return // skip it - } - found, err := unstructured.Get(context.Background(), fv.Name, v1.GetOptions{}) - require.NoErrorf(t, err, "getting folder: %s", fv.Name) - require.Equal(t, found.GetName(), fv.Name) - }) + // ensure sure GET also works on each folder we can list + listed.forEach(func(fv *FolderView) { + if fv.Name == folder.SharedWithMeFolderUID { + return // skip it + } + found, err := unstructured.Get(context.Background(), fv.Name, v1.GetOptions{}) + require.NoErrorf(t, err, "getting folder: %s", fv.Name) + require.Equal(t, found.GetName(), fv.Name) + }) - // Forbidden things should really be hidden - for _, name := range expect.E403 { - _, err := unstructured.Get(context.Background(), name, v1.GetOptions{}) - require.Error(t, err) - require.Truef(t, apierrors.IsForbidden(err), "error: %w", err) // 404 vs 403 ???? + // Forbidden things should really be hidden + for _, name := range expect.E403 { + _, err := unstructured.Get(context.Background(), name, v1.GetOptions{}) + require.Error(t, err) + require.Truef(t, apierrors.IsForbidden(err), "error: %w", err) // 404 vs 403 ???? - result := client.Get().AbsPath("api", "folders", name). - Do(context.Background()). - StatusCode(&statusCode) - require.Equal(t, int(http.StatusForbidden), statusCode) - require.Error(t, result.Error()) + result := client.Get().AbsPath("api", "folders", name). + Do(context.Background()). + StatusCode(&statusCode) + require.Equal(t, int(http.StatusForbidden), statusCode) + require.Error(t, result.Error()) - // Verify sub-resources are hidden - for _, sub := range []string{"access", "parents", "children", "counts"} { - _, err := unstructured.Get(context.Background(), name, v1.GetOptions{}, sub) - require.Error(t, err, "expect error for subresource", sub) - require.Truef(t, apierrors.IsForbidden(err), "error: %w", err) // 404 vs 403 ???? - } + // Verify sub-resources are hidden + for _, sub := range []string{"access", "parents", "children", "counts"} { + _, err := unstructured.Get(context.Background(), name, v1.GetOptions{}, sub) + require.Error(t, err, "expect error for subresource", sub) + require.Truef(t, apierrors.IsForbidden(err), "error: %w", err) // 404 vs 403 ???? + } - // Verify legacy API access is also hidden - for _, sub := range []string{"permissions", "counts"} { - result := client.Get().AbsPath("api", "folders", name, sub). - Do(context.Background()). - StatusCode(&statusCode) - require.Equalf(t, int(http.StatusForbidden), statusCode, "legacy access to: %s", sub) - require.Error(t, result.Error()) - } - } - }) + // Verify legacy API access is also hidden + for _, sub := range []string{"permissions", "counts"} { + result := client.Get().AbsPath("api", "folders", name, sub). + Do(context.Background()). + StatusCode(&statusCode) + require.Equalf(t, int(http.StatusForbidden), statusCode, "legacy access to: %s", sub) + require.Error(t, result.Error()) + } } }) } diff --git a/pkg/tests/testinfra/testinfra.go b/pkg/tests/testinfra/testinfra.go index 6e2f0660c32..13507b7987a 100644 --- a/pkg/tests/testinfra/testinfra.go +++ b/pkg/tests/testinfra/testinfra.go @@ -609,6 +609,20 @@ func CreateGrafDir(t *testing.T, opts GrafanaOpts) (string, string) { require.NoError(t, err) } + if opts.ZanzanaReconciliationInterval != 0 { + rbacSect, err := cfg.NewSection("rbac") + require.NoError(t, err) + _, err = rbacSect.NewKey("zanzana_reconciliation_interval", opts.ZanzanaReconciliationInterval.String()) + require.NoError(t, err) + } + + if opts.DisableZanzanaCache { + rbacSect, err := cfg.NewSection("rbac") + require.NoError(t, err) + _, err = rbacSect.NewKey("disable_zanzana_cache", "true") + require.NoError(t, err) + } + dashboardsSection, err := getOrCreateSection("dashboards") require.NoError(t, err) _, err = dashboardsSection.NewKey("min_refresh_interval", "10s") @@ -687,6 +701,8 @@ type GrafanaOpts struct { SecretsManagerEnableDBMigrations bool OpenFeatureAPIEnabled bool DisableAuthZClientCache bool + ZanzanaReconciliationInterval time.Duration + DisableZanzanaCache bool // Allow creating grafana dir beforehand Dir string