diff --git a/tests/integration/commontestutils/skrcontextimpl/dual_cluster.go b/tests/integration/commontestutils/skrcontextimpl/dual_cluster.go index 521de7e2ba..19b3a854b9 100644 --- a/tests/integration/commontestutils/skrcontextimpl/dual_cluster.go +++ b/tests/integration/commontestutils/skrcontextimpl/dual_cluster.go @@ -10,20 +10,25 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" + "fmt" "github.com/kyma-project/lifecycle-manager/internal/event" "github.com/kyma-project/lifecycle-manager/internal/remote" ) var ( ErrEmptyRestConfig = errors.New("rest.Config is nil") - errSkrEnvNotStarted = errors.New("SKR envtest environment not started") + ErrSkrEnvNotStarted = errors.New("SKR envtest environment not started") ) +type Stopper interface { + Stop() error +} + type DualClusterFactory struct { clients sync.Map scheme *machineryruntime.Scheme event event.Event - skrEnv *envtest.Environment + SkrEnvs sync.Map } func NewDualClusterFactory(scheme *machineryruntime.Scheme, event event.Event) *DualClusterFactory { @@ -31,6 +36,7 @@ func NewDualClusterFactory(scheme *machineryruntime.Scheme, event event.Event) * clients: sync.Map{}, scheme: scheme, event: event, + SkrEnvs: sync.Map{}, } } @@ -40,57 +46,102 @@ func (f *DualClusterFactory) Init(_ context.Context, kyma types.NamespacedName) return nil } - f.skrEnv = &envtest.Environment{ + skrEnv := &envtest.Environment{ ErrorIfCRDPathMissing: true, // Scheme: scheme, } - cfg, err := f.GetSkrEnv().Start() + + // Start the envtest and record the returned cfg + cfg, err := skrEnv.Start() if err != nil { return err } if cfg == nil { + // cleanup fast - if start returned nil cfg + _ = skrEnv.Stop() return ErrEmptyRestConfig } var authUser *envtest.AuthenticatedUser - authUser, err = f.GetSkrEnv().AddUser(envtest.User{ + authUser, err = skrEnv.AddUser(envtest.User{ Name: "skr-admin-account", Groups: []string{"system:masters"}, }, cfg) if err != nil { + _ = skrEnv.Stop() return err } skrClient, err := client.New(authUser.Config(), client.Options{Scheme: f.scheme}) + if err != nil { + _ = skrEnv.Stop() + return err + } newClient := remote.NewClientWithConfig(skrClient, authUser.Config()) f.clients.Store(kyma.Name, newClient) + // track this envtest so Stop() can stop all started envs + f.SkrEnvs.Store(kyma.Name, skrEnv) + return err } func (f *DualClusterFactory) Get(kyma types.NamespacedName) (*remote.SkrContext, error) { value, ok := f.clients.Load(kyma.Name) if !ok { - return nil, errSkrEnvNotStarted + return nil, ErrSkrEnvNotStarted } skrClient, ok := value.(*remote.ConfigAndClient) if !ok { - return nil, errSkrEnvNotStarted + return nil, ErrSkrEnvNotStarted } return remote.NewSkrContext(skrClient, f.event), nil } +func (f *DualClusterFactory) StoreEnv(name string, env *envtest.Environment) error { + if name == "" { + return errors.New("environment name cannot be empty") + } + f.SkrEnvs.Store(name, env) + return nil +} + func (f *DualClusterFactory) InvalidateCache(_ types.NamespacedName) { // no-op } func (f *DualClusterFactory) GetSkrEnv() *envtest.Environment { - return f.skrEnv + var env *envtest.Environment + f.SkrEnvs.Range(func(key, value any) bool { + if e, ok := value.(*envtest.Environment); ok { + env = e + return false + } + return true + }) + return env } func (f *DualClusterFactory) Stop() error { - if f.skrEnv == nil { - return nil + var errs []error + + f.SkrEnvs.Range(func(key, value any) bool { + name, ok := key.(string) + if !ok { + return true + } + if stopper, ok := value.(Stopper); ok { + if err := stopper.Stop(); err != nil { + errs = append(errs, fmt.Errorf("stop %s: %w", name, err)) + } + } + f.SkrEnvs.Delete(key) + f.clients.Delete(name) + return true + }) + + if len(errs) > 0 { + return fmt.Errorf("errors stopping envtests: %w", errors.Join(errs...)) } - return f.skrEnv.Stop() + return nil } diff --git a/tests/integration/commontestutils/skrcontextimpl/dual_cluster_test.go b/tests/integration/commontestutils/skrcontextimpl/dual_cluster_test.go new file mode 100644 index 0000000000..46b75280cf --- /dev/null +++ b/tests/integration/commontestutils/skrcontextimpl/dual_cluster_test.go @@ -0,0 +1,152 @@ +package skrcontextimpl_test + +import ( + "errors" + "sync" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + machineryruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/envtest" + + testskrcontext "github.com/kyma-project/lifecycle-manager/tests/integration/commontestutils/skrcontextimpl" +) + +func TestDualClusterFactory(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "DualCluster Factory Suite") +} + +func newFactory() *testskrcontext.DualClusterFactory { + scheme := machineryruntime.NewScheme() + return testskrcontext.NewDualClusterFactory(scheme, nil) +} + +func Test_GetBeforeInit(t *testing.T) { + dualFactory := newFactory() + + _, err := dualFactory.Get(types.NamespacedName{Name: "kymaUninitialized"}) + + require.Error(t, err) + assert.ErrorIs(t, err, testskrcontext.ErrSkrEnvNotStarted) +} + +func Test_StoreEnv_ValidatesInput(t *testing.T) { + dualFactory := newFactory() + + err := dualFactory.StoreEnv("", &envtest.Environment{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "name cannot be empty") +} + +func Test_StopWithErrors(t *testing.T) { + dualFactory := newFactory() + + // Store real envtest.Environment first, then replace with test doubles + require.NoError(t, dualFactory.StoreEnv("primary-env", &envtest.Environment{})) + require.NoError(t, dualFactory.StoreEnv("secondary-env", &envtest.Environment{})) + require.NoError(t, dualFactory.StoreEnv("tertiary-env", &envtest.Environment{})) + + // Create test doubles + envPrimary := &fakeEnvTest{name: "primary-env", stopErr: errors.New("primary stop failure")} + envSecondary := &fakeEnvTest{name: "secondary-env", stopErr: errors.New("secondary stop failure")} + envTertiary := &fakeEnvTest{name: "tertiary-env"} + + // Replace with test doubles directly in the map + dualFactory.SkrEnvs.Store("primary-env", envPrimary) + dualFactory.SkrEnvs.Store("secondary-env", envSecondary) + dualFactory.SkrEnvs.Store("tertiary-env", envTertiary) + + err := dualFactory.Stop() + + require.Error(t, err) + msg := err.Error() + assert.Contains(t, msg, "primary stop failure") + assert.Contains(t, msg, "secondary stop failure") + assert.True(t, envPrimary.stopCalled) + assert.True(t, envSecondary.stopCalled) + assert.True(t, envTertiary.stopCalled) +} + +func Test_StopClearsAllEntriesAndIsIdempotent(t *testing.T) { + dualFactory := newFactory() + + require.NoError(t, dualFactory.StoreEnv("test-env", &envtest.Environment{})) + + fakeEnv := &fakeEnvTest{name: "test-env"} + dualFactory.SkrEnvs.Store("test-env", fakeEnv) + + require.NoError(t, dualFactory.Stop()) + assert.True(t, fakeEnv.stopCalled) + assert.Nil(t, dualFactory.GetSkrEnv()) + + // Verify entry is cleared + _, err := dualFactory.Get(types.NamespacedName{Name: "test-env"}) + require.Error(t, err) + + // Second stop should also succeed (idempotent) + require.NoError(t, dualFactory.Stop()) +} + +func Test_ConcurrentStopCalls(t *testing.T) { + dualFactory := newFactory() + + for range 10 { + require.NoError(t, dualFactory.StoreEnv("test-env", &envtest.Environment{})) + } + + fakeEnv := &fakeEnvTest{name: "test-env"} + dualFactory.SkrEnvs.Store("test-env", fakeEnv) + + var waitGroup sync.WaitGroup + errors := make(chan error, 5) + + for range 5 { + waitGroup.Add(1) + go func() { + defer waitGroup.Done() + errors <- dualFactory.Stop() + }() + } + waitGroup.Wait() + close(errors) + + for err := range errors { + assert.NoError(t, err) + } +} + +func Test_StopperInterfaceHandling(t *testing.T) { + dualFactory := newFactory() + + require.NoError(t, dualFactory.StoreEnv("normal", &envtest.Environment{})) + require.NoError(t, dualFactory.StoreEnv("error", &envtest.Environment{})) + + normalStopper := &fakeEnvTest{name: "normal"} + errorStopper := &fakeEnvTest{name: "error", stopErr: errors.New("stop error")} + + dualFactory.SkrEnvs.Store("normal", normalStopper) + dualFactory.SkrEnvs.Store("error", errorStopper) + + err := dualFactory.Stop() + + require.Error(t, err) + assert.Contains(t, err.Error(), "stop error") + assert.True(t, normalStopper.stopCalled) + assert.True(t, errorStopper.stopCalled) +} + +type fakeEnvTest struct { + name string + stopCalled bool + stopErr error +} + +func (f *fakeEnvTest) Stop() error { + f.stopCalled = true + return f.stopErr +}