/* 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 webhook import ( "context" "testing" "time" "github.com/stretchr/testify/assert" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" webhookutil "k8s.io/apiserver/pkg/util/webhook" "k8s.io/client-go/rest" "k8s.io/utils/ptr" configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1" workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" "github.com/karmada-io/karmada/pkg/resourceinterpreter/customized/webhook/configmanager" "github.com/karmada-io/karmada/pkg/resourceinterpreter/customized/webhook/request" ) func TestHookEnabled(t *testing.T) { tests := []struct { name string hasSynced bool hooks []configmanager.WebhookAccessor gvk schema.GroupVersionKind operation configv1alpha1.InterpreterOperation want bool }{ { name: "not synced", hasSynced: false, want: false, }, { name: "no hooks", hasSynced: true, hooks: []configmanager.WebhookAccessor{}, gvk: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", }, operation: configv1alpha1.InterpreterOperationInterpretReplica, want: false, }, { name: "matching hook exists", hasSynced: true, hooks: []configmanager.WebhookAccessor{ &mockWebhookAccessor{ uid: "test-hook", rules: []configv1alpha1.RuleWithOperations{ { Operations: []configv1alpha1.InterpreterOperation{ configv1alpha1.InterpreterOperationInterpretReplica, }, Rule: configv1alpha1.Rule{ APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Kinds: []string{"Deployment"}, }, }, }, }, }, gvk: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", }, operation: configv1alpha1.InterpreterOperationInterpretReplica, want: true, }, { name: "no matching hook", hasSynced: true, hooks: []configmanager.WebhookAccessor{ &mockWebhookAccessor{ uid: "test-hook", rules: []configv1alpha1.RuleWithOperations{ { Operations: []configv1alpha1.InterpreterOperation{ configv1alpha1.InterpreterOperationInterpretReplica, }, Rule: configv1alpha1.Rule{ APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Kinds: []string{"Deployment"}, }, }, }, }, }, gvk: schema.GroupVersionKind{ Group: "batch", Version: "v1", Kind: "Job", }, operation: configv1alpha1.InterpreterOperationInterpretReplica, want: false, }, { name: "hook with wildcard matches", hasSynced: true, hooks: []configmanager.WebhookAccessor{ &mockWebhookAccessor{ uid: "test-hook", rules: []configv1alpha1.RuleWithOperations{ { Operations: []configv1alpha1.InterpreterOperation{ configv1alpha1.InterpreterOperationInterpretReplica, }, Rule: configv1alpha1.Rule{ APIGroups: []string{"*"}, APIVersions: []string{"*"}, Kinds: []string{"*"}, }, }, }, }, }, gvk: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", }, operation: configv1alpha1.InterpreterOperationInterpretReplica, want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { interpreter := &CustomizedInterpreter{ hookManager: &mockConfigManager{ hasSynced: tt.hasSynced, hooks: tt.hooks, }, } got := interpreter.HookEnabled(tt.gvk, tt.operation) assert.Equal(t, tt.want, got) }) } } func TestGetReplicas(t *testing.T) { tests := []struct { name string hasSynced bool hooks []configmanager.WebhookAccessor attributes *request.Attributes wantReplicas int32 wantRequires *workv1alpha2.ReplicaRequirements wantMatched bool wantErr bool errorContains string }{ { name: "not synced", hasSynced: false, attributes: &request.Attributes{ Object: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{ "name": "test-deployment", "namespace": "test-ns", }, }, }, Operation: configv1alpha1.InterpreterOperationInterpretReplica, }, wantErr: true, errorContains: "not yet ready to handle request", }, { name: "no matching hooks", hasSynced: true, hooks: []configmanager.WebhookAccessor{}, attributes: &request.Attributes{ Object: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{ "name": "test-deployment", "namespace": "test-ns", }, }, }, Operation: configv1alpha1.InterpreterOperationInterpretReplica, }, wantMatched: false, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { interpreter := &CustomizedInterpreter{ hookManager: &mockConfigManager{ hasSynced: tt.hasSynced, hooks: tt.hooks, }, } replicas, requires, matched, err := interpreter.GetReplicas(context.Background(), tt.attributes) if tt.wantErr { assert.Error(t, err) if tt.errorContains != "" { assert.Contains(t, err.Error(), tt.errorContains) } return } assert.NoError(t, err) assert.Equal(t, tt.wantMatched, matched) if matched { assert.Equal(t, tt.wantReplicas, replicas) assert.Equal(t, tt.wantRequires, requires) } }) } } func TestPatch(t *testing.T) { tests := []struct { name string hasSynced bool hooks []configmanager.WebhookAccessor attributes *request.Attributes wantObject *unstructured.Unstructured wantMatched bool wantErr bool errorContains string }{ { name: "not synced", hasSynced: false, attributes: &request.Attributes{ Object: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{ "name": "test-deployment", "namespace": "test-ns", }, }, }, Operation: configv1alpha1.InterpreterOperationRetain, }, wantErr: true, errorContains: "not yet ready to handle request", }, { name: "no matching hooks", hasSynced: true, hooks: []configmanager.WebhookAccessor{}, attributes: &request.Attributes{ Object: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{ "name": "test-deployment", "namespace": "test-ns", }, }, }, Operation: configv1alpha1.InterpreterOperationRetain, }, wantMatched: false, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { interpreter := &CustomizedInterpreter{ hookManager: &mockConfigManager{ hasSynced: tt.hasSynced, hooks: tt.hooks, }, } object, matched, err := interpreter.Patch(context.Background(), tt.attributes) if tt.wantErr { assert.Error(t, err) if tt.errorContains != "" { assert.Contains(t, err.Error(), tt.errorContains) } return } assert.NoError(t, err) assert.Equal(t, tt.wantMatched, matched) if matched { assert.Equal(t, tt.wantObject, object) } }) } } func TestGetFirstRelevantHook(t *testing.T) { tests := []struct { name string hooks []configmanager.WebhookAccessor gvk schema.GroupVersionKind operation configv1alpha1.InterpreterOperation wantHook string }{ { name: "no hooks", hooks: []configmanager.WebhookAccessor{}, gvk: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", }, operation: configv1alpha1.InterpreterOperationInterpretReplica, wantHook: "", }, { name: "single matching hook", hooks: []configmanager.WebhookAccessor{ &mockWebhookAccessor{ uid: "hook-1", rules: []configv1alpha1.RuleWithOperations{ { Operations: []configv1alpha1.InterpreterOperation{ configv1alpha1.InterpreterOperationInterpretReplica, }, Rule: configv1alpha1.Rule{ APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Kinds: []string{"Deployment"}, }, }, }, }, }, gvk: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", }, operation: configv1alpha1.InterpreterOperationInterpretReplica, wantHook: "hook-1", }, { name: "multiple matching hooks - should select alphabetically first", hooks: []configmanager.WebhookAccessor{ &mockWebhookAccessor{ uid: "hook-2", rules: []configv1alpha1.RuleWithOperations{ { Operations: []configv1alpha1.InterpreterOperation{ configv1alpha1.InterpreterOperationInterpretReplica, }, Rule: configv1alpha1.Rule{ APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Kinds: []string{"Deployment"}, }, }, }, }, &mockWebhookAccessor{ uid: "hook-1", rules: []configv1alpha1.RuleWithOperations{ { Operations: []configv1alpha1.InterpreterOperation{ configv1alpha1.InterpreterOperationInterpretReplica, }, Rule: configv1alpha1.Rule{ APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Kinds: []string{"Deployment"}, }, }, }, }, }, gvk: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", }, operation: configv1alpha1.InterpreterOperationInterpretReplica, wantHook: "hook-1", }, { name: "wildcard matches", hooks: []configmanager.WebhookAccessor{ &mockWebhookAccessor{ uid: "hook-1", rules: []configv1alpha1.RuleWithOperations{ { Operations: []configv1alpha1.InterpreterOperation{ configv1alpha1.InterpreterOperationInterpretReplica, }, Rule: configv1alpha1.Rule{ APIGroups: []string{"*"}, APIVersions: []string{"*"}, Kinds: []string{"*"}, }, }, }, }, }, gvk: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", }, operation: configv1alpha1.InterpreterOperationInterpretReplica, wantHook: "hook-1", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { interpreter := &CustomizedInterpreter{ hookManager: &mockConfigManager{ hasSynced: true, hooks: tt.hooks, }, } got := interpreter.getFirstRelevantHook(tt.gvk, tt.operation) if tt.wantHook == "" { assert.Nil(t, got) } else { assert.NotNil(t, got) assert.Equal(t, tt.wantHook, got.GetUID()) } }) } } func TestInterpret(t *testing.T) { defaultDeployment := &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{ "name": "test", }, }, } tests := []struct { name string hasSynced bool hooks []configmanager.WebhookAccessor attributes *request.Attributes wantResponse *request.ResponseAttributes wantMatched bool wantErr bool errorContains string setupContext func() (context.Context, context.CancelFunc) }{ { name: "not synced", hasSynced: false, attributes: &request.Attributes{ Object: defaultDeployment, Operation: configv1alpha1.InterpreterOperationInterpretReplica, }, wantErr: true, errorContains: "not yet ready to handle request", }, { name: "no matching hooks", hasSynced: true, hooks: []configmanager.WebhookAccessor{}, attributes: &request.Attributes{ Object: defaultDeployment, Operation: configv1alpha1.InterpreterOperationInterpretReplica, }, wantMatched: false, wantErr: false, }, { name: "context timeout", hasSynced: true, hooks: []configmanager.WebhookAccessor{ &mockWebhookAccessor{ uid: "test-hook", rules: []configv1alpha1.RuleWithOperations{ { Operations: []configv1alpha1.InterpreterOperation{ configv1alpha1.InterpreterOperationInterpretReplica, }, Rule: configv1alpha1.Rule{ APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Kinds: []string{"Deployment"}, }, }, }, }, }, attributes: &request.Attributes{ Object: defaultDeployment, Operation: configv1alpha1.InterpreterOperationInterpretReplica, }, wantMatched: true, wantErr: true, setupContext: func() (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), time.Nanosecond) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { interpreter := &CustomizedInterpreter{ hookManager: &mockConfigManager{ hasSynced: tt.hasSynced, hooks: tt.hooks, }, } ctx := context.Background() var cancel context.CancelFunc if tt.setupContext != nil { ctx, cancel = tt.setupContext() defer cancel() } response, matched, err := interpreter.interpret(ctx, tt.attributes) if tt.wantErr { assert.Error(t, err) if tt.errorContains != "" { assert.Contains(t, err.Error(), tt.errorContains) } return } assert.NoError(t, err) assert.Equal(t, tt.wantMatched, matched) if matched { assert.Equal(t, tt.wantResponse, response) } }) } } func TestShouldCallHook(t *testing.T) { tests := []struct { name string hook *mockWebhookAccessor gvk schema.GroupVersionKind operation configv1alpha1.InterpreterOperation want bool }{ { name: "exact match", hook: &mockWebhookAccessor{ rules: []configv1alpha1.RuleWithOperations{ { Operations: []configv1alpha1.InterpreterOperation{ configv1alpha1.InterpreterOperationInterpretReplica, }, Rule: configv1alpha1.Rule{ APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Kinds: []string{"Deployment"}, }, }, }, }, gvk: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", }, operation: configv1alpha1.InterpreterOperationInterpretReplica, want: true, }, { name: "wildcard match", hook: &mockWebhookAccessor{ rules: []configv1alpha1.RuleWithOperations{ { Operations: []configv1alpha1.InterpreterOperation{ configv1alpha1.InterpreterOperationInterpretReplica, }, Rule: configv1alpha1.Rule{ APIGroups: []string{"*"}, APIVersions: []string{"*"}, Kinds: []string{"*"}, }, }, }, }, gvk: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", }, operation: configv1alpha1.InterpreterOperationInterpretReplica, want: true, }, { name: "no operation match", hook: &mockWebhookAccessor{ rules: []configv1alpha1.RuleWithOperations{ { Operations: []configv1alpha1.InterpreterOperation{ configv1alpha1.InterpreterOperationInterpretHealth, }, Rule: configv1alpha1.Rule{ APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Kinds: []string{"Deployment"}, }, }, }, }, gvk: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", }, operation: configv1alpha1.InterpreterOperationInterpretReplica, want: false, }, { name: "no kind match", hook: &mockWebhookAccessor{ rules: []configv1alpha1.RuleWithOperations{ { Operations: []configv1alpha1.InterpreterOperation{ configv1alpha1.InterpreterOperationInterpretReplica, }, Rule: configv1alpha1.Rule{ APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Kinds: []string{"StatefulSet"}, }, }, }, }, gvk: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", }, operation: configv1alpha1.InterpreterOperationInterpretReplica, want: false, }, { name: "multiple rules - one matches", hook: &mockWebhookAccessor{ rules: []configv1alpha1.RuleWithOperations{ { Operations: []configv1alpha1.InterpreterOperation{ configv1alpha1.InterpreterOperationInterpretHealth, }, Rule: configv1alpha1.Rule{ APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Kinds: []string{"StatefulSet"}, }, }, { Operations: []configv1alpha1.InterpreterOperation{ configv1alpha1.InterpreterOperationInterpretReplica, }, Rule: configv1alpha1.Rule{ APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Kinds: []string{"Deployment"}, }, }, }, }, gvk: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", }, operation: configv1alpha1.InterpreterOperationInterpretReplica, want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := shouldCallHook(tt.hook, tt.gvk, tt.operation) assert.Equal(t, tt.want, got) }) } } func TestApplyPatch(t *testing.T) { defaultObject := &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{ "name": "test", }, }, } tests := []struct { name string object *unstructured.Unstructured patch []byte patchType configv1alpha1.PatchType wantErr bool validate func(*testing.T, *unstructured.Unstructured) }{ { name: "empty patch and empty patch type", object: defaultObject.DeepCopy(), patch: []byte{}, patchType: "", wantErr: false, validate: func(t *testing.T, got *unstructured.Unstructured) { assert.Equal(t, "test", got.GetName()) }, }, { name: "empty patch with JSONPatch type", object: defaultObject.DeepCopy(), patch: []byte{}, patchType: configv1alpha1.PatchTypeJSONPatch, wantErr: false, validate: func(t *testing.T, got *unstructured.Unstructured) { assert.Equal(t, "test", got.GetName()) }, }, { name: "empty JSON patch array", object: defaultObject.DeepCopy(), patch: []byte(`[]`), patchType: configv1alpha1.PatchTypeJSONPatch, wantErr: false, validate: func(t *testing.T, got *unstructured.Unstructured) { assert.Equal(t, "test", got.GetName()) }, }, { name: "valid JSON patch - replace", object: defaultObject.DeepCopy(), patch: []byte(`[{"op": "replace", "path": "/metadata/name", "value": "test-patched"}]`), patchType: configv1alpha1.PatchTypeJSONPatch, wantErr: false, validate: func(t *testing.T, got *unstructured.Unstructured) { assert.Equal(t, "test-patched", got.GetName()) }, }, { name: "valid JSON patch - add", object: defaultObject.DeepCopy(), patch: []byte(`[{"op": "add", "path": "/metadata/annotations", "value": {"key": "value"}}]`), patchType: configv1alpha1.PatchTypeJSONPatch, wantErr: false, validate: func(t *testing.T, got *unstructured.Unstructured) { annotations := got.GetAnnotations() assert.Equal(t, "value", annotations["key"]) }, }, { name: "invalid JSON patch format", object: defaultObject.DeepCopy(), patch: []byte(`invalid json patch`), patchType: configv1alpha1.PatchTypeJSONPatch, wantErr: true, }, { name: "invalid JSON patch operation", object: defaultObject.DeepCopy(), patch: []byte(`[{"op": "invalid", "path": "/metadata/name", "value": "test"}]`), patchType: configv1alpha1.PatchTypeJSONPatch, wantErr: true, }, { name: "unsupported patch type", object: defaultObject.DeepCopy(), patch: []byte(`{}`), patchType: "UnsupportedType", wantErr: true, }, { name: "invalid object for marshaling", object: &unstructured.Unstructured{Object: map[string]interface{}{"invalid": make(chan int)}}, patch: []byte(`[{"op": "replace", "path": "/metadata/name", "value": "test"}]`), patchType: configv1alpha1.PatchTypeJSONPatch, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := applyPatch(tt.object, tt.patch, tt.patchType) if tt.wantErr { assert.Error(t, err) return } assert.NoError(t, err) if tt.validate != nil { tt.validate(t, got) } }) } } func TestGetDependencies(t *testing.T) { tests := []struct { name string hasSynced bool hooks []configmanager.WebhookAccessor attributes *request.Attributes wantDeps []configv1alpha1.DependentObjectReference wantMatched bool wantErr bool errorContains string }{ { name: "not synced", hasSynced: false, attributes: &request.Attributes{ Object: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{ "name": "test-deployment", "namespace": "test-ns", }, }, }, Operation: configv1alpha1.InterpreterOperationInterpretDependency, }, wantErr: true, errorContains: "not yet ready to handle request", }, { name: "no matching hooks", hasSynced: true, hooks: []configmanager.WebhookAccessor{}, attributes: &request.Attributes{ Object: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{ "name": "test-deployment", "namespace": "test-ns", }, }, }, Operation: configv1alpha1.InterpreterOperationInterpretDependency, }, wantMatched: false, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { interpreter := &CustomizedInterpreter{ hookManager: &mockConfigManager{ hasSynced: tt.hasSynced, hooks: tt.hooks, }, } deps, matched, err := interpreter.GetDependencies(context.Background(), tt.attributes) if tt.wantErr { assert.Error(t, err) if tt.errorContains != "" { assert.Contains(t, err.Error(), tt.errorContains) } return } assert.NoError(t, err) assert.Equal(t, tt.wantMatched, matched) if matched { assert.Equal(t, tt.wantDeps, deps) } }) } } func TestReflectStatus(t *testing.T) { tests := []struct { name string hasSynced bool hooks []configmanager.WebhookAccessor attributes *request.Attributes wantStatus *runtime.RawExtension wantMatched bool wantErr bool errorContains string }{ { name: "not synced", hasSynced: false, attributes: &request.Attributes{ Object: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{ "name": "test-deployment", "namespace": "test-ns", }, }, }, Operation: configv1alpha1.InterpreterOperationAggregateStatus, }, wantErr: true, errorContains: "not yet ready to handle request", }, { name: "no matching hooks", hasSynced: true, hooks: []configmanager.WebhookAccessor{}, attributes: &request.Attributes{ Object: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{ "name": "test-deployment", "namespace": "test-ns", }, }, }, Operation: configv1alpha1.InterpreterOperationAggregateStatus, }, wantMatched: false, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { interpreter := &CustomizedInterpreter{ hookManager: &mockConfigManager{ hasSynced: tt.hasSynced, hooks: tt.hooks, }, } status, matched, err := interpreter.ReflectStatus(context.Background(), tt.attributes) if tt.wantErr { assert.Error(t, err) if tt.errorContains != "" { assert.Contains(t, err.Error(), tt.errorContains) } return } assert.NoError(t, err) assert.Equal(t, tt.wantMatched, matched) if matched { assert.Equal(t, tt.wantStatus, status) } }) } } func TestInterpretHealth(t *testing.T) { tests := []struct { name string hasSynced bool hooks []configmanager.WebhookAccessor attributes *request.Attributes wantHealthy bool wantMatched bool wantErr bool errorContains string }{ { name: "not synced", hasSynced: false, attributes: &request.Attributes{ Object: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{ "name": "test-deployment", "namespace": "test-ns", }, }, }, Operation: configv1alpha1.InterpreterOperationInterpretHealth, }, wantErr: true, errorContains: "not yet ready to handle request", }, { name: "no matching hooks", hasSynced: true, hooks: []configmanager.WebhookAccessor{}, attributes: &request.Attributes{ Object: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{ "name": "test-deployment", "namespace": "test-ns", }, }, }, Operation: configv1alpha1.InterpreterOperationInterpretHealth, }, wantMatched: false, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { interpreter := &CustomizedInterpreter{ hookManager: &mockConfigManager{ hasSynced: tt.hasSynced, hooks: tt.hooks, }, } healthy, matched, err := interpreter.InterpretHealth(context.Background(), tt.attributes) if tt.wantErr { assert.Error(t, err) if tt.errorContains != "" { assert.Contains(t, err.Error(), tt.errorContains) } return } assert.NoError(t, err) assert.Equal(t, tt.wantMatched, matched) if matched { assert.Equal(t, tt.wantHealthy, healthy) } }) } } // Mock Implementations // mockConfigManager implements configmanager.ConfigManager interface for testing type mockConfigManager struct { hasSynced bool hooks []configmanager.WebhookAccessor } func (m *mockConfigManager) HasSynced() bool { return m.hasSynced } func (m *mockConfigManager) HookAccessors() []configmanager.WebhookAccessor { return m.hooks } // mockWebhookAccessor implements configmanager.WebhookAccessor interface for testing type mockWebhookAccessor struct { uid string name string configName string rules []configv1alpha1.RuleWithOperations timeoutSeconds *int32 contextVersions []string } func (m *mockWebhookAccessor) GetUID() string { return m.uid } func (m *mockWebhookAccessor) GetName() string { return m.name } func (m *mockWebhookAccessor) GetConfigurationName() string { return m.configName } func (m *mockWebhookAccessor) GetRules() []configv1alpha1.RuleWithOperations { return m.rules } func (m *mockWebhookAccessor) GetTimeoutSeconds() *int32 { return m.timeoutSeconds } func (m *mockWebhookAccessor) GetInterpreterContextVersions() []string { return m.contextVersions } func (m *mockWebhookAccessor) GetClientConfig() admissionregistrationv1.WebhookClientConfig { return admissionregistrationv1.WebhookClientConfig{ URL: ptr.To("https://test-webhook"), } } func (m *mockWebhookAccessor) GetRESTClient(_ *webhookutil.ClientManager) (*rest.RESTClient, error) { return nil, nil }