2025-01-14 05:15:35 +08:00
package folderimpl
import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"testing"
2025-01-24 04:23:59 +08:00
"github.com/stretchr/testify/mock"
2025-01-14 05:15:35 +08:00
"github.com/stretchr/testify/require"
2025-02-12 03:14:25 +08:00
"k8s.io/apimachinery/pkg/selection"
2025-01-22 04:42:38 +08:00
clientrest "k8s.io/client-go/rest"
2025-01-14 05:15:35 +08:00
"github.com/grafana/grafana/pkg/apimachinery/identity"
2025-01-22 12:45:59 +08:00
"github.com/grafana/grafana/pkg/apimachinery/utils"
2025-01-14 05:15:35 +08:00
"github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/log/logtest"
"github.com/grafana/grafana/pkg/infra/tracing"
internalfolders "github.com/grafana/grafana/pkg/registry/apis/folders"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
2025-02-12 03:14:25 +08:00
"github.com/grafana/grafana/pkg/services/apiserver/client"
2025-01-22 04:42:38 +08:00
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
2025-01-14 05:15:35 +08:00
"github.com/grafana/grafana/pkg/services/dashboards"
2025-02-12 03:14:25 +08:00
dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search"
2025-01-14 05:15:35 +08:00
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/guardian"
ngstore "github.com/grafana/grafana/pkg/services/ngalert/store"
2025-01-24 04:23:59 +08:00
"github.com/grafana/grafana/pkg/services/publicdashboards"
2025-01-30 07:44:42 +08:00
"github.com/grafana/grafana/pkg/services/search/model"
2025-02-19 02:30:11 +08:00
"github.com/grafana/grafana/pkg/services/search/sort"
2025-01-14 05:15:35 +08:00
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/user"
2025-01-29 00:46:07 +08:00
"github.com/grafana/grafana/pkg/services/user/usertest"
2025-02-19 22:50:39 +08:00
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
2025-01-22 12:45:59 +08:00
"github.com/grafana/grafana/pkg/storage/unified/resource"
2025-01-14 05:15:35 +08:00
)
2025-01-22 04:42:38 +08:00
type rcp struct {
Host string
}
func ( r rcp ) GetRestConfig ( ctx context . Context ) * clientrest . Config {
return & clientrest . Config {
Host : r . Host ,
}
}
2025-01-14 05:15:35 +08:00
func TestIntegrationFolderServiceViaUnifiedStorage ( t * testing . T ) {
if testing . Short ( ) {
t . Skip ( "skipping integration test" )
}
m := map [ string ] v0alpha1 . Folder { }
unifiedStorageFolder := & v0alpha1 . Folder { }
unifiedStorageFolder . Kind = "folder"
fooFolder := & folder . Folder {
2025-01-22 12:45:59 +08:00
ID : 123 ,
2025-01-14 05:15:35 +08:00
Title : "Foo Folder" ,
OrgID : orgID ,
UID : "foo" ,
URL : "/dashboards/f/foo/foo-folder" ,
CreatedByUID : "user:1" ,
UpdatedByUID : "user:1" ,
}
updateFolder := & folder . Folder {
Title : "Folder" ,
OrgID : orgID ,
UID : "updatefolder" ,
}
mux := http . NewServeMux ( )
mux . HandleFunc ( "DELETE /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/deletefolder" , func ( w http . ResponseWriter , req * http . Request ) {
w . Header ( ) . Set ( "Content-Type" , "application/json" )
} )
mux . HandleFunc ( "GET /apis/folder.grafana.app/v0alpha1/namespaces/default/folders" , func ( w http . ResponseWriter , req * http . Request ) {
w . Header ( ) . Set ( "Content-Type" , "application/json" )
l := & v0alpha1 . FolderList { }
l . Kind = "Folder"
err := json . NewEncoder ( w ) . Encode ( l )
require . NoError ( t , err )
} )
mux . HandleFunc ( "GET /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/foo" , func ( w http . ResponseWriter , req * http . Request ) {
w . Header ( ) . Set ( "Content-Type" , "application/json" )
namespacer := func ( _ int64 ) string { return "1" }
result , err := internalfolders . LegacyFolderToUnstructured ( fooFolder , namespacer )
require . NoError ( t , err )
err = json . NewEncoder ( w ) . Encode ( result )
require . NoError ( t , err )
} )
mux . HandleFunc ( "GET /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/updatefolder" , func ( w http . ResponseWriter , req * http . Request ) {
w . Header ( ) . Set ( "Content-Type" , "application/json" )
namespacer := func ( _ int64 ) string { return "1" }
result , err := internalfolders . LegacyFolderToUnstructured ( updateFolder , namespacer )
require . NoError ( t , err )
err = json . NewEncoder ( w ) . Encode ( result )
require . NoError ( t , err )
} )
mux . HandleFunc ( "PUT /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/updatefolder" , func ( w http . ResponseWriter , req * http . Request ) {
w . Header ( ) . Set ( "Content-Type" , "application/json" )
buf , err := io . ReadAll ( req . Body )
require . NoError ( t , err )
var foldr v0alpha1 . Folder
err = json . Unmarshal ( buf , & foldr )
require . NoError ( t , err )
updateFolder . Title = foldr . Spec . Title
namespacer := func ( _ int64 ) string { return "1" }
result , err := internalfolders . LegacyFolderToUnstructured ( updateFolder , namespacer )
require . NoError ( t , err )
err = json . NewEncoder ( w ) . Encode ( result )
require . NoError ( t , err )
} )
mux . HandleFunc ( "GET /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/ady4yobv315a8e" , func ( w http . ResponseWriter , req * http . Request ) {
w . Header ( ) . Set ( "Content-Type" , "application/json" )
err := json . NewEncoder ( w ) . Encode ( unifiedStorageFolder )
require . NoError ( t , err )
} )
mux . HandleFunc ( "PUT /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/ady4yobv315a8e" , func ( w http . ResponseWriter , req * http . Request ) {
w . Header ( ) . Set ( "Content-Type" , "application/json" )
err := json . NewEncoder ( w ) . Encode ( unifiedStorageFolder )
require . NoError ( t , err )
} )
mux . HandleFunc ( "POST /apis/folder.grafana.app/v0alpha1/namespaces/default/folders" , func ( w http . ResponseWriter , req * http . Request ) {
w . Header ( ) . Set ( "Content-Type" , "application/json" )
buf , err := io . ReadAll ( req . Body )
require . NoError ( t , err )
var folder v0alpha1 . Folder
err = json . Unmarshal ( buf , & folder )
require . NoError ( t , err )
m [ folder . Name ] = folder
fmt . Printf ( "buf: %+v\n" , folder )
folder . Kind = "Folder"
err = json . NewEncoder ( w ) . Encode ( folder )
require . NoError ( t , err )
} )
folderApiServerMock := httptest . NewServer ( mux )
defer folderApiServerMock . Close ( )
origNewGuardian := guardian . New
t . Cleanup ( func ( ) {
guardian . New = origNewGuardian
} )
db , cfg := sqlstore . InitTestDB ( t )
cfg . AppURL = folderApiServerMock . URL
2025-01-22 04:42:38 +08:00
restCfgProvider := rcp {
Host : folderApiServerMock . URL ,
}
2025-01-29 00:46:07 +08:00
userService := & usertest . FakeUserService {
ExpectedUser : & user . User { } ,
}
2025-02-12 03:14:25 +08:00
featuresArr := [ ] any {
2025-02-19 07:11:26 +08:00
featuremgmt . FlagKubernetesClientDashboardsFolders }
2025-02-12 03:14:25 +08:00
features := featuremgmt . WithFeatures ( featuresArr ... )
dashboardStore := dashboards . NewFakeDashboardStore ( t )
2025-02-19 22:50:39 +08:00
k8sCli := client . NewK8sHandler ( dualwrite . ProvideTestService ( ) , request . GetNamespaceMapper ( cfg ) , v0alpha1 . FolderResourceInfo . GroupVersionResource ( ) , restCfgProvider . GetRestConfig , dashboardStore , userService , nil , sort . ProvideService ( ) )
2025-02-12 03:14:25 +08:00
unifiedStore := ProvideUnifiedStore ( k8sCli , userService )
2025-01-14 05:15:35 +08:00
ctx := context . Background ( )
usr := & user . SignedInUser { UserID : 1 , OrgID : 1 , Permissions : map [ int64 ] map [ string ] [ ] string {
1 : accesscontrol . GroupScopesByActionContext (
ctx ,
[ ] accesscontrol . Permission {
{ Action : dashboards . ActionFoldersCreate , Scope : dashboards . ScopeFoldersAll } ,
{ Action : dashboards . ActionFoldersWrite , Scope : dashboards . ScopeFoldersAll } ,
{ Action : accesscontrol . ActionAlertingRuleDelete , Scope : dashboards . ScopeFoldersAll } ,
} ) ,
} }
alertingStore := ngstore . DBstore {
SQLStore : db ,
Cfg : cfg . UnifiedAlerting ,
Logger : log . New ( "test-alerting-store" ) ,
AccessControl : actest . FakeAccessControl { ExpectedEvaluate : true } ,
}
2025-01-24 04:23:59 +08:00
publicDashboardService := publicdashboards . NewFakePublicDashboardServiceWrapper ( t )
2025-01-14 05:15:35 +08:00
2025-02-19 07:11:26 +08:00
fakeK8sClient := new ( client . MockK8sHandler )
2025-01-14 05:15:35 +08:00
folderService := & Service {
2025-01-24 04:23:59 +08:00
log : slog . New ( logtest . NewTestHandler ( t ) ) . With ( "logger" , "test-folder-service" ) ,
unifiedStore : unifiedStore ,
features : features ,
bus : bus . ProvideBus ( tracing . InitializeTracerForTest ( ) ) ,
accessControl : acimpl . ProvideAccessControl ( features ) ,
registry : make ( map [ string ] folder . RegistryService ) ,
metrics : newFoldersMetrics ( nil ) ,
tracer : tracing . InitializeTracerForTest ( ) ,
2025-02-12 03:14:25 +08:00
k8sclient : k8sCli ,
2025-02-19 07:11:26 +08:00
dashboardK8sClient : fakeK8sClient ,
2025-01-24 04:23:59 +08:00
publicDashboardService : publicDashboardService ,
2025-01-14 05:15:35 +08:00
}
require . NoError ( t , folderService . RegisterService ( alertingStore ) )
t . Run ( "Folder service tests" , func ( t * testing . T ) {
t . Run ( "Given user has no permissions" , func ( t * testing . T ) {
origNewGuardian := guardian . New
guardian . MockDashboardGuardian ( & guardian . FakeDashboardGuardian { } )
ctx = identity . WithRequester ( context . Background ( ) , noPermUsr )
f := folder . NewFolder ( "Folder" , "" )
f . UID = "foo"
t . Run ( "When get folder by id should return access denied error" , func ( t * testing . T ) {
_ , err := folderService . Get ( ctx , & folder . GetFolderQuery {
UID : & f . UID ,
OrgID : orgID ,
SignedInUser : noPermUsr ,
} )
require . Equal ( t , dashboards . ErrFolderAccessDenied , err )
} )
t . Run ( "When get folder by uid should return access denied error" , func ( t * testing . T ) {
_ , err := folderService . Get ( ctx , & folder . GetFolderQuery {
UID : & f . UID ,
OrgID : orgID ,
SignedInUser : noPermUsr ,
} )
require . Equal ( t , dashboards . ErrFolderAccessDenied , err )
} )
t . Run ( "When creating folder should return access denied error" , func ( t * testing . T ) {
_ , err := folderService . Create ( ctx , & folder . CreateFolderCommand {
OrgID : orgID ,
Title : f . Title ,
UID : f . UID ,
SignedInUser : noPermUsr ,
} )
require . Error ( t , err )
} )
title := "Folder-TEST"
t . Run ( "When updating folder should return access denied error" , func ( t * testing . T ) {
_ , err := folderService . Update ( ctx , & folder . UpdateFolderCommand {
UID : f . UID ,
OrgID : orgID ,
NewTitle : & title ,
SignedInUser : noPermUsr ,
} )
require . Error ( t , err )
require . Equal ( t , dashboards . ErrFolderAccessDenied , err )
} )
t . Run ( "When deleting folder by uid should return access denied error" , func ( t * testing . T ) {
err := folderService . Delete ( ctx , & folder . DeleteFolderCommand {
UID : f . UID ,
OrgID : orgID ,
ForceDeleteRules : false ,
SignedInUser : noPermUsr ,
} )
require . Error ( t , err )
require . Equal ( t , dashboards . ErrFolderAccessDenied , err )
} )
t . Cleanup ( func ( ) {
guardian . New = origNewGuardian
} )
} )
t . Run ( "Given user has permission to save" , func ( t * testing . T ) {
origNewGuardian := guardian . New
guardian . MockDashboardGuardian ( & guardian . FakeDashboardGuardian { CanSaveValue : true , CanViewValue : true } )
ctx = identity . WithRequester ( context . Background ( ) , usr )
f := & folder . Folder {
OrgID : orgID ,
Title : "Test-Folder" ,
UID : "testfolder" ,
URL : "/dashboards/f/testfolder/test-folder" ,
CreatedByUID : "user:1" ,
UpdatedByUID : "user:1" ,
}
t . Run ( "When creating folder should not return access denied error" , func ( t * testing . T ) {
actualFolder , err := folderService . Create ( ctx , & folder . CreateFolderCommand {
OrgID : orgID ,
Title : f . Title ,
UID : f . UID ,
SignedInUser : usr ,
} )
require . NoError ( t , err )
require . Equal ( t , f , actualFolder )
} )
t . Run ( "When creating folder should return error if uid is general" , func ( t * testing . T ) {
_ , err := folderService . Create ( ctx , & folder . CreateFolderCommand {
OrgID : orgID ,
Title : f . Title ,
UID : "general" ,
SignedInUser : usr ,
} )
require . ErrorIs ( t , err , dashboards . ErrFolderInvalidUID )
} )
t . Run ( "When updating folder should not return access denied error" , func ( t * testing . T ) {
title := "TEST-Folder"
req := & folder . UpdateFolderCommand {
UID : updateFolder . UID ,
OrgID : orgID ,
NewTitle : & title ,
SignedInUser : usr ,
}
reqResult , err := folderService . Update ( ctx , req )
require . NoError ( t , err )
require . Equal ( t , title , reqResult . Title )
} )
t . Run ( "When deleting folder by uid should not return access denied error - ForceDeleteRules true" , func ( t * testing . T ) {
err := folderService . Delete ( ctx , & folder . DeleteFolderCommand {
UID : "deletefolder" ,
OrgID : orgID ,
ForceDeleteRules : true ,
SignedInUser : usr ,
} )
require . NoError ( t , err )
} )
t . Run ( "When deleting folder by uid should not return access denied error - ForceDeleteRules false" , func ( t * testing . T ) {
2025-02-19 07:11:26 +08:00
fakeK8sClient . On ( "Search" , mock . Anything , mock . Anything , mock . Anything ) . Return ( & resource . ResourceSearchResponse { Results : & resource . ResourceTable { } } , nil ) . Once ( )
2025-01-24 04:23:59 +08:00
publicDashboardService . On ( "DeleteByDashboardUIDs" , mock . Anything , mock . Anything , mock . Anything ) . Return ( nil )
2025-01-14 05:15:35 +08:00
err := folderService . Delete ( ctx , & folder . DeleteFolderCommand {
UID : "deletefolder" ,
OrgID : orgID ,
ForceDeleteRules : false ,
SignedInUser : usr ,
} )
require . NoError ( t , err )
} )
t . Run ( "When deleting folder by uid, expectedForceDeleteRules as false, and dashboard Restore turned on should not return access denied error" , func ( t * testing . T ) {
folderService . features = featuremgmt . WithFeatures ( append ( featuresArr , featuremgmt . FlagDashboardRestore ) ... )
2025-02-19 07:11:26 +08:00
fakeK8sClient . On ( "Search" , mock . Anything , mock . Anything , mock . Anything ) . Return ( & resource . ResourceSearchResponse { Results : & resource . ResourceTable { } } , nil ) . Once ( )
2025-01-14 05:15:35 +08:00
expectedForceDeleteRules := false
err := folderService . Delete ( ctx , & folder . DeleteFolderCommand {
UID : "deletefolder" ,
OrgID : orgID ,
ForceDeleteRules : expectedForceDeleteRules ,
SignedInUser : usr ,
} )
require . NoError ( t , err )
} )
t . Run ( "When deleting folder by uid, expectedForceDeleteRules as true, and dashboard Restore turned on should not return access denied error" , func ( t * testing . T ) {
folderService . features = featuremgmt . WithFeatures ( append ( featuresArr , featuremgmt . FlagDashboardRestore ) ... )
2025-02-19 07:11:26 +08:00
fakeK8sClient . On ( "Search" , mock . Anything , mock . Anything , mock . Anything ) . Return ( & resource . ResourceSearchResponse { Results : & resource . ResourceTable { } } , nil ) . Once ( )
2025-01-14 05:15:35 +08:00
expectedForceDeleteRules := true
err := folderService . Delete ( ctx , & folder . DeleteFolderCommand {
UID : "deletefolder" ,
OrgID : orgID ,
ForceDeleteRules : expectedForceDeleteRules ,
SignedInUser : usr ,
} )
require . NoError ( t , err )
} )
t . Cleanup ( func ( ) {
guardian . New = origNewGuardian
} )
} )
t . Run ( "Given user has permission to view" , func ( t * testing . T ) {
origNewGuardian := guardian . New
guardian . MockDashboardGuardian ( & guardian . FakeDashboardGuardian { CanViewValue : true } )
t . Run ( "When get folder by uid should return folder" , func ( t * testing . T ) {
actual , err := folderService . Get ( ctx , & folder . GetFolderQuery {
UID : & fooFolder . UID ,
OrgID : fooFolder . OrgID ,
SignedInUser : usr ,
} )
require . Equal ( t , fooFolder , actual )
require . NoError ( t , err )
} )
t . Run ( "When get folder by uid and uid is general should return the root folder object" , func ( t * testing . T ) {
uid := accesscontrol . GeneralFolderUID
query := & folder . GetFolderQuery {
UID : & uid ,
OrgID : 1 ,
SignedInUser : usr ,
}
actual , err := folderService . Get ( ctx , query )
require . Equal ( t , folder . RootFolder , actual )
require . NoError ( t , err )
} )
2025-01-29 03:00:57 +08:00
t . Run ( "When get folder by ID and uid is an empty string should return folder by id" , func ( t * testing . T ) {
2025-02-12 03:14:25 +08:00
dashboardStore . On ( "FindDashboards" , mock . Anything , mock . Anything ) . Return ( [ ] dashboards . DashboardSearchProjection {
{
IsFolder : true ,
ID : fooFolder . ID , // nolint:staticcheck
UID : fooFolder . UID ,
} ,
} , nil ) . Once ( )
2025-01-22 12:45:59 +08:00
id := int64 ( 123 )
2025-01-29 03:00:57 +08:00
emptyString := ""
2025-01-22 12:45:59 +08:00
query := & folder . GetFolderQuery {
2025-01-29 03:00:57 +08:00
UID : & emptyString ,
2025-01-22 12:45:59 +08:00
ID : & id ,
OrgID : 1 ,
SignedInUser : usr ,
}
actual , err := folderService . Get ( context . Background ( ) , query )
require . Equal ( t , fooFolder , actual )
require . NoError ( t , err )
} )
t . Run ( "When get folder by non existing ID should return not found error" , func ( t * testing . T ) {
2025-02-12 03:14:25 +08:00
dashboardStore . On ( "FindDashboards" , mock . Anything , mock . Anything ) . Return ( [ ] dashboards . DashboardSearchProjection { } , nil ) . Once ( )
2025-01-22 12:45:59 +08:00
id := int64 ( 111111 )
query := & folder . GetFolderQuery {
ID : & id ,
OrgID : 1 ,
SignedInUser : usr ,
}
actual , err := folderService . Get ( context . Background ( ) , query )
require . Nil ( t , actual )
require . ErrorIs ( t , err , dashboards . ErrFolderNotFound )
} )
2025-01-23 03:47:46 +08:00
t . Run ( "When get folder by Title should return folder" , func ( t * testing . T ) {
2025-02-12 03:14:25 +08:00
dashboardStore . On ( "FindDashboards" , mock . Anything , mock . Anything ) . Return ( [ ] dashboards . DashboardSearchProjection {
{
IsFolder : true ,
ID : fooFolder . ID , // nolint:staticcheck
UID : fooFolder . UID ,
} ,
} , nil ) . Once ( )
2025-01-23 03:47:46 +08:00
title := "foo"
query := & folder . GetFolderQuery {
Title : & title ,
OrgID : 1 ,
SignedInUser : usr ,
}
actual , err := folderService . Get ( context . Background ( ) , query )
require . Equal ( t , fooFolder , actual )
require . NoError ( t , err )
} )
t . Run ( "When get folder by non existing Title should return not found error" , func ( t * testing . T ) {
2025-02-12 03:14:25 +08:00
dashboardStore . On ( "FindDashboards" , mock . Anything , mock . Anything ) . Return ( [ ] dashboards . DashboardSearchProjection { } , nil ) . Once ( )
2025-01-23 03:47:46 +08:00
title := "does not exists"
query := & folder . GetFolderQuery {
Title : & title ,
OrgID : 1 ,
SignedInUser : usr ,
}
actual , err := folderService . Get ( context . Background ( ) , query )
require . Nil ( t , actual )
require . ErrorIs ( t , err , dashboards . ErrFolderNotFound )
} )
2025-01-14 05:15:35 +08:00
t . Cleanup ( func ( ) {
guardian . New = origNewGuardian
} )
} )
t . Run ( "Returns root folder" , func ( t * testing . T ) {
2025-01-29 03:00:57 +08:00
t . Run ( "When the folder UID and title are blank, and id is 0, should return the root folder" , func ( t * testing . T ) {
2025-01-14 05:15:35 +08:00
emptyString := ""
2025-01-29 03:00:57 +08:00
idZero := int64 ( 0 )
2025-01-14 05:15:35 +08:00
actual , err := folderService . Get ( ctx , & folder . GetFolderQuery {
UID : & emptyString ,
2025-01-29 03:00:57 +08:00
ID : & idZero ,
Title : & emptyString ,
2025-01-14 05:15:35 +08:00
OrgID : 1 ,
SignedInUser : usr ,
} )
require . NoError ( t , err )
require . Equal ( t , folder . GeneralFolder . UID , actual . UID )
require . Equal ( t , folder . GeneralFolder . Title , actual . Title )
} )
} )
} )
}
2025-01-22 12:45:59 +08:00
2025-02-12 03:14:25 +08:00
func TestSearchFoldersFromApiServer ( t * testing . T ) {
fakeK8sClient := new ( client . MockK8sHandler )
guardian . MockDashboardGuardian ( & guardian . FakeDashboardGuardian {
CanSaveValue : true ,
CanViewValue : true ,
} )
folderStore := folder . NewFakeStore ( )
folderStore . ExpectedFolder = & folder . Folder {
UID : "parent-uid" ,
ID : 2 ,
Title : "parent title" ,
}
service := Service {
k8sclient : fakeK8sClient ,
2025-02-19 07:11:26 +08:00
features : featuremgmt . WithFeatures ( featuremgmt . FlagKubernetesClientDashboardsFolders ) ,
2025-02-12 03:14:25 +08:00
unifiedStore : folderStore ,
}
user := & user . SignedInUser { OrgID : 1 }
ctx := identity . WithRequester ( context . Background ( ) , user )
fakeK8sClient . On ( "GetNamespace" , mock . Anything , mock . Anything ) . Return ( "default" )
2025-01-22 12:45:59 +08:00
2025-02-12 03:14:25 +08:00
t . Run ( "Should call search with uids, if provided" , func ( t * testing . T ) {
fakeK8sClient . On ( "Search" , mock . Anything , int64 ( 1 ) , & resource . ResourceSearchRequest {
Options : & resource . ListOptions {
Key : & resource . ResourceKey {
Namespace : "default" ,
Group : v0alpha1 . FolderResourceInfo . GroupVersionResource ( ) . Group ,
Resource : v0alpha1 . FolderResourceInfo . GroupVersionResource ( ) . Resource ,
2025-01-22 12:45:59 +08:00
} ,
2025-02-12 03:14:25 +08:00
Fields : [ ] * resource . Requirement {
2025-01-22 12:45:59 +08:00
{
2025-02-12 03:14:25 +08:00
Key : resource . SEARCH_FIELD_NAME ,
Operator : string ( selection . In ) ,
Values : [ ] string { "uid1" , "uid2" } , // should only search by uid since it is provided
2025-01-22 12:45:59 +08:00
} ,
} ,
2025-02-12 03:14:25 +08:00
Labels : [ ] * resource . Requirement { } ,
2025-01-22 12:45:59 +08:00
} ,
2025-02-20 02:06:26 +08:00
Limit : folderSearchLimit } ) . Return ( & resource . ResourceSearchResponse {
2025-01-23 03:47:46 +08:00
Results : & resource . ResourceTable {
Columns : [ ] * resource . ResourceTableColumnDefinition {
{
Name : "title" ,
Type : resource . ResourceTableColumnDefinition_STRING ,
} ,
{
Name : "folder" ,
Type : resource . ResourceTableColumnDefinition_STRING ,
} ,
} ,
Rows : [ ] * resource . ResourceTableRow {
{
Key : & resource . ResourceKey {
2025-02-12 03:14:25 +08:00
Name : "uid1" ,
Resource : "folder" ,
2025-01-23 03:47:46 +08:00
} ,
Cells : [ ] [ ] byte {
2025-02-12 03:14:25 +08:00
[ ] byte ( "folder0" ) ,
2025-01-23 03:47:46 +08:00
[ ] byte ( "" ) ,
} ,
} ,
2025-01-30 07:44:42 +08:00
{
Key : & resource . ResourceKey {
2025-02-12 03:14:25 +08:00
Name : "uid2" ,
Resource : "folder" ,
2025-01-30 07:44:42 +08:00
} ,
Cells : [ ] [ ] byte {
2025-02-12 03:14:25 +08:00
[ ] byte ( "folder1" ) ,
[ ] byte ( "" ) ,
2025-01-30 07:44:42 +08:00
} ,
} ,
} ,
} ,
2025-02-12 03:14:25 +08:00
TotalHits : 2 ,
} , nil ) . Once ( )
2025-01-30 07:44:42 +08:00
query := folder . SearchFoldersQuery {
UIDs : [ ] string { "uid1" , "uid2" } ,
IDs : [ ] int64 { 1 , 2 } , // will ignore these because uid is passed in
SignedInUser : user ,
}
result , err := service . searchFoldersFromApiServer ( ctx , query )
require . NoError ( t , err )
expectedResult := model . HitList {
{
UID : "uid1" ,
// no parent folder is returned, so the general folder should be set
FolderID : 0 ,
FolderTitle : "General" ,
// orgID should be taken from signed in user
OrgID : 1 ,
// the rest should be automatically set when parsing the hit results from search
Type : model . DashHitFolder ,
URI : "db/folder0" ,
Title : "folder0" ,
URL : "/dashboards/f/uid1/folder0" ,
} ,
{
UID : "uid2" ,
FolderID : 0 ,
FolderTitle : "General" ,
OrgID : 1 ,
Type : model . DashHitFolder ,
URI : "db/folder1" ,
Title : "folder1" ,
URL : "/dashboards/f/uid2/folder1" ,
} ,
}
require . Equal ( t , expectedResult , result )
2025-02-12 03:14:25 +08:00
fakeK8sClient . AssertExpectations ( t )
2025-01-30 07:44:42 +08:00
} )
2025-02-12 03:14:25 +08:00
t . Run ( "Should call search by ID if uids are not provided" , func ( t * testing . T ) {
2025-01-30 07:44:42 +08:00
query := folder . SearchFoldersQuery {
IDs : [ ] int64 { 123 } ,
SignedInUser : user ,
}
2025-02-12 03:14:25 +08:00
fakeK8sClient . On ( "Search" , mock . Anything , int64 ( 1 ) , & resource . ResourceSearchRequest {
Options : & resource . ListOptions {
Key : & resource . ResourceKey {
Namespace : "default" ,
Group : v0alpha1 . FolderResourceInfo . GroupVersionResource ( ) . Group ,
Resource : v0alpha1 . FolderResourceInfo . GroupVersionResource ( ) . Resource ,
} ,
Fields : [ ] * resource . Requirement { } ,
Labels : [ ] * resource . Requirement {
{
Key : utils . LabelKeyDeprecatedInternalID ,
Operator : string ( selection . In ) ,
Values : [ ] string { "123" } ,
} ,
} ,
} ,
2025-02-20 02:06:26 +08:00
Limit : folderSearchLimit } ) . Return ( & resource . ResourceSearchResponse {
2025-02-12 03:14:25 +08:00
Results : & resource . ResourceTable {
Columns : [ ] * resource . ResourceTableColumnDefinition {
{
Name : "title" ,
Type : resource . ResourceTableColumnDefinition_STRING ,
} ,
{
Name : "folder" ,
Type : resource . ResourceTableColumnDefinition_STRING ,
} ,
} ,
Rows : [ ] * resource . ResourceTableRow {
{
Key : & resource . ResourceKey {
Name : "foo" ,
Resource : "folder" ,
} ,
Cells : [ ] [ ] byte {
[ ] byte ( "folder1" ) ,
[ ] byte ( "" ) ,
} ,
} ,
} ,
} ,
TotalHits : 1 ,
} , nil ) . Once ( )
2025-01-30 07:44:42 +08:00
result , err := service . searchFoldersFromApiServer ( ctx , query )
require . NoError ( t , err )
expectedResult := model . HitList {
{
UID : "foo" ,
FolderID : 0 ,
FolderTitle : "General" ,
OrgID : 1 ,
Type : model . DashHitFolder ,
URI : "db/folder1" ,
Title : "folder1" ,
URL : "/dashboards/f/foo/folder1" ,
} ,
}
require . Equal ( t , expectedResult , result )
2025-02-12 03:14:25 +08:00
fakeK8sClient . AssertExpectations ( t )
2025-01-30 07:44:42 +08:00
} )
t . Run ( "Search by title, wildcard should be added to search request (won't match in search mock if not)" , func ( t * testing . T ) {
// the search here will return a parent, this will be the parent folder returned when we query for it to add to the hit info
fakeFolderStore := folder . NewFakeStore ( )
fakeFolderStore . ExpectedFolder = & folder . Folder {
UID : "parent-uid" ,
ID : 2 ,
Title : "parent title" ,
}
service . unifiedStore = fakeFolderStore
2025-02-12 03:14:25 +08:00
fakeK8sClient . On ( "Search" , mock . Anything , int64 ( 1 ) , & resource . ResourceSearchRequest {
Options : & resource . ListOptions {
Key : & resource . ResourceKey {
Namespace : "default" ,
Group : v0alpha1 . FolderResourceInfo . GroupVersionResource ( ) . Group ,
Resource : v0alpha1 . FolderResourceInfo . GroupVersionResource ( ) . Resource ,
} ,
Fields : [ ] * resource . Requirement { } ,
Labels : [ ] * resource . Requirement { } ,
} ,
Query : "*test*" ,
Fields : dashboardsearch . IncludeFields ,
2025-02-20 02:06:26 +08:00
Limit : folderSearchLimit } ) . Return ( & resource . ResourceSearchResponse {
2025-02-12 03:14:25 +08:00
Results : & resource . ResourceTable {
Columns : [ ] * resource . ResourceTableColumnDefinition {
{
Name : "title" ,
Type : resource . ResourceTableColumnDefinition_STRING ,
} ,
{
Name : "folder" ,
Type : resource . ResourceTableColumnDefinition_STRING ,
} ,
} ,
Rows : [ ] * resource . ResourceTableRow {
{
Key : & resource . ResourceKey {
Name : "uid" ,
Resource : "folder" ,
} ,
Cells : [ ] [ ] byte {
[ ] byte ( "testing-123" ) ,
[ ] byte ( "parent-uid" ) ,
} ,
} ,
} ,
} ,
TotalHits : 1 ,
} , nil ) . Once ( )
2025-01-30 07:44:42 +08:00
query := folder . SearchFoldersQuery {
Title : "test" ,
SignedInUser : user ,
}
result , err := service . searchFoldersFromApiServer ( ctx , query )
require . NoError ( t , err )
expectedResult := model . HitList {
{
UID : "uid" ,
FolderID : 2 ,
FolderTitle : "parent title" ,
FolderUID : "parent-uid" ,
OrgID : 1 ,
Type : model . DashHitFolder ,
URI : "db/testing-123" ,
Title : "testing-123" ,
URL : "/dashboards/f/uid/testing-123" ,
} ,
}
require . Equal ( t , expectedResult , result )
2025-02-12 03:14:25 +08:00
fakeK8sClient . AssertExpectations ( t )
2025-01-30 07:44:42 +08:00
} )
}
2025-02-04 12:38:57 +08:00
func TestDeleteFoldersFromApiServer ( t * testing . T ) {
2025-02-12 03:14:25 +08:00
fakeK8sClient := new ( client . MockK8sHandler )
fakeK8sClient . On ( "GetNamespace" , mock . Anything , mock . Anything ) . Return ( "default" )
dashboardK8sclient := new ( client . MockK8sHandler )
2025-02-04 12:38:57 +08:00
fakeFolderStore := folder . NewFakeStore ( )
dashboardStore := dashboards . NewFakeDashboardStore ( t )
publicDashboardFakeService := publicdashboards . NewFakePublicDashboardServiceWrapper ( t )
service := Service {
k8sclient : fakeK8sClient ,
2025-02-12 03:14:25 +08:00
dashboardK8sClient : dashboardK8sclient ,
2025-02-04 12:38:57 +08:00
unifiedStore : fakeFolderStore ,
dashboardStore : dashboardStore ,
publicDashboardService : publicDashboardFakeService ,
registry : make ( map [ string ] folder . RegistryService ) ,
2025-02-19 07:11:26 +08:00
features : featuremgmt . WithFeatures ( featuremgmt . FlagKubernetesClientDashboardsFolders ) ,
2025-02-04 12:38:57 +08:00
}
user := & user . SignedInUser { OrgID : 1 }
ctx := identity . WithRequester ( context . Background ( ) , user )
guardian . MockDashboardGuardian ( & guardian . FakeDashboardGuardian {
CanSaveValue : true ,
CanViewValue : true ,
} )
db , cfg := sqlstore . InitTestDB ( t )
alertingStore := ngstore . DBstore {
SQLStore : db ,
Cfg : cfg . UnifiedAlerting ,
Logger : log . New ( "test-alerting-store" ) ,
AccessControl : actest . FakeAccessControl { ExpectedEvaluate : true } ,
}
require . NoError ( t , service . RegisterService ( alertingStore ) )
t . Run ( "Should delete folder" , func ( t * testing . T ) {
2025-02-19 07:11:26 +08:00
publicDashboardFakeService . On ( "DeleteByDashboardUIDs" , mock . Anything , int64 ( 1 ) , [ ] string { } ) . Return ( nil ) . Once ( )
dashboardK8sclient . On ( "Search" , mock . Anything , int64 ( 1 ) , mock . Anything ) . Return ( & resource . ResourceSearchResponse { Results : & resource . ResourceTable { } } , nil ) . Once ( )
2025-02-04 12:38:57 +08:00
err := service . deleteFromApiServer ( ctx , & folder . DeleteFolderCommand {
2025-02-19 07:11:26 +08:00
UID : "uid1" ,
2025-02-04 12:38:57 +08:00
OrgID : 1 ,
SignedInUser : user ,
} )
require . NoError ( t , err )
2025-02-19 07:11:26 +08:00
dashboardK8sclient . AssertExpectations ( t )
2025-02-04 12:38:57 +08:00
publicDashboardFakeService . AssertExpectations ( t )
} )
2025-02-19 07:11:26 +08:00
t . Run ( "Should delete folders, dashboards, and public dashboards within the folder" , func ( t * testing . T ) {
fakeFolderStore . ExpectedFolders = [ ] * folder . Folder { { UID : "uid2" , ID : 2 } }
dashboardK8sclient . On ( "Delete" , mock . Anything , "test" , int64 ( 1 ) , mock . Anything ) . Return ( nil ) . Once ( )
dashboardK8sclient . On ( "Delete" , mock . Anything , "test2" , int64 ( 1 ) , mock . Anything ) . Return ( nil ) . Once ( )
2025-02-12 03:14:25 +08:00
dashboardK8sclient . On ( "Search" , mock . Anything , int64 ( 1 ) , & resource . ResourceSearchRequest {
Options : & resource . ListOptions {
Labels : [ ] * resource . Requirement { } ,
Fields : [ ] * resource . Requirement {
{
Key : resource . SEARCH_FIELD_FOLDER ,
Operator : string ( selection . In ) ,
2025-02-19 07:11:26 +08:00
Values : [ ] string { "uid" , "uid2" } ,
2025-02-12 03:14:25 +08:00
} ,
} ,
} ,
2025-02-20 02:06:26 +08:00
Limit : folderSearchLimit } ) . Return ( & resource . ResourceSearchResponse {
2025-02-12 03:14:25 +08:00
Results : & resource . ResourceTable {
Columns : [ ] * resource . ResourceTableColumnDefinition {
{
Name : "title" ,
Type : resource . ResourceTableColumnDefinition_STRING ,
} ,
{
Name : "folder" ,
Type : resource . ResourceTableColumnDefinition_STRING ,
} ,
} ,
Rows : [ ] * resource . ResourceTableRow {
{
Key : & resource . ResourceKey {
2025-02-19 07:11:26 +08:00
Name : "test" ,
Resource : "dashboard" ,
2025-02-12 03:14:25 +08:00
} ,
Cells : [ ] [ ] byte {
2025-02-19 07:11:26 +08:00
[ ] byte ( "uid" ) ,
[ ] byte ( "" ) ,
} ,
} ,
{
Key : & resource . ResourceKey {
Name : "test2" ,
Resource : "dashboard" ,
} ,
Cells : [ ] [ ] byte {
[ ] byte ( "uid2" ) ,
2025-02-12 03:14:25 +08:00
[ ] byte ( "" ) ,
} ,
} ,
} ,
} ,
TotalHits : 1 ,
} , nil ) . Once ( )
2025-02-19 07:11:26 +08:00
publicDashboardFakeService . On ( "DeleteByDashboardUIDs" , mock . Anything , int64 ( 1 ) , [ ] string { "test" , "test2" } ) . Return ( nil ) . Once ( )
2025-02-04 12:38:57 +08:00
err := service . deleteFromApiServer ( ctx , & folder . DeleteFolderCommand {
2025-02-19 07:11:26 +08:00
UID : "uid" ,
2025-02-04 12:38:57 +08:00
OrgID : 1 ,
SignedInUser : user ,
} )
require . NoError ( t , err )
2025-02-19 07:11:26 +08:00
dashboardStore . AssertExpectations ( t )
2025-02-04 12:38:57 +08:00
publicDashboardFakeService . AssertExpectations ( t )
} )
}