From e689eafd3ff62b5a832f55368e3aba56a4cdea34 Mon Sep 17 00:00:00 2001 From: Mohamed Awnallah Date: Sun, 8 Sep 2024 12:05:51 +0300 Subject: [PATCH] pkg: test ResourceBinding mutating webhook In this commit, we introduce unit tests for the `MutatingAdmission` webhook that mutates the `ResourceBinding` resource. These tests ensure correct behavior for various mutation scenarios and improve coverage: - Testing the handling of decode errors and ensuring that admission is denied when decoding fails. - Full coverage testing of policy mutation, including setting default UUID labels for the `ResourceBinding`. - Verifying that only the expected patch for the UUID label is applied, ensuring no other unnecessary mutations. Signed-off-by: Mohamed Awnallah --- pkg/webhook/resourcebinding/mutating_test.go | 183 +++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 pkg/webhook/resourcebinding/mutating_test.go diff --git a/pkg/webhook/resourcebinding/mutating_test.go b/pkg/webhook/resourcebinding/mutating_test.go new file mode 100644 index 000000000000..68a9356a56f2 --- /dev/null +++ b/pkg/webhook/resourcebinding/mutating_test.go @@ -0,0 +1,183 @@ +/* +Copyright 2024 The Karmada Authors. + +Licensed 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 resourcebinding + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "reflect" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" +) + +type fakeMutationDecoder struct { + err error + obj runtime.Object +} + +// Decode mocks the Decode method of admission.Decoder. +func (f *fakeMutationDecoder) Decode(_ admission.Request, obj runtime.Object) error { + if f.err != nil { + return f.err + } + if f.obj != nil { + reflect.ValueOf(obj).Elem().Set(reflect.ValueOf(f.obj).Elem()) + } + return nil +} + +// DecodeRaw mocks the DecodeRaw method of admission.Decoder. +func (f *fakeMutationDecoder) DecodeRaw(_ runtime.RawExtension, obj runtime.Object) error { + if f.err != nil { + return f.err + } + if f.obj != nil { + reflect.ValueOf(obj).Elem().Set(reflect.ValueOf(f.obj).Elem()) + } + return nil +} + +func TestMutatingAdmission_Handle(t *testing.T) { + tests := []struct { + name string + decoder admission.Decoder + req admission.Request + want admission.Response + }{ + { + name: "Handle_DecodeError_DeniesAdmission", + decoder: &fakeMutationDecoder{ + err: errors.New("decode error"), + }, + req: admission.Request{}, + want: admission.Errored(http.StatusBadRequest, errors.New("decode error")), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := MutatingAdmission{ + Decoder: tt.decoder, + } + got := m.Handle(context.Background(), tt.req) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Handle() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMutatingAdmission_Handle_FullCoverage(t *testing.T) { + // Define the rb name and namespace to be used in the test. + name := "test-resource-binding" + namespace := "test-namespace" + podName := "test-pod" + + // Mock an admission request. + req := admission.Request{} + + // Create the initial rb object with default values for testing. + rb := &workv1alpha2.ResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: workv1alpha2.ResourceBindingSpec{ + Resource: workv1alpha2.ObjectReference{ + APIVersion: "v1", + Kind: "Pod", + Namespace: namespace, + Name: podName, + }, + Clusters: []workv1alpha2.TargetCluster{ + { + Name: "member1", + Replicas: 1, + }, + }, + }, + } + + // Define the expected rb object after mutations. + wantRB := &workv1alpha2.ResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: map[string]string{ + workv1alpha2.ResourceBindingPermanentIDLabel: "some-unique-uuid", + }, + }, + Spec: workv1alpha2.ResourceBindingSpec{ + Resource: workv1alpha2.ObjectReference{ + APIVersion: "v1", + Kind: "Pod", + Namespace: namespace, + Name: podName, + }, + Clusters: []workv1alpha2.TargetCluster{ + { + Name: "member1", + Replicas: 1, + }, + }, + }, + } + + // Mock decoder that decodes the request into the rb object. + decoder := &fakeMutationDecoder{ + obj: rb, + } + + // Marshal the expected rb object to simulate the final mutated object. + wantBytes, err := json.Marshal(wantRB) + if err != nil { + t.Fatalf("Failed to marshal expected resource binding: %v", err) + } + req.Object.Raw = wantBytes + + // Instantiate the mutating handler. + mutatingHandler := MutatingAdmission{ + Decoder: decoder, + } + + // Call the Handle function. + got := mutatingHandler.Handle(context.Background(), req) + + // Check if exactly one patch is applied. + if len(got.Patches) != 1 { + t.Errorf("Handle() returned an unexpected number of patches. Expected one patch, received: %v", got.Patches) + } + + // Verify that the only patch applied is for the UUID label. + // If any other patches are present, it indicates that the rb object was not handled as expected. + firstPatch := got.Patches[0] + if firstPatch.Operation != "replace" || firstPatch.Path != "/metadata/labels/resourcebinding.karmada.io~1permanent-id" { + t.Errorf("Handle() returned unexpected patches. Only the UUID patch was expected. Received patches: %v", got.Patches) + } + + // Check if the admission request was allowed. + if !got.Allowed { + t.Errorf("Handle() got.Allowed = false, want true") + } +}