This is an automated email from the ASF dual-hosted git repository.
jimin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-seata-k8s.git
The following commit(s) were added to refs/heads/master by this push:
new 6813165 optimize: add some UT for controllers (#48)
6813165 is described below
commit 6813165ae69cff0e78c3b8216457396b81958a2e
Author: jimin <[email protected]>
AuthorDate: Sat Jan 10 11:15:02 2026 +0800
optimize: add some UT for controllers (#48)
---
controllers/seataserver_controller_test.go | 691 +++++++++++++++++++++++++++++
pkg/finalizer/cleanup_handler_test.go | 338 ++++++++++++++
pkg/seata/fetchers_test.go | 429 ++++++++++++++++++
pkg/seata/generators_test.go | 342 ++++++++++++++
pkg/seata/synchronizers_test.go | 264 +++++++++++
pkg/utils/utils_test.go | 360 +++++++++++++++
6 files changed, 2424 insertions(+)
diff --git a/controllers/seataserver_controller_test.go
b/controllers/seataserver_controller_test.go
new file mode 100644
index 0000000..dbe4097
--- /dev/null
+++ b/controllers/seataserver_controller_test.go
@@ -0,0 +1,691 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package controllers
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/go-logr/logr"
+ appsv1 "k8s.io/api/apps/v1"
+ apiv1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/api/resource"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+ seatav1alpha1 "github.com/apache/seata-k8s/api/v1alpha1"
+)
+
+func TestSeataServerReconciler_Reconcile(t *testing.T) {
+ scheme := createTestScheme()
+
+ seataServer := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ },
+ Spec: seatav1alpha1.SeataServerSpec{
+ ContainerSpec: seatav1alpha1.ContainerSpec{
+ ContainerName: "seata-server",
+ Image: "apache/seata-server:latest",
+ Resources: apiv1.ResourceRequirements{
+ Requests: apiv1.ResourceList{
+ apiv1.ResourceCPU:
resource.MustParse("500m"),
+ apiv1.ResourceMemory:
resource.MustParse("1Gi"),
+ },
+ },
+ },
+ ServiceName: "seata-cluster",
+ Replicas: 3,
+ Ports: seatav1alpha1.Ports{
+ ServicePort: 8091,
+ ConsolePort: 7091,
+ RaftPort: 9091,
+ },
+ Persistence: seatav1alpha1.Persistence{
+ VolumeReclaimPolicy:
seatav1alpha1.VolumeReclaimPolicyRetain,
+ PersistentVolumeClaimSpec:
apiv1.PersistentVolumeClaimSpec{
+ Resources: apiv1.ResourceRequirements{
+ Requests: apiv1.ResourceList{
+ apiv1.ResourceStorage:
resource.MustParse("5Gi"),
+ },
+ },
+ },
+ },
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(seataServer).
+ WithStatusSubresource(seataServer).
+ Build()
+
+ reconciler := &SeataServerReconciler{
+ Client: fakeClient,
+ Scheme: scheme,
+ Log: logr.Discard(),
+ }
+
+ req := ctrl.Request{
+ NamespacedName: types.NamespacedName{
+ Name: "test-seata",
+ Namespace: "default",
+ },
+ }
+
+ result, err := reconciler.Reconcile(context.Background(), req)
+ if err != nil {
+ t.Errorf("Reconcile failed: %v", err)
+ }
+
+ if result.Requeue {
+ t.Log("Result requested requeue, which is expected for
unsynchronized servers")
+ }
+}
+
+func TestSeataServerReconciler_Reconcile_NotFound(t *testing.T) {
+ scheme := createTestScheme()
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ Build()
+
+ reconciler := &SeataServerReconciler{
+ Client: fakeClient,
+ Scheme: scheme,
+ Log: logr.Discard(),
+ }
+
+ req := ctrl.Request{
+ NamespacedName: types.NamespacedName{
+ Name: "nonexistent",
+ Namespace: "default",
+ },
+ }
+
+ result, err := reconciler.Reconcile(context.Background(), req)
+ if err != nil {
+ t.Errorf("Reconcile should not return error for not found: %v",
err)
+ }
+
+ if result.Requeue {
+ t.Error("Result should not request requeue for not found")
+ }
+}
+
+func TestSeataServerReconciler_ReconcileHeadlessService(t *testing.T) {
+ scheme := createTestScheme()
+
+ seataServer := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ },
+ Spec: seatav1alpha1.SeataServerSpec{
+ ServiceName: "seata-cluster",
+ Ports: seatav1alpha1.Ports{
+ ServicePort: 8091,
+ ConsolePort: 7091,
+ RaftPort: 9091,
+ },
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(seataServer).
+ Build()
+
+ reconciler := &SeataServerReconciler{
+ Client: fakeClient,
+ Scheme: scheme,
+ Log: logr.Discard(),
+ }
+
+ err := reconciler.reconcileHeadlessService(context.Background(),
seataServer)
+ if err != nil {
+ t.Errorf("reconcileHeadlessService failed: %v", err)
+ }
+
+ // Verify service was created
+ svc := &apiv1.Service{}
+ err = fakeClient.Get(context.Background(), types.NamespacedName{
+ Name: "seata-cluster",
+ Namespace: "default",
+ }, svc)
+
+ if err != nil {
+ t.Errorf("Failed to get created service: %v", err)
+ }
+
+ if svc.Spec.ClusterIP != "None" {
+ t.Errorf("Expected headless service (ClusterIP=None), got
ClusterIP=%s", svc.Spec.ClusterIP)
+ }
+}
+
+func TestSeataServerReconciler_ReconcileStatefulSet(t *testing.T) {
+ scheme := createTestScheme()
+
+ seataServer := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ UID: types.UID("test-uid-123"),
+ },
+ Spec: seatav1alpha1.SeataServerSpec{
+ ContainerSpec: seatav1alpha1.ContainerSpec{
+ ContainerName: "seata-server",
+ Image: "apache/seata-server:latest",
+ },
+ ServiceName: "seata-cluster",
+ Replicas: 3,
+ Ports: seatav1alpha1.Ports{
+ ServicePort: 8091,
+ ConsolePort: 7091,
+ RaftPort: 9091,
+ },
+ Persistence: seatav1alpha1.Persistence{
+ PersistentVolumeClaimSpec:
apiv1.PersistentVolumeClaimSpec{
+ Resources: apiv1.ResourceRequirements{
+ Requests: apiv1.ResourceList{
+ apiv1.ResourceStorage:
resource.MustParse("5Gi"),
+ },
+ },
+ },
+ },
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(seataServer).
+ WithStatusSubresource(seataServer).
+ Build()
+
+ reconciler := &SeataServerReconciler{
+ Client: fakeClient,
+ Scheme: scheme,
+ Log: logr.Discard(),
+ }
+
+ err := reconciler.reconcileStatefulSet(context.Background(),
seataServer)
+ if err != nil {
+ t.Errorf("reconcileStatefulSet failed: %v", err)
+ }
+
+ // Verify StatefulSet was created
+ sts := &appsv1.StatefulSet{}
+ err = fakeClient.Get(context.Background(), types.NamespacedName{
+ Name: "test-seata",
+ Namespace: "default",
+ }, sts)
+
+ if err != nil {
+ t.Errorf("Failed to get created StatefulSet: %v", err)
+ }
+
+ if *sts.Spec.Replicas != 3 {
+ t.Errorf("Expected 3 replicas, got %d", *sts.Spec.Replicas)
+ }
+}
+
+func TestSeataServerReconciler_UpdateStatefulSet(t *testing.T) {
+ scheme := createTestScheme()
+ replicas3 := int32(3)
+
+ seataServer := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ UID: types.UID("test-uid-123"),
+ },
+ Spec: seatav1alpha1.SeataServerSpec{
+ ContainerSpec: seatav1alpha1.ContainerSpec{
+ ContainerName: "seata-server",
+ Image: "apache/seata-server:v2",
+ },
+ ServiceName: "seata-cluster",
+ Replicas: 3,
+ Ports: seatav1alpha1.Ports{
+ ServicePort: 8091,
+ ConsolePort: 7091,
+ RaftPort: 9091,
+ },
+ Persistence: seatav1alpha1.Persistence{
+ PersistentVolumeClaimSpec:
apiv1.PersistentVolumeClaimSpec{
+ Resources: apiv1.ResourceRequirements{
+ Requests: apiv1.ResourceList{
+ apiv1.ResourceStorage:
resource.MustParse("5Gi"),
+ },
+ },
+ },
+ },
+ },
+ }
+
+ existingSts := &appsv1.StatefulSet{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ },
+ Spec: appsv1.StatefulSetSpec{
+ Replicas: &replicas3,
+ Template: apiv1.PodTemplateSpec{
+ Spec: apiv1.PodSpec{
+ Containers: []apiv1.Container{
+ {
+ Name: "seata-server",
+ Image:
"apache/seata-server:v1",
+ },
+ },
+ },
+ },
+ },
+ Status: appsv1.StatefulSetStatus{
+ Replicas: 3,
+ ReadyReplicas: 3,
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(seataServer, existingSts).
+ WithStatusSubresource(seataServer, existingSts).
+ Build()
+
+ reconciler := &SeataServerReconciler{
+ Client: fakeClient,
+ Scheme: scheme,
+ Log: logr.Discard(),
+ }
+
+ err := reconciler.reconcileStatefulSet(context.Background(),
seataServer)
+ if err != nil {
+ t.Errorf("reconcileStatefulSet failed during update: %v", err)
+ }
+}
+
+func TestSeataServerReconciler_ReconcileFinalizers_Delete(t *testing.T) {
+ scheme := createTestScheme()
+
+ seataServer := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ UID: types.UID("test-uid-123"),
+ DeletionTimestamp: &metav1.Time{Time: time.Now()},
+ Finalizers: []string{"cleanUpSeataPVC"},
+ },
+ Spec: seatav1alpha1.SeataServerSpec{
+ Replicas: 1,
+ Persistence: seatav1alpha1.Persistence{
+ VolumeReclaimPolicy:
seatav1alpha1.VolumeReclaimPolicyDelete,
+ },
+ },
+ }
+
+ pvc := &apiv1.PersistentVolumeClaim{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pvc-0",
+ Namespace: "default",
+ Labels: map[string]string{
+ "app": "test-seata",
+ "uid": "test-uid-123",
+ },
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(seataServer, pvc).
+ Build()
+
+ reconciler := &SeataServerReconciler{
+ Client: fakeClient,
+ Scheme: scheme,
+ Log: logr.Discard(),
+ }
+
+ err := reconciler.reconcileFinalizers(context.Background(), seataServer)
+ if err != nil {
+ t.Errorf("reconcileFinalizers failed: %v", err)
+ }
+}
+
+func TestSeataServerReconciler_ReconcileFinalizers_Retain(t *testing.T) {
+ scheme := createTestScheme()
+
+ seataServer := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ UID: types.UID("test-uid-123"),
+ },
+ Spec: seatav1alpha1.SeataServerSpec{
+ Replicas: 1,
+ Persistence: seatav1alpha1.Persistence{
+ VolumeReclaimPolicy:
seatav1alpha1.VolumeReclaimPolicyRetain,
+ },
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(seataServer).
+ Build()
+
+ reconciler := &SeataServerReconciler{
+ Client: fakeClient,
+ Scheme: scheme,
+ Log: logr.Discard(),
+ }
+
+ err := reconciler.reconcileFinalizers(context.Background(), seataServer)
+ if err != nil {
+ t.Errorf("reconcileFinalizers with Retain policy failed: %v",
err)
+ }
+}
+
+func TestSeataServerReconciler_CleanupOrphanPVCs(t *testing.T) {
+ scheme := createTestScheme()
+
+ seataServer := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ UID: types.UID("test-uid-123"),
+ },
+ Spec: seatav1alpha1.SeataServerSpec{
+ Replicas: 2,
+ },
+ Status: seatav1alpha1.SeataServerStatus{
+ ReadyReplicas: 2,
+ },
+ }
+
+ // PVC 0 and 1 are in use, PVC 2 is orphan
+ pvc0 := &apiv1.PersistentVolumeClaim{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "seata-store-test-seata-0",
+ Namespace: "default",
+ Labels: map[string]string{
+ "app": "test-seata",
+ "uid": "test-uid-123",
+ },
+ },
+ }
+
+ pvc1 := &apiv1.PersistentVolumeClaim{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "seata-store-test-seata-1",
+ Namespace: "default",
+ Labels: map[string]string{
+ "app": "test-seata",
+ "uid": "test-uid-123",
+ },
+ },
+ }
+
+ pvc2 := &apiv1.PersistentVolumeClaim{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "seata-store-test-seata-2",
+ Namespace: "default",
+ Labels: map[string]string{
+ "app": "test-seata",
+ "uid": "test-uid-123",
+ },
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(seataServer, pvc0, pvc1, pvc2).
+ Build()
+
+ reconciler := &SeataServerReconciler{
+ Client: fakeClient,
+ Scheme: scheme,
+ Log: logr.Discard(),
+ }
+
+ err := reconciler.cleanupOrphanPVCs(context.Background(), seataServer)
+ if err != nil {
+ t.Errorf("cleanupOrphanPVCs failed: %v", err)
+ }
+}
+
+func TestSeataServerReconciler_GetPVCList(t *testing.T) {
+ scheme := createTestScheme()
+
+ seataServer := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ UID: types.UID("test-uid-123"),
+ },
+ Spec: seatav1alpha1.SeataServerSpec{
+ Replicas: 2,
+ },
+ }
+
+ pvc0 := &apiv1.PersistentVolumeClaim{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pvc-0",
+ Namespace: "default",
+ Labels: map[string]string{
+ "app": "test-seata",
+ "uid": "test-uid-123",
+ },
+ },
+ }
+
+ pvc1 := &apiv1.PersistentVolumeClaim{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pvc-1",
+ Namespace: "default",
+ Labels: map[string]string{
+ "app": "test-seata",
+ "uid": "test-uid-123",
+ },
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(seataServer, pvc0, pvc1).
+ Build()
+
+ reconciler := &SeataServerReconciler{
+ Client: fakeClient,
+ Scheme: scheme,
+ Log: logr.Discard(),
+ }
+
+ pvcList, err := reconciler.getPVCList(context.Background(), seataServer)
+ if err != nil {
+ t.Errorf("getPVCList failed: %v", err)
+ }
+
+ if len(pvcList.Items) != 2 {
+ t.Errorf("Expected 2 PVCs, got %d", len(pvcList.Items))
+ }
+}
+
+func TestSeataServerReconciler_GetPVCCount(t *testing.T) {
+ scheme := createTestScheme()
+
+ seataServer := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ UID: types.UID("test-uid-123"),
+ },
+ }
+
+ pvc := &apiv1.PersistentVolumeClaim{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pvc",
+ Namespace: "default",
+ Labels: map[string]string{
+ "app": "test-seata",
+ "uid": "test-uid-123",
+ },
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(seataServer, pvc).
+ Build()
+
+ reconciler := &SeataServerReconciler{
+ Client: fakeClient,
+ Scheme: scheme,
+ Log: logr.Discard(),
+ }
+
+ count, err := reconciler.getPVCCount(context.Background(), seataServer)
+ if err != nil {
+ t.Errorf("getPVCCount failed: %v", err)
+ }
+
+ if count != 1 {
+ t.Errorf("Expected PVC count 1, got %d", count)
+ }
+}
+
+func TestSeataServerReconciler_RecordError(t *testing.T) {
+ scheme := createTestScheme()
+
+ seataServer := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ },
+ Status: seatav1alpha1.SeataServerStatus{
+ Errors: []seatav1alpha1.SeataServerError{},
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(seataServer).
+ WithStatusSubresource(seataServer).
+ Build()
+
+ reconciler := &SeataServerReconciler{
+ Client: fakeClient,
+ Scheme: scheme,
+ Log: logr.Discard(),
+ }
+
+ err := reconciler.recordError(
+ context.Background(),
+ types.NamespacedName{Name: "test-seata", Namespace: "default"},
+ seatav1alpha1.ErrorTypeK8s_SeataServer,
+ "Test error message",
+ reconcile.TerminalError(nil),
+ )
+
+ if err != nil {
+ t.Errorf("recordError failed: %v", err)
+ }
+}
+
+func TestSeataServerReconciler_ReconcileClientObject(t *testing.T) {
+ scheme := createTestScheme()
+
+ seataServer := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ },
+ Spec: seatav1alpha1.SeataServerSpec{
+ ServiceName: "test-service",
+ Ports: seatav1alpha1.Ports{
+ ServicePort: 8091,
+ ConsolePort: 7091,
+ RaftPort: 9091,
+ },
+ },
+ }
+
+ svc := &apiv1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-service",
+ Namespace: "default",
+ },
+ Spec: apiv1.ServiceSpec{
+ Ports: []apiv1.ServicePort{
+ {Name: "service-port", Port: 8091},
+ },
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(seataServer).
+ Build()
+
+ reconciler := &SeataServerReconciler{
+ Client: fakeClient,
+ Scheme: scheme,
+ Log: logr.Discard(),
+ }
+
+ err := reconciler.reconcileClientObject(
+ context.Background(),
+ seataServer,
+ svc,
+ func() client.Object { return &apiv1.Service{} },
+ func(found, desired client.Object) {},
+ seatav1alpha1.ErrorTypeK8s_HeadlessService,
+ "Service",
+ )
+
+ if err != nil {
+ t.Errorf("reconcileClientObject failed: %v", err)
+ }
+
+ // Verify service was created
+ createdSvc := &apiv1.Service{}
+ err = fakeClient.Get(context.Background(), types.NamespacedName{
+ Name: "test-service",
+ Namespace: "default",
+ }, createdSvc)
+
+ if err != nil {
+ t.Errorf("Failed to get created service: %v", err)
+ }
+}
+
+func createTestScheme() *runtime.Scheme {
+ scheme := runtime.NewScheme()
+ _ = apiv1.AddToScheme(scheme)
+ _ = appsv1.AddToScheme(scheme)
+ _ = seatav1alpha1.AddToScheme(scheme)
+ return scheme
+}
+
diff --git a/pkg/finalizer/cleanup_handler_test.go
b/pkg/finalizer/cleanup_handler_test.go
new file mode 100644
index 0000000..b6c04a0
--- /dev/null
+++ b/pkg/finalizer/cleanup_handler_test.go
@@ -0,0 +1,338 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package finalizer
+
+import (
+ "context"
+ "testing"
+
+ "github.com/go-logr/logr"
+ apiv1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/api/resource"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+ seatav1 "github.com/apache/seata-k8s/api/v1"
+ seatav1alpha1 "github.com/apache/seata-k8s/api/v1alpha1"
+)
+
+func TestNewCleanupHandler(t *testing.T) {
+ scheme := createCleanupTestScheme()
+ fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
+ logger := logr.Discard()
+
+ handler := NewCleanupHandler(fakeClient, logger)
+
+ if handler == nil {
+ t.Fatal("NewCleanupHandler should not return nil")
+ }
+ if handler.Client == nil {
+ t.Error("CleanupHandler.Client should not be nil")
+ }
+}
+
+func TestHandleCleanup_V1(t *testing.T) {
+ scheme := createCleanupTestScheme()
+
+ seataServer := &seatav1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ UID: types.UID("test-uid-123"),
+ },
+ Spec: seatav1.SeataServerSpec{
+ Persistence: seatav1.Persistence{
+ VolumeReclaimPolicy:
seatav1.VolumeReclaimPolicyDelete,
+ PersistentVolumeClaimSpec:
apiv1.PersistentVolumeClaimSpec{
+ Resources: apiv1.ResourceRequirements{
+ Requests: apiv1.ResourceList{
+ apiv1.ResourceStorage:
resource.MustParse("5Gi"),
+ },
+ },
+ },
+ },
+ },
+ }
+
+ pvc := &apiv1.PersistentVolumeClaim{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pvc",
+ Namespace: "default",
+ Labels: map[string]string{
+ "app": "test-seata",
+ "uid": "test-uid-123",
+ },
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(seataServer, pvc).
+ Build()
+
+ handler := NewCleanupHandler(fakeClient, logr.Discard())
+
+ err := handler.HandleCleanup(context.Background(), seataServer)
+ if err != nil {
+ t.Errorf("HandleCleanup failed: %v", err)
+ }
+}
+
+func TestHandleCleanup_V1Alpha1(t *testing.T) {
+ scheme := createCleanupTestScheme()
+
+ seataServer := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ UID: types.UID("test-uid-123"),
+ },
+ Spec: seatav1alpha1.SeataServerSpec{
+ Persistence: seatav1alpha1.Persistence{
+ VolumeReclaimPolicy:
seatav1alpha1.VolumeReclaimPolicyDelete,
+ PersistentVolumeClaimSpec:
apiv1.PersistentVolumeClaimSpec{
+ Resources: apiv1.ResourceRequirements{
+ Requests: apiv1.ResourceList{
+ apiv1.ResourceStorage:
resource.MustParse("5Gi"),
+ },
+ },
+ },
+ },
+ },
+ }
+
+ pvc := &apiv1.PersistentVolumeClaim{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pvc",
+ Namespace: "default",
+ Labels: map[string]string{
+ "app": "test-seata",
+ "uid": "test-uid-123",
+ },
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(seataServer, pvc).
+ Build()
+
+ handler := NewCleanupHandler(fakeClient, logr.Discard())
+
+ err := handler.HandleCleanup(context.Background(), seataServer)
+ if err != nil {
+ t.Errorf("HandleCleanup failed: %v", err)
+ }
+}
+
+func TestHandleCleanup_UnsupportedType(t *testing.T) {
+ scheme := createCleanupTestScheme()
+ fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
+
+ handler := NewCleanupHandler(fakeClient, logr.Discard())
+
+ // Pass an unsupported type
+ err := handler.HandleCleanup(context.Background(), "unsupported")
+ if err == nil {
+ t.Error("HandleCleanup should return error for unsupported
type")
+ }
+}
+
+func TestHandleCleanup_V1_RetainPolicy(t *testing.T) {
+ scheme := createCleanupTestScheme()
+
+ seataServer := &seatav1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ UID: types.UID("test-uid-123"),
+ },
+ Spec: seatav1.SeataServerSpec{
+ Persistence: seatav1.Persistence{
+ VolumeReclaimPolicy:
seatav1.VolumeReclaimPolicyRetain,
+ },
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(seataServer).
+ Build()
+
+ handler := NewCleanupHandler(fakeClient, logr.Discard())
+
+ err := handler.HandleCleanup(context.Background(), seataServer)
+ if err != nil {
+ t.Errorf("HandleCleanup with Retain policy failed: %v", err)
+ }
+}
+
+func TestCleanupAllPVCs(t *testing.T) {
+ scheme := createCleanupTestScheme()
+
+ pvc1 := &apiv1.PersistentVolumeClaim{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pvc-1",
+ Namespace: "default",
+ Labels: map[string]string{
+ "app": "test-seata",
+ "uid": "test-uid-123",
+ },
+ },
+ }
+
+ pvc2 := &apiv1.PersistentVolumeClaim{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pvc-2",
+ Namespace: "default",
+ Labels: map[string]string{
+ "app": "test-seata",
+ "uid": "test-uid-123",
+ },
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(pvc1, pvc2).
+ Build()
+
+ handler := NewCleanupHandler(fakeClient, logr.Discard())
+
+ err := handler.cleanupAllPVCs(context.Background(), "test-seata",
"default", types.UID("test-uid-123"))
+ if err != nil {
+ t.Errorf("cleanupAllPVCs failed: %v", err)
+ }
+}
+
+func TestGetPVCList(t *testing.T) {
+ scheme := createCleanupTestScheme()
+
+ pvc := &apiv1.PersistentVolumeClaim{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pvc",
+ Namespace: "default",
+ Labels: map[string]string{
+ "app": "test-seata",
+ "uid": "test-uid-123",
+ },
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(pvc).
+ Build()
+
+ handler := NewCleanupHandler(fakeClient, logr.Discard())
+
+ pvcList, err := handler.getPVCList(context.Background(), "test-seata",
"default", types.UID("test-uid-123"))
+ if err != nil {
+ t.Errorf("getPVCList failed: %v", err)
+ }
+
+ if len(pvcList.Items) != 1 {
+ t.Errorf("Expected 1 PVC, got %d", len(pvcList.Items))
+ }
+}
+
+func TestDeletePVC(t *testing.T) {
+ scheme := createCleanupTestScheme()
+
+ pvc := &apiv1.PersistentVolumeClaim{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pvc",
+ Namespace: "default",
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(pvc).
+ Build()
+
+ handler := NewCleanupHandler(fakeClient, logr.Discard())
+
+ err := handler.deletePVC(context.Background(), pvc)
+ if err != nil {
+ t.Errorf("deletePVC failed: %v", err)
+ }
+}
+
+func TestCleanupConfigMaps(t *testing.T) {
+ scheme := createCleanupTestScheme()
+
+ cm := &apiv1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-cm",
+ Namespace: "default",
+ Labels: map[string]string{
+ "app": "test-seata",
+ },
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(cm).
+ Build()
+
+ handler := NewCleanupHandler(fakeClient, logr.Discard())
+
+ err := handler.cleanupConfigMaps(context.Background(), "test-seata",
"default")
+ if err != nil {
+ t.Errorf("cleanupConfigMaps failed: %v", err)
+ }
+}
+
+func TestCleanupSecrets(t *testing.T) {
+ scheme := createCleanupTestScheme()
+
+ secret := &apiv1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-secret",
+ Namespace: "default",
+ Labels: map[string]string{
+ "app": "test-seata",
+ },
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(secret).
+ Build()
+
+ handler := NewCleanupHandler(fakeClient, logr.Discard())
+
+ err := handler.cleanupSecrets(context.Background(), "test-seata",
"default")
+ if err != nil {
+ t.Errorf("cleanupSecrets failed: %v", err)
+ }
+}
+
+func createCleanupTestScheme() *runtime.Scheme {
+ scheme := runtime.NewScheme()
+ _ = apiv1.AddToScheme(scheme)
+ _ = seatav1.AddToScheme(scheme)
+ _ = seatav1alpha1.AddToScheme(scheme)
+ return scheme
+}
+
diff --git a/pkg/seata/fetchers_test.go b/pkg/seata/fetchers_test.go
new file mode 100644
index 0000000..fbec08e
--- /dev/null
+++ b/pkg/seata/fetchers_test.go
@@ -0,0 +1,429 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package seata
+
+import (
+ "context"
+ "testing"
+
+ seatav1alpha1 "github.com/apache/seata-k8s/api/v1alpha1"
+ v1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+)
+
+func TestFetchEnvVar_DirectValue(t *testing.T) {
+ scheme := runtime.NewScheme()
+ _ = v1.AddToScheme(scheme)
+ _ = seatav1alpha1.AddToScheme(scheme)
+
+ fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
+
+ cr := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ },
+ }
+
+ envVar := v1.EnvVar{
+ Name: "TEST_VAR",
+ Value: "test-value",
+ }
+
+ result, err := FetchEnvVar(context.Background(), fakeClient, cr, envVar)
+ if err != nil {
+ t.Errorf("FetchEnvVar failed: %v", err)
+ }
+
+ if result != "test-value" {
+ t.Errorf("Expected 'test-value', got '%s'", result)
+ }
+}
+
+func TestFetchEnvVar_FromConfigMap(t *testing.T) {
+ scheme := runtime.NewScheme()
+ _ = v1.AddToScheme(scheme)
+ _ = seatav1alpha1.AddToScheme(scheme)
+
+ configMap := &v1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-cm",
+ Namespace: "default",
+ },
+ Data: map[string]string{
+ "username": "admin",
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(configMap).
+ Build()
+
+ cr := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ },
+ }
+
+ envVar := v1.EnvVar{
+ Name: "TEST_VAR",
+ ValueFrom: &v1.EnvVarSource{
+ ConfigMapKeyRef: &v1.ConfigMapKeySelector{
+ LocalObjectReference: v1.LocalObjectReference{
+ Name: "test-cm",
+ },
+ Key: "username",
+ },
+ },
+ }
+
+ result, err := FetchEnvVar(context.Background(), fakeClient, cr, envVar)
+ if err != nil {
+ t.Errorf("FetchEnvVar failed: %v", err)
+ }
+
+ if result != "admin" {
+ t.Errorf("Expected 'admin', got '%s'", result)
+ }
+}
+
+func TestFetchEnvVar_FromConfigMap_KeyNotFound(t *testing.T) {
+ scheme := runtime.NewScheme()
+ _ = v1.AddToScheme(scheme)
+ _ = seatav1alpha1.AddToScheme(scheme)
+
+ configMap := &v1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-cm",
+ Namespace: "default",
+ },
+ Data: map[string]string{
+ "username": "admin",
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(configMap).
+ Build()
+
+ cr := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ },
+ }
+
+ envVar := v1.EnvVar{
+ Name: "TEST_VAR",
+ ValueFrom: &v1.EnvVarSource{
+ ConfigMapKeyRef: &v1.ConfigMapKeySelector{
+ LocalObjectReference: v1.LocalObjectReference{
+ Name: "test-cm",
+ },
+ Key: "nonexistent",
+ },
+ },
+ }
+
+ _, err := FetchEnvVar(context.Background(), fakeClient, cr, envVar)
+ if err == nil {
+ t.Error("Expected error for non-existent key, got nil")
+ }
+}
+
+func TestFetchEnvVar_FromConfigMap_Optional(t *testing.T) {
+ scheme := runtime.NewScheme()
+ _ = v1.AddToScheme(scheme)
+ _ = seatav1alpha1.AddToScheme(scheme)
+
+ configMap := &v1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-cm",
+ Namespace: "default",
+ },
+ Data: map[string]string{
+ "username": "admin",
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(configMap).
+ Build()
+
+ cr := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ },
+ }
+
+ optional := true
+ envVar := v1.EnvVar{
+ Name: "TEST_VAR",
+ ValueFrom: &v1.EnvVarSource{
+ ConfigMapKeyRef: &v1.ConfigMapKeySelector{
+ LocalObjectReference: v1.LocalObjectReference{
+ Name: "test-cm",
+ },
+ Key: "nonexistent",
+ Optional: &optional,
+ },
+ },
+ }
+
+ result, err := FetchEnvVar(context.Background(), fakeClient, cr, envVar)
+ if err != nil {
+ t.Errorf("FetchEnvVar failed: %v", err)
+ }
+
+ if result != "" {
+ t.Errorf("Expected empty string for optional missing key, got
'%s'", result)
+ }
+}
+
+func TestFetchEnvVar_FromSecret(t *testing.T) {
+ scheme := runtime.NewScheme()
+ _ = v1.AddToScheme(scheme)
+ _ = seatav1alpha1.AddToScheme(scheme)
+
+ secret := &v1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-secret",
+ Namespace: "default",
+ },
+ Data: map[string][]byte{
+ "password": []byte("secret123"),
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(secret).
+ Build()
+
+ cr := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ },
+ }
+
+ envVar := v1.EnvVar{
+ Name: "TEST_VAR",
+ ValueFrom: &v1.EnvVarSource{
+ SecretKeyRef: &v1.SecretKeySelector{
+ LocalObjectReference: v1.LocalObjectReference{
+ Name: "test-secret",
+ },
+ Key: "password",
+ },
+ },
+ }
+
+ result, err := FetchEnvVar(context.Background(), fakeClient, cr, envVar)
+ if err != nil {
+ t.Errorf("FetchEnvVar failed: %v", err)
+ }
+
+ if result != "secret123" {
+ t.Errorf("Expected 'secret123', got '%s'", result)
+ }
+}
+
+func TestFetchEnvVar_FromSecret_KeyNotFound(t *testing.T) {
+ scheme := runtime.NewScheme()
+ _ = v1.AddToScheme(scheme)
+ _ = seatav1alpha1.AddToScheme(scheme)
+
+ secret := &v1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-secret",
+ Namespace: "default",
+ },
+ Data: map[string][]byte{
+ "password": []byte("secret123"),
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(secret).
+ Build()
+
+ cr := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ },
+ }
+
+ envVar := v1.EnvVar{
+ Name: "TEST_VAR",
+ ValueFrom: &v1.EnvVarSource{
+ SecretKeyRef: &v1.SecretKeySelector{
+ LocalObjectReference: v1.LocalObjectReference{
+ Name: "test-secret",
+ },
+ Key: "nonexistent",
+ },
+ },
+ }
+
+ _, err := FetchEnvVar(context.Background(), fakeClient, cr, envVar)
+ if err == nil {
+ t.Error("Expected error for non-existent key, got nil")
+ }
+}
+
+func TestFetchEnvVar_FromSecret_Optional(t *testing.T) {
+ scheme := runtime.NewScheme()
+ _ = v1.AddToScheme(scheme)
+ _ = seatav1alpha1.AddToScheme(scheme)
+
+ secret := &v1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-secret",
+ Namespace: "default",
+ },
+ Data: map[string][]byte{
+ "password": []byte("secret123"),
+ },
+ }
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(secret).
+ Build()
+
+ cr := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ },
+ }
+
+ optional := true
+ envVar := v1.EnvVar{
+ Name: "TEST_VAR",
+ ValueFrom: &v1.EnvVarSource{
+ SecretKeyRef: &v1.SecretKeySelector{
+ LocalObjectReference: v1.LocalObjectReference{
+ Name: "test-secret",
+ },
+ Key: "nonexistent",
+ Optional: &optional,
+ },
+ },
+ }
+
+ result, err := FetchEnvVar(context.Background(), fakeClient, cr, envVar)
+ if err != nil {
+ t.Errorf("FetchEnvVar failed: %v", err)
+ }
+
+ if result != "" {
+ t.Errorf("Expected empty string for optional missing key, got
'%s'", result)
+ }
+}
+
+func TestFetchEnvVar_ConfigMapNotFound_Optional(t *testing.T) {
+ scheme := runtime.NewScheme()
+ _ = v1.AddToScheme(scheme)
+ _ = seatav1alpha1.AddToScheme(scheme)
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ Build()
+
+ cr := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ },
+ }
+
+ optional := true
+ envVar := v1.EnvVar{
+ Name: "TEST_VAR",
+ ValueFrom: &v1.EnvVarSource{
+ ConfigMapKeyRef: &v1.ConfigMapKeySelector{
+ LocalObjectReference: v1.LocalObjectReference{
+ Name: "nonexistent-cm",
+ },
+ Key: "key",
+ Optional: &optional,
+ },
+ },
+ }
+
+ result, err := FetchEnvVar(context.Background(), fakeClient, cr, envVar)
+ if err != nil {
+ t.Errorf("FetchEnvVar failed: %v", err)
+ }
+
+ if result != "" {
+ t.Errorf("Expected empty string for optional missing ConfigMap,
got '%s'", result)
+ }
+}
+
+func TestFetchEnvVar_SecretNotFound_Optional(t *testing.T) {
+ scheme := runtime.NewScheme()
+ _ = v1.AddToScheme(scheme)
+ _ = seatav1alpha1.AddToScheme(scheme)
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ Build()
+
+ cr := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ },
+ }
+
+ optional := true
+ envVar := v1.EnvVar{
+ Name: "TEST_VAR",
+ ValueFrom: &v1.EnvVarSource{
+ SecretKeyRef: &v1.SecretKeySelector{
+ LocalObjectReference: v1.LocalObjectReference{
+ Name: "nonexistent-secret",
+ },
+ Key: "key",
+ Optional: &optional,
+ },
+ },
+ }
+
+ result, err := FetchEnvVar(context.Background(), fakeClient, cr, envVar)
+ if err != nil {
+ t.Errorf("FetchEnvVar failed: %v", err)
+ }
+
+ if result != "" {
+ t.Errorf("Expected empty string for optional missing Secret,
got '%s'", result)
+ }
+}
+
diff --git a/pkg/seata/generators_test.go b/pkg/seata/generators_test.go
new file mode 100644
index 0000000..3abfb6d
--- /dev/null
+++ b/pkg/seata/generators_test.go
@@ -0,0 +1,342 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package seata
+
+import (
+ "strings"
+ "testing"
+
+ seatav1alpha1 "github.com/apache/seata-k8s/api/v1alpha1"
+ apiv1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/api/resource"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+)
+
+func TestMakeLabels(t *testing.T) {
+ name := "test-seata"
+ labels := makeLabels(name)
+
+ if labels["cr_name"] != name {
+ t.Errorf("Expected label cr_name='%s', got '%s'", name,
labels["cr_name"])
+ }
+}
+
+func TestMakeHeadlessService(t *testing.T) {
+ seataServer := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ },
+ Spec: seatav1alpha1.SeataServerSpec{
+ ServiceName: "seata-cluster",
+ Ports: seatav1alpha1.Ports{
+ ServicePort: 8091,
+ ConsolePort: 7091,
+ RaftPort: 9091,
+ },
+ },
+ }
+
+ svc := MakeHeadlessService(seataServer)
+
+ if svc.Name != "seata-cluster" {
+ t.Errorf("Expected service name 'seata-cluster', got '%s'",
svc.Name)
+ }
+
+ if svc.Namespace != "default" {
+ t.Errorf("Expected namespace 'default', got '%s'",
svc.Namespace)
+ }
+
+ if svc.Spec.ClusterIP != "None" {
+ t.Errorf("Expected ClusterIP 'None', got '%s'",
svc.Spec.ClusterIP)
+ }
+
+ if len(svc.Spec.Ports) != 3 {
+ t.Errorf("Expected 3 ports, got %d", len(svc.Spec.Ports))
+ }
+
+ // Verify ports
+ portMap := make(map[string]int32)
+ for _, port := range svc.Spec.Ports {
+ portMap[port.Name] = port.Port
+ }
+
+ if portMap["service-port"] != 8091 {
+ t.Errorf("Expected service-port 8091, got %d",
portMap["service-port"])
+ }
+ if portMap["console-port"] != 7091 {
+ t.Errorf("Expected console-port 7091, got %d",
portMap["console-port"])
+ }
+ if portMap["raft-port"] != 9091 {
+ t.Errorf("Expected raft-port 9091, got %d",
portMap["raft-port"])
+ }
+}
+
+func TestMakeStatefulSet(t *testing.T) {
+ seataServer := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ UID: types.UID("test-uid-123"),
+ Labels: map[string]string{
+ "app": "seata",
+ },
+ Annotations: map[string]string{
+ "description": "test seata server",
+ },
+ },
+ Spec: seatav1alpha1.SeataServerSpec{
+ ContainerSpec: seatav1alpha1.ContainerSpec{
+ ContainerName: "seata-server",
+ Image: "apache/seata-server:latest",
+ Resources: apiv1.ResourceRequirements{
+ Requests: apiv1.ResourceList{
+ apiv1.ResourceCPU:
resource.MustParse("500m"),
+ apiv1.ResourceMemory:
resource.MustParse("1Gi"),
+ },
+ },
+ },
+ ServiceName: "seata-cluster",
+ Replicas: 3,
+ Ports: seatav1alpha1.Ports{
+ ServicePort: 8091,
+ ConsolePort: 7091,
+ RaftPort: 9091,
+ },
+ Persistence: seatav1alpha1.Persistence{
+ PersistentVolumeClaimSpec:
apiv1.PersistentVolumeClaimSpec{
+ Resources: apiv1.ResourceRequirements{
+ Requests: apiv1.ResourceList{
+ apiv1.ResourceStorage:
resource.MustParse("5Gi"),
+ },
+ },
+ },
+ },
+ },
+ }
+
+ sts := MakeStatefulSet(seataServer)
+
+ if sts.Name != "test-seata" {
+ t.Errorf("Expected StatefulSet name 'test-seata', got '%s'",
sts.Name)
+ }
+
+ if sts.Namespace != "default" {
+ t.Errorf("Expected namespace 'default', got '%s'",
sts.Namespace)
+ }
+
+ if *sts.Spec.Replicas != 3 {
+ t.Errorf("Expected 3 replicas, got %d", *sts.Spec.Replicas)
+ }
+
+ if sts.Spec.ServiceName != "seata-cluster" {
+ t.Errorf("Expected service name 'seata-cluster', got '%s'",
sts.Spec.ServiceName)
+ }
+
+ // Check VolumeClaimTemplates
+ if len(sts.Spec.VolumeClaimTemplates) != 1 {
+ t.Fatalf("Expected 1 VolumeClaimTemplate, got %d",
len(sts.Spec.VolumeClaimTemplates))
+ }
+
+ pvc := sts.Spec.VolumeClaimTemplates[0]
+ if pvc.Name != "seata-store" {
+ t.Errorf("Expected PVC name 'seata-store', got '%s'", pvc.Name)
+ }
+
+ if pvc.Labels["app"] != "test-seata" {
+ t.Errorf("Expected PVC label app='test-seata', got '%s'",
pvc.Labels["app"])
+ }
+
+ if pvc.Labels["uid"] != "test-uid-123" {
+ t.Errorf("Expected PVC label uid='test-uid-123', got '%s'",
pvc.Labels["uid"])
+ }
+
+ // Check container
+ if len(sts.Spec.Template.Spec.Containers) != 1 {
+ t.Fatalf("Expected 1 container, got %d",
len(sts.Spec.Template.Spec.Containers))
+ }
+
+ container := sts.Spec.Template.Spec.Containers[0]
+ if container.Name != "seata-server" {
+ t.Errorf("Expected container name 'seata-server', got '%s'",
container.Name)
+ }
+
+ if container.Image != "apache/seata-server:latest" {
+ t.Errorf("Expected image 'apache/seata-server:latest', got
'%s'", container.Image)
+ }
+
+ // Check ports
+ if len(container.Ports) != 3 {
+ t.Fatalf("Expected 3 container ports, got %d",
len(container.Ports))
+ }
+
+ // Check volume mounts
+ if len(container.VolumeMounts) != 1 {
+ t.Fatalf("Expected 1 volume mount, got %d",
len(container.VolumeMounts))
+ }
+
+ if container.VolumeMounts[0].Name != "seata-store" {
+ t.Errorf("Expected volume mount name 'seata-store', got '%s'",
container.VolumeMounts[0].Name)
+ }
+
+ if container.VolumeMounts[0].MountPath != "/seata-server/sessionStore" {
+ t.Errorf("Expected mount path '/seata-server/sessionStore', got
'%s'", container.VolumeMounts[0].MountPath)
+ }
+}
+
+func TestBuildEntrypointScript(t *testing.T) {
+ seataServer := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ },
+ Spec: seatav1alpha1.SeataServerSpec{
+ ServiceName: "seata-cluster",
+ },
+ }
+
+ script := buildEntrypointScript(seataServer)
+
+ if !strings.Contains(script, "SEATA_IP=$(HOST_NAME).seata-cluster") {
+ t.Error("Script should contain SEATA_IP export")
+ }
+
+ if !strings.Contains(script, "python3 -c") {
+ t.Error("Script should contain python3 command")
+ }
+
+ if !strings.Contains(script, "/seata-server-entrypoint.sh") {
+ t.Error("Script should contain seata-server-entrypoint.sh")
+ }
+}
+
+func TestBuildEnvVars(t *testing.T) {
+ seataServer := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ },
+ Spec: seatav1alpha1.SeataServerSpec{
+ ContainerSpec: seatav1alpha1.ContainerSpec{
+ Env: []apiv1.EnvVar{
+ {Name: "CUSTOM_ENV", Value:
"custom-value"},
+ },
+ },
+ Ports: seatav1alpha1.Ports{
+ ServicePort: 8091,
+ ConsolePort: 7091,
+ RaftPort: 9091,
+ },
+ },
+ }
+
+ envs := buildEnvVars(seataServer)
+
+ // Check that we have at least the base envs + custom env
+ if len(envs) < 5 {
+ t.Errorf("Expected at least 5 env vars, got %d", len(envs))
+ }
+
+ // Create a map for easier checking
+ envMap := make(map[string]string)
+ for _, env := range envs {
+ if env.ValueFrom == nil {
+ envMap[env.Name] = env.Value
+ }
+ }
+
+ // Check standard environment variables
+ if envMap["store.mode"] != "raft" {
+ t.Errorf("Expected store.mode='raft', got '%s'",
envMap["store.mode"])
+ }
+
+ if envMap["server.port"] != "7091" {
+ t.Errorf("Expected server.port='7091', got '%s'",
envMap["server.port"])
+ }
+
+ if envMap["server.servicePort"] != "8091" {
+ t.Errorf("Expected server.servicePort='8091', got '%s'",
envMap["server.servicePort"])
+ }
+
+ // Check custom env
+ if envMap["CUSTOM_ENV"] != "custom-value" {
+ t.Errorf("Expected CUSTOM_ENV='custom-value', got '%s'",
envMap["CUSTOM_ENV"])
+ }
+
+ // Check HOST_NAME has ValueFrom
+ hasHostName := false
+ for _, env := range envs {
+ if env.Name == "HOST_NAME" && env.ValueFrom != nil {
+ hasHostName = true
+ if env.ValueFrom.FieldRef.FieldPath != "metadata.name" {
+ t.Errorf("Expected HOST_NAME field path
'metadata.name', got '%s'", env.ValueFrom.FieldRef.FieldPath)
+ }
+ }
+ }
+ if !hasHostName {
+ t.Error("Expected HOST_NAME environment variable with
ValueFrom")
+ }
+
+ // Check server.raft.serverAddr exists
+ if _, exists := envMap["server.raft.serverAddr"]; !exists {
+ t.Error("Expected server.raft.serverAddr environment variable")
+ }
+}
+
+func TestBuildEnvVars_WithMultipleReplicas(t *testing.T) {
+ seataServer := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-seata",
+ Namespace: "default",
+ },
+ Spec: seatav1alpha1.SeataServerSpec{
+ Replicas: 3,
+ Ports: seatav1alpha1.Ports{
+ ServicePort: 8091,
+ ConsolePort: 7091,
+ RaftPort: 9091,
+ },
+ ServiceName: "seata-cluster",
+ },
+ }
+
+ envs := buildEnvVars(seataServer)
+
+ envMap := make(map[string]string)
+ for _, env := range envs {
+ if env.ValueFrom == nil {
+ envMap[env.Name] = env.Value
+ }
+ }
+
+ raftAddr := envMap["server.raft.serverAddr"]
+ // Should contain all 3 replicas in the address
+ expectedSubstrings := []string{
+ "test-seata-0.seata-cluster:9091",
+ "test-seata-1.seata-cluster:9091",
+ "test-seata-2.seata-cluster:9091",
+ }
+
+ for _, substr := range expectedSubstrings {
+ if !strings.Contains(raftAddr, substr) {
+ t.Errorf("Expected raft address to contain '%s', got
'%s'", substr, raftAddr)
+ }
+ }
+}
+
diff --git a/pkg/seata/synchronizers_test.go b/pkg/seata/synchronizers_test.go
new file mode 100644
index 0000000..bcae8d2
--- /dev/null
+++ b/pkg/seata/synchronizers_test.go
@@ -0,0 +1,264 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package seata
+
+import (
+ "testing"
+
+ appsv1 "k8s.io/api/apps/v1"
+ apiv1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/api/resource"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func TestSyncService(t *testing.T) {
+ // Current service
+ curr := &apiv1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-service",
+ Namespace: "default",
+ },
+ Spec: apiv1.ServiceSpec{
+ Ports: []apiv1.ServicePort{
+ {Name: "old-port", Port: 8080},
+ },
+ },
+ }
+
+ // Next/desired service
+ next := &apiv1.Service{
+ Spec: apiv1.ServiceSpec{
+ Ports: []apiv1.ServicePort{
+ {Name: "service-port", Port: 8091},
+ {Name: "console-port", Port: 7091},
+ {Name: "raft-port", Port: 9091},
+ },
+ },
+ }
+
+ SyncService(curr, next)
+
+ // Verify ports are synced
+ if len(curr.Spec.Ports) != 3 {
+ t.Errorf("Expected 3 ports after sync, got %d",
len(curr.Spec.Ports))
+ }
+
+ portMap := make(map[string]int32)
+ for _, port := range curr.Spec.Ports {
+ portMap[port.Name] = port.Port
+ }
+
+ if portMap["service-port"] != 8091 {
+ t.Errorf("Expected service-port 8091, got %d",
portMap["service-port"])
+ }
+ if portMap["console-port"] != 7091 {
+ t.Errorf("Expected console-port 7091, got %d",
portMap["console-port"])
+ }
+ if portMap["raft-port"] != 9091 {
+ t.Errorf("Expected raft-port 9091, got %d",
portMap["raft-port"])
+ }
+}
+
+func TestSyncStatefulSet(t *testing.T) {
+ replicas1 := int32(1)
+ replicas3 := int32(3)
+
+ // Current StatefulSet
+ curr := &appsv1.StatefulSet{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-sts",
+ Namespace: "default",
+ },
+ Spec: appsv1.StatefulSetSpec{
+ Replicas: &replicas1,
+ Template: apiv1.PodTemplateSpec{
+ Spec: apiv1.PodSpec{
+ Containers: []apiv1.Container{
+ {
+ Name: "old-container",
+ Image: "old-image:v1",
+ },
+ },
+ },
+ },
+ },
+ }
+
+ // Next/desired StatefulSet
+ next := &appsv1.StatefulSet{
+ Spec: appsv1.StatefulSetSpec{
+ Replicas: &replicas3,
+ Template: apiv1.PodTemplateSpec{
+ ObjectMeta: metav1.ObjectMeta{
+ Labels: map[string]string{
+ "app": "seata",
+ },
+ },
+ Spec: apiv1.PodSpec{
+ Containers: []apiv1.Container{
+ {
+ Name: "seata-server",
+ Image:
"apache/seata-server:latest",
+ Resources:
apiv1.ResourceRequirements{
+ Requests:
apiv1.ResourceList{
+
apiv1.ResourceCPU: resource.MustParse("500m"),
+
apiv1.ResourceMemory: resource.MustParse("1Gi"),
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ SyncStatefulSet(curr, next)
+
+ // Verify template is synced
+ if len(curr.Spec.Template.Spec.Containers) != 1 {
+ t.Errorf("Expected 1 container after sync, got %d",
len(curr.Spec.Template.Spec.Containers))
+ }
+
+ container := curr.Spec.Template.Spec.Containers[0]
+ if container.Name != "seata-server" {
+ t.Errorf("Expected container name 'seata-server', got '%s'",
container.Name)
+ }
+
+ if container.Image != "apache/seata-server:latest" {
+ t.Errorf("Expected image 'apache/seata-server:latest', got
'%s'", container.Image)
+ }
+
+ // Verify replicas are synced
+ if *curr.Spec.Replicas != 3 {
+ t.Errorf("Expected 3 replicas after sync, got %d",
*curr.Spec.Replicas)
+ }
+
+ // Verify labels are synced
+ if curr.Spec.Template.Labels["app"] != "seata" {
+ t.Errorf("Expected template label app='seata', got '%s'",
curr.Spec.Template.Labels["app"])
+ }
+}
+
+func TestSyncStatefulSet_EmptyReplicas(t *testing.T) {
+ replicas1 := int32(1)
+
+ // Current StatefulSet
+ curr := &appsv1.StatefulSet{
+ Spec: appsv1.StatefulSetSpec{
+ Replicas: nil,
+ Template: apiv1.PodTemplateSpec{
+ Spec: apiv1.PodSpec{
+ Containers: []apiv1.Container{
+ {Name: "old", Image: "old:v1"},
+ },
+ },
+ },
+ },
+ }
+
+ // Next StatefulSet
+ next := &appsv1.StatefulSet{
+ Spec: appsv1.StatefulSetSpec{
+ Replicas: &replicas1,
+ Template: apiv1.PodTemplateSpec{
+ Spec: apiv1.PodSpec{
+ Containers: []apiv1.Container{
+ {Name: "new", Image: "new:v2"},
+ },
+ },
+ },
+ },
+ }
+
+ SyncStatefulSet(curr, next)
+
+ if curr.Spec.Replicas == nil {
+ t.Error("Expected replicas to be set")
+ } else if *curr.Spec.Replicas != 1 {
+ t.Errorf("Expected 1 replica, got %d", *curr.Spec.Replicas)
+ }
+}
+
+func TestSyncService_EmptyPorts(t *testing.T) {
+ curr := &apiv1.Service{
+ Spec: apiv1.ServiceSpec{
+ Ports: []apiv1.ServicePort{
+ {Name: "port1", Port: 8080},
+ {Name: "port2", Port: 9090},
+ },
+ },
+ }
+
+ next := &apiv1.Service{
+ Spec: apiv1.ServiceSpec{
+ Ports: []apiv1.ServicePort{},
+ },
+ }
+
+ SyncService(curr, next)
+
+ if len(curr.Spec.Ports) != 0 {
+ t.Errorf("Expected 0 ports after sync, got %d",
len(curr.Spec.Ports))
+ }
+}
+
+func TestSyncStatefulSet_WithMultipleContainers(t *testing.T) {
+ replicas2 := int32(2)
+
+ curr := &appsv1.StatefulSet{
+ Spec: appsv1.StatefulSetSpec{
+ Replicas: &replicas2,
+ Template: apiv1.PodTemplateSpec{
+ Spec: apiv1.PodSpec{
+ Containers: []apiv1.Container{
+ {Name: "container1", Image:
"image1:v1"},
+ },
+ },
+ },
+ },
+ }
+
+ next := &appsv1.StatefulSet{
+ Spec: appsv1.StatefulSetSpec{
+ Replicas: &replicas2,
+ Template: apiv1.PodTemplateSpec{
+ Spec: apiv1.PodSpec{
+ Containers: []apiv1.Container{
+ {Name: "container1", Image:
"image1:v2"},
+ {Name: "sidecar", Image:
"sidecar:latest"},
+ },
+ },
+ },
+ },
+ }
+
+ SyncStatefulSet(curr, next)
+
+ if len(curr.Spec.Template.Spec.Containers) != 2 {
+ t.Errorf("Expected 2 containers after sync, got %d",
len(curr.Spec.Template.Spec.Containers))
+ }
+
+ if curr.Spec.Template.Spec.Containers[0].Image != "image1:v2" {
+ t.Errorf("Expected first container image 'image1:v2', got
'%s'", curr.Spec.Template.Spec.Containers[0].Image)
+ }
+
+ if curr.Spec.Template.Spec.Containers[1].Name != "sidecar" {
+ t.Errorf("Expected second container name 'sidecar', got '%s'",
curr.Spec.Template.Spec.Containers[1].Name)
+ }
+}
+
diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go
new file mode 100644
index 0000000..a9f0fd4
--- /dev/null
+++ b/pkg/utils/utils_test.go
@@ -0,0 +1,360 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package utils
+
+import (
+ "strings"
+ "testing"
+
+ seatav1alpha1 "github.com/apache/seata-k8s/api/v1alpha1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func TestConcatRaftServerAddress(t *testing.T) {
+ testCases := []struct {
+ name string
+ seataServer *seatav1alpha1.SeataServer
+ expectedParts []string
+ expectedLength int
+ }{
+ {
+ name: "single replica",
+ seataServer: &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "seata",
+ },
+ Spec: seatav1alpha1.SeataServerSpec{
+ Replicas: 1,
+ ServiceName: "seata-svc",
+ Ports: seatav1alpha1.Ports{
+ RaftPort: 9091,
+ },
+ },
+ },
+ expectedParts: []string{"seata-0.seata-svc:9091"},
+ expectedLength: 1,
+ },
+ {
+ name: "three replicas",
+ seataServer: &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "seata-cluster",
+ },
+ Spec: seatav1alpha1.SeataServerSpec{
+ Replicas: 3,
+ ServiceName: "seata-headless",
+ Ports: seatav1alpha1.Ports{
+ RaftPort: 9091,
+ },
+ },
+ },
+ expectedParts: []string{
+ "seata-cluster-0.seata-headless:9091",
+ "seata-cluster-1.seata-headless:9091",
+ "seata-cluster-2.seata-headless:9091",
+ },
+ expectedLength: 3,
+ },
+ {
+ name: "zero replicas",
+ seataServer: &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "seata",
+ },
+ Spec: seatav1alpha1.SeataServerSpec{
+ Replicas: 0,
+ ServiceName: "seata-svc",
+ Ports: seatav1alpha1.Ports{
+ RaftPort: 9091,
+ },
+ },
+ },
+ expectedParts: []string{},
+ expectedLength: 0,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ result := ConcatRaftServerAddress(tc.seataServer)
+
+ if tc.expectedLength == 0 {
+ if result != "" {
+ t.Errorf("Expected empty string for
zero replicas, got '%s'", result)
+ }
+ return
+ }
+
+ // Check that result contains all expected parts
+ for _, part := range tc.expectedParts {
+ if !strings.Contains(result, part) {
+ t.Errorf("Expected result to contain
'%s', but got '%s'", part, result)
+ }
+ }
+
+ // Check number of addresses (separated by commas)
+ addresses := strings.Split(result, ",")
+ if len(addresses) != tc.expectedLength {
+ t.Errorf("Expected %d addresses, got %d: %v",
tc.expectedLength, len(addresses), addresses)
+ }
+ })
+ }
+}
+
+func TestContainsString(t *testing.T) {
+ testCases := []struct {
+ name string
+ slice []string
+ str string
+ expected bool
+ }{
+ {
+ name: "string exists in slice",
+ slice: []string{"apple", "banana", "cherry"},
+ str: "banana",
+ expected: true,
+ },
+ {
+ name: "string does not exist in slice",
+ slice: []string{"apple", "banana", "cherry"},
+ str: "orange",
+ expected: false,
+ },
+ {
+ name: "empty slice",
+ slice: []string{},
+ str: "test",
+ expected: false,
+ },
+ {
+ name: "empty string in slice",
+ slice: []string{"", "a", "b"},
+ str: "",
+ expected: true,
+ },
+ {
+ name: "single element matching",
+ slice: []string{"only"},
+ str: "only",
+ expected: true,
+ },
+ {
+ name: "single element not matching",
+ slice: []string{"only"},
+ str: "other",
+ expected: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ result := ContainsString(tc.slice, tc.str)
+ if result != tc.expected {
+ t.Errorf("Expected %v, got %v", tc.expected,
result)
+ }
+ })
+ }
+}
+
+func TestRemoveString(t *testing.T) {
+ testCases := []struct {
+ name string
+ slice []string
+ str string
+ expected []string
+ }{
+ {
+ name: "remove existing string",
+ slice: []string{"apple", "banana", "cherry"},
+ str: "banana",
+ expected: []string{"apple", "cherry"},
+ },
+ {
+ name: "remove non-existing string",
+ slice: []string{"apple", "banana", "cherry"},
+ str: "orange",
+ expected: []string{"apple", "banana", "cherry"},
+ },
+ {
+ name: "remove from empty slice",
+ slice: []string{},
+ str: "test",
+ expected: []string{},
+ },
+ {
+ name: "remove only element",
+ slice: []string{"only"},
+ str: "only",
+ expected: []string{},
+ },
+ {
+ name: "remove duplicate strings",
+ slice: []string{"a", "b", "a", "c", "a"},
+ str: "a",
+ expected: []string{"b", "c"},
+ },
+ {
+ name: "remove first element",
+ slice: []string{"first", "second", "third"},
+ str: "first",
+ expected: []string{"second", "third"},
+ },
+ {
+ name: "remove last element",
+ slice: []string{"first", "second", "third"},
+ str: "third",
+ expected: []string{"first", "second"},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ result := RemoveString(tc.slice, tc.str)
+
+ if len(result) != len(tc.expected) {
+ t.Errorf("Expected length %d, got %d",
len(tc.expected), len(result))
+ return
+ }
+
+ for i, v := range result {
+ if v != tc.expected[i] {
+ t.Errorf("At index %d: expected '%s',
got '%s'", i, tc.expected[i], v)
+ }
+ }
+ })
+ }
+}
+
+func TestIsPVCOrphan(t *testing.T) {
+ testCases := []struct {
+ name string
+ pvcName string
+ replicas int32
+ expected bool
+ }{
+ {
+ name: "PVC within replica range",
+ pvcName: "seata-store-seata-0",
+ replicas: 3,
+ expected: false,
+ },
+ {
+ name: "PVC at replica boundary",
+ pvcName: "seata-store-seata-2",
+ replicas: 3,
+ expected: false,
+ },
+ {
+ name: "PVC beyond replica range",
+ pvcName: "seata-store-seata-3",
+ replicas: 3,
+ expected: true,
+ },
+ {
+ name: "PVC far beyond replica range",
+ pvcName: "seata-store-seata-10",
+ replicas: 3,
+ expected: true,
+ },
+ {
+ name: "zero replicas with PVC 0",
+ pvcName: "seata-store-seata-0",
+ replicas: 0,
+ expected: true,
+ },
+ {
+ name: "single replica with PVC 1",
+ pvcName: "seata-store-seata-1",
+ replicas: 1,
+ expected: true,
+ },
+ {
+ name: "invalid PVC name format",
+ pvcName: "invalid-name-without-number",
+ replicas: 3,
+ expected: false,
+ },
+ {
+ name: "PVC name with letters after dash",
+ pvcName: "seata-store-abc",
+ replicas: 3,
+ expected: false,
+ },
+ {
+ name: "PVC name without dash",
+ pvcName: "singlename",
+ replicas: 3,
+ expected: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ result := IsPVCOrphan(tc.pvcName, tc.replicas)
+ if result != tc.expected {
+ t.Errorf("Expected %v, got %v for pvcName='%s'
with replicas=%d",
+ tc.expected, result, tc.pvcName,
tc.replicas)
+ }
+ })
+ }
+}
+
+func TestSeataFinalizer(t *testing.T) {
+ // Test that the constant is defined
+ if SeataFinalizer == "" {
+ t.Error("SeataFinalizer constant should not be empty")
+ }
+
+ if SeataFinalizer != "cleanUpSeataPVC" {
+ t.Errorf("Expected SeataFinalizer to be 'cleanUpSeataPVC', got
'%s'", SeataFinalizer)
+ }
+}
+
+func TestConcatRaftServerAddress_CustomPort(t *testing.T) {
+ seataServer := &seatav1alpha1.SeataServer{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "custom",
+ },
+ Spec: seatav1alpha1.SeataServerSpec{
+ Replicas: 2,
+ ServiceName: "custom-svc",
+ Ports: seatav1alpha1.Ports{
+ RaftPort: 7777,
+ },
+ },
+ }
+
+ result := ConcatRaftServerAddress(seataServer)
+
+ expectedParts := []string{
+ "custom-0.custom-svc:7777",
+ "custom-1.custom-svc:7777",
+ }
+
+ for _, part := range expectedParts {
+ if !strings.Contains(result, part) {
+ t.Errorf("Expected result to contain '%s', but got
'%s'", part, result)
+ }
+ }
+
+ // Should not contain trailing comma
+ if strings.HasSuffix(result, ",") {
+ t.Error("Result should not have trailing comma")
+ }
+}
+
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]