1161 lines
47 KiB
Go
1161 lines
47 KiB
Go
package delete
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
|
|
|
v0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
|
"github.com/grafana/grafana/apps/provisioning/pkg/repository"
|
|
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
|
|
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type mockReaderWriter struct {
|
|
*repository.MockRepository
|
|
}
|
|
|
|
func (m *mockReaderWriter) Delete(ctx context.Context, path, ref, message string) error {
|
|
args := m.Called(ctx, path, ref, message)
|
|
return args.Error(0)
|
|
}
|
|
|
|
// simpleRepository implements only the base Repository interface, not ReaderWriter
|
|
type simpleRepository struct{}
|
|
|
|
func (s *simpleRepository) Config() *v0alpha1.Repository { return nil }
|
|
func (s *simpleRepository) Validate() field.ErrorList { return nil }
|
|
func (s *simpleRepository) Test(ctx context.Context) (*v0alpha1.TestResults, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func TestDeleteWorker_IsSupported(t *testing.T) {
|
|
metrics := jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())
|
|
tests := []struct {
|
|
name string
|
|
job v0alpha1.Job
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "delete action is supported",
|
|
job: v0alpha1.Job{
|
|
Spec: v0alpha1.JobSpec{
|
|
Action: v0alpha1.JobActionDelete,
|
|
},
|
|
},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "pull action is not supported",
|
|
job: v0alpha1.Job{
|
|
Spec: v0alpha1.JobSpec{
|
|
Action: v0alpha1.JobActionPull,
|
|
},
|
|
},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "push action is not supported",
|
|
job: v0alpha1.Job{
|
|
Spec: v0alpha1.JobSpec{
|
|
Action: v0alpha1.JobActionPush,
|
|
},
|
|
},
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
worker := NewWorker(nil, nil, nil, metrics)
|
|
result := worker.IsSupported(context.Background(), tt.job)
|
|
require.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeleteWorker_ProcessMissingDeleteSettings(t *testing.T) {
|
|
job := v0alpha1.Job{
|
|
Spec: v0alpha1.JobSpec{
|
|
Action: v0alpha1.JobActionDelete,
|
|
},
|
|
}
|
|
|
|
worker := NewWorker(nil, nil, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()))
|
|
err := worker.Process(context.Background(), nil, job, nil)
|
|
require.EqualError(t, err, "missing delete settings")
|
|
}
|
|
|
|
func TestDeleteWorker_ProcessNotReaderWriter(t *testing.T) {
|
|
job := v0alpha1.Job{
|
|
Spec: v0alpha1.JobSpec{
|
|
Action: v0alpha1.JobActionDelete,
|
|
Delete: &v0alpha1.DeleteJobOptions{
|
|
Paths: []string{"test/path"},
|
|
},
|
|
},
|
|
}
|
|
|
|
mockRepo := repository.NewMockRepository(t)
|
|
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
|
mockWrapFn := repository.NewMockWrapWithStageFn(t)
|
|
|
|
mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.MatchedBy(func(opts repository.StageOptions) bool {
|
|
return !opts.PushOnWrites &&
|
|
opts.Timeout == 10*time.Minute &&
|
|
opts.Mode == repository.StageModeCommitOnlyOnce &&
|
|
opts.CommitOnlyOnceMessage == "Delete from Grafana "+job.Name
|
|
}), mock.Anything).Return(errors.New("delete job submitted targeting repository that is not a ReaderWriter"))
|
|
|
|
mockProgress.On("SetTotal", mock.Anything, 1).Return()
|
|
mockProgress.On("StrictMaxErrors", 1).Return()
|
|
|
|
worker := NewWorker(nil, mockWrapFn.Execute, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()))
|
|
err := worker.Process(context.Background(), mockRepo, job, mockProgress)
|
|
require.EqualError(t, err, "delete files from repository: delete job submitted targeting repository that is not a ReaderWriter")
|
|
}
|
|
|
|
func TestDeleteWorker_ProcessWrapFnError(t *testing.T) {
|
|
job := v0alpha1.Job{
|
|
Spec: v0alpha1.JobSpec{
|
|
Action: v0alpha1.JobActionDelete,
|
|
Delete: &v0alpha1.DeleteJobOptions{
|
|
Paths: []string{"test/path"},
|
|
},
|
|
},
|
|
}
|
|
|
|
mockRepo := repository.NewMockRepository(t)
|
|
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
|
mockWrapFn := repository.NewMockWrapWithStageFn(t)
|
|
|
|
mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(errors.New("stage failed"))
|
|
mockProgress.On("SetTotal", mock.Anything, 1).Return()
|
|
mockProgress.On("StrictMaxErrors", 1).Return()
|
|
|
|
worker := NewWorker(nil, mockWrapFn.Execute, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()))
|
|
err := worker.Process(context.Background(), mockRepo, job, mockProgress)
|
|
require.EqualError(t, err, "delete files from repository: stage failed")
|
|
}
|
|
|
|
func TestDeleteWorker_ProcessDeleteFilesSuccess(t *testing.T) {
|
|
job := v0alpha1.Job{
|
|
Spec: v0alpha1.JobSpec{
|
|
Action: v0alpha1.JobActionDelete,
|
|
Delete: &v0alpha1.DeleteJobOptions{
|
|
Paths: []string{"test/path1", "test/path2"},
|
|
Ref: "main",
|
|
},
|
|
},
|
|
}
|
|
|
|
mockRepo := &mockReaderWriter{
|
|
MockRepository: repository.NewMockRepository(t),
|
|
}
|
|
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
|
mockWrapFn := repository.NewMockWrapWithStageFn(t)
|
|
|
|
mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.MatchedBy(func(opts repository.StageOptions) bool {
|
|
return !opts.PushOnWrites &&
|
|
opts.Timeout == 10*time.Minute &&
|
|
opts.Mode == repository.StageModeCommitOnlyOnce &&
|
|
opts.Ref == "main" &&
|
|
opts.CommitOnlyOnceMessage == "Delete from Grafana "+job.Name
|
|
}), mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error {
|
|
return fn(mockRepo, false)
|
|
})
|
|
|
|
mockProgress.On("SetTotal", mock.Anything, 2).Return()
|
|
mockProgress.On("StrictMaxErrors", 1).Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Deleting test/path1").Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Deleting test/path2").Return()
|
|
mockProgress.On("TooManyErrors").Return(nil).Twice()
|
|
mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{})
|
|
|
|
mockRepo.On("Delete", mock.Anything, "test/path1", "main", "Delete test/path1").Return(nil)
|
|
mockRepo.On("Delete", mock.Anything, "test/path2", "main", "Delete test/path2").Return(nil)
|
|
|
|
mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Path == "test/path1" && result.Action == repository.FileActionDeleted && result.Error == nil
|
|
})).Return()
|
|
mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Path == "test/path2" && result.Action == repository.FileActionDeleted && result.Error == nil
|
|
})).Return()
|
|
|
|
worker := NewWorker(nil, mockWrapFn.Execute, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()))
|
|
err := worker.Process(context.Background(), mockRepo, job, mockProgress)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestDeleteWorker_ProcessDeleteFilesWithError(t *testing.T) {
|
|
job := v0alpha1.Job{
|
|
Spec: v0alpha1.JobSpec{
|
|
Action: v0alpha1.JobActionDelete,
|
|
Delete: &v0alpha1.DeleteJobOptions{
|
|
Paths: []string{"test/path1", "test/path2"},
|
|
Ref: "main",
|
|
},
|
|
},
|
|
}
|
|
|
|
mockRepo := &mockReaderWriter{
|
|
MockRepository: repository.NewMockRepository(t),
|
|
}
|
|
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
|
mockWrapFn := repository.NewMockWrapWithStageFn(t)
|
|
|
|
mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error {
|
|
return fn(mockRepo, false)
|
|
})
|
|
|
|
mockProgress.On("SetTotal", mock.Anything, 2).Return()
|
|
mockProgress.On("StrictMaxErrors", 1).Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Deleting test/path1").Return()
|
|
|
|
deleteError := errors.New("delete failed")
|
|
mockRepo.On("Delete", mock.Anything, "test/path1", "main", "Delete test/path1").Return(deleteError)
|
|
|
|
mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Path == "test/path1" && result.Action == repository.FileActionDeleted && errors.Is(result.Error, deleteError)
|
|
})).Return()
|
|
mockProgress.On("TooManyErrors").Return(errors.New("too many errors"))
|
|
|
|
worker := NewWorker(nil, mockWrapFn.Execute, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()))
|
|
err := worker.Process(context.Background(), mockRepo, job, mockProgress)
|
|
require.EqualError(t, err, "delete files from repository: too many errors")
|
|
}
|
|
|
|
func TestDeleteWorker_ProcessWithSyncWorker(t *testing.T) {
|
|
job := v0alpha1.Job{
|
|
Spec: v0alpha1.JobSpec{
|
|
Action: v0alpha1.JobActionDelete,
|
|
Delete: &v0alpha1.DeleteJobOptions{
|
|
Paths: []string{"test/path"},
|
|
},
|
|
},
|
|
}
|
|
|
|
mockRepo := &mockReaderWriter{
|
|
MockRepository: repository.NewMockRepository(t),
|
|
}
|
|
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
|
mockSyncWorker := jobs.NewMockWorker(t)
|
|
mockWrapFn := repository.NewMockWrapWithStageFn(t)
|
|
|
|
mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error {
|
|
return fn(mockRepo, false)
|
|
})
|
|
|
|
mockProgress.On("SetTotal", mock.Anything, 1).Return()
|
|
mockProgress.On("StrictMaxErrors", 1).Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Deleting test/path").Return()
|
|
mockProgress.On("TooManyErrors").Return(nil)
|
|
mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{})
|
|
|
|
mockRepo.On("Delete", mock.Anything, "test/path", "", "Delete test/path").Return(nil)
|
|
|
|
mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Path == "test/path" && result.Action == repository.FileActionDeleted && result.Error == nil
|
|
})).Return()
|
|
|
|
mockProgress.On("ResetResults").Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "pull resources").Return()
|
|
|
|
mockSyncWorker.On("Process", mock.Anything, mockRepo, mock.MatchedBy(func(syncJob v0alpha1.Job) bool {
|
|
return syncJob.Spec.Pull != nil && !syncJob.Spec.Pull.Incremental
|
|
}), mockProgress).Return(nil)
|
|
|
|
worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()))
|
|
err := worker.Process(context.Background(), mockRepo, job, mockProgress)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestDeleteWorker_ProcessSyncWorkerError(t *testing.T) {
|
|
job := v0alpha1.Job{
|
|
Spec: v0alpha1.JobSpec{
|
|
Action: v0alpha1.JobActionDelete,
|
|
Delete: &v0alpha1.DeleteJobOptions{
|
|
Paths: []string{"test/path"},
|
|
},
|
|
},
|
|
}
|
|
|
|
mockRepo := &mockReaderWriter{
|
|
MockRepository: repository.NewMockRepository(t),
|
|
}
|
|
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
|
mockSyncWorker := jobs.NewMockWorker(t)
|
|
mockWrapFn := repository.NewMockWrapWithStageFn(t)
|
|
|
|
mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error {
|
|
return fn(mockRepo, false)
|
|
})
|
|
|
|
mockProgress.On("SetTotal", mock.Anything, 1).Return()
|
|
mockProgress.On("StrictMaxErrors", 1).Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Deleting test/path").Return()
|
|
mockProgress.On("TooManyErrors").Return(nil)
|
|
|
|
mockRepo.On("Delete", mock.Anything, "test/path", "", "Delete test/path").Return(nil)
|
|
|
|
mockProgress.On("Record", mock.Anything, mock.Anything).Return()
|
|
mockProgress.On("ResetResults").Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "pull resources").Return()
|
|
|
|
syncError := errors.New("sync failed")
|
|
mockSyncWorker.On("Process", mock.Anything, mockRepo, mock.Anything, mockProgress).Return(syncError)
|
|
|
|
worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()))
|
|
err := worker.Process(context.Background(), mockRepo, job, mockProgress)
|
|
require.EqualError(t, err, "pull resources: sync failed")
|
|
}
|
|
|
|
func TestDeleteWorker_deleteFiles(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
paths []string
|
|
deleteResults []error
|
|
tooManyErrors error
|
|
expectedError string
|
|
expectedCalls int
|
|
}{
|
|
{
|
|
name: "single file success",
|
|
paths: []string{"test/file1.yaml"},
|
|
deleteResults: []error{nil},
|
|
expectedCalls: 1,
|
|
},
|
|
{
|
|
name: "multiple files success",
|
|
paths: []string{"test/file1.yaml", "test/file2.yaml", "test/file3.yaml"},
|
|
deleteResults: []error{nil, nil, nil},
|
|
expectedCalls: 3,
|
|
},
|
|
{
|
|
name: "single file with error continues",
|
|
paths: []string{"test/file1.yaml", "test/file2.yaml"},
|
|
deleteResults: []error{errors.New("delete failed"), nil},
|
|
expectedCalls: 2,
|
|
},
|
|
{
|
|
name: "too many errors stops processing",
|
|
paths: []string{"test/file1.yaml", "test/file2.yaml", "test/file3.yaml"},
|
|
deleteResults: []error{errors.New("delete failed")},
|
|
tooManyErrors: errors.New("too many errors"),
|
|
expectedError: "too many errors",
|
|
expectedCalls: 1,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockRepo := &mockReaderWriter{
|
|
MockRepository: repository.NewMockRepository(t),
|
|
}
|
|
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
|
|
|
opts := v0alpha1.DeleteJobOptions{
|
|
Ref: "main",
|
|
}
|
|
|
|
for i, path := range tt.paths {
|
|
if i < len(tt.deleteResults) {
|
|
mockRepo.On("Delete", mock.Anything, path, "main", "Delete "+path).Return(tt.deleteResults[i]).Once()
|
|
mockProgress.On("SetMessage", mock.Anything, "Deleting "+path).Return().Once()
|
|
mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Path == path && result.Action == repository.FileActionDeleted
|
|
})).Return().Once()
|
|
|
|
if tt.tooManyErrors != nil && i == 0 {
|
|
mockProgress.On("TooManyErrors").Return(tt.tooManyErrors).Once()
|
|
} else {
|
|
mockProgress.On("TooManyErrors").Return(nil).Once()
|
|
}
|
|
}
|
|
}
|
|
|
|
worker := NewWorker(nil, nil, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()))
|
|
err := worker.deleteFiles(context.Background(), mockRepo, mockProgress, opts, tt.paths...)
|
|
|
|
if tt.expectedError != "" {
|
|
require.EqualError(t, err, tt.expectedError)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
mockRepo.AssertExpectations(t)
|
|
mockProgress.AssertExpectations(t)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeleteWorker_ProcessWithResourceRefs(t *testing.T) {
|
|
job := v0alpha1.Job{
|
|
Spec: v0alpha1.JobSpec{
|
|
Action: v0alpha1.JobActionDelete,
|
|
Delete: &v0alpha1.DeleteJobOptions{
|
|
Paths: []string{"test/path1"},
|
|
Resources: []v0alpha1.ResourceRef{
|
|
{
|
|
Name: "test-dashboard",
|
|
Kind: "Dashboard",
|
|
Group: "dashboard.grafana.app",
|
|
},
|
|
{
|
|
Name: "test-folder",
|
|
Kind: "Folder",
|
|
Group: "folder.grafana.app",
|
|
},
|
|
},
|
|
Ref: "main",
|
|
},
|
|
},
|
|
}
|
|
|
|
mockRepo := &mockReaderWriter{
|
|
MockRepository: repository.NewMockRepository(t),
|
|
}
|
|
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
|
mockWrapFn := repository.NewMockWrapWithStageFn(t)
|
|
mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t)
|
|
mockRepositoryResources := resources.NewMockRepositoryResources(t)
|
|
|
|
// Mock repository resources factory and client
|
|
mockResourcesFactory.On("Client", mock.Anything, mockRepo).Return(mockRepositoryResources, nil)
|
|
|
|
// Mock FindResourcePath calls
|
|
mockRepositoryResources.On("FindResourcePath", mock.Anything, "test-dashboard", schema.GroupVersionKind{
|
|
Group: "dashboard.grafana.app",
|
|
Kind: "Dashboard",
|
|
// Version is empty - ForKind will discover the preferred version
|
|
}).Return("dashboards/test-dashboard.json", nil)
|
|
|
|
mockRepositoryResources.On("FindResourcePath", mock.Anything, "test-folder", schema.GroupVersionKind{
|
|
Group: "folder.grafana.app",
|
|
Kind: "Folder",
|
|
// Version is empty - ForKind will discover the preferred version
|
|
}).Return("folders/test-folder.json", nil)
|
|
|
|
mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.MatchedBy(func(opts repository.StageOptions) bool {
|
|
return !opts.PushOnWrites && opts.Timeout == 10*time.Minute
|
|
}), mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error {
|
|
return fn(mockRepo, false)
|
|
})
|
|
|
|
// Progress tracking - expects 3 total (1 path + 2 resources)
|
|
mockProgress.On("SetTotal", mock.Anything, 3).Return()
|
|
mockProgress.On("StrictMaxErrors", 1).Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Finding path for resource dashboard.grafana.app/Dashboard/test-dashboard").Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Finding path for resource folder.grafana.app/Folder/test-folder").Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Deleting test/path1").Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Deleting dashboards/test-dashboard.json").Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Deleting folders/test-folder.json").Return()
|
|
mockProgress.On("TooManyErrors").Return(nil).Times(3)
|
|
mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{})
|
|
|
|
// Mock file deletions
|
|
mockRepo.On("Delete", mock.Anything, "test/path1", "main", "Delete test/path1").Return(nil)
|
|
mockRepo.On("Delete", mock.Anything, "dashboards/test-dashboard.json", "main", "Delete dashboards/test-dashboard.json").Return(nil)
|
|
mockRepo.On("Delete", mock.Anything, "folders/test-folder.json", "main", "Delete folders/test-folder.json").Return(nil)
|
|
|
|
// Mock progress records
|
|
mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Path == "test/path1" && result.Action == repository.FileActionDeleted && result.Error == nil
|
|
})).Return()
|
|
mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Path == "dashboards/test-dashboard.json" && result.Action == repository.FileActionDeleted && result.Error == nil
|
|
})).Return()
|
|
mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Path == "folders/test-folder.json" && result.Action == repository.FileActionDeleted && result.Error == nil
|
|
})).Return()
|
|
|
|
worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()))
|
|
err := worker.Process(context.Background(), mockRepo, job, mockProgress)
|
|
require.NoError(t, err)
|
|
|
|
mockResourcesFactory.AssertExpectations(t)
|
|
mockRepositoryResources.AssertExpectations(t)
|
|
}
|
|
|
|
func TestDeleteWorker_ProcessResourceRefsOnly(t *testing.T) {
|
|
job := v0alpha1.Job{
|
|
Spec: v0alpha1.JobSpec{
|
|
Action: v0alpha1.JobActionDelete,
|
|
Delete: &v0alpha1.DeleteJobOptions{
|
|
Resources: []v0alpha1.ResourceRef{
|
|
{
|
|
Name: "test-dashboard",
|
|
Kind: "Dashboard",
|
|
Group: "dashboard.grafana.app",
|
|
},
|
|
},
|
|
Ref: "main",
|
|
},
|
|
},
|
|
}
|
|
|
|
mockRepo := &mockReaderWriter{
|
|
MockRepository: repository.NewMockRepository(t),
|
|
}
|
|
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
|
mockWrapFn := repository.NewMockWrapWithStageFn(t)
|
|
mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t)
|
|
mockRepositoryResources := resources.NewMockRepositoryResources(t)
|
|
|
|
mockResourcesFactory.On("Client", mock.Anything, mockRepo).Return(mockRepositoryResources, nil)
|
|
|
|
mockRepositoryResources.On("FindResourcePath", mock.Anything, "test-dashboard", schema.GroupVersionKind{
|
|
Group: "dashboard.grafana.app",
|
|
Kind: "Dashboard",
|
|
// Version is empty - ForKind will discover the preferred version
|
|
}).Return("dashboards/test-dashboard.json", nil)
|
|
|
|
mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error {
|
|
return fn(mockRepo, false)
|
|
})
|
|
|
|
mockProgress.On("SetTotal", mock.Anything, 1).Return()
|
|
mockProgress.On("StrictMaxErrors", 1).Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Finding path for resource dashboard.grafana.app/Dashboard/test-dashboard").Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Deleting dashboards/test-dashboard.json").Return()
|
|
mockProgress.On("TooManyErrors").Return(nil)
|
|
mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{})
|
|
|
|
mockRepo.On("Delete", mock.Anything, "dashboards/test-dashboard.json", "main", "Delete dashboards/test-dashboard.json").Return(nil)
|
|
|
|
mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Path == "dashboards/test-dashboard.json" && result.Action == repository.FileActionDeleted && result.Error == nil
|
|
})).Return()
|
|
|
|
worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()))
|
|
err := worker.Process(context.Background(), mockRepo, job, mockProgress)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestDeleteWorker_ProcessResourceResolutionError(t *testing.T) {
|
|
job := v0alpha1.Job{
|
|
Spec: v0alpha1.JobSpec{
|
|
Action: v0alpha1.JobActionDelete,
|
|
Delete: &v0alpha1.DeleteJobOptions{
|
|
Resources: []v0alpha1.ResourceRef{
|
|
{
|
|
Name: "nonexistent-dashboard",
|
|
Kind: "Dashboard",
|
|
Group: "dashboard.grafana.app",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
mockRepo := &mockReaderWriter{
|
|
MockRepository: repository.NewMockRepository(t),
|
|
}
|
|
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
|
mockWrapFn := repository.NewMockWrapWithStageFn(t)
|
|
mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t)
|
|
mockRepositoryResources := resources.NewMockRepositoryResources(t)
|
|
|
|
mockResourcesFactory.On("Client", mock.Anything, mockRepo).Return(mockRepositoryResources, nil)
|
|
|
|
findPathError := errors.New("resource not found in repository: dashboard.grafana.app/dashboards/nonexistent-dashboard")
|
|
mockRepositoryResources.On("FindResourcePath", mock.Anything, "nonexistent-dashboard", schema.GroupVersionKind{
|
|
Group: "dashboard.grafana.app",
|
|
Kind: "Dashboard",
|
|
// Version is empty - ForKind will discover the preferred version
|
|
}).Return("", findPathError)
|
|
|
|
mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error {
|
|
return fn(mockRepo, false)
|
|
})
|
|
|
|
mockProgress.On("SetTotal", mock.Anything, 1).Return()
|
|
mockProgress.On("StrictMaxErrors", 1).Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Finding path for resource dashboard.grafana.app/Dashboard/nonexistent-dashboard").Return()
|
|
mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{})
|
|
|
|
// Expect error to be recorded, not thrown
|
|
mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Name == "nonexistent-dashboard" &&
|
|
result.Group == "dashboard.grafana.app" &&
|
|
result.Action == repository.FileActionDeleted &&
|
|
result.Error != nil
|
|
})).Return()
|
|
mockProgress.On("TooManyErrors").Return(nil)
|
|
|
|
// Mock sync worker behavior that happens when no ref is specified
|
|
mockProgress.On("ResetResults").Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "pull resources").Return()
|
|
|
|
mockSyncWorker := jobs.NewMockWorker(t)
|
|
mockSyncWorker.On("Process", mock.Anything, mockRepo, mock.MatchedBy(func(syncJob v0alpha1.Job) bool {
|
|
return syncJob.Spec.Pull != nil && !syncJob.Spec.Pull.Incremental
|
|
}), mockProgress).Return(nil)
|
|
|
|
worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()))
|
|
err := worker.Process(context.Background(), mockRepo, job, mockProgress)
|
|
require.NoError(t, err) // Should succeed even with resource resolution error
|
|
}
|
|
|
|
func TestDeleteWorker_ProcessResourcesFactoryError(t *testing.T) {
|
|
job := v0alpha1.Job{
|
|
Spec: v0alpha1.JobSpec{
|
|
Action: v0alpha1.JobActionDelete,
|
|
Delete: &v0alpha1.DeleteJobOptions{
|
|
Resources: []v0alpha1.ResourceRef{
|
|
{
|
|
Name: "test-dashboard",
|
|
Kind: "Dashboard",
|
|
Group: "dashboard.grafana.app",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
mockRepo := &mockReaderWriter{
|
|
MockRepository: repository.NewMockRepository(t),
|
|
}
|
|
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
|
mockWrapFn := repository.NewMockWrapWithStageFn(t)
|
|
mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t)
|
|
|
|
factoryError := errors.New("failed to create repository resources client")
|
|
mockResourcesFactory.On("Client", mock.Anything, mockRepo).Return(nil, factoryError)
|
|
|
|
mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error {
|
|
return fn(mockRepo, false)
|
|
})
|
|
|
|
mockProgress.On("SetTotal", mock.Anything, 1).Return()
|
|
mockProgress.On("StrictMaxErrors", 1).Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return()
|
|
|
|
worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()))
|
|
err := worker.Process(context.Background(), mockRepo, job, mockProgress)
|
|
require.EqualError(t, err, "delete files from repository: create repository resources client: failed to create repository resources client")
|
|
}
|
|
|
|
func TestDeleteWorker_ProcessResourceRefsNotReaderWriter(t *testing.T) {
|
|
job := v0alpha1.Job{
|
|
Spec: v0alpha1.JobSpec{
|
|
Action: v0alpha1.JobActionDelete,
|
|
Delete: &v0alpha1.DeleteJobOptions{
|
|
Resources: []v0alpha1.ResourceRef{
|
|
{
|
|
Name: "test-dashboard",
|
|
Kind: "Dashboard",
|
|
Group: "dashboard.grafana.app",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Create a simple repository that doesn't implement ReaderWriter
|
|
mockRepo := &simpleRepository{}
|
|
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
|
mockWrapFn := repository.NewMockWrapWithStageFn(t)
|
|
mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t)
|
|
|
|
// Mock the wrap function that will call our function and get the ReaderWriter error
|
|
mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error {
|
|
return fn(mockRepo, false)
|
|
})
|
|
|
|
// The ReaderWriter check should fail immediately, so no resource resolution calls should happen
|
|
mockProgress.On("SetTotal", mock.Anything, 1).Return()
|
|
mockProgress.On("StrictMaxErrors", 1).Return()
|
|
|
|
worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()))
|
|
err := worker.Process(context.Background(), mockRepo, job, mockProgress)
|
|
require.EqualError(t, err, "delete files from repository: delete job submitted targeting repository that is not a ReaderWriter")
|
|
}
|
|
|
|
func TestDeleteWorker_ProcessResourceResolutionTooManyErrors(t *testing.T) {
|
|
job := v0alpha1.Job{
|
|
Spec: v0alpha1.JobSpec{
|
|
Action: v0alpha1.JobActionDelete,
|
|
Delete: &v0alpha1.DeleteJobOptions{
|
|
Resources: []v0alpha1.ResourceRef{
|
|
{
|
|
Name: "nonexistent-dashboard",
|
|
Kind: "Dashboard",
|
|
Group: "dashboard.grafana.app",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
mockRepo := &mockReaderWriter{
|
|
MockRepository: repository.NewMockRepository(t),
|
|
}
|
|
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
|
mockWrapFn := repository.NewMockWrapWithStageFn(t)
|
|
mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t)
|
|
mockRepositoryResources := resources.NewMockRepositoryResources(t)
|
|
|
|
mockResourcesFactory.On("Client", mock.Anything, mockRepo).Return(mockRepositoryResources, nil)
|
|
|
|
findPathError := errors.New("resource not found in repository")
|
|
mockRepositoryResources.On("FindResourcePath", mock.Anything, "nonexistent-dashboard", schema.GroupVersionKind{
|
|
Group: "dashboard.grafana.app",
|
|
Kind: "Dashboard",
|
|
// Version is empty - ForKind will discover the preferred version
|
|
}).Return("", findPathError)
|
|
|
|
mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error {
|
|
return fn(mockRepo, false)
|
|
})
|
|
|
|
mockProgress.On("SetTotal", mock.Anything, 1).Return()
|
|
mockProgress.On("StrictMaxErrors", 1).Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Finding path for resource dashboard.grafana.app/Dashboard/nonexistent-dashboard").Return()
|
|
|
|
// Mock recording error and TooManyErrors returning error
|
|
mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Name == "nonexistent-dashboard" && result.Error != nil
|
|
})).Return()
|
|
mockProgress.On("TooManyErrors").Return(errors.New("too many errors"))
|
|
|
|
worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()))
|
|
err := worker.Process(context.Background(), mockRepo, job, mockProgress)
|
|
require.EqualError(t, err, "delete files from repository: too many errors")
|
|
}
|
|
|
|
func TestDeleteWorker_ProcessMixedResourcesWithPartialFailure(t *testing.T) {
|
|
job := v0alpha1.Job{
|
|
Spec: v0alpha1.JobSpec{
|
|
Action: v0alpha1.JobActionDelete,
|
|
Delete: &v0alpha1.DeleteJobOptions{
|
|
Resources: []v0alpha1.ResourceRef{
|
|
{
|
|
Name: "valid-dashboard",
|
|
Kind: "Dashboard",
|
|
Group: "dashboard.grafana.app",
|
|
},
|
|
{
|
|
Name: "nonexistent-dashboard",
|
|
Kind: "Dashboard",
|
|
Group: "dashboard.grafana.app",
|
|
},
|
|
{
|
|
Name: "valid-folder",
|
|
Kind: "Folder",
|
|
Group: "folder.grafana.app",
|
|
},
|
|
},
|
|
Ref: "main",
|
|
},
|
|
},
|
|
}
|
|
|
|
mockRepo := &mockReaderWriter{
|
|
MockRepository: repository.NewMockRepository(t),
|
|
}
|
|
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
|
mockWrapFn := repository.NewMockWrapWithStageFn(t)
|
|
mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t)
|
|
mockRepositoryResources := resources.NewMockRepositoryResources(t)
|
|
|
|
mockResourcesFactory.On("Client", mock.Anything, mockRepo).Return(mockRepositoryResources, nil)
|
|
|
|
// First resource succeeds
|
|
mockRepositoryResources.On("FindResourcePath", mock.Anything, "valid-dashboard", schema.GroupVersionKind{
|
|
Group: "dashboard.grafana.app",
|
|
Kind: "Dashboard",
|
|
// Version is empty - ForKind will discover the preferred version
|
|
}).Return("dashboards/valid-dashboard.json", nil)
|
|
|
|
// Second resource fails
|
|
findPathError := errors.New("resource not found")
|
|
mockRepositoryResources.On("FindResourcePath", mock.Anything, "nonexistent-dashboard", schema.GroupVersionKind{
|
|
Group: "dashboard.grafana.app",
|
|
Kind: "Dashboard",
|
|
// Version is empty - ForKind will discover the preferred version
|
|
}).Return("", findPathError)
|
|
|
|
// Third resource succeeds
|
|
mockRepositoryResources.On("FindResourcePath", mock.Anything, "valid-folder", schema.GroupVersionKind{
|
|
Group: "folder.grafana.app",
|
|
Kind: "Folder",
|
|
// Version is empty - ForKind will discover the preferred version
|
|
}).Return("folders/valid-folder.json", nil)
|
|
|
|
mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error {
|
|
return fn(mockRepo, false)
|
|
})
|
|
|
|
mockProgress.On("SetTotal", mock.Anything, 3).Return()
|
|
mockProgress.On("StrictMaxErrors", 1).Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Finding path for resource dashboard.grafana.app/Dashboard/valid-dashboard").Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Finding path for resource dashboard.grafana.app/Dashboard/nonexistent-dashboard").Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Finding path for resource folder.grafana.app/Folder/valid-folder").Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Deleting dashboards/valid-dashboard.json").Return()
|
|
mockProgress.On("SetMessage", mock.Anything, "Deleting folders/valid-folder.json").Return()
|
|
mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{})
|
|
// Record the error for the failed resource
|
|
mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Name == "nonexistent-dashboard" && result.Error != nil
|
|
})).Return()
|
|
|
|
// Allow continuing after error
|
|
mockProgress.On("TooManyErrors").Return(nil).Times(3) // Called after each resource resolution and file deletion
|
|
|
|
// Mock successful file deletions for resolved resources
|
|
mockRepo.On("Delete", mock.Anything, "dashboards/valid-dashboard.json", "main", "Delete dashboards/valid-dashboard.json").Return(nil)
|
|
mockRepo.On("Delete", mock.Anything, "folders/valid-folder.json", "main", "Delete folders/valid-folder.json").Return(nil)
|
|
|
|
// Record successful deletions
|
|
mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Path == "dashboards/valid-dashboard.json" && result.Error == nil
|
|
})).Return()
|
|
mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Path == "folders/valid-folder.json" && result.Error == nil
|
|
})).Return()
|
|
|
|
worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()))
|
|
err := worker.Process(context.Background(), mockRepo, job, mockProgress)
|
|
require.NoError(t, err) // Should succeed overall, with only the failed resource recorded as error
|
|
}
|
|
|
|
func TestDeleteWorker_ProcessWithPathDeduplication(t *testing.T) {
|
|
// Test that duplicate paths from explicit paths and resource resolution are deduplicated
|
|
job := v0alpha1.Job{
|
|
Spec: v0alpha1.JobSpec{
|
|
Action: v0alpha1.JobActionDelete,
|
|
Delete: &v0alpha1.DeleteJobOptions{
|
|
Ref: "main", // Add ref to avoid sync worker execution
|
|
Paths: []string{"dashboards/test-dashboard.json", "folders/test-folder/"}, // Explicit paths
|
|
Resources: []v0alpha1.ResourceRef{
|
|
{
|
|
Name: "test-dashboard", // This will resolve to "dashboards/test-dashboard.json" (duplicate)
|
|
Kind: "Dashboard",
|
|
Group: "dashboard.grafana.app",
|
|
},
|
|
{
|
|
Name: "test-folder", // This will resolve to "folders/test-folder/" (duplicate)
|
|
Kind: "Folder",
|
|
Group: "folder.grafana.app",
|
|
},
|
|
{
|
|
Name: "unique-dashboard", // This will resolve to "dashboards/unique-dashboard.json" (unique)
|
|
Kind: "Dashboard",
|
|
Group: "dashboard.grafana.app",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
mockRepo := &mockReaderWriter{
|
|
MockRepository: repository.NewMockRepository(t),
|
|
}
|
|
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
|
mockWrapFn := repository.NewMockWrapWithStageFn(t)
|
|
|
|
// Mock resources factory and repository resources
|
|
mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t)
|
|
mockRepositoryResources := resources.NewMockRepositoryResources(t)
|
|
|
|
mockResourcesFactory.On("Client", mock.Anything, mockRepo).Return(mockRepositoryResources, nil)
|
|
|
|
mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error {
|
|
return fn(mockRepo, false)
|
|
})
|
|
|
|
// Expect total of 5 items (2 explicit paths + 3 resources), but only 3 unique paths will be deleted
|
|
mockProgress.On("SetTotal", mock.Anything, 5).Return()
|
|
mockProgress.On("StrictMaxErrors", 1).Return()
|
|
mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{})
|
|
// Resource resolution phase
|
|
mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return()
|
|
|
|
// Mock resource path resolution - note duplicates with explicit paths
|
|
mockProgress.On("SetMessage", mock.Anything, "Finding path for resource dashboard.grafana.app/Dashboard/test-dashboard").Return()
|
|
mockRepositoryResources.On("FindResourcePath", mock.Anything, "test-dashboard", schema.GroupVersionKind{
|
|
Group: "dashboard.grafana.app",
|
|
Kind: "Dashboard",
|
|
}).Return("dashboards/test-dashboard.json", nil) // Duplicate of explicit path
|
|
|
|
mockProgress.On("SetMessage", mock.Anything, "Finding path for resource folder.grafana.app/Folder/test-folder").Return()
|
|
mockRepositoryResources.On("FindResourcePath", mock.Anything, "test-folder", schema.GroupVersionKind{
|
|
Group: "folder.grafana.app",
|
|
Kind: "Folder",
|
|
}).Return("folders/test-folder/", nil) // Duplicate of explicit path
|
|
|
|
mockProgress.On("SetMessage", mock.Anything, "Finding path for resource dashboard.grafana.app/Dashboard/unique-dashboard").Return()
|
|
mockRepositoryResources.On("FindResourcePath", mock.Anything, "unique-dashboard", schema.GroupVersionKind{
|
|
Group: "dashboard.grafana.app",
|
|
Kind: "Dashboard",
|
|
}).Return("dashboards/unique-dashboard.json", nil) // Unique path
|
|
|
|
// Note: successful resource resolution does not call Record - only failures do
|
|
|
|
// Deletion phase - should only delete 3 unique paths (deduplication working)
|
|
mockProgress.On("SetMessage", mock.Anything, "Deleting dashboards/test-dashboard.json").Return()
|
|
mockRepo.On("Delete", mock.Anything, "dashboards/test-dashboard.json", "main", "Delete dashboards/test-dashboard.json").Return(nil)
|
|
mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Path == "dashboards/test-dashboard.json" && result.Action == repository.FileActionDeleted && result.Error == nil
|
|
})).Return()
|
|
mockProgress.On("TooManyErrors").Return(nil)
|
|
|
|
mockProgress.On("SetMessage", mock.Anything, "Deleting folders/test-folder/").Return()
|
|
mockRepo.On("Delete", mock.Anything, "folders/test-folder/", "main", "Delete folders/test-folder/").Return(nil)
|
|
mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Path == "folders/test-folder/" && result.Action == repository.FileActionDeleted && result.Error == nil
|
|
})).Return()
|
|
|
|
mockProgress.On("SetMessage", mock.Anything, "Deleting dashboards/unique-dashboard.json").Return()
|
|
mockRepo.On("Delete", mock.Anything, "dashboards/unique-dashboard.json", "main", "Delete dashboards/unique-dashboard.json").Return(nil)
|
|
mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Path == "dashboards/unique-dashboard.json" && result.Action == repository.FileActionDeleted && result.Error == nil
|
|
})).Return()
|
|
|
|
worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()))
|
|
err := worker.Process(context.Background(), mockRepo, job, mockProgress)
|
|
require.NoError(t, err)
|
|
|
|
// Verify all mocks were called as expected - key point is that each file is only deleted once
|
|
mockRepo.AssertExpectations(t)
|
|
mockProgress.AssertExpectations(t)
|
|
mockResourcesFactory.AssertExpectations(t)
|
|
mockRepositoryResources.AssertExpectations(t)
|
|
}
|
|
|
|
func TestDeduplicatePaths(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input []string
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "empty slice",
|
|
input: []string{},
|
|
expected: []string{},
|
|
},
|
|
{
|
|
name: "single path",
|
|
input: []string{"path1"},
|
|
expected: []string{"path1"},
|
|
},
|
|
{
|
|
name: "no duplicates",
|
|
input: []string{"path1", "path2", "path3"},
|
|
expected: []string{"path1", "path2", "path3"},
|
|
},
|
|
{
|
|
name: "with duplicates",
|
|
input: []string{"path1", "path2", "path1", "path3", "path2"},
|
|
expected: []string{"path1", "path2", "path3"},
|
|
},
|
|
{
|
|
name: "all same paths",
|
|
input: []string{"path1", "path1", "path1"},
|
|
expected: []string{"path1"},
|
|
},
|
|
{
|
|
name: "mixed paths with folder trailing slash",
|
|
input: []string{"folder/", "file.json", "folder/", "nested/file.json", "file.json"},
|
|
expected: []string{"folder/", "file.json", "nested/file.json"},
|
|
},
|
|
{
|
|
name: "preserves order",
|
|
input: []string{"c", "a", "b", "a", "c"},
|
|
expected: []string{"c", "a", "b"},
|
|
},
|
|
{
|
|
name: "realistic scenario - explicit paths and resource refs resolve to same paths",
|
|
input: []string{"dashboards/dashboard1.json", "folder/", "dashboards/dashboard1.json", "alerts/alert1.yaml", "folder/"},
|
|
expected: []string{"dashboards/dashboard1.json", "folder/", "alerts/alert1.yaml"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := deduplicatePaths(tt.input)
|
|
require.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeleteWorker_RefURLsSetWithRef(t *testing.T) {
|
|
mockRepoWithURLs := repository.NewMockRepositoryWithURLs(t)
|
|
config := &v0alpha1.Repository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-repo",
|
|
Namespace: "test-namespace",
|
|
},
|
|
Spec: v0alpha1.RepositorySpec{
|
|
Type: v0alpha1.GitHubRepositoryType,
|
|
},
|
|
}
|
|
mockRepoWithURLs.On("Config").Return(config).Maybe() // Config may be called multiple times
|
|
|
|
// Mock RefURLs method to return expected URLs
|
|
expectedRefURLs := &v0alpha1.RepositoryURLs{
|
|
SourceURL: "https://github.com/grafana/grafana/tree/feature-branch",
|
|
CompareURL: "https://github.com/grafana/grafana/compare/main...feature-branch",
|
|
NewPullRequestURL: "https://github.com/grafana/grafana/compare/main...feature-branch?quick_pull=1&labels=grafana",
|
|
}
|
|
mockRepoWithURLs.On("RefURLs", mock.Anything, "feature-branch").Return(expectedRefURLs, nil)
|
|
|
|
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
|
mockProgress.On("SetTotal", mock.Anything, 1).Once()
|
|
mockProgress.On("StrictMaxErrors", 1).Once()
|
|
mockProgress.On("SetMessage", mock.Anything, "Deleting test.json").Once()
|
|
mockProgress.On("Record", mock.Anything, mock.Anything).Once()
|
|
mockProgress.On("TooManyErrors").Return(nil).Once()
|
|
mockProgress.On("SetRefURLs", mock.Anything, expectedRefURLs).Once()
|
|
mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{})
|
|
mockReaderWriter := repository.NewMockReaderWriter(t)
|
|
mockReaderWriter.On("Delete", mock.Anything, "test.json", "feature-branch", "Delete test.json").Return(nil)
|
|
|
|
mockWrapFn := repository.NewMockWrapWithStageFn(t)
|
|
mockWrapFn.On("Execute", mock.Anything, mockRepoWithURLs, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, opts repository.StageOptions, fn func(repository.Repository, bool) error) error {
|
|
return fn(mockReaderWriter, true)
|
|
})
|
|
|
|
mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t)
|
|
|
|
job := v0alpha1.Job{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "test-job"},
|
|
Spec: v0alpha1.JobSpec{
|
|
Action: v0alpha1.JobActionDelete,
|
|
Delete: &v0alpha1.DeleteJobOptions{
|
|
Ref: "feature-branch",
|
|
Paths: []string{"test.json"},
|
|
},
|
|
},
|
|
}
|
|
|
|
worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()))
|
|
err := worker.Process(context.Background(), mockRepoWithURLs, job, mockProgress)
|
|
require.NoError(t, err)
|
|
|
|
// Verify that SetRefURLs was called with the expected RefURLs
|
|
mockProgress.AssertExpectations(t)
|
|
mockRepoWithURLs.AssertExpectations(t)
|
|
}
|
|
|
|
func TestDeleteWorker_RefURLsNotSetWithoutRef(t *testing.T) {
|
|
mockRepoWithURLs := repository.NewMockRepositoryWithURLs(t)
|
|
config := &v0alpha1.Repository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-repo",
|
|
Namespace: "test-namespace",
|
|
},
|
|
Spec: v0alpha1.RepositorySpec{
|
|
Type: v0alpha1.GitHubRepositoryType,
|
|
},
|
|
}
|
|
mockRepoWithURLs.On("Config").Return(config).Maybe() // Config may be called multiple times
|
|
|
|
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
|
mockProgress.On("SetTotal", mock.Anything, 1).Once()
|
|
mockProgress.On("StrictMaxErrors", 1).Once()
|
|
mockProgress.On("SetMessage", mock.Anything, "Deleting test.json").Once()
|
|
mockProgress.On("Record", mock.Anything, mock.Anything).Once()
|
|
mockProgress.On("TooManyErrors").Return(nil).Once()
|
|
mockProgress.On("ResetResults").Once()
|
|
mockProgress.On("SetMessage", mock.Anything, "pull resources").Once()
|
|
mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{})
|
|
// SetRefURLs should NOT be called since no ref is specified
|
|
|
|
mockReaderWriter := repository.NewMockReaderWriter(t)
|
|
mockReaderWriter.On("Delete", mock.Anything, "test.json", "", "Delete test.json").Return(nil)
|
|
|
|
mockWrapFn := repository.NewMockWrapWithStageFn(t)
|
|
mockWrapFn.On("Execute", mock.Anything, mockRepoWithURLs, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, opts repository.StageOptions, fn func(repository.Repository, bool) error) error {
|
|
return fn(mockReaderWriter, true)
|
|
})
|
|
|
|
mockSyncWorker := jobs.NewMockWorker(t)
|
|
mockSyncWorker.On("Process", mock.Anything, mockRepoWithURLs, mock.Anything, mockProgress).Return(nil)
|
|
|
|
mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t)
|
|
|
|
job := v0alpha1.Job{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "test-job"},
|
|
Spec: v0alpha1.JobSpec{
|
|
Action: v0alpha1.JobActionDelete,
|
|
Delete: &v0alpha1.DeleteJobOptions{
|
|
// No ref specified
|
|
Paths: []string{"test.json"},
|
|
},
|
|
},
|
|
}
|
|
|
|
worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()))
|
|
err := worker.Process(context.Background(), mockRepoWithURLs, job, mockProgress)
|
|
require.NoError(t, err)
|
|
|
|
// Verify that SetRefURLs was NOT called since no ref was specified
|
|
mockProgress.AssertExpectations(t)
|
|
}
|
|
|
|
func TestDeleteWorker_RefURLsNotSetForNonURLRepository(t *testing.T) {
|
|
mockRepo := repository.NewMockRepository(t)
|
|
config := &v0alpha1.Repository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-repo",
|
|
Namespace: "test-namespace",
|
|
},
|
|
Spec: v0alpha1.RepositorySpec{
|
|
Type: v0alpha1.GitRepositoryType, // Regular git repo, not GitHub
|
|
},
|
|
}
|
|
mockRepo.On("Config").Return(config).Maybe() // Config may be called multiple times
|
|
|
|
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
|
mockProgress.On("SetTotal", mock.Anything, 1).Once()
|
|
mockProgress.On("StrictMaxErrors", 1).Once()
|
|
mockProgress.On("SetMessage", mock.Anything, "Deleting test.json").Once()
|
|
mockProgress.On("Record", mock.Anything, mock.Anything).Once()
|
|
mockProgress.On("TooManyErrors").Return(nil).Once()
|
|
mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{})
|
|
// SetRefURLs should NOT be called since repo doesn't support URLs
|
|
|
|
mockReaderWriter := repository.NewMockReaderWriter(t)
|
|
mockReaderWriter.On("Delete", mock.Anything, "test.json", "feature-branch", "Delete test.json").Return(nil)
|
|
|
|
mockWrapFn := repository.NewMockWrapWithStageFn(t)
|
|
mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, opts repository.StageOptions, fn func(repository.Repository, bool) error) error {
|
|
return fn(mockReaderWriter, true)
|
|
})
|
|
|
|
mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t)
|
|
|
|
job := v0alpha1.Job{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "test-job"},
|
|
Spec: v0alpha1.JobSpec{
|
|
Action: v0alpha1.JobActionDelete,
|
|
Delete: &v0alpha1.DeleteJobOptions{
|
|
Ref: "feature-branch",
|
|
Paths: []string{"test.json"},
|
|
},
|
|
},
|
|
}
|
|
|
|
worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()))
|
|
err := worker.Process(context.Background(), mockRepo, job, mockProgress)
|
|
require.NoError(t, err)
|
|
|
|
// Verify that SetRefURLs was NOT called since repo doesn't support URLs
|
|
mockProgress.AssertExpectations(t)
|
|
}
|