From 59f266cd0cf24a3486d381cbce2d595d1e11abef Mon Sep 17 00:00:00 2001 From: Anuj Agrawal Date: Sun, 17 Nov 2024 20:52:11 +0530 Subject: [PATCH] Added unit tests for pkg/resourceinterpreter/default/native/reflectstatus.go Signed-off-by: Anuj Agrawal --- .../default/native/reflectstatus_test.go | 1129 +++++++++++++++-- 1 file changed, 1039 insertions(+), 90 deletions(-) diff --git a/pkg/resourceinterpreter/default/native/reflectstatus_test.go b/pkg/resourceinterpreter/default/native/reflectstatus_test.go index dbf18ede9e5d..78a392f9fc58 100644 --- a/pkg/resourceinterpreter/default/native/reflectstatus_test.go +++ b/pkg/resourceinterpreter/default/native/reflectstatus_test.go @@ -17,156 +17,621 @@ limitations under the License. package native import ( - "reflect" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" policyv1 "k8s.io/api/policy/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" + "github.com/karmada-io/karmada/pkg/util" "github.com/karmada-io/karmada/pkg/util/helper" ) -func Test_getEntireStatus(t *testing.T) { - testMap := map[string]interface{}{"key": "value"} - wantRawExtension, _ := helper.BuildStatusRawExtension(testMap) - type args struct { - object *unstructured.Unstructured +func Test_reflectPodDisruptionBudgetStatus(t *testing.T) { + tests := []struct { + name string + object *unstructured.Unstructured + want *runtime.RawExtension + wantErr bool + }{ + { + name: "PDB with valid status", + object: func() *unstructured.Unstructured { + pdb := &policyv1.PodDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pdb", + Namespace: "test-ns", + }, + Status: policyv1.PodDisruptionBudgetStatus{ + CurrentHealthy: 2, + DesiredHealthy: 3, + DisruptionsAllowed: 1, + ExpectedPods: 3, + DisruptedPods: map[string]metav1.Time{ + "pod1": metav1.Now(), + }, + }, + } + obj, _ := helper.ToUnstructured(pdb) + return obj + }(), + want: func() *runtime.RawExtension { + status := &policyv1.PodDisruptionBudgetStatus{ + CurrentHealthy: 2, + DesiredHealthy: 3, + DisruptionsAllowed: 1, + ExpectedPods: 3, + DisruptedPods: map[string]metav1.Time{ + "pod1": metav1.Now(), + }, + } + raw, _ := helper.BuildStatusRawExtension(status) + return raw + }(), + wantErr: false, + }, + { + name: "PDB without status", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "policy/v1", + "kind": "PodDisruptionBudget", + "metadata": map[string]interface{}{ + "name": "test-pdb", + "namespace": "test-ns", + }, + }, + }, + want: nil, + wantErr: false, + }, + { + name: "PDB with invalid status format", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "policy/v1", + "kind": "PodDisruptionBudget", + "metadata": map[string]interface{}{ + "name": "test-pdb", + "namespace": "test-ns", + }, + "status": "invalid", + }, + }, + want: nil, + wantErr: true, + }, } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := reflectPodDisruptionBudgetStatus(tt.object) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + if tt.want == nil { + assert.Nil(t, got) + } else { + assert.NotNil(t, got) + assert.JSONEq(t, string(tt.want.Raw), string(got.Raw)) + } + }) + } +} + +func Test_reflectHorizontalPodAutoscalerStatus(t *testing.T) { tests := []struct { name string - args args + object *unstructured.Unstructured want *runtime.RawExtension wantErr bool }{ { - "object doesn't have status", - args{ - &unstructured.Unstructured{ - Object: map[string]interface{}{}, + name: "HPA with valid status", + object: func() *unstructured.Unstructured { + hpa := &autoscalingv2.HorizontalPodAutoscaler{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-hpa", + Namespace: "test-ns", + }, + Status: autoscalingv2.HorizontalPodAutoscalerStatus{ + CurrentReplicas: 2, + DesiredReplicas: 3, + }, + } + obj, _ := helper.ToUnstructured(hpa) + return obj + }(), + want: func() *runtime.RawExtension { + status := &autoscalingv2.HorizontalPodAutoscalerStatus{ + CurrentReplicas: 2, + DesiredReplicas: 3, + } + raw, _ := helper.BuildStatusRawExtension(status) + return raw + }(), + wantErr: false, + }, + { + name: "HPA without status", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "autoscaling/v2", + "kind": "HorizontalPodAutoscaler", + "metadata": map[string]interface{}{ + "name": "test-hpa", + "namespace": "test-ns", + }, }, }, - nil, - false, + want: nil, + wantErr: false, }, { - "object have wrong format status", - args{ - &unstructured.Unstructured{ - Object: map[string]interface{}{ - "status": "a string", + name: "HPA with invalid status format", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "autoscaling/v2", + "kind": "HorizontalPodAutoscaler", + "metadata": map[string]interface{}{ + "name": "test-hpa", + "namespace": "test-ns", }, + "status": "invalid", }, }, - nil, - true, + want: nil, + wantErr: true, }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := reflectHorizontalPodAutoscalerStatus(tt.object) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + if tt.want == nil { + assert.Nil(t, got) + } else { + assert.NotNil(t, got) + assert.JSONEq(t, string(tt.want.Raw), string(got.Raw)) + } + }) + } +} + +func Test_reflectWholeStatus(t *testing.T) { + tests := []struct { + name string + object *unstructured.Unstructured + want *runtime.RawExtension + wantErr bool + }{ { - "object have correct format status", - args{ - &unstructured.Unstructured{ - Object: map[string]interface{}{ - "status": testMap, + name: "object with valid status", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "status": map[string]interface{}{ + "key": "value", + "num": int64(1), }, }, }, - wantRawExtension, - false, + want: func() *runtime.RawExtension { + status := map[string]interface{}{ + "key": "value", + "num": int64(1), + } + raw, _ := helper.BuildStatusRawExtension(status) + return raw + }(), + wantErr: false, + }, + { + name: "object without status", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{}, + }, + want: nil, + wantErr: false, + }, + { + name: "object with invalid status format", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "status": "invalid", + }, + }, + want: nil, + wantErr: true, }, } - for i := range tests { - tt := tests[i] + + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Parallel() - got, err := reflectWholeStatus(tt.args.object) - if (err != nil) != tt.wantErr { - t.Errorf("reflectWholeStatus() error = %v, wantErr %v", err, tt.wantErr) + got, err := reflectWholeStatus(tt.object) + if tt.wantErr { + assert.Error(t, err) return } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("reflectWholeStatus() got = %v, want %v", got, tt.want) + assert.NoError(t, err) + if tt.want == nil { + assert.Nil(t, got) + } else { + assert.NotNil(t, got) + assert.JSONEq(t, string(tt.want.Raw), string(got.Raw)) } }) } } -func Test_reflectPodDisruptionBudgetStatus(t *testing.T) { - currStatus := policyv1.PodDisruptionBudgetStatus{ - CurrentHealthy: 1, - DesiredHealthy: 1, - DisruptionsAllowed: 1, - ExpectedPods: 1, +func Test_getAllDefaultReflectStatusInterpreter(t *testing.T) { + tests := []struct { + name string + gvk schema.GroupVersionKind + wantFn bool + }{ + { + name: "Deployment interpreter exists", + gvk: appsv1.SchemeGroupVersion.WithKind(util.DeploymentKind), + wantFn: true, + }, + { + name: "Service interpreter exists", + gvk: corev1.SchemeGroupVersion.WithKind(util.ServiceKind), + wantFn: true, + }, + { + name: "Ingress interpreter exists", + gvk: networkingv1.SchemeGroupVersion.WithKind(util.IngressKind), + wantFn: true, + }, + { + name: "Job interpreter exists", + gvk: batchv1.SchemeGroupVersion.WithKind(util.JobKind), + wantFn: true, + }, + { + name: "DaemonSet interpreter exists", + gvk: appsv1.SchemeGroupVersion.WithKind(util.DaemonSetKind), + wantFn: true, + }, + { + name: "StatefulSet interpreter exists", + gvk: appsv1.SchemeGroupVersion.WithKind(util.StatefulSetKind), + wantFn: true, + }, + { + name: "PodDisruptionBudget interpreter exists", + gvk: policyv1.SchemeGroupVersion.WithKind(util.PodDisruptionBudgetKind), + wantFn: true, + }, + { + name: "HorizontalPodAutoscaler interpreter exists", + gvk: autoscalingv2.SchemeGroupVersion.WithKind(util.HorizontalPodAutoscalerKind), + wantFn: true, + }, + { + name: "Non-existent resource should not have interpreter", + gvk: schema.GroupVersionKind{Group: "fake", Version: "v1", Kind: "Fake"}, + wantFn: false, + }, + } + + interpreters := getAllDefaultReflectStatusInterpreter() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + interpreter, exists := interpreters[tt.gvk] + assert.Equal(t, tt.wantFn, exists, "interpreter existence mismatch for %v", tt.gvk) + if tt.wantFn { + assert.NotNil(t, interpreter, "interpreter should not be nil for %v", tt.gvk) + // Type checking + assert.IsType(t, (reflectStatusInterpreter)(nil), interpreter, + "interpreter should be of type reflectStatusInterpreter") + } + }) + } + + // Verify total number of interpreters + assert.Len(t, interpreters, 8, "unexpected number of interpreters") + + // Verify map is not nil + assert.NotNil(t, interpreters, "interpreters map should not be nil") +} + +func Test_reflectDeploymentStatus(t *testing.T) { + validDeployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-deployment", + Namespace: "test-ns", + Generation: 2, + Annotations: map[string]string{ + workv1alpha2.ResourceTemplateGenerationAnnotationKey: "1", + }, + }, + Status: appsv1.DeploymentStatus{ + Replicas: 3, + UpdatedReplicas: 3, + ReadyReplicas: 3, + AvailableReplicas: 3, + UnavailableReplicas: 0, + ObservedGeneration: 2, + }, + } + + tests := []struct { + name string + deployment *appsv1.Deployment + modifyFunc func(*unstructured.Unstructured) + want *runtime.RawExtension + wantErr bool + }{ + { + name: "deployment with valid status and generation annotation", + deployment: validDeployment.DeepCopy(), + want: func() *runtime.RawExtension { + wantStatus := &WrappedDeploymentStatus{ + FederatedGeneration: FederatedGeneration{ + Generation: 2, + ResourceTemplateGeneration: 1, + }, + DeploymentStatus: validDeployment.Status, + } + raw, _ := helper.BuildStatusRawExtension(wantStatus) + return raw + }(), + wantErr: false, + }, + { + name: "deployment without status field", + deployment: validDeployment.DeepCopy(), + modifyFunc: func(u *unstructured.Unstructured) { + delete(u.Object, "status") + }, + want: nil, + wantErr: false, + }, + { + name: "deployment with invalid generation annotation", + deployment: validDeployment.DeepCopy(), + modifyFunc: func(u *unstructured.Unstructured) { + annotations := u.GetAnnotations() + annotations[workv1alpha2.ResourceTemplateGenerationAnnotationKey] = "invalid" + u.SetAnnotations(annotations) + }, + want: nil, + wantErr: true, + }, + { + name: "deployment without generation annotation", + deployment: validDeployment.DeepCopy(), + modifyFunc: func(u *unstructured.Unstructured) { + annotations := u.GetAnnotations() + delete(annotations, workv1alpha2.ResourceTemplateGenerationAnnotationKey) + u.SetAnnotations(annotations) + }, + want: nil, + wantErr: true, + }, } - currStatusUnstructured, _ := helper.ToUnstructured(&policyv1.PodDisruptionBudget{Status: currStatus}) - wantRawExtension, _ := helper.BuildStatusRawExtension(&currStatus) - type args struct { - object *unstructured.Unstructured + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Convert deployment to unstructured + unstrObj, err := helper.ToUnstructured(tt.deployment) + require.NoError(t, err, "Failed to convert deployment to unstructured") + + // Apply modifications if specified + if tt.modifyFunc != nil { + tt.modifyFunc(unstrObj) + } + + got, err := reflectDeploymentStatus(unstrObj) + + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + + if tt.want == nil { + assert.Nil(t, got) + return + } + + assert.NotNil(t, got) + assert.JSONEq(t, string(tt.want.Raw), string(got.Raw)) + }) } +} + +func Test_reflectServiceStatus(t *testing.T) { tests := []struct { name string - args args + object *unstructured.Unstructured want *runtime.RawExtension wantErr bool }{ { - "object doesn't have status", - args{ - &unstructured.Unstructured{ - Object: map[string]interface{}{}, + name: "non-LoadBalancer service should return nil", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "type": "ClusterIP", + }, }, }, - nil, - false, + want: nil, + wantErr: false, }, { - "object have wrong format status", - args{ - &unstructured.Unstructured{ - Object: map[string]interface{}{ - "status": "a string", + name: "LoadBalancer service without status should return nil", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "type": "LoadBalancer", }, }, }, - nil, - true, + want: nil, + wantErr: false, }, { - "object have correct format status", - args{ - currStatusUnstructured, + name: "LoadBalancer service with status should return status", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "type": "LoadBalancer", + }, + "status": map[string]interface{}{ + "loadBalancer": map[string]interface{}{ + "ingress": []interface{}{ + map[string]interface{}{ + "ip": "192.0.2.1", + }, + }, + }, + }, + }, }, - wantRawExtension, - false, + want: func() *runtime.RawExtension { + status := corev1.ServiceStatus{ + LoadBalancer: corev1.LoadBalancerStatus{ + Ingress: []corev1.LoadBalancerIngress{ + { + IP: "192.0.2.1", + }, + }, + }, + } + raw, _ := helper.BuildStatusRawExtension(status) + return raw + }(), + wantErr: false, + }, + { + name: "invalid status format should return error", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "type": "LoadBalancer", + }, + "status": "invalid", + }, + }, + want: nil, + wantErr: true, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := reflectPodDisruptionBudgetStatus(tt.args.object) - if (err != nil) != tt.wantErr { - t.Errorf("reflectPodDisruptionBudgetStatus() error = %v, wantErr %v", err, tt.wantErr) + got, err := reflectServiceStatus(tt.object) + if tt.wantErr { + assert.Error(t, err) return } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("reflectPodDisruptionBudgetStatus() got = %v, want %v", got, tt.want) + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_reflectIngressStatus(t *testing.T) { + testIngress := networkingv1.IngressStatus{ + LoadBalancer: networkingv1.IngressLoadBalancerStatus{ + Ingress: []networkingv1.IngressLoadBalancerIngress{ + { + IP: "192.0.2.1", + }, + }, + }, + } + + ingressStatusMap, _ := helper.ToUnstructured(&networkingv1.Ingress{Status: testIngress}) + wantRawExtension, _ := helper.BuildStatusRawExtension(testIngress) + + tests := []struct { + name string + object *unstructured.Unstructured + want *runtime.RawExtension + wantErr bool + }{ + { + name: "ingress with valid status", + object: ingressStatusMap, + want: wantRawExtension, + wantErr: false, + }, + { + name: "ingress without status", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{}, + }, + want: nil, + wantErr: false, + }, + { + name: "ingress with invalid status", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "status": "invalid", + }, + }, + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := reflectIngressStatus(tt.object) + if tt.wantErr { + assert.Error(t, err) + return } + assert.NoError(t, err) + assert.Equal(t, tt.want, got) }) } } -func Test_reflectHorizontalPodAutoscalerStatus(t *testing.T) { - hpaStatus, _ := helper.ToUnstructured(&autoscalingv2.HorizontalPodAutoscaler{ - Status: autoscalingv2.HorizontalPodAutoscalerStatus{ - CurrentReplicas: 2, - DesiredReplicas: 2, - }, - }) - grabStatus, _ := helper.BuildStatusRawExtension(autoscalingv2.HorizontalPodAutoscalerStatus{ - CurrentReplicas: 2, - DesiredReplicas: 2, - }) - nilHpaStatus, _ := helper.ToUnstructured(&map[string]interface{}{}) +func Test_reflectJobStatus(t *testing.T) { + timeNow := metav1.Now() + completionTime := timeNow.DeepCopy() + startTime := timeNow.DeepCopy() + jobStatus := batchv1.JobStatus{ + Active: 1, + Succeeded: 2, + Failed: 0, + CompletionTime: completionTime, + StartTime: startTime, + Conditions: []batchv1.JobCondition{ + { + Type: "Complete", + Status: "True", + Reason: "JobComplete", + Message: "Job completed successfully", + }, + }, + } + tests := []struct { name string object *unstructured.Unstructured @@ -174,25 +639,509 @@ func Test_reflectHorizontalPodAutoscalerStatus(t *testing.T) { wantErr bool }{ { - name: "reflect hap status", - object: hpaStatus, - want: grabStatus, + name: "job with all status fields", + object: func() *unstructured.Unstructured { + obj, _ := helper.ToUnstructured(&batchv1.Job{Status: jobStatus}) + return obj + }(), + want: func() *runtime.RawExtension { + raw, _ := helper.BuildStatusRawExtension(jobStatus) + return raw + }(), wantErr: false, }, { - name: "reflect nil hpa status", - object: nilHpaStatus, + name: "job without status", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{}, + }, + want: nil, + wantErr: false, + }, + { + name: "job with invalid status type", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "status": map[string]interface{}{ + "active": "invalid", + }, + }, + }, + want: nil, + wantErr: true, + }, + { + name: "job with empty conditions", + object: func() *unstructured.Unstructured { + status := jobStatus.DeepCopy() + status.Conditions = []batchv1.JobCondition{} + obj, _ := helper.ToUnstructured(&batchv1.Job{Status: *status}) + return obj + }(), + want: func() *runtime.RawExtension { + status := jobStatus.DeepCopy() + status.Conditions = []batchv1.JobCondition{} + raw, _ := helper.BuildStatusRawExtension(*status) + return raw + }(), + wantErr: false, + }, + { + name: "job with failed status", + object: func() *unstructured.Unstructured { + status := jobStatus.DeepCopy() + status.Failed = 1 + status.Active = 0 + status.Succeeded = 0 + status.Conditions = []batchv1.JobCondition{ + { + Type: "Failed", + Status: "True", + Reason: "JobFailed", + Message: "Job failed due to error", + }, + } + obj, _ := helper.ToUnstructured(&batchv1.Job{Status: *status}) + return obj + }(), + want: func() *runtime.RawExtension { + status := jobStatus.DeepCopy() + status.Failed = 1 + status.Active = 0 + status.Succeeded = 0 + status.Conditions = []batchv1.JobCondition{ + { + Type: "Failed", + Status: "True", + Reason: "JobFailed", + Message: "Job failed due to error", + }, + } + raw, _ := helper.BuildStatusRawExtension(*status) + return raw + }(), + wantErr: false, + }, + { + name: "job with only active status", + object: func() *unstructured.Unstructured { + status := jobStatus.DeepCopy() + status.Failed = 0 + status.Succeeded = 0 + status.Active = 1 + status.CompletionTime = nil + status.Conditions = []batchv1.JobCondition{} + obj, _ := helper.ToUnstructured(&batchv1.Job{Status: *status}) + return obj + }(), + want: func() *runtime.RawExtension { + status := jobStatus.DeepCopy() + status.Failed = 0 + status.Succeeded = 0 + status.Active = 1 + status.CompletionTime = nil + status.Conditions = []batchv1.JobCondition{} + raw, _ := helper.BuildStatusRawExtension(*status) + return raw + }(), + wantErr: false, + }, + { + name: "job with invalid status field", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "status": "invalid", + }, + }, want: nil, + wantErr: true, + }, + { + name: "job with nil completion time", + object: func() *unstructured.Unstructured { + status := jobStatus.DeepCopy() + status.CompletionTime = nil + obj, _ := helper.ToUnstructured(&batchv1.Job{Status: *status}) + return obj + }(), + want: func() *runtime.RawExtension { + status := jobStatus.DeepCopy() + status.CompletionTime = nil + raw, _ := helper.BuildStatusRawExtension(*status) + return raw + }(), + wantErr: false, + }, + { + name: "job with nil start time", + object: func() *unstructured.Unstructured { + status := jobStatus.DeepCopy() + status.StartTime = nil + obj, _ := helper.ToUnstructured(&batchv1.Job{Status: *status}) + return obj + }(), + want: func() *runtime.RawExtension { + status := jobStatus.DeepCopy() + status.StartTime = nil + raw, _ := helper.BuildStatusRawExtension(*status) + return raw + }(), wantErr: false, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := reflectHorizontalPodAutoscalerStatus(tt.object) - if (err != nil) != tt.wantErr { - t.Errorf("Test_reflectHorizontalPodAutoscalerStatus() err = %v, wantErr %v", err, tt.wantErr) + got, err := reflectJobStatus(tt.object) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + if tt.want == nil { + assert.Nil(t, got) + } else { + assert.NotNil(t, got) + assert.JSONEq(t, string(tt.want.Raw), string(got.Raw)) + } + }) + } +} + +func Test_reflectDaemonSetStatus(t *testing.T) { + tests := []struct { + name string + object *unstructured.Unstructured + want *runtime.RawExtension + wantErr bool + }{ + { + name: "daemonset with valid status and generation annotation", + object: func() *unstructured.Unstructured { + ds := &appsv1.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-daemonset", + Namespace: "test-ns", + Generation: 2, + Annotations: map[string]string{ + workv1alpha2.ResourceTemplateGenerationAnnotationKey: "1", + }, + }, + Status: appsv1.DaemonSetStatus{ + CurrentNumberScheduled: 3, + DesiredNumberScheduled: 3, + NumberAvailable: 3, + NumberMisscheduled: 0, + NumberReady: 3, + UpdatedNumberScheduled: 3, + NumberUnavailable: 0, + ObservedGeneration: 2, + }, + } + obj, _ := helper.ToUnstructured(ds) + return obj + }(), + want: func() *runtime.RawExtension { + status := &WrappedDaemonSetStatus{ + FederatedGeneration: FederatedGeneration{ + Generation: 2, + ResourceTemplateGeneration: 1, + }, + DaemonSetStatus: appsv1.DaemonSetStatus{ + CurrentNumberScheduled: 3, + DesiredNumberScheduled: 3, + NumberAvailable: 3, + NumberMisscheduled: 0, + NumberReady: 3, + UpdatedNumberScheduled: 3, + NumberUnavailable: 0, + ObservedGeneration: 2, + }, + } + raw, _ := helper.BuildStatusRawExtension(status) + return raw + }(), + wantErr: false, + }, + { + name: "daemonset without status field", + object: func() *unstructured.Unstructured { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "DaemonSet", + "metadata": map[string]interface{}{ + "name": "test-daemonset", + "namespace": "test-ns", + "generation": int64(1), + "annotations": map[string]interface{}{ + workv1alpha2.ResourceTemplateGenerationAnnotationKey: "1", + }, + }, + }, + } + return obj + }(), + want: nil, + wantErr: false, + }, + { + name: "daemonset with invalid generation annotation", + object: func() *unstructured.Unstructured { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "DaemonSet", + "metadata": map[string]interface{}{ + "name": "test-daemonset", + "namespace": "test-ns", + "generation": int64(1), + "annotations": map[string]interface{}{ + workv1alpha2.ResourceTemplateGenerationAnnotationKey: "invalid", + }, + }, + "status": map[string]interface{}{ + "currentNumberScheduled": int64(1), + "numberReady": int64(1), + }, + }, + } + return obj + }(), + want: nil, + wantErr: true, + }, + { + name: "daemonset with invalid status format", + object: func() *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "DaemonSet", + "metadata": map[string]interface{}{ + "name": "test-daemonset", + "namespace": "test-ns", + "annotations": map[string]interface{}{ + workv1alpha2.ResourceTemplateGenerationAnnotationKey: "1", + }, + }, + "status": "invalid", + }, + } + }(), + want: nil, + wantErr: true, + }, + { + name: "daemonset without generation annotation", + object: func() *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "DaemonSet", + "metadata": map[string]interface{}{ + "name": "test-daemonset", + "namespace": "test-ns", + }, + "status": map[string]interface{}{ + "currentNumberScheduled": int64(1), + }, + }, + } + }(), + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := reflectDaemonSetStatus(tt.object) + if tt.wantErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + if tt.want == nil { + assert.Nil(t, got) + } else { + assert.NotNil(t, got) + assert.JSONEq(t, string(tt.want.Raw), string(got.Raw)) + } + }) + } +} + +func Test_reflectStatefulSetStatus(t *testing.T) { + tests := []struct { + name string + object *unstructured.Unstructured + want *runtime.RawExtension + wantErr bool + }{ + { + name: "statefulset with valid status and generation annotation", + object: func() *unstructured.Unstructured { + sts := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-statefulset", + Namespace: "test-ns", + Generation: 2, + Annotations: map[string]string{ + workv1alpha2.ResourceTemplateGenerationAnnotationKey: "1", + }, + }, + Status: appsv1.StatefulSetStatus{ + Replicas: 3, + ReadyReplicas: 3, + CurrentReplicas: 3, + UpdatedReplicas: 3, + AvailableReplicas: 3, + }, + } + obj, _ := helper.ToUnstructured(sts) + return obj + }(), + want: func() *runtime.RawExtension { + status := &WrappedStatefulSetStatus{ + FederatedGeneration: FederatedGeneration{ + Generation: 2, + ResourceTemplateGeneration: 1, + }, + StatefulSetStatus: appsv1.StatefulSetStatus{ + Replicas: 3, + ReadyReplicas: 3, + CurrentReplicas: 3, + UpdatedReplicas: 3, + AvailableReplicas: 3, + }, + } + raw, _ := helper.BuildStatusRawExtension(status) + return raw + }(), + wantErr: false, + }, + { + name: "statefulset without status field", + object: func() *unstructured.Unstructured { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "StatefulSet", + "metadata": map[string]interface{}{ + "name": "test-statefulset", + "namespace": "test-ns", + "generation": int64(1), + "annotations": map[string]interface{}{ + workv1alpha2.ResourceTemplateGenerationAnnotationKey: "1", + }, + }, + }, + } + return obj + }(), + want: nil, + wantErr: false, + }, + { + name: "statefulset with invalid status format", + object: func() *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "StatefulSet", + "metadata": map[string]interface{}{ + "name": "test-statefulset", + "namespace": "test-ns", + "annotations": map[string]interface{}{ + workv1alpha2.ResourceTemplateGenerationAnnotationKey: "1", + }, + }, + "status": "invalid", + }, + } + }(), + want: nil, + wantErr: true, + }, + { + name: "statefulset with partial status fields", + object: func() *unstructured.Unstructured { + sts := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-statefulset", + Namespace: "test-ns", + Generation: 2, + Annotations: map[string]string{ + workv1alpha2.ResourceTemplateGenerationAnnotationKey: "1", + }, + }, + Status: appsv1.StatefulSetStatus{ + Replicas: 2, + ReadyReplicas: 1, + CurrentReplicas: 1, + // UpdatedReplicas and AvailableReplicas missing + }, + } + obj, _ := helper.ToUnstructured(sts) + return obj + }(), + want: func() *runtime.RawExtension { + status := &WrappedStatefulSetStatus{ + FederatedGeneration: FederatedGeneration{ + Generation: 2, + ResourceTemplateGeneration: 1, + }, + StatefulSetStatus: appsv1.StatefulSetStatus{ + Replicas: 2, + ReadyReplicas: 1, + CurrentReplicas: 1, + }, + } + raw, _ := helper.BuildStatusRawExtension(status) + return raw + }(), + wantErr: false, + }, + { + name: "statefulset with no annotations", + object: func() *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "StatefulSet", + "metadata": map[string]interface{}{ + "name": "test-statefulset", + "namespace": "test-ns", + }, + "status": map[string]interface{}{ + "replicas": int64(1), + "readyReplicas": int64(1), + "currentReplicas": int64(1), + }, + }, + } + }(), + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := reflectStatefulSetStatus(tt.object) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + if tt.want == nil { + assert.Nil(t, got) + } else { + assert.NotNil(t, got) + assert.JSONEq(t, string(tt.want.Raw), string(got.Raw)) } - assert.Equalf(t, tt.want, got, "reflectHorizontalPodAutoscalerStatus(%v)", tt.object) }) } }