5ecfc79e14
* Provisioning: Add Connection resource * adding some more integration tests * updating openapi snapshot, linting * generating FE code, fixing issue in unit tests * addressing comments * addressing comments * adding more integration tests * fixing rebase issues * removing linting exception * addressing comments: improving validation and tests * adding Connection URL at mutation time, updating tests accordingly * linting
706 lines
21 KiB
Go
706 lines
21 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/watch"
|
|
"k8s.io/client-go/rest"
|
|
|
|
"github.com/grafana/grafana/pkg/registry/apis/provisioning/controller/mocks"
|
|
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
|
|
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
|
provisioningv0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1"
|
|
client "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1"
|
|
"github.com/grafana/grafana/apps/provisioning/pkg/repository"
|
|
)
|
|
|
|
type mockProvisioningV0alpha1Interface struct {
|
|
repositoriesFunc func(namespace string) client.RepositoryInterface
|
|
}
|
|
|
|
func (m mockProvisioningV0alpha1Interface) RESTClient() rest.Interface {
|
|
panic("not needed for testing")
|
|
}
|
|
|
|
func (m mockProvisioningV0alpha1Interface) HistoricJobs(namespace string) client.HistoricJobInterface {
|
|
panic("not needed for testing")
|
|
}
|
|
|
|
func (m mockProvisioningV0alpha1Interface) Jobs(namespace string) client.JobInterface {
|
|
panic("not needed for testing")
|
|
}
|
|
|
|
func (m mockProvisioningV0alpha1Interface) Connections(namespace string) client.ConnectionInterface {
|
|
panic("not needed for testing")
|
|
}
|
|
|
|
func (m mockProvisioningV0alpha1Interface) Repositories(namespace string) client.RepositoryInterface {
|
|
if m.repositoriesFunc != nil {
|
|
return m.repositoriesFunc(namespace)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type mockRepoInterface struct {
|
|
patchFunc func(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *provisioning.Repository, err error)
|
|
}
|
|
|
|
func (m mockRepoInterface) Create(ctx context.Context, repository *provisioning.Repository, opts metav1.CreateOptions) (*provisioning.Repository, error) {
|
|
panic("not needed for testing")
|
|
}
|
|
|
|
func (m mockRepoInterface) Update(ctx context.Context, repository *provisioning.Repository, opts metav1.UpdateOptions) (*provisioning.Repository, error) {
|
|
panic("not needed for testing")
|
|
}
|
|
|
|
func (m mockRepoInterface) UpdateStatus(ctx context.Context, repository *provisioning.Repository, opts metav1.UpdateOptions) (*provisioning.Repository, error) {
|
|
panic("not needed for testing")
|
|
}
|
|
|
|
func (m mockRepoInterface) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error {
|
|
panic("not needed for testing")
|
|
}
|
|
|
|
func (m mockRepoInterface) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
|
|
panic("not needed for testing")
|
|
}
|
|
|
|
func (m mockRepoInterface) Get(ctx context.Context, name string, opts metav1.GetOptions) (*provisioning.Repository, error) {
|
|
panic("not needed for testing")
|
|
}
|
|
|
|
func (m mockRepoInterface) List(ctx context.Context, opts metav1.ListOptions) (*provisioning.RepositoryList, error) {
|
|
panic("not needed for testing")
|
|
}
|
|
|
|
func (m mockRepoInterface) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
|
|
panic("not needed for testing")
|
|
}
|
|
|
|
func (m mockRepoInterface) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *provisioning.Repository, err error) {
|
|
if m.patchFunc != nil {
|
|
return m.patchFunc(ctx, name, pt, data, opts, subresources...)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m mockRepoInterface) Apply(ctx context.Context, repository *provisioningv0alpha1.RepositoryApplyConfiguration, opts metav1.ApplyOptions) (result *provisioning.Repository, err error) {
|
|
panic("not needed for testing")
|
|
}
|
|
|
|
func (m mockRepoInterface) ApplyStatus(ctx context.Context, repository *provisioningv0alpha1.RepositoryApplyConfiguration, opts metav1.ApplyOptions) (result *provisioning.Repository, err error) {
|
|
panic("not needed for testing")
|
|
}
|
|
|
|
var (
|
|
_ client.ProvisioningV0alpha1Interface = (*mockProvisioningV0alpha1Interface)(nil)
|
|
_ client.RepositoryInterface = (*mockRepoInterface)(nil)
|
|
)
|
|
|
|
func TestRepositoryController_handleDelete(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
repoFactory repository.Factory
|
|
finalizer finalizerProcessor
|
|
client client.ProvisioningV0alpha1Interface
|
|
statusPatcher StatusPatcher
|
|
repo *provisioning.Repository
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "No finalizers",
|
|
repoFactory: nil,
|
|
finalizer: nil,
|
|
client: nil,
|
|
statusPatcher: nil,
|
|
repo: &provisioning.Repository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Finalizers: []string{},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "Finalizers deleted successfully",
|
|
repoFactory: func() repository.Factory {
|
|
f := repository.NewMockFactory(t)
|
|
|
|
f.
|
|
On("Build", context.Background(), mock.Anything).
|
|
Once().
|
|
Return(nil, nil)
|
|
|
|
return f
|
|
}(),
|
|
finalizer: func() finalizerProcessor {
|
|
f := NewMockFinalizerProcessor(t)
|
|
|
|
f.
|
|
On("process", context.Background(), nil, []string{
|
|
repository.RemoveOrphanResourcesFinalizer,
|
|
}).
|
|
Once().
|
|
Return(nil)
|
|
|
|
return f
|
|
}(),
|
|
client: func() client.ProvisioningV0alpha1Interface {
|
|
repo := &mockRepoInterface{
|
|
patchFunc: func(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *provisioning.Repository, err error) {
|
|
return &provisioning.Repository{}, nil
|
|
},
|
|
}
|
|
c := &mockProvisioningV0alpha1Interface{
|
|
repositoriesFunc: func(namespace string) client.RepositoryInterface {
|
|
return repo
|
|
},
|
|
}
|
|
|
|
return c
|
|
}(),
|
|
statusPatcher: nil,
|
|
repo: &provisioning.Repository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Finalizers: []string{
|
|
repository.RemoveOrphanResourcesFinalizer,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "Error when building repository",
|
|
repoFactory: func() repository.Factory {
|
|
f := repository.NewMockFactory(t)
|
|
|
|
f.
|
|
On("Build", context.Background(), mock.Anything).
|
|
Once().
|
|
Return(nil, assert.AnError)
|
|
|
|
return f
|
|
}(),
|
|
finalizer: nil,
|
|
client: nil,
|
|
statusPatcher: nil,
|
|
repo: &provisioning.Repository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Finalizers: []string{
|
|
repository.RemoveOrphanResourcesFinalizer,
|
|
},
|
|
},
|
|
},
|
|
expectedErr: "create repository from configuration: " + assert.AnError.Error(),
|
|
},
|
|
{
|
|
name: "Error when processing finalizer",
|
|
repoFactory: func() repository.Factory {
|
|
f := repository.NewMockFactory(t)
|
|
|
|
f.
|
|
On("Build", context.Background(), mock.Anything).
|
|
Once().
|
|
Return(nil, nil)
|
|
|
|
return f
|
|
}(),
|
|
finalizer: func() finalizerProcessor {
|
|
f := NewMockFinalizerProcessor(t)
|
|
|
|
f.
|
|
On("process", context.Background(), nil, []string{
|
|
repository.RemoveOrphanResourcesFinalizer,
|
|
}).
|
|
Once().
|
|
Return(assert.AnError)
|
|
|
|
return f
|
|
}(),
|
|
statusPatcher: func() StatusPatcher {
|
|
s := mocks.NewStatusPatcher(t)
|
|
|
|
s.
|
|
On("Patch", context.Background(), mock.AnythingOfType("*v0alpha1.Repository"), mock.AnythingOfType("map[string]interface {}")).
|
|
Once().
|
|
Return(nil) // Return nil error for the status patch
|
|
|
|
return s
|
|
}(),
|
|
client: nil,
|
|
repo: &provisioning.Repository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Finalizers: []string{
|
|
repository.RemoveOrphanResourcesFinalizer,
|
|
},
|
|
},
|
|
},
|
|
expectedErr: "process finalizers: " + assert.AnError.Error(),
|
|
},
|
|
{
|
|
name: "Error when patching finalizers",
|
|
repoFactory: func() repository.Factory {
|
|
f := repository.NewMockFactory(t)
|
|
|
|
f.
|
|
On("Build", context.Background(), mock.Anything).
|
|
Once().
|
|
Return(nil, nil)
|
|
|
|
return f
|
|
}(),
|
|
finalizer: func() finalizerProcessor {
|
|
f := NewMockFinalizerProcessor(t)
|
|
|
|
f.
|
|
On("process", context.Background(), nil, []string{
|
|
repository.RemoveOrphanResourcesFinalizer,
|
|
}).
|
|
Once().
|
|
Return(nil)
|
|
|
|
return f
|
|
}(),
|
|
client: func() client.ProvisioningV0alpha1Interface {
|
|
repo := &mockRepoInterface{
|
|
patchFunc: func(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *provisioning.Repository, err error) {
|
|
return &provisioning.Repository{}, assert.AnError
|
|
},
|
|
}
|
|
c := &mockProvisioningV0alpha1Interface{
|
|
repositoriesFunc: func(namespace string) client.RepositoryInterface {
|
|
return repo
|
|
},
|
|
}
|
|
|
|
return c
|
|
}(),
|
|
statusPatcher: nil,
|
|
repo: &provisioning.Repository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Finalizers: []string{
|
|
repository.RemoveOrphanResourcesFinalizer,
|
|
},
|
|
},
|
|
},
|
|
expectedErr: "remove finalizers: " + assert.AnError.Error(),
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
c := &RepositoryController{
|
|
repoFactory: tc.repoFactory,
|
|
finalizer: tc.finalizer,
|
|
client: tc.client,
|
|
statusPatcher: tc.statusPatcher,
|
|
}
|
|
|
|
err := c.handleDelete(context.Background(), tc.repo)
|
|
if tc.expectedErr != "" {
|
|
assert.Error(t, err)
|
|
assert.ErrorContains(t, err, tc.expectedErr)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestShouldUseIncrementalSync(t *testing.T) {
|
|
versioned := repository.NewMockVersioned(t)
|
|
obj := &provisioning.Repository{
|
|
Status: provisioning.RepositoryStatus{
|
|
Sync: provisioning.SyncStatus{
|
|
LastRef: "123",
|
|
},
|
|
},
|
|
}
|
|
latestRef := "456"
|
|
t.Run("should use incremental sync", func(t *testing.T) {
|
|
versioned.On("CompareFiles", context.Background(), obj.Status.Sync.LastRef, latestRef).Return([]repository.VersionedFileChange{
|
|
{
|
|
Action: repository.FileActionDeleted,
|
|
Path: "test.json",
|
|
},
|
|
}, nil).Once()
|
|
got, err := shouldUseIncrementalSync(context.Background(), versioned, obj, latestRef)
|
|
assert.NoError(t, err)
|
|
assert.True(t, got)
|
|
})
|
|
|
|
t.Run("should not use incremental sync", func(t *testing.T) {
|
|
versioned.On("CompareFiles", context.Background(), obj.Status.Sync.LastRef, latestRef).Return([]repository.VersionedFileChange{
|
|
{
|
|
Action: repository.FileActionDeleted,
|
|
Path: "test/.keep",
|
|
},
|
|
}, nil).Once()
|
|
got, err := shouldUseIncrementalSync(context.Background(), versioned, obj, latestRef)
|
|
assert.NoError(t, err)
|
|
assert.False(t, got)
|
|
})
|
|
}
|
|
|
|
// mockJobsQueueStore implements both jobs.Queue and jobs.Store for testing
|
|
type mockJobsQueueStore struct {
|
|
*jobs.MockQueue
|
|
*jobs.MockStore
|
|
}
|
|
|
|
func TestRepositoryController_process_UnhealthyRepositoryStatusUpdate(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
repo *provisioning.Repository
|
|
healthStatus provisioning.HealthStatus
|
|
hasHealthStatusChanged bool
|
|
expectedUnhealthyMessage bool
|
|
description string
|
|
}{
|
|
{
|
|
name: "unhealthy repository should set unhealthy message in sync status",
|
|
repo: &provisioning.Repository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-repo",
|
|
Namespace: "default",
|
|
Generation: 1,
|
|
},
|
|
Spec: provisioning.RepositorySpec{
|
|
Sync: provisioning.SyncOptions{
|
|
Enabled: true,
|
|
IntervalSeconds: 300,
|
|
},
|
|
},
|
|
Status: provisioning.RepositoryStatus{
|
|
ObservedGeneration: 1,
|
|
Health: provisioning.HealthStatus{
|
|
Healthy: true,
|
|
Checked: time.Now().Add(-10 * time.Minute).UnixMilli(),
|
|
},
|
|
Sync: provisioning.SyncStatus{
|
|
State: provisioning.JobStateSuccess,
|
|
Finished: time.Now().Add(-1 * time.Minute).UnixMilli(),
|
|
Message: []string{},
|
|
},
|
|
},
|
|
},
|
|
healthStatus: provisioning.HealthStatus{
|
|
Healthy: false,
|
|
Error: provisioning.HealthFailureHealth,
|
|
Checked: time.Now().UnixMilli(),
|
|
Message: []string{"connection failed"},
|
|
},
|
|
hasHealthStatusChanged: true,
|
|
expectedUnhealthyMessage: true,
|
|
description: "should set unhealthy message when repository becomes unhealthy",
|
|
},
|
|
{
|
|
name: "unhealthy repository should not duplicate unhealthy message",
|
|
repo: &provisioning.Repository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-repo",
|
|
Namespace: "default",
|
|
Generation: 1,
|
|
},
|
|
Spec: provisioning.RepositorySpec{
|
|
Sync: provisioning.SyncOptions{
|
|
Enabled: true,
|
|
IntervalSeconds: 300,
|
|
},
|
|
},
|
|
Status: provisioning.RepositoryStatus{
|
|
ObservedGeneration: 1,
|
|
Health: provisioning.HealthStatus{
|
|
Healthy: false,
|
|
Checked: time.Now().Add(-2 * time.Minute).UnixMilli(),
|
|
},
|
|
Sync: provisioning.SyncStatus{
|
|
State: provisioning.JobStateError,
|
|
Finished: time.Now().Add(-1 * time.Minute).UnixMilli(),
|
|
Message: []string{"Repository is unhealthy"},
|
|
},
|
|
},
|
|
},
|
|
healthStatus: provisioning.HealthStatus{
|
|
Healthy: false,
|
|
Error: provisioning.HealthFailureHealth,
|
|
Checked: time.Now().UnixMilli(),
|
|
Message: []string{"connection failed"},
|
|
},
|
|
hasHealthStatusChanged: false,
|
|
expectedUnhealthyMessage: false,
|
|
description: "should not set unhealthy message when it already exists",
|
|
},
|
|
{
|
|
name: "healthy repository should clear unhealthy message",
|
|
repo: &provisioning.Repository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-repo",
|
|
Namespace: "default",
|
|
Generation: 1,
|
|
},
|
|
Spec: provisioning.RepositorySpec{
|
|
Sync: provisioning.SyncOptions{
|
|
Enabled: true,
|
|
IntervalSeconds: 300,
|
|
},
|
|
},
|
|
Status: provisioning.RepositoryStatus{
|
|
ObservedGeneration: 1,
|
|
Health: provisioning.HealthStatus{
|
|
Healthy: false,
|
|
Checked: time.Now().Add(-2 * time.Minute).UnixMilli(),
|
|
},
|
|
Sync: provisioning.SyncStatus{
|
|
State: provisioning.JobStateError,
|
|
Finished: time.Now().Add(-1 * time.Minute).UnixMilli(),
|
|
Message: []string{"Repository is unhealthy"},
|
|
},
|
|
},
|
|
},
|
|
healthStatus: provisioning.HealthStatus{
|
|
Healthy: true,
|
|
Checked: time.Now().UnixMilli(),
|
|
Message: []string{},
|
|
},
|
|
hasHealthStatusChanged: true,
|
|
expectedUnhealthyMessage: false,
|
|
description: "should clear unhealthy message when repository becomes healthy",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Create controller
|
|
rc := &RepositoryController{}
|
|
|
|
// Determine sync status ops (this is a pure function, no mocks needed)
|
|
syncOps := rc.determineSyncStatusOps(tc.repo, nil, tc.healthStatus)
|
|
|
|
// Verify expectations
|
|
hasUnhealthyOp := false
|
|
hasClearUnhealthyOp := false
|
|
for _, op := range syncOps {
|
|
if path, ok := op["path"].(string); ok {
|
|
if path == "/status/sync/message" {
|
|
if messages, ok := op["value"].([]string); ok {
|
|
if len(messages) > 0 && messages[0] == "Repository is unhealthy" {
|
|
hasUnhealthyOp = true
|
|
} else if len(messages) == 0 {
|
|
hasClearUnhealthyOp = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if tc.expectedUnhealthyMessage {
|
|
assert.True(t, hasUnhealthyOp, tc.description+": expected unhealthy message operation")
|
|
} else if len(tc.repo.Status.Sync.Message) > 0 && tc.healthStatus.Healthy {
|
|
assert.True(t, hasClearUnhealthyOp, tc.description+": expected clear unhealthy message operation")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRepositoryController_shouldResync_StaleSyncStatus(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
repo *provisioning.Repository
|
|
jobGetError error
|
|
expectedResync bool
|
|
description string
|
|
}{
|
|
{
|
|
name: "stale sync status with Pending state - job not found",
|
|
repo: &provisioning.Repository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-repo",
|
|
Namespace: "default",
|
|
},
|
|
Spec: provisioning.RepositorySpec{
|
|
Sync: provisioning.SyncOptions{
|
|
Enabled: true,
|
|
IntervalSeconds: 300,
|
|
},
|
|
},
|
|
Status: provisioning.RepositoryStatus{
|
|
Sync: provisioning.SyncStatus{
|
|
State: provisioning.JobStatePending,
|
|
JobID: "test-job-123",
|
|
Started: time.Now().Add(-10 * time.Minute).UnixMilli(),
|
|
Finished: time.Now().Add(-10 * time.Minute).UnixMilli(),
|
|
},
|
|
},
|
|
},
|
|
jobGetError: apierrors.NewNotFound(schema.GroupResource{Resource: "jobs"}, "test-job-123"),
|
|
expectedResync: true,
|
|
description: "should return true to trigger resync when job is not found",
|
|
},
|
|
{
|
|
name: "stale sync status with Working state - job not found",
|
|
repo: &provisioning.Repository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-repo",
|
|
Namespace: "default",
|
|
},
|
|
Spec: provisioning.RepositorySpec{
|
|
Sync: provisioning.SyncOptions{
|
|
Enabled: true,
|
|
IntervalSeconds: 300,
|
|
},
|
|
},
|
|
Status: provisioning.RepositoryStatus{
|
|
Sync: provisioning.SyncStatus{
|
|
State: provisioning.JobStateWorking,
|
|
JobID: "test-job-456",
|
|
Started: time.Now().Add(-5 * time.Minute).UnixMilli(),
|
|
Finished: time.Now().Add(-5 * time.Minute).UnixMilli(),
|
|
},
|
|
},
|
|
},
|
|
jobGetError: apierrors.NewNotFound(schema.GroupResource{Resource: "jobs"}, "test-job-456"),
|
|
expectedResync: true,
|
|
description: "should return true to trigger resync when working job is not found",
|
|
},
|
|
{
|
|
name: "non-stale sync status - job exists",
|
|
repo: &provisioning.Repository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-repo",
|
|
Namespace: "default",
|
|
},
|
|
Spec: provisioning.RepositorySpec{
|
|
Sync: provisioning.SyncOptions{
|
|
Enabled: true,
|
|
IntervalSeconds: 300,
|
|
},
|
|
},
|
|
Status: provisioning.RepositoryStatus{
|
|
Sync: provisioning.SyncStatus{
|
|
State: provisioning.JobStatePending,
|
|
JobID: "test-job-789",
|
|
Started: time.Now().Add(-2 * time.Minute).UnixMilli(),
|
|
Finished: time.Now().Add(-2 * time.Minute).UnixMilli(),
|
|
},
|
|
},
|
|
},
|
|
jobGetError: nil, // Job exists
|
|
expectedResync: false, // Should continue with normal logic
|
|
description: "should continue with normal logic when job exists",
|
|
},
|
|
{
|
|
name: "non-stale sync status - no JobID",
|
|
repo: &provisioning.Repository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-repo",
|
|
Namespace: "default",
|
|
},
|
|
Spec: provisioning.RepositorySpec{
|
|
Sync: provisioning.SyncOptions{
|
|
Enabled: true,
|
|
IntervalSeconds: 300,
|
|
},
|
|
},
|
|
Status: provisioning.RepositoryStatus{
|
|
Sync: provisioning.SyncStatus{
|
|
State: provisioning.JobStatePending,
|
|
JobID: "",
|
|
Started: time.Now().Add(-2 * time.Minute).UnixMilli(),
|
|
Finished: time.Now().Add(-2 * time.Minute).UnixMilli(),
|
|
},
|
|
},
|
|
},
|
|
jobGetError: nil,
|
|
expectedResync: false,
|
|
description: "should not check when JobID is empty",
|
|
},
|
|
{
|
|
name: "non-stale sync status - already finished",
|
|
repo: &provisioning.Repository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-repo",
|
|
Namespace: "default",
|
|
},
|
|
Spec: provisioning.RepositorySpec{
|
|
Sync: provisioning.SyncOptions{
|
|
Enabled: true,
|
|
IntervalSeconds: 300,
|
|
},
|
|
},
|
|
Status: provisioning.RepositoryStatus{
|
|
Sync: provisioning.SyncStatus{
|
|
State: provisioning.JobStateSuccess,
|
|
JobID: "test-job-999",
|
|
Finished: time.Now().Add(-1 * time.Minute).UnixMilli(),
|
|
},
|
|
},
|
|
},
|
|
jobGetError: nil,
|
|
expectedResync: false,
|
|
description: "should not check when sync status is already finished",
|
|
},
|
|
{
|
|
name: "stale sync status - job lookup error (non-NotFound)",
|
|
repo: &provisioning.Repository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-repo",
|
|
Namespace: "default",
|
|
},
|
|
Spec: provisioning.RepositorySpec{
|
|
Sync: provisioning.SyncOptions{
|
|
Enabled: true,
|
|
IntervalSeconds: 300,
|
|
},
|
|
},
|
|
Status: provisioning.RepositoryStatus{
|
|
Sync: provisioning.SyncStatus{
|
|
State: provisioning.JobStatePending,
|
|
JobID: "test-job-error",
|
|
Started: time.Now().Add(-2 * time.Minute).UnixMilli(),
|
|
Finished: time.Now().Add(-2 * time.Minute).UnixMilli(),
|
|
},
|
|
},
|
|
},
|
|
jobGetError: assert.AnError, // Non-NotFound error
|
|
expectedResync: false, // Should continue with normal logic
|
|
description: "should handle non-NotFound errors gracefully and continue with normal logic",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Create mocks
|
|
mockQueue := jobs.NewMockQueue(t)
|
|
mockStore := jobs.NewMockStore(t)
|
|
mockJobs := &mockJobsQueueStore{
|
|
MockQueue: mockQueue,
|
|
MockStore: mockStore,
|
|
}
|
|
|
|
// Set up job Get mock
|
|
if tc.repo.Status.Sync.JobID != "" && (tc.repo.Status.Sync.State == provisioning.JobStatePending || tc.repo.Status.Sync.State == provisioning.JobStateWorking) {
|
|
mockStore.On("Get", mock.Anything, tc.repo.Namespace, tc.repo.Status.Sync.JobID).Return(nil, tc.jobGetError).Once()
|
|
}
|
|
|
|
// Create controller
|
|
rc := &RepositoryController{
|
|
jobs: mockJobs,
|
|
}
|
|
|
|
// Test shouldResync
|
|
ctx := context.Background()
|
|
result := rc.shouldResync(ctx, tc.repo)
|
|
|
|
// Verify
|
|
assert.Equal(t, tc.expectedResync, result, tc.description)
|
|
})
|
|
}
|
|
}
|